Skip to content

Blocking NetworkStatsManager.querySummaryForDevice() call during widget update #19

@venkyqz

Description

@venkyqz

Hi Average Data Usage Widget Team,

I’m a PhD student researching Android thread-related issues. My research group recently ran a static analysis scan for thread-related bugs in real-world Android apps, and our prototype flagged a potential issue in Average Data Usage Widget.

Checked target

  • Source-level caller: com.trianguloy.continuousDataUsage.common.DataUsage.getDataFromPeriod(long, long)
  • Detected API / pattern: synchronous NetworkStatsManager.querySummaryForDevice(...)
  • Observed context: app widget update / AppWidgetProvider dispatch path on the main thread
  • Expected context: worker/background thread before querying network-usage statistics

What I found

DataUsage.getDataFromPeriod(long, long) directly calls NetworkStatsManager.querySummaryForDevice(...):

public double getDataFromPeriod(long from, long to) throws Error {
    //get data
    NetworkStats.Bucket bucket;
    try {
        bucket = getNsm().querySummaryForDevice(
            ConnectivityManager.TYPE_MOBILE,
            null,
            from,
            to
        );
    } catch (RemoteException e) {
        Log.d("widget", "error on querySummaryForDevice-RemoteException");
        throw new Error(R.string.txt_widget_errorQuering);
    } catch (SecurityException se) {
        Log.d("widget", "error on querySummaryForDevice-SecurityException");
        throw new Error(R.string.txt_widget_noPermission);
    }

    double bytesConversion = pref.getAltConversion() ? 1f / 1000f / 1000f : 1f / 1024f / 1024f;
    return (bucket.getRxBytes() + bucket.getTxBytes()) * bytesConversion;
}

This data query is reachable from the widget update path. AppWidgetBase.onUpdate(...) iterates over widget ids and synchronously calls updateAppWidget(...):

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

For example, AppWidgetProgress.updateAppWidget(...) calls updateViews(context, views) synchronously:

void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
    ...
    RemoteViews views = new RemoteViews(context.getPackageName(), small ? R.layout.widget_progress_short : R.layout.widget_progress);
    updateViews(context, views);
    ...
    appWidgetManager.updateAppWidget(appWidgetId, views);
}

updateViews(...) calls getCommonInfo(context):

public static void updateViews(Context context, RemoteViews views) {
    ReturnedInfo commonInfo = getCommonInfo(context);
    ...
}

getCommonInfo(context) constructs DataUsage / Accumulated and then synchronously computes the current usage:

DataUsage dataUsage = new DataUsage(context, pref);
Accumulated accumulated = new Accumulated(pref, dataUsage, periodCalendar);
...
megabytes = accumulated.getUsedDataFromCurrentPeriod();

Accumulated.getUsedDataFromCurrentPeriod() directly calls dataUsage.getDataFromPeriod(...):

public double getUsedDataFromCurrentPeriod() throws DataUsage.Error {
    double usedData = dataUsage.getDataFromPeriod(
        periodCalendar.getLimitsOfPeriod(0).first,
        Long.MAX_VALUE
    );
    usedData -= pref.getAccumulated();
    return usedData;
}

The Android documentation for NetworkStatsManager.querySummaryForDevice(...) says this query may take a long time, apps should avoid calling it on the main thread, and it should only be called from a worker thread. Therefore, a widget update can synchronously reach a potentially slow system query from the main-thread receiver/update path.

Verified bug trace

Android widget update / broadcast dispatch
  -> AppWidgetProvider.onUpdate(...)
  -> AppWidgetBase.onUpdate(...)
  -> updateAppWidget(...)
  -> AppWidgetProgress.updateViews(...) / other widget updateViews(...)
  -> AppWidgetBase.getCommonInfo(...)
  -> Accumulated.getUsedDataFromCurrentPeriod()
  -> DataUsage.getDataFromPeriod(...)
  -> NetworkStatsManager.querySummaryForDevice(...)
  -> potentially slow network-usage statistics query runs on the main thread

This looks like a source-verified candidate because the latest source still calls querySummaryForDevice(...) synchronously in DataUsage.getDataFromPeriod(...), and the widget update path reaches it without an Executor, background Thread, WorkManager, or other worker-thread hop.

I also searched the current GitHub issues for this pattern using terms such as querySummaryForDevice, NetworkStatsManager, worker thread, and ANR widget update, and did not find an existing matching discussion.

Why this matters

NetworkStatsManager queries can be slow because they ask the system for historical network-usage statistics over a time range. The Android API documentation explicitly warns that querySummaryForDevice(...) may take several seconds and should only be called from a worker thread.

Widget updates and broadcast receiver dispatch are sensitive to main-thread blocking. If this query is slow, repeated widget refreshes or user-triggered widget actions may block the app main thread, causing visible delay, UI jank, or an ANR-risk pattern.

This issue may be hard to reproduce consistently because the query may be fast on some devices or for short intervals, but the current implementation is fragile: it performs a documented worker-thread-only query directly on the widget update path.

Possible fix

Move the network-usage query off the main thread and update the widget after the result is ready.

One possible structure is:

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    Executors.newSingleThreadExecutor().execute(() -> {
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    });
}

Alternatively, the app could keep widget update dispatch lightweight and move only the data calculation into a background helper:

Executors.newSingleThreadExecutor().execute(() -> {
    ReturnedInfo info = getCommonInfo(context);

    RemoteViews views = new RemoteViews(context.getPackageName(), layoutId);
    applyInfoToViews(context, views, info);

    appWidgetManager.updateAppWidget(appWidgetId, views);
});

For more robust scheduling, WorkManager or JobIntentService-style background work could also be used, especially if updates may be triggered repeatedly.

Reference

Relevant Android documentation wording:

This may take a long time, and apps should avoid calling this on their main thread. This method may take several seconds to complete, so it should only be called from a worker thread.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions