diff --git a/CHANGELOG.md b/CHANGELOG.md index e2ffcbf..26b99c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Change Log +## Version 5.1.11 *(2024-12-02)* +* Bug fixes and performance improvements + # Change Log ## Version 5.1.10 *(2024-09-25)* * Changed exception tracking to include 'ct=webtrekk_ignore' as a query parameter in a tracking requests. diff --git a/android-sdk/src/main/java/webtrekk/android/sdk/core/Scheduler.kt b/android-sdk/src/main/java/webtrekk/android/sdk/core/Scheduler.kt index 6072c8f..1268593 100644 --- a/android-sdk/src/main/java/webtrekk/android/sdk/core/Scheduler.kt +++ b/android-sdk/src/main/java/webtrekk/android/sdk/core/Scheduler.kt @@ -40,20 +40,25 @@ internal interface Scheduler { * @param repeatInterval the periodic time that will be used by [WorkManager] to send the requests from the cache to the server. * @param constraints the [WorkManager] constraints that will be applied on that worker. */ - fun scheduleSendRequests(repeatInterval: Long, constraints: Constraints) + suspend fun scheduleSendRequests(repeatInterval: Long, constraints: Constraints) /** * A one time worker that will be used to send all available requests in the cache to the server, then cleaning up the cache. Used for Opt out. */ - fun sendRequestsThenCleanUp() + suspend fun sendRequestsThenCleanUp() /** * A worker that is scheduled to clean up the requests in the cache that are already sent to the server. */ - fun scheduleCleanUp() + suspend fun scheduleCleanUp() /** * Cancel current periodic worker that is used to send the request every n times. Used for Opt out. */ - fun cancelScheduleSendRequests() + suspend fun cancelScheduleSendRequests() + + /** + * Delete records about completed or canceled works + */ + suspend fun pruneWorks() } \ No newline at end of file diff --git a/android-sdk/src/main/java/webtrekk/android/sdk/core/SchedulerImpl.kt b/android-sdk/src/main/java/webtrekk/android/sdk/core/SchedulerImpl.kt index 2d07baa..6a57249 100644 --- a/android-sdk/src/main/java/webtrekk/android/sdk/core/SchedulerImpl.kt +++ b/android-sdk/src/main/java/webtrekk/android/sdk/core/SchedulerImpl.kt @@ -32,14 +32,18 @@ import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.OutOfQuotaPolicy -import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkInfo import androidx.work.WorkManager +import androidx.work.WorkQuery +import androidx.work.await import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import webtrekk.android.sdk.Config import webtrekk.android.sdk.domain.worker.CleanUpWorker import webtrekk.android.sdk.domain.worker.SendRequestsWorker +import webtrekk.android.sdk.module.AppModule import webtrekk.android.sdk.util.webtrekkLogger import java.util.concurrent.TimeUnit @@ -51,65 +55,71 @@ internal class SchedulerImpl( private val config: Config, ) : Scheduler { - private val mutex = Mutex() - - override fun scheduleSendRequests( + override suspend fun scheduleSendRequests( repeatInterval: Long, constraints: Constraints, ) { - webtrekkLogger.debug("SEND WORKER - scheduleSendRequests") - synchronized(mutex) { - val data = Data.Builder().apply { - putStringArray("trackIds", config.trackIds.toTypedArray()) - putString("trackDomain", config.trackDomain) - }.build() - - val workBuilder = PeriodicWorkRequest.Builder( - SendRequestsWorker::class.java, - repeatInterval, - TimeUnit.MINUTES - ).setConstraints(constraints) - .setInitialDelay(0, TimeUnit.MILLISECONDS) - .setInputData(data) - .addTag(SendRequestsWorker.TAG) - - val sendRequestsWorker = workBuilder.build() - - workManager.enqueueUniquePeriodicWork( - SEND_REQUESTS_WORKER, - ExistingPeriodicWorkPolicy.UPDATE, - sendRequestsWorker - ) + withContext(AppModule.dispatchers.mainDispatcher) { + webtrekkLogger.debug("SEND WORKER - scheduleSendRequests") + mutex.withLock { + val data = Data.Builder().apply { + putStringArray("trackIds", config.trackIds.toTypedArray()) + putString("trackDomain", config.trackDomain) + }.build() + + val workBuilder = PeriodicWorkRequestBuilder( + repeatInterval, + TimeUnit.MINUTES + ).setConstraints(constraints) + .setInitialDelay(0, TimeUnit.MILLISECONDS) + .setInputData(data) + + val sendRequestsWorker = workBuilder.build() + + workManager.enqueueUniquePeriodicWork( + SEND_REQUESTS_WORKER, + ExistingPeriodicWorkPolicy.UPDATE, + sendRequestsWorker + ) + } } } // To be changed to clean up after executing the requests - override fun scheduleCleanUp() { - val data = Data.Builder().apply { - putStringArray("trackIds", config.trackIds.toTypedArray()) - putString("trackDomain", config.trackDomain) - }.build() - - val cleanWorkBuilder = PeriodicWorkRequestBuilder(60, TimeUnit.MINUTES) - .addTag(CleanUpWorker.TAG) - .setInitialDelay(5, TimeUnit.MINUTES) - .setInputData(data) - - workManager.enqueueUniquePeriodicWork( - CLEAN_UP_WORKER, - ExistingPeriodicWorkPolicy.UPDATE, - cleanWorkBuilder.build() - ) + override suspend fun scheduleCleanUp() { + withContext(AppModule.dispatchers.mainDispatcher) { + mutex.withLock { + val data = Data.Builder().apply { + putStringArray("trackIds", config.trackIds.toTypedArray()) + putString("trackDomain", config.trackDomain) + }.build() + + val cleanWorkBuilder = + PeriodicWorkRequestBuilder( + 60, + TimeUnit.MINUTES + ).setInitialDelay(5, TimeUnit.MINUTES) + .setInputData(data) + + workManager.enqueueUniquePeriodicWork( + CLEAN_UP_WORKER, + ExistingPeriodicWorkPolicy.UPDATE, + cleanWorkBuilder.build() + ) + } + } } - override fun sendRequestsThenCleanUp() { - webtrekkLogger.debug("SEND WORKER - sendRequestsThenCleanUp") - synchronized(mutex) { - // check if SendRequestsWorker already running as periodic work request - val future = workManager.getWorkInfosForUniqueWork(ONE_TIME_REQUEST) - val workers = future.get() - if (workers.none { it.state in listOf(WorkInfo.State.RUNNING) }) { - scheduleSendAndCleanWorkers() + override suspend fun sendRequestsThenCleanUp() { + withContext(AppModule.dispatchers.mainDispatcher) { + webtrekkLogger.debug("SEND WORKER - sendRequestsThenCleanUp") + mutex.withLock { + // check if SendRequestsWorker already running as periodic work request + val query = WorkQuery.fromTags(SendRequestsWorker::class.java.name) + val workers = workManager.getWorkInfos(query).get() + if (workers.none { it.state in listOf(WorkInfo.State.RUNNING) }) { + scheduleSendAndCleanWorkers() + } } } } @@ -122,33 +132,33 @@ internal class SchedulerImpl( val sendWorkBuilder = OneTimeWorkRequest.Builder(SendRequestsWorker::class.java) .setInputData(data) - .addTag(SendRequestsWorker.TAG) val cleanWorkBuilder = OneTimeWorkRequest.Builder(CleanUpWorker::class.java) .setInputData(data) - .addTag(CleanUpWorker.TAG) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { sendWorkBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) cleanWorkBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) } - workManager.beginUniqueWork( - ONE_TIME_REQUEST, - ExistingWorkPolicy.REPLACE, - sendWorkBuilder.build() - ) + workManager + .beginUniqueWork(ONE_TIME_REQUEST, ExistingWorkPolicy.REPLACE, sendWorkBuilder.build()) .then(cleanWorkBuilder.build()) .enqueue() } - override fun cancelScheduleSendRequests() { + override suspend fun cancelScheduleSendRequests() { workManager.cancelUniqueWork(CLEAN_UP_WORKER) workManager.cancelUniqueWork(ONE_TIME_REQUEST) workManager.cancelUniqueWork(SEND_REQUESTS_WORKER) } + override suspend fun pruneWorks() { + workManager.pruneWork().await() + } + companion object { + private val mutex = Mutex() const val SEND_REQUESTS_WORKER = "send_requests_worker" const val ONE_TIME_REQUEST = "one_time_request_send_and_clean" const val CLEAN_UP_WORKER = "CleanUpWorker" diff --git a/android-sdk/src/main/java/webtrekk/android/sdk/core/WebtrekkImpl.kt b/android-sdk/src/main/java/webtrekk/android/sdk/core/WebtrekkImpl.kt index d223850..f8cae2d 100644 --- a/android-sdk/src/main/java/webtrekk/android/sdk/core/WebtrekkImpl.kt +++ b/android-sdk/src/main/java/webtrekk/android/sdk/core/WebtrekkImpl.kt @@ -546,8 +546,14 @@ constructor() : Webtrekk(), if (config.exceptionLogLevel.isUncaughtAllowed()) { initUncaughtExceptionTracking() } - // Scheduling the workers for cleaning up the current cache, and setting up the periodic worker for sending the requests. + + // Clean all finished works (SUCCEEDED, FAILED, CANCELED) + scheduler.pruneWorks() + + // Scheduling the workers for clearing sent requests. scheduler.scheduleCleanUp() + + // Scheduling the periodic worker for sending requests scheduler.scheduleSendRequests( repeatInterval = config.requestsInterval, constraints = config.workManagerConstraints diff --git a/android-sdk/src/main/java/webtrekk/android/sdk/domain/external/Optout.kt b/android-sdk/src/main/java/webtrekk/android/sdk/domain/external/Optout.kt index b22a5e8..c508c65 100644 --- a/android-sdk/src/main/java/webtrekk/android/sdk/domain/external/Optout.kt +++ b/android-sdk/src/main/java/webtrekk/android/sdk/domain/external/Optout.kt @@ -29,6 +29,7 @@ import android.content.Context import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import webtrekk.android.sdk.core.AppState import webtrekk.android.sdk.core.Scheduler @@ -52,7 +53,7 @@ internal class Optout( private val clearTrackRequests: ClearTrackRequests ) : ExternalInteractor { - private val _job = Job() + private val _job = SupervisorJob() override val scope = CoroutineScope(_job + coroutineContext) // Starting a new job with context of the parent. @@ -62,23 +63,23 @@ internal class Optout( private val logger by lazy { AppModule.logger } override fun invoke(invokeParams: Params, coroutineDispatchers: CoroutineDispatchers) { - // Store the opt out value in the shared preferences. - sessions.optOut(invokeParams.optOutValue) + scope.launch( + context = coroutineDispatchers.ioDispatcher + coroutineExceptionHandler( + logger + ), + start = CoroutineStart.DEFAULT + ) { + // Store the opt out value in the shared preferences. + sessions.optOut(invokeParams.optOutValue) - // If opt out value is set to true, then disable tracking data, cancel all work manager workers and delete or send then delete current data in the data base. - if (invokeParams.optOutValue) { - appState.disable(invokeParams.context) // Disable the auto track - scheduler.cancelScheduleSendRequests() // Cancel the work manager workers - // If sendCurrentData is true, then one time worker will send current data requests to the server, then clean up the data base. - if (invokeParams.sendCurrentData) { - scheduler.sendRequestsThenCleanUp() - } else { - scope.launch( - context = coroutineDispatchers.ioDispatcher + coroutineExceptionHandler( - logger - ), - start = CoroutineStart.DEFAULT - ) { + // If opt out value is set to true, then disable tracking data, cancel all work manager workers and delete or send then delete current data in the data base. + if (invokeParams.optOutValue) { + appState.disable(invokeParams.context) // Disable the auto track + scheduler.cancelScheduleSendRequests() // Cancel the work manager workers + // If sendCurrentData is true, then one time worker will send current data requests to the server, then clean up the data base. + if (invokeParams.sendCurrentData) { + scheduler.sendRequestsThenCleanUp() + } else { clearTrackRequests(ClearTrackRequests.Params(trackRequests = emptyList())) .onSuccess { logger.debug("Cleared all track requests, opt out is active") } .onFailure { logger.error("Failed to clear the track requests while opting out") } diff --git a/android-sdk/src/main/java/webtrekk/android/sdk/domain/external/SendAndClean.kt b/android-sdk/src/main/java/webtrekk/android/sdk/domain/external/SendAndClean.kt index 5cc3ea3..767279c 100644 --- a/android-sdk/src/main/java/webtrekk/android/sdk/domain/external/SendAndClean.kt +++ b/android-sdk/src/main/java/webtrekk/android/sdk/domain/external/SendAndClean.kt @@ -28,6 +28,8 @@ package webtrekk.android.sdk.domain.external import android.content.Context import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import webtrekk.android.sdk.Config import webtrekk.android.sdk.core.Scheduler import webtrekk.android.sdk.domain.ExternalInteractor @@ -42,12 +44,14 @@ internal class SendAndClean( private val scheduler: Scheduler ) : ExternalInteractor { - private val _job = Job() + private val _job = SupervisorJob() override val scope = CoroutineScope(_job + coroutineContext) // Starting a new job with context of the parent. override fun invoke(invokeParams: Params, coroutineDispatchers: CoroutineDispatchers) { - scheduler.sendRequestsThenCleanUp() + scope.launch { + scheduler.sendRequestsThenCleanUp() + } } /** diff --git a/android-sdk/src/main/java/webtrekk/android/sdk/domain/worker/CleanUpWorker.kt b/android-sdk/src/main/java/webtrekk/android/sdk/domain/worker/CleanUpWorker.kt index 6bd013e..4d90920 100644 --- a/android-sdk/src/main/java/webtrekk/android/sdk/domain/worker/CleanUpWorker.kt +++ b/android-sdk/src/main/java/webtrekk/android/sdk/domain/worker/CleanUpWorker.kt @@ -27,8 +27,11 @@ package webtrekk.android.sdk.domain.worker import android.content.Context import androidx.work.CoroutineWorker +import androidx.work.WorkManager import androidx.work.WorkerParameters import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import webtrekk.android.sdk.Webtrekk import webtrekk.android.sdk.WebtrekkConfiguration @@ -48,51 +51,46 @@ internal class CleanUpWorker( workerParameters: WorkerParameters ) : CoroutineWorker(context, workerParameters) { + /** + * [coroutineDispatchers] the injected coroutine dispatchers. + */ + val coroutineDispatchers: CoroutineDispatchers = AppModule.dispatchers override suspend fun doWork(): Result = coroutineScope { - // this check and initialization is needed for cross platform solutions - if (!Webtrekk.getInstance().isInitialized()) { - val configJson = WebtrekkSharedPrefs(applicationContext).configJson - val config = WebtrekkConfiguration.fromJson(configJson) - Webtrekk.getInstance().init(applicationContext, config) - } - - /** - * [coroutineDispatchers] the injected coroutine dispatchers. - */ - /** - * [coroutineDispatchers] the injected coroutine dispatchers. - */ - val coroutineDispatchers: CoroutineDispatchers = AppModule.dispatchers + withContext(coroutineDispatchers.ioDispatcher) { + mutex.withLock { + /** + * [logger] the injected logger from Webtrekk. + */ + val logger by lazy { AppModule.logger } - /** - * [getCachedDataTracks] the injected internal interactor for getting the data from the data base. - */ - /** - * [getCachedDataTracks] the injected internal interactor for getting the data from the data base. - */ - val getCachedDataTracks: GetCachedDataTracks = InteractorModule.getCachedDataTracks() + logger.debug("doWork - starting... ${tags.joinToString(separator = ", ")}") + // this check and initialization is needed for cross platform solutions + if (!Webtrekk.getInstance().isInitialized()) { + val configJson = WebtrekkSharedPrefs(applicationContext).configJson + val config = WebtrekkConfiguration.fromJson(configJson) + Webtrekk.getInstance().init(applicationContext, config) + } - /** - * [clearTrackRequests] the injected internal interactor for deleting the data in the data base. - */ - /** - * [clearTrackRequests] the injected internal interactor for deleting the data in the data base. - */ - val clearTrackRequests: ClearTrackRequests = InteractorModule.clearTrackRequest() + /** + * [getCachedDataTracks] the injected internal interactor for getting the data from the data base. + */ + val getCachedDataTracks: GetCachedDataTracks = + InteractorModule.getCachedDataTracks() - /** - * [logger] the injected logger from Webtrekk. - */ - /** - * [logger] the injected logger from Webtrekk. - */ - val logger by lazy { AppModule.logger } + /** + * [clearTrackRequests] the injected internal interactor for deleting the data in the data base. + */ + val clearTrackRequests: ClearTrackRequests = InteractorModule.clearTrackRequest() - // get the data from the data base with state DONE only. - withContext(coroutineDispatchers.ioDispatcher) { - getCachedDataTracks(GetCachedDataTracks.Params(requestStates = listOf(TrackRequest.RequestState.DONE))) - .onSuccess { dataTracks -> + // get the data from the data base with state DONE only. + getCachedDataTracks( + GetCachedDataTracks.Params( + requestStates = listOf( + TrackRequest.RequestState.DONE + ) + ) + ).onSuccess { dataTracks -> if (dataTracks.isNotEmpty()) { logger.info("Cleaning up the completed requests") @@ -102,13 +100,15 @@ internal class CleanUpWorker( logger.error("Failed while cleaning up the completed requests: $it") } } - } - .onFailure { logger.error("Error getting the cached completed requests: $it") } + }.onFailure { logger.error("Error getting the cached completed requests: $it") } + + return@withContext Result.success() + } } - Result.success() } companion object { + val mutex = Mutex() const val TAG = "clean_up" } } diff --git a/android-sdk/src/main/java/webtrekk/android/sdk/domain/worker/SendRequestsWorker.kt b/android-sdk/src/main/java/webtrekk/android/sdk/domain/worker/SendRequestsWorker.kt index 433f1c6..f78be20 100644 --- a/android-sdk/src/main/java/webtrekk/android/sdk/domain/worker/SendRequestsWorker.kt +++ b/android-sdk/src/main/java/webtrekk/android/sdk/domain/worker/SendRequestsWorker.kt @@ -26,15 +26,14 @@ package webtrekk.android.sdk.domain.worker import android.content.Context -import android.util.Log import androidx.work.CoroutineWorker import androidx.work.WorkManager import androidx.work.WorkerParameters import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import webtrekk.android.sdk.BuildConfig import webtrekk.android.sdk.Webtrekk import webtrekk.android.sdk.WebtrekkConfiguration import webtrekk.android.sdk.data.WebtrekkSharedPrefs @@ -60,15 +59,20 @@ internal class SendRequestsWorker( ) : CoroutineWorker(context, workerParameters) { override suspend fun doWork(): Result = coroutineScope { + /** + * [logger] the injected logger from Webtrekk. + */ + val logger by lazy { AppModule.logger } + withContext(AppModule.dispatchers.ioDispatcher) { mutex.withLock { - Log.d(TAG, "doWork - starting... ${tags.joinToString(separator = ", ")}") + logger.debug("doWork - starting... ${tags.joinToString(separator = ", ")}") // this check and initialization is needed for cross platform solutions if (!Webtrekk.getInstance().isInitialized()) { val configJson = WebtrekkSharedPrefs(applicationContext).configJson val config = WebtrekkConfiguration.fromJson(configJson) Webtrekk.getInstance().init(applicationContext, config) - Log.d(TAG, "doWork - initialized!") + logger.debug("doWork - initialized!") } /** @@ -88,11 +92,6 @@ internal class SendRequestsWorker( val executePostRequest: ExecutePostRequest = InteractorModule.executePostRequest() - /** - * [logger] the injected logger from Webtrekk. - */ - val logger by lazy { AppModule.logger } - val activeConfig = Webtrekk.getInstance().getCurrentConfiguration() // retrieves the data in the data base with state of NEW or FAILED only. @@ -143,15 +142,11 @@ internal class SendRequestsWorker( } } } - - if (BuildConfig.DEBUG) { - logger.debug(activeConfig.printUsageStatisticCalculation()) - } } .onFailure { logger.error("Error getting cached data tracks: $it") } logger.debug("SEND WORKER - END ${this@SendRequestsWorker}") + return@withContext Result.success() } - return@withContext Result.success() } } diff --git a/sample/build.gradle b/sample/build.gradle index efe8a21..efba56a 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -150,11 +150,11 @@ dependencies { implementation('com.google.firebase:firebase-messaging') implementation("com.google.firebase:firebase-crashlytics") - implementation('com.mapp.sdk:mapp-android:6.0.24') + implementation('com.mapp.sdk:mapp-android:6.0.25') - //implementation(project(':android-sdk')) + implementation(project(':android-sdk')) - implementation("com.mapp.sdk:intelligence-android:5.1.9") + //implementation("com.mapp.sdk:intelligence-android:5.1.10") //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' diff --git a/sample/google-services.json b/sample/google-services.json index 763ab9d..36751fb 100644 --- a/sample/google-services.json +++ b/sample/google-services.json @@ -1,34 +1,295 @@ { "project_info": { - "project_number": "357810879619", - "firebase_url": "https://webtrekk-mobile.firebaseio.com", - "project_id": "webtrekk-mobile", - "storage_bucket": "webtrekk-mobile.appspot.com" + "project_number": "1028993954364", + "firebase_url": "https://test-52c43.firebaseio.com", + "project_id": "test-52c43", + "storage_bucket": "test-52c43.appspot.com" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:357810879619:android:472bc0eee7bfaf90d4afb6", + "mobilesdk_app_id": "1:1028993954364:android:b7e199762c30a4f4", + "android_client_info": { + "package_name": "com.appoxee.example" + } + }, + "oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCGr1nIm1vMo3iWZvKHYjAU9h-jLPL5Uyg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1028993954364:android:3e13d76c79b42e32", + "android_client_info": { + "package_name": "com.example" + } + }, + "oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCGr1nIm1vMo3iWZvKHYjAU9h-jLPL5Uyg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1028993954364:android:78142f013641efa7164939", "android_client_info": { "package_name": "com.example.webtrekk.androidsdk" } }, "oauth_client": [ { - "client_id": "357810879619-kvto1fdm1gct8n3s0m73qndlelvufpl9.apps.googleusercontent.com", + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCGr1nIm1vMo3iWZvKHYjAU9h-jLPL5Uyg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1028993954364:android:4d2db66f8c189413164939", + "android_client_info": { + "package_name": "com.mapp.engagesample" + } + }, + "oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCGr1nIm1vMo3iWZvKHYjAU9h-jLPL5Uyg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1028993954364:android:4932cf9ad7039b61164939", + "android_client_info": { + "package_name": "com.mapp.flutter.example" + } + }, + "oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCGr1nIm1vMo3iWZvKHYjAU9h-jLPL5Uyg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1028993954364:android:8ea4e52b021064f8164939", + "android_client_info": { + "package_name": "com.mapp.flutter.sdk" + } + }, + "oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCGr1nIm1vMo3iWZvKHYjAU9h-jLPL5Uyg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1028993954364:android:6235ef07862e0b91164939", + "android_client_info": { + "package_name": "com.mapp.rn" + } + }, + "oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCGr1nIm1vMo3iWZvKHYjAU9h-jLPL5Uyg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1028993954364:android:3505d4dbd547c6a4", + "android_client_info": { + "package_name": "com.mappp.MappAndroidSDKTest" + } + }, + "oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCGr1nIm1vMo3iWZvKHYjAU9h-jLPL5Uyg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1028993954364:android:720b0c3abbaac128", + "android_client_info": { + "package_name": "example.mapp.com.androidexample" + } + }, + "oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCGr1nIm1vMo3iWZvKHYjAU9h-jLPL5Uyg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1028993954364:android:5312ec78e9232c91", + "android_client_info": { + "package_name": "mapp.cordova.cep" + } + }, + "oauth_client": [ + { + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { - "current_key": "AIzaSyDyuH5fywqzRI-oKl1GWDkO6v8aJJQuRCk" + "current_key": "AIzaSyCGr1nIm1vMo3iWZvKHYjAU9h-jLPL5Uyg" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [ { - "client_id": "357810879619-kvto1fdm1gct8n3s0m73qndlelvufpl9.apps.googleusercontent.com", + "client_id": "1028993954364-cggcnd9g225dftb71eeqik5ribahjf1j.apps.googleusercontent.com", "client_type": 3 } ] diff --git a/sample/src/main/java/com/example/webtrekk/androidsdk/WorkSchedulerTest.kt b/sample/src/main/java/com/example/webtrekk/androidsdk/WorkSchedulerTest.kt index c391d76..a2740f4 100644 --- a/sample/src/main/java/com/example/webtrekk/androidsdk/WorkSchedulerTest.kt +++ b/sample/src/main/java/com/example/webtrekk/androidsdk/WorkSchedulerTest.kt @@ -7,17 +7,26 @@ import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.lifecycleScope import androidx.work.Constraints import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import com.example.webtrekk.androidsdk.databinding.ActivityWorkSchedulerTestBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import webtrekk.android.sdk.Webtrekk +import java.util.concurrent.TimeUnit class WorkSchedulerTest : AppCompatActivity() { @@ -38,10 +47,13 @@ class WorkSchedulerTest : AppCompatActivity() { binding.btnScheduleWorks.setOnClickListener { val workCount = binding.etWorkCount.text.toString().toIntOrNull() ?: 0 if (workCount > 0) { - for (i in 0 until workCount) { - Webtrekk.getInstance().sendRequestsNowAndClean() - //MyWorker.enqueue(this) - Log.d(TAG, "SCHEDULED WORK: ${i + 1}") + lifecycleScope.launch(Dispatchers.IO) { + for (i in 0 until workCount) { + //Webtrekk.getInstance().sendRequestsNowAndClean() + MyWorker.enqueue(this@WorkSchedulerTest, i + 1) + //MyWorker.enqueuePeriodic(this@WorkSchedulerTest, i + 1) + Log.d(TAG, "SCHEDULED WORK(s): ${i + 1}") + } } } } @@ -54,21 +66,69 @@ internal class MyWorker(context: Context, workerParameters: WorkerParameters) : private val dispatcher = Dispatchers.IO override suspend fun doWork(): Result = coroutineScope { return@coroutineScope withContext(dispatcher) { - Log.d(TAG, "WORK STARTED - ${id}") - delay(5000) - Result.success() + mutex.withLock { + val workNumber = inputData.getInt("workNumber", 0) + Log.d(TAG, "WORK STARTED - $id") + delay(5000) + val result = if (workNumber % 3 == 0) Result.failure() else Result.success() + Log.d(TAG, "WORK FINISHED - $id - Returning $result") + result + } } } companion object { - fun enqueue(context: Context) { - val constraints = Constraints.Builder() - .setRequiresCharging(true) - .build() - val work = OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .build() - WorkManager.getInstance(context).enqueue(work) + val mutex = Mutex() + val workName = "my-worker" + val periodicWorkName = "my-worker-periodic" + fun enqueue(context: Context, workNumber: Int) { + val workManager = WorkManager.getInstance(context) + val currentWork = + workManager.getWorkInfosForUniqueWork(workName).get().firstOrNull() + if (currentWork == null || currentWork.state != WorkInfo.State.RUNNING) { + val constraints = Constraints.Builder() + .setRequiresCharging(true) + .build() + val inputData = Data.Builder() + .putInt("workNumber", workNumber) + .build() + val work = OneTimeWorkRequestBuilder() + .setInitialDelay(0, TimeUnit.SECONDS) + .setConstraints(constraints) + .setInputData(inputData) + .build() + workManager + .enqueueUniqueWork( + workName, + ExistingWorkPolicy.APPEND_OR_REPLACE, + work + ) + } + } + + fun enqueuePeriodic(context: Context, workNumber: Int) { + val workManager = WorkManager.getInstance(context) + val currentWork = + workManager.getWorkInfosForUniqueWork(periodicWorkName).get().firstOrNull() + if (currentWork == null || currentWork.state != WorkInfo.State.RUNNING) { + val constraints = Constraints.Builder() + .setRequiresCharging(true) + .build() + val inputData = Data.Builder() + .putInt("workNumber", workNumber) + .build() + val work = PeriodicWorkRequestBuilder(15, TimeUnit.SECONDS) + .setInitialDelay(0, TimeUnit.SECONDS) + .setConstraints(constraints) + .setInputData(inputData) + .build() + workManager + .enqueueUniquePeriodicWork( + periodicWorkName, + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + work + ) + } } } } \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_work_scheduler_test.xml b/sample/src/main/res/layout/activity_work_scheduler_test.xml index d31c338..8d46c59 100644 --- a/sample/src/main/res/layout/activity_work_scheduler_test.xml +++ b/sample/src/main/res/layout/activity_work_scheduler_test.xml @@ -14,7 +14,9 @@ style="@style/Widget.MaterialComponents.TextInputEditText.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="Number of works"/> + android:text="1" + android:hint="Number of works" + tools:ignore="HardcodedText" />