I'm trying to run a task in backgroung (do not respond with an existing plug-in),
but on the first run I get this error "-32601 received from application: Method not found"
step by step
I have a method that sends, via MethodChannel, to the native a handle
class Backgroundtask {
static const MethodChannel _channel = const MethodChannel('backgroundtask');
static Future<bool> periodic({#required Function callback}) async {
assert(callback != null, 'task cannot be null');
CallbackHandle handle = PluginUtilities.getCallbackHandle(callback);
if (handle == null) {
return false;
}
final bool result = await _channel.invokeMethod(
'periodic',
<dynamic>[
handle.toRawHandle(),
],
);
return result ?? false;
}
}
on the other side, I start a WorkerManager, passing the handle as a Data.
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
[...]
val data = Data.Builder()
data.putLong("handle", handle)
val constrains = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val pWorkRequest = PeriodicWorkRequest
.Builder(CustomWorker::class.java, 15, TimeUnit.MINUTES)
.setInputData(data.build())
.setConstraints(constrains)
.build()
WorkManager.getInstance(context).enqueue(pWorkRequest)
[...]
}
My androidx.work.Worker.doWork()
[...]
Handler(Looper.getMainLooper()).post {
Log.d("periodic", "background service start")
val handle = inputData.getLong("handle", 0)
val flutterEngine = FlutterEngine(context)
val flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(handle)
if (flutterCallback.callbackName == null) {
Log.e("periodic", "callback not found")
return
}
val assets = context.assets
val bundle = FlutterMain.findAppBundlePath()
val executor = flutterEngine.dartExecutor
val dartCallback = DartExecutor.DartCallback(assets, bundle, flutterCallback)
executor.executeDartCallback(dartCallback)
Log.d("periodic", "background service end")
}
[...]
my error occurs after the callback is executed, regardless of what it does.
I think it must be something stupid that I'm not seeing, but my head is broken.
Thank you very much in advance.
error image
Related
I'm doing a small project to learn flow and the latest Android features, and I'm currently facing the viewModel's testing, which I don't know if I'm performing correctly. can you help me with it?
Currently, I am using a use case to call the repository which calls a remote data source that gets from an API service a list of strings.
I have created a State to control the values in the view model:
data class StringItemsState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String = ""
)
and the flow:
private val stringItemsState = StringtemsState()
private val _stateFlow = MutableStateFlow(stringItemsState)
val stateFlow = _stateFlow.asStateFlow()
and finally the method that performs all the logic in the viewModel:
fun fetchStringItems() {
try {
_stateFlow.value = stringItemsState.copy(isLoading = true)
viewModelScope.launch(Dispatchers.IO) {
val result = getStringItemsUseCase.execute()
if (result.isEmpty()) {
_stateFlow.value = stringItemsState
} else {
_stateFlow.value = stringItemsState.copy(items = result)
}
}
} catch (e: Exception) {
e.localizedMessage?.let {
_stateFlow.value = stringItemsState.copy(error = it)
}
}
}
I am trying to perform the test following the What / Where / Then pattern, but the result is always an empty list and the assert verification always fails:
private val stringItems = listOf<String>("A", "B", "C")
#Test
fun `get string items - not empty`() = runBlocking {
// What
coEvery {
useCase.execute()
} returns stringItems
// Where
viewModel.fetchStringItems()
// Then
assert(viewModel.stateFlow.value.items == stringItems)
coVerify(exactly = 1) { viewModel.fetchStringItems() }
}
Can someone help me and tell me if I am doing it correctly? Thanks.
I am new to test cases and I am trying to write test cases for the below code but I did not get success after trying several method. My main target is to cover code coverage of MaintenanceStatusResponseHandler.kt class. I am using mockito to write the test cases. I am already implemented jococo for code coverage but I am facing some issue to write a test cases. Please help me to write test cases of MaintenanceStatusResponseHandler class
Thanks in advance
internal class MaintenanceStatusResponseHandler {
public fun getMaintenanceResponse(voiceAiConfig : VoiceAiConfig):MaintenanceStatus{
val maintenanceStatus = MaintenanceStatus()
val retrofitRepository = RetrofitRepository()
val maintenanceUrl : String
val jwtToken : String
when (voiceAiConfig.server) {
BuildConfig.ENV_PRODUCTION_SERVER -> {
jwtToken = BuildConfig.JWT_TOKEN_PRODUCTION
maintenanceUrl = BuildConfig.MAINTENANCE_PROD_URL
}
BuildConfig.ENV_STAGING_SERVER -> {
jwtToken = BuildConfig.JWT_TOKEN_STAGING
maintenanceUrl = BuildConfig.MAINTENANCE_SANDBOX_URL
}
else -> {
jwtToken = BuildConfig.JWT_TOKEN_SANDBOX
maintenanceUrl = BuildConfig.MAINTENANCE_SANDBOX_URL
}
}
val header = "${VoiceAISDKConstant.JWT_TOKEN_PREFIX} $jwtToken"
retrofitRepository.getRetrofit(maintenanceUrl)
.getMaintenanceStatus(header)
.subscribe { response: MaintenanceStatus.Content, error: Throwable? ->
error.let {
if (error != null) {
maintenanceStatus.error = error
}
}
response.let {
maintenanceStatus.content = response
}
}
return maintenanceStatus
}
}
repository class
class RetrofitRepository() {
val TAG = RetrofitRepository::class.java.canonicalName
fun getRetrofit(baseUrl: String?): VoiceAiServices {
val voiceAiServices: VoiceAiServices = Retrofit.Builder()
.baseUrl(baseUrl!!)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build().create(VoiceAiServices::class.java)
return voiceAiServices
}
}
interface
interface VoiceAiServices {
#GET("/v1/api/status")
fun getMaintenanceStatus(#Header("Authorization")header: String): Single<MaintenanceStatus.Content>
}
Pojo class
data class MaintenanceStatus(
var error: Throwable? = null,
var content: Content? = null
) {
data class Content(
val enabled: Boolean,
val maintenanceMsg: String
)
}
I am testing this code and I got success in the success response code but I am not able to reach the onError code
internal class MaintenanceStatusResponseHandler {
fun getMaintenanceResponse (voiceAiServices: VoiceAiServices, header: String): MaintenanceStatus {
val maintenanceStatus = MaintenanceStatus()
voiceAiServices.getMaintenanceStatus(header)
.subscribe { response: MaintenanceStatus.Content, error: Throwable? ->
error.let {
if (error != null) {
maintenanceStatus.error = error
}
}
response.let {
maintenanceStatus.content = response
}
}
return maintenanceStatus
}
}
repository class
class RetrofitRepository() {
val TAG = RetrofitRepository::class.java.canonicalName
fun getRetrofit(baseUrl: String?): VoiceAiServices {
val voiceAiServices: VoiceAiServices = Retrofit.Builder()
.baseUrl(baseUrl!!)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build().create(VoiceAiServices::class.java)
return voiceAiServices
}
}
interface
interface VoiceAiServices {
#GET("/v1/api/status")
fun getMaintenanceStatus(#Header("Authorization")header: String): Single<MaintenanceStatus.Content>
}
Pojo class
data class MaintenanceStatus(
var error: Throwable? = null,
var content: Content? = null
) {
data class Content(
val enabled: Boolean,
val maintenanceMsg: String
)
}
Below test code for Success response which is working fine
fun mockedObservableErrorSuccess(): Single<MaintenanceStatus.Content> {
return Single.create { e ->
e.onSuccess(MaintenanceStatus.Content(true, "Under error maintenance"))
e.onError(Throwable("Error message"))
}
}
#Test
fun testMaintenanceSuccessResponse() {
val voiceAiService: VoiceAiServices = mock(VoiceAiServices::class.java)
val maintenanceStatusHandler = MaintenanceStatusResponseHandler()
val content = MaintenanceStatus.Content(true, "Under maintenance")
`when`(voiceAiService.getMaintenanceStatus(Utilities.getHeader(voiceAIConfig))).thenReturn(just(content))
val testObserver: TestObserver<MaintenanceStatus.Content> = TestObserver.create()
val observer = mockedObservableErrorSuccess()
observer.subscribe(testObserver)
maintenanceStatusHandler.getMaintenanceResponse(voiceAiService, "header")
testObserver.awaitTerminalEvent()
testObserver.onComplete()
}
Below is code that is not able to reach to Error method and for this code I need help
I really appreciate any help you can provide.
#Test(expected = java.lang.Exception::class)
fun testMaintenanceErrorResponse() {
val voiceAiService: VoiceAiServices = mock(VoiceAiServices::class.java)
val maintenanceStatusHandler = MaintenanceStatusResponseHandler()
`when`(voiceAiService.getMaintenanceStatus(Utilities.getHeader(voiceAIConfig))).thenReturn(error(Throwable("Error message")))
val testObserver: TestObserver<MaintenanceStatus.Content> = TestObserver.create()
val observer = mockedObservableErrorSuccess()
observer.subscribe(testObserver)
maintenanceStatusHandler.getMaintenanceResponse(voiceAiService, Utilities.getHeader(voiceAIConfig))
testObserver.awaitTerminalEvent()
testObserver.assertError(Throwable::class.java)
// testObserver.onError(Throwable()) //Also tried this method
}
Caused by: java.lang.AssertionError: No errors (latch = 0, values = 1, errors = 0, completions = 1)
So I have an Android Job which handle network request.
This job can started in many ways, so it can run easily parralel, which is bad for me.
I would like to achive that, the job don't started twice, or if it started, than wait before the try catch block, until the first finishes.
So how can I to reach that, just only one object be/run at the same time.
I tried add TAG and setUpdateCurrent false, but it didn't do anything, so when I started twice the job, it rung parallel. After that I tried mutex lock, and unlock. But it did the same thing.
With mutex, I should have to create an atomic mutex, and call lock by uniq tag or uuid?
Ok, so I figured it out what is the problem with my mutex, the job create a new mutex object every time, so it never be the same, and it never will wait.
My Job:
class SendCertificatesJob #Inject constructor(
private val sendSync: SendSync,
private val sharedPreferences: SharedPreferences,
private val userLogger: UserLogger,
private val healthCheckApi: HealthCheckApi
) : Job(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO
private val countDownLatch = CountDownLatch(1)
private val mutex = Mutex()
override fun onRunJob(params: Params): Result {
var jobResult = Result.SUCCESS
if (!CommonUtils.isApiEnabled(context))
return jobResult
val notificationHelper = NotificationHelper(context)
var nb: NotificationCompat.Builder? = null
if (!params.isPeriodic) {
nb = notificationHelper.defaultNotificationBuilder.apply {
setContentTitle(context.resources.getString(R.string.sending_certificates))
.setTicker(context.resources.getString(R.string.sending_certificates))
.setOngoing(true)
.setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
R.mipmap.ic_launcher
)
)
}
notificationHelper.notify(NOTIFICATION_ID, nb)
jobCallback?.jobStart()
}
val failureCount = params.failureCount
if (failureCount >= 3) {
nb?.setOngoing(false)
?.setContentTitle(context.resources.getString(R.string.sending_certificates_failed))
?.setTicker(context.resources.getString(R.string.sending_certificates_failed))
?.setProgress(100, 100, false)
?.setSmallIcon(android.R.drawable.stat_sys_warning)
notificationHelper.notify(NOTIFICATION_ID, nb)
return Result.FAILURE
}
GlobalScope.launch(Dispatchers.IO) {
mutex.lock()
userLogger.writeLogToFile("SendCertificatesJob.onRunJob(), date:" + Calendar.getInstance().time)
try {
//Test
var doIt = true
var count =0
while (doIt){
Timber.d("SendSyncWorker: $count")
count++
delay(10000)
if(count == 12)
doIt = false
}
healthCheckApi.checkHealth(ApiModule.API_KEY).await()
try {
sendSync.syncRecordedClients()
} catch (e: Exception) {
e.printStackTrace()
}
val result = sendSync().forEachParallel2()
result.firstOrNull { it.second != null }?.let { throw Exception(it.second) }
val sb = StringBuilder()
if (nb != null) {
nb.setOngoing(false)
.setContentTitle(context.resources.getString(R.string.sending_certificates_succeeded))
.setTicker(context.resources.getString(R.string.sending_certificates_succeeded))
.setProgress(100, 100, false)
.setStyle(NotificationCompat.BigTextStyle().bigText(sb.toString()))
.setSmallIcon(android.R.drawable.stat_notify_sync_noanim)
notificationHelper.notify(NOTIFICATION_ID, nb)
jobCallback?.jobEnd()
}
sharedPreferences.edit().putLong(KEY_LATEST_CERTIFICATES_SEND_DATE, Date().time)
.apply()
} catch (e: Exception) {
Timber.tag(TAG).e(e)
if (nb != null) {
nb.setOngoing(false)
.setContentTitle(context.resources.getString(R.string.sending_certificates_failed))
.setTicker(context.resources.getString(R.string.sending_certificates_failed))
.setProgress(100, 100, false)
.setSmallIcon(android.R.drawable.stat_sys_warning)
notificationHelper.notify(NOTIFICATION_ID, nb)
jobCallback?.jobEnd()
}
jobResult = Result.RESCHEDULE
} finally {
countDownLatch.countDown()
mutex.unlock()
}
}
countDownLatch.await()
return jobResult
}
Job schedule:
fun scheduleNowAsync(_jobCallback: JobCallback? = null) {
jobCallback = _jobCallback
JobRequest.Builder(TAG_NOW)
.setExecutionWindow(1, 1)
.setBackoffCriteria(30000, JobRequest.BackoffPolicy.LINEAR)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build()
.scheduleAsync()
}
fun schedulePeriodicAsync() {
jobCallback = null
JobRequest.Builder(TAG)
.setPeriodic(900000)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build()
.scheduleAsync()
}
I found the solution for my problem.
So because I use dagger, I provided a singleton Mutex object, and injected into the job. When the job starts call mutex.lock(), and beacuse there is only 1 object from the mutex, even if another job starts, the second job will waite until the firsjob is done.
By using LiveData's latest version "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03", I have developed a code for a feature called "Search Products" in the ViewModel using LiveData's new building block (LiveData + Coroutine) that performs a synchronous network call using Retrofit and update different flags (isLoading, isError) in ViewModel accordingly. I am using Transforamtions.switchMap on "query" LiveData so whenever there is a change in "query" from the UI, the "Search Products" code starts its executing using Transformations.switchMap. Every thing is working fine, except that i want to cancel the previous Retrofit Call whenever a change happens in "query" LiveData. Currently i can't see any way to do this. Any help would be appreciated.
class ProductSearchViewModel : ViewModel() {
val completableJob = Job()
private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)
// Query Observable Field
val query: MutableLiveData<String> = MutableLiveData()
// IsLoading Observable Field
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
val products: LiveData<List<ProductModel>> = query.switchMap { q ->
liveData(context = coroutineScope.coroutineContext) {
emit(emptyList())
_isLoading.postValue(true)
val service = MyApplication.getRetrofitService()
val response = service?.searchProducts(q)
if (response != null && response.isSuccessful && response.body() != null) {
_isLoading.postValue(false)
val body = response.body()
if (body != null && body.results != null) {
emit(body.results)
}
} else {
_isLoading.postValue(false)
}
}
}
}
You can solve this problem in two ways:
Method # 1 ( Easy Method )
Just like Mel has explained in his answer, you can keep a referece to the job instance outside of switchMap and cancel instantance of that job right before returning your new liveData in switchMap.
class ProductSearchViewModel : ViewModel() {
// Job instance
private var job = Job()
val products = Transformations.switchMap(_query) {
job.cancel() // Cancel this job instance before returning liveData for new query
job = Job() // Create new one and assign to that same variable
// Pass that instance to CoroutineScope so that it can be cancelled for next query
liveData(CoroutineScope(job + Dispatchers.IO).coroutineContext) {
// Your code here
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
Method # 2 ( Not so clean but self contained and reusable)
Since liveData {} builder block runs inside a coroutine scope, you can use a combination of CompletableDeffered and coroutine launch builder to suspend that liveData block and observe query liveData manually to launch jobs for network requests.
class ProductSearchViewModel : ViewModel() {
private val _query = MutableLiveData<String>()
val products: LiveData<List<String>> = liveData {
var job: Job? = null // Job instance to keep reference of last job
// LiveData observer for query
val queryObserver = Observer<String> {
job?.cancel() // Cancel job before launching new coroutine
job = GlobalScope.launch {
// Your code here
}
}
// Observe query liveData here manually
_query.observeForever(queryObserver)
try {
// Create CompletableDeffered instance and call await.
// Calling await will suspend this current block
// from executing anything further from here
CompletableDeferred<Unit>().await()
} finally {
// Since we have called await on CompletableDeffered above,
// this will cause an Exception on this liveData when onDestory
// event is called on a lifeCycle . By wrapping it in
// try/finally we can use this to know when that will happen and
// cleanup to avoid any leaks.
job?.cancel()
_query.removeObserver(queryObserver)
}
}
}
You can download and test run both of these methods in this demo project
Edit: Updated Method # 1 to add job cancellation on onCleared method as pointed out by yasir in comments.
Retrofit request should be cancelled when parent scope is cancelled.
class ProductSearchViewModel : ViewModel() {
val completableJob = Job()
private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)
/**
* Adding job that will be used to cancel liveData builder.
* Be wary - after cancelling, it'll return a new one like:
*
* ongoingRequestJob.cancel() // Cancelled
* ongoingRequestJob.isActive // Will return true because getter created a new one
*/
var ongoingRequestJob = Job(coroutineScope.coroutineContext[Job])
get() = if (field.isActive) field else Job(coroutineScope.coroutineContext[Job])
// Query Observable Field
val query: MutableLiveData<String> = MutableLiveData()
// IsLoading Observable Field
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
val products: LiveData<List<ProductModel>> = query.switchMap { q ->
liveData(context = ongoingRequestJob) {
emit(emptyList())
_isLoading.postValue(true)
val service = MyApplication.getRetrofitService()
val response = service?.searchProducts(q)
if (response != null && response.isSuccessful && response.body() != null) {
_isLoading.postValue(false)
val body = response.body()
if (body != null && body.results != null) {
emit(body.results)
}
} else {
_isLoading.postValue(false)
}
}
}
}
Then you need to cancel ongoingRequestJob when you need to. Next time liveData(context = ongoingRequestJob) is triggered, since it'll return a new job, it should run without problems. All you need to left is cancel it where you need to, i.e. in query.switchMap function scope.