I have a list view in my home screen widget and I want to launch a pending intent when the item is clicked. I referred to this and set a fillInIntent for every list item and then set a pending intent template which triggers the broadcast receiver of the widget:
class WidgetProvider : HomeWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) {
appWidgetIds.forEach { widgetId ->
val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
val todosStr = widgetData.getString("todos", "null")
val todos = ArrayList<HashMap<String, Any>>()
val todosRemoteView = RemoteViews.RemoteCollectionItems.Builder()
if(todosStr != "null"){
val jObj = JSONObject(todosStr)
val jsonArry = jObj.getJSONArray("todos")
for (i in 0 until jsonArry.length()) {
val todo = HashMap<String, Any>()
val obj = jsonArry.getJSONObject(i)
todo["id"] = obj.getInt("id")
todos.add(todo)
val view = RemoteViews(context.packageName, R.layout.each_todo).apply {
setTextViewText(R.id.each_todo_container_text, todo["taskName"].toString())
val fillInIntent = Intent().apply {
Bundle().also { extras ->
extras.putInt("todo_id", todo["id"].toString().toInt())//this isn't working for some reason
putExtras(extras)
}
}
Log.d("debugging", "id received is ${todo["id"].toString()}" )
setOnClickFillInIntent(R.id.each_todo_container_text, fillInIntent)
}
todosRemoteView.addItem(todo["id"].toString().toInt().toLong(), view)
}
}
setRemoteAdapter(
R.id.todos_list,
todosRemoteView
.build()
)
val pendingIntentx: PendingIntent = Intent(
context,
WidgetProvider::class.java
).run {
PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_IMMUTABLE or Intent.FILL_IN_COMPONENT)
}
setPendingIntentTemplate(R.id.todos_list, pendingIntentx)
}
appWidgetManager.updateAppWidget(widgetId, views)
}
}
override fun onReceive(context: Context?, intent: Intent?) {
val viewIndex: Int = intent!!.getIntExtra("todo_id", 0)
Log.d("debugging", "an item is clicked $viewIndex")
//the default value gets used, which means that the receiver isn't receiving the intent data
super.onReceive(context, intent)
}
}
The problem is that the receiver isn't receiving the data which was put in the fillInIntent, the default value 0 is getting printed by the last statement. I also tried setting the FILL_IN_COMPONENT flag as suggested here, but that didn't work. Where am I going wrong?
Related
I am using this package with customisation home_widget and trying to build a home widget with number buttons like this image.
I am trying to achieve it by the following code
My WidgetProvider.kt
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) {
appWidgetIds.forEach { widgetId ->
val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
// Open App on Widget Click
val pendingIntent = HomeWidgetLaunchIntent.getActivity(context,
MainActivity::class.java)
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
val counter = widgetData.getString("_number", "")
Log.i("onMethodCall", "onMethodCall: $counter")
print("onMethodCall: $counter")
var counterText = "$counter"
setTextViewText(R.id.tv_counter, counterText)
val pendingIntent2 = HomeWidgetLaunchIntent.getActivity(context,
MainActivity::class.java)
setOnClickPendingIntent(R.id.bt_update, pendingIntent2)
val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("myAppWidget://onNumberBtnClick"))
setOnClickPendingIntent(R.id.bt_one, backgroundIntent)
}
appWidgetManager.updateAppWidget(widgetId, views)
}
}
I am passing intent as "onNumberBtnClick" and this is my broadcast function
fun getBroadcast(context: Context, uri: Uri? = null): PendingIntent {
val intent = Intent(context, HomeWidgetBackgroundReceiver::class.java)
intent.data = uri
intent.action = HOME_WIDGET_BACKGROUND_ACTION
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= 23) {
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
return PendingIntent.getBroadcast(context, 0, intent, flags)
}
in my "HomeWidgetPlugin" class I have set that value in shared preference.
"onNumberBtnClick" -> {
if (call.hasArgument("id") && call.hasArgument("data")) {
Log.i("onMethodCallHere", "onMethodCallHere: ")
print("onMethodCallHere")
val id = call.argument<String>("id")
val data = call.argument<Any>("data")
val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit()
if(data != null) {
when (data) {
is Boolean -> prefs.putBoolean(id, data)
is Float -> prefs.putFloat(id, data)
is String -> prefs.putString(id, data)
is Double -> prefs.putLong(id, java.lang.Double.doubleToRawLongBits(data))
is Int -> prefs.putInt(id, data)
else -> result.error("-10", "Invalid Type ${data!!::class.java.simpleName}. Supported types are Boolean, Float, String, Double, Long", IllegalArgumentException())
}
} else {
prefs.remove(id);
}
result.success(prefs.commit())
}else {
result.error("-1", "InvalidArguments saveWidgetData must be called with id and data", IllegalArgumentException())
}
}
In main.dart I handled callback with workmanager plugin as in the example of the home widget plugin
Future<void> backgroundCallback(Uri? uri) async {
if (uri?.host == 'onNumberBtnClick') {
await HomeWidget.getWidgetData<String>('_number', defaultValue: "1").then((value) {
print("num:$value");
});
await HomeWidget.saveWidgetData<String>('_number', "13");
await HomeWidget.updateWidget(name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider');
}
}
Can someone please help me with what I am missing or how can I achieve this?
I'm making a widget for my WebView app, and it's got a list of buttons on it. Currently, It's firing an intent whenever their pressed. In that intent, I'm putting some string extra's, but when the onNewIntent receives the intent, the value for the extra is NULL. So I'm stuck on receiving the actual string extra.
Here's the code on my list provider:
override fun getViewAt(positionIndexNum: Int): RemoteViews {
........
val extrasObj = Bundle()
extrasObj.putString("shortcutUrl", listViewUrlArr[positionIndexNum]) // I've tried hardcoding this part and it still returns null.
extrasObj.putString("shortcutJs", listViewJsArr[positionIndexNum])
extrasObj.putString("shortcutId", listViewIdArr[positionIndexNum])
val fillInIntentObj = Intent()
fillInIntentObj.putExtras(extrasObj)
viewObj.setOnClickFillInIntent(listViewItemId, fillInIntentObj)
return viewObj
}
Here's the code from the onNewIntent function:
override fun onNewIntent(intentObj: Intent) {
super.onNewIntent(intentObj)
val bundle = intentObj.extras
if (bundle != null) {
for (key in bundle.keySet()) {
Log.e("TAG", key + " : " + if (bundle[key] != null) bundle[key] else "NULL")
}
}
.....
}
That outputs in the logcat:
shortcutUrl : NULL
shortcutId : NULL
shortcutJs : NULL
I've also tried: intentObj.getStringExtra("shortcutId") which still returns NULL
EDIT:
I also have this PendingIntent code in the updateAppWidget function:
val clickIntent = Intent(contextObj, MainActivity::class.java)
val clickPI = PendingIntent.getActivity(contextObj, 0,
clickIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT);
viewsObj.setPendingIntentTemplate(R.id.widget_list, clickPI)
I finally found a fix for this, I'm not sure how it really works but I changed:
PendingIntent.FLAG_IMMUTABLE
to
PendingIntent.FLAG_MUTABLE
in the PendingIntent. Hopefully this helps someone else!
Here is a full answer which I posted on another question:
Application widget with bundle?
Read the Intent in MyActivity:
private fun readIntent() {
val intentExtras: Bundle? = intent.extras
intentExtras?.let {
val intentMessage: String? = intentExtras.getString(APPWIDGET_INTENT_MESSAGE)
println(intentMessage)
}
}
Here is a method in Kotlin to get pending intent to open your activity
fun getPendingIntentMyActivity(context: Context, message: String): PendingIntent {
val intent = Intent(context, MyActivity::class.java)
intent.action = APPWIDGET_INTENT
intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))
val extras = Bundle().apply {
putString(APPWIDGET_INTENT, APPWIDGET_OPEN_APP)
putString(APPWIDGET_INTENT_MESSAGE, message)
}
intent.putExtras(extras)
return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
}
Then set it in the Widget
remoteViews.setOnClickPendingIntent(R.id.rootView, getPendingIntentMyActivity(context, "Hello World")
Samsung A10 android 11 updated, Galaxy S9 and Galaxy S10 tested on these devices but its not working
This code is only for android Oreo and above
Here is the code which I used for creating the shortcut in android programmatically. In all other devices its work perfectly but on this specific device it create the short but generate my own app shortcut not for desired.
val shortcutIntent = finalPackageName?.let {
context?.packageManager!!.getLaunchIntentForPackage(
it
)
}
val shortcutManager: ShortcutManager? = context?.getSystemService(ShortcutManager::class.java)
if (shortcutManager != null) {
if (shortcutManager.isRequestPinShortcutSupported) {
val shortcut = ShortcutInfo.Builder(context, "unique_id")
.setShortLabel(finalAppName)
.setLongLabel("Open the Android Docu")
.setIcon(Icon.createWithBitmap(finalBitmap))
.setIntent(shortcutIntent!!)
.build()
((activity) as MainActivity).registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
findNavController().navigate(R.id.resultFragment)
context.unregisterReceiver(this)
}
}, IntentFilter("test_action"))
val intent = Intent("test_action")
val pendingIntent = PendingIntent.getBroadcast(context, 123, intent, 0)
shortcutManager.requestPinShortcut(shortcut, pendingIntent.intentSender)
} else
Toast.makeText(
context,
"Pinned shortcuts are not supported!",
Toast.LENGTH_SHORT
).show()
}
I solved it
if (ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
val intent = Intent(context, MainActivity::class.java)
intent.action = "android.intent.action.MAIN"
intent.putExtra("appName", originalAppName)
intent.putExtra("pkgName", finalPackageName)
val build: ShortcutInfoCompat =
ShortcutInfoCompat.Builder(context, "uniqueId")
.setIntent(intent).setShortLabel(
finalAppName
).setIcon(IconCompat.createWithBitmap(finalBitmap)).build()
val shortcutManager =
context.getSystemService(ShortcutManager::class.java)
//context is required when call from the fragment
context.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
//this method is called when shortcut is created
Log.d("intent", intent.data.toString())
}
}, IntentFilter("test_action"))
val receiverIntent = Intent("test_action")
val pendingIntent =
PendingIntent.getBroadcast(context, 123, receiverIntent, 0)
ShortcutManagerCompat.requestPinShortcut(
context,
build,
pendingIntent.intentSender
)
return
}
Toast.makeText(
context,
"launcher does not support short cut icon",
Toast.LENGTH_SHORT
).show()
then go to your main activity and get the intent data
val stringExtra = intent.getStringExtra("pkgName")
if (stringExtra != null) {
startActivity(packageManager.getLaunchIntentForPackage(stringExtra))
finish()
}
I want to update widget with data from my Activity every second. I have Item which has timers and its updating by onBind method sending payload to it. Every time it will update data and send new (Strings, Dates, Color resources) to payload, I want to update existing Widget with these data. I should update it every second as timer goes on. (payload is sent every second) Is this possible? Because I can add my data to extras and send Intent to AppWidgetProvider, but I don't know how to update views in Widget with new data sent into onReceive. Tried this code in onUpdate function. Which should be called every 1000ms which I set in xml for widget provider - android:updatePeriodMillis="1000". What is happening is Grey box on my homescreen with Problem loading widget text. No crashes or errors were present in logs.
RecyclerView item:
private fun updateWidgetTimer(text: String, color: Int){
val int = Intent(a, WidgetProvider::class.java)
int.apply {
putExtra(WidgetProvider.REM_TIME, text)
putExtra(WidgetProvider.REM_TIME_COLOR, color)
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
}
}
AppWidgetProvider:
private var time: String = ""
private var color: Int = R.color.colorGreen
override fun onUpdate(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetIds: IntArray?
) {
val remoteViews = RemoteViews(context?.packageName, R.layout.widget_custom_layout)
remoteViews.setTextViewText(R.id.timer, time)
context?.let {
remoteViews.setTextColor(R.id.timer, ContextCompat.getColor(context, color))
}
}
override fun onReceive(context: Context?, intent: Intent?) {
super.onReceive(context, intent)
val extras = intent?.extras
extras?.let {
time = extras.getString(REM_TIME)?:""
color = extras.getInt(REM_TIME_COLOR)
}
}
UPDATE:
Payload is successfully send to onReceive, but as onUpdate is called, none of my widgets are updated with new data.
Example of my updated onUpdate function:
override fun onUpdate(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetIds: IntArray?
) {
context?.let {
val currWidgetInstanceName = ComponentName(context, WidgetProvider::class.java)
val widgetManager = AppWidgetManager.getInstance(context)
val widgetIds = widgetManager.getAppWidgetIds(currWidgetInstanceName)
widgetIds?.forEach {wId->
val remoteViews = RemoteViews(context.packageName, R.layout.widget_layout)
remoteViews.apply {
//set info title
setTextViewText(R.id.title, title)
//set subtitle
setTextViewText(R.id.subT, subt)
//set info
setTextViewText(R.id.info, info)
//set rem timer
setTextViewText(R.id.outTimer, remTime)
setTextColor(R.id.outTimer, ContextCompat.getColor(context, remTimeColor))
}
widgetManager?.updateAppWidget(currWidgetInstanceName, remoteViews)
}
}
}
From https://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo
Note: Updates requested with updatePeriodMillis will not be delivered
more than once every 30 minutes.
If you want frequent updates, you should use
AlarmManager
Use a service and update the widget, example:
Intent intent = new Intent(context, WidgetUpdateService.class);
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarm.cancel(pending);
alarm.setRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime(), 1000, pendingIntent);
AppWidgetProvider.onUpdate is called by AppWidgetProvider.onReceive. Call super.onReceive(context, intent) at the end of onReceive method. Data will be updated before onUpdate will be called.
override fun onReceive(context: Context?, intent: Intent?) {
val extras = intent?.extras
extras?.let {
time = extras.getString(REM_TIME)?:""
color = extras.getInt(REM_TIME_COLOR)
}
super.onReceive(context, intent)
}
I show a notification with actions to the user, I handle these actions with a BroadcastReceiver, from there I update a realm database, but it doesn't get updated, even though I'm sure(through logs) the transaction gets executed.
NotificationBroadcastReceiver:
override fun onReceive(context: Context, intent: Intent) {
val notionId = intent.getStringExtra(NOTION_ID_EXTRA)
val actionType = intent.getIntExtra(ACTION_TYPE, ACTION_TYPE_PUTBACK)
when (actionType) {
ACTION_TYPE_PUTBACK -> {
Toast.makeText(context, R.string.notion_is_putback, Toast.LENGTH_SHORT).show()
}
ACTION_TYPE_ARCHIVE -> {
NotionsRealm.changeIdleState(notionId, true)
}
}
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(NotionsReminder.NOTION_NOTIFICATION_ID)
}
NotionsRealm:
fun changeIdleState(id: String, state: Boolean) {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val notion = it.where<Notion>().equalTo("id", id).findFirst()
notion?.isArchived = state
debug("${notion?.isArchived}") //prints true to the log, but the data doesn't change.
}
closeRealm(realm)
}
private fun closeRealm(realm: Realm) {
try {
realm.close()
} catch (e: Exception) {
error(e)
} finally {
debug("realm closed")
}
}
edit:
I just let the receiver start an empty activity(with no layout) to handle the database. the same thing happened. I think it's no longer a BroadcastReceiver issue. It's strange, other realm transactions run perfectly in other activities/fragments.
Turns out it's not a problem with realm, it was How I fired the broadcast, I did it this way:
fun notificationAction(context: Context, id: String, actionType: Int): PendingIntent {
return PendingIntent.getBroadcast(
context, actionType,
Intent(context, NotificationBroadcastReceiver::class.java).apply {
putExtra(NotificationBroadcastReceiver.NOTION_ID_EXTRA, id)
putExtra(NotificationBroadcastReceiver.ACTION_TYPE, actionType)
}, 0)
}
I found that the passed id was incorrect, after some searching I found out that I should include this flag in the broadcast: PendingIntent.FLAG_UPDATE_CURRENT so it's like this:
fun notificationAction(context: Context, id: String, actionType: Int): PendingIntent {
return PendingIntent.getBroadcast(
context, actionType,
Intent(context, NotificationBroadcastReceiver::class.java).apply {
putExtra(NotificationBroadcastReceiver.NOTION_ID_EXTRA, id)
putExtra(NotificationBroadcastReceiver.ACTION_TYPE, actionType)
}, PendingIntent.FLAG_UPDATE_CURRENT)
}
Now the passed id is the right one, I still don't understand why did this happen, or why was the id extra completely different(yet not random, I kept seeing the same wrong id every time) without this flag.