Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the correct way for sharing data among AppWidgetProvider and RemoteViewsService.RemoteViewsFactory

Tags:

android

Currently, my AppWidgetProvider is having a static data. It is used for information passing around AppWidgetProvider & RemoteViewsService.RemoteViewsFactory

public class MyAppWidgetProvider extends AppWidgetProvider {
    // Key will be widget id
    private static Map<Integer, Holder> holderMap = new java.util.concurrent.ConcurrentHashMap<Integer, Holder>();

    public static int getClickedColumn(int appWidgetId) {
        Holder holder = holderMap.get(appWidgetId);  
        if (holder == null) {
            return -1;
        }
        return holder.clickedColumn;
    }

public class AppWidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
    @Override
    public void onDataSetChanged() {
        int clickedColumn = MyAppWidgetProvider.getClickedColumn(mAppWidgetId);

Calling AppWidgetProvider's static method works fine in most of the situation.

However, sometimes, if I place the widget to home screen, let it be there for few hours. When I come back and scoll the ListView, I might get the following error randomly.

java.lang.ExceptionInInitializerError
    at org.yccheok.project.gui.widget.AppWidgetRemoteViewsFactory.onDataSetChanged(AppWidgetRemoteViewsService.java:390)
    at android.widget.RemoteViewsService$RemoteViewsFactoryAdapter.onDataSetChanged(RemoteViewsService.java:142)
    at com.android.internal.widget.IRemoteViewsFactory$Stub.onTransact(IRemoteViewsFactory.java:49)
    at android.os.Binder.execTransact(Binder.java:367)
    at dalvik.system.NativeStart.run(Native Method)
Caused by: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
    at android.os.Handler.<init>(Handler.java:121)
    at org.yccheok.project.gui.widget.MyAppWidgetProvider.<clinit>(MyAppWidgetProvider.java:564)

From <clinit>, I suspect MyAppWidgetProvider is destroyed by OS? This cause AppWidgetRemoteViewsFactory wants to perform class initialization, before calling the static function?

Does this mean, MyAppWidgetProvider can be destroyed anytime by OS, and we shouldn't place share-able static data in it?

If so, what is the correct way for sharing data among AppWidgetProvider and RemoteViewsService.RemoteViewsFactory? (Besides using File, or SharedPreferences)

like image 910
Cheok Yan Cheng Avatar asked Dec 07 '22 02:12

Cheok Yan Cheng


1 Answers

RemoteViewsFactory -> AppWidgetProvider

The communication from the RemoteViewsFactory to the AppWidgetProvider can be done using Broadcasts, e.g. like this:

Intent intent = new Intent(ACTION_PROGRESS_OFF);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);

The AppWidgetProvider receives the event like so:

@Override
public void onReceive(final Context context, final Intent intent) {

    // here goes the check if the app widget id is ours

    final String action = intent.getAction();
    if (ACTION_PROGRESS_OFF.equals(action)) {
        // turn off the refresh spinner

Of course the broadcast action needs to be defined in the manifest:

<receiver
    android:name="...">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        <action android:name="myPackage.ACTION_PROGRESS_OFF" />
    </intent-filter>
    <meta-data ... />
</receiver>

AppWidgetProvider -> RemoteViewsFactory

One way to communicate with the RemoteViewsFactory (and in your case probably the best one) is to send the information in the intent of the service you're passing to the RemoteViewsAdapter:

Intent intentRVService = new Intent(context, RemoteViewsService.class);
intentRVService.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);

// let's put in some extra information we want to pass to the RemoteViewsFactory
intentRVService.putExtra("HELLO", "WORLD");

// when intents are compared, the extras are ignored, so we need to
// embed the extras into the data so that the extras will not be ignored
intentRVService.setData(Uri.parse(intentRVService.toUri(Intent.URI_INTENT_SCHEME)));

rv.setRemoteAdapter(appWidgetId, R.id.my_list, intentRVService);
rv.setEmptyView(R.id.my_list, android.R.id.empty);

// create the pending intent template for individual list items
...
rv.setPendingIntentTemplate(R.id.my_list, pendingIntentTemplate);

appWidgetMgr.notifyAppWidgetViewDataChanged(appWidgetId, R.id.my_list);

The RemoteViewsService can easily retrieve the information from the intent and pass it along to the RemoteViewsService.RemoteViewsFactory.

I'm not 100% sure when and how your widget decides to sort the data but I'd assume if the user picks a column to sort then you have to go through the update cycle with notifyAppWidgetViewDataChanged and then you would pass that column along. If you need that information later on then you'd have to store the information somehow (SharedPreferences).

like image 129
Emanuel Moecklin Avatar answered Dec 10 '22 11:12

Emanuel Moecklin