Konubinix' opinionated web of thoughts

Simple Timer in Kivy

Fleeting

import os

import math
import time
from kivy.app import App
from kivy.clock import Clock
from kivy.graphics import Color, Rectangle
from kivy.properties import BooleanProperty, NumericProperty, StringProperty
from kivy.uix.boxlayout import BoxLayout
from plyer import orientation
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.progressbar import ProgressBar
from kivy.uix.screenmanager import Screen, ScreenManager, ScreenManagerException
from kivy.core.audio import SoundLoader

from helpers.osc import oschandler, to_service
from helpers.wakelock import WakeLock
from android.runnable import run_on_ui_thread
from logging import getLogger
from kivy.lang import Builder

logger = getLogger(__name__)

pink = (1, 0.75, 0.8)
orange = (1, 0.5, 0)
cyan = (0, 1, 1)

Builder.load_string("""
# copy from the source code of kivy, but increased
<ProgressBar>:
    canvas:
        Color:
            rgb: 1, 1, 1
        BorderImage:
            border: (12, 12, 12, 12)
            pos: self.x, self.y
            size: self.width, self.height
            source: 'atlas://data/images/defaulttheme/progressbar_background'
        BorderImage:
            border: [int(min(self.width * (self.value / float(self.max)) if self.max else 0, 12))] * 4
            pos: self.x, self.y
            size: self.width * (self.value / float(self.max)) if self.max else 0, self.height
            source: 'atlas://data/images/defaulttheme/progressbar'
""")

class StateMachine(object):

    def __init__(self, manager, duration):
        self.manager = manager
        self.duration = duration
        self.state = "step"
        self.timer = self.manager.get_screen('simpletimer:timer')

    def update(self):
        if self.state == "step":
            self.timer.finish()
        else:
            raise NotImplementedError()

    def start(self):

        self.timer.start_timer(
            what="Tic, Tac...",
            color=cyan,
            duration=self.duration,
            on_done=self.update,
            sound_map={}
        )


class SettingsScreen(Screen):
    duration = NumericProperty(60)

    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

    def __init__(self, **kwargs):
        super(SettingsScreen, self).__init__(**kwargs)
        with self.canvas.before:
            self.color = Color(1, 1, 1, 1)
            self.rect = Rectangle(size=self.size, pos=self.pos)

        self.bind(size=self._update_rect, pos=self._update_rect)

        layout = BoxLayout(orientation='vertical')

        duration_layout = BoxLayout(size_hint=(1, 0.15))
        duration_layout.add_widget(
            Button(text="-",
                   background_color=(0, 0, 1, 1),
                   color=(1, 1, 1, 1),
                   on_press=self.decrease_duration))
        self.duration_label = Label(text="", color=(0, 0, 0, 1))
        duration_layout.add_widget(self.duration_label)
        duration_layout.add_widget(
            Button(text="+",
                   background_color=(0, 0, 1, 1),
                   color=(1, 1, 1, 1),
                   on_press=self.increase_duration))
        layout.add_widget(duration_layout)

        # Go button
        go_button = Button(text="Go !!!",
                           background_color=(0, 0, 1, 1),
                           color=(1, 1, 1, 1),
                           size_hint=(1, 0.15))
        go_button.bind(on_press=self.start_timer)
        layout.add_widget(go_button)


        self.add_widget(layout)
        self.update_display()

        self.bind(on_pre_enter=self.pre_enter_handler)

        @oschandler("alarmtimer:start")
        def _(config):
            self.duration = config["duration"]
            self.update_display()
            self.start_timer()

    def pre_enter_handler(self, *_):
        to_service("simpletimer:settings")
        orientation.set_sensor()

    def update_display(self):
        self.duration_label.text = "Temps: " + str(self.duration // 60) + "m"


    def increase_duration(self, instance):
        self.duration += 60
        self.update_display()

    def decrease_duration(self, instance):
        if self.duration > 60:
            self.duration -= 60
            self.update_display()

    def start_timer(self, *instance):
        app = App.get_running_app()
        app.root.state_machine = StateMachine(
            self.manager,
            self.duration,
        )
        app.root.state_machine.start()

class TimerScreen(Screen):
    remaining_time = NumericProperty(0)
    duration = NumericProperty(0)
    timer_running = BooleanProperty(False)
    what = StringProperty("")

    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

    def __init__(self, **kwargs):
        super(TimerScreen, self).__init__(**kwargs)
        self.leave_is_stop = True
        self.alarm = SoundLoader.load("/sdcard/Alarms/alarm.ogg")
        if self.alarm is None:
            raise Exception("Alarm could not be loaded")
        with self.canvas.before:
            self.color = Color(0, 1, 1, 1)
            self.rect = Rectangle(size=self.size, pos=self.pos)

        self.bind(size=self._update_rect, pos=self._update_rect)

        layout = GridLayout(cols=1)
        self.progress = ProgressBar(size_hint=(1, 0.5))

        layout.add_widget(self.progress)

        self.time_label = Label(text="", font_size='50sp', color=(0, 0, 0, 1))
        layout.add_widget(self.time_label)

        self.what_label = Label(text="", font_size='20sp', color=(0, 0, 0, 1))
        layout.add_widget(self.what_label)

        button_layout = BoxLayout()
        self.pause_button = Button(text="Pause",
                                   background_color=(0, 0, 1, 1),
                                   color=(1, 1, 1, 1))
        self.pause_button.bind(on_press=self.toggle_clock)
        button_layout.add_widget(self.pause_button)

        layout.add_widget(button_layout)
        self.add_widget(layout)

        self.bind(on_pre_enter=self.pre_enter_handler)
        self.bind(on_pre_leave=self.pre_leave_handler)

        with WakeLock("simpletimer", wakeup=True):
            self.keep_screen_on()

    def keep_screen_on(self, revert=False):
        from android import mActivity
        from jnius import autoclass
        from android.runnable import run_on_ui_thread
        LayoutParams = autoclass('android.view.WindowManager$LayoutParams')

        @run_on_ui_thread
        def do():
            if revert:
                logger.debug("Leaving screen always on")
                mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
            else:
                logger.debug("Entering screen always on")
                mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)

        do()
        time.sleep(0.5)

    def pre_leave_handler(self, *args):
        self.stop_clock()
        if self.alarmtime_clock:
            self.alarmtime_clock.cancel()
            self.alarmtime_clock = None
        self.keep_screen_on(revert=True)
        if self.alarm:
            self.alarm.stop()
            self.alarm = None
        from jnius import autoclass
        from android import mActivity

        View = autoclass('android.view.View')

        option = View.SYSTEM_UI_FLAG_VISIBLE

        @run_on_ui_thread
        def fs():
            logger.debug("Leaving fullscreen mode")
            mActivity.getWindow().getDecorView().setSystemUiVisibility(option)

        fs()

    def pre_enter_handler(self, *args):
        self.keep_screen_on()

        from jnius import autoclass
        from android import mActivity

        View = autoclass('android.view.View')

        option = View.SYSTEM_UI_FLAG_FULLSCREEN

        @run_on_ui_thread
        def fs():
            logger.debug("Entering fullscreen mode")
            mActivity.getWindow().getDecorView().setSystemUiVisibility(option)

        fs()

    def start_timer(
            self,
            what,
            color,
            duration,
            on_done,
            sound_map,
    ):
        self.wanted_color = color
        self.color.rgb = self.wanted_color
        App.get_running_app().goto("simpletimer:timer")
        self.duration = duration
        self.what = what
        self.elapsed_alarm_time = 0.
        self.duration_alarm_fade_in = 90.
        self.alarmtime_clock = None
        self.remaining_time = duration
        self.on_done = on_done
        self.finished = False
        self.sound_map = sound_map
        self.sound = self.sound_map.get(None)
        if self.sound:
            self.sound.play()

        self.start_clock()

    def update_display(self):
        self.what_label.text = self.what

        if self.clock:
            self.color.rgb = self.wanted_color
            self.pause_button.text = "Pause"
        else:
            if self.finished:
                self.what_label.text = "C'est fini !!!"
                self.pause_button.text = "Ok"
            else:
                self.color.rgb = orange
                self.pause_button.text = "Continue ?"

        self.time_label.text = str(self.remaining_time)
        self.progress.max = self.duration
        self.progress.value = self.remaining_time

    def countdown(self, dt):
        if self.remaining_time > 0:
            self.remaining_time -= 1
        else:
            self.stop_clock()
            self.alarmtime_clock = Clock.schedule_interval(self.alarmtime, 1)
            self.alarm.loop = True
            self.alarm.volume = 0
            self.color.rgb = (0, 0, 0)
            self.alarm.play()
            self.finished = True
        self.update_display()
        new_sound = self.sound_map.get(self.remaining_time)
        if new_sound:
            if self.sound:
                self.sound.stop()
                self.sound = new_sound
                self.sound.play()

    def alarmtime(self, *_):
        self.elapsed_alarm_time += 1
        self.time_label.text = str(int(self.elapsed_alarm_time))
        if self.elapsed_alarm_time < self.duration_alarm_fade_in:
            normalized_time = self.elapsed_alarm_time / self.duration_alarm_fade_in
            vol = math.sin(normalized_time * math.pi / 2)
            self.color.rgb = (vol,) * 3
            logger.debug("Alarm volume: {}".format(vol))
            self.alarm.volume = vol
        else:
            self.alarm.volume = 1.0

    def start_clock(self):
        self.clock = Clock.schedule_interval(self.countdown, 1)
        self.update_display()

    def stop_clock(self):
        Clock.unschedule(self.clock)
        self.clock = None
        self.update_display()

    def toggle_clock(self, instance=None):
        if self.finished:
            self.leave()
        elif self.clock:
            self.stop_clock()
        else:
            self.start_clock()

    def finish(self):
        self.finished = True

    def leave(self, instance=None):
        if self.leave_is_stop:
            to_service("exit")
            import time
            time.sleep(2)
            App.get_running_app().stop()
        else:
            App.get_running_app().back()


class SimpleTimerApp(App):

    def goto(self, screen):
        self.manager.current = screen

    def back(self):
        self.manager.current = 'simpletimer:settings'

    @staticmethod
    def populate(sm):
        try:
            sm.get_screen('simpletimer:settings')
            return
        except ScreenManagerException:
            pass # not populated yet

        sm.add_widget(SettingsScreen(name='simpletimer:settings'))
        sm.add_widget(TimerScreen(name='simpletimer:timer'))

    def build(self):
        sm = ScreenManager()
        self.populate(sm)
        return sm


def run():
    SimpleTimerApp().run()

import os

import math
import time
from kivy.app import App
from kivy.clock import Clock
from kivy.graphics import Color, Rectangle
from kivy.properties import BooleanProperty, NumericProperty, StringProperty
from kivy.uix.boxlayout import BoxLayout
from plyer import orientation
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.progressbar import ProgressBar
from kivy.uix.screenmanager import Screen, ScreenManager, ScreenManagerException
from kivy.core.audio import SoundLoader

from helpers.osc import oschandler, to_service
from helpers.wakelock import WakeLock
from android.runnable import run_on_ui_thread
from logging import getLogger
from kivy.lang import Builder

logger = getLogger(__name__)

pink = (1, 0.75, 0.8)
orange = (1, 0.5, 0)
cyan = (0, 1, 1)

Builder.load_string("""
# copy from the source code of kivy, but increased
<ProgressBar>:
    canvas:
        Color:
            rgb: 1, 1, 1
        BorderImage:
            border: (12, 12, 12, 12)
            pos: self.x, self.y
            size: self.width, self.height
            source: 'atlas://data/images/defaulttheme/progressbar_background'
        BorderImage:
            border: [int(min(self.width * (self.value / float(self.max)) if self.max else 0, 12))] * 4
            pos: self.x, self.y
            size: self.width * (self.value / float(self.max)) if self.max else 0, self.height
            source: 'atlas://data/images/defaulttheme/progressbar'
""")

class StateMachine(object):

    def __init__(self, manager, duration):
        self.manager = manager
        self.duration = duration
        self.state = "step"
        self.timer = self.manager.get_screen('simpletimer:timer')

    def update(self):
        if self.state == "step":
            self.timer.finish()
        else:
            raise NotImplementedError()

    def start(self):

        self.timer.start_timer(
            what="Tic, Tac...",
            color=cyan,
            duration=self.duration,
            on_done=self.update,
            sound_map={}
        )


class SettingsScreen(Screen):
    duration = NumericProperty(60)

    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

    def __init__(self, **kwargs):
        super(SettingsScreen, self).__init__(**kwargs)
        with self.canvas.before:
            self.color = Color(1, 1, 1, 1)
            self.rect = Rectangle(size=self.size, pos=self.pos)

        self.bind(size=self._update_rect, pos=self._update_rect)

        layout = BoxLayout(orientation='vertical')

        duration_layout = BoxLayout(size_hint=(1, 0.15))
        duration_layout.add_widget(
            Button(text="-",
                   background_color=(0, 0, 1, 1),
                   color=(1, 1, 1, 1),
                   on_press=self.decrease_duration))
        self.duration_label = Label(text="", color=(0, 0, 0, 1))
        duration_layout.add_widget(self.duration_label)
        duration_layout.add_widget(
            Button(text="+",
                   background_color=(0, 0, 1, 1),
                   color=(1, 1, 1, 1),
                   on_press=self.increase_duration))
        layout.add_widget(duration_layout)

        # Go button
        go_button = Button(text="Go !!!",
                           background_color=(0, 0, 1, 1),
                           color=(1, 1, 1, 1),
                           size_hint=(1, 0.15))
        go_button.bind(on_press=self.start_timer)
        layout.add_widget(go_button)


        self.add_widget(layout)
        self.update_display()

        self.bind(on_pre_enter=self.pre_enter_handler)

        @oschandler("alarmtimer:start")
        def _(config):
            self.duration = config["duration"]
            self.update_display()
            self.start_timer()

    def pre_enter_handler(self, *_):
        to_service("simpletimer:settings")
        orientation.set_sensor()

    def update_display(self):
        self.duration_label.text = "Temps: " + str(self.duration // 60) + "m"


    def increase_duration(self, instance):
        self.duration += 60
        self.update_display()

    def decrease_duration(self, instance):
        if self.duration > 60:
            self.duration -= 60
            self.update_display()

    def start_timer(self, *instance):
        app = App.get_running_app()
        app.root.state_machine = StateMachine(
            self.manager,
            self.duration,
        )
        app.root.state_machine.start()

class TimerScreen(Screen):
    remaining_time = NumericProperty(0)
    duration = NumericProperty(0)
    timer_running = BooleanProperty(False)
    what = StringProperty("")

    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

    def __init__(self, **kwargs):
        super(TimerScreen, self).__init__(**kwargs)
        self.leave_is_stop = True
        self.alarm = SoundLoader.load("/sdcard/Alarms/alarm.ogg")
        if self.alarm is None:
            raise Exception("Alarm could not be loaded")
        with self.canvas.before:
            self.color = Color(0, 1, 1, 1)
            self.rect = Rectangle(size=self.size, pos=self.pos)

        self.bind(size=self._update_rect, pos=self._update_rect)

        layout = GridLayout(cols=1)
        self.progress = ProgressBar(size_hint=(1, 0.5))

        layout.add_widget(self.progress)

        self.time_label = Label(text="", font_size='50sp', color=(0, 0, 0, 1))
        layout.add_widget(self.time_label)

        self.what_label = Label(text="", font_size='20sp', color=(0, 0, 0, 1))
        layout.add_widget(self.what_label)

        button_layout = BoxLayout()
        self.pause_button = Button(text="Pause",
                                   background_color=(0, 0, 1, 1),
                                   color=(1, 1, 1, 1))
        self.pause_button.bind(on_press=self.toggle_clock)
        button_layout.add_widget(self.pause_button)

        layout.add_widget(button_layout)
        self.add_widget(layout)

        self.bind(on_pre_enter=self.pre_enter_handler)
        self.bind(on_pre_leave=self.pre_leave_handler)

        with WakeLock("simpletimer", wakeup=True):
            self.keep_screen_on()

    def keep_screen_on(self, revert=False):
        from android import mActivity
        from jnius import autoclass
        from android.runnable import run_on_ui_thread
        LayoutParams = autoclass('android.view.WindowManager$LayoutParams')

        @run_on_ui_thread
        def do():
            if revert:
                logger.debug("Leaving screen always on")
                mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
            else:
                logger.debug("Entering screen always on")
                mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)

        do()
        time.sleep(0.5)

    def pre_leave_handler(self, *args):
        self.stop_clock()
        if self.alarmtime_clock:
            self.alarmtime_clock.cancel()
            self.alarmtime_clock = None
        self.keep_screen_on(revert=True)
        if self.alarm:
            self.alarm.stop()
            self.alarm = None
        from jnius import autoclass
        from android import mActivity

        View = autoclass('android.view.View')

        option = View.SYSTEM_UI_FLAG_VISIBLE

        @run_on_ui_thread
        def fs():
            logger.debug("Leaving fullscreen mode")
            mActivity.getWindow().getDecorView().setSystemUiVisibility(option)

        fs()

    def pre_enter_handler(self, *args):
        self.keep_screen_on()

        from jnius import autoclass
        from android import mActivity

        View = autoclass('android.view.View')

        option = View.SYSTEM_UI_FLAG_FULLSCREEN

        @run_on_ui_thread
        def fs():
            logger.debug("Entering fullscreen mode")
            mActivity.getWindow().getDecorView().setSystemUiVisibility(option)

        fs()

    def start_timer(
            self,
            what,
            color,
            duration,
            on_done,
            sound_map,
    ):
        self.wanted_color = color
        self.color.rgb = self.wanted_color
        App.get_running_app().goto("simpletimer:timer")
        self.duration = duration
        self.what = what
        self.elapsed_alarm_time = 0.
        self.duration_alarm_fade_in = 90.
        self.alarmtime_clock = None
        self.remaining_time = duration
        self.on_done = on_done
        self.finished = False
        self.sound_map = sound_map
        self.sound = self.sound_map.get(None)
        if self.sound:
            self.sound.play()

        self.start_clock()

    def update_display(self):
        self.what_label.text = self.what

        if self.clock:
            self.color.rgb = self.wanted_color
            self.pause_button.text = "Pause"
        else:
            if self.finished:
                self.what_label.text = "C'est fini !!!"
                self.pause_button.text = "Ok"
            else:
                self.color.rgb = orange
                self.pause_button.text = "Continue ?"

        self.time_label.text = str(self.remaining_time)
        self.progress.max = self.duration
        self.progress.value = self.remaining_time

    def countdown(self, dt):
        if self.remaining_time > 0:
            self.remaining_time -= 1
        else:
            self.stop_clock()
            self.alarmtime_clock = Clock.schedule_interval(self.alarmtime, 1)
            self.alarm.loop = True
            self.alarm.volume = 0
            self.color.rgb = (0, 0, 0)
            self.alarm.play()
            self.finished = True
        self.update_display()
        new_sound = self.sound_map.get(self.remaining_time)
        if new_sound:
            if self.sound:
                self.sound.stop()
                self.sound = new_sound
                self.sound.play()

    def alarmtime(self, *_):
        self.elapsed_alarm_time += 1
        self.time_label.text = str(int(self.elapsed_alarm_time))
        if self.elapsed_alarm_time < self.duration_alarm_fade_in:
            normalized_time = self.elapsed_alarm_time / self.duration_alarm_fade_in
            vol = math.sin(normalized_time * math.pi / 2)
            self.color.rgb = (vol,) * 3
            logger.debug("Alarm volume: {}".format(vol))
            self.alarm.volume = vol
        else:
            self.alarm.volume = 1.0

    def start_clock(self):
        self.clock = Clock.schedule_interval(self.countdown, 1)
        self.update_display()

    def stop_clock(self):
        Clock.unschedule(self.clock)
        self.clock = None
        self.update_display()

    def toggle_clock(self, instance=None):
        if self.finished:
            self.leave()
        elif self.clock:
            self.stop_clock()
        else:
            self.start_clock()

    def finish(self):
        self.finished = True

    def leave(self, instance=None):
        if self.leave_is_stop:
            to_service("exit")
            import time
            time.sleep(2)
            App.get_running_app().stop()
        else:
            App.get_running_app().back()


class SimpleTimerApp(App):

    def goto(self, screen):
        self.manager.current = screen

    def back(self):
        self.manager.current = 'simpletimer:settings'

    @staticmethod
    def populate(sm):
        try:
            sm.get_screen('simpletimer:settings')
            return
        except ScreenManagerException:
            pass # not populated yet

        sm.add_widget(SettingsScreen(name='simpletimer:settings'))
        sm.add_widget(TimerScreen(name='simpletimer:timer'))

    def build(self):
        sm = ScreenManager()
        self.populate(sm)
        return sm


def run():
    SimpleTimerApp().run()