如何确保小部件每分钟精确更新

11

我被难住了。我知道这个问题已经有一百个答案,但是我试过的所有方法都不起作用。

我的问题是:我制作了一个 Android 小部件,需要每分钟精确地刷新,就像所有时钟小部件一样。(这个小部件告诉我火车还有多少分钟就要离开了,一分钟的误差会使它变得无用)。

这是我到目前为止尝试过的方法以及相应的结果:

  • 我在我的appwidget_info.xml中添加了android:updatePeriodMillis="60000"。然而,如API文档所指定的那样,“使用updatePeriodMillis请求的更新将不会每30分钟以上传递一次”,实际上,这就是我的小部件更新的频率。
  • 我尝试使用AlarmManager。在我的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));

然而,根据API文档所述,“从API 19开始,所有重复的闹钟都是不精确的”,实际上我的小部件每隔五分钟左右才更新一次。

  • 基于上面的观点,我尝试将targetSdkVersion设置为18,但没有看到任何区别(每隔五分钟左右更新)。
  • setRepeating文档似乎建议使用setExact。我尝试了以下内容。在我的更新逻辑结束时:

    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));
    

    它可以完美地运行几分钟,然后恢复到每五分钟更新一次(甚至没有接近一分钟的更改)。这是一些更新意图接收的时间戳:

    • 21:44:17.962
    • 21:52:37.232
    • 21:59:13.872
    • 22:00:00.012 ← 嘿,突然变得精确了吗?
    • 22:01:47.352
    • 22:02:25.132
    • 22:06:56.202

    一些人建议使用Handler。我定义了一个Service,在小部件提供程序启用时启动,并在更新代码之后执行此操作:

    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);
    

    这个闹钟的应用在几小时内工作得完美,但突然服务被杀死并在长时间后才会"计划重新启动"。具体地说,小部件在某个点上就被卡住了,根本没有更新。

    我在网上看到了一些其他选项: 上面链接的帖子建议创建一个前台服务(如果我理解正确,这意味着在我的已经拥挤的状态栏中有一个永久可见的图标。我没有为每个闹钟小部件使用一个永久的图标,所以这不是必要的)。另一个建议是从服务中运行高优先级线程,这感觉非常过度kill。

    我也看到了一些使用TimersBroadcastReceiver的建议,但前者被认为是"不适合此任务",而我记得在做后者时遇到了麻烦。我想我必须在一个服务中执行它,然后该服务就像使用Handler时一样被杀死。

    值得注意的是,在手机连接到计算机时,AlarmManager似乎工作良好(可能是因为这意味着电池正在充电),但这并没有帮助,因为大多数时候我想知道我的火车何时离开是当我已经在路上时...

    由于Handler完全准确但一段时间后停止工作,而AlarmManager选项太不准确,但不会停止工作,所以我考虑通过让AlarmManager每十分钟左右启动一个服务,并让该服务使用Handler每分钟更新显示来将它们组合起来。不知怎么的,我感觉Android会将其检测为一个耗电量很高的程序并将其杀死,而且无论如何,我肯定会错过一些明显的东西。做一个基本上只有文本的闹钟小部件不应该那么难。

    编辑: 如果有关系的话,我在使用三星Galaxy Note 4 (2016-06-01)和Android 6.0.1上使用我的小部件。


  • 1
    你真的解决了这个问题吗? 我面临着几乎相同的问题。我不关心到秒的精确度,我关心的是不要停止,这在Android 6+设备上一直发生。 我跳过了setRepeating(),选择了setExactAndAllowWhileIdle来解决这个问题,但仍然存在 - Nikiforos
    1
    不,我最好的解决方法是使用 AlarmManager ,它有时每分钟更新一次,有时更新较慢(长达五到六分钟)。闹钟小部件如何工作对我来说仍然是个谜。 - tendays
    1
    @Nikiforos:你解决了这个问题吗?我被卡在同样的问题上好多天了。 - user1090751
    @user1090751 实际上我是使用了“全对全”策略。 - Nikiforos
    1
    @user1090751,我昨天已经在下面的回复中发送了代码!https://dev59.com/iFkT5IYBdhLWcg3wZehe#43920978 - Nikiforos
    显示剩余5条评论
    5个回答

    4
    抱歉,我完全忘记了,非常忙碌。我希望你已经明白了你需要的东西,代码片段如下,希望我没有漏掉什么。
    关于小部件提供者类。
    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);
            }
    }
    

    重复作业类

    @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类

    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);
        }
    }
    

    清单文件
    <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" />
    

    如果我们故意关闭屏幕然后重新打开,小部件可能会有几分钟没有更新。这个问题有什么解决方案吗? - user1090751
    1
    “小部件更新代码”始终位于“onupdate”方法中,我通过“onUpdate(context, appWidgetManager, appWidgetIds);”在我的“onReceive”方法中调用它。闹钟管理器每4分钟发送一个“ACTION_TICK”(也称为INTERVAL_MILLIS = 240.000,您可以更改它)。此ACTION_TICK在“onReceive方法”上接收。 - Nikiforos
    @user1090751请发布一个新帖子,包含您的问题、代码和错误日志。这个问题与那个不相关。 - Nikiforos
    由于在后台运行应用程序时使用startService(),因此现在在针对API 26 Oreo的情况下会崩溃:( - Carson Holzheimer
    @CarsonHolzheimer,疼,是在使用API 26构建时发生的,还是在实际运行Oreo时发生的? - Nikiforos
    显示剩余4条评论

    1
    The code snippets provided by @Nikiforos were very helpful for me in programming, but I encountered many problems when using them on Android 8. Therefore, I decided to share how I solved these issues. There are two problems related to the provided snippets:
    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 the first issue, I had to switch from BackgroundService to ForegroundService. I understand that this may not be possible in many cases, but for those who can make 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();
        }
    }
    

    更新您的WidgetBackgroundService代码中的onStartCommand()函数:

    @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. 在你的WidgetBackgroundService中添加sendImplicitBroadcast()函数:

    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. 按以下方式修改registerOnTickReceiver()函数:

    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);
    }
    

    希望它有所帮助!

    0

    使用小部件本身作为延迟运行的主机。小部件具有postDelayed方法。

    如果小部件被杀死并重新创建,则在基本初始化的一部分中重新创建可运行项。

    编辑:

    以上建议是基于不准确的假设,即OP正在编写自定义视图而不是应用程序小部件。对于应用程序小部件,我最好的建议是:

    • 创建一个带有一个图标的前台服务。
    • 该服务管理所有小部件,单击通知图标将显示各种活动提醒,并允许管理它们

    谢谢您的回复,但我并不真正理解您的建议。 "小部件本身"?我看到View类有一个postDelayed方法,但我无法访问它(对于View)。由于我使用应用程序小部件,所以我只有一个RemoteViews对象,整数appWidgetIdAppWidgetManager(没有一个具有postDelayed方法)... - tendays
    唉,现在我明白了,我曾认为小部件是指自定义视图,而不是 Android 应用小部件。我有一段时间没有玩过应用小部件了,但我会在晚上考虑一下... - RabidMutant
    更新,单个前台服务似乎是唯一的方法。 - RabidMutant
    正如我在问题中所写的,我不想在我的通知区域(已经很拥挤了)中放置一个小部件图标,而这个小部件本质上就像一个时钟。标准的时钟小部件可以很好地工作,而且不需要前台服务(但可能使用未公开的API??)。 - tendays

    0

    关于每分钟更新小部件的正确和完全可行的答案是不存在的。Android操作系统开发人员故意排除了这样的功能或API,以节省电池和工作负载。

    对于我的情况,我尝试创建时钟主屏幕小部件,并尝试使用闹钟管理器、服务等多种方法。

    它们都没有正常工作。

    对于那些想要创建需要精确每分钟更新时间的时钟小部件的人们。

    只需使用

     <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" />
    

    数字时钟文本视图和

    模拟时钟

    <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" />
    

    我从Google桌面时钟开源项目中找到了这些代码。你可能已经知道,谷歌时钟有一个小部件,每分钟都能精确更新。

    了解更多信息,请访问Google Desk Clock Opensource Repo


    -1

    试试这段代码

        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);
    

    1
    正如方法名称所示,重复间隔是“不精确的”,因此可能会延迟几分钟。 - tendays

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接