mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-24 15:28:10 +00:00
feat(core): hold homescreen to lock
This commit is contained in:
parent
db2db8e6f3
commit
192d0dcf87
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
62
tests/click_tests/test_lock.py
Normal file
62
tests/click_tests/test_lock.py
Normal file
@ -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…
Reference in New Issue
Block a user