Search code examples
c#.nettizentizen-wearable-sdktizen-emulator

Nondeterministic Errors in Tizen Watch App


I'm trying to create a simple counter app for a Samsung Galaxy Watch that responds to the bezel and has plus/minus buttons to increment/decrement the counter, respectively. When the counter value is below certain thresholds, its color should change (for example, going from white to orange to red as it gets closer to 0). The buttons should be able to be held down to cause the counter to rapidly increment/decrement after a slight delay until they are released. This functionality mostly works, except I sometimes get nondeterministic errors when I hold the buttons down. From inspection of my code, there shouldn't be any problem, but maybe I'm missing something. Here's an SCCM that replicates the issue:

using System;
using System.Collections.Generic;
using System.Timers;
using Tizen.Wearable.CircularUI.Forms;
using Xamarin.Forms;

namespace RepeatableCounter
{
    class Program : global::Xamarin.Forms.Platform.Tizen.FormsApplication
    {
        protected override void OnCreate()
        {
            base.OnCreate();
            LoadApplication(new App());
        }

        static void Main(string[] args)
        {
            var app = new Program();
            Forms.Init(app);
            global::Tizen.Wearable.CircularUI.Forms.FormsCircularUI.Init();
            app.Run(args);
        }
    }

    class App : Application
    {
        public App()
        {
            MainPage = new MainPage { Value = 20, Thresholds = { (5, Color.Red), (10, Color.Orange) }};
        }
    }

    class MainPage : BezelInteractionPage, IRotaryEventReceiver
    {
        private int value;
        private int ticks;
        private readonly Label counter;
        private readonly Timer resetTicks;

        public MainPage() : base()
        {
            value = 0;
            ticks = 0;

            counter = new Label
            {
                Text = value.ToString(),
                FontSize = 32,
                BackgroundColor = Color.Transparent,
                HorizontalTextAlignment = TextAlignment.Center
            };
            RepeatButton plusButton = new RepeatButton
            {
                Text = "+",
                Delay = 500,
                Interval = 100,
                HorizontalOptions = LayoutOptions.Center,
                WidthRequest = 60
            };
            RepeatButton minusButton = new RepeatButton
            {
                Text = "\u2212",
                Delay = 500,
                Interval = 100,
                HorizontalOptions = LayoutOptions.Center,
                WidthRequest = 60
            };

            Content = new StackLayout
            {
                VerticalOptions = LayoutOptions.Center,
                Spacing = -24,
                Children = {
                    plusButton,
                    counter,
                    minusButton
                }
            };

            RotaryFocusObject = this;
            resetTicks = new Timer
            {
                Interval = 500,
                Enabled = false,
                AutoReset = false,
            };
            resetTicks.Elapsed += (sender, e) => ticks = 0;

            plusButton.Pressed += (sender, e) => Value++;
            plusButton.Held += (sender, e) => Value++;
            minusButton.Pressed += (sender, e) => Value--;
            minusButton.Held += (sender, e) => Value--;
        }
        public int Value
        {
            get { return value; }
            set
            {
                this.value = value;
                counter.Text = this.value.ToString();

                Color selected = Color.Default;
                foreach ((int threshold, Color color) in Thresholds)
                {
                    if (value <= threshold)
                    {
                        selected = color;
                        break;
                    }
                }
                counter.TextColor = selected;
            }
        }

        public int TickThreshold { get; set; } = 10;

        public int FastTickStep { get; set; } = 5;

        public IList<(int, Color)> Thresholds { get; set; } = new List<(int, Color)>();

        public void Rotate(RotaryEventArgs args)
        {
            if (args.IsClockwise)
            {
                if (ticks >= 0)
                    ticks++;
                else
                    ticks = 1;
                if (ticks <= TickThreshold)
                    Value++;
                else
                    Value += FastTickStep;
            }
            else
            {
                if (ticks <= 0)
                    ticks--;
                else
                    ticks = -1;
                if (ticks >= -TickThreshold)
                    Value--;
                else
                    Value -= FastTickStep;
            }

            resetTicks.Stop();
            resetTicks.Start();
        }
    }

    public class RepeatButton : Button
    {
        private readonly Timer timer;

        public RepeatButton() : base()
        {
            timer = new Timer
            {
                Enabled = false,
                AutoReset = true
            };
            timer.Elapsed += (sender, e) => {
                timer.Interval = Interval;
                Held.Invoke(timer, e);
            };

            Pressed += (sender, e) => {
                timer.Interval = Delay;
                timer.Start();
            };
            Released += (sender, e) => timer.Stop();
        }

        public double Delay { get; set; } = 100;

        public double Interval { get; set; } = 100;

        public event EventHandler Held;
    }
}

To replicate the issue, hold down the + and/or - buttons. Since the issue is nondeterministic, you might have to occasionally switch buttons. I've mostly seen it occur after a color change.

There are three "symptoms" that I've encountered:

  • The label disappears
  • The button doesn't respond to its Release event (the counter keeps counting even after the button is released)
  • A segmentation fault occurs (I've seen it reported for lines 22 and 91 of the code above)

Adding more UI elements seems to exacerbate the issue and cause the errors to happen faster, and this occurs both in the emulator and on an actual watch. Is there some sort of synchronization or threading issue I'm missing? What could be causing these errors?


Solution

  • Please use Device.StartTimer instead System.Threading.Timer. System.Threading.Timer use worker thread, so it should not directly access to UI

    To execute your code on UI thread, you can use Device.BeginInvokeOnMainThread