0

I have a widget made up of 2 TextViews, 2 ImageViews, and 2 TextClocks. I am trying to update the widget using a ScreenListener (code below) and an alarm. I want all views to update when the screen is turned On. Also, a repeating alarm is set when the screen is turned On. The alarm is used to trigger the update of the battery level indicator (an ImageView). The image for the battery level indicator is generated using the ProgressRing (code below). The alarm is cancelled when the screen is turned Off. The TextClocks take care of themselves.

Everything works when debugging using the emulator or my phone. The various components update when I turn the screen On and Off. However, when I install the widget on my phone, the widget stops updating after a period of time. Initially, I can turn the screen On and Off and everything updates. And, I can plug (or unplug) the phone and the alarm will update the battery level indicator. But, after some period of time (I think with the screen Off), updating stops. Turning the screen On no longer updates anything and, I beleive, the alarm is no longer set. (Note: The TextClock continue to work and show the correct time.)

When debugging, I have seen the instances of the ScreenLister and ProgressRing become null. So, I've included checks for this and make new instances when it happens. This doesn't seem like the right solution. Should this be occurring?

Is the system killing my widget? Is there a way around this, if so? Is my AppWidgetProvider somehow losing it's connection to the RemoteViews?

Thanks for any help. And, let me know if you need more information.

P.S. I'm currently building for Android 9.0. And, I'm new to Android programming.

AppWidgetProvider

namespace ClockCalBattery
{
    [BroadcastReceiver(Enabled = true)]
    [IntentFilter(new string[] { "android.appwidget.action.APPWIDGET_UPDATE", Intent.ActionUserPresent})]
    [MetaData("android.appwidget.provider", Resource = "@xml/appwidgetprovider")]
    public class CCBWidget : AppWidgetProvider, ScreenStateListener
    {
        public static ProgressRing progressRing = new ProgressRing();
        private static String ACTION_UPDATE_BATTERY = "com.lifetree.clockcalbattery.UPDATE-BATTERY";
        public static ScreenListener mScreenListener;
        public static int n_alarms = 0;
        
        public override void OnReceive(Context context, Intent intent)
        {
            base.OnReceive(context, intent);

            if (intent.Action == ACTION_UPDATE_BATTERY)
            {
                update_batteryLevel(context);
                if (mScreenListener == null)
                {
                    mScreenListener = new ScreenListener(context);
                    mScreenListener.begin(this);
                }

                if (progressRing == null)
                {
                    progressRing = new ProgressRing();
                }
            }
        }

        public override void OnEnabled(Context context)
        {
            base.OnEnabled(context);
            if (mScreenListener == null)
            {
                mScreenListener = new ScreenListener(context);
                mScreenListener.begin(this);
            }

            if (progressRing == null)
            {
                progressRing = new ProgressRing();
            }

            update_batteryLevel(Application.Context);
            update_nextAlarm(Application.Context);
        }

        public override void OnDisabled(Context context)
        {
            base.OnDisabled(context);
        }

        public override void OnUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
        {
            base.OnUpdate(context, appWidgetManager, appWidgetIds);
            mScreenListener = new ScreenListener(context);
            progressRing = new ProgressRing();

            var me = new ComponentName(context, Java.Lang.Class.FromType(typeof(CCBWidget)).Name);
            var widgetView = BuildRemoteViews(context, appWidgetIds);
            appWidgetManager.UpdateAppWidget(me, widgetView);
        }

        private RemoteViews BuildRemoteViews(Context context, int[] appWidgetIds)
        {
            RemoteViews widgetView = new RemoteViews(context.PackageName, Resource.Layout.CCBWidget);
            
            update_TodaysDate(widgetView);
            update_nextAlarm(context, widgetView);
            update_batteryLevel(widgetView);
            return widgetView;
        }

        private void update_TodaysDate(RemoteViews widgetView)
        {
            string today = DateTime.Today.ToString("D");
            widgetView.SetTextViewText(Resource.Id.longDate, today);
        }

        private void update_nextAlarm(Context context, RemoteViews widgetView)
        {
            AlarmManager am = (AlarmManager)context.GetSystemService(Context.AlarmService);

            AlarmManager.AlarmClockInfo alarmInfo = am.NextAlarmClock;

            if (alarmInfo != null)
            {
                long time = alarmInfo.TriggerTime;
                DateTime alarmDateTime = new DateTime(1970, 1, 1).AddMilliseconds(time).ToLocalTime();

                string alarmTime = alarmDateTime.ToString("ddd ");
                alarmTime += alarmDateTime.ToString("t");

                widgetView.SetTextViewText(Resource.Id.textView_alarmTime, alarmTime);
                widgetView.SetImageViewResource(Resource.Id.imageView_alarmIcon, Resource.Drawable.whiteOnBlack_clock);
            }

            else
            {
                widgetView.SetTextViewText(Resource.Id.textView_alarmTime, "");
                widgetView.SetImageViewResource(Resource.Id.imageView_alarmIcon, Resource.Drawable.navigation_empty_icon);
            }
        }

        private void update_nextAlarm(Context mContext)
        {
            RemoteViews widgetView = new RemoteViews(mContext.PackageName, Resource.Layout.CCBWidget);
            update_nextAlarm(mContext, widgetView);
            var me = new ComponentName(mContext, Java.Lang.Class.FromType(typeof(CCBWidget)).Name);
            var awm = AppWidgetManager.GetInstance(mContext);
            awm.UpdateAppWidget(me, widgetView);
        }

        private void update_batteryLevel(RemoteViews widgetView)
        {
            progressRing.setProgress((int)(Battery.ChargeLevel * 100.0));
            progressRing.drawProgressBitmap();
            widgetView.SetImageViewBitmap(Resource.Id.progressRing, progressRing.ring);
        }

        private void update_batteryLevel(Context mContext)
        {
            RemoteViews widgetView = new RemoteViews(mContext.PackageName, Resource.Layout.CCBWidget);
            update_batteryLevel(widgetView);
            var me = new ComponentName(mContext, Java.Lang.Class.FromType(typeof(CCBWidget)).Name);
            var awm = AppWidgetManager.GetInstance(mContext);
            awm.UpdateAppWidget(me, widgetView);
        }

        public static void turnUpdateAlarmOnOff(Context context, bool turnOn, int time)
        {
            AlarmManager alarmManager = (AlarmManager)context.GetSystemService(Context.AlarmService);
            Intent intent = new Intent(context, typeof(CCBWidget));
            intent.SetAction(ACTION_UPDATE_BATTERY);
            PendingIntent pendingIntent = PendingIntent.GetBroadcast(context, 0, intent, 0);

            if (turnOn)
            {
                // Add extra 1 sec because sometimes ACTION_BATTERY_CHANGED is called after the first alarm
                alarmManager.SetInexactRepeating(AlarmType.ElapsedRealtime, SystemClock.ElapsedRealtime() + 1000, time * 1000, pendingIntent);
                n_alarms++;
            }
            else
            {
                alarmManager.Cancel(pendingIntent);
                n_alarms--;
            }
        }

       public void onScreenOn()
        {
            if (mScreenListener == null)
            {
                mScreenListener = new ScreenListener(Application.Context);
                mScreenListener.begin(this);
            }

            if (progressRing == null)
            {
                progressRing = new ProgressRing();
            }

            update_batteryLevel(Application.Context);
            update_nextAlarm(Application.Context);

            turnUpdateAlarmOnOff(Application.Context, true, 60);
        }

        public void onScreenOff()
        {
            turnUpdateAlarmOnOff(Application.Context, false, 1);
        }

        public void onUserPresent()
        {
            //Console.WriteLine("onUserPresent");
        }
    }
}

ScreenListener

namespace ClockCalBattery
{
    public class ScreenListener
    {
        private Context mContext;
        private ScreenBroadcastReceiver mScreenReceiver;
        private static ScreenStateListener mScreenStateListener;

        public ScreenListener(Context context)
        {
            mContext = context;
            mScreenReceiver = new ScreenBroadcastReceiver();
        }

        /**
         * screen BroadcastReceiver
         */
        private class ScreenBroadcastReceiver : BroadcastReceiver
        {
            private String action = null;

            public override void OnReceive(Context context, Intent intent)
            {
                action = intent.Action;
                if (Intent.ActionScreenOn == action)
                { // screen on
                    mScreenStateListener.onScreenOn();
                }
                else if (Intent.ActionScreenOff == action)
                { // screen off
                    mScreenStateListener.onScreenOff();
                }
                else if (Intent.ActionUserPresent == action)
                { // unlock
                    mScreenStateListener.onUserPresent();
                }
            }
        }

        /**
         * begin to listen screen state
         *
         * @param listener
         */
        public void begin(ScreenStateListener listener)
        {
            mScreenStateListener = listener;
            registerListener();
            getScreenState();
        }

        /**
         * get screen state
         */
        private void getScreenState()
        {
            PowerManager manager = (PowerManager)mContext
                    .GetSystemService(Context.PowerService);
            if (manager.IsInteractive)
            {
                if (mScreenStateListener != null)
                {
                    mScreenStateListener.onScreenOn();
                }
            }
            else
            {
                if (mScreenStateListener != null)
                {
                    mScreenStateListener.onScreenOff();
                }
            }
        }

        /**
         * stop listen screen state
         */
        public void unregisterListener()
        {
            mContext.UnregisterReceiver(mScreenReceiver);
        }

        /**
         * regist screen state broadcast
         */
        private void registerListener()
        {
            IntentFilter filter = new IntentFilter();
            filter.AddAction(Intent.ActionScreenOn);
            filter.AddAction(Intent.ActionScreenOff);
            filter.AddAction(Intent.ActionUserPresent);
            mContext.ApplicationContext.RegisterReceiver(mScreenReceiver, filter);
            //mContext.RegisterReceiver(mScreenReceiver, filter);
        }

        public interface ScreenStateListener
        {// Returns screen status information to the caller
            void onScreenOn();

            void onScreenOff();

            void onUserPresent();
        }
    }
}

ProgressRing

namespace ClockCalBattery
{
    public class ProgressRing
    {
        private int max = 100;
        public int progress;

        private Path path = new Path();
        Color color = new Color(50, 50, 255, 255);
        private Paint paint;
        private Paint mPaintProgress;
        private RectF mRectF;
        private Paint batteryLevelTextPaint;
        private Paint batteryStateTextPaint;

        private String batteryLevelText = "0%";
        private String batteryStateText = null;
        private BatteryState bs;

        private Rect textBounds = new Rect();
        private int centerY;
        private int centerX;

        private float swipeAndgle = 0;

        public Bitmap ring;
        private int bitmapWidth = 70;
        private int bitmapHeight = 70;

        public ProgressRing()
        {
            progress = -1;

            paint = new Paint();
            paint.AntiAlias = true;
            paint.StrokeWidth = 1;
            paint.SetStyle(Paint.Style.Stroke);
            paint.Color = color;

            mPaintProgress = new Paint();
            mPaintProgress.AntiAlias = true;
            mPaintProgress.SetStyle(Paint.Style.Stroke);
            mPaintProgress.StrokeWidth = 5;
            mPaintProgress.Color = color;

            batteryLevelTextPaint = new Paint();
            batteryLevelTextPaint.AntiAlias = true;
            batteryLevelTextPaint.SetStyle(Paint.Style.Fill);
            batteryLevelTextPaint.Color = color;
            batteryLevelTextPaint.StrokeWidth = 1;

            batteryStateTextPaint = new Paint();
            batteryStateTextPaint.AntiAlias = true;
            batteryStateTextPaint.SetStyle(Paint.Style.Fill); ;
            batteryStateTextPaint.Color = color;
            batteryStateTextPaint.StrokeWidth = 1;
            //batteryStateTextPaint.SetTypeface(Typeface.Create(Typeface.Default, TypefaceStyle.Bold));

            Init_ring();
        }

        private void Init_ring()
        {
            int viewWidth = bitmapWidth;
            int viewHeight = bitmapHeight;

            float radius = (float)(bitmapHeight / 2.0);

            path.Reset();

            centerX = viewWidth / 2;
            centerY = viewHeight / 2;
            path.AddCircle(centerX, centerY, radius, Path.Direction.Cw);

            float smallCirclRadius = radius - (float)(0.1 * radius);
            path.AddCircle(centerX, centerY, smallCirclRadius, Path.Direction.Cw);
            

            //mRectF = new RectF(0, 0, viewWidth, viewHeight);
            mRectF = new RectF(centerX - smallCirclRadius, centerY - smallCirclRadius, centerX + smallCirclRadius, centerY + smallCirclRadius);

            batteryLevelTextPaint.TextSize = radius * 0.5f;
            batteryStateTextPaint.TextSize = radius * 0.30f;
        }

        internal void setProgress(int progress)
        {
            this.progress = progress;

            int percentage = progress * 100 / max;

            swipeAndgle = percentage * 360 / 100;

            batteryLevelText = percentage + "%";

            batteryStateText = null;
            bs = Battery.State;

            if (bs == BatteryState.Charging)
                batteryStateText = "Charging";

            else if (percentage > 99)
                batteryStateText = "Full";

            else if (percentage < 15)
                batteryStateText = "Low";
        }

        internal void drawProgressBitmap()
        {
            ring = Bitmap.CreateBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.Argb8888);
            Canvas c = new Canvas(ring);
            byte r = 0;
            byte g = 0;
            byte b = 0;
            byte a = 255;

            hls2rgb(swipeAndgle * (120.0 / 360.0), 1, 128, ref r, ref g, ref b);
            paint.Color = new Color(r, g, b, a);
            mPaintProgress.Color = new Color(r, g, b, a);
            batteryLevelTextPaint.Color = new Color(r, g, b, a);
            batteryStateTextPaint.Color = new Color(r, g, b, a);

            c.DrawArc(mRectF, 270, swipeAndgle, false, mPaintProgress);

            drawTextCentred(c);
        }

        private void drawTextCentred(Canvas c)
        {
            batteryLevelTextPaint.GetTextBounds(batteryLevelText, 0, batteryLevelText.Length, textBounds);

            c.DrawText(batteryLevelText, centerX - textBounds.ExactCenterX(), centerY - textBounds.ExactCenterY(), batteryLevelTextPaint);
            if (batteryStateText != null)
            {
                batteryStateTextPaint.GetTextBounds(batteryStateText, 0, batteryStateText.Length, textBounds);
                c.DrawText(batteryStateText, centerX - textBounds.ExactCenterX(), centerY - textBounds.ExactCenterY() + 15, batteryStateTextPaint);
            }
        }

        public void hls2rgb(double color_wheel_angle, double tlen, byte ilum,
                             ref byte color_r, ref byte color_g, ref byte color_b)
        {
            double rlum, rm1, rm2;

            if (ilum > 255) ilum = 255;
            if (ilum < 0) ilum = 0;
            if (tlen > 1) tlen = 1;
            if (tlen < 0) tlen = 0;


            rlum = (double)ilum / 255;

            if (rlum < 0.5)
                rm2 = rlum * (1.0 + tlen);
            else
                rm2 = rlum + tlen - (rlum * tlen);

            rm1 = (2.0 * rlum) - rm2;

            if (tlen == 0)
            {
                color_r = ilum;
                color_g = ilum;
                color_b = ilum;
            }
            else
            {
                color_g = (byte)(value(rm1, rm2, color_wheel_angle + 120) * 255);
                color_b = (byte)(value(rm1, rm2, color_wheel_angle) * 255);
                color_r = (byte)(value(rm1, rm2, color_wheel_angle - 120) * 255);
            }

            return;
        }

        public double value(double r1, double r2, double angle)
        {
            double value;

            angle = angle - 120;

            if (angle > 360) angle = angle - 360;
            if (angle < 0) angle = angle + 360;

            if (angle < 60)
                value = r1 + (r2 - r1) * angle / 60;

            else if (angle < 180)
                value = r2;

            else if (angle < 240)
                value = r1 + (r2 - r1) * (240 - angle) / 60;

            else
                value = r1;

            return (value);
        }
    }
}
amm811
  • 1
  • 2
  • In android, when the process is killed, the static variables will be set as the default value. So after a period of time, the process of your application is killed when you don't operate it and the screen is out. So the ScreenListener and ProcessRing have been set to null. You can save them in a class which is extend to the Application class or use the OnSaveInstanceState method of the activity. – Liyun Zhang - MSFT Dec 08 '21 at 07:53
  • Possibly a silly question... My Main Activity simply checks for Calendar permission and closes. It's my AppWidgetProvider that stops working. Where should I save the InstanceState? Is a method called before the Widget process is killed? How is the AppWidgetProvider restarted? Should the Main Activity always be running instead? I think I'm missing some understanding in the lifecycles of Main Activites and AppWidgetProvider. Could someone point me in right direction to answer these questions if it too much for here? Thanks. – amm811 Dec 08 '21 at 13:13
  • The AppWidgetProvider is extend to the BroadcastReceiver. And the life cycle of the BroadcastReceiver is very short. So the AppWidgetProvider is easily killed by the system. First, you can try to put the initialization of the ScreenListener and ProcessRing in the OnEnable method or OnReceive method. If it still doesn't work, you can use the Timer to make your AppWidgetProvider alive all the time. In addition, you can use a service or another thread to update the widget. There are many examples on google. The TextClocks still work because it use handler and new thread to update it. – Liyun Zhang - MSFT Dec 09 '21 at 07:24
  • In my appwidgetprovider.xml, I changed the updatePeriodMillis from "0" to "300000" (5 minutes). Now, it seems to do what I want. Is this the Timer you are referring to? If not, can you provide a link for the Timer? Thanks for your help. – amm811 Dec 09 '21 at 14:12
  • @Liyun Zhang - MSFT Your comments help me a lot. I wasted a long time deciding whether to use the service or other things to keep my widget alive. Do you have more suggestions for my situation? Thanks.(https://stackoverflow.com/questions/70032050/can-i-use-action-time-tick-to-update-android-time-widget) – Krahmal Dec 10 '21 at 00:56
  • The property updatePeriodMillis means how long time call the OnUpdate method of the AppWidgetProvider once. But it will not be delivered more than once every 30 minutes in android and 0 means no period.[link](https://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo#updatePeriodMillis) In addition, it will be set to 30 minutes when it less then 30 minutes. So I suggest you use the Timer which can update widget with short period.[link](https://developer.android.com/reference/java/util/Timer) – Liyun Zhang - MSFT Dec 10 '21 at 02:04
  • Thanks for your help. I finally figured out how to use Alarms to keep the widget alive. I have a "keep alive" alarm that builds the remoteviews and restarts the ScreenListener, if necessary. I have a "battery update" alarm that simply updates the battery level indicator. When the screen is OFF, the "keep alive" alarm is ON and the "battery update" alarm is OFF. When the screen is turned on, the data for the remoteviews are updated and the view ?rebuilt? Also, the "keep alive" alarm is turned OFF and the "battery update" alarm is ON. – amm811 Dec 11 '21 at 18:53

0 Answers0