Java Question Instances from same Homescreenwidget

corwin42

Expert
Licensed User
Longtime User
I'm trying to create individual instances from homescreen widgets with the possibility to set individual values for the different widgets.

I took a quick look into the RemoteViewsWrapper of the core library.

Wouldn't it be possible to just update a special widget with the following new method in the core library?

B4X:
public void UpdateWidgetWithId(BA ba, int WidgetId)
    throws ClassNotFoundException
  {
    checkNull();
    AppWidgetManager.getInstance(ba.context).updateAppWidget(WidgetId, this.current);
    this.current = null;
  }

With this it should be possible to update a single widget with the given ID.

You can get the AppWidgetId(s) from the Intent. If the service is called manually we would need another method to get the appwidget Ids:

B4X:
public int[] GetAppWidgetIds(BA ba) throws ClassNotFoundException {
      ComponentName cn = new ComponentName(ba.context,
            Class.forName(BA.packageName + "." + ba.className + "$" + ba.className + "_BR"));
      return AppWidgetManager.getInstance(ba.context).getAppWidgetIds(cn);
   }

Erel, what do you think?
 
Last edited:

corwin42

Expert
Licensed User
Longtime User
I have implemented the UpdateSingleWidget(WidgetId) method with reflection and this works great:

B4X:
Sub UpdateSingleWidget(pRv As RemoteViews, WidgetId As Int)
   Dim Obj1, Obj2 As Reflector
   Dim current As Object
   
   Obj1.Target = pRv ' a RemoteViewsWrapper
   
   Obj1.RunMethod("checkNull") 'does some internal checking and may set current
   current = Obj1.GetField("current") ' a RemoteViews - get this after checkNull
   Obj2.Target = Obj1.RunStaticMethod("android.appwidget.AppWidgetManager", "getInstance", Array As Object(Obj1.GetContext), Array As String("android.content.Context"))
   Obj2.RunMethod4("updateAppWidget", Array As Object(WidgetId, current), Array As String("java.lang.int", "android.widget.RemoteViews"))
   Obj1.SetField2("current", Null)
End Sub

Updating different instances of the widget with this works just great.

But I'm stuck with the second one. I tried the following:

B4X:
Sub GetWidgetIds As Int()
   Dim Obj1, Obj2 As Reflector
   Dim cn As Object
   
   Log("Trying to get Widget IDs...")
   Obj2.Target = Obj1.RunStaticMethod("android.appwidget.AppWidgetManager", "getInstance", Array As Object(Obj1.GetContext), Array As String("android.content.Context"))
   cn = Obj1.CreateObject2("android.content.ComponentName", Array As Object(Obj1.GetContext, Obj1.GetB4AClass("AdvancedWidget")), Array As String("android.content.Context", "java.lang.Class"))
   Return Obj2.RunMethod4( "getAppWidgetIds", Array As Object(cn), Array As String("android.content.ComponentName"))
End Sub

But this does not work. I think my parameters for the ComponentName object are wrong. I pass Obj1.GetB4Class("AdvancedWidget") to it. This is the Class of my service but I think I need to pass the class of the Broadcast Receiver. But whenever I try to pass the class <packagename><serviceclass>$<serviceclass>_BR I get a ClassNotFoundException.

Any ideas?
 

corwin42

Expert
Licensed User
Longtime User
Ah, got it:

B4X:
Sub GetWidgetIds(ServiceName As String) As Int()
   Dim Obj1, Obj2 As Reflector
   Dim cn As Object
   
   Log("Trying to get Widget IDs...")
   Obj2.Target = Obj1.RunStaticMethod("android.appwidget.AppWidgetManager", "getInstance", Array As Object(Obj1.GetContext), Array As String("android.content.Context"))
   cn = Obj1.CreateObject2("android.content.ComponentName", Array As Object(Obj1.GetContext, Obj1.GetStaticField("anywheresoftware.b4a.BA", "packageName") & "." & ServiceName & "$" & ServiceName & "_BR"), Array As String("android.content.Context", "java.lang.String"))
   Return Obj2.RunMethod4( "getAppWidgetIds", Array As Object(cn), Array As String("android.content.ComponentName"))
End Sub

This sub returns a list of all of our existing widget instances.

Great, a first simple testprogram works. The widgets in the screenshot just update a label with their own widget id. I think I can work with this.
 

Attachments

  • Screenshot_2013-01-24-13-42-51.jpg
    Screenshot_2013-01-24-13-42-51.jpg
    45.9 KB · Views: 336

corwin42

Expert
Licensed User
Longtime User
Ok, I am creating an example with this "new feature" and encountered only one Problem for now:

Click events. The Intent generated for click events does not have any information about the widget instance from which widget the event was raised.

All other things work great. I have created a widget with a configuration activity. You can change your settiings for the widget there and save them. And the widgets can read these settings and update their content respectively.

Erel, I think we will need your help here. Do you have any idea how to add the WidgetId to the PendingIntent that is created for a click event? I think if we get this running this will make many people happy.

Btw. I will soon release a complete example and perhaps I will create a tutorial.

Maybe this thread can be moved to the questions forum or the wishlist forum since it is not related to any library development anymore.
 

corwin42

Expert
Licensed User
Longtime User
A first hack

Here is a first "hack" of a working world clock. The user interface is awful but it's ok to show that it works.

- The main activity of the app is useless
- Try to add a "WorldClock" widget to your homescreen
- A configuration Activity will show
- Enter a name for your location and select a timezone (yes I know, the list is long)
- Press the "Apply changes" buton and a new widget is created. The clock should display the time in your selected timezone and the name of the location is shown, too.
- If you press a widget, a message will be shown. :)

This example is a B4A 2.50 project. You will need AHLocale, Reflection and RandomAccessFile libraries.
 

Attachments

  • WorldClock0_1.zip
    14.2 KB · Views: 352
Last edited:

corwin42

Expert
Licensed User
Longtime User
Good job! Do you still need my help?

Yes, see post #5. It is not possible to detect from which widget instance an event was fired. Would be great if you have any idea how to add the WidgetId to the PendingIntent for the click event. But I think this can only be changed in the core library.

I have an idea to add my own onClick listener on widget configuration change but I don't know if this will work and I think I will have to write an additional library for that.

BTW: Just updated the example. I changed the package name before upload and forgot to change it in the res/xml/advancedwidget_info.xml, too. This is fixed now (before you got "App not installed" error when you try to add a widget).
 

corwin42

Expert
Licensed User
Longtime User
I now see. You will not be able to implement it without building a new library that replaces the core implementation.

I am. :sign0102:

Attached is a fully working example.

If you click on the widget, the configuration is shown again and you can change the location name and the timezone. The changes are applied to the widget immediately after pressing the "Apply changes" button in the configuration activity.

The trick is just to replace the onClickPendingIntent listener for the widget with this sub:

B4X:
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

   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 & "_click2"), 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"))

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

   vPendingIntent = ref.RunStaticMethod("android.app.PendingIntent", "getService", Array As Object(ref.GetContext, WidgetId, vIntent, 134217728), Array As String("android.content.Context", "java.lang.int", "android.content.Intent", "java.lang.int"))
   ref.RunMethod("checkNull") 'does some internal checking and may set current
   ref.Target = ref.GetField("current") ' a RemoteViews - get this after checkNull
   ref.RunMethod4("setOnClickPendingIntent", Array As Object(ViewId, vPendingIntent), Array As String("java.lang.int", "android.app.PendingIntent"))
   
   UpdateSingleWidget(rv, WidgetId)
End Sub

See the example for a better explanation.
 

Attachments

  • WorldClock0_2.zip
    14.9 KB · Views: 367
Last edited:

corwin42

Expert
Licensed User
Longtime User
More AppWidget magic

Here comes the next step of my AppWidget magic.

This example shows how to set a completely different layout for the widget. The layout can be selected in the widget configuration activity. Even for an existing widget you can change the layout.
 

Attachments

  • WorldClock1_1.zip
    17.2 KB · Views: 352
  • WorldClock.apk
    130.4 KB · Views: 336
Top