Android: loading WebView output into App Widget

Whilst I appreciate that an App Widget does not support a WebView directly, is it at all possible to use an ImageView (which is supported), and a technique like described here to generate the image for the ImageView? The WebView wouldn't be used directly, but only used in the background in order to provide the image for the ImageView.

Answers


This is possible, apparently, at least on my device, though your mileage may vary.

In the manifest, we register the Widget's <receiver> as normal, and also register our Service that handles the WebView and its image capture.

<manifest ... >
    ...

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    <application ... >
        ...

        <receiver
            android:name=".WebWidgetProvider"
            android:label="@string/widget_name" >
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

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

        <service android:name=".WebShotService" />

    </application>

</manifest>

In the Widget's info file, widget_info.xml, I've set the minimum size to 4 x 2.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget_layout"
    android:minWidth="292dip"
    android:minHeight="146dip" />

For this example, the Widget's layout, widget_layout.xml, is simply an ImageView.

<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/widget_image"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scaleType="fitXY"
    android:src="@drawable/ic_launcher" />

The AppWidgetProvider in this example is overriding only the onUpdate() method, as we're using the same ACTION_APPWIDGET_UPDATE to trigger our own updates with AlarmManager. Please note that the example alarm will fire every 30 seconds, as long as there's an active Widget. You probably don't want to do this outside of testing, as Widget updates can be rather costly.

public class WebWidgetProvider extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // We can't trust the appWidgetIds param here, as we're using
        // ACTION_APPWIDGET_UPDATE to trigger our own updates, and
        // Widgets might've been removed/added since the alarm was last set.
        final int[] currentIds = appWidgetManager.getAppWidgetIds(
            new ComponentName(context, WebWidgetProvider.class));

        if (currentIds.length < 1) {
            return;
        }

        // We attach the current Widget IDs to the alarm Intent to ensure its
        // broadcast is correctly routed to onUpdate() when our AppWidgetProvider
        // next receives it.
        Intent iWidget = new Intent(context, WebWidgetProvider.class)
            .setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
            .putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, currentIds);
        PendingIntent pi = PendingIntent.getBroadcast(context, 0, iWidget, 0);

        ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE))
            .setExact(AlarmManager.RTC, System.currentTimeMillis() + 30000, pi);

        Intent iService = new Intent(context, WebShotService.class);
        context.startService(iService);
    }
}

In the Service, we instantiate a WebView, and add it to a FrameLayout, which in turn gets added to the WindowManager with zero for its width and height parameters. We then load a test URL in the WebView, and when the page finishes, we force the WebView to layout to an appropriate size, and draw it to our target Bitmap via a Canvas object. This is done after a slight delay to allow the WebView to fully render itself, lest we end up with a blank, white image. The Bitmap is then set on a RemoteViews object to update the ImageView in the Widget's layout. I've left a Toast in the code for testing purposes, as our example Widget may update more frequently than Stack Overflow's front page refreshes.

public class WebShotService extends Service {
    private WebView webView;
    private WindowManager winManager;

    public int onStartCommand(Intent intent, int flags, int startId) {
        winManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        webView = new WebView(this);
        webView.setVerticalScrollBarEnabled(false);
        webView.setWebViewClient(client);

        final WindowManager.LayoutParams params =
            new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
                                           WindowManager.LayoutParams.WRAP_CONTENT,
                                           WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
                                           WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                                           PixelFormat.TRANSLUCENT);
        params.x = 0;
        params.y = 0;
        params.width = 0;
        params.height = 0;

        final FrameLayout frame = new FrameLayout(this);
        frame.addView(webView);
        winManager.addView(frame, params);

        webView.loadUrl("http://stackoverflow.com");

        return START_STICKY;
    }

    private final WebViewClient client = new WebViewClient() {
        public void onPageFinished(WebView view, String url) {
            final Point p = new Point();
            winManager.getDefaultDisplay().getSize(p);

            webView.measure(MeasureSpec.makeMeasureSpec((p.x < p.y ? p.y : p.x),
                                MeasureSpec.EXACTLY), 
                            MeasureSpec.makeMeasureSpec((p.x < p.y ? p.x : p.y),
                                MeasureSpec.EXACTLY));
            webView.layout(0, 0, webView.getMeasuredWidth(), webView.getMeasuredHeight());

            webView.postDelayed(capture, 1000);
        }
    };

    private final Runnable capture = new Runnable() {
        @Override
        public void run() {
            try {
                final Bitmap bmp = Bitmap.createBitmap(webView.getWidth(),
                    webView.getHeight(), Bitmap.Config.ARGB_8888);
                final Canvas c = new Canvas(bmp);
                webView.draw(c);

                updateWidgets(bmp);
            }
            catch (IllegalArgumentException e) {
                e.printStackTrace();
            }

            stopSelf();
        }
    };

    private void updateWidgets(Bitmap bmp) {
        final AppWidgetManager widgetManager = AppWidgetManager.getInstance(this);
        final int[] ids = widgetManager.getAppWidgetIds(
            new ComponentName(this, WebWidgetProvider.class));

        if (ids.length < 1) {
            return;
        }

        final RemoteViews views = new RemoteViews(getPackageName(), R.layout.widget_layout);
        views.setImageViewBitmap(R.id.widget_image, bmp);
        widgetManager.updateAppWidget(ids, views);

        Toast.makeText(this, "WebWidget Update", 0).show();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

Here's a screenshot of the Widgety goodness.


Adding to the above answer,

For people who get this error - Unable to add window. Permission denied for this window type at least in my case with devices running marshmallow the type need to be changed.

Simply replace

WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY to either

WindowManager.LayoutParams.TYPE_TOAST (or)

WindowManager.LayoutParams.TYPE_APPLICATION_PANEL

and it will work perfectly fine.

Note: Checked and confirmed with Nexus6p running the latest version of marsmallow.

The WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY requires the permission android.permission.INTERNAL_SYSTEM_WINDOW which can be added only for android system related apps.

Also don't forget to change the web view height and width once the page has finished loading at onPageFinished() for people who are looking to parse data using Javascript.

I completely overlooked the importance of this in the above answer and ended up wasting a lot of time.

Initially my assumption was since my requirement was not to display the contents in the web view to the widget via an Image view this was not required.

As it turns out javascript cannot parse the data from the web view unless the view width and height is not zero.


Need Your Help

What properties can I set via an UIAppearance proxy?

iphone ios ios5 ios6 uikit

What properties can I set via an UIAppearance proxy? Apple's UIKit documentation does not list them. Is there a list of these properties?