feat(core): hold homescreen to lock

pull/1504/head
Martin Milata 3 years ago
parent db2db8e6f3
commit 192d0dcf87

@ -79,6 +79,10 @@ if __debug__:
content = await layout_change_chan.take() content = await layout_change_chan.take()
await ctx.write(DebugLinkLayout(lines=content)) 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( async def dispatch_DebugLinkWatchLayout(
ctx: wire.Context, msg: DebugLinkWatchLayout ctx: wire.Context, msg: DebugLinkWatchLayout
) -> Success: ) -> Success:
@ -94,11 +98,14 @@ if __debug__:
if debuglink_decision_chan.putters: if debuglink_decision_chan.putters:
log.warning(__name__, "DebugLinkDecision queue is not empty") 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_down = io.TOUCH_START, msg.x, msg.y
evt_up = io.TOUCH_END, 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_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: else:
debuglink_decision_chan.publish(msg) debuglink_decision_chan.publish(msg)

@ -1,26 +1,11 @@
import storage.device import storage.device
from trezor import io, loop, res, ui from trezor import res, ui
class HomescreenBase(ui.Layout): class HomescreenBase(ui.Layout):
RENDER_SLEEP = loop.SLEEP_FOREVER
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.label = storage.device.get_label() or "My Trezor" self.label = storage.device.get_label() or "My Trezor"
self.image = storage.device.get_homescreen() or res.load( self.image = storage.device.get_homescreen() or res.load(
"apps/homescreen/res/bg.toif" "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()

@ -1,12 +1,25 @@
import utime
from micropython import const
import storage import storage
import storage.device import storage.device
from trezor import config, ui from trezor import config, ui
from trezor.ui.loader import Loader, LoaderNeutral
from apps.base import lock_device
from . import HomescreenBase from . import HomescreenBase
if False:
from typing import Optional
_LOADER_DELAY_MS = const(500)
_LOADER_TOTAL_MS = const(2500)
async def homescreen() -> None: async def homescreen() -> None:
await Homescreen() await Homescreen()
lock_device()
class Homescreen(HomescreenBase): class Homescreen(HomescreenBase):
@ -15,7 +28,17 @@ class Homescreen(HomescreenBase):
if not storage.device.is_initialized(): if not storage.device.is_initialized():
self.label = "Go to trezor.io/start" 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: def on_render(self) -> None:
if not self.repaint:
return
# warning bar on top # warning bar on top
if storage.device.is_initialized() and storage.device.no_backup(): if storage.device.is_initialized() and storage.device.no_backup():
ui.header_error("SEEDLESS") ui.header_error("SEEDLESS")
@ -33,3 +56,39 @@ class Homescreen(HomescreenBase):
# homescreen with shifted avatar and text on bottom # homescreen with shifted avatar and text on bottom
ui.display.avatar(48, 48 - 10, self.image, ui.WHITE, ui.BLACK) 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) 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)

@ -1,4 +1,4 @@
from trezor import res, ui, wire from trezor import loop, res, ui, wire
from . import HomescreenBase from . import HomescreenBase
@ -21,6 +21,7 @@ async def lockscreen() -> None:
class Lockscreen(HomescreenBase): class Lockscreen(HomescreenBase):
BACKLIGHT_LEVEL = ui.BACKLIGHT_LOW BACKLIGHT_LEVEL = ui.BACKLIGHT_LOW
RENDER_SLEEP = loop.SLEEP_FOREVER
def __init__(self, bootscreen: bool = False) -> None: def __init__(self, bootscreen: bool = False) -> None:
if bootscreen: if bootscreen:
@ -34,6 +35,9 @@ class Lockscreen(HomescreenBase):
super().__init__() super().__init__()
def on_render(self) -> None: def on_render(self) -> None:
if not self.repaint:
return
# homescreen with label text on top # homescreen with label text on top
ui.display.text_center( ui.display.text_center(
ui.WIDTH // 2, 35, self.label, ui.BOLD, ui.TITLE_GREY, ui.BG 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) 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) raise ui.Result(None)

@ -18,8 +18,8 @@ class LoaderDefault:
class active(normal): class active(normal):
bg_color = ui.BG bg_color = ui.BG
fg_color = ui.GREEN fg_color = ui.GREEN
icon = ui.ICON_CHECK icon: Optional[str] = ui.ICON_CHECK
icon_fg_color = ui.WHITE icon_fg_color: Optional[int] = ui.WHITE
class LoaderDanger(LoaderDefault): class LoaderDanger(LoaderDefault):
@ -30,21 +30,36 @@ class LoaderDanger(LoaderDefault):
fg_color = ui.RED 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: if False:
LoaderStyleType = Type[LoaderDefault] LoaderStyleType = Type[LoaderDefault]
_TARGET_MS = const(1000) _TARGET_MS = const(1000)
_OFFSET_Y = const(-24)
class Loader(ui.Component): 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__() super().__init__()
self.normal_style = style.normal self.normal_style = style.normal
self.active_style = style.active self.active_style = style.active
self.target_ms = _TARGET_MS self.target_ms = target_ms
self.start_ms: Optional[int] = None self.start_ms: Optional[int] = None
self.stop_ms: Optional[int] = None self.stop_ms: Optional[int] = None
self.offset_y = offset_y
def start(self) -> None: def start(self) -> None:
self.start_ms = utime.ticks_ms() self.start_ms = utime.ticks_ms()
@ -75,13 +90,18 @@ class Loader(ui.Component):
else: else:
s = self.active_style s = self.active_style
_Y = const(-24) progress = r * 1000 // target
if s.icon is None: 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: else:
display.loader( 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): if (r == 0) and (self.stop_ms is not None):
self.start_ms = None self.start_ms = None

@ -122,7 +122,16 @@ class DebugLink:
state = self._call(messages.DebugLinkGetState(wait_word_pos=True)) state = self._call(messages.DebugLinkGetState(wait_word_pos=True))
return state.reset_word_pos 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: if not self.allow_interactions:
return return
@ -131,7 +140,7 @@ class DebugLink:
raise ValueError("Invalid input - must use one of word, button, swipe") raise ValueError("Invalid input - must use one of word, button, swipe")
decision = messages.DebugLinkDecision( 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) ret = self._call(decision, nowait=not wait)
if ret is not None: if ret is not None:

@ -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 <https://www.gnu.org/licenses/lgpl-3.0.html>.
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
Loading…
Cancel
Save