I have an app which has a home screen widget via GlanceAppWidget().
I would like to run a worker inside the Content() function of GlanceAppWidget(). I have used enqueue(work) from WorkManager api to successfully execute my worker.
The problem is that onReceive is getting called multiple (infinite) number of times. How can I run the worker once without having onReceive called multiple times?
class MyWidget : GlanceAppWidget() {
#Composable
override fun Content() {
val work = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
WorkManager.getInstance().enqueue(work)
//some composable/ui code that consumes worker output
}
}
class MyWorker(
private val context: Context,
private val workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {
override suspend fun doWork(): Result {
return try {
startForegroundService()
//some task here
Result.Success.success(
workDataOf(
"MyKey" to "worker completed successfully"
)
)
} catch (throwable: Throwable) {
Result.failure()
}
}
private suspend fun startForegroundService() {
setForeground(
ForegroundInfo(
Random.nextInt(),
NotificationCompat.Builder(context, "download_channel")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentText("Downloading...")
.setContentTitle("Download in progress")
.build()
)
)
}
}
class GlanceReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget
get() = MyWidget()
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
}
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
}
}
Related
I have an async process in my android app and I am using a co-routine scope to call a suspend function. But I am getting this error.
Suspend function 'getByTimeType' should be called only from a
coroutine or another suspend function
This is the code giving me error:
class WidgetProvider : HomeWidgetProvider() {
private var job: Job = Job()
private val scope = CoroutineScope(job + Dispatchers.Main)
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
widgetData: SharedPreferences
) {
Log.d("debugging", "widget loaded")
appWidgetIds.forEach { widgetId ->
val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
val todosRemoteView = RemoteViews.RemoteCollectionItems.Builder()
scope.launch {
val db = Room.databaseBuilder(
context,
AppDatabase::class.java, "db"
).build()
val todosDAO = db.TodoDAO()
val todos: List<Todo> = todosDAO.getByTimeType("day")
}
Log.d("debugging", "update is triggered");
setRemoteAdapter(
R.id.todos_list,
todosRemoteView
.build()
)
}
appWidgetManager.updateAppWidget(widgetId, views)
}
}
}
I am running a worker in a glance composable on android.
why is onReceive being continuously called in an infinite loop?
What am I missing here?
class MyWidget : GlanceAppWidget() {
#Composable
override fun Content() {
val work = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
WorkManager.getInstance().enqueue(work)
}
}
class MyWorker(
private val context: Context,
private val workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {
override suspend fun doWork(): Result {
return Result.success()
}
}
class GlanceReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget
get() = MyWidget()
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
}
}
//Dependencies
implementation "androidx.work:work-runtime-ktx:2.7.1" // WorkManager with Coroutines
implementation "androidx.glance:glance-appwidget:1.0.0-alpha03" //Glance
That's because WM is disabling the on boot receiver when there are no workers scheduled. When an android app disables a receiver Android OS sends the PACKAGE_CHANGED broadcast event, causing the widget onReceive to be called.
https://issuetracker.google.com/115575872
For now the recommendation is to schedule a work with a long delay (e.g 10 years). We are working on a way to improve this.
I have an Retrofit2 API:
interface Api {
#POST("/my/url")
suspend fun function()
}
My ViewModel can call this HTTP function:
class MainViewModel : ViewModel() {
private val retrofiClient = APIClient.client!!.create(Api::class.java)
fun test() {
viewModelScope.launch {
retrofitClient.function()
}
}
}
No problem when i call this from my activity.
But my goal is to call this test() function from my app's widget.
My basic AppWidgetProvider looks like this:
class AppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// Perform this loop procedure for each App Widget that belongs to this provider
appWidgetIds.forEach { appWidgetId ->
val intent = Intent()
intent.action = "custom-event-name"
intent.setClassName(
MainActivity::class.java.getPackage().name,
MainActivity::class.java.name
)
val pendingIntent = PendingIntent.getBroadcast(
context.applicationContext,
0,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
)
val views: RemoteViews = RemoteViews(
context.packageName,
R.layout.appwidget
).apply {
setOnClickPendingIntent(R.id.test, pendingIntent)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
MainActivity:
class MainActivity : AppCompatActivity() {
private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
println("INTENT RECEIVED")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
...
registerReceiver(receiver, IntentFilter("custom-event-name"))
...
}
The BroadcastReceiver does not receive the intent. Even if it does, can i call my app functions there?
How can i setup my app and/or widget, to call this viewModel.test() method when i click on a button in the widget?
You can use Channel(https://kotlinlang.org/docs/channels.html, for coroutines) or RxRelay (https://github.com/JakeWharton/RxRelay, for rxjava) instead of using broadcast receiver. Just put your Channel or Relay instance on your Application class so you can access it across your activities, fragments, etc.
I have an app widget implemented using shared preferences.
Now I am working on its migration to Data Store.
The problem here is how can I observe/collect the Flow data in AppWidgetProvider (a subclass of BroadCastReceiver)?
Minimum Code to reproduce the issue.
MyAppWidgetProvider:
class MyAppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
}
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
RemoteViews(context.packageName, R.layout.widget_layout).also { views ->
val data = loadDataFromPreferences(context, appWidgetId)
views.setTextViewText(
R.id.textview_title,
data.title
)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
DataStoreUtil:
internal fun loadDataFromPreferences(context: Context, appWidgetId: Int): Flow<Data> {
val dataStore: DataStore<Preferences> = context.createDataStore(
name = PREFS_NAME,
migrations = listOf(SharedPreferencesMigration(context, PREFS_NAME))
)
val PREF_TITLE = stringPreferencesKey(PREF_PREFIX_KEY + appWidgetId + PREF_SUFFIX_TITLE)
return dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences ->
// No type safety.
val title = preferences[PREF_TITLE] ?: ""
Data(title)
}
}
Note:
Data - A custom model class
loadDataFromPreferences() return type was Data when using Shared Preferences. Changed it to Flow<Data> for DataStore which causes error in updateAppWidget() in the line :
val data = loadDataFromPreferences(context, appWidgetId) - as the data type has changed to Flow.
You can use collect to get the data from Flow
val result = loadDataFromPreferences(context, appWidgetId)
CoroutineScope(Dispatchers.Main).launch{
result.collect{ data ->
views.setTextViewText(
R.id.textview_title,
data.title
}
}
Thanks to rajan kt's answer.
Started with the CoroutineScope(Dispatchers.Main).launch which was the main aspect of the solution.
But, collect() didn't work as expected. Tried with first() instead and it solved the issue.
Posting the working code below:
MyAppWidgetProvider:
class MyAppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
CoroutineScope(Dispatchers.Main).launch {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
}
}
internal suspend fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
RemoteViews(context.packageName, R.layout.widget_layout).also { views ->
loadDataFromPreferences(context, appWidgetId)
.first {
views.setTextViewText(
R.id.textview_title,
data.title
)
appWidgetManager.updateAppWidget(appWidgetId, views)
true
}
}
}
DataStoreUtil:
internal suspend fun loadDataFromPreferences(context: Context, appWidgetId: Int): Flow<Data> {
val dataStore: DataStore<Preferences> = context.createDataStore(
name = PREFS_NAME,
migrations = listOf(SharedPreferencesMigration(context, PREFS_NAME))
)
val PREF_TITLE = stringPreferencesKey(PREF_PREFIX_KEY + appWidgetId + PREF_SUFFIX_TITLE)
return dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences ->
// No type safety.
val title = preferences[PREF_TITLE] ?: ""
Data(title)
}
}
I am new to Android Studio and my problem is my widget is not updating. Let's say I changed my data JSON. I have read some threads and says that a widget has a time interval of 30mins to refresh. But I have waited for my simulator 30mins and nothing changed. Can someone help me out on this ? Because I want my widget to update atleast 30mins default..
class TestWidget : AppWidgetProvider() {
private val httpClient = AsyncHttpClient()
private var title = String.toString()
private var imageUrl = String.toString()
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
// There may be multiple widgets active, so update all of them
httpClient.get(jsonUrl, object : JsonHttpResponseHandler() {
override fun onSuccess(statusCode: Int, headers: Array<out Header>, response: JSONArray) {
val json = response.getJSONObject(0)
title = json.getString("title")
imageUrl = json.getString("imageUrl")
val views = RemoteViews(context.packageName, R.layout.test_widget)
views.setTextViewText(R.id.text, title)
Picasso.with(context)
.load(imageUrl)
.into(views, R.id.image, appWidgetIds)
}
override fun onFailure(statusCode: Int, headers: Array<out Header>?, throwable: Throwable?, errorResponse: JSONObject?) {
println(throwable?.localizedMessage)
}
})
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
companion object {
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager,
appWidgetId: Int) {
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.test_widget)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
OH, I think i have found a solution.
There is a android:updatePeriodMillis on my Widget Info XML. That should do the trick.