From fd791a77dd95947d07243f0cd573d5512db631f8 Mon Sep 17 00:00:00 2001 From: Andras Sarro Date: Fri, 13 Sep 2024 15:45:14 +0200 Subject: [PATCH 1/5] refactor(navbar): rename routes SUITEDEV-36595 Co-authored-by: davidSchuppa <32750715+davidSchuppa@users.noreply.github.com> Co-authored-by: megamegax Co-authored-by: matusekma <36794575+matusekma@users.noreply.github.com> --- .../sample/main/navigation/BottomNavigationBar.kt | 13 ++++++------- .../sample/main/navigation/NavigationBarItem.kt | 11 +++++++++++ .../main/navigation/NavigationControllerProvider.kt | 12 ++++++------ .../sample/main/navigation/NavigationbarItem.kt | 11 ----------- 4 files changed, 23 insertions(+), 24 deletions(-) create mode 100644 sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationBarItem.kt delete mode 100644 sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationbarItem.kt diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/navigation/BottomNavigationBar.kt b/sample/src/main/kotlin/com/emarsys/sample/main/navigation/BottomNavigationBar.kt index 403a47f9..b552efbf 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/main/navigation/BottomNavigationBar.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/main/navigation/BottomNavigationBar.kt @@ -1,4 +1,4 @@ -package com.emarsys.sample.ui.component.navbar +package com.emarsys.sample.main.navigation import androidx.compose.material.BottomNavigation @@ -12,17 +12,16 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.currentBackStackEntryAsState -import com.emarsys.sample.main.navigation.NavigationbarItem import com.emarsys.sample.ui.style.rowWithMaxWidth @Composable fun BottomNavigationBar(navController: NavController) { val navItems = listOf( - NavigationbarItem.BottomDashBoard, - NavigationbarItem.BottomMobileEngage, - NavigationbarItem.BottomInbox, - NavigationbarItem.BottomPredict, - NavigationbarItem.BottomInApp + NavigationBarItem.BottomDashBoard, + NavigationBarItem.BottomMobileEngage, + NavigationBarItem.BottomInbox, + NavigationBarItem.BottomPredict, + NavigationBarItem.BottomInApp ) val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationBarItem.kt b/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationBarItem.kt new file mode 100644 index 00000000..f7e4761a --- /dev/null +++ b/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationBarItem.kt @@ -0,0 +1,11 @@ +package com.emarsys.sample.main.navigation + +import com.emarsys.sample.R + +sealed class NavigationBarItem(var route: String, var icon: Int, var title: String) { + data object BottomDashBoard : NavigationBarItem("dashboard", R.drawable.ic_settings, "Dashboard") + data object BottomMobileEngage : NavigationBarItem("mobile-engage", R.drawable.mobile_engage_logo_icon, "Mobile Engage") + data object BottomInbox : NavigationBarItem("inbox", R.drawable.inbox_mailbox_icon, "Inbox") + data object BottomPredict : NavigationBarItem("predict", R.drawable.predict_scarab_icon, "Predict") + data object BottomInApp : NavigationBarItem("inapp", R.drawable.mobile_engage_logo_icon, "InApp") +} \ No newline at end of file diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationControllerProvider.kt b/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationControllerProvider.kt index b6362672..dce5521f 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationControllerProvider.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationControllerProvider.kt @@ -12,12 +12,12 @@ class NavigationControllerProvider(private val mainViewModel: MainViewModel) { @Composable fun provide(): NavHostController { val navHostController = rememberNavController() - NavHost(navHostController, startDestination = "bottom-dashboard") { - composable("bottom-dashboard") { mainViewModel.detailScreen.value = mainViewModel.dashBoardScreen } - composable("bottom-mobile-engage") { mainViewModel.detailScreen.value = mainViewModel.mobileEngageScreen } - composable("bottom-inbox") { mainViewModel.detailScreen.value = mainViewModel.inboxScreen } - composable("bottom-predict") { mainViewModel.detailScreen.value = mainViewModel.predictScreen } - composable("bottom-inapp") { mainViewModel.detailScreen.value = mainViewModel.inAppScreen } + NavHost(navHostController, startDestination = "dashboard") { + composable("dashboard") { mainViewModel.detailScreen.value = mainViewModel.dashBoardScreen } + composable("mobile-engage") { mainViewModel.detailScreen.value = mainViewModel.mobileEngageScreen } + composable("inbox") { mainViewModel.detailScreen.value = mainViewModel.inboxScreen } + composable("predict") { mainViewModel.detailScreen.value = mainViewModel.predictScreen } + composable("inapp") { mainViewModel.detailScreen.value = mainViewModel.inAppScreen } } return navHostController } diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationbarItem.kt b/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationbarItem.kt deleted file mode 100644 index 62ef675a..00000000 --- a/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationbarItem.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.emarsys.sample.main.navigation - -import com.emarsys.sample.R - -sealed class NavigationbarItem(var route: String, var icon: Int, var title: String) { - object BottomDashBoard : NavigationbarItem("bottom-dashboard", R.drawable.ic_settings, "Dashboard") - object BottomMobileEngage : NavigationbarItem("bottom-mobile-engage", R.drawable.mobile_engage_logo_icon, "Mobile Engage") - object BottomInbox : NavigationbarItem("bottom-inbox", R.drawable.inbox_mailbox_icon, "Inbox") - object BottomPredict : NavigationbarItem("bottom-predict", R.drawable.predict_scarab_icon, "Predict") - object BottomInApp : NavigationbarItem("bottom-inapp", R.drawable.mobile_engage_logo_icon, "InApp") -} \ No newline at end of file From b926e48dcbbc537fa06952362bc70dc7dcf44035 Mon Sep 17 00:00:00 2001 From: Andras Sarro Date: Fri, 13 Sep 2024 15:53:53 +0200 Subject: [PATCH 2/5] refactor(prefs): reorganize state management SUITEDEV-36367 Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> Co-authored-by: megamegax --- .../com/emarsys/sample/SampleApplication.kt | 7 +- .../sample/dashboard/DashboardScreen.kt | 84 ++++------ .../sample/dashboard/DashboardViewModel.kt | 42 +++-- .../com/emarsys/sample/inapp/InAppScreen.kt | 6 +- .../com/emarsys/sample/inbox/InboxScreen.kt | 13 +- .../emarsys/sample/inbox/MessagePresenter.kt | 10 +- .../com/emarsys/sample/main/MainActivity.kt | 14 +- .../com/emarsys/sample/main/MainViewModel.kt | 19 ++- .../emarsys/sample/main/sdkinfo/SetupInfo.kt | 42 ----- .../sample/main/sdkinfo/TopCardViewModel.kt | 8 +- .../sample/main/sdkinfo/TopExpandableCard.kt | 157 ++++++++++-------- .../sample/mobileengage/MobileEngageScreen.kt | 8 +- .../emarsys/sample/predict/PredictScreen.kt | 12 +- .../sample/predict/RecommendedProductsCard.kt | 2 +- .../sample/predict/cart/SampleCartItemCard.kt | 2 +- .../kotlin/com/emarsys/sample/pref/Prefs.kt | 26 ++- .../main/kotlin/com/emarsys/sample/ui/Card.kt | 2 +- .../ui/component/screen/DetailScreen.kt | 5 +- 18 files changed, 205 insertions(+), 254 deletions(-) delete mode 100644 sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/SetupInfo.kt diff --git a/sample/src/main/kotlin/com/emarsys/sample/SampleApplication.kt b/sample/src/main/kotlin/com/emarsys/sample/SampleApplication.kt index 9b9694c6..48a6078b 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/SampleApplication.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/SampleApplication.kt @@ -28,7 +28,7 @@ class SampleApplication : Application(), EventHandler, NotificationInformationLi val config = EmarsysConfig( application = this, - applicationCode = if (Prefs.applicationCode.isEmpty()) null else Prefs.applicationCode, + applicationCode = Prefs.applicationCode.ifEmpty { null }, merchantId = Prefs.merchantId, verboseConsoleLoggingEnabled = true ) @@ -37,7 +37,10 @@ class SampleApplication : Application(), EventHandler, NotificationInformationLi Emarsys.setup(config) createNotificationChannels() setupEventHandlers() + setContactIfPresent(context) + } + private fun setContactIfPresent(context: Context) { if (Prefs.contactFieldId != 0 && Prefs.contactFieldValue.isNotEmpty()) { Emarsys.setContact( contactFieldValue = Prefs.contactFieldValue, @@ -51,7 +54,7 @@ class SampleApplication : Application(), EventHandler, NotificationInformationLi } } - fun setupEventHandlers() { + private fun setupEventHandlers() { if (Prefs.applicationCode.isNotEmpty()) { Emarsys.push.setNotificationEventHandler(this) Emarsys.push.setSilentMessageEventHandler(this) diff --git a/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardScreen.kt b/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardScreen.kt index 3e8fd544..05e5d181 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardScreen.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardScreen.kt @@ -1,6 +1,5 @@ package com.emarsys.sample.dashboard -import android.app.Application import android.content.Context import android.os.Build import android.util.Log @@ -22,12 +21,10 @@ import androidx.compose.ui.res.stringResource import coil.annotation.ExperimentalCoilApi import com.emarsys.Emarsys import com.emarsys.sample.R -import com.emarsys.sample.SampleApplication import com.emarsys.sample.dashboard.button.CheckLocationPermissionsButton import com.emarsys.sample.dashboard.button.CopyPushTokenButton import com.emarsys.sample.dashboard.button.TrackPushTokenButton import com.emarsys.sample.dashboard.button.trackPushToken -import com.emarsys.sample.main.sdkinfo.SetupInfo import com.emarsys.sample.ui.component.ColumnWithTapGesture import com.emarsys.sample.ui.component.button.GoogleSignInButton import com.emarsys.sample.ui.component.button.StyledTextButton @@ -46,9 +43,8 @@ import kotlin.system.exitProcess class DashboardScreen( override val context: Context, - override val application: Application + private val viewModel: DashboardViewModel ) : DetailScreen() { - private val viewModel = DashboardViewModel() @OptIn(ExperimentalMaterialApi::class) @ExperimentalCoilApi @@ -104,8 +100,6 @@ class DashboardScreen( Emarsys.config.changeMerchantId( merchantId = viewModel.getTfMerchantIdValue(), ) - SetupInfo.merchantId = viewModel.getTfMerchantIdValue() - SetupInfo.notifyObservers() customTextToast( context, context.getString(R.string.merchant_id_change_success) @@ -130,7 +124,7 @@ class DashboardScreen( Text(text = stringResource(id = R.string.enable)) Switch( checked = viewModel.isGeofenceEnabled(), - onCheckedChange = { enabled -> + onCheckedChange = { isEnabled -> if (!viewModel.isGeofenceEnabled()) { Emarsys.geofence.enable { if (it != null) { @@ -140,7 +134,7 @@ class DashboardScreen( context.getString(R.string.something_went_wrong) ) } else { - viewModel.geofenceEnabled.value = enabled + viewModel.geofenceEnabled.value = isEnabled customTextToast( context, context.getString(R.string.geofence_enabled) @@ -149,7 +143,7 @@ class DashboardScreen( } } else { Emarsys.geofence.disable() - viewModel.geofenceEnabled.value = enabled + viewModel.geofenceEnabled.value = isEnabled customTextToast(context, context.getString(R.string.geofence_disabled)) } } @@ -208,7 +202,7 @@ class DashboardScreen( account.email!! ) { if (verifyLogin(it, context)) { - setupInfoSetContact(context) + setContact(context) } } } @@ -216,42 +210,28 @@ class DashboardScreen( private fun onChangeAppCodeClicked(somethingWentWrong: String, appCodeChangeSuccess: String) { Emarsys.config.changeApplicationCode( - applicationCode = viewModel.getTfAppCodeValue(), - completionListener = { changeError -> - if (viewModel.shouldChangeEnv()) { - if (changeError != null) { - customTextToast(context, somethingWentWrong) - } - exitProcess(0) - } else if (changeError != null) { - customTextToast(context, somethingWentWrong) - } else { - if (viewModel.hasLogin()) { - setupInfoClearContact() - customTextToast(context, appCodeChangeSuccess) - } else { - (application as SampleApplication).setupEventHandlers() - customTextToast(context, appCodeChangeSuccess) - } - viewModel.resetContactInfo() - } + applicationCode = viewModel.getTfAppCodeValue() + ) { changeError -> + if (changeError != null) { + customTextToast(context, somethingWentWrong) + } else { + viewModel.clearContact() + customTextToast(context, appCodeChangeSuccess) } - ) - SetupInfo.applicationCode = viewModel.getTfAppCodeValue() - SetupInfo.notifyObservers() + if (viewModel.shouldChangeEnv()) { + exitProcess(0) + } + } } private fun onLoginClicked() { - if (!viewModel.hasLogin()) { - if (viewModel.isContactDataPresent() + if (!viewModel.hasLogin() && viewModel.isContactDataPresent()) { + Emarsys.setContact( + contactFieldId = viewModel.getTfContactFieldIdValue().toInt(), + contactFieldValue = viewModel.getTfContactFieldValue() ) { - Emarsys.setContact( - contactFieldId = viewModel.getTfContactFieldIdValue().toInt(), - contactFieldValue = viewModel.getTfContactFieldValue() - ) { - if (verifyLogin(it, context)) { - setupInfoSetContact(context) - } + if (verifyLogin(it, context)) { + setContact(context) } } } else { @@ -259,8 +239,7 @@ class DashboardScreen( if (it != null) { customTextToast(context, context.getString(R.string.log_out_fail)) } else { - setupInfoClearContact() - viewModel.resetContactInfo() + viewModel.clearContact() customTextToast(context, context.getString(R.string.log_out_success)) } } @@ -281,18 +260,11 @@ class DashboardScreen( } } - private fun setupInfoClearContact() { - SetupInfo.contactFieldId = 0.toString() - SetupInfo.contactFieldValue = "" - SetupInfo.loggedIn = false - SetupInfo.notifyObservers() - } - - private fun setupInfoSetContact(context: Context) { - SetupInfo.contactFieldValue = viewModel.getTfContactFieldValue() - SetupInfo.contactFieldId = viewModel.getTfContactFieldIdValue() - SetupInfo.loggedIn = true - SetupInfo.notifyObservers() + private fun setContact(context: Context) { + viewModel.setContact( + fieldId = viewModel.getTfContactFieldIdValue(), + fieldValue = viewModel.getTfContactFieldValue() + ) viewModel.isLoggedIn.value = true trackPushToken(context) } diff --git a/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardViewModel.kt b/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardViewModel.kt index 63e764fb..5c74c734 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardViewModel.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/dashboard/DashboardViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.emarsys.Emarsys import com.emarsys.sample.pref.Prefs +import com.emarsys.sample.pref.update class DashboardViewModel : ViewModel() { @@ -30,18 +31,10 @@ class DashboardViewModel : ViewModel() { return this.tfContactFieldId.value } - fun setTfContactFieldId(value: String) { - this.tfContactFieldId.value = value - } - fun getTfContactFieldValue(): String { return this.tfContactFieldValue.value } - fun setTfContactField(value: String) { - this.tfContactFieldValue.value = value - } - fun hasLogin(): Boolean { return this.isLoggedIn.value } @@ -66,12 +59,6 @@ class DashboardViewModel : ViewModel() { return this.geofenceEnabled.value } - fun resetContactInfo() { - this.setTfContactField("") - this.setTfContactFieldId("0") - this.isLoggedIn.value = false - } - fun isContactDataPresent(): Boolean { return if ( this.getTfContactFieldValue().isNotEmpty() && @@ -97,4 +84,31 @@ class DashboardViewModel : ViewModel() { fun isErrorVisible(): Boolean { return this.errorVisible.value } + + fun getInfoMap(): Map { + return mapOf( + "loggedIn" to isLoggedIn.value.toString(), + "applicationCode" to tfAppCode.value, + "merchantId" to tfMerchantId.value, + "hardwareId" to Prefs.hardwareId, + "languageCode" to Prefs.languageCode, + "sdkVersion" to Prefs.sdkVersion, + "contactFieldValue" to tfContactFieldValue.value, + "contactFieldId" to tfContactFieldId.value + ) + } + + fun clearContact() { + tfContactFieldId.value = "0" + tfContactFieldValue.value = "" + isLoggedIn.value = false + Prefs.update(getInfoMap()) + } + + fun setContact(fieldId: String, fieldValue: String) { + tfContactFieldId.value = fieldId + tfContactFieldValue.value = fieldValue + isLoggedIn.value = true + Prefs.update(getInfoMap()) + } } \ No newline at end of file diff --git a/sample/src/main/kotlin/com/emarsys/sample/inapp/InAppScreen.kt b/sample/src/main/kotlin/com/emarsys/sample/inapp/InAppScreen.kt index 04dcf072..7e4704f0 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/inapp/InAppScreen.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/inapp/InAppScreen.kt @@ -1,11 +1,9 @@ package com.emarsys.sample.inapp -import android.app.Application import android.content.Context import android.util.Log import android.view.View import android.widget.Toast -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -46,8 +44,7 @@ import com.emarsys.sample.ui.style.rowWithPointEightWidth import java.util.UUID class InAppScreen( - override val context: Context, - override val application: Application + override val context: Context ) : DetailScreen() { private val trackCustomEvent = { if (viewModel.isEventPresent()) { @@ -71,7 +68,6 @@ class InAppScreen( private val viewModel = InAppViewModel() - @OptIn(ExperimentalAnimationApi::class) @ExperimentalCoilApi @ExperimentalComposeUiApi @Composable diff --git a/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxScreen.kt b/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxScreen.kt index 3c1dce4b..23375ba8 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxScreen.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxScreen.kt @@ -1,6 +1,5 @@ package com.emarsys.sample.inbox -import android.app.Application import android.content.Context import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.PaddingValues @@ -10,7 +9,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -26,12 +30,11 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import kotlinx.coroutines.delay class InboxScreen( - override val context: Context, - override val application: Application + override val context: Context ) : DetailScreen() { private val viewModel = InboxViewModel() - private val messagePresenter = MessagePresenter(context, viewModel) + private val messagePresenter = MessagePresenter(context) @ExperimentalCoilApi @ExperimentalComposeUiApi diff --git a/sample/src/main/kotlin/com/emarsys/sample/inbox/MessagePresenter.kt b/sample/src/main/kotlin/com/emarsys/sample/inbox/MessagePresenter.kt index 8597995c..483323c0 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/inbox/MessagePresenter.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/inbox/MessagePresenter.kt @@ -6,7 +6,11 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.material.Card -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -18,6 +22,7 @@ import com.emarsys.mobileengage.api.inbox.Message import com.emarsys.sample.R import com.emarsys.sample.inbox.component.InboxButton import com.emarsys.sample.inbox.event.InboxAppEventHandler +import com.emarsys.sample.ui.cardWithFullWidth import com.emarsys.sample.ui.component.button.StyledTextButton import com.emarsys.sample.ui.component.divider.DividerWithBackgroundColor import com.emarsys.sample.ui.component.row.RowWithCenteredContent @@ -26,11 +31,10 @@ import com.emarsys.sample.ui.component.text.TextWithFullWidthLine import com.emarsys.sample.ui.component.text.TitleText import com.emarsys.sample.ui.component.textfield.StyledTextField import com.emarsys.sample.ui.component.toast.customTextToast -import com.emarsys.sample.ui.style.cardWithFullWidth import com.emarsys.sample.ui.style.columnWithMaxWidth import com.emarsys.sample.ui.style.rowWithMaxWidth -class MessagePresenter(private val context: Context, private val viewModel: InboxViewModel) { +class MessagePresenter(private val context: Context) { private val inboxAppEventHandler = InboxAppEventHandler() diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt b/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt index 5407e684..bfb12efc 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt @@ -1,17 +1,18 @@ package com.emarsys.sample.main -import android.app.Application -import android.content.Context import android.os.Build import android.os.Bundle import androidx.activity.compose.setContent import androidx.annotation.RequiresApi import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.imePadding import androidx.compose.material.Scaffold +import androidx.compose.ui.Modifier import androidx.fragment.app.FragmentActivity +import com.emarsys.sample.dashboard.DashboardViewModel +import com.emarsys.sample.main.navigation.BottomNavigationBar import com.emarsys.sample.main.navigation.NavigationControllerProvider import com.emarsys.sample.main.sdkinfo.TopExpandableCard -import com.emarsys.sample.ui.component.navbar.BottomNavigationBar import com.emarsys.sample.ui.theme.AndroidSampleAppTheme class MainActivity : FragmentActivity() { @@ -23,9 +24,8 @@ class MainActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val application: Application = this.application - val context: Context = this.applicationContext - val mainViewModel = MainViewModel(context, application) + val dashboardViewModel = DashboardViewModel() + val mainViewModel = MainViewModel(this.applicationContext, dashboardViewModel) val navControllerProvider = NavigationControllerProvider(mainViewModel) setContent { @@ -33,7 +33,7 @@ class MainActivity : FragmentActivity() { AndroidSampleAppTheme { Scaffold( topBar = { - TopExpandableCard().TopExpandableCard(context) + TopExpandableCard(this.applicationContext, dashboardViewModel) }, bottomBar = { BottomNavigationBar(navController = navHostController) diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/MainViewModel.kt b/sample/src/main/kotlin/com/emarsys/sample/main/MainViewModel.kt index fdabef0f..a1b95513 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/main/MainViewModel.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/main/MainViewModel.kt @@ -1,22 +1,25 @@ package com.emarsys.sample.main -import android.app.Application import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import com.emarsys.sample.ui.component.screen.DetailScreen import com.emarsys.sample.dashboard.DashboardScreen +import com.emarsys.sample.dashboard.DashboardViewModel import com.emarsys.sample.inapp.InAppScreen import com.emarsys.sample.inbox.InboxScreen import com.emarsys.sample.mobileengage.MobileEngageScreen import com.emarsys.sample.predict.PredictScreen +import com.emarsys.sample.ui.component.screen.DetailScreen -class MainViewModel(context: Context, application: Application) : ViewModel() { - val dashBoardScreen = DashboardScreen(context, application) - val mobileEngageScreen = MobileEngageScreen(context, application) - val inboxScreen = InboxScreen(context, application) - val predictScreen = PredictScreen(context, application) - val inAppScreen = InAppScreen(context, application) +class MainViewModel( + context: Context, + dashboardViewModel: DashboardViewModel +) : ViewModel() { + val dashBoardScreen = DashboardScreen(context, dashboardViewModel) + val mobileEngageScreen = MobileEngageScreen(context) + val inboxScreen = InboxScreen(context) + val predictScreen = PredictScreen(context) + val inAppScreen = InAppScreen(context) val detailScreen = mutableStateOf( dashBoardScreen diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/SetupInfo.kt b/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/SetupInfo.kt deleted file mode 100644 index d73f6b83..00000000 --- a/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/SetupInfo.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.emarsys.sample.main.sdkinfo - -import androidx.compose.runtime.mutableStateMapOf -import com.emarsys.Emarsys -import com.emarsys.sample.pref.Prefs -import java.util.* - -object SetupInfo : Observable() { - var loggedIn: Boolean = Prefs.loggedIn - var hardwareId: String = Emarsys.config.hardwareId - var languageCode: String = Emarsys.config.languageCode - var sdkVersion: String = Emarsys.config.sdkVersion - var contactFieldValue: String = Prefs.contactFieldValue - var contactFieldId: String = Prefs.contactFieldId.toString() - var applicationCode: String = Prefs.applicationCode - var merchantId: String = Prefs.merchantId - - private val observers = listOf(TopCardViewModel, Prefs) - - override fun addObserver(o: Observer?) { - super.addObserver(o) - } - - override fun notifyObservers() { - observers.forEach { observer -> - observer.update(this, provideInfoMap()) - } - } - - fun provideInfoMap(): MutableMap { - return mutableStateMapOf( - "LoggedIn" to loggedIn.toString(), - "ApplicationCode" to applicationCode, - "MerchantId" to merchantId, - "HardwareId" to hardwareId, - "LanguageCode" to languageCode, - "SdkVersion" to sdkVersion, - "ContactFieldValue" to contactFieldValue, - "ContactFieldId" to contactFieldId - ) - } -} \ No newline at end of file diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/TopCardViewModel.kt b/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/TopCardViewModel.kt index ff5ed509..efa9c00e 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/TopCardViewModel.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/TopCardViewModel.kt @@ -2,10 +2,8 @@ package com.emarsys.sample.main.sdkinfo import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import java.util.* -object TopCardViewModel : ViewModel(), Observer { - val setupInfo = mutableStateOf(SetupInfo.provideInfoMap()) +object TopCardViewModel : ViewModel() { val expanded = mutableStateOf(false) fun getMoreLessText(): String { @@ -15,8 +13,4 @@ object TopCardViewModel : ViewModel(), Observer { fun toggleCardExpansion() { expanded.value = !expanded.value } - - override fun update(o: Observable?, arg: Any?) { - setupInfo.value = arg as MutableMap - } } diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/TopExpandableCard.kt b/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/TopExpandableCard.kt index 8a1a0cfe..dc679227 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/TopExpandableCard.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/main/sdkinfo/TopExpandableCard.kt @@ -7,7 +7,11 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.Card import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -18,88 +22,97 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat -import com.emarsys.sample.ui.style.cardWithFullWidth -import com.emarsys.sample.ui.style.columnWithMaxWidth +import com.emarsys.sample.dashboard.DashboardViewModel +import com.emarsys.sample.ui.cardWithFullWidth import com.emarsys.sample.ui.component.toast.customTextToast +import com.emarsys.sample.ui.style.columnWithMaxWidth -class TopExpandableCard { - - @ExperimentalAnimationApi - @Composable - fun TopExpandableCard(context: Context) { - Card(modifier = Modifier - .clickable { - TopCardViewModel.toggleCardExpansion() - } - .cardWithFullWidth(), - elevation = 8.dp - ) { - AnimatedVisibility(visible = !TopCardViewModel.expanded.value) { - LoginStatus(itemsToList = TopCardViewModel.setupInfo.value) - MoreLessText(textToShow = TopCardViewModel.getMoreLessText()) - } - AnimatedVisibility(visible = TopCardViewModel.expanded.value) { - Column( - modifier = Modifier - .fillMaxWidth(1f) - .padding(start = 4.dp) - ) { - SettingsStatus(context, TopCardViewModel.setupInfo.value) - Row(horizontalArrangement = Arrangement.End) { - MoreLessText(textToShow = TopCardViewModel.getMoreLessText()) - } +@ExperimentalAnimationApi +@Composable +fun TopExpandableCard(context: Context, dashboardViewModel: DashboardViewModel) { + Card(modifier = Modifier + .clickable { + TopCardViewModel.toggleCardExpansion() + } + .cardWithFullWidth(), + elevation = 8.dp + ) { + AnimatedVisibility(visible = !TopCardViewModel.expanded.value) { + LoginStatus(dashboardViewModel.hasLogin()) + MoreLessText(textToShow = TopCardViewModel.getMoreLessText()) + } + AnimatedVisibility(visible = TopCardViewModel.expanded.value) { + Column( + modifier = Modifier + .fillMaxWidth(1f) + .padding(start = 4.dp) + ) { + SettingsStatus(context, dashboardViewModel.getInfoMap()) + Row(horizontalArrangement = Arrangement.End) { + MoreLessText(textToShow = TopCardViewModel.getMoreLessText()) } } } } +} - @Composable - private fun LoginStatus(itemsToList: MutableMap) { - Column( - modifier = Modifier.columnWithMaxWidth() - ) { - Row { Text(text = "Logged in: " + itemsToList["LoggedIn"].toString()) } +@Composable +private fun LoginStatus(isLoggedIn: Boolean) { + Column( + modifier = Modifier + .columnWithMaxWidth() + .padding(4.dp) + ) { + Row { + Text( + text = "Logged in: $isLoggedIn", + style = MaterialTheme.typography.body1 + ) } } +} - @Composable - private fun MoreLessText(textToShow: String) { - Text( - text = textToShow, - modifier = Modifier - .fillMaxWidth(1f), - textAlign = TextAlign.End - ) - } +@Composable +private fun MoreLessText(textToShow: String) { + Text( + text = textToShow, + style = MaterialTheme.typography.body1, + modifier = Modifier + .fillMaxWidth(1f) + .padding(4.dp), + textAlign = TextAlign.End + ) +} - @Composable - private fun SettingsStatus(context: Context, itemsToList: MutableMap) { - itemsToList.forEach { item -> - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly - ) { - Text(text = "${item.key}: ") - Text( - text = item.value, - style = MaterialTheme.typography.caption, - modifier = Modifier.pointerInput(Unit) { - detectTapGestures(onTap = { - copyToClipboard(context, item.value) - }) - } - ) - } +@Composable +private fun SettingsStatus(context: Context, itemsToList: Map) { + itemsToList.forEach { item -> + Row( + modifier = Modifier.padding(4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Text(text = "${item.key}: ", style = MaterialTheme.typography.body1) + Text( + text = item.value, + style = MaterialTheme.typography.body1, + modifier = Modifier.pointerInput(Unit) { + detectTapGestures(onTap = { + copyToClipboard(context, item.value) + }) + } + ) } } - private fun copyToClipboard(context: Context, copy: String) { - val myClipboard = - ContextCompat.getSystemService( - context, - ClipboardManager::class.java - ) as ClipboardManager - val clip = ClipData.newPlainText("copied text", copy) - myClipboard.setPrimaryClip(clip) - customTextToast(context, "Copied to clipboard, value: $copy") - } +} + +private fun copyToClipboard(context: Context, copy: String) { + val myClipboard = + ContextCompat.getSystemService( + context, + ClipboardManager::class.java + ) as ClipboardManager + val clip = ClipData.newPlainText("copied text", copy) + myClipboard.setPrimaryClip(clip) + customTextToast(context, "Copied to clipboard, value: $copy") } diff --git a/sample/src/main/kotlin/com/emarsys/sample/mobileengage/MobileEngageScreen.kt b/sample/src/main/kotlin/com/emarsys/sample/mobileengage/MobileEngageScreen.kt index eb454f62..6e48f8ed 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/mobileengage/MobileEngageScreen.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/mobileengage/MobileEngageScreen.kt @@ -1,6 +1,5 @@ package com.emarsys.sample.mobileengage -import android.app.Application import android.content.Context import android.util.Log import android.widget.Toast @@ -19,19 +18,18 @@ import coil.annotation.ExperimentalCoilApi import com.emarsys.Emarsys import com.emarsys.sample.R import com.emarsys.sample.ui.component.ColumnWithTapGesture -import com.emarsys.sample.ui.component.screen.DetailScreen import com.emarsys.sample.ui.component.button.StyledTextButton import com.emarsys.sample.ui.component.divider.DividerWithBackgroundColor import com.emarsys.sample.ui.component.divider.GreyLine +import com.emarsys.sample.ui.component.screen.DetailScreen import com.emarsys.sample.ui.component.text.TitleText import com.emarsys.sample.ui.component.textfield.EventPayloadTextArea import com.emarsys.sample.ui.component.textfield.StyledTextField -import com.emarsys.sample.ui.style.rowWithPointEightWidth import com.emarsys.sample.ui.component.toast.ErrorDialog +import com.emarsys.sample.ui.style.rowWithPointEightWidth class MobileEngageScreen( - override val context: Context, - override val application: Application + override val context: Context ) : DetailScreen() { private val viewModel = MobileEngageViewModel() private val trackCustomEvent = { diff --git a/sample/src/main/kotlin/com/emarsys/sample/predict/PredictScreen.kt b/sample/src/main/kotlin/com/emarsys/sample/predict/PredictScreen.kt index 475ad4ba..06150433 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/predict/PredictScreen.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/predict/PredictScreen.kt @@ -1,6 +1,5 @@ package com.emarsys.sample.predict -import android.app.Application import android.content.Context import android.util.Log import androidx.compose.foundation.gestures.detectTapGestures @@ -23,20 +22,19 @@ import androidx.compose.ui.res.stringResource import coil.annotation.ExperimentalCoilApi import com.emarsys.Emarsys import com.emarsys.sample.R -import com.emarsys.sample.ui.component.button.StyledTextButton import com.emarsys.sample.predict.cart.SampleCartItemCard +import com.emarsys.sample.ui.component.button.StyledTextButton import com.emarsys.sample.ui.component.divider.GreyLine +import com.emarsys.sample.ui.component.screen.DetailScreen import com.emarsys.sample.ui.component.text.TitleText +import com.emarsys.sample.ui.component.toast.ErrorDialog +import com.emarsys.sample.ui.component.toast.customTextToast import com.emarsys.sample.ui.style.columnWithMaxWidth import com.emarsys.sample.ui.style.rowWithMaxWidth import com.emarsys.sample.ui.style.rowWithPointEightWidth -import com.emarsys.sample.ui.component.screen.DetailScreen -import com.emarsys.sample.ui.component.toast.ErrorDialog -import com.emarsys.sample.ui.component.toast.customTextToast class PredictScreen( - override val context: Context, - override val application: Application + override val context: Context ) : DetailScreen() { private val viewModel = PredictViewModel() diff --git a/sample/src/main/kotlin/com/emarsys/sample/predict/RecommendedProductsCard.kt b/sample/src/main/kotlin/com/emarsys/sample/predict/RecommendedProductsCard.kt index 6edb0e78..f42ce25e 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/predict/RecommendedProductsCard.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/predict/RecommendedProductsCard.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.emarsys.predict.api.model.Product import com.emarsys.sample.R -import com.emarsys.sample.ui.style.cardWithPointEightWidth +import com.emarsys.sample.ui.cardWithPointEightWidth import com.emarsys.sample.ui.style.columnWithPointEightWidth import com.emarsys.sample.ui.style.rowWithPointEightWidth diff --git a/sample/src/main/kotlin/com/emarsys/sample/predict/cart/SampleCartItemCard.kt b/sample/src/main/kotlin/com/emarsys/sample/predict/cart/SampleCartItemCard.kt index 88f5d494..7f62ded8 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/predict/cart/SampleCartItemCard.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/predict/cart/SampleCartItemCard.kt @@ -8,7 +8,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.emarsys.sample.ui.style.cardWithPointEightWidth +import com.emarsys.sample.ui.cardWithPointEightWidth import com.emarsys.sample.ui.style.rowWithPointEightWidth @Composable diff --git a/sample/src/main/kotlin/com/emarsys/sample/pref/Prefs.kt b/sample/src/main/kotlin/com/emarsys/sample/pref/Prefs.kt index a7ada5d0..c1b4c765 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/pref/Prefs.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/pref/Prefs.kt @@ -1,9 +1,9 @@ package com.emarsys.sample.pref import com.chibatching.kotpref.KotprefModel -import java.util.* -object Prefs : KotprefModel(), Observer { +object Prefs : KotprefModel() { + override val commitAllPropertiesByDefault: Boolean = true var sdkVersion by stringPref("") var languageCode by stringPref("") @@ -13,17 +13,15 @@ object Prefs : KotprefModel(), Observer { var contactFieldValue by stringPref("") var contactFieldId by intPref(0) var loggedIn by booleanPref(false) +} - override fun update(o: Observable?, arg: Any?) { - val newSettings = arg as MutableMap - - loggedIn = newSettings["LoggedIn"]!!.toBoolean() - applicationCode = newSettings["ApplicationCode"]!! - merchantId = newSettings["MerchantId"]!! - hardwareId = newSettings["HardwareId"]!! - languageCode = newSettings["LanguageCode"]!! - sdkVersion = newSettings["SdkVersion"]!! - contactFieldValue = newSettings["ContactFieldValue"]!! - contactFieldId = newSettings["ContactFieldId"]!!.toInt() - } +fun Prefs.update(sdkInfo: Map) { + sdkVersion = sdkInfo.getOrDefault("sdkVersion", "") + languageCode = sdkInfo.getOrDefault("languageCode", "") + hardwareId = sdkInfo.getOrDefault("hardwareId", "") + applicationCode = sdkInfo.getOrDefault("applicationCode", "") + merchantId = sdkInfo.getOrDefault("merchantId", "") + contactFieldValue = sdkInfo.getOrDefault("contactFieldValue", "") + contactFieldId = sdkInfo.getOrDefault("contactFieldId", "0").toInt() + loggedIn = sdkInfo.getOrDefault("loggedIn", "false").toBoolean() } \ No newline at end of file diff --git a/sample/src/main/kotlin/com/emarsys/sample/ui/Card.kt b/sample/src/main/kotlin/com/emarsys/sample/ui/Card.kt index 48793736..09ad14ea 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/ui/Card.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/ui/Card.kt @@ -1,4 +1,4 @@ -package com.emarsys.sample.ui.style +package com.emarsys.sample.ui import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding diff --git a/sample/src/main/kotlin/com/emarsys/sample/ui/component/screen/DetailScreen.kt b/sample/src/main/kotlin/com/emarsys/sample/ui/component/screen/DetailScreen.kt index 9a4d8d53..3550abfd 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/ui/component/screen/DetailScreen.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/ui/component/screen/DetailScreen.kt @@ -1,6 +1,5 @@ package com.emarsys.sample.ui.component.screen -import android.app.Application import android.content.Context import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable @@ -10,11 +9,9 @@ import coil.annotation.ExperimentalCoilApi abstract class DetailScreen { abstract val context: Context - abstract val application: Application @ExperimentalCoilApi @ExperimentalComposeUiApi @Composable - open fun Detail(paddingValues: PaddingValues) { - } + abstract fun Detail(paddingValues: PaddingValues) } \ No newline at end of file From 14ad3ab3f9b9c67b7d11fa6ea4f9c429c1934463 Mon Sep 17 00:00:00 2001 From: Andras Sarro Date: Fri, 13 Sep 2024 15:54:57 +0200 Subject: [PATCH 3/5] fix(dashboard): fix input field positioning SUITEDEV-36367 Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> Co-authored-by: megamegax --- sample/src/main/AndroidManifest.xml | 3 ++- sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index a8cb8ddc..f993d2d9 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -26,7 +26,8 @@ + android:exported="true" + android:windowSoftInputMode="adjustResize"> diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt b/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt index bfb12efc..0f2c2f1e 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt @@ -31,7 +31,7 @@ class MainActivity : FragmentActivity() { setContent { val navHostController = navControllerProvider.provide() AndroidSampleAppTheme { - Scaffold( + Scaffold(modifier = Modifier.imePadding(), topBar = { TopExpandableCard(this.applicationContext, dashboardViewModel) }, From 84e54bf694b84ba0ee09dff65528784501869883 Mon Sep 17 00:00:00 2001 From: Andras Sarro Date: Fri, 13 Sep 2024 16:58:30 +0200 Subject: [PATCH 4/5] chore(inbox): remove deprecated component SUITEDEV-36595 Co-authored-by: davidSchuppa <32750715+davidSchuppa@users.noreply.github.com> Co-authored-by: megamegax Co-authored-by: matusekma <36794575+matusekma@users.noreply.github.com> --- gradle/libs.versions.toml | 2 - sample/build.gradle.kts | 1 - .../com/emarsys/sample/inbox/InboxScreen.kt | 100 ++++++++---------- .../emarsys/sample/inbox/InboxViewModel.kt | 7 +- .../com/emarsys/sample/main/MainActivity.kt | 10 +- .../com/emarsys/sample/main/MainViewModel.kt | 4 + .../NavigationControllerProvider.kt | 6 ++ 7 files changed, 65 insertions(+), 65 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 95669384..91eda299 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,7 +45,6 @@ hms_push = "6.12.0.300" googleServices = "4.4.2" google-play-services-location = "21.3.0" google-play-services-auth = "20.7.0" -google-accompanist-swipetorefresh = "0.34.0" webkit = "1.11.0" mockito-android = "5.10.0" mockito-core = "5.10.0" @@ -90,7 +89,6 @@ google-play-services-location = { module = "com.google.android.gms:play-services google-play-services-auth = { module = "androidx.core:core-ktx", version.ref = "google-play-services-auth" } google-firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "fcm" } google-firebase-common = { group = "com.google.firebase", name = "firebase-common", version.ref = "firebase-common" } -google-accompanist-swipetorefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "google-accompanist-swipetorefresh" } google-gson = { module = "com.google.code.gson:gson", version.ref = "google-gson" } huawei-hms-push = { group = "com.huawei.hms", name = "push", version.ref = "hms_push" } huawei-agconnect-core = { group = "com.huawei.agconnect", name = "agconnect-core", version.ref = "agconnect_core" } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index d139db24..d79167cc 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -149,7 +149,6 @@ dependencies { implementation(libs.google.firebase.common) implementation(libs.google.firebase.messaging) implementation(libs.play.services.auth) - implementation(libs.google.accompanist.swipetorefresh) implementation(libs.google.gson) debugImplementation(libs.androidx.compose.ui.tooling) diff --git a/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxScreen.kt b/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxScreen.kt index 23375ba8..a6193313 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxScreen.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxScreen.kt @@ -2,19 +2,17 @@ package com.emarsys.sample.inbox import android.content.Context import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Scaffold +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -24,71 +22,59 @@ import com.emarsys.sample.R import com.emarsys.sample.ui.component.row.RowWithCenteredContent import com.emarsys.sample.ui.component.screen.DetailScreen import com.emarsys.sample.ui.component.text.TitleText -import com.emarsys.sample.ui.style.columnWithMaxWidth -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState -import kotlinx.coroutines.delay +import com.emarsys.sample.ui.style.rowWithMaxWidth +@ExperimentalCoilApi +@ExperimentalMaterialApi +@ExperimentalComposeUiApi class InboxScreen( override val context: Context ) : DetailScreen() { - private val viewModel = InboxViewModel() private val messagePresenter = MessagePresenter(context) - @ExperimentalCoilApi - @ExperimentalComposeUiApi @Composable override fun Detail(paddingValues: PaddingValues) { - SwipeRefreshCompose(messagePresenter = messagePresenter, paddingValues) - } - - @OptIn(ExperimentalCoilApi::class, androidx.compose.animation.ExperimentalAnimationApi::class) - @Composable - private fun SwipeRefreshCompose( - messagePresenter: MessagePresenter, - innerPadding: PaddingValues - ) { - Scaffold( - Modifier - .columnWithMaxWidth(), - topBar = { - TitleText(titleText = stringResource(id = R.string.inbox_title)) - }) + val pullRefreshState = rememberPullRefreshState( + refreshing = viewModel.refreshing.value, + onRefresh = viewModel::onSwipeFetchMessages + ) + Box( + modifier = Modifier + .fillMaxSize() + .rowWithMaxWidth() + .pullRefresh(pullRefreshState), + ) { - it.calculateBottomPadding() - var refreshing by remember { mutableStateOf(false) } - LaunchedEffect(key1 = refreshing) { - if (refreshing) { - delay(1000) - refreshing = false - } - } - SwipeRefresh( - state = rememberSwipeRefreshState(isRefreshing = refreshing), - onRefresh = { - refreshing = true - viewModel.onSwipeFetchMessages() - }) - { - AnimatedVisibility(visible = viewModel.isFetchedMessagesEmpty()) { - RowWithCenteredContent { - Text(text = stringResource(id = R.string.pull_down)) + LazyColumn( + modifier = Modifier + .fillMaxSize(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + RowWithCenteredContent() { + TitleText(titleText = stringResource(id = R.string.inbox_title)) } } - LazyColumn( - modifier = Modifier - .fillMaxSize(1f) - .padding(bottom = innerPadding.calculateBottomPadding()), - horizontalAlignment = Alignment.CenterHorizontally - ) { - items( - items = viewModel.fetchedMessages.toList(), - key = { message -> message.id }) { message -> - messagePresenter.MessageCard(message = message) + item { + AnimatedVisibility(visible = viewModel.isFetchedMessagesEmpty()) { + RowWithCenteredContent { + Text(text = stringResource(id = R.string.pull_down)) + } } } + items( + items = viewModel.fetchedMessages.toList(), + key = { message -> message.id }) { message -> + messagePresenter.MessageCard(message = message) + } } + + PullRefreshIndicator( + refreshing = viewModel.refreshing.value, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) } } } diff --git a/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxViewModel.kt b/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxViewModel.kt index 8ae2d56d..9c4b218c 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxViewModel.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/inbox/InboxViewModel.kt @@ -1,13 +1,14 @@ package com.emarsys.sample.inbox import android.util.Log +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.emarsys.Emarsys import com.emarsys.inbox.InboxTag import com.emarsys.mobileengage.api.inbox.Message class InboxViewModel : ViewModel() { - + val refreshing = mutableStateOf(false) val fetchedMessages = mutableSetOf() fun isFetchedMessagesEmpty(): Boolean { @@ -19,6 +20,7 @@ class InboxViewModel : ViewModel() { } fun onSwipeFetchMessages() { + refreshing.value = true Emarsys.messageInbox.fetchMessages { if (it.errorCause != null) { Log.e("INBOX", "Inbox Error" + it.errorCause) @@ -32,9 +34,10 @@ class InboxViewModel : ViewModel() { messageId = message.id ) } - this.addMessageToFetched(message) + addMessageToFetched(message) } } + refreshing.value = false } } } diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt b/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt index 0f2c2f1e..33fc46dc 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/main/MainActivity.kt @@ -6,9 +6,12 @@ import androidx.activity.compose.setContent import androidx.annotation.RequiresApi import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.imePadding +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Scaffold +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.fragment.app.FragmentActivity +import coil.annotation.ExperimentalCoilApi import com.emarsys.sample.dashboard.DashboardViewModel import com.emarsys.sample.main.navigation.BottomNavigationBar import com.emarsys.sample.main.navigation.NavigationControllerProvider @@ -17,9 +20,10 @@ import com.emarsys.sample.ui.theme.AndroidSampleAppTheme class MainActivity : FragmentActivity() { - @OptIn(ExperimentalAnimationApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class, - coil.annotation.ExperimentalCoilApi::class - ) + @ExperimentalAnimationApi + @ExperimentalComposeUiApi + @ExperimentalCoilApi + @ExperimentalMaterialApi @RequiresApi(Build.VERSION_CODES.Q) override fun onCreate(savedInstanceState: Bundle?) { diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/MainViewModel.kt b/sample/src/main/kotlin/com/emarsys/sample/main/MainViewModel.kt index a1b95513..86d97a24 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/main/MainViewModel.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/main/MainViewModel.kt @@ -1,8 +1,11 @@ package com.emarsys.sample.main import android.content.Context +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.lifecycle.ViewModel +import coil.annotation.ExperimentalCoilApi import com.emarsys.sample.dashboard.DashboardScreen import com.emarsys.sample.dashboard.DashboardViewModel import com.emarsys.sample.inapp.InAppScreen @@ -11,6 +14,7 @@ import com.emarsys.sample.mobileengage.MobileEngageScreen import com.emarsys.sample.predict.PredictScreen import com.emarsys.sample.ui.component.screen.DetailScreen +@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class, ExperimentalCoilApi::class) class MainViewModel( context: Context, dashboardViewModel: DashboardViewModel diff --git a/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationControllerProvider.kt b/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationControllerProvider.kt index dce5521f..20c71ac9 100644 --- a/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationControllerProvider.kt +++ b/sample/src/main/kotlin/com/emarsys/sample/main/navigation/NavigationControllerProvider.kt @@ -1,14 +1,20 @@ package com.emarsys.sample.main.navigation +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import coil.annotation.ExperimentalCoilApi import com.emarsys.sample.main.MainViewModel class NavigationControllerProvider(private val mainViewModel: MainViewModel) { + @ExperimentalMaterialApi + @ExperimentalComposeUiApi + @ExperimentalCoilApi @Composable fun provide(): NavHostController { val navHostController = rememberNavController() From 5b27a08b525314e8834c407369f22b7df499ca48 Mon Sep 17 00:00:00 2001 From: Andras Sarro Date: Tue, 24 Sep 2024 16:41:22 +0200 Subject: [PATCH 5/5] feat(geofence): register broadcastReceiver from manifest and fetch geofences when nearestGeofences list is empty SUITEDEV-36595 Co-authored-by: LasOri <24588073+LasOri@users.noreply.github.com> Co-authored-by: megamegax Co-authored-by: matusekma <36794575+matusekma@users.noreply.github.com> --- .../emarsys/core/util/AndroidVersionUtils.kt | 6 +- emarsys-sdk/src/main/AndroidManifest.xml | 8 + .../com/emarsys/di/DefaultEmarsysComponent.kt | 1 - .../geofence/DefaultGeofenceInternalTest.kt | 758 ++++++------------ .../geofence/DefaultGeofenceInternal.kt | 48 +- .../geofence/GeofenceBroadcastReceiver.kt | 14 +- .../geofence/GeofencePendingIntentProvider.kt | 6 +- 7 files changed, 295 insertions(+), 546 deletions(-) diff --git a/core/src/main/java/com/emarsys/core/util/AndroidVersionUtils.kt b/core/src/main/java/com/emarsys/core/util/AndroidVersionUtils.kt index d1f71d58..a7d75cb9 100644 --- a/core/src/main/java/com/emarsys/core/util/AndroidVersionUtils.kt +++ b/core/src/main/java/com/emarsys/core/util/AndroidVersionUtils.kt @@ -22,7 +22,11 @@ object AndroidVersionUtils { @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - val isUOrAbove: Boolean + val isSOrAbove: Boolean + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + + val isUOrAbove: Boolean @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE diff --git a/emarsys-sdk/src/main/AndroidManifest.xml b/emarsys-sdk/src/main/AndroidManifest.xml index 9c9b0919..07efa8de 100644 --- a/emarsys-sdk/src/main/AndroidManifest.xml +++ b/emarsys-sdk/src/main/AndroidManifest.xml @@ -9,6 +9,14 @@ + + + + + private lateinit var mockInitialEnterTriggerEnabledStorage: Storage private lateinit var mockPendingIntentProvider: GeofencePendingIntentProvider @@ -110,91 +114,63 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { @Before fun setUp() { concurrentHandlerHolder = ConcurrentHandlerHolderFactory.create() - mockInitialEnterTriggerEnabledStorage = mock() - - whenever(mockInitialEnterTriggerEnabledStorage.get()).thenReturn(false) - - mockResponseModel = mock() - mockGeofenceRequestModel = mock() - mockRequestModelFactory = mock() - whenever(mockRequestModelFactory.createFetchGeofenceRequest()).thenReturn( - mockGeofenceRequestModel - ) - - mockContext = mock() - val context: Context = getInstrumentation().targetContext - whenever(mockContext.packageName).thenReturn( - "com.emarsys.mobileengage.test" - ) - if (!AndroidVersionUtils.isBelow30) { - whenever(mockContext.attributionTag).thenReturn("tag") - } + mockInitialEnterTriggerEnabledStorage = mockk(relaxed = true) - val intent = Intent("com.emarsys.sdk.GEOFENCE_ACTION") - pendingIntent = - PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + every { mockInitialEnterTriggerEnabledStorage.get() } returns false - mockPendingIntentProvider = mock() - whenever(mockPendingIntentProvider.providePendingIntent()).thenReturn(pendingIntent) + mockResponseModel = mockk(relaxed = true) + mockGeofenceRequestModel = mockk(relaxed = true) + mockRequestModelFactory = mockk(relaxed = true) + every { mockRequestModelFactory.createFetchGeofenceRequest() } returns mockGeofenceRequestModel + mockPendingIntentProvider = mockk(relaxed = true) + pendingIntent = mockk(relaxed = true) + every { mockPendingIntentProvider.providePendingIntent() } returns pendingIntent fakeRequestManager = - spy(FakeRequestManager(FakeRequestManager.ResponseType.SUCCESS, mockResponseModel)) + spyk(FakeRequestManager(FakeRequestManager.ResponseType.SUCCESS, mockResponseModel)) - mockGeofenceResponseMapper = mock() - whenever( - mockGeofenceResponseMapper.map(any()) - ).thenReturn(GeofenceResponse(listOf())) - - mockPermissionChecker = mock() - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)).thenReturn( - PackageManager.PERMISSION_DENIED - ) + mockGeofenceResponseMapper = mockk(relaxed = true) + every { mockGeofenceResponseMapper.map(any()) } returns GeofenceResponse(listOf()) + mockPermissionChecker = mockk(relaxed = true) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) } returns PackageManager.PERMISSION_DENIED - mockLocation = mock() + mockLocation = mockk(relaxed = true) - mockTask = mock() - argumentCaptor> { - whenever(mockTask.addOnCompleteListener(capture())).thenAnswer { - firstValue.onComplete(mockTask) - mockTask - } + mockTask = mockk(relaxed = true) + every { mockTask.addOnCompleteListener(any()) } answers { + firstArg>().onComplete(mockTask) + mockTask } - - mockFusedLocationProviderClient = mock() - whenever( + mockFusedLocationProviderClient = mockk(relaxed = true) + every { mockFusedLocationProviderClient.requestLocationUpdates( any(), any() ) - ).thenReturn(mockTask) + } returns mockTask + every { mockFusedLocationProviderClient.lastLocation } returns FakeLocationTask(mockLocation) - whenever(mockFusedLocationProviderClient.lastLocation).thenReturn( - FakeLocationTask(mockLocation) - ) - mockGeofenceFilter = mock() - - whenever( + mockGeofenceFilter = mockk(relaxed = true) + every { mockGeofenceFilter.findNearestGeofences( any(), any() ) - ).thenReturn(nearestGeofencesWithRefreshArea) - - mockLocation = mock() - whenever(mockLocation.latitude).thenReturn(47.493165) - whenever(mockLocation.longitude).thenReturn(19.058359) - - mockGeofencingClient = mock() + } returns nearestGeofencesWithRefreshArea - mockActionCommandFactory = mock() - mockCacheableEventHandler = mock() - mockEnabledStorage = mock() + mockLocation = mockk(relaxed = true) + every { mockLocation.latitude } returns 47.493165 + every { mockLocation.longitude } returns 19.058359 - whenever(mockEnabledStorage.get()).thenReturn(true) + mockGeofencingClient = mockk(relaxed = true) + mockActionCommandFactory = mockk(relaxed = true) + mockCacheableEventHandler = mockk(relaxed = true) + mockEnabledStorage = mockk(relaxed = true) + every { mockEnabledStorage.get() } returns true geofenceInternal = DefaultGeofenceInternal( mockRequestModelFactory, @@ -204,24 +180,6 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { mockFusedLocationProviderClient, mockGeofenceFilter, mockGeofencingClient, - mockContext, - mockActionCommandFactory, - mockCacheableEventHandler, - mockEnabledStorage, - mockPendingIntentProvider, - concurrentHandlerHolder, - mockInitialEnterTriggerEnabledStorage - ) - - geofenceInternalWithMockContext = DefaultGeofenceInternal( - mockRequestModelFactory, - fakeRequestManager, - mockGeofenceResponseMapper, - mockPermissionChecker, - mockFusedLocationProviderClient, - mockGeofenceFilter, - mockGeofencingClient, - mockContext, mockActionCommandFactory, mockCacheableEventHandler, mockEnabledStorage, @@ -235,22 +193,22 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { fun testFetchGeofences_shouldSendRequest_viaRequestManager_submitNow() { geofenceInternal.fetchGeofences(null) - verify(fakeRequestManager).submitNow(any(), any()) + verify { fakeRequestManager.submitNow(any(), any()) } } @Test fun testFetchGeofences_callsMapOnResponseMapper_onSuccess() { geofenceInternal.fetchGeofences(null) - verify(mockGeofenceResponseMapper).map(mockResponseModel) + verify { mockGeofenceResponseMapper.map(mockResponseModel) } } @Test fun testFetchGeofences_callsEnable_onSuccess() { - val spyGeofenceInternal = spy(geofenceInternal) + val spyGeofenceInternal = spyk(geofenceInternal) spyGeofenceInternal.fetchGeofences(null) - verify(spyGeofenceInternal).enable(null) + verify { spyGeofenceInternal.enable(null) } } @Test @@ -258,145 +216,39 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { geofenceInternal.enable(null) if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) - } - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) - } - - @Test - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU) - fun testEnable_registersGeofenceBroadcastReceiverAboveTiramisu() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - geofenceInternalWithMockContext.enable(null) - geofenceInternalWithMockContext.enable(null) - - verify( - mockContext, - timeout(100).times(1) - ).registerReceiver( - any(), - any(), - eq(Context.RECEIVER_EXPORTED) - ) - - } - } - - @Test - @SdkSuppress(maxSdkVersion = 32) - fun testEnable_registersGeofenceBroadcastReceiverBelowTiramisu() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - geofenceInternalWithMockContext.enable(null) - geofenceInternalWithMockContext.enable(null) - - verify( - mockContext, - timeout(100).times(1) - ).registerReceiver(any(), any()) - } - } - - @Test - fun testDisable_unregistersGeofenceBroadcastReceiver() { - geofenceInternalWithMockContext.enable(null) - geofenceInternalWithMockContext.disable() - - verify(mockContext).unregisterReceiver(any()) - verify(mockFusedLocationProviderClient).removeLocationUpdates(pendingIntent) - } - - @Test - fun testDisable_shouldNotCallUnregisterReceiver_ifReceiversAreNotRegistered() { - geofenceInternalWithMockContext.enable(null) - geofenceInternalWithMockContext.disable() - geofenceInternalWithMockContext.disable() - - verify( - mockContext, - timeout(100).times(1) - ).unregisterReceiver(any()) - - } - - @Test - fun testDisable_shouldNotCrash_whenUnregisterReceiverCalled_multipleTimes() { - geofenceInternal.enable(null) - geofenceInternal.disable() - ReflectionTestUtils.setInstanceField( - geofenceInternalWithMockContext, - "receiverRegistered", - true - ) - geofenceInternal.disable() - } - - @Test - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU) - fun testDisableAboveTiramisu() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - geofenceInternalWithMockContext.enable(null) - geofenceInternalWithMockContext.disable() - geofenceInternalWithMockContext.enable(null) - - verify( - mockContext, timeout(100).times(2) - ).registerReceiver( - any(), - any(), - eq(Context.RECEIVER_EXPORTED) - ) - } - } - - @Test - @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.S_V2) - fun testDisableBelowTiramisu() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - geofenceInternalWithMockContext.enable(null) - geofenceInternalWithMockContext.disable() - geofenceInternalWithMockContext.enable(null) - - verify( - mockContext, timeout(100).times(2) - ).registerReceiver(any(), any()) + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } } + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } } @Test fun testFetch_doNotFetch_whenGeofenceIsNotEnabled() { - whenever(mockEnabledStorage.get()).thenReturn(false) + every { mockEnabledStorage.get() } returns false geofenceInternal.fetchGeofences(null) - verify(mockRequestModelFactory, times(0)).createFetchGeofenceRequest() - verify(fakeRequestManager, times(0)).submit(any(), any()) + verify(exactly = 0) { mockRequestModelFactory.createFetchGeofenceRequest() } + verify(exactly = 0) { fakeRequestManager.submit(any(), any()) } } @Test fun testFetch_fetch_whenGeofenceIsEnabled() { - whenever(mockEnabledStorage.get()).thenReturn( - true - ) + every { mockEnabledStorage.get() } returns true geofenceInternal.fetchGeofences(null) - verify(mockRequestModelFactory).createFetchGeofenceRequest() - verify(fakeRequestManager).submitNow(eq(mockGeofenceRequestModel), any()) - + verify { mockRequestModelFactory.createFetchGeofenceRequest() } + verify { fakeRequestManager.submitNow(eq(mockGeofenceRequestModel), any()) } } @Test fun testEnable_shouldSetEnabledStorage_andFetchIfWasDisabled() { val latch = CountDownLatch(1) var completionListenerHasBeenCalled = false - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_GRANTED - whenever(mockEnabledStorage.get()).thenReturn(false).thenReturn(true) + every { mockEnabledStorage.get() } returns false andThen true geofenceInternal.enable { it shouldBe null @@ -406,91 +258,65 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { latch.await() completionListenerHasBeenCalled shouldBe true - verify(mockEnabledStorage, timeout(100)).set(true) - verify(mockRequestModelFactory).createFetchGeofenceRequest() + verify { mockEnabledStorage.set(true) } + verify { mockRequestModelFactory.createFetchGeofenceRequest() } } - @Test fun testDisable_shouldClearEnabledStorage() { - geofenceInternalWithMockContext.enable(null) - geofenceInternalWithMockContext.disable() - - verify(mockEnabledStorage).set(false) + geofenceInternal.enable(null) + geofenceInternal.disable() + verify { mockEnabledStorage.set(false) } } @Test fun testIsEnabled_shouldReturnTrue_whenGeofenceIsEnabled() { - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockEnabledStorage.get()).thenReturn(false).thenReturn(false).thenReturn(true) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockEnabledStorage.get() } returns false andThen false andThen true geofenceInternal.isEnabled() shouldBe false geofenceInternal.enable(null) - verify(mockEnabledStorage, timeout(100)).set(true) + verify { mockEnabledStorage.set(true) } geofenceInternal.isEnabled() shouldBe true } @Test fun testIsEnabled_shouldReturnFalse_whenGeofenceIsDisabled() { - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockEnabledStorage.get()).thenReturn(true).thenReturn(true).thenReturn(false) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockEnabledStorage.get() } returns true andThen true andThen false geofenceInternal.isEnabled() shouldBe true geofenceInternal.disable() - verify(mockEnabledStorage, timeout(100)).set(false) + verify { mockEnabledStorage.set(false) } geofenceInternal.isEnabled() shouldBe false } @Test fun testEnable_fetchLastKnownLocation_fromLocationManager_whenPermissionGranted() { - whenever( - mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) - ).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_GRANTED geofenceInternal.enable(null) if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) - verify(mockFusedLocationProviderClient).lastLocation + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } + verify { mockFusedLocationProviderClient.lastLocation } } } @Test fun testEnable_fetchLastKnownLocation_fromLocationManager_whenPermissionGranted_withCompletionListener() { val latch = CountDownLatch(1) - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_GRANTED var completionListenerHasBeenCalled = false @@ -502,28 +328,19 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { latch.await() completionListenerHasBeenCalled shouldBe true if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } } - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) - - verify(mockFusedLocationProviderClient).lastLocation + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } + verify { mockFusedLocationProviderClient.lastLocation } } @Test fun testEnable_returnDoNoTReturn_PermissionForLocationException_whenFineLocationPermissionDenied_andCoarseLocationGranted() { val latch = CountDownLatch(1) - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)).thenReturn( - PackageManager.PERMISSION_DENIED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_DENIED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_GRANTED var completionListenerHasBeenCalled = false @@ -535,36 +352,22 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { latch.await() completionListenerHasBeenCalled shouldBe true if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } } - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) - - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) - verify(mockFusedLocationProviderClient).lastLocation + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) } + verify { mockFusedLocationProviderClient.lastLocation } } @Test fun testEnable_returnNoPermissionForLocationException_whenFineLocationPermissionDenied() { val geofenceResponse = GeofenceResponse(listOf(), 0.0) - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)).thenReturn( - PackageManager.PERMISSION_DENIED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockFusedLocationProviderClient.lastLocation).thenReturn( - FakeLocationTask( - mockLocation - ) - ) - - whenever(mockGeofenceResponseMapper.map(any())).thenReturn( - geofenceResponse - ) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_DENIED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockFusedLocationProviderClient.lastLocation } returns FakeLocationTask(mockLocation) + every { mockGeofenceResponseMapper.map(any()) } returns geofenceResponse var completionListenerHasBeenCalled = false geofenceInternal.enable { @@ -575,40 +378,23 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { completionListenerHasBeenCalled shouldBe true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } } - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) - - verifyNoInteractions( - mockFusedLocationProviderClient - ) + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } - verifyNoInteractions(mockGeofenceFilter) + confirmVerified(mockFusedLocationProviderClient) + confirmVerified(mockGeofenceFilter) } - @Test fun testEnable_returnNoPermissionForLocationException_whenBackgroundLocationPermissionDenied() { val geofenceResponse = GeofenceResponse(listOf(), 0.0) - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_DENIED - ) - - whenever(mockFusedLocationProviderClient.lastLocation).thenReturn( - FakeLocationTask( - mockLocation - ) - ) - - whenever(mockGeofenceResponseMapper.map(any())).thenReturn( - geofenceResponse - ) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_DENIED + every { mockFusedLocationProviderClient.lastLocation } returns FakeLocationTask(mockLocation) + every { mockGeofenceResponseMapper.map(any()) } returns geofenceResponse var completionListenerHasBeenCalled = false geofenceInternal.enable { @@ -621,35 +407,22 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { completionListenerHasBeenCalled shouldBe true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) - verifyNoInteractions(mockFusedLocationProviderClient) - verifyNoInteractions(mockGeofenceFilter) + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } + confirmVerified(mockFusedLocationProviderClient) + confirmVerified(mockGeofenceFilter) } - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } } @Test fun testEnable_returnNoPermissionForLocationException_whenPermissionsDenied() { val geofenceResponse = GeofenceResponse(listOf(), 0.0) - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)).thenReturn( - PackageManager.PERMISSION_DENIED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_DENIED - ) - - whenever(mockFusedLocationProviderClient.lastLocation).thenReturn( - FakeLocationTask( - mockLocation - ) - ) - - whenever(mockGeofenceResponseMapper.map(any())).thenReturn( - geofenceResponse - ) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_DENIED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_DENIED + every { mockFusedLocationProviderClient.lastLocation } returns FakeLocationTask(mockLocation) + every { mockGeofenceResponseMapper.map(any()) } returns geofenceResponse var completionListenerHasBeenCalled = false geofenceInternal.enable { @@ -664,92 +437,73 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { completionListenerHasBeenCalled shouldBe true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } } - verify(mockPermissionChecker).checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) - - verifyNoInteractions( - mockFusedLocationProviderClient - ) - - verifyNoInteractions(mockGeofenceFilter) + verify { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } + confirmVerified(mockFusedLocationProviderClient) + confirmVerified(mockGeofenceFilter) } @Test fun testEnable_registersGeofencesWithAdditionalRefreshArea() { val geofenceResponse = GeofenceResponse(listOf(), 0.3) - whenever(mockGeofenceResponseMapper.map(any())).thenReturn( - geofenceResponse - ) + every { mockGeofenceResponseMapper.map(any()) } returns geofenceResponse - val spyGeofenceInternal: GeofenceInternal = spy(geofenceInternal) + val spyGeofenceInternal: GeofenceInternal = spyk(geofenceInternal) spyGeofenceInternal.fetchGeofences(null) - whenever(mockGeofenceFilter.findNearestGeofences(any(), any())).thenReturn( - nearestGeofencesWithoutRefreshArea - ) + every { + mockGeofenceFilter.findNearestGeofences( + any(), + any() + ) + } returns nearestGeofencesWithoutRefreshArea val currentLocation = Location(LocationManager.GPS_PROVIDER).apply { this.latitude = 47.493160 this.longitude = 19.058355 } - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockFusedLocationProviderClient.lastLocation).thenReturn( - FakeLocationTask( - currentLocation - ) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockFusedLocationProviderClient.lastLocation } returns FakeLocationTask( + currentLocation ) spyGeofenceInternal.enable(null) - argumentCaptor> { - verify(spyGeofenceInternal, times(2)).registerGeofences(capture()) + val slot = mutableListOf>() + verify(exactly = 2) { spyGeofenceInternal.registerGeofences(capture(slot)) } + slot.size shouldBe 2 - lastValue.size shouldBe 4 - lastValue[3].toString() shouldBe refreshArea.toString() - } + slot[1].size shouldBe 4 + slot[1][3].toString() shouldBe refreshArea.toString() } @Test fun testEnable_shouldNotCrash_registersGeofencesWhenRefreshRadiusWouldBeNegative() { val geofenceResponse = GeofenceResponse(listOf(), 1.0) - whenever(mockGeofenceResponseMapper.map(any())).thenReturn( - geofenceResponse - ) + every { mockGeofenceResponseMapper.map(any()) } returns geofenceResponse - val spyGeofenceInternal: GeofenceInternal = spy(geofenceInternal) + val spyGeofenceInternal: GeofenceInternal = spyk(geofenceInternal) spyGeofenceInternal.fetchGeofences(null) - whenever(mockGeofenceFilter.findNearestGeofences(any(), any())).thenReturn( - listOf(nearestGeofencesWithoutRefreshArea[0]) + every { mockGeofenceFilter.findNearestGeofences(any(), any()) } returns listOf( + nearestGeofencesWithoutRefreshArea[0] ) - val currentLocation = (Location(LocationManager.GPS_PROVIDER).apply { + val currentLocation = Location(LocationManager.GPS_PROVIDER).apply { this.latitude = 47.493160 this.longitude = 19.058355 - }) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) - - whenever(mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).thenReturn( - PackageManager.PERMISSION_GRANTED - ) + } - whenever(mockFusedLocationProviderClient.lastLocation).thenReturn( - FakeLocationTask(currentLocation) + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockPermissionChecker.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } returns PackageManager.PERMISSION_GRANTED + every { mockFusedLocationProviderClient.lastLocation } returns FakeLocationTask( + currentLocation ) spyGeofenceInternal.enable(null) @@ -762,73 +516,44 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { refreshArea ) val geofencingRequest = - GeofencingRequest.Builder().addGeofences(geofencesToTest).setInitialTrigger(0) - .build() - argumentCaptor { - whenever( - mockGeofencingClient.addGeofences( - capture(), - any() - ) - ).thenReturn( - mockTask - ) + GeofencingRequest.Builder().addGeofences(geofencesToTest).setInitialTrigger(0).build() - geofenceInternal.registerGeofences(nearestGeofencesWithoutRefreshArea + refreshArea) + val slot = slot() + every { mockGeofencingClient.addGeofences(capture(slot), any()) } returns mockTask - firstValue.initialTrigger shouldBe geofencingRequest.initialTrigger - firstValue.geofences.forEachIndexed { index, geofence -> - geofence.requestId shouldBe geofencingRequest.geofences[index].requestId - } - } + geofenceInternal.registerGeofences(nearestGeofencesWithoutRefreshArea + refreshArea) + slot.captured.initialTrigger shouldBe geofencingRequest.initialTrigger + slot.captured.geofences.forEachIndexed { index, geofence -> + geofence.requestId shouldBe geofencingRequest.geofences[index].requestId + } } @Test fun testRegisterGeofences_geofencingRequest_shouldIncludeInitialEnterTrigger() { val geofencesToTest = nearestGeofencesWithoutRefreshArea.map { createGeofence(it) } + createGeofence( - Companion.refreshArea - ) - val geofencingRequest = - GeofencingRequest.Builder().addGeofences(geofencesToTest).build() - argumentCaptor { - whenever( - mockGeofencingClient.addGeofences( - capture(), - any() - ) - ).thenReturn( - mockTask + refreshArea ) + val geofencingRequest = GeofencingRequest.Builder().addGeofences(geofencesToTest).build() + val slot = slot() + every { mockGeofencingClient.addGeofences(capture(slot), any()) } returns mockTask - geofenceInternal.setInitialEnterTriggerEnabled(true) - geofenceInternal.registerGeofences(nearestGeofencesWithoutRefreshArea + refreshArea) + geofenceInternal.setInitialEnterTriggerEnabled(true) + geofenceInternal.registerGeofences(nearestGeofencesWithoutRefreshArea + refreshArea) - verify(mockInitialEnterTriggerEnabledStorage).set(true) - firstValue.initialTrigger shouldBe GeofencingRequest.INITIAL_TRIGGER_ENTER - firstValue.geofences.forEachIndexed { index, geofence -> - geofence.requestId shouldBe geofencingRequest.geofences[index].requestId - } + verify { mockInitialEnterTriggerEnabledStorage.set(true) } + slot.captured.initialTrigger shouldBe GeofencingRequest.INITIAL_TRIGGER_ENTER + slot.captured.geofences.forEachIndexed { index, geofence -> + geofence.requestId shouldBe geofencingRequest.geofences[index].requestId } } @Test - fun testOnGeofenceTriggered() { + fun testOnGeofenceTriggered_shouldExecuteActions_ifFeatureIsEnabled_andNearestGeofences_isNotEmpty() { val latch = CountDownLatch(1) - val mockAction: Runnable = mock() - val appEventAction = JSONObject( - """ - { - "type": "MEAppEvent", - "name": "nameValue", - "payload": { - "someKey": "someValue" - } - } - """.trimIndent() - ) + val mockAction: Runnable = mockk(relaxed = true) val testTrigger = Trigger(id = "appEventActionId", type = TriggerType.ENTER, action = appEventAction) val testExitTrigger = @@ -849,10 +574,11 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { MEGeofence("geofenceId5", 47.492292, 19.056440, 10.0, null, listOf(trigger)) ) ReflectionTestUtils.setInstanceField(geofenceInternal, "nearestGeofences", allGeofences) - whenever(mockAction.run()).thenAnswer { latch.countDown() } - whenever(mockActionCommandFactory.createActionCommand(appEventAction)).thenReturn( - mockAction - ) + every { mockAction.run() } answers { latch.countDown() } + every { mockActionCommandFactory.createActionCommand(appEventAction) } returns mockAction + + every { mockEnabledStorage.get() } returns true + geofenceInternal.onGeofenceTriggered( listOf( TriggeringEmarsysGeofence( @@ -862,41 +588,75 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { ) ) - verify( - mockActionCommandFactory, times(1) - ).createActionCommand( - appEventAction - ) + verify { mockActionCommandFactory.createActionCommand(appEventAction) } latch.await() - verify(mockAction).run() + verify { mockAction.run() } } + @Test + fun testOnGeofenceTriggered_shouldFetchFirst_andHandleActions_ifFeatureIsEnabled_andNearestGeofences_isEmpty() { + every { mockEnabledStorage.get() } returns true + ReflectionTestUtils.setInstanceField( + geofenceInternal, + "nearestGeofences", + emptyList() + ) + every { mockGeofenceResponseMapper.map(mockResponseModel) } returns GeofenceResponse( + listOf( + GeofenceGroup( + "group1", null, listOf( + com.emarsys.mobileengage.api.geofence.Geofence( + "geofenceId1", 47.493827, 19.060715, 10.0, null, listOf( + Trigger("triggerId", TriggerType.ENTER, 0, appEventAction) + ) + ) + ) + ) + ) + ) + geofenceInternal.onGeofenceTriggered( + listOf( + TriggeringEmarsysGeofence( + "geofenceId1", + TriggerType.ENTER + ) + ) + ) + + verify { mockGeofenceResponseMapper.map(mockResponseModel) } + verify { mockActionCommandFactory.createActionCommand(any()) } + } @Test - fun testOnGeofenceTriggered_onRefreshArea_recalculateGeofences() { - val spyGeofenceInternal = spy(geofenceInternal) - val latch = CountDownLatch(1) - val mockAction: Runnable = mock() - val appEventAction = JSONObject( - """ - { - "type": "MEAppEvent", - "name": "nameValue", - "payload": { - "someKey": "someValue" - } - } - """.trimIndent() + fun testOnGeofenceTriggered_shouldNotHandleActions_ifFeatureIsDisabled() { + every { mockEnabledStorage.get() } returns false + + geofenceInternal.onGeofenceTriggered( + listOf( + TriggeringEmarsysGeofence( + "testId", + TriggerType.ENTER + ) + ) ) + verify(exactly = 0) { mockActionCommandFactory.createActionCommand(any()) } + } + + @Test + fun testOnGeofenceTriggered_onRefreshArea_recalculateGeofences() { + val spyGeofenceInternal = spyk(geofenceInternal) + val latch = CountDownLatch(1) + val mockAction: Runnable = mockk(relaxed = true) val testTrigger = Trigger(id = "appEventActionId", type = TriggerType.ENTER, action = appEventAction) val trigger = Trigger(id = "triggerId", type = TriggerType.ENTER, action = JSONObject()) - val currentLocation = (Location(LocationManager.GPS_PROVIDER).apply { + val currentLocation = Location(LocationManager.GPS_PROVIDER).apply { this.latitude = 47.493160 this.longitude = 19.058355 - }) + } + every { mockEnabledStorage.get() } returns true val allGeofences = listOf( MEGeofence("geofenceId1", 47.493160, 19.058355, 10.0, null, listOf(trigger)), MEGeofence("geofenceId2", 47.493812, 19.058537, 10.0, null, listOf(trigger)), @@ -952,51 +712,39 @@ class DefaultGeofenceInternalTest : AnnotationSpec() { "geofenceResponse", geofenceResponse ) - whenever(mockAction.run()).thenAnswer { latch.countDown() } - whenever(mockActionCommandFactory.createActionCommand(appEventAction)).thenReturn( - mockAction - ) + every { mockAction.run() } answers { latch.countDown() } + every { mockActionCommandFactory.createActionCommand(appEventAction) } returns mockAction - whenever(mockFusedLocationProviderClient.lastLocation).thenReturn( - FakeLocationTask(currentLocation) + every { mockFusedLocationProviderClient.lastLocation } returns FakeLocationTask( + currentLocation ) - whenever( + every { mockGeofenceFilter.findNearestGeofences( currentLocation, geofenceResponse ) - ).thenReturn( - nearestGeofences2 - ) + } returns nearestGeofences2 + val slot = slot>() spyGeofenceInternal.onGeofenceTriggered( listOf( - TriggeringEmarsysGeofence( - "geofenceId1", - TriggerType.ENTER - ), TriggeringEmarsysGeofence("refreshArea", TriggerType.EXIT) + TriggeringEmarsysGeofence("geofenceId1", TriggerType.ENTER), + TriggeringEmarsysGeofence("refreshArea", TriggerType.EXIT) ) ) - argumentCaptor> { - verify(spyGeofenceInternal).registerGeofences(capture()) - verify(mockActionCommandFactory).createActionCommand(appEventAction) + verify { spyGeofenceInternal.registerGeofences(capture(slot)) } + verify { mockActionCommandFactory.createActionCommand(appEventAction) } - latch.await() + latch.await() - verify(mockAction).run() + verify { mockAction.run() } - verify( - mockGeofenceFilter - ).findNearestGeofences( - currentLocation, - geofenceResponse - ) + verify { mockGeofenceFilter.findNearestGeofences(currentLocation, geofenceResponse) } - firstValue.size shouldBe 2 - firstValue[1].toString() shouldBe refreshArea.toString() - } + slot.captured.size shouldBe 2 + slot.captured[1].toString() shouldBe refreshArea.toString() } private fun createGeofence(geofence: MEGeofence): Geofence { diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternal.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternal.kt index c826c99c..cc57ce13 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternal.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/DefaultGeofenceInternal.kt @@ -2,8 +2,6 @@ package com.emarsys.mobileengage.geofence import android.Manifest import android.app.PendingIntent -import android.content.Context -import android.content.IntentFilter import android.content.pm.PackageManager import android.location.Location import androidx.annotation.RequiresPermission @@ -43,7 +41,6 @@ class DefaultGeofenceInternal( private val fusedLocationProviderClient: FusedLocationProviderClient, private val geofenceFilter: GeofenceFilter, private val geofencingClient: GeofencingClient, - private val context: Context, private val actionCommandFactory: ActionCommandFactory, private val geofenceCacheableEventHandler: CacheableEventHandler, private val geofenceEnabledStorage: Storage, @@ -57,7 +54,6 @@ class DefaultGeofenceInternal( const val MAX_WAIT_TIME: Long = 60_000 } - private val geofenceBroadcastReceiver = GeofenceBroadcastReceiver(concurrentHandlerHolder) private var geofenceResponse: GeofenceResponse? = null private var nearestGeofences: MutableList = mutableListOf() override val registeredGeofences: List @@ -68,7 +64,6 @@ class DefaultGeofenceInternal( private val geofencePendingIntent: PendingIntent by lazy { geofencePendingIntentProvider.providePendingIntent() } - private var receiverRegistered = false private var initialEnterTriggerEnabled = initialEnterTriggerEnabledStorage.get() ?: false private var initialDwellingTriggerEnabled = false private var initialExitTriggerEnabled = false @@ -115,7 +110,6 @@ class DefaultGeofenceInternal( } } registerNearestGeofences(completionListener) - registerBroadcastReceiver() } else { completionListener?.onCompleted(MissingPermissionException("Couldn't acquire permission for $missingPermissions")) } @@ -123,13 +117,9 @@ class DefaultGeofenceInternal( override fun disable() { if (geofenceEnabledStorage.get()) { - if (receiverRegistered) { - try { - context.unregisterReceiver(geofenceBroadcastReceiver) - receiverRegistered = false - fusedLocationProviderClient.removeLocationUpdates(geofencePendingIntent) - } catch (ignored: IllegalArgumentException) { - } + try { + fusedLocationProviderClient.removeLocationUpdates(geofencePendingIntent) + } catch (ignored: IllegalArgumentException) { } geofenceEnabledStorage.set(false) @@ -146,26 +136,6 @@ class DefaultGeofenceInternal( return geofenceEnabledStorage.get() } - private fun registerBroadcastReceiver() { - if (!receiverRegistered) { - concurrentHandlerHolder.postOnMain { - if (AndroidVersionUtils.isTiramisuOrAbove) { - context.registerReceiver( - geofenceBroadcastReceiver, - IntentFilter("com.emarsys.sdk.GEOFENCE_ACTION"), - Context.RECEIVER_EXPORTED - ) - } else { - context.registerReceiver( - geofenceBroadcastReceiver, - IntentFilter("com.emarsys.sdk.GEOFENCE_ACTION"), - ) - } - } - receiverRegistered = true - } - } - @RequiresPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) private fun validateBackgroundPermission() { } @@ -299,6 +269,18 @@ class DefaultGeofenceInternal( } override fun onGeofenceTriggered(triggeringEmarsysGeofences: List) { + if (isEnabled()) { + if (nearestGeofences.isEmpty()) { + fetchGeofences { + handleActions(triggeringEmarsysGeofences) + } + } else { + handleActions(triggeringEmarsysGeofences) + } + } + } + + private fun handleActions(triggeringEmarsysGeofences: List) { extractActions(triggeringEmarsysGeofences).run { executeActions(this) } diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofenceBroadcastReceiver.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofenceBroadcastReceiver.kt index 2dcfafaa..42d34d46 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofenceBroadcastReceiver.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofenceBroadcastReceiver.kt @@ -15,6 +15,7 @@ import com.google.android.gms.location.GeofencingEvent class GeofenceBroadcastReceiver(val concurrentHandlerHolder: ConcurrentHandlerHolder) : BroadcastReceiver() { + constructor() : this(mobileEngage().concurrentHandlerHolder) override fun onReceive(context: Context, intent: Intent) { val geofencingEvent = GeofencingEvent.fromIntent(intent) @@ -52,10 +53,17 @@ class GeofenceBroadcastReceiver(val concurrentHandlerHolder: ConcurrentHandlerHo private fun logTriggeringGeofences(triggeringEmarsysGeofences: List) { triggeringEmarsysGeofences.forEach { val status = mapOf( - "triggerType" to it.triggerType, - "geofenceId" to it.geofenceId + "triggerType" to it.triggerType, + "geofenceId" to it.geofenceId + ) + Logger.debug( + StatusLog( + GeofenceBroadcastReceiver::class.java, + SystemUtils.getCallerMethodName(), + mapOf(), + status + ) ) - Logger.debug(StatusLog(GeofenceBroadcastReceiver::class.java, SystemUtils.getCallerMethodName(), mapOf(), status)) } } diff --git a/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofencePendingIntentProvider.kt b/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofencePendingIntentProvider.kt index b0fa1405..112d2347 100644 --- a/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofencePendingIntentProvider.kt +++ b/mobile-engage/src/main/java/com/emarsys/mobileengage/geofence/GeofencePendingIntentProvider.kt @@ -11,15 +11,15 @@ class GeofencePendingIntentProvider(private val context: Context) { fun providePendingIntent(): PendingIntent { val intent = Intent("com.emarsys.sdk.GEOFENCE_ACTION") intent.setPackage(context.packageName) - if (AndroidVersionUtils.isUOrAbove) { - return PendingIntent.getBroadcast( + return if (AndroidVersionUtils.isSOrAbove) { + PendingIntent.getBroadcast( context, 0, intent, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) } else { - return PendingIntent.getBroadcast( + PendingIntent.getBroadcast( context, 0, intent,