Android Tutorial Multiple instances of the same widget

corwin42

Expert
Licensed User
General Information

Normally it is not possible to create several widget instances of the same homescreen widget which all display different data.

I found a solution for this and with the help of a small code module with some special subs it works really great and is not too complicated to use.

What's missing from plain B4A are the following things:

- A possibility to start a configuration Activity for your widget to make individual settings
- A possibility to update only a single widget instance and not all of them
- A possibility to get a list of widgets created by the widget service
- A way to decide which widget fired a click event

WARNING: This tutorial is for the advanced B4A developer. You need to have basic knowledge of Intents and XML modification.
But you don't need to understand the magic stuff done in the provided code module subs. If you want to understand this you will need some knowledge about how the RemoteViews object works and how it is implemented.

Setting up a configuration activity

To make a widget individual from others you will need a possibility to change some settings for a single widget instance. For this you can create a widget configuration activity. This activity has to handle some extra information in the starting intent and has to return a special result to the widget.

To start we need a Project with a widget service completely set up. You have to compile the project at least once to create the xml files we have to change.

Just create a normal B4A activity called "WidgetConfig" for the configuration activity. We have to add a special intent filter in the manifest editor for it:

B4X:
AddActivityText (WidgetConfig , <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter> )
Then we have to tell our widget that it has to call a configuration activity on creation of a new widget. This has to be done in the widget provider info xml found under Objects\res\xml\<widgetservicename>_info.xml.
Add the following property to the xml (replace <your.package.name> with you apps package name. This adds our activity as a configuration activity.

B4X:
android:configure="<your.package.name>.widgetconfig"
Be sure to make the file read only after this or use the new B4A2.50 CustomBuildActions!

Note that the following code is not complete (all UI stuff is missing). See the example for the complete code!

Now we can take a look at the activity code. The activity is called with the AppWidgetId in the extra data. We have to save this in a global variable so we have access to the id everywhere.

B4X:
Sub Activity_Create(FirstTime As Boolean )
        StartingIntent = Activity.GetStartingIntent
       
        If StartingIntent.HasExtra("appWidgetId" ) Then
               gAppWidgetId = StartingIntent.GetExtra("appWidgetId")
        Else
               gAppWidgetId = -1
        End If
End Sub
We need to save the settings for the widget instances somewhere. I decided to use StateManager code module for this. In the example there are two settings for each widget. A "cityname" and a "timezone". The WidgetId is added to the setting name so we can see what setting is for which widget instance. So for widget "42" the setting for the city name is called "cityname_42".

If the user changed the settings we have to tell the launcher if the user canceled the operation or saved the settings. For this the activity must return a result (0 for cancel, -1 for ok, in the example there are variables for these values).

B4X:
Sub Activity_Resume
   'Initialize the result code with "CANCEL"
   Activity.SetActivityResult(ACTIVITY_RESULT_CANCELED, StartingIntent)
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   'If the activity is paused, close it and cancel the configuration process
   Activity.Finish
End Sub

Sub btSave_Click
   'The user clicked "Save" so set the OK-Result
   Activity.SetActivityResult(ACTIVITY_RESULT_OK, StartingIntent)
   'Save the settings for the widgetId
   StateManager.SetSetting("cityname_" & gAppWidgetId, eLocation.Text)
   StateManager.SetSetting("timezone_" & gAppWidgetId, spTimeZone.GetItem(spTimeZone.SelectedIndex))
   StateManager.SaveSettings

   gAppWidgetId = -1
   Activity.Finish
End Sub
The widget service

The main difference of our widget service to a normal B4A widget service is that we first need the information for which widget the service was called. The information is provided in the starting intent. There are 3 possibilities:

1. The widget service is called with a single widget id. This happens when configuring the widget or deleting a single widget instance from the homescreen. We have to handle/update only this widget.

2. The widget service is called with a list of widgets. If this is the case we have to handle/update all widgets in the list.

3. The widget service is called with no widget ids. This happens when the service is started with "StarServiceAt". In this case we have to find out the widget ids ourself. For this there is a Sub called GetWidgetIds() in the Util code module.

B4X:
Sub Service_Start (StartingIntent As Intent)
   mAppWidgetIdList.Clear
   'Is the service called from a single widget?
   If StartingIntent.HasExtra("appWidgetId") Then
      mAppWidgetIdList.Add(StartingIntent.GetExtra("appWidgetId"))
   End If

   Dim helper() As Int
   'Is there a list of widget ids in the Intent?
   If mAppWidgetIdList.Size = 0 Then
      If StartingIntent.HasExtra("appWidgetIds") Then
         helper = StartingIntent.GetExtra("appWidgetIds")
         mAppWidgetIdList.AddAll(helper)
      End If
   End If

   'Is the service called manually without any widget ids in starting intent?
   If mAppWidgetIdList.Size = 0 Then
      helper = Util.GetWidgetIds("advancedwidget")
      mAppWidgetIdList.AddAll(helper)
   End If

   'If there is no widget on the homescreen we can just exit
   If mAppWidgetIdList.Size = 0 Then
      Log("No AppWidget Ids found!")
      Return
   End If
End Sub
After we have a list of widget ids (or just a single id) we can handle/update every widget instance by its own. There are two more actions we can handle for our widget: "APPWIDGET_UPDATE_OPTIONS" and "APPWIDGET_DELETE".
The first is called when the app returns from the configuration activity. The second one if a single widget instance is removed.

Updating the widget is done by setting up the data for the RemoteViews object and then update a single widget instance with Util.UpdateSingleWidget() in a loop over all WidgetIds in list of widget ids.

B4X:
Sub Widget_RequestUpdate
   Dim LocationText As String
   Dim tzLocal As AHTimeZone
   tzLocal.Initialize

   'Instead of just updating the widget like in normal B4A code we need to loop over all widget ids and
   'set the content of each widget individually.
   'Here the setting information for each widget is read and the content is updated for each widget instance
   'individually.
   For i = 0 To mAppWidgetIdList.Size - 1
      Log("Update Widget: " & mAppWidgetIdList.Get(i))
      Dim tz As AHTimeZone
      tz.Initialize2(StateManager.GetSetting2("timezone_" & mAppWidgetIdList.Get(i), tzLocal.ID))
      LocationText = StateManager.GetSetting2("cityname_" & mAppWidgetIdList.Get(i), "Default")

      Dim dt As AHDateTime
      dt.Initialize
      dt.TimeZone=tz
      dt.Pattern="HH:mm"

      rv.SetText("lLocation", LocationText)
      rv.SetText("lTimeZone", tz.ID)
      rv.SetText("lClock", dt.Format(DateTime.Now))

      'Just update one widget instance with this special sub
      Util.UpdateSingleWidget(rv, mAppWidgetIdList.Get(i))
   Next
End Sub
Handling Events

If we want to use click events of the views in our widget we will have a problem. The normal B4A click event for widget views does not contain a widget id so it is not possible to find out which widget fired the click event.

We have to overwrite the click event and setup our own. This is done with the Util.SetWidgetClickEvent() sub. We have to set up the event for each widgetid:

B4X:
If mAppWidgetIdList.Size > 0 Then
   For i = 0 To mAppWidgetIdList.Size - 1
      Log("AppWidgetId: " & mAppWidgetIdList.Get(i))
      Util.SetWidgetClickEvent("advancedwidget", mAppWidgetIdList.Get(i), rv, "pMain", "pMain")
      '... add more views the same way
   Next
End If
Now the service is called with the correct widget id when a view on the widget is clicked.

Thats all.

Provided is an example of a world clock widget. If you add a widget of this type the configuration activity will pop up. Here you can set a name for a location and a timezone. If you press "Apply changes" then the widget is added and the configured location is shown and a clock with the current time in this timezone. You can add as may widgets with different locations and timezones as you want. If you press one of the widgets, the configuration activity is shown again and you can change the settings of this single widget. So the example shows how to call the configuration activity manually. There are many comments in the example so I hope you will understand it.

The example is written for B4A 2.70 (The "magic" shown in this tutorial works with older B4A versions, too) and needs the Reflection library, AHLocale library and RandomAccessFile library (for the StateManager).

Questions welcome.
 

Attachments

Last edited:

johnb4a

Member
Licensed User
Hi corwin42,


Thanks its a great help. But your attached example of world clock is giving an error when i am trying to run it. i have attached the error.
 

Attachments

corwin42

Expert
Licensed User
Do you have the latest version of AHLocale library installed?

sent from my Galaxy Nexus
 

NeoTechni

Well-Known Member
Licensed User
I'm having some trouble with this code.
Randomly, the widgets will show up blank/completely transparent.
I've checked, and my code is drawing them properly (I save the BMP to file to check)
 

corwin42

Expert
Licensed User
As you said you save the BMP to file I suggest you are using ImageViews in your widget?

If yes, how big is your ImageView and Bitmap you want to display? There are some issues with sending large bitmaps to a RemoteView since the data gets serialized and the buffer for transfering it is limited.
 

NeoTechni

Well-Known Member
Licensed User
As you said you save the BMP to file I suggest you are using ImageViews in your widget?

If yes, how big is your ImageView and Bitmap you want to display? There are some issues with sending large bitmaps to a RemoteView since the data gets serialized and the buffer for transfering it is limited.
You are correct. I'm using it for a lockscreen widget on my nexus 7, and that's ~500x500 pixels



As you can see, I'm not using standard controls.
And my program uses multiple kinds of widgets, with a single resizable layout
I'm forced to use the imageview method.
 

corwin42

Expert
Licensed User
Try to load the bitmap with LoadBitmapSample() instead of LoadBitmap().

Unfortunately on high resolution devices I think you will still get problems. I made a workaround for this problem which sets the RemoveView Image with an URI (this works since SDK version >= 8). But this is quite complicated and currently my solution uses a custom library so I can't post a full working example here.
 

lxracer

Member
Licensed User
Thanks

Hello corwin, i like your example and it works good, im having a problem with the click event, yours works good for config, but i have different option when user clicks as i do not open config, and i cant figure out how to see which widget has been clicked, ive tried searching within startingintent and addextra like you have in SetWidgetClickEvent, i cannot retrieve widgetid when clicked, and my "mAppWidgetIdList" doesnt reset every minute as yours does for my setup its filled with all instances of same widget that are alive, maybe if you could give me help in the right direction or how to go about it, thanks :)

Added:
also i thought i could have a workaround by setting a certain EVENTNAME on first SetWidgetClickEvent on
B4X:
Public Sub SetWidgetClickEvent(ServiceName As String, WidgetId As Int, rv As RemoteViews, ViewName As String, EventName As String)
ref.RunMethod4("putExtra", Array As Object("b4a_internal_event", EventName & "_clicked"), Array As String("java.lang.String", "java.lang.String"))
but all i can get in return is ViewName_Click instead of (EventName as string), i cant find it todo anything because i change that and it returns to ViewName_Click

B4X:
If StartingIntent.HasExtra("b4a_internal_event") Then Log(StartingIntent.GetExtra("b4a_internal_event"))
returns it as ViewName_Click
 
Last edited:

corwin42

Expert
Licensed User
I'm not sure if I understand correctly.

You say that your mAppWidgetIdList does not get cleared on every Service_start(). But this is the main way to check if the service is called from a single widget or gets a broadcast for all widget instances.

So mAppWidgetIdList always holds exaclty the widgets which should be updated or which received the event.
 

lxracer

Member
Licensed User
Hello thanks for reply,
i was sure that APPWIDGET_ENABLED and APPWIDGET_UPDATE with helper produced a widget ID
i updated today to 2.71 and looks like it doesnt produce a id any more,
is it me or was it always this way? i donno what i did to change it or than
update

Added, i just tested your example again and yours does produce a id on appwidget_enabled
i guess i donno what i did to stop that
 
Last edited:

corwin42

Expert
Licensed User
Small bugfix.

lxracer was correct that there was a problem with SetWidgetClickEvent() not setting the event correctly for different views. If an event was set for different views only events for the last view was fired. It is a bit complex to explain what caused this (you need internal knowledge of PendingIntent) but here is a fixed version of SetWidgetClickEvent() that should work:

B4X:
Code removed. See next post
There is another problem for which I don't have a fix currently. If the launcher restarts the widget layout will reset. I'm searching for a solution.
 
Last edited:

corwin42

Expert
Licensed User
Ok, got it.

Another update to SetWidgetClickEvent(). The UpdateSingleWidget(rv, WidgetId) call at the end must be removed. Then everything seems to work fine. Problem is that the RemoteViews object has to be completely "configured" and filled with data when updateAppWidget() gets called. This is because the RemoteViews object gets cached and when the Launcher restarts this cached version is used.

B4X:
Public Sub SetWidgetClickEvent(ServiceName As String, WidgetId As Int, rv As RemoteViews, ViewName As String, EventName As String)
   Dim vIntent As Object
   Dim vPendingIntent As Object
   Dim ref As Reflector
   Dim ViewId As Int
   Dim URI As Object


   vIntent = ref.RunStaticMethod("anywheresoftware.b4a.keywords.Common", "getComponentIntent", Array As Object(ref.GetProcessBA(ServiceName), Null), Array As String("anywheresoftware.b4a.BA", "java.lang.Object"))
   ref.Target = vIntent
   EventName = EventName.ToLowerCase
   ref.RunMethod4("putExtra", Array As Object("b4a_internal_event", EventName & "_clicked"), Array As String("java.lang.String", "java.lang.String"))
   ref.RunMethod4("putExtra", Array As Object("appWidgetId", WidgetId), Array As String("java.lang.String", "java.lang.int"))
   'Hack to make the Intent unique so that setOnClickPendingIntent() does not reuse same Intent for different widgets.
   URI = ref.RunStaticMethod("android.net.Uri", "parse", Array As Object("WID://widget/id" & WidgetId), Array As String("java.lang.String"))
   ref.RunMethod4("setData", Array As Object(URI), Array As String("android.net.Uri"))

   ref.Target = rv
   ViewId = ref.RunMethod4("getIdForView", Array As Object(ref.GetProcessBA(ServiceName), ViewName), Array As String("anywheresoftware.b4a.BA", "java.lang.String"))

   ref.RunMethod("checkNull")
   ref.Target = ref.GetField("current")
   vPendingIntent = ref.RunStaticMethod("android.app.PendingIntent", "getService", Array As Object(ref.GetContext, ViewId, vIntent, 134217728), Array As String("android.content.Context", "java.lang.int", "android.content.Intent", "java.lang.int"))
   ref.RunMethod4("setOnClickPendingIntent", Array As Object(ViewId, vPendingIntent), Array As String("java.lang.int", "android.app.PendingIntent"))
End Sub
I will clean up the example a bit and I will update the first post in the next days.
 
Last edited:

lxracer

Member
Licensed User
thanks for fast reply, i will check in when you have example, im off to test the new code, thanks again :sign0098:
 

lxracer

Member
Licensed User
Hello, no feedback on setclickeven Yet, but i have a question, i was working
on my project and was fine till last night, i have no idea what i did, as i did not
change nothing with GetWidgetId processes but for some reason
my android.appwidget.action.APPWIDGET_ENABLED STOPED producing a
Widget ID, and for the first time my app started producing
android.appwidget.action.APPWIDGET_DISABLED when it was disabled,
which i never saw before on my app for some reason

so only way i get a id is on APPWIDGET_UPDATED and APPWIDGET_DELETED
i guess this is ok
to work with but if you have any suggestions

thanks :sign0098:
 
Last edited:

corwin42

Expert
Licensed User
If I remember correctly android.appwidget.action.APPWIDGET_ENABLED and android.appwidget.action.APPWIDGET_DISABLED do not have any WidgetIds in their data. The first is called when the first widget is added to the homescreen. The second is called when tha last instance of the widget is removed.
 

lxracer

Member
Licensed User
On your example it does have a widgetids on enabled, maybe thats because its added from the config screen? im not sure, can i ask you for another suggestion, when you added a widget to the device it opens the config screen, i have 2 different services/widgets which i only know it opens config screen from what i added in manifest and xml files, is there a way to add more info for when the config opens is knowns from which service and widgetid it was created from?

and thanks for fast response your great


If I remember correctly android.appwidget.action.APPWIDGET_ENABLED and android.appwidget.action.APPWIDGET_DISABLED do not have any WidgetIds in their data. The first is called when the first widget is added to the homescreen. The second is called when tha last instance of the widget is removed.
 

corwin42

Expert
Licensed User
In the example the mAppWidgetIdList has an entry on APPWIDGET_ENABLED action because if there is not Id in the StartingIntent extras the service tries to get all appwidget ids with GetWidgetIds("advancedwidget") (which asks the AppWidgetManager for the Ids). The id is not submitted to the service though the intent.

For your other question: I don't think that it is possible to find out for which service the configuration activity gets called because the sender of the intent is the launcher and not the service. If you have different widget services you need to add their own configuration activity for each service if they should do different things.
 

Laurent95

Active Member
Licensed User
Ok, got it.

Another update to SetWidgetClickEvent(). The UpdateSingleWidget(rv, WidgetId) call at the end must be removed. Then everything seems to work fine. Problem is that the RemoteViews object has to be completely "configured" and filled with data when updateAppWidget() gets called. This is because the RemoteViews object gets cached and when the Launcher restarts this cached version is used.
.....
I will clean up the example a bit and I will update the first post in the next days.
Hello Corwin,

News about your example ?
Because i try to test your code it without the UpdatSingleWidget at end but that don't work for me, if i put it again, that seems working good.
But you talk also about when the launcher is restarted, i fear to do a mistake in my project where i have used your great code :sign0098:

Can we have your example to be sure to don't do any mistake ?

Thank you by advance.

Regards.
 

corwin42

Expert
Licensed User
News about your example ?
Sorry, it took some time.

I have uploaded an updated version of the example to the first post.

New version has the code cleaned up (in separate code modules) and uses the new designer custom view feature of B4A 2.70+. Also all bugs discussed in the previous posts should be fixed.

I haven't seen any bugs anymore. I think if I add a nice application Icon it could be released to the market without change. :)
 
Top