【问题标题】:Xamarin: Android Widget with timer, stops when app killedXamarin:带有计时器的Android Widget,当应用程序被杀死时停止
【发布时间】:2018-12-20 22:30:20
【问题描述】:

我有这个代码:

public class MyWidgetProvider : AppWidgetProvider
{
    public override void OnUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
    {
        Log.Debug("WIDGET", "Updating the widget");

        // Open app on click
        RemoteViews views = new RemoteViews(context.PackageName, Resource.Layout.MyWidget);

        Intent launchAppIntent = new Intent(context, typeof(MainActivity));
        PendingIntent launchAppPendingIntent = PendingIntent.GetActivity(context, 0, launchAppIntent, PendingIntentFlags.UpdateCurrent);
        views.SetOnClickPendingIntent(Resource.Id.main, launchAppPendingIntent);

        appWidgetManager.UpdateAppWidget(appWidgetIds[0], views);

        // Start timer
        System.Timers.Timer timer = new System.Timers.Timer();
        timer.Interval = 1000;
        timer.Elapsed += OnTimedEvent;
        timer.Enabled = true;
    }

    private void OnTimedEvent(object sender, ElapsedEventArgs e)
    {
        Log.Debug("WIDGET", "Updating status...");
        new Handler(Looper.MainLooper).Post(() =>
        {
          //Run my code to periodically update the widget
        });
    }
}

我想知道为什么会发生以下情况:

  1. 当我将小部件放在手机屏幕上时,计时器开始运行,这没关系。
  2. 当我点击应用程序启动的小部件时,计时器继续运行,这没关系。
  3. 当我点击后退按钮时,应用程序进入后台,计时器继续运行,这没关系。
  4. 当我在任务管理器中终止应用程序时,计时器停止,这很糟糕。
  5. 当我再次单击小部件时,应用程序启动但计时器没有恢复运行,这很糟糕。
  6. 计时器仅在调用下一个 OnUpdate 时才恢复运行(我的最短时间间隔为 30 分钟),这很糟糕,因为我需要在屏幕打开时频繁更新(或者当小部件对用户可见时更好)。

我想了解这里的基础知识,因为我找不到任何相关信息。为什么计时器在我第一次将小部件放在屏幕上时运行(没有运行应用程序)并在应用程序被终止时停止?

是的,我已经阅读了几乎所有关于小部件基础知识的内容,然后是关于使用 AlarmManager、Service、JobService、JobIntentService、JobScheduler 等的内容。但是我对这个带有计时器的解决方案很感兴趣,因为它非常简单并且适用于所有当前的 Android 版本(甚至最新的奥利奥)。要解决的问题是在屏幕关闭时停止计时器,并在它继续时重新启动它。为了节省手机电池。

【问题讨论】:

  • 那行不通。当您的应用程序的进程终止时,Timer 也会消失。像使用AlarmManager这样的解决方案之所以起作用,是因为时间是由系统处理的,并且您的应用可以在需要时从外部重新启动。
  • 是的,看来我会使用 timer + AlarmManager 组合。 AlarmManager 应该在被杀死时再次唤醒应用程序。将在这里进行更多测试。

标签: android xamarin android-widget


【解决方案1】:

我就是这样解决的:

public static class WidgetConsts
{
    public const string DebugTag = "com.myapp.WIDGET";
    public const string ActionWakeup = "com.myapp.WIDGET_WAKEUP";
    public const string ActionWidgetUpdate = "android.appwidget.action.APPWIDGET_UPDATE";
    public const string ActionWidgetDisabled = "android.appwidget.action.APPWIDGET_DISABLED";
}

[BroadcastReceiver]
[IntentFilter(new string[] { WidgetConsts.ActionWakeup })]
public class AlarmReceiver : BroadcastReceiver
{
    public override void OnReceive(Context context, Intent intent)
    {
        if (intent.Action.Equals(WidgetConsts.ActionWakeup))
        {
            Log.Debug(WidgetConsts.DebugTag, "Wakeup alarm called");
            if (MyWidgetProvider.widgetTimer == null)
            {
                Log.Debug(WidgetConsts.DebugTag, "Widget updating does not run, enforcing update...");
                MyWidgetProvider.UpdateAppWidget(context);
            }
            else
            {
                Log.Debug(WidgetConsts.DebugTag, "Widget updating runs, no action needed");
            }
        }
    }
}

[BroadcastReceiver]
[IntentFilter(new string[] { WidgetConsts.ActionWidgetUpdate })]
[IntentFilter(new string[] { WidgetConsts.ActionWidgetDisabled })]
[MetaData("android.appwidget.provider", Resource = "@xml/widget_info")]
public class MyWidgetProvider : AppWidgetProvider
{
    public static System.Timers.Timer widgetTimer = null;

    public override void OnUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
    {
        Log.Debug(WidgetConsts.DebugTag, "Updating the widget");

        // Open app on click
        RemoteViews views = new RemoteViews(context.PackageName, Resource.Layout.MyWidget);

        Intent launchAppIntent = new Intent(context, typeof(MainActivity));
        PendingIntent launchAppPendingIntent = PendingIntent.GetActivity(context, 0, launchAppIntent, PendingIntentFlags.UpdateCurrent);
        views.SetOnClickPendingIntent(Resource.Id.main, launchAppPendingIntent);

        appWidgetManager.UpdateAppWidget(appWidgetIds[0], views);

        // set timer for updating the widget views each 5 sec
        if (widgetTimer == null)
        {
            widgetTimer = new System.Timers.Timer();
            widgetTimer.Interval = 5000;
            widgetTimer.Elapsed += OnTimedEvent;
        }
        widgetTimer.Enabled = true;

        // set alarm to wake up the app when killed, each 60 sec
        // needs a fresh BroadcastReceiver because AppWidgetProvider.OnReceive is
        // not virtual and overriden method in this class would not be called
        AlarmManager am = (AlarmManager)context.GetSystemService(Context.AlarmService);
        Intent ai = new Intent(context, typeof(AlarmReceiver));
        ai.SetAction(WidgetConsts.ActionWakeup);
        PendingIntent pi = PendingIntent.GetBroadcast(context, 0, ai, PendingIntentFlags.CancelCurrent);
        am.SetRepeating(AlarmType.ElapsedRealtime, SystemClock.ElapsedRealtime(), 1000 * 60, pi);
    }

    public override void OnDisabled(Context context)
    {
        Log.Debug(WidgetConsts.DebugTag, "Disabling the widget");
        if (widgetTimer != null)
        {
            Log.Debug(WidgetConsts.DebugTag, "Stopping timer");
            widgetTimer.Enabled = false;
        }
        else
            Log.Debug(WidgetConsts.DebugTag, "Timer is null");
        base.OnDisabled(context);
    }

    private void OnTimedEvent(object sender, ElapsedEventArgs e)
    {
        Log.Debug(WidgetConsts.DebugTag, "Updating status...");
        new Handler(Looper.MainLooper).Post(() =>
        {
            //Run my code to periodically update the widget
            RemoteViews views = new RemoteViews(Application.Context.PackageName, Resource.Layout.MyWidget);
            AppWidgetManager manager = AppWidgetManager.GetInstance(Application.Context);
            ComponentName thisWidget = new ComponentName(Application.Context, Java.Lang.Class.FromType(typeof(MyWidgetProvider)));
            int[] appWidgetIds = manager.GetAppWidgetIds(thisWidget);

            views.SetTextViewText(Resource.Id.myText, "my text");

            manager.UpdateAppWidget(appWidgetIds[0], views);
        });
    }

    static public void UpdateAppWidget(Context context)
    {
        Intent intent = new Intent(context, typeof(MyWidgetProvider));
        intent.SetAction(WidgetConsts.ActionWidgetUpdate);
        int[] ids = AppWidgetManager.GetInstance(context).GetAppWidgetIds(new ComponentName(context, Java.Lang.Class.FromType(typeof(MyWidgetProvider))));
        intent.PutExtra(AppWidgetManager.ExtraAppwidgetIds, ids);
        context.SendBroadcast(intent);
    }
}

优点: 简单的解决方案,适用于所有 Android 系统(在 3.2、4.3、8.1 上测试)。 Android 系统上的电池友好 >= 6.0 与打盹模式(使用 GSam 电池监视器测量)。不受 >=8.0 中新的后台执行限制的限制。

缺点: 在没有打盹模式的情况下在 6.0 以下的系统上耗尽电池,但今天没有人关心这些......

【讨论】:

    【解决方案2】:

    首先,你可以尝试让Widget app不熟练。

    小部件本身不会被杀死。小部件最初是一个广播接收器,它是静态的。这意味着可以随时接收订阅的广播小部件,并且会调用 onReceive() 方法。 widget 无法运行的原因是应该为相应的服务而杀掉它们。 如果要让widget 一直运行,那么服务应该被杀掉并重新启动。

    Service是Android系统的一个组件,类似于Activity的层次,但是他不能自己运行,只能在后台运行,并且可以和其他组件交互。 在Android开发过程中,每次调用startService(Intent)都会调用Service对象的OnStartCommand(Intent, int, int)方法,然后在onStartCommand方法中进行一些处理。

    1、创建不被杀死的服务

    @Override
     public int onStartCommand(Intent intent, int flags, int startId)
     {
     return START_STICKY_COMPATIBILITY;
     //return super.onStartCommand(intent, flags, startId);
     }
    
    @Override
     public int onStartCommand(Intent intent, int flags, int startId)
     {
     flags = START_STICKY;
     return super.onStartCommand(intent, flags, startId);
     // return START_REDELIVER_INTENT;
     }
    @Override
    public void onStart(Intent intent, int startId)
    {
    // again regsiter broadcast
    IntentFilter localIntentFilter = new IntentFilter("android.intent.action.USER_PRESENT");
    localIntentFilter.setPriority(Integer.MAX_VALUE);// max int
    myReceiver searchReceiver = new myReceiver();
    registerReceiver(searchReceiver, localIntentFilter);
    super.onStart(intent, startId);
    }
    

    2、在Service的onDestroy()中重启Service。

    public void onDestroy()
    {
    Intent localIntent = new Intent();
    localIntent.setClass(this, MyService.class); // restart Service
    this.startService(localIntent);
    }
    

    3、在XML中创建广播和注册器

    public class myReceiver extends BroadcastReceiver
    {
     @Override
     public void onReceive(Context context, Intent intent)
     {
     context.startService(new Intent(context, Google.class));
     }
    }
    
    <receiver android:name=".myReceiver" >
          <intent-filter android:priority="2147483647" ><!--Priority plus highest-->
            <!-- when applicayion lauch invoke -->
            <action android:name="android.intent.action.BOOT_COMPLETED" />       
            <!-- unlock invole -->
            <action android:name="android.intent.action.USER_PRESENT" />
            <!--context switch -->
            <action android:name="android.media.RINGER_MODE_CHANGED" />       
          </intent-filter>
    </receiver>
    <service android:name=".MyService" >
    

    注意:解锁、启动、切换场景激活广播需要添加权限,如启动完成、手机状态等。

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    

    ================================================ ====================

    其次,如果Widget应用不熟练,可以听听屏幕是锁定还是解锁。

    自定义一个ScreenListener并添加ScreenBroadcastReceiver

    private class ScreenBroadcastReceiver extends BroadcastReceiver {
        private String action = null;
    
        @Override
        public void onReceive(Context context, Intent intent) {
            action = intent.getAction();
            if (Intent.ACTION_SCREEN_ON.equals(action)) { // screen on
                mScreenStateListener.onScreenOn();
            } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { // screen off
                mScreenStateListener.onScreenOff();
            } else if (Intent.ACTION_USER_PRESENT.equals(action)) { // screen unlock
                mScreenStateListener.onUserPresent();
            }
        }
    }
    

    这样您就可以使用计时器或与客户进行其他展示。

    ================================================ ================================

    更多信息:

    这个方法不是最好的,还有很多需要改进的地方,给个建议吧。

    【讨论】:

    • 感谢您提供如此长而详细的回答。但不幸的是,我有服务解决方案,但由于新的限制,它不再适用于 Android 8。我应该使用 JobScheduler 而不是服务。但我只是在测试计时器解决方案,因为它很简单。看来我会用timer + AlarmManager 组合。如果被杀死,Alarmmanager 应该再次唤醒应用程序。将在这里进行更多测试。
    猜你喜欢
    • 1970-01-01
    • 2018-06-08
    • 1970-01-01
    • 2022-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-18
    • 1970-01-01
    相关资源
    最近更新 更多