14

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

zaplitny
  • 416
  • 4
  • 19

2 Answers2

10

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.

Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • @zaplitny IMO this is a bug in `Chronometer`. The code I presented will work around the problem. – Cheticamp Mar 06 '18 at 13:44
  • sorry I deleted my previous comment as I missed some details in your answer. I need chronometer in AppWidget and AFAIK it's not possible with your workaround. But I may be wrong – zaplitny Mar 06 '18 at 13:47
  • @zaplitny The code I presented works with the test app you provided. Don't know about `AppWidget`, though. I surmise that it is the same problem. It still uses a `ListView`, right? So, a check for API 26+ to suppress the reuse of views should work. – Cheticamp Mar 06 '18 at 13:51
  • @zaplitny I see what you mean about the `AppWidget` - the `ListView` is internal so you don't have access like in the test app. I still think that the issue has been identified. The question is how to export a fix to `AppWidget`. – Cheticamp Mar 06 '18 at 14:04
  • Just for information, I posted the issue to Google Dev team - https://issuetracker.google.com/issues/74058586. Everyone interested can vote – zaplitny Mar 06 '18 at 15:03
  • @zaplitny Got my vote. I looked at this again, but I didn't see a work around for the widget implementation. I think that you will have this problem until the underlying issue is fixed. There is such a small keyhole view into the widget machinery. – Cheticamp Mar 08 '18 at 00:09
  • 2
    issuetracker.google.com/issues/74058586 is marked as "Won't Fix (Obsolete)" at Jun 27, 2019 07:38AM... Cheticamp options still working! :) Thanks! – Joan Casadellà Dec 18 '19 at 10:13
0

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.

Sagar
  • 554
  • 5
  • 21
  • The question is not how to update widget. Widget content is updated well but Chronometer doesn't work as expected in ListView – zaplitny Mar 09 '18 at 23:20