From 192d0dcf87d4e8d803e3c39c6c85566b1a608b53 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Mon, 18 Jan 2021 21:18:56 +0100 Subject: [PATCH] feat(core): hold homescreen to lock --- core/src/apps/debug/__init__.py | 11 ++++- core/src/apps/homescreen/__init__.py | 17 +------ core/src/apps/homescreen/homescreen.py | 59 ++++++++++++++++++++++++ core/src/apps/homescreen/lockscreen.py | 10 ++++- core/src/trezor/ui/loader.py | 36 +++++++++++---- python/src/trezorlib/debuglink.py | 13 +++++- tests/click_tests/test_lock.py | 62 ++++++++++++++++++++++++++ 7 files changed, 178 insertions(+), 30 deletions(-) create mode 100644 tests/click_tests/test_lock.py diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py index 327720718..deec7709e 100644 --- a/core/src/apps/debug/__init__.py +++ b/core/src/apps/debug/__init__.py @@ -79,6 +79,10 @@ if __debug__: content = await layout_change_chan.take() await ctx.write(DebugLinkLayout(lines=content)) + async def touch_hold(x: int, y: int, duration_ms: int) -> None: + await loop.sleep(duration_ms) + loop.synthetic_events.append((io.TOUCH, (io.TOUCH_END, x, y))) + async def dispatch_DebugLinkWatchLayout( ctx: wire.Context, msg: DebugLinkWatchLayout ) -> Success: @@ -94,11 +98,14 @@ if __debug__: if debuglink_decision_chan.putters: log.warning(__name__, "DebugLinkDecision queue is not empty") - if msg.x is not None: + if msg.x is not None and msg.y is not None: evt_down = io.TOUCH_START, msg.x, msg.y evt_up = io.TOUCH_END, msg.x, msg.y loop.synthetic_events.append((io.TOUCH, evt_down)) - loop.synthetic_events.append((io.TOUCH, evt_up)) + if msg.hold_ms is not None: + loop.schedule(touch_hold(msg.x, msg.y, msg.hold_ms)) + else: + loop.synthetic_events.append((io.TOUCH, evt_up)) else: debuglink_decision_chan.publish(msg) diff --git a/core/src/apps/homescreen/__init__.py b/core/src/apps/homescreen/__init__.py index 1b9cf2f47..bc9447b9a 100644 --- a/core/src/apps/homescreen/__init__.py +++ b/core/src/apps/homescreen/__init__.py @@ -1,26 +1,11 @@ import storage.device -from trezor import io, loop, res, ui +from trezor import res, ui class HomescreenBase(ui.Layout): - RENDER_SLEEP = loop.SLEEP_FOREVER - def __init__(self) -> None: super().__init__() self.label = storage.device.get_label() or "My Trezor" self.image = storage.device.get_homescreen() or res.load( "apps/homescreen/res/bg.toif" ) - - def on_tap(self) -> None: - """Called when the user taps the screen.""" - pass - - def dispatch(self, event: int, x: int, y: int) -> None: - if event is ui.REPAINT: - self.repaint = True - elif event is ui.RENDER and self.repaint: - self.repaint = False - self.on_render() - elif event is io.TOUCH_END: - self.on_tap() diff --git a/core/src/apps/homescreen/homescreen.py b/core/src/apps/homescreen/homescreen.py index 1adfc26ab..b030f5d9c 100644 --- a/core/src/apps/homescreen/homescreen.py +++ b/core/src/apps/homescreen/homescreen.py @@ -1,12 +1,25 @@ +import utime +from micropython import const + import storage import storage.device from trezor import config, ui +from trezor.ui.loader import Loader, LoaderNeutral + +from apps.base import lock_device from . import HomescreenBase +if False: + from typing import Optional + +_LOADER_DELAY_MS = const(500) +_LOADER_TOTAL_MS = const(2500) + async def homescreen() -> None: await Homescreen() + lock_device() class Homescreen(HomescreenBase): @@ -15,7 +28,17 @@ class Homescreen(HomescreenBase): if not storage.device.is_initialized(): self.label = "Go to trezor.io/start" + self.loader = Loader( + style=LoaderNeutral, + target_ms=_LOADER_TOTAL_MS - _LOADER_DELAY_MS, + offset_y=-10, + ) + self.touch_ms: Optional[int] = None + def on_render(self) -> None: + if not self.repaint: + return + # warning bar on top if storage.device.is_initialized() and storage.device.no_backup(): ui.header_error("SEEDLESS") @@ -33,3 +56,39 @@ class Homescreen(HomescreenBase): # homescreen with shifted avatar and text on bottom ui.display.avatar(48, 48 - 10, self.image, ui.WHITE, ui.BLACK) ui.display.text_center(ui.WIDTH // 2, 220, self.label, ui.BOLD, ui.FG, ui.BG) + + self.repaint = False + + def on_touch_start(self, _x: int, _y: int) -> None: + if self.loader.start_ms is not None: + self.loader.start() + elif config.has_pin(): + self.touch_ms = utime.ticks_ms() + + def on_touch_end(self, _x: int, _y: int) -> None: + if self.loader.start_ms is not None: + self.repaint = True + self.loader.stop() + self.touch_ms = None + + # raise here instead of self.loader.on_finish so as not to send TOUCH_END to the lockscreen + if self.loader.elapsed_ms() >= self.loader.target_ms: + raise ui.Result(None) + + def _loader_start(self) -> None: + ui.display.clear() + ui.display.text_center(ui.WIDTH // 2, 35, "Hold to lock", ui.BOLD, ui.FG, ui.BG) + self.loader.start() + + def dispatch(self, event: int, x: int, y: int) -> None: + if ( + self.touch_ms is not None + and self.touch_ms + _LOADER_DELAY_MS < utime.ticks_ms() + ): + self.touch_ms = None + self._loader_start() + + if event is ui.RENDER and self.loader.start_ms is not None: + self.loader.dispatch(event, x, y) + else: + super().dispatch(event, x, y) diff --git a/core/src/apps/homescreen/lockscreen.py b/core/src/apps/homescreen/lockscreen.py index ee13153ab..4f520d979 100644 --- a/core/src/apps/homescreen/lockscreen.py +++ b/core/src/apps/homescreen/lockscreen.py @@ -1,4 +1,4 @@ -from trezor import res, ui, wire +from trezor import loop, res, ui, wire from . import HomescreenBase @@ -21,6 +21,7 @@ async def lockscreen() -> None: class Lockscreen(HomescreenBase): BACKLIGHT_LEVEL = ui.BACKLIGHT_LOW + RENDER_SLEEP = loop.SLEEP_FOREVER def __init__(self, bootscreen: bool = False) -> None: if bootscreen: @@ -34,6 +35,9 @@ class Lockscreen(HomescreenBase): super().__init__() def on_render(self) -> None: + if not self.repaint: + return + # homescreen with label text on top ui.display.text_center( ui.WIDTH // 2, 35, self.label, ui.BOLD, ui.TITLE_GREY, ui.BG @@ -53,5 +57,7 @@ class Lockscreen(HomescreenBase): ) ui.display.icon(45, 202, res.load(ui.ICON_CLICK), ui.TITLE_GREY, ui.BG) - def on_tap(self) -> None: + self.repaint = False + + def on_touch_end(self, _x: int, _y: int) -> None: raise ui.Result(None) diff --git a/core/src/trezor/ui/loader.py b/core/src/trezor/ui/loader.py index 2648fe3b3..9f93a77a5 100644 --- a/core/src/trezor/ui/loader.py +++ b/core/src/trezor/ui/loader.py @@ -18,8 +18,8 @@ class LoaderDefault: class active(normal): bg_color = ui.BG fg_color = ui.GREEN - icon = ui.ICON_CHECK - icon_fg_color = ui.WHITE + icon: Optional[str] = ui.ICON_CHECK + icon_fg_color: Optional[int] = ui.WHITE class LoaderDanger(LoaderDefault): @@ -30,21 +30,36 @@ class LoaderDanger(LoaderDefault): fg_color = ui.RED +class LoaderNeutral(LoaderDefault): + class normal(LoaderDefault.normal): + fg_color = ui.FG + + class active(LoaderDefault.active): + fg_color = ui.FG + + if False: LoaderStyleType = Type[LoaderDefault] _TARGET_MS = const(1000) +_OFFSET_Y = const(-24) class Loader(ui.Component): - def __init__(self, style: LoaderStyleType = LoaderDefault) -> None: + def __init__( + self, + style: LoaderStyleType = LoaderDefault, + target_ms: int = _TARGET_MS, + offset_y: int = _OFFSET_Y, + ) -> None: super().__init__() self.normal_style = style.normal self.active_style = style.active - self.target_ms = _TARGET_MS + self.target_ms = target_ms self.start_ms: Optional[int] = None self.stop_ms: Optional[int] = None + self.offset_y = offset_y def start(self) -> None: self.start_ms = utime.ticks_ms() @@ -75,13 +90,18 @@ class Loader(ui.Component): else: s = self.active_style - _Y = const(-24) - + progress = r * 1000 // target if s.icon is None: - display.loader(r, False, _Y, s.fg_color, s.bg_color) + display.loader(progress, False, self.offset_y, s.fg_color, s.bg_color) else: display.loader( - r, False, _Y, s.fg_color, s.bg_color, res.load(s.icon), s.icon_fg_color + progress, + False, + self.offset_y, + s.fg_color, + s.bg_color, + res.load(s.icon), + s.icon_fg_color, ) if (r == 0) and (self.stop_ms is not None): self.start_ms = None diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index c64ba909a..a17223d1d 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -122,7 +122,16 @@ class DebugLink: state = self._call(messages.DebugLinkGetState(wait_word_pos=True)) return state.reset_word_pos - def input(self, word=None, button=None, swipe=None, x=None, y=None, wait=False): + def input( + self, + word=None, + button=None, + swipe=None, + x=None, + y=None, + wait=False, + hold_ms=None, + ): if not self.allow_interactions: return @@ -131,7 +140,7 @@ class DebugLink: raise ValueError("Invalid input - must use one of word, button, swipe") decision = messages.DebugLinkDecision( - yes_no=button, swipe=swipe, input=word, x=x, y=y, wait=wait + yes_no=button, swipe=swipe, input=word, x=x, y=y, wait=wait, hold_ms=hold_ms ) ret = self._call(decision, nowait=not wait) if ret is not None: diff --git a/tests/click_tests/test_lock.py b/tests/click_tests/test_lock.py new file mode 100644 index 000000000..896af04c4 --- /dev/null +++ b/tests/click_tests/test_lock.py @@ -0,0 +1,62 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2021 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +import time + +import pytest + +from .. import buttons, common + +PIN4 = "1234" + + +@pytest.mark.setup_client(pin=PIN4) +def test_hold_to_lock(device_handler): + debug = device_handler.debuglink() + + def hold(duration, wait=True): + debug.input(x=13, y=37, hold_ms=duration, wait=wait) + time.sleep(duration / 1000 + 0.5) + + assert device_handler.features().unlocked is False + + # unlock with message + device_handler.run(common.get_test_address) + layout = debug.wait_layout() + assert layout.text == "PinDialog" + debug.input("1234", wait=True) + assert device_handler.result() + + assert device_handler.features().unlocked is True + + # short touch + hold(1000, wait=False) + assert device_handler.features().unlocked is True + + # lock + hold(3500) + assert device_handler.features().unlocked is False + + # unlock by touching + layout = debug.click(buttons.INFO, wait=True) + assert layout.text == "PinDialog" + debug.input("1234", wait=True) + + assert device_handler.features().unlocked is True + + # lock + hold(3500) + assert device_handler.features().unlocked is False