zaplitny
zaplitny

Reputation: 416

Chronometer doesn't update in app widget ListView

I have an app widget displaying timers. Timers can be started, stopped or resumed.

On Android 8 timers don't tick sometimes (50/50). Some users complained about the issue on Android 7 but I'm not absolutely sure if it's the same issue. Everything works well on Nexus 5 with Android 6 installed. If scroll down list view (until chronometer is invisible) and scroll up - timer starts ticking. If I put one more Chronometer above ListView and start - the chronometer is ticking well

ActivitiesRemoteViewsFactory

public RemoteViews getViewAt(int position) {
...

    RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), getItemLayoutId());

    if (timeLog.getState() == TimeLog.TimeLogState.RUNNING) {          

        remoteViews.setChronometer(R.id.widget_timer,
                SystemClock.elapsedRealtime() - (duration * 1000 + System.currentTimeMillis() - timeLog.getStartDate().getTime()), null, true);
    } else {
        long timerValue = SystemClock.elapsedRealtime() - duration * 1000;
        remoteViews.setChronometer(R.id.widget_timer, timerValue, null, false);

    }

    return remoteViews;

Update is sent from AppWidgetProvider's onReceive method

public void onReceive(Context context, Intent intent) {   
   AppWidgetManager widgetManager = AppWidgetManager.getInstance(ctx);
    ...
   widgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_activities_list);
}

TargetSDK is 25

UPDATE The issue is also reproduced in main app. Something is wrong with listView as it works well in RecyclerView. Added simple example reproducing the issue at https://github.com/zaplitny/WidgetIssue

Upvotes: 14

Views: 1410

Answers (2)

Sagar
Sagar

Reputation: 554

Add this in you application manifest :

 <!-- Widget Receiver -->
        <receiver android:name=".widgets.WidgetProvider">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

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

Now when you want to update widget from application call this:

                  try {
                        Intent intent = new Intent(ctxt, WidgetProvider.class);
                        intent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
                        int ids[] = AppWidgetManager.getInstance(ctxt).getAppWidgetIds(new ComponentName(ctxt, WidgetProvider.class));
                        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,ids);
                        ctxt.sendBroadcast(intent);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

I have used this to update my widget. Thank you.

Upvotes: 0

Cheticamp
Cheticamp

Reputation: 62831

The Chronometer widget was updated between API 25 and API 26 to prevent non-visible timers from updating. (That is my best guess from looking at the underlying code.) Specifically, the updateRunning() method was changed to take into account whether the widget is shown or not. From Chronometer.java:

For API 25: boolean running = mVisible && mStarted;

For API 26: boolean running = mVisible && mStarted && isShown();

That isShown() is the source of your problem and it stops your Chronometers from being reused. For API 26+, reused Chronometers do not get updated, it seems, when they are in a ListView.

There are several ways to take care of this issue. The first is to not reuse Chronometers. In getView() of your adapters, you can use the following code:

if (view == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    view = LayoutInflater.from(getContext()).inflate(R.layout.widget_timer, parent, false);
}

With this code, you will always create new Chronometers and never reuse them.

An alternative to this is to call chronometer.start() after the widget is laid out and isShown() will be true. Add the following code to getView():

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
    chronometer.start();
} else {
    view.post(new Runnable() {
        @Override
        public void run() {
            chronometer.start();
        }
    });
} 

Either way works and there are other ways.

Upvotes: 10

Related Questions