mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-21 21:12:03 +00:00
feat(ui-core,ui-lvgl): channel management in the Core, L2 channel screens
ChannelControl (add Public/Hashtag/Private, rename, delete, notif/fav/scope) and Favourites slots move into ui-core; ui-new uses them. Contact / channel pref cleanup moves from ui-new's UITask into UiCore so it runs on every frontend (L2 had none). L2: channel options popup (hold a row or the gear in a channel) and the Add / Edit channel form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
// Either way only a 16-byte (128-bit) secret is produced, matching the
|
||||
// existing CMD_GET_CHANNEL comment ("NOTE: only 128-bit supported").
|
||||
|
||||
#include <Utils.h>
|
||||
#include "../ui-core/ChannelControl.h"
|
||||
|
||||
class ChannelsView {
|
||||
UITask* _task;
|
||||
@@ -57,107 +57,31 @@ class ChannelsView {
|
||||
_kb_active = true;
|
||||
}
|
||||
|
||||
// Parses exactly 32 hex chars into a 16-byte secret (zero-padded to 32 for
|
||||
// ChannelDetails.secret's full field width). Rejects malformed hex and the
|
||||
// all-zero sentinel setChannelLocal() reads as "slot deleted" (see
|
||||
// isAllZero() in MyMesh.cpp) -- saving one would pass, then immediately
|
||||
// trigger onChannelRemoved() on this very slot, silently discarding the
|
||||
// channel and its bot/notif/favourite state. Shared by deriveSecret()'s
|
||||
// hex-mode branch and the Public quick-add (a fixed, known-good constant,
|
||||
// but parsed through the same path rather than duplicated).
|
||||
static bool hexToSecret(const char* hex32, uint8_t out[32]) {
|
||||
if (strlen(hex32) != 32) return false;
|
||||
uint8_t tmp[16];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
char byte_str[3] = { hex32[i * 2], hex32[i * 2 + 1], 0 };
|
||||
char* end = nullptr;
|
||||
long v = strtol(byte_str, &end, 16);
|
||||
if (*end != 0) return false;
|
||||
tmp[i] = (uint8_t)v;
|
||||
}
|
||||
bool all_zero = true;
|
||||
for (int i = 0; i < 16 && all_zero; i++) if (tmp[i]) all_zero = false;
|
||||
if (all_zero) return false;
|
||||
memset(out, 0, 32);
|
||||
memcpy(out, tmp, 16);
|
||||
return true;
|
||||
// Secret parsing, duplicate check and the save itself live in the Core
|
||||
// (ui-core/ChannelControl.h), shared with ui-lvgl.
|
||||
void report(chanctl::Result r, const char* ok_text) {
|
||||
if (r == chanctl::OK) { _task->showAlert(ok_text, 1000); _mode = OFF; }
|
||||
else if (r == chanctl::BAD_SECRET) _task->showAlert("Invalid secret", 1400);
|
||||
else _task->showAlert(chanctl::resultText(r), 1400);
|
||||
}
|
||||
|
||||
// Turn the typed Secret field into a 16-byte channel secret. Returns false
|
||||
// (and leaves out[] untouched) if the field can't be parsed as configured.
|
||||
bool deriveSecret(uint8_t out[32]) const {
|
||||
if (_hex_mode) return hexToSecret(_secret_text, out);
|
||||
if (_secret_text[0] == '\0') return false; // blank passphrase not allowed
|
||||
uint8_t digest[32];
|
||||
mesh::Utils::sha256(digest, sizeof(digest), (const uint8_t*)_secret_text, (int)strlen(_secret_text));
|
||||
memset(out, 0, 32);
|
||||
memcpy(out, digest, 16);
|
||||
return true;
|
||||
}
|
||||
|
||||
// True if a *different* slot already holds this secret. The secret is the
|
||||
// channel's identity on the air (the name is only a label), so a second slot
|
||||
// with the same key would be the same channel listed twice -- with split
|
||||
// history/unread state. Edit skips its own slot (_idx) so re-saving a channel
|
||||
// under a new name still works.
|
||||
bool secretInUse(const uint8_t secret[32]) const {
|
||||
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
if (i == _idx) continue;
|
||||
ChannelDetails ch;
|
||||
if (!the_mesh.getChannel(i, ch) || ch.name[0] == '\0') continue;
|
||||
if (memcmp(ch.channel.secret, secret, 16) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool saveChannel(const char* name, const uint8_t secret[32]) {
|
||||
ChannelDetails ch;
|
||||
memset(&ch, 0, sizeof(ch));
|
||||
strncpy(ch.name, name, sizeof(ch.name) - 1);
|
||||
memcpy(ch.channel.secret, secret, sizeof(ch.channel.secret));
|
||||
return the_mesh.setChannelLocal((uint8_t)_idx, ch);
|
||||
}
|
||||
|
||||
// Type picker's Public row: no fields to fill in, so this commits directly
|
||||
// instead of dropping into the Name+Secret form -- the well-known channel's
|
||||
// name and secret are both fixed. Hex confirmed via
|
||||
// `python3 -c "import base64; print(base64.b64decode('izOH6cXN6mrJ5e26oRXNcg==').hex())"`
|
||||
// to match MyMesh.cpp's PUBLIC_GROUP_PSK (base64) and
|
||||
// docs/companion_protocol.md's documented public channel key (hex).
|
||||
// Type picker's Public row: no fields to fill in -- the well-known
|
||||
// channel's name and secret are both fixed.
|
||||
void commitPublic() {
|
||||
static const char PUBLIC_SECRET_HEX[] = "8b3387e9c5cdea6ac9e5edbaa115cd72";
|
||||
uint8_t secret[32];
|
||||
hexToSecret(PUBLIC_SECRET_HEX, secret); // fixed, known-good constant -- can't fail
|
||||
if (secretInUse(secret)) { _task->showAlert("Already added", 1200); _mode = OFF; return; }
|
||||
if (saveChannel("Public", secret)) _task->showAlert("Channel added", 1000);
|
||||
else _task->showAlert("Save failed", 1200);
|
||||
chanctl::Result r = chanctl::addPublic();
|
||||
if (r == chanctl::EXISTS) { _task->showAlert("Already added", 1200); _mode = OFF; return; }
|
||||
report(r, "Channel added");
|
||||
_mode = OFF;
|
||||
}
|
||||
|
||||
void commit() {
|
||||
if (_mode == ADD_HASHTAG) {
|
||||
if (_topic[0] == '\0') { _task->showAlert("Topic required", 1200); return; }
|
||||
// "#topic" as both the channel's display name and the passphrase fed to
|
||||
// sha256 -- the exact "Hashtag Channels" convention in
|
||||
// docs/companion_protocol.md, just synthesized instead of hand-typed.
|
||||
snprintf(_name, sizeof(_name), "#%s", _topic);
|
||||
strncpy(_secret_text, _name, sizeof(_secret_text) - 1);
|
||||
_secret_text[sizeof(_secret_text) - 1] = '\0';
|
||||
_hex_mode = false;
|
||||
}
|
||||
if (_name[0] == '\0') { _task->showAlert("Name required", 1200); return; }
|
||||
uint8_t secret[32];
|
||||
if (!deriveSecret(secret)) {
|
||||
_task->showAlert(_hex_mode ? "Invalid secret" : "Secret required", 1400);
|
||||
report(chanctl::addHashtag(_topic), "Channel added");
|
||||
return;
|
||||
}
|
||||
if (secretInUse(secret)) { _task->showAlert("Channel already exists", 1400); return; }
|
||||
if (saveChannel(_name, secret)) {
|
||||
_task->showAlert((_mode == ADD || _mode == ADD_HASHTAG) ? "Channel added" : "Channel updated", 1000);
|
||||
_mode = OFF;
|
||||
} else {
|
||||
_task->showAlert("Save failed", 1200);
|
||||
}
|
||||
report(chanctl::savePrivate(_idx, _name, _secret_text, _hex_mode),
|
||||
_mode == ADD ? "Channel added" : "Channel updated");
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
@@ -886,15 +886,9 @@ class MessagesScreen : public UIScreen {
|
||||
memcpy(tbl[0].prefix, pub_key, 4); tbl[0].*val = v; // table full — overwrite slot 0
|
||||
}
|
||||
|
||||
// Channel notif: state 1 = muted (variant bit set), 2 = force-on (variant clear).
|
||||
uint8_t chNotifState(uint8_t ch_idx) const {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
return p ? maskPairGet(p->ch_notif_override, p->ch_notif_muted, ch_idx, 1, 2) : 0;
|
||||
}
|
||||
void setChNotifState(uint8_t ch_idx, uint8_t state) {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
if (p) maskPairSet(p->ch_notif_override, p->ch_notif_muted, ch_idx, state, 1);
|
||||
}
|
||||
// Channel notif (ui-core/ChannelControl.h): 0 = default, 1 = muted, 2 = force-on.
|
||||
uint8_t chNotifState(uint8_t ch_idx) const { return chanctl::notif(_task->getNodePrefs(), ch_idx); }
|
||||
void setChNotifState(uint8_t ch_idx, uint8_t state) { chanctl::setNotif(_task->getNodePrefs(), ch_idx, state); }
|
||||
|
||||
uint8_t dmNotifState(const uint8_t* pub_key) const {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
@@ -955,13 +949,7 @@ public:
|
||||
bool navActive() const { return _nav_active; }
|
||||
|
||||
// First free channel slot (existing config or blank name), or -1 if full.
|
||||
int findFreeChannelSlot() const {
|
||||
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
ChannelDetails ch;
|
||||
if (!the_mesh.getChannel(i, ch) || ch.name[0] == '\0') return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
int findFreeChannelSlot() const { return chanctl::freeSlot(); }
|
||||
|
||||
// CHANNEL_PICK row count including the synthetic "+ Add channel" row
|
||||
// (suppressed while picking a channel for the bot).
|
||||
@@ -2198,9 +2186,7 @@ public:
|
||||
if (_ch_delete_confirm_active) {
|
||||
// Delete/Cancel sub-menu, defaults to Cancel (see where it's opened).
|
||||
if (res == PopupMenu::SELECTED && _ctx_menu.selectedIndex() == 0) { // "Delete"
|
||||
ChannelDetails ch;
|
||||
memset(&ch, 0, sizeof(ch));
|
||||
the_mesh.setChannelLocal(_ctx_ch_idx, ch);
|
||||
chanctl::remove(_ctx_ch_idx);
|
||||
_task->showAlert("Channel deleted", 1000);
|
||||
}
|
||||
if (res != PopupMenu::NONE) {
|
||||
|
||||
@@ -2893,104 +2893,6 @@ void UITask::setTargetNow(uint8_t kind, const uint8_t* key, int32_t lat, int32_t
|
||||
void UITask::clearTarget() { _core->locator.clearTarget(); }
|
||||
void UITask::clearTargetIfWaypoint(int32_t lat_1e6, int32_t lon_1e6) { _core->locator.clearTargetIfWaypoint(lat_1e6, lon_1e6); }
|
||||
|
||||
// CONTRACT: every NodePrefs field that keys on a contact pubkey/prefix is
|
||||
// cleared here, so a removed contact can't leave a dangling reference. If you
|
||||
// add such a field, add its cleanup below (and mark the field in NodePrefs.h).
|
||||
// Currently covered: favourite_contacts, locator_key, loc_share_dm_prefix,
|
||||
// dm_notif[], dm_melody[]. Also clears the DM unread table (RAM-only, not a
|
||||
// NodePrefs field, so no savePrefs() needed for it) -- same 4-byte-prefix
|
||||
// shape and same 16-slot starvation risk as dm_notif/dm_melody above. Called
|
||||
// for both explicit removal and silent auto-eviction (see MyMesh
|
||||
// CMD_REMOVE_CONTACT / onContactOverwrite).
|
||||
void UITask::onContactRemoved(const uint8_t* pub_key) {
|
||||
if (!_node_prefs || !pub_key) return;
|
||||
bool changed = false;
|
||||
|
||||
clearDMUnread(pub_key);
|
||||
|
||||
int slot = findFavouriteSlot(pub_key);
|
||||
if (slot >= 0) { clearFavouriteSlot(slot); changed = true; }
|
||||
|
||||
if (_core->locator.onContactRemoved(pub_key)) changed = true;
|
||||
// Fail closed rather than guess a new recipient: a contact target that's
|
||||
// gone just turns auto-share off, it doesn't fall back to some other target.
|
||||
if (_node_prefs->loc_share_target_type == 1
|
||||
&& memcmp(_node_prefs->loc_share_dm_prefix, pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
||||
_node_prefs->loc_share_enabled = 0;
|
||||
changed = true;
|
||||
}
|
||||
// Same fail-closed rule for the room bot's target: the room contact is
|
||||
// gone, disable it rather than risk a re-added contact silently inheriting
|
||||
// the old bot config.
|
||||
if (_node_prefs->bot_room_enabled
|
||||
&& memcmp(_node_prefs->bot_room_prefix, pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
||||
_node_prefs->bot_room_enabled = 0;
|
||||
changed = true;
|
||||
}
|
||||
// Per-contact mute/melody overrides — only 16 slots shared across every
|
||||
// contact, so an orphaned entry isn't just stale, it can eventually starve
|
||||
// new overrides for contacts that still exist. Keyed by a 4-byte prefix
|
||||
// (narrower than the 6-byte one above), so compare only that many bytes.
|
||||
for (int i = 0; i < NodePrefs::DM_NOTIF_TABLE_MAX; i++) {
|
||||
if (_node_prefs->dm_notif[i].state && memcmp(_node_prefs->dm_notif[i].prefix, pub_key, 4) == 0) {
|
||||
memset(&_node_prefs->dm_notif[i], 0, sizeof(_node_prefs->dm_notif[i]));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) {
|
||||
if (_node_prefs->dm_melody[i].slot && memcmp(_node_prefs->dm_melody[i].prefix, pub_key, 4) == 0) {
|
||||
memset(&_node_prefs->dm_melody[i], 0, sizeof(_node_prefs->dm_melody[i]));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) the_mesh.savePrefs();
|
||||
}
|
||||
|
||||
// CONTRACT: every NodePrefs field that keys on a channel index is cleared here,
|
||||
// so a channel re-added at a freed slot can't inherit the old one's settings.
|
||||
// If you add such a field, add its cleanup below (and mark it in NodePrefs.h).
|
||||
// Currently covered: bot_channel_idx, loc_share_channel_idx, ch_notif_melody_*,
|
||||
// ch_notif_override/ch_notif_muted, ch_fav_bitmask, favourite_contacts/_kinds,
|
||||
// ch_scope_idx.
|
||||
void UITask::onChannelRemoved(uint8_t channel_idx) {
|
||||
if (!_node_prefs) return;
|
||||
bool changed = false;
|
||||
|
||||
if (_node_prefs->bot_channel_enabled && _node_prefs->bot_channel_idx == channel_idx) {
|
||||
_node_prefs->bot_channel_enabled = 0;
|
||||
changed = true;
|
||||
}
|
||||
// Fail closed, same policy as onContactRemoved()'s Live Share case.
|
||||
if (_node_prefs->loc_share_target_type == 0 && _node_prefs->loc_share_channel_idx == channel_idx) {
|
||||
_node_prefs->loc_share_enabled = 0;
|
||||
changed = true;
|
||||
}
|
||||
uint64_t mask = 1ULL << channel_idx;
|
||||
if (_node_prefs->ch_notif_melody_set & mask) {
|
||||
_node_prefs->ch_notif_melody_set &= ~mask;
|
||||
_node_prefs->ch_notif_melody_2 &= ~mask;
|
||||
changed = true;
|
||||
}
|
||||
if (_node_prefs->ch_notif_override & mask) {
|
||||
_node_prefs->ch_notif_override &= ~mask;
|
||||
_node_prefs->ch_notif_muted &= ~mask;
|
||||
changed = true;
|
||||
}
|
||||
if (_node_prefs->ch_fav_bitmask & mask) {
|
||||
_node_prefs->ch_fav_bitmask &= ~mask;
|
||||
changed = true;
|
||||
}
|
||||
if (channel_idx < NodePrefs::MAX_SCOPED_CHANNELS && _node_prefs->ch_scope_idx[channel_idx]) {
|
||||
_node_prefs->ch_scope_idx[channel_idx] = 0; // back to "*", same as a never-configured channel
|
||||
changed = true;
|
||||
}
|
||||
int fav_slot = findFavouriteChannelSlot(channel_idx);
|
||||
if (fav_slot >= 0) { clearFavouriteSlot(fav_slot); changed = true; }
|
||||
|
||||
if (changed) the_mesh.savePrefs();
|
||||
}
|
||||
|
||||
// Homing beeper: while armed with a target and inside the radius, emit a short
|
||||
// tick whose interval shrinks linearly with distance — slow at the edge, rapid
|
||||
// near the centre. Polls distance a few times a second; silent outside the
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include "../LiveTrack.h"
|
||||
#include "KeyboardWidget.h"
|
||||
#include "../ui-core/UiCoreHost.h"
|
||||
#include "../ui-core/Favourites.h"
|
||||
|
||||
class UiCore;
|
||||
struct UiEvent;
|
||||
@@ -298,17 +299,8 @@ public:
|
||||
// from waypoint deletion so the Locator can't keep pointing at a spot
|
||||
// that no longer exists.
|
||||
void clearTargetIfWaypoint(int32_t lat_1e6, int32_t lon_1e6);
|
||||
// A contact was removed (companion app / CLI): drop any UI reference to its
|
||||
// pubkey that would otherwise dangle — a pinned favourite slot, the Locator
|
||||
// target if it was this contact, the Live Share target if it was this
|
||||
// contact (auto-share turns off rather than guessing a new recipient), and
|
||||
// any per-contact mute/melody override (those tables have only 16 shared
|
||||
// slots, so an orphan isn't just stale — it can starve other contacts).
|
||||
void onContactRemoved(const uint8_t* pub_key) override;
|
||||
// A channel slot was cleared: turn off anything armed against it by index
|
||||
// (the bot's channel, Live Share's channel target) and drop its per-channel
|
||||
// melody bit, so a future channel re-added at the same slot starts clean.
|
||||
void onChannelRemoved(uint8_t channel_idx) override;
|
||||
// Contact / channel removal cleanup lives in the Core (UiCore::onContactRemoved,
|
||||
// chanctl::onRemoved).
|
||||
// Resolve a person target (6-byte pubkey prefix) to a current position:
|
||||
// prefers an active [LOC] live share, falls back to their last-advertised
|
||||
// GPS fix. Returns false when neither is known. Optional live/ts report
|
||||
@@ -397,56 +389,13 @@ public:
|
||||
// Favourites dial helpers. Slot index 0..FAVOURITES_COUNT-1. A slot holds
|
||||
// either a contact/room (pubkey prefix) or a channel (index), per
|
||||
// favourite_kinds[] — see NodePrefs.
|
||||
uint8_t favouriteSlotKind(int slot) const {
|
||||
if (!_node_prefs || slot < 0 || slot >= NodePrefs::FAVOURITES_COUNT) return NodePrefs::FAV_KIND_CONTACT;
|
||||
return _node_prefs->favourite_kinds[slot];
|
||||
}
|
||||
int findFavouriteSlot(const uint8_t* pub_key) const {
|
||||
if (!_node_prefs || !pub_key) return -1;
|
||||
for (int i = 0; i < NodePrefs::FAVOURITES_COUNT; i++) {
|
||||
if (_node_prefs->favourite_kinds[i] != NodePrefs::FAV_KIND_CONTACT) continue;
|
||||
if (memcmp(_node_prefs->favourite_contacts[i], pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
||||
// All-zero prefix is "empty" — never matches a real key.
|
||||
bool any = false;
|
||||
for (uint8_t b = 0; b < NodePrefs::FAVOURITE_PREFIX_LEN; b++)
|
||||
if (_node_prefs->favourite_contacts[i][b]) { any = true; break; }
|
||||
if (any) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
int findFavouriteChannelSlot(uint8_t ch_idx) const {
|
||||
if (!_node_prefs) return -1;
|
||||
for (int i = 0; i < NodePrefs::FAVOURITES_COUNT; i++) {
|
||||
if (_node_prefs->favourite_kinds[i] == NodePrefs::FAV_KIND_CHANNEL &&
|
||||
_node_prefs->favourite_contacts[i][0] == ch_idx) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
bool isFavouriteSlotEmpty(int slot) const {
|
||||
if (!_node_prefs || slot < 0 || slot >= NodePrefs::FAVOURITES_COUNT) return true;
|
||||
// A channel slot is never empty: channel 0's prefix is all zeroes.
|
||||
if (_node_prefs->favourite_kinds[slot] == NodePrefs::FAV_KIND_CHANNEL) return false;
|
||||
for (uint8_t b = 0; b < NodePrefs::FAVOURITE_PREFIX_LEN; b++)
|
||||
if (_node_prefs->favourite_contacts[slot][b]) return false;
|
||||
return true;
|
||||
}
|
||||
void setFavouriteSlot(int slot, const uint8_t* pub_key) {
|
||||
if (!_node_prefs || slot < 0 || slot >= NodePrefs::FAVOURITES_COUNT || !pub_key) return;
|
||||
memcpy(_node_prefs->favourite_contacts[slot], pub_key, NodePrefs::FAVOURITE_PREFIX_LEN);
|
||||
_node_prefs->favourite_kinds[slot] = NodePrefs::FAV_KIND_CONTACT;
|
||||
}
|
||||
void setFavouriteChannelSlot(int slot, uint8_t ch_idx) {
|
||||
if (!_node_prefs || slot < 0 || slot >= NodePrefs::FAVOURITES_COUNT) return;
|
||||
memset(_node_prefs->favourite_contacts[slot], 0, NodePrefs::FAVOURITE_PREFIX_LEN);
|
||||
_node_prefs->favourite_contacts[slot][0] = ch_idx;
|
||||
_node_prefs->favourite_kinds[slot] = NodePrefs::FAV_KIND_CHANNEL;
|
||||
}
|
||||
void clearFavouriteSlot(int slot) {
|
||||
if (!_node_prefs || slot < 0 || slot >= NodePrefs::FAVOURITES_COUNT) return;
|
||||
memset(_node_prefs->favourite_contacts[slot], 0, NodePrefs::FAVOURITE_PREFIX_LEN);
|
||||
_node_prefs->favourite_kinds[slot] = NodePrefs::FAV_KIND_CONTACT;
|
||||
}
|
||||
uint8_t favouriteSlotKind(int slot) const { return favslots::kind(_node_prefs, slot); }
|
||||
int findFavouriteSlot(const uint8_t* pub_key) const { return favslots::findContact(_node_prefs, pub_key); }
|
||||
int findFavouriteChannelSlot(uint8_t ch_idx) const { return favslots::findChannel(_node_prefs, ch_idx); }
|
||||
bool isFavouriteSlotEmpty(int slot) const { return favslots::isEmpty(_node_prefs, slot); }
|
||||
void setFavouriteSlot(int slot, const uint8_t* pub_key) { favslots::setContact(_node_prefs, slot, pub_key); }
|
||||
void setFavouriteChannelSlot(int slot, uint8_t ch_idx) { favslots::setChannel(_node_prefs, slot, ch_idx); }
|
||||
void clearFavouriteSlot(int slot) { favslots::clear(_node_prefs, slot); }
|
||||
bool isButtonPressed() const;
|
||||
|
||||
// Lock-screen support: the HomeScreen LOCK page draws the unlock hint from
|
||||
|
||||
Reference in New Issue
Block a user