Im trying to make an android widget which the user can refresh the content on by clicking a button. Im stuck.
Currently neither the scheduled refresh or the button seems to work.
Is there anyone out there which either can see what Im doing wrong, or has some code examples which does something similar to what I attempt to do?
class Widget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
Log.d("WIDGET","UPDATE")
// There may be multiple widgets active, so update all of them
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
}
}
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
Log.d("WIDGET","1")
val widgetText = (0..10).random().toString()
val views = RemoteViews(context.packageName, R.layout.widget)
views.setTextViewText(R.id.txtWidget, widgetText)
val intentSync = Intent(context, Widget::class.java)
intentSync.action =
AppWidgetManager.ACTION_APPWIDGET_UPDATE
val pendingSync = PendingIntent.getBroadcast(
context,
0,
intentSync,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) //You need to specify a proper flag for the intent. Or else the intent will become deleted.
views.setOnClickPendingIntent(R.id.buttonWidget, pendingSync)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
Related
That's my code for the widget
class MovieOfTheDayWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
// There may be multiple widgets active, so update all of them
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
}
}
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
) {
val randomMovieList = Constants.getAllMovies()
val movie: AllMovies
val randomMovieListSize = randomMovieList.size
var randomPosition = Random().nextInt(randomMovieListSize)
movie = randomMovieList[randomPosition]
val idArray = arrayListOf<RemoteViews>()
// Construct the RemoteViews object
val movieName = RemoteViews(context.packageName, R.layout.movie_of_the_day_widget)
movieName.setTextViewText(R.id.seriesMovieNameAndYearWidget, "${movie.movieName} (${movie.date}")
val movieImg = RemoteViews(context.packageName, R.layout.movie_of_the_day_widget)
movieImg.setImageViewResource(R.id.seriesMovieImgWidget, movie.moviePic)
val movieRating = RemoteViews(context.packageName, R.layout.movie_of_the_day_widget)
movieRating.setTextViewText(R.id.seriesMovieRatingWidget, movie.rating)
idArray.add(movieName)
idArray.add(movieRating)
idArray.add(movieImg)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, idArray[0])
appWidgetManager.updateAppWidget(appWidgetId, idArray[1])
appWidgetManager.updateAppWidget(appWidgetId, idArray[2])
}
But the last 3 lines don't work properly, they only the second line works, how to update all of the RemoteViews in the idArray.
I've tried putting it in a for loop and updating i, but it also didn't work.
Is it even the wright thing to call the updateAppWidget function more than once? I've tried passing more than one parameter to the function but it returned an error.
You are correct, appWidgetManager.updateAppWidget() should be called just once.
You need to apply all changes to the same RemoteViews instance, so it should be:
// Construct the RemoteViews object
val remoteViews = RemoteViews(context.packageName, R.layout.movie_of_the_day_widget)
remoteViews.setTextViewText(R.id.seriesMovieNameAndYearWidget, "${movie.movieName} (${movie.date}")
remoteViews.setImageViewResource(R.id.seriesMovieImgWidget, movie.moviePic)
remoteViews.setTextViewText(R.id.seriesMovieRatingWidget, movie.rating)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
I created a Widget that contains a ViewFlipper and with buttons you can switch. Found a solution, applied it, everything works, but not in newer versions of Android. For example, in version API 24 (Android 7) it works fine, but in new ones, for example API 30, the widget does not handle clicks. Please help me figure out what the problem is.
package com.example.informationwidget.widgets.actual_info_widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.widget.RemoteViews
import com.example.informationwidget.R
import com.example.informationwidget.SettingsActivity
import com.example.informationwidget.services.LastCompaniesService
/**
* Implementation of App Widget functionality.
*/
class ActualInfoWidget : AppWidgetProvider() {
private val WIDGET_LC_LEFT_BUTTON = "android.appwidget.action.WIDGET_LC_LEFT_BUTTON"
private val WIDGET_LC_RIGHT_BUTTON = "android.appwidget.action.WIDGET_LC_RIGHT_BUTTON"
private val WIDGET_LC_SETTINGS_BUTTON = "android.appwidget.action.WIDGET_LC_SETTINGS_BUTTON"
private val WIDGET_LC_UPDATE_BUTTON = "android.appwidget.action.WIDGET_LC_UPDATE_BUTTON"
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onReceive(context: Context?, intent: Intent?) {
super.onReceive(context, intent)
val v = intent!!.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
val views = RemoteViews(context!!.packageName, R.layout.actual_info_widget)
val appWidgetManager = AppWidgetManager.getInstance(context)
val widgetName = ComponentName(context, ActualInfoWidget::class.java)
val widgetId = AppWidgetManager.getInstance(context).getAppWidgetIds(widgetName)
// Button click
when {
WIDGET_LC_LEFT_BUTTON == intent!!.action -> {
Log.e("This is ViewFlipper:", "This is left button, id = " + v) //widgetId[0]
views.showPrevious(R.id.vfLastChosenCompanies)
}
WIDGET_LC_RIGHT_BUTTON == intent.action -> {
Log.e("This is ViewFlipper:", "This is right button, id = " + v)
views.showNext(R.id.vfLastChosenCompanies)
}
WIDGET_LC_UPDATE_BUTTON == intent.action -> {
updateAppWidget(context, appWidgetManager, widgetId[0])
}
}
appWidgetManager.updateAppWidget(widgetId[0], views)
}
private fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val views = RemoteViews(context.packageName, R.layout.actual_info_widget)
val mainIntent = Intent(context, ActualInfoWidget::class.java)
mainIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
mainIntent.data = Uri.parse(mainIntent.toUri(Intent.URI_INTENT_SCHEME))
// Buttons from widget
val pendingSettingsBtn = PendingIntent.getActivity(context, 0, clickSettingsBtn(context), 0)
val viewerIntentLeftClick = Intent(WIDGET_LC_LEFT_BUTTON)
val viewerPendingLeftClick = PendingIntent.getBroadcast(
context,
0,
viewerIntentLeftClick,
PendingIntent.FLAG_UPDATE_CURRENT
)
val viewerIntentRightClick = Intent(WIDGET_LC_RIGHT_BUTTON)
val viewerPendingRightClick = PendingIntent.getBroadcast(
context,
0,
viewerIntentRightClick,
PendingIntent.FLAG_UPDATE_CURRENT
)
val intentUpdateClick = Intent(WIDGET_LC_UPDATE_BUTTON)
val pendingUpdateBtn = PendingIntent.getBroadcast(
context,
0,
intentUpdateClick,
PendingIntent.FLAG_UPDATE_CURRENT
)
showLastCompanies(views, context, appWidgetId)
Log.e("ID: ", appWidgetId.toString())
views.setOnClickPendingIntent(R.id.btnLcSettings, pendingSettingsBtn)
views.setOnClickPendingIntent(R.id.btnLeftScroll, viewerPendingLeftClick)
views.setOnClickPendingIntent(R.id.btnRightScroll, viewerPendingRightClick)
views.setOnClickPendingIntent(R.id.btnLcUpdate, pendingUpdateBtn)
appWidgetManager.updateAppWidget(appWidgetId, views)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.lvLastCompanies)
}
private fun clickSettingsBtn(context: Context): Intent {
val intent = Intent(context, SettingsActivity::class.java)
intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))
return intent
}
private fun showLastCompanies(rv: RemoteViews, context: Context, appWidgetId: Int) {
val adapter: Intent = Intent(context, LastCompaniesService::class.java)
rv.setRemoteAdapter(R.id.lvLastCompanies, adapter)
}
}
The problem is that you are using implicit broadcast. Just make sure that you make them all explicit.
For example:
val viewerIntentLeftClick = Intent(WIDGET_LC_LEFT_BUTTON)
ComponentName componentName = ComponentName(context, ActualInfoWidget::class.java);
viewerIntentLeftClick.setComponent(componentName);
Check out limitations starting from Android N
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 have a ContentProvider from a main app. The content will be shared with a consumer app. This consumer app has an app widget. I have tested the ContentProvider and ContentObserver to this consumer app in its Activity and all is well (meaning the RecyclerView of the Activity is updated whenever an update from the main app triggers changes to the database). However, registering the ContentObserver inside my AppWidgetProvider does not work as expected.
My AppWidgetProvider has the following code.
class StackWidgetProvider : AppWidgetProvider() {
override fun onEnabled(context: Context) {
Timber.i("Enabled")
if (favoriteUserProviderObserver == null) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, StackWidgetProvider::class.java)
favoriteUserProviderObserver = FavoriteUserProviderObserver(appWidgetManager, componentName).let {
context.contentResolver.registerContentObserver(CONTENT_URI, true, it)
it
}
}
}
override fun onDisabled(context: Context) {
Timber.i("Disabled")
favoriteUserProviderObserver?.let {
context.contentResolver.unregisterContentObserver(it)
}
favoriteUserProviderObserver = null
}
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
....
companion object {
private var favoriteUserProviderObserver: FavoriteUserProviderObserver? = null
private fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val intent = Intent(context, StackWidgetService::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = toUri(Intent.URI_INTENT_SCHEME).toUri()
}
val views = RemoteViews(context.packageName, R.layout.widget_favorite_user_stack).apply {
setRemoteAdapter(R.id.widget_favorite_user_stack_view, intent)
setEmptyView(R.id.widget_favorite_user_stack_view, R.id.widget_favorite_user_empty)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
I created a simple custom ContentObserver class like below.
class FavoriteUserProviderObserver(
private val appWidgetManager: AppWidgetManager,
private val componentName: ComponentName
) : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
Timber.i("Provider observer triggered")
appWidgetManager.notifyAppWidgetViewDataChanged(
appWidgetManager.getAppWidgetIds(componentName), R.id.widget_favorite_user_stack_view
)
}
}
The above observer class is never triggered (even when I change the data in my main app). For further clarity, here's the code for my RemoteViewsService and its factory.
class StackWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory =
StackRemoteViewsFactory(this.applicationContext)
}
class StackRemoteViewsFactory(private val context: Context) :
RemoteViewsService.RemoteViewsFactory {
private var widgetItems = listOf<UserProfileSummary>()
private lateinit var repository: FavoriteUserRepository
override fun onCreate() {
repository = FavoriteUserRepository(
FavoriteUserDataSource(context.contentResolver),
Dispatchers.IO
)
}
override fun onDataSetChanged() {
GlobalScope.launch {
widgetItems = repository.favoriteUsers().toList() // Tested; working on the Activity scope of the consumer app
Timber.i(widgetItems.toString())
}
}
override fun getViewAt(position: Int): RemoteViews =
RemoteViews(context.packageName, R.layout.widget_favorite_user_item).apply {
setTextViewText(R.id.widget_favorite_user_item_text, widgetItems[position].username)
}
override fun getLoadingView(): RemoteViews? = null
override fun getItemId(position: Int): Long = 0
override fun hasStableIds(): Boolean = false
override fun getCount(): Int {
return widgetItems.size
}
override fun getViewTypeCount(): Int = 1
override fun onDestroy() {}
}
So the logic is to ask the ContentObserver to observe changes in the ContentProvider. The observer is registered on the onEnabled and onDisabled part of the AppWidgetProvider. Once the observer notices a change in ContentProvider, it will ask the AppWidgetProvider to update itself, thus calling onDataSetChanged and fetching a new list of data.
However, the observer is never called. What could be the reason it's not working as expected here? (It can't be because of a lack of permission, because the Activity part of the consumer app is able to fetch the data just fine.)
What could be the reason it's not working as expected here?
An AppWidgetProvider is a subclass of BroadcastReceiver. Your instance of AppWidgetProvider will live for (hopefully) a very short time, best measured in milliseconds. Basically, you get one onUpdate() call (or other callback), and that instance is thrown away. The next callback gets a new instance.
As a result, doing anything in an AppWidgetProvider that requires it to be around for a period of time is doomed.
The most likely solution, taking modern versions of Android into account, is to adopt more of a push solution. Bear in mind that any of your code can update the RemoteViews for an app widget, simply by working with AppWidgetManager. So, some code that is already running and knows about the data updates needs to push a new RemoteViews, rather than expecting your AppWidgetProvider to be able to react to changes.
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.