Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to really update a widget precisely every minute

I'm stumped. I know this question has already been answered a hundred times but nothing I've tried works.

My question: I made an Android widget that needs to refresh precisely at each minute, much like all clock widgets do. (This widget tells me in how many minutes are left before my train leaves, a one minute error makes it useless).

Here are my attempts to far, and the respective outcomes:

  • I put android:updatePeriodMillis="60000" in my appwidget_info.xml. However, as specified in API Docs, "Updates requested with updatePeriodMillis will not be delivered more than once every 30 minutes" and indeed that's about how often my widget gets updated.
  • I tried using an AlarmManager. In my WidgetProvider.onEnabled:

    AlarmManager am = (AlarmManager)context.getSystemService
            (Context.ALARM_SERVICE);
    Calendar calendar = Calendar.getInstance();
    long now = System.currentTimeMillis();
    // start at the next minute
    calendar.setTimeInMillis(now + 60000 - (now % 60000));
    am.setRepeating(AlarmManager.RTC, calendar.getTimeInMillis(), 60000,
            createUpdateIntent(context));
    

    however as stated in the API docs, "as of API 19, all repeating alarms are inexact" and indeed my widget actually gets updated every five minutes or so.

  • Based on the previous point I tried setting targetSdkVersion to 18 and saw no difference (updates every five minutes or so).
  • The setRepeating documentation seems to recommend using setExact. I tried the following. At the end of my update logic:

    Calendar calendar = Calendar.getInstance();
    long now = System.currentTimeMillis();
    long delta = 60000 - (now % 60000);
    
    Log.d(LOG_TAG, "Scheduling another update in "+ (delta/1000) +" seconds");
    calendar.setTimeInMillis(now + delta);
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    alarmManager.setExact(AlarmManager.RTC, calendar.getTimeInMillis(), //UPDATE_PERIOD_SECONDS * 1000,
            createUpdateIntent(context));
    

    It works perfectly for a couple minutes and then reverts to updating every five minutes or so (and not even near minute changes). Here are some timestamps of when the update intent is received:

    • 21:44:17.962
    • 21:52:37.232
    • 21:59:13.872
    • 22:00:00.012 ← hey suddenly it becomes exact again??
    • 22:01:47.352
    • 22:02:25.132
    • 22:06:56.202
  • Some recommend using a Handler. I defined a Service which I start when the widget provider is enabled, and does this after update code:

    int delay = (int)(60000 - (System.currentTimeMillis() % 60000));
    Log.d(LOG_TAG, "Scheduling another update in " + delay/1000 + " seconds");
    new Handler().postDelayed(new Runnable() {
        public void run() {
            Log.d(LOG_TAG, "Scheduled update running");
            updateAppWidget();
        }
    }, delay);
    

    and this one works perfectly for several hours, but then the service gets suddenly killed and gets "scheduled to restart after HUGE delay". Concretely, the widget just gets stuck at some point and doesn't get updated at all.

Some other options I've seen online: the linked post above suggests creating a foreground service (which, if I understand correctly, means having a permanently visible icon in my already crowded status bar. I don't have one permanent icon for each clock widget I use so that should not be necessary). Another suggestion is to run a high priority thread from the service, which feels awfully overkill.

I've also seen recommendations to use Timers and BroadcastReceivers but the former is said to be "not appropriate for the task" and I remember having trouble doing the latter. I think I had to do it in a service and then the service gets killed just like when I use Handlers.

It should be noted that the AlarmManager seems to work well when the phone is connected to the computer (presumably because it means the battery is charging), which doesn't help because most of the time I want to know when my train will leave is when I'm already on the way...

As the Handler is perfectly accurate but just stops working after a while, and the AlarmManager option is too inaccurate but does not stop working, I'm thinking of combining them by having AlarmManager start a service every ten minutes or so, and have that service use a Handler to update the display each minute. Somehow I feel this will get detected by Android as a power hog and get killed, and anyway I'm sure I must be missing something obvious. It shouldn't be that hard to do what's essentially a text-only clock widget.

EDIT: if it matters, I'm using my widget on a Samsung Galaxy Note 4 (2016-06-01) with Android 6.0.1.

like image 755
tendays Avatar asked Jul 30 '16 20:07

tendays


People also ask

How often do Apple widgets update?

For a widget the user frequently views, a daily budget typically includes from 40 to 70 refreshes. This rate roughly translates to widget reloads every 15 to 60 minutes, but it's common for these intervals to vary due to the many factors involved. The system takes a few days to learn the user's behavior.

How do I refresh widgets?

You need to open the app from the widget for a minute and close it. The widget will fetch new information from the app. You don't need to find the app from the folder, App Library, or home screen pages.

How do you refresh widgets on IOS 14?

Sometimes all you need to do to update the information display on a widget is to tap on it and open the app. When the app opens, its content will refresh, which should refresh the widget too. After tapping on the widget and opening the corresponding app, return to the Home screen to see if the issue persists.

Why is my clock widget not updating?

Restart Your Device If the internet is working fine and you're still facing trouble with your widgets, then you can try restarting your Android to see if it helps. Often, issues such as widgets not updating are easily resolved by merely rebooting your Android device.


1 Answers

Sorry, i totally forgot, was busy.. Well, i hope you got the idea of what you need, snippets are following, hope i dod not forgot something.

on the widget provider class.

public static final String ACTION_TICK = "CLOCK_TICK";
public static final String SETTINGS_CHANGED = "SETTINGS_CHANGED";
public static final String JOB_TICK = "JOB_CLOCK_TICK";

 @Override
    public void onReceive(Context context, Intent intent){
        super.onReceive(context, intent);

        preferences =  PreferenceManager.getDefaultSharedPreferences(context);

        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        ComponentName thisAppWidget = new ComponentName(context.getPackageName(), WidgetProvider.class.getName());
        int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget);

        if (intent.getAction().equals(SETTINGS_CHANGED)) {
            onUpdate(context, appWidgetManager, appWidgetIds);
            if (appWidgetIds.length > 0) {
                restartAll(context);
            }
        }

        if (intent.getAction().equals(JOB_TICK) || intent.getAction().equals(ACTION_TICK) ||
                intent.getAction().equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
                || intent.getAction().equals(Intent.ACTION_DATE_CHANGED)
                || intent.getAction().equals(Intent.ACTION_TIME_CHANGED)
                || intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {
            restartAll(context);
            onUpdate(context, appWidgetManager, appWidgetIds);
        }
    }

private void restartAll(Context context){
        Intent serviceBG = new Intent(context.getApplicationContext(), WidgetBackgroundService.class);
        context.getApplicationContext().startService(serviceBG);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            scheduleJob(context);
        } else {
            AppWidgetAlarm appWidgetAlarm = new AppWidgetAlarm(context.getApplicationContext());
            appWidgetAlarm.startAlarm();
        }
    }



 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void scheduleJob(Context context) {
        ComponentName serviceComponent = new ComponentName(context.getPackageName(), RepeatingJob.class.getName());
        JobInfo.Builder builder = new JobInfo.Builder(0, serviceComponent);
        builder.setPersisted(true);
        builder.setPeriodic(600000);
        JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        int jobResult = jobScheduler.schedule(builder.build());
        if (jobResult == JobScheduler.RESULT_SUCCESS){
        }
    }


    @Override
    public void onEnabled(Context context){

        restartAll(context);
    }


    @Override
    public void onDisabled(Context context){

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
            jobScheduler.cancelAll();
        } else {
            // stop alarm
            AppWidgetAlarm appWidgetAlarm = new AppWidgetAlarm(context.getApplicationContext());
            appWidgetAlarm.stopAlarm();
        }

        Intent serviceBG = new Intent(context.getApplicationContext(), WidgetBackgroundService.class);
        serviceBG.putExtra("SHUTDOWN", true);
        context.getApplicationContext().startService(serviceBG);
        context.getApplicationContext().stopService(serviceBG);
    }

WidgetBackgroundService

public class WidgetBackgroundService extends Service {

        private static final String TAG = "WidgetBackground";
        private static BroadcastReceiver mMinuteTickReceiver;

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

        @Override
        public void onCreate(){
            super.onCreate();
        }



        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            if(intent != null) {
                if (intent.hasExtra("SHUTDOWN")) {
                    if (intent.getBooleanExtra("SHUTDOWN", false)) {

                        if(mMinuteTickReceiver!=null) {
                            unregisterReceiver(mMinuteTickReceiver);
                            mMinuteTickReceiver = null;
                        }
                        stopSelf();
                        return START_NOT_STICKY;
                    }
                }
            }

            if(mMinuteTickReceiver==null) {
                registerOnTickReceiver();
            }
            // We want this service to continue running until it is explicitly
            // stopped, so return sticky.
            return START_STICKY;
        }

        @Override
        public void onDestroy(){
            if(mMinuteTickReceiver!=null) {
                unregisterReceiver(mMinuteTickReceiver);
                mMinuteTickReceiver = null;
            }

            super.onDestroy();
        }

        private void registerOnTickReceiver() {
            mMinuteTickReceiver = new BroadcastReceiver(){
                @Override
                public void onReceive(Context context, Intent intent){
                    Intent timeTick=new Intent(WidgetProvider.ACTION_TICK);
                    sendBroadcast(timeTick);
                }
            };
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_TIME_TICK);
            filter.addAction(Intent.ACTION_SCREEN_ON);
            registerReceiver(mMinuteTickReceiver, filter);
        }
}

RepeatingJob class

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class RepeatingJob extends JobService {
    private final static String TAG = "RepeatingJob";

    @Override
    public boolean onStartJob(JobParameters params) {
        Log.d(TAG, "onStartJob");
        Intent intent=new Intent(WidgetProvider.JOB_TICK);
        sendBroadcast(intent);
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        return false;
    }
}

AppWidgetAlarm class

public class AppWidgetAlarm {
    private static final String TAG = "AppWidgetAlarm";

    private final int ALARM_ID = 0;
    private static final int INTERVAL_MILLIS = 240000;
    private Context mContext;


    public AppWidgetAlarm(Context context){
        mContext = context;
    }


    public void startAlarm() {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MILLISECOND, INTERVAL_MILLIS);
        AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
        Intent alarmIntent = new Intent(WidgetProvider.ACTION_TICK);
        PendingIntent removedIntent = PendingIntent.getBroadcast(mContext, ALARM_ID, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, ALARM_ID, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        Log.d(TAG, "StartAlarm");
        alarmManager.cancel(removedIntent);
        // needs RTC_WAKEUP to wake the device
        alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), INTERVAL_MILLIS, pendingIntent);
    }

    public void stopAlarm()
    {
        Log.d(TAG, "StopAlarm");

        Intent alarmIntent = new Intent(WidgetProvider.ACTION_TICK);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, ALARM_ID, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);

        AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
        alarmManager.cancel(pendingIntent);
    }
}

manifest

<receiver android:name=".services.SlowWidgetProvider" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <intent-filter>
        <action android:name="CLOCK_TICK" />
    </intent-filter>
    <intent-filter>
        <action android:name="JOB_CLOCK_TICK" />
    </intent-filter>
    <intent-filter>
        <action android:name="SETTINGS_CHANGED" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.TIME_SET" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.TIMEZONE_CHANGED" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.DATE_CHANGED" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.os.action.DEVICE_IDLE_MODE_CHANGED"/>
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.ACTION_DREAMING_STOPPED" />
    </intent-filter>
    <meta-data android:name="android.appwidget.provider"
        android:resource="@xml/slow_widget_info" />
</receiver>

<service
    android:name=".services.RepeatingJob"
    android:permission="android.permission.BIND_JOB_SERVICE"
    android:exported="true"/>

<service android:name=".services.WidgetBackgroundService" />
like image 73
Nikiforos Avatar answered Sep 21 '22 10:09

Nikiforos