Android AppWidget using WorkManager experiencing network issues - android

I have an application in production that contains an AppWidget that schedules periodic refreshes via a WorkManager CoroutineWorker. My RefreshWidgetWorker simply requests some data from a Retrofit service & then persists that data via a Room DAO. I am receiving hundreds of crash logs of network connectivity issues & reports from users that the widget will not load for them. The exception is typically:
java.net.UnknownHostException: Unable to resolve host "[my-server]": No address associated with hostname
Although I've also seen:
java.net.SocketTimeoutException: failed to connect to (port ###) from (port #####) after ####
The flow is as follows:
widget onUpdate() gets called (every hour) -> check X duration since last refresh -> schedule RefreshWidgetWorker -> request from remote api -> display widget view -> persist cached data
My AppWidgetProvider (paraphrasing) looks like the following:
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
GlobalScope.launch {
if (timeSinceLastRefresh >= refreshThreshold) {
val initialWorkRequest = OneTimeWorkRequest.Builder(RefreshWidgetWorker::class.java).setInputData(/*requestData*/).build()
WorkManager.getInstance(context).enqueueUniqueWork(
REFRESH_WIDGET_WORK_NAME, ExistingWorkPolicy.KEEP, initialWorkRequest
)
}
}
}
My RefreshWidgetWorker looks like:
class RefreshWidgetWorker(val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams), KoinComponent {
private val dataService: DataService by inject()
override suspend fun doWork(): Result {
val d = dataService.getData()
val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
setTextViewText(R.id.textView, d.item)
}
AppWidgetManager.getInstance(context).updateAppWidget(widgetId, views)
cache.put(d)
return Result.success()
}
And DataService is just a simple Retrofit service:
Retrofit.Builder()
.baseUrl(Constants.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.client(
OkHttpClient.Builder().connectTimeout(2, TimeUnit.SECONDS)
.readTimeout(2, TimeUnit.SECONDS).writeTimeout(2, TimeUnit.SECONDS).build()
)
.build()
.create(DataService::class.java)
I've been unable to reproduce this issue on my own device & am unsure if it has to do with spotty mobile networks or a system optimization that is preventing my requests from going through. I'd appreciate any insight anyone might be able to provide as I have been stumped on this issue for a while.

Related

Android WorkManager - Can I pass input data to a Worker that runs periodically?

In my app I start a WebSocketWorker tasks that runs periodically every 15 minutes. As the name implies, it contains a WebSocket for listening to a socket in the background:
// MainApplication.kt
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(DebugTree())
}
val work = PeriodicWorkRequestBuilder<WebSocketWorker>(15, TimeUnit.MINUTES).build()
workManager.enqueueUniquePeriodicWork("UniqueWebSocketWorker", ExistingPeriodicWorkPolicy.KEEP, work)
}
The WebSocketWorker contains the following logic:
#HiltWorker
class WebSocketWorker #AssistedInject constructor(
#Assisted appContext: Context,
#Assisted workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
inner class MyWebSocketListener : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
Timber.d("The message sent is %s", text)
// do sth. with the message
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
t.localizedMessage?.let { Timber.e("onFailure: %s", it) }
response?.message?.let { Timber.e("onFailure: %s", it) }
}
}
override suspend fun doWork(): Result {
try{
// code to be executed
val request = Request.Builder().url("ws://***.***.**.***:8000/ws/chat/lobby/").build()
val myWebSocketListener = MyWebSocketListener()
val client = OkHttpClient()
client.newWebSocket(request, myWebSocketListener)
return Result.success()
}
catch (throwable:Throwable){
Timber.e("There is a failure")
Timber.e("throwable.localizedMessage: %s", throwable.localizedMessage)
// clean up and log
return Result.failure()
}
}
}
As you can see, in the Worker class I set the WebSocket and everything is fine. Listening to the socket works.
Now, I also want to add the "sending of messages" functionality to my app. How can I reuse the websocket created in WebSocketWorker? Can I pass input data to the WebSocketWorker that runs in the background ?
Let's say I have a EditText for typing the message and a Button to send the message with a setOnClickListener attached like this:
binding.sendButton.setOnClickListener {
// get message
val message = binding.chatMessageEditText.text.toString()
// check if not empty
if(message.isNotEmpty()) {
// HOW CAN I REUSE THE WEBSOCKET RUNNING PERIODICALLY IN THE BACKGROUND?
// CAN I PASS THE MESSAGE TO THAT WEBSOCKET ?
// OR SHOULD I CREATE A DIFFERENT WORKER FOR SENDING MESSAGES (e.g.: a OneTimeRequest<SendMessageWorker> for sending messages ?
}
}
From the documentation, I know that you need to build Data objects for passing inputs and so on but there was no example which showcased how to pass input to a worker running periodically in the background.
My experience is saying that you can. Basically you "can't" interact with the worker object via the API. It is really annoying.
For example, with the JS you have the option to get a job and check the parameters of the job. There is no such option with the work. For example, I want to check what is the current state of the restrictions - what is satisfied, what is not. Nothing like this. You can just check states, cancel and that is almost all.
My suggestions is that it is because the WorkManager is a "facade/adapter" over other libraries like JS. It has it's own DB to restore JS jobs on device restart and stuff like this, but beside that if you want to interact with the internals I guess it was just too complicated for them to do so they just skipped.
You can just inject some other object and every time the work can ask it for it's data. I don't see other option.

How can I download files in specific days? [ Kotlin Android Development ]

What is a safe and greater method to execute tasks ( in my case call a function ) in specific time ( in my case every two days ) ?
The function retrieves data from the Web. My target is update the data in app whenever it changes on the webserver.
Thanks, good job!
1- If you don't need to be exact about time, you need to use WorkManager and set periodic work request
https://developer.android.com/topic/libraries/architecture/workmanager
fun CreateWorkRequest()
{
val workManager = WorkManager.getInstance(context)
val duration = 2*24*60*60*1000L //2 days in mili second
val workRequest = PeriodicWorkRequestBuilder<AutoBackUpWorker>(duration, TimeUnit.MILLISECONDS)
.setInitialDelay(duration/2, TimeUnit.MILLISECONDS)
.addTag("diaryBackUpWork")
.build()
workManager.enqueueUniquePeriodicWork("backupwork", ExistingPeriodicWorkPolicy.REPLACE, autoBackUpRequest)
}
Worker Class:
class RequestedWorker(val appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams)
{
override fun doWork()
{
}
}
2- If you need to be exact about time (like at 8pm everyday) you need to use Alarm Manager
3- In your case the best choice is implementing DownloadManager for download request in the doWork() method

Periodic work requests still running even when constraints are not met

I'm using WorkManager API (version 2.4.0) to create simple periodic running tasks. Here is the worker class
class BackupWorker(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = coroutineScope {
Log.i(TAG, "Starting worker")
makeNotification(
"Preparing worker",
applicationContext
)
sleep() //simulate long running task
for(i in 0..20) {
makeNotification(
"Firing update $i",
applicationContext,
true
)
sleep()
}
makeNotification(
"Worker complete",
applicationContext
)
Log.i(TAG, "Finishing backup worker")
Result.success()
}
}
And the work request is set up as follows
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresStorageNotLow(true).build();
PeriodicWorkRequest workRequest =
new PeriodicWorkRequest.Builder(BackupWorker.class,
15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build();
WorkManager.getInstance(context).enqueueUniquePeriodicWork("tag_worker",
ExistingPeriodicWorkPolicy.REPLACE,
workRequest);
The work request is being correctly picked up by the OS. However, if I turn Wi-Fi off whilst running, it doesn't stop and continues to run what's inside doWork() which in fact contradicts the whole purpose of this API.
Is there anything missing here? Any thoughts?

Coroutines to call a REST API -> FATAL EXCEPTION : main

I need to fetch some data from a REST API, everything is ok when I'm 4G or wifi connected but when I'm in airplane mode, the app crashes with : "E/AndroidRuntime: FATAL EXCEPTION: main"
Before that I have a log (not an error saying : "Skipped 1013 frames! The application may be doing too much work on its main thread.")
So I suppose fetching the API with no network crashes the app because it's running in the main thread. BUT I'm using coroutines and to me, I'm doing it right :
ViewModel
private val viewModelJob = SupervisorJob()
private val viewModelScope = CoroutineScope(viewModelJob + Dispatchers.Main)
init {
viewModelScope.launch {
videosRepository.refreshVideos()
}
}
Repository
suspend fun refreshVideos() {
withContext(Dispatchers.IO) {
val playlist = Network.devbytes.getPlaylist().await()
//database.videoDao().insertAll(*playlist.asDatabaseModel())
}
}
Service
/**
* A retrofit service to fetch a devbyte playlist.
*/
interface DevbyteService {
#GET("devbytes.json")
fun getPlaylist(): Deferred<NetworkVideoContainer>
}
/**
* Build the Moshi object that Retrofit will be using, making sure to add the Kotlin adapter for
* full Kotlin compatibility.
*/
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
/**
* Main entry point for network access. Call like `Network.devbytes.getPlaylist()`
*/
object Network {
// Configure retrofit to parse JSON and use coroutines
private val retrofit = Retrofit.Builder()
.baseUrl("https://devbytes.udacity.com/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
val devbytes: DevbyteService = retrofit.create(DevbyteService::class.java)
}
So the complete chain is :
ViewModel -> coroutine with Dispatchers.Main
that calls Repository -> suspend function that launch a coroutine with Dispatchers.IO
that calls Service -> through object Network, I get a retrofit instance with a getPlaylist() that returns a Deferred, and the call to that method is in the repository with an await()
What am I doing wrong ?
Your API call is throwing an exception because there is no network connection (most likely UnknownHostException).
Wrap it in a try-catch and deal with the exception.
CoroutineExceptionHandler might be a solution. https://kotlinlang.org/docs/reference/coroutines/exception-handling.html#coroutineexceptionhandler
When you turn on the airplane mode, the coroutine that do network calls will throw an exception.
In your case, you can do something like this.
val handler = CoroutineExceptionHandler { _, exception ->
//Handle your exception
}
init {
viewModelScope.launch(handler) {
videosRepository.refreshVideos()
}
}

Android WorkManager doWork() run repeatedly even after task successfully completed

I am using Android WorkManager(version 1.0.0-alpha13) to get some data from my server and store it on local database. Used following worker, without any operations and just returned success. doWork() run repeatedly,even after task successfully completed. Is it normal behaviour of Worker or is it any issue in this version?
HomeRepository.kt
val fetchVideosWorker = OneTimeWorkRequest.Builder(FetchVideosWorker::class.java)
.build()
val enqueue = WorkManager.getInstance().enqueue(fetchVideosWorker)
FetchVideosWorker.kt
class FetchVideosWorker(context : Context, params : WorkerParameters) : Worker(context,params) {
override fun doWork(): Result {
Log.d("youtubevideo","doWork")
return Result.success()
}
}

Categories

Resources