Search code examples
androidwidgetbroadcastreceiverandroid-pendingintent

Widget onUpdate is not setting pendingIntent on button click after reboot


I'm creating a test widget that shows random number by clicking its button. everything is inside onUpdate of my Provider independently, including the pendingIntent. it works fine but after rebooting the phone views.setOnClickPendingIntent is not working although RemoteViews is recreated with no issue but the button becomes unresponsive.

public class TestWidget extends AppWidgetProvider {
    static HashMap<Integer, BroadcastReceiver> br = new HashMap<>();

    static void updateAppWidget(Context context, final AppWidgetManager appWidgetManager,
                                final int appWidgetId) {
        context = context.getApplicationContext();
        final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.test_widget);

        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                views.setTextViewText(R.id.appwidget_text, Math.random() + "");
                appWidgetManager.updateAppWidget(appWidgetId, views);
            }
        };
        br.put(appWidgetId, broadcastReceiver);//to unregister later

        Intent intent = new Intent("action");
        IntentFilter intentFilter = new IntentFilter("action");

        context.registerReceiver(broadcastReceiver, intentFilter);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 123, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        views.setOnClickPendingIntent(R.id.appwidget_button, pendingIntent);

        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {
            context.unregisterReceiver(br.get(appWidgetId));
        }
    }
}

manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.aeza.sta">
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <receiver android:name=".TestWidget" android:enabled="true" android:exported="false" >
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/test_widget_info" />
        </receiver>

        <activity android:name=".TestWidgetConfigureActivity">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Solution

  • Dynamically registering BroadcastReceivers in an AppWidgetProvider is a shaky solution, at best. AppWidgetProvider itself is a BroadcastReceiver, and instances of those statically registered in an app's manifest are meant to be rather short-lived.

    However, since AppWidgetProvider is a BroadcastReceiver, we can take advantage of that, and simply target your TestWidget in the click PendingIntents. We can also attach the Widget ID as an extra to the Intent here, so we update the correct one when the click fires. For example:

    Intent intent = new Intent(context, TestWidget.class);
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
                                                             appWidgetId,
                                                             intent,
                                                             PendingIntent.FLAG_UPDATE_CURRENT);
    views.setOnClickPendingIntent(R.id.appwidget_button, pendingIntent);
    

    Note that we've also used the appWidgetId for the PendingIntent's requestCode. It's important that a distinct PendingIntent is used for each Widget instance, lest the wrong Widget instance be updated with the wrong extras. Using the already-unique Widget ID allows us to do that easily.

    We then override TestWidget's onReceive() method, and check the Intent's action to determine if this is our click broadcast, or a normal Widget event broadcast from the system. In the example above, we didn't set an action, so we'll simply check for null here. However, you certainly could specify an action String, and it may be preferable to do so in some cases; e.g., if you have multiple Buttons in your Widget, and need to distinguish their click broadcasts.

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction() == null) {
            int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
            if (appWidgetId != -1) {
                updateWidgetText(context, appWidgetId, Math.random() + "");
            }
        }
        else {
            super.onReceive(context, intent);
        }
    }
    

    In the above, you can see that we pass the broadcast to the super method if we've found that it's not ours. AppWidgetProvider's onReceive() will then examine the Intent, and delegate to the appropriate event method, per usual.

    Apart from being a stable solution, this approach has another upshot, in that a separate BroadcastReceiver instance does not need to be created, registered, and then unregistered for each Widget instance. Though we've added an onReceive() method, we can remove all of the dynamic BroadcastReceiver code, so our TestWidget class is still pretty short and simple.

    public class TestWidget extends AppWidgetProvider {
    
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction() == null) {
                int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
                if (appWidgetId != -1) {
                    updateWidgetText(context, appWidgetId, Math.random() + "");
                }
            }
            else {
                super.onReceive(context, intent);
            }
        }
    
        static void updateWidgetText(Context context, int appWidgetId, String newText) {
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.test_widget);
            views.setTextViewText(R.id.appwidget_text, newText);
            AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views);
        }
    
        static void updateAppWidget(Context context, final AppWidgetManager appWidgetManager,
                                    final int appWidgetId) {
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.test_widget);
    
            Intent intent = new Intent(context, TestWidget.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
                                                                     appWidgetId,
                                                                     intent,
                                                                     PendingIntent.FLAG_UPDATE_CURRENT);
            views.setOnClickPendingIntent(R.id.appwidget_button, pendingIntent);
    
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    
        @Override
        public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
            for (int appWidgetId : appWidgetIds) {
                updateAppWidget(context, appWidgetManager, appWidgetId);
            }
        }
    }