How to really update a widget precisely every minute
Asked Answered
H

5

11

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.

Hydrotherapy answered 30/7, 2016 at 20:34 Comment(10)
Did you actually solve this? I am facing almost the same problems. I do not care about being precise to the second, i care about not stopping, something that keeps happening on Android 6+ devices. I skipped setRepeating() and gone for setExactAndAllowWhileIdle to counter this, but stillAtion
No, the best I've managed is AlarmManager which updates sometimes every minute, sometimes slower (up to five or six minutes). How clock widgets can work is a mystery to me.Hydrotherapy
@Ation : Did you solved the problem? I am stuck on the same problem for so many days.Clarkclarke
@Clarkclarke Actually i did by using an "all for all" strategy.Ation
@Clarkclarke (edit failed) I used a job sceduler for android 5,6,7 and a repeating alarm for android 4. They all (re)start a service which i am trying to keep up at all times. The service registers for ACTION_TIME_TICK and ACTION_SCREEN_ON. When it receives them it throws the ACTION_TICK which is register for the widget. When onReceive() method gets any of the ticks, it restarts everything (Job sceduler, alarm, service) to keep up. Clock gets updated every minute, and never stops for Android 4.4-7. If you have any more questions, i could provide some code when i find some time.Ation
@Ation : Thank you for your reply. I will be glad to see the code and get help myself understand it.Clarkclarke
Sorry @Clarkclarke , forgot/was busy.. i replied as an awnser.Ation
@Ation : Thanks for your reply despite being busy. You can send me the code when you are comfortable.Clarkclarke
@Clarkclarke i sent the code as a reply underneath yesterday! https://mcmap.net/q/1028825/-how-to-really-update-a-widget-precisely-every-minuteAtion
@Clarkclarke i would be happy with an upvote if my answer helped you :)Ation
A
4

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" />
Ation answered 11/5, 2017 at 16:26 Comment(9)
Widget is not updated for some minutes if we deliberately off the screen and on the screen. Is there any solutions to this particular problem?Clarkclarke
hm? I haven't noticed anything like that. It is supossed to "trigger" every time the screen goes on. And, as far as i had tested it, it was working.Ation
For Api 4 that uses alarm manager, i am confused where to place the widget update code and how it works..Clarkclarke
The "widget update code" is always in the "onupdate" method which i invoke by "onUpdate(context, appWidgetManager, appWidgetIds);" in my "onReceive" method. The Alarm Manager sends an "ACTION_TICK" every 4 minutes (aka INTERVAL_MILLIS = 240.000 , you can change that). This ACTION_TICK is received on "onReceive method"Ation
@Clarkclarke its "code" , you can do anything you want. You can request a NotificationManager instance and update notificationsAtion
@Clarkclarke Make a new post with your question, code and error logs.. This question is not about thatAtion
What should i do to add another widget? Should i duplicate all the files RepeatingJob.java, ClockWidget, AppWidgetAlarm, WidgetBackgroundService etc?Clarkclarke
This now crashes oif targeting API 26 Oreo due to startService() while app is in the background :(Efficacy
@CarsonHolzheimer , ouch, does it happen when building with API 26, or when actually running on Oreo ?Ation
H
1

The code snippets provided by @Nikiforos was a blessing for me, although I've felt into many problems when using them on Android 8, thus I decided to let you know how I've solved my issues. There are two problems related with the snippets provided:

  1. they use BackgroundService which is now forbidden in some cases in Android 8
  2. they use implicit broadcasts which have also been restricted in Android O (you can read about why it happened here)

To address first issue I had to switch from BackgroundService to ForegroundService. I know this is not possible in many cases, but for those who can do the change here are the instructions to modify the codes:

  1. Change the restartAll() function as follows:

private void restartAll(Context context){
    Intent serviceBG = new Intent(context.getApplicationContext(), WidgetBackgroundService.class);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        // for Android 8 start the service in foreground
        context.startForegroundService(serviceBG);
    } else {
        context.startService(serviceBG);
    }
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        scheduleJob(context);
    } else {
        AppWidgetAlarm appWidgetAlarm = new AppWidgetAlarm(context.getApplicationContext());
        appWidgetAlarm.startAlarm();
    }
}

  1. Update the onStartCommand() function in your WidgetBackgroundService code:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    // for Android 8 bring the service to foreground
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
        startForeground(1, buildForegroundNotification("Test 3"));
    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;
}

  1. Add sendImplicitBroadcast() function to your WidgetBackgroundService:

private static void sendImplicitBroadcast(Context ctxt, Intent i) {
    PackageManager pm=ctxt.getPackageManager();
    List<ResolveInfo> matches=pm.queryBroadcastReceivers(i, 0);

    for (ResolveInfo resolveInfo : matches) {
        Intent explicit=new Intent(i);
        ComponentName cn=
                new ComponentName(resolveInfo.activityInfo.applicationInfo.packageName,
                        resolveInfo.activityInfo.name);

        explicit.setComponent(cn);
        ctxt.sendBroadcast(explicit);
    }
}

  1. Modify registerOnTickReceiver() function in the following way:

private void registerOnTickReceiver() {
    mMinuteTickReceiver = new BroadcastReceiver(){
        @Override
        public void onReceive(Context context, Intent intent){
            Intent timeTick=new Intent(LifeTimerClockWidget.ACTION_TICK);
            // for Android 8 send an explicit broadcast
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
                sendImplicitBroadcast(context, timeTick);
            else
                sendBroadcast(timeTick);
        }
    };
    IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_TIME_TICK);
    filter.addAction(Intent.ACTION_SCREEN_ON);
    registerReceiver(mMinuteTickReceiver, filter);
}

Hope it helps!

Hospitable answered 15/6, 2018 at 8:45 Comment(0)
C
0

Use the widget itself as the host for the delayed runnable. Widgets have a postDelayed method.

If the widget is killed and recreated, then also recreate the runnable as part of the basic initialization.

Edit:

The above suggestion was based on the inaccurate assumption that the OP was writing a custom view, not an app widget. For an app widget my best suggestion is:

  • create a foreground service with ONE icon.
  • the service manages all widgets and clicking on the notification icon will show the various reminders that are active and/allow them to be managed
Civil answered 31/7, 2016 at 0:29 Comment(4)
Thanks for your reply, but I don't really understand your suggestion. "The widget itself"? I see the View class has a postDelayed method, but I don't have access to it (to the View). As I work with app widgets, all I have is a RemoteViews object, the integer appWidgetId and the AppWidgetManager (none which doesn't have a postDelayed method)...Hydrotherapy
Argh. Now I understand, I had taken widget to mean a custom view, not an android app widget. I haven't played with App Widgets in a while, but will give it some thought overnight...Civil
Updated, a single foreground service seems the only wayCivil
As I wrote in the question I don't want an icon in my (already crowded) notification area for a widget essentially behaving like a clock. The standard clock widgets work fine and don't require foreground services (but maybe use undocumented API??)Hydrotherapy
V
0

There is no correct and fully working answer to widget update every minute. Android OS developer purposely exclude such feature or api in order to save the battery and workload. For my case, I tried to create clock homescreen appwidget and tried many attempt on alarm manager, service etc.

None of them are working correctly.

For those who want to create Clock Widget, which need update time everyminute precisely.

Just use

 <TextClock
    android:id="@+id/clock"
    style="@style/widget_big_thin"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal|top"
    android:ellipsize="none"
    android:format12Hour="@string/lock_screen_12_hour_format"
    android:format24Hour="@string/lock_screen_24_hour_format"
    android:includeFontPadding="false"
    android:singleLine="true"
    android:textColor="@color/white" />

for digital clock text view and

For Analog Clock

<AnalogClock xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/analog_appwidget"
android:dial="@drawable/appwidget_clock_dial"
android:hand_hour="@drawable/appwidget_clock_hour"
android:hand_minute="@drawable/appwidget_clock_minute"
android:layout_width="match_parent"
android:layout_height="match_parent" />

I've found those code from Google Desk Clock Opensource Project. You may already know Google Clock has such widget which update precisely every minute.

To learn more Google Desk Clock Opensource Repo

Vino answered 3/1, 2022 at 11:4 Comment(0)
S
-1

Try this code

    Intent intent = new Intent(ACTION_AUTO_UPDATE_WIDGET);
    PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(System.currentTimeMillis());
    calendar.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE) + 1);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 0);

    AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    alarmMgr.setInexactRepeating(AlarmManager.RTC, calendar.getTimeInMillis(), 60 * 1000, alarmIntent);
Sardinia answered 27/8, 2016 at 10:50 Comment(1)
As it says in the method name the repeating interval is "inexact", so it can be delayed by several minutes.Hydrotherapy

© 2022 - 2024 — McMap. All rights reserved.