mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-10-22 21:22:14 +00:00
feat: add nrf lora key editing
This commit is contained in:
1 parent
51a92f16a7
commit
6504a3bc8e
4 files changed
+705
-54
No files matched your search
@@ -195,7 +195,7 @@ class TEchoLiteBoard final : public BoardBase
|
||||
void applyKeyboardBacklight();
|
||||
bool ensureKeyboardBacklightReady();
|
||||
bool ensureMessageAudioReady();
|
||||
bool playMessageToneStep(unsigned frequency_hz, uint16_t duration_ms);
|
||||
bool playMessageToneStep(unsigned start_frequency_hz, unsigned end_frequency_hz, uint16_t duration_ms);
|
||||
|
||||
bool initialized_ = false;
|
||||
bool i2c_initialized_ = false;
|
||||
|
||||
@@ -235,18 +235,29 @@ bool applyAw21009Brightness(TEchoLiteBoard& board, uint16_t brightness)
|
||||
return ok;
|
||||
}
|
||||
|
||||
void fillSquareToneBuffer(unsigned frequency_hz, uint8_t volume_percent)
|
||||
void fillCooToneBuffer(unsigned start_frequency_hz, unsigned end_frequency_hz, uint8_t volume_percent)
|
||||
{
|
||||
const unsigned safe_frequency = frequency_hz == 0U ? 1U : frequency_hz;
|
||||
const uint32_t half_period_samples =
|
||||
std::max<uint32_t>(1U, kMessageToneSampleRateHz / (safe_frequency * 2U));
|
||||
const uint8_t volume = volume_percent > 100U ? 100U : volume_percent;
|
||||
const int16_t amplitude = static_cast<int16_t>(600 + ((static_cast<uint16_t>(volume) * 11000U) / 100U));
|
||||
const double amplitude = 500.0 + (static_cast<double>(volume) * 80.0);
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
const unsigned safe_start = start_frequency_hz == 0U ? 1U : start_frequency_hz;
|
||||
const unsigned safe_end = end_frequency_hz == 0U ? safe_start : end_frequency_hz;
|
||||
double phase = 0.0;
|
||||
|
||||
for (uint16_t index = 0; index < kMessageToneI2sWords; ++index)
|
||||
{
|
||||
const bool positive = ((index / half_period_samples) & 0x01U) == 0U;
|
||||
const int16_t sample = positive ? amplitude : static_cast<int16_t>(-amplitude);
|
||||
const double t = static_cast<double>(index) /
|
||||
static_cast<double>(kMessageToneI2sWords > 1U ? kMessageToneI2sWords - 1U : 1U);
|
||||
const double glide = static_cast<double>(safe_start) +
|
||||
(static_cast<double>(safe_end) - static_cast<double>(safe_start)) * t;
|
||||
const double vibrato = std::sin(2.0 * kPi * 5.0 * t) * 7.0;
|
||||
const double frequency = std::max(1.0, glide + vibrato);
|
||||
const double attack = std::min(1.0, t / 0.12);
|
||||
const double release = std::min(1.0, (1.0 - t) / 0.18);
|
||||
const double envelope = std::max(0.0, std::min(attack, release));
|
||||
phase += (2.0 * kPi * frequency) / static_cast<double>(kMessageToneSampleRateHz);
|
||||
const double voice = std::sin(phase) + 0.22 * std::sin(phase * 2.0);
|
||||
const int16_t sample = static_cast<int16_t>(voice * amplitude * envelope);
|
||||
const uint16_t packed_sample = static_cast<uint16_t>(sample);
|
||||
s_message_tone_i2s_buffer[index] =
|
||||
static_cast<uint32_t>(packed_sample) | (static_cast<uint32_t>(packed_sample) << 16);
|
||||
@@ -581,7 +592,9 @@ bool TEchoLiteBoard::ensureMessageAudioReady()
|
||||
return audio_codec_ready_;
|
||||
}
|
||||
|
||||
bool TEchoLiteBoard::playMessageToneStep(unsigned frequency_hz, uint16_t duration_ms)
|
||||
bool TEchoLiteBoard::playMessageToneStep(unsigned start_frequency_hz,
|
||||
unsigned end_frequency_hz,
|
||||
uint16_t duration_ms)
|
||||
{
|
||||
const auto& audio = kBoardProfile.audio;
|
||||
if (audio.dac_data < 0 || audio.bit_clock < 0 || audio.word_select < 0)
|
||||
@@ -589,7 +602,7 @@ bool TEchoLiteBoard::playMessageToneStep(unsigned frequency_hz, uint16_t duratio
|
||||
return false;
|
||||
}
|
||||
|
||||
fillSquareToneBuffer(frequency_hz, message_tone_volume_);
|
||||
fillCooToneBuffer(start_frequency_hz, end_frequency_hz, message_tone_volume_);
|
||||
|
||||
nrf_i2s_disable(NRF_I2S);
|
||||
nrf_i2s_event_clear(NRF_I2S, NRF_I2S_EVENT_TXPTRUPD);
|
||||
@@ -625,8 +638,6 @@ bool TEchoLiteBoard::playMessageToneStep(unsigned frequency_hz, uint16_t duratio
|
||||
|
||||
void TEchoLiteBoard::playMessageTone()
|
||||
{
|
||||
pulseNotificationLed(25);
|
||||
|
||||
if (message_tone_volume_ == 0)
|
||||
{
|
||||
return;
|
||||
@@ -634,22 +645,25 @@ void TEchoLiteBoard::playMessageTone()
|
||||
|
||||
struct ToneStep
|
||||
{
|
||||
unsigned frequency_hz;
|
||||
unsigned start_frequency_hz;
|
||||
unsigned end_frequency_hz;
|
||||
uint16_t duration_ms;
|
||||
uint16_t gap_ms;
|
||||
};
|
||||
|
||||
const bool audio_ready = ensureMessageAudioReady();
|
||||
static constexpr ToneStep kMessageTone[] = {
|
||||
{1760U, 70U, 25U},
|
||||
{2093U, 110U, 0U},
|
||||
{430U, 340U, 135U, 45U},
|
||||
{360U, 295U, 150U, 0U},
|
||||
};
|
||||
|
||||
for (const ToneStep& step : kMessageTone)
|
||||
{
|
||||
if (audio_ready)
|
||||
{
|
||||
(void)playMessageToneStep(step.frequency_hz, step.duration_ms);
|
||||
(void)playMessageToneStep(step.start_frequency_hz,
|
||||
step.end_frequency_hz,
|
||||
step.duration_ms);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -164,6 +164,8 @@ class Runtime : public chat::ChatService::IncomingTextObserver
|
||||
None = 0,
|
||||
Message,
|
||||
MeshCoreChannelName,
|
||||
MeshtasticPrimaryKey,
|
||||
MeshCoreChannelKey,
|
||||
};
|
||||
|
||||
void handleInput(InputAction action);
|
||||
@@ -223,7 +225,7 @@ class Runtime : public chat::ChatService::IncomingTextObserver
|
||||
void appendComposeWord(const char* text);
|
||||
void appendComposeRawText(const char* text);
|
||||
void activateComposeAction();
|
||||
void saveEditedTextToConfig();
|
||||
bool saveEditedTextToConfig();
|
||||
void formatTime(char* out_time, size_t out_len, char* out_date, size_t date_len) const;
|
||||
void formatTimestamp(char* out, size_t out_len, uint32_t timestamp_s) const;
|
||||
void formatConversationTime(char* out, size_t out_len, uint32_t timestamp_s, bool expanded = false) const;
|
||||
@@ -242,6 +244,10 @@ class Runtime : public chat::ChatService::IncomingTextObserver
|
||||
const char* text, bool selected, bool align_right);
|
||||
void refreshGnssSnapshot(bool force = false);
|
||||
bool editUsesHexCharset() const;
|
||||
bool editUsesKeyCharset() const;
|
||||
bool editUsesDirectTextEntry() const;
|
||||
bool editAllowsChineseInput() const;
|
||||
bool editAcceptsTextInput() const;
|
||||
bool usesSmartCompose() const;
|
||||
bool usesPhysicalTextInput() const;
|
||||
bool usesFullVirtualKeyboard() const;
|
||||
@@ -348,7 +354,7 @@ class Runtime : public chat::ChatService::IncomingTextObserver
|
||||
size_t message_info_count_ = 0;
|
||||
|
||||
EditTarget edit_target_ = EditTarget::None;
|
||||
static constexpr size_t kComposeMax = 64;
|
||||
static constexpr size_t kComposeMax = 96;
|
||||
char compose_buffer_[kComposeMax] = {};
|
||||
size_t compose_len_ = 0;
|
||||
size_t compose_charset_index_ = 0;
|
||||
|
||||
+667
-36
@@ -4,6 +4,7 @@
|
||||
#include "ble/ble_manager.h"
|
||||
#include "chat/infra/mesh_protocol_utils.h"
|
||||
#include "chat/infra/meshcore/mc_region_presets.h"
|
||||
#include "chat/infra/meshtastic/mt_protocol_helpers.h"
|
||||
#include "chat/infra/meshtastic/mt_region.h"
|
||||
#include "chat/runtime/self_identity_policy.h"
|
||||
#include "chat/usecase/chat_service.h"
|
||||
@@ -113,6 +114,7 @@ enum class RadioSettingItem
|
||||
MtCodingRate,
|
||||
MtTxPower,
|
||||
MtChannelSlot,
|
||||
MtPrimaryKey,
|
||||
MtOverrideFrequency,
|
||||
MtFrequencyOffset,
|
||||
McRegion,
|
||||
@@ -123,6 +125,7 @@ enum class RadioSettingItem
|
||||
McTxPower,
|
||||
McChannelSlot,
|
||||
McChannelName,
|
||||
McChannelKey,
|
||||
Encrypt,
|
||||
};
|
||||
|
||||
@@ -142,6 +145,7 @@ constexpr RadioSettingDef kRadioItems[] = {
|
||||
{RadioSettingItem::MtCodingRate, "MT CR"},
|
||||
{RadioSettingItem::MtTxPower, "MT TX"},
|
||||
{RadioSettingItem::MtChannelSlot, "MT SLOT"},
|
||||
{RadioSettingItem::MtPrimaryKey, "MT PSK"},
|
||||
{RadioSettingItem::MtOverrideFrequency, "MT FREQ"},
|
||||
{RadioSettingItem::MtFrequencyOffset, "MT OFFSET"},
|
||||
{RadioSettingItem::McRegion, "MC REGION"},
|
||||
@@ -152,6 +156,7 @@ constexpr RadioSettingDef kRadioItems[] = {
|
||||
{RadioSettingItem::McTxPower, "MC TX"},
|
||||
{RadioSettingItem::McChannelSlot, "MC SLOT"},
|
||||
{RadioSettingItem::McChannelName, "MC NAME"},
|
||||
{RadioSettingItem::McChannelKey, "MC KEY"},
|
||||
{RadioSettingItem::Encrypt, "ENCRYPT"},
|
||||
};
|
||||
|
||||
@@ -261,19 +266,19 @@ constexpr size_t kNodeActionItemCount = 7;
|
||||
|
||||
constexpr const char* kWeekdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
|
||||
constexpr const char* kComposeCharset =
|
||||
" ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?-_/:#@+*=()[]";
|
||||
" ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?-_/:#@+*=%&$~()[]{}<>\\|;\"'`";
|
||||
constexpr const char* kHexCharset = "0123456789ABCDEF";
|
||||
constexpr const char* kComposeAlphaKeys = "QWERTYUIOPASDFGHJKLZXCVBNM";
|
||||
constexpr const char* kComposeAbcKeys = " ETAOINSHRDLUCMFWYPVBGKQJXZ";
|
||||
constexpr const char* kComposeNumKeys = "0123456789";
|
||||
constexpr const char* kComposeSymKeys = " .,!?/-:@#()[]+=";
|
||||
constexpr const char* kComposeSymKeys = " .,!?/-:@#()[]{}+=_*%&$~\\|;\"'`<>";
|
||||
constexpr const char* kComposeActionLabels[] = {"ABC", "SP", "BACK", "DEL", "SEND"};
|
||||
constexpr size_t kVirtualKeyboardCols = 3;
|
||||
constexpr size_t kVirtualKeyboardRows = 3;
|
||||
constexpr size_t kVirtualKeyboardPageSize = kVirtualKeyboardCols * kVirtualKeyboardRows;
|
||||
constexpr const char* kFullAlphaRows[] = {"QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"};
|
||||
constexpr const char* kFullNumRows[] = {"1234567890"};
|
||||
constexpr const char* kFullSymRows[] = {".,!?/-:@#", "()[]+="};
|
||||
constexpr const char* kFullSymRows[] = {".,!?/-:@#", "()[]{}+=", "_*%&$~\\|"};
|
||||
|
||||
struct PinyinCandidateEntry
|
||||
{
|
||||
@@ -287,6 +292,7 @@ constexpr uint32_t kBootMinMs = 1800;
|
||||
constexpr uint32_t kComposeMultiTapWindowMs = 1500;
|
||||
constexpr uint32_t kLargeScreensaverRefreshMs = 60000;
|
||||
constexpr uint32_t kGnssSnapshotRefreshMs = 5000;
|
||||
constexpr size_t kKeyTextMax = 96;
|
||||
constexpr int kTimezoneMin = -12 * 60;
|
||||
constexpr int kTimezoneMax = 14 * 60;
|
||||
constexpr int kTimezoneStep = 60;
|
||||
@@ -742,6 +748,7 @@ const char* composeModeLabel(ui::mono::Runtime::ComposeMode mode)
|
||||
switch (mode)
|
||||
{
|
||||
case ui::mono::Runtime::ComposeMode::AbcLower:
|
||||
return "en";
|
||||
case ui::mono::Runtime::ComposeMode::AbcUpper:
|
||||
return "EN";
|
||||
case ui::mono::Runtime::ComposeMode::Num:
|
||||
@@ -761,6 +768,8 @@ const char* physicalComposeModeLabel(ui::mono::Runtime::ComposeMode mode)
|
||||
{
|
||||
case ui::mono::Runtime::ComposeMode::Num:
|
||||
return "123";
|
||||
case ui::mono::Runtime::ComposeMode::Sym:
|
||||
return "SYN";
|
||||
case ui::mono::Runtime::ComposeMode::Cn:
|
||||
return "CN";
|
||||
case ui::mono::Runtime::ComposeMode::AbcLower:
|
||||
@@ -827,6 +836,35 @@ const char* phoneCycleCharsForKey(char ch)
|
||||
}
|
||||
}
|
||||
|
||||
const char* phoneSymbolCycleCharsForKey(char ch)
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case '1':
|
||||
return ".,?!1";
|
||||
case '2':
|
||||
return "@/:;2";
|
||||
case '3':
|
||||
return "#+-=3";
|
||||
case '4':
|
||||
return "_$%&4";
|
||||
case '5':
|
||||
return "()[]5";
|
||||
case '6':
|
||||
return "{}<>6";
|
||||
case '7':
|
||||
return "'\"`~7";
|
||||
case '8':
|
||||
return "\\|*^8";
|
||||
case '9':
|
||||
return "-+=/9";
|
||||
case '0':
|
||||
return " 0";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
char applyPhoneCycleCase(ui::mono::Runtime::ComposeMode mode, char ch)
|
||||
{
|
||||
if (std::isalpha(static_cast<unsigned char>(ch)) == 0)
|
||||
@@ -849,7 +887,9 @@ void formatPhoneCycleHint(char* out, size_t out_len,
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
const char* chars = phoneCycleCharsForKey(key);
|
||||
const char* chars = mode == ui::mono::Runtime::ComposeMode::Sym
|
||||
? phoneSymbolCycleCharsForKey(key)
|
||||
: phoneCycleCharsForKey(key);
|
||||
const size_t count = std::strlen(chars);
|
||||
if (count == 0)
|
||||
{
|
||||
@@ -1340,6 +1380,447 @@ void formatOffset(char* out, size_t out_len, float offset_mhz)
|
||||
std::snprintf(out, out_len, "%+.3f", static_cast<double>(offset_mhz));
|
||||
}
|
||||
|
||||
bool textEqualsIgnoreCase(const char* lhs, const char* rhs)
|
||||
{
|
||||
if (!lhs || !rhs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
while (*lhs && *rhs)
|
||||
{
|
||||
if (upperAscii(*lhs) != upperAscii(*rhs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
++lhs;
|
||||
++rhs;
|
||||
}
|
||||
return *lhs == '\0' && *rhs == '\0';
|
||||
}
|
||||
|
||||
void compactKeyText(const char* input, char* out, size_t out_len)
|
||||
{
|
||||
if (!out || out_len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
out[0] = '\0';
|
||||
if (!input)
|
||||
{
|
||||
return;
|
||||
}
|
||||
size_t pos = 0;
|
||||
for (size_t i = 0; input[i] != '\0' && pos + 1 < out_len; ++i)
|
||||
{
|
||||
const unsigned char ch = static_cast<unsigned char>(input[i]);
|
||||
if (std::isspace(ch) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
out[pos++] = static_cast<char>(ch);
|
||||
}
|
||||
out[pos] = '\0';
|
||||
}
|
||||
|
||||
int hexValue(char ch)
|
||||
{
|
||||
if (ch >= '0' && ch <= '9')
|
||||
{
|
||||
return ch - '0';
|
||||
}
|
||||
if (ch >= 'a' && ch <= 'f')
|
||||
{
|
||||
return ch - 'a' + 10;
|
||||
}
|
||||
if (ch >= 'A' && ch <= 'F')
|
||||
{
|
||||
return ch - 'A' + 10;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool decodeHexBytes(const char* text, uint8_t* out, size_t out_capacity, size_t* out_len)
|
||||
{
|
||||
if (out_len)
|
||||
{
|
||||
*out_len = 0;
|
||||
}
|
||||
if (!text || !out || out_capacity == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
char digits[chat::kMeshtasticChannelKeyMaxLen * 2 + 1] = {};
|
||||
size_t digit_count = 0;
|
||||
for (size_t i = 0; text[i] != '\0'; ++i)
|
||||
{
|
||||
const char ch = text[i];
|
||||
if (hexValue(ch) >= 0)
|
||||
{
|
||||
if (digit_count + 1 >= sizeof(digits))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
digits[digit_count++] = ch;
|
||||
continue;
|
||||
}
|
||||
if (ch == ':' || ch == '-' || ch == '_')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (digit_count == 0 || (digit_count % 2U) != 0U)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t byte_count = digit_count / 2U;
|
||||
if (byte_count > out_capacity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < byte_count; ++i)
|
||||
{
|
||||
const int hi = hexValue(digits[i * 2U]);
|
||||
const int lo = hexValue(digits[i * 2U + 1U]);
|
||||
if (hi < 0 || lo < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out[i] = static_cast<uint8_t>((hi << 4) | lo);
|
||||
}
|
||||
if (out_len)
|
||||
{
|
||||
*out_len = byte_count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int base64Value(char ch)
|
||||
{
|
||||
if (ch >= 'A' && ch <= 'Z')
|
||||
{
|
||||
return ch - 'A';
|
||||
}
|
||||
if (ch >= 'a' && ch <= 'z')
|
||||
{
|
||||
return ch - 'a' + 26;
|
||||
}
|
||||
if (ch >= '0' && ch <= '9')
|
||||
{
|
||||
return ch - '0' + 52;
|
||||
}
|
||||
if (ch == '+')
|
||||
{
|
||||
return 62;
|
||||
}
|
||||
if (ch == '/')
|
||||
{
|
||||
return 63;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool decodeBase64Bytes(const char* text, uint8_t* out, size_t out_capacity, size_t* out_len)
|
||||
{
|
||||
if (out_len)
|
||||
{
|
||||
*out_len = 0;
|
||||
}
|
||||
if (!text || !out)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t out_pos = 0;
|
||||
int quartet[4] = {};
|
||||
size_t quartet_len = 0;
|
||||
bool saw_padding = false;
|
||||
for (size_t i = 0;; ++i)
|
||||
{
|
||||
const char ch = text[i];
|
||||
if (ch == '\0' || std::isspace(static_cast<unsigned char>(ch)) != 0)
|
||||
{
|
||||
if (ch == '\0')
|
||||
{
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int value = 0;
|
||||
if (ch == '=')
|
||||
{
|
||||
value = -2;
|
||||
saw_padding = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (saw_padding)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
value = base64Value(ch);
|
||||
if (value < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
quartet[quartet_len++] = value;
|
||||
if (quartet_len < 4)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (quartet[0] < 0 || quartet[1] < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const uint32_t packed = (static_cast<uint32_t>(quartet[0]) << 18) |
|
||||
(static_cast<uint32_t>(quartet[1]) << 12) |
|
||||
(static_cast<uint32_t>(quartet[2] < 0 ? 0 : quartet[2]) << 6) |
|
||||
static_cast<uint32_t>(quartet[3] < 0 ? 0 : quartet[3]);
|
||||
const size_t emit_count = quartet[2] < 0 ? 1U : (quartet[3] < 0 ? 2U : 3U);
|
||||
if (out_pos + emit_count > out_capacity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out[out_pos++] = static_cast<uint8_t>((packed >> 16) & 0xFFU);
|
||||
if (emit_count >= 2U)
|
||||
{
|
||||
out[out_pos++] = static_cast<uint8_t>((packed >> 8) & 0xFFU);
|
||||
}
|
||||
if (emit_count >= 3U)
|
||||
{
|
||||
out[out_pos++] = static_cast<uint8_t>(packed & 0xFFU);
|
||||
}
|
||||
quartet_len = 0;
|
||||
}
|
||||
|
||||
if (quartet_len != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (out_len)
|
||||
{
|
||||
*out_len = out_pos;
|
||||
}
|
||||
return out_pos > 0;
|
||||
}
|
||||
|
||||
void encodeBase64Bytes(const uint8_t* data, size_t len, char* out, size_t out_len)
|
||||
{
|
||||
static constexpr char kAlphabet[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
if (!out || out_len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
out[0] = '\0';
|
||||
if (!data || len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
size_t pos = 0;
|
||||
for (size_t i = 0; i < len && pos + 4 < out_len; i += 3U)
|
||||
{
|
||||
const size_t remain = std::min<size_t>(3U, len - i);
|
||||
const uint32_t packed = (static_cast<uint32_t>(data[i]) << 16) |
|
||||
(remain > 1U ? (static_cast<uint32_t>(data[i + 1U]) << 8) : 0U) |
|
||||
(remain > 2U ? static_cast<uint32_t>(data[i + 2U]) : 0U);
|
||||
out[pos++] = kAlphabet[(packed >> 18) & 0x3FU];
|
||||
out[pos++] = kAlphabet[(packed >> 12) & 0x3FU];
|
||||
out[pos++] = remain > 1U ? kAlphabet[(packed >> 6) & 0x3FU] : '=';
|
||||
out[pos++] = remain > 2U ? kAlphabet[packed & 0x3FU] : '=';
|
||||
}
|
||||
out[pos] = '\0';
|
||||
}
|
||||
|
||||
bool meshtasticDefaultPsk(uint8_t* out, size_t out_capacity, size_t* out_len)
|
||||
{
|
||||
if (out_len)
|
||||
{
|
||||
*out_len = 0;
|
||||
}
|
||||
if (!out || out_capacity < chat::kMeshtasticChannelKeyDefaultLen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
size_t expanded_len = 0;
|
||||
chat::meshtastic::expandShortPsk(1U, out, &expanded_len);
|
||||
if (expanded_len == 0 || expanded_len > out_capacity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (out_len)
|
||||
{
|
||||
*out_len = expanded_len;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isMeshtasticDefaultPsk(const uint8_t* key, size_t len)
|
||||
{
|
||||
uint8_t default_key[chat::kMeshtasticChannelKeyDefaultLen] = {};
|
||||
size_t default_len = 0;
|
||||
return meshtasticDefaultPsk(default_key, sizeof(default_key), &default_len) &&
|
||||
len == default_len &&
|
||||
std::memcmp(key, default_key, default_len) == 0;
|
||||
}
|
||||
|
||||
uint8_t storedMeshtasticKeyLen(const chat::MeshConfig& mesh)
|
||||
{
|
||||
return chat::normalizeMeshtasticChannelKeyLen(mesh.primary_key,
|
||||
sizeof(mesh.primary_key),
|
||||
mesh.primary_key_len);
|
||||
}
|
||||
|
||||
void formatMeshtasticPrimaryKeySummary(const chat::MeshConfig& mesh, char* out, size_t out_len)
|
||||
{
|
||||
if (!out || out_len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const uint8_t key_len = storedMeshtasticKeyLen(mesh);
|
||||
if (key_len == 0 || isMeshtasticDefaultPsk(mesh.primary_key, key_len))
|
||||
{
|
||||
std::snprintf(out, out_len, "AQ==");
|
||||
return;
|
||||
}
|
||||
std::snprintf(out, out_len, "B64 %uB", static_cast<unsigned>(key_len));
|
||||
}
|
||||
|
||||
void formatMeshtasticPrimaryKeyForEdit(const chat::MeshConfig& mesh, char* out, size_t out_len)
|
||||
{
|
||||
if (!out || out_len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const uint8_t key_len = storedMeshtasticKeyLen(mesh);
|
||||
if (key_len == 0 || isMeshtasticDefaultPsk(mesh.primary_key, key_len))
|
||||
{
|
||||
std::snprintf(out, out_len, "AQ==");
|
||||
return;
|
||||
}
|
||||
encodeBase64Bytes(mesh.primary_key, key_len, out, out_len);
|
||||
}
|
||||
|
||||
void formatMeshCoreChannelKeySummary(const chat::MeshConfig& mesh, char* out, size_t out_len)
|
||||
{
|
||||
if (!out || out_len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (chat::isAllZeroKeyBytes(mesh.secondary_key, chat::kMeshCoreChannelKeyLen))
|
||||
{
|
||||
std::snprintf(out, out_len, "PUBLIC");
|
||||
return;
|
||||
}
|
||||
std::snprintf(out, out_len, "B64 16B");
|
||||
}
|
||||
|
||||
void formatMeshCoreChannelKeyForEdit(const chat::MeshConfig& mesh, char* out, size_t out_len)
|
||||
{
|
||||
if (!out || out_len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (chat::isAllZeroKeyBytes(mesh.secondary_key, chat::kMeshCoreChannelKeyLen))
|
||||
{
|
||||
out[0] = '\0';
|
||||
return;
|
||||
}
|
||||
encodeBase64Bytes(mesh.secondary_key, chat::kMeshCoreChannelKeyLen, out, out_len);
|
||||
}
|
||||
|
||||
bool applyMeshtasticPrimaryKeyText(chat::MeshConfig& mesh, const char* input)
|
||||
{
|
||||
char compact[kKeyTextMax] = {};
|
||||
compactKeyText(input, compact, sizeof(compact));
|
||||
|
||||
std::memset(mesh.primary_key, 0, sizeof(mesh.primary_key));
|
||||
mesh.primary_key_len = 0;
|
||||
if (compact[0] == '\0' ||
|
||||
textEqualsIgnoreCase(compact, "DEFAULT") ||
|
||||
textEqualsIgnoreCase(compact, "PUBLIC"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t decoded[chat::kMeshtasticChannelKeyMaxLen] = {};
|
||||
size_t decoded_len = 0;
|
||||
if (decodeHexBytes(compact, decoded, sizeof(decoded), &decoded_len))
|
||||
{
|
||||
if (decoded_len != chat::kMeshtasticChannelKeyDefaultLen &&
|
||||
decoded_len != chat::kMeshtasticChannelKeyMaxLen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::memcpy(mesh.primary_key, decoded, decoded_len);
|
||||
mesh.primary_key_len = static_cast<uint8_t>(decoded_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!decodeBase64Bytes(compact, decoded, sizeof(decoded), &decoded_len))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (decoded_len == 1U)
|
||||
{
|
||||
size_t expanded_len = 0;
|
||||
if (!meshtasticDefaultPsk(mesh.primary_key, sizeof(mesh.primary_key), &expanded_len))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (decoded[0] != 1U)
|
||||
{
|
||||
chat::meshtastic::expandShortPsk(decoded[0], mesh.primary_key, &expanded_len);
|
||||
}
|
||||
mesh.primary_key_len = static_cast<uint8_t>(expanded_len);
|
||||
return expanded_len == chat::kMeshtasticChannelKeyDefaultLen;
|
||||
}
|
||||
if (decoded_len != chat::kMeshtasticChannelKeyDefaultLen &&
|
||||
decoded_len != chat::kMeshtasticChannelKeyMaxLen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::memcpy(mesh.primary_key, decoded, decoded_len);
|
||||
mesh.primary_key_len = static_cast<uint8_t>(decoded_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool applyMeshCoreChannelKeyText(chat::MeshConfig& mesh, const char* input)
|
||||
{
|
||||
char compact[kKeyTextMax] = {};
|
||||
compactKeyText(input, compact, sizeof(compact));
|
||||
|
||||
std::memset(mesh.secondary_key, 0, sizeof(mesh.secondary_key));
|
||||
mesh.secondary_key_len = 0;
|
||||
if (compact[0] == '\0' ||
|
||||
textEqualsIgnoreCase(compact, "PUBLIC") ||
|
||||
textEqualsIgnoreCase(compact, "NONE"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t decoded[chat::kMeshtasticChannelKeyMaxLen] = {};
|
||||
size_t decoded_len = 0;
|
||||
if (!decodeHexBytes(compact, decoded, sizeof(decoded), &decoded_len) &&
|
||||
!decodeBase64Bytes(compact, decoded, sizeof(decoded), &decoded_len))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (decoded_len != chat::kMeshCoreChannelKeyLen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::memcpy(mesh.secondary_key, decoded, chat::kMeshCoreChannelKeyLen);
|
||||
mesh.secondary_key_len = static_cast<uint8_t>(chat::kMeshCoreChannelKeyLen);
|
||||
return true;
|
||||
}
|
||||
|
||||
void formatRadioSettingValue(const app::AppConfig& cfg, RadioSettingItem item, char* out, size_t out_len)
|
||||
{
|
||||
if (!out || out_len == 0)
|
||||
@@ -1381,6 +1862,9 @@ void formatRadioSettingValue(const app::AppConfig& cfg, RadioSettingItem item, c
|
||||
case RadioSettingItem::MtChannelSlot:
|
||||
formatMeshtasticChannelSlot(out, out_len, mt.channel_num);
|
||||
return;
|
||||
case RadioSettingItem::MtPrimaryKey:
|
||||
formatMeshtasticPrimaryKeySummary(mt, out, out_len);
|
||||
return;
|
||||
case RadioSettingItem::MtOverrideFrequency:
|
||||
if (mt.override_frequency_mhz <= 0.0f)
|
||||
{
|
||||
@@ -1421,6 +1905,9 @@ void formatRadioSettingValue(const app::AppConfig& cfg, RadioSettingItem item, c
|
||||
case RadioSettingItem::McChannelName:
|
||||
std::snprintf(out, out_len, "%s", mc.meshcore_channel_name);
|
||||
return;
|
||||
case RadioSettingItem::McChannelKey:
|
||||
formatMeshCoreChannelKeySummary(mc, out, out_len);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2354,10 +2841,27 @@ void Runtime::handleInput(InputAction action)
|
||||
}
|
||||
else if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary)
|
||||
{
|
||||
if (radioSettingItem(radio_index_) == RadioSettingItem::McChannelName)
|
||||
const RadioSettingItem item = radioSettingItem(radio_index_);
|
||||
if (item == RadioSettingItem::McChannelName)
|
||||
{
|
||||
openCompose(EditTarget::MeshCoreChannelName, app()->getConfig().meshcore_config.meshcore_channel_name);
|
||||
}
|
||||
else if (item == RadioSettingItem::MtPrimaryKey)
|
||||
{
|
||||
char key_text[kKeyTextMax] = {};
|
||||
formatMeshtasticPrimaryKeyForEdit(app()->getConfig().meshtastic_config,
|
||||
key_text,
|
||||
sizeof(key_text));
|
||||
openCompose(EditTarget::MeshtasticPrimaryKey, key_text);
|
||||
}
|
||||
else if (item == RadioSettingItem::McChannelKey)
|
||||
{
|
||||
char key_text[kKeyTextMax] = {};
|
||||
formatMeshCoreChannelKeyForEdit(app()->getConfig().meshcore_config,
|
||||
key_text,
|
||||
sizeof(key_text));
|
||||
openCompose(EditTarget::MeshCoreChannelKey, key_text);
|
||||
}
|
||||
else
|
||||
{
|
||||
beginSettingPopup(Page::RadioSettings, radio_index_);
|
||||
@@ -3438,11 +3942,31 @@ void Runtime::renderCompose()
|
||||
}
|
||||
const int line_h = text_renderer_.lineHeight();
|
||||
|
||||
char target[16] = {};
|
||||
formatComposeTarget(target, sizeof(target));
|
||||
char to_line[24] = {};
|
||||
std::snprintf(to_line, sizeof(to_line), "TO:%s", target[0] != '\0' ? target : "-");
|
||||
drawTextClipped(0, 0, display_.width(), to_line, false);
|
||||
if (edit_target_ == EditTarget::Message)
|
||||
{
|
||||
char target[16] = {};
|
||||
formatComposeTarget(target, sizeof(target));
|
||||
char to_line[24] = {};
|
||||
std::snprintf(to_line, sizeof(to_line), "TO:%s", target[0] != '\0' ? target : "-");
|
||||
drawTextClipped(0, 0, display_.width(), to_line, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
const char* title = "EDIT";
|
||||
if (edit_target_ == EditTarget::MeshtasticPrimaryKey)
|
||||
{
|
||||
title = "MT PSK";
|
||||
}
|
||||
else if (edit_target_ == EditTarget::MeshCoreChannelKey)
|
||||
{
|
||||
title = "MC KEY";
|
||||
}
|
||||
else if (edit_target_ == EditTarget::MeshCoreChannelName)
|
||||
{
|
||||
title = "MC NAME";
|
||||
}
|
||||
drawTitleBar(title, composeModeLabel(compose_mode_));
|
||||
}
|
||||
|
||||
constexpr size_t kBodyVisiblePerLine = 18;
|
||||
char body_text[kComposeMax + 1] = {};
|
||||
@@ -3587,7 +4111,11 @@ void Runtime::renderCompose()
|
||||
|
||||
const int action_y = line_h * 7;
|
||||
const int action_x[5] = {0, 24, 48, 72, 96};
|
||||
const char* action_labels[5] = {composeModeLabel(compose_mode_), " ", "\xE2\x86\x90", "\xE2\x87\xA4", "SEND"};
|
||||
const char* action_labels[5] = {composeModeLabel(compose_mode_),
|
||||
" ",
|
||||
"\xE2\x86\x90",
|
||||
"\xE2\x87\xA4",
|
||||
edit_target_ == EditTarget::Message ? "SEND" : "OK"};
|
||||
for (size_t i = 0; i < 5; ++i)
|
||||
{
|
||||
char action_cell[10] = {};
|
||||
@@ -4293,9 +4821,18 @@ void Runtime::openCompose(EditTarget target, const char* seed_text)
|
||||
compose_buffer_[0] = '\0';
|
||||
compose_len_ = 0;
|
||||
compose_charset_index_ = 0;
|
||||
compose_mode_ = usesPhysicalTextInput()
|
||||
? (target == EditTarget::Message ? ComposeMode::Cn : ComposeMode::AbcLower)
|
||||
: ComposeMode::AbcLower;
|
||||
if (target == EditTarget::Message && usesPhysicalTextInput())
|
||||
{
|
||||
compose_mode_ = ComposeMode::Cn;
|
||||
}
|
||||
else if (editUsesKeyCharset())
|
||||
{
|
||||
compose_mode_ = ComposeMode::AbcUpper;
|
||||
}
|
||||
else
|
||||
{
|
||||
compose_mode_ = ComposeMode::AbcLower;
|
||||
}
|
||||
compose_focus_ = ComposeFocus::Body;
|
||||
compose_action_index_ = 0;
|
||||
compose_abc_group_index_ = 0;
|
||||
@@ -4323,9 +4860,9 @@ void Runtime::openCompose(EditTarget target, const char* seed_text)
|
||||
|
||||
void Runtime::finishTextEdit(bool accept)
|
||||
{
|
||||
if (accept)
|
||||
if (accept && !saveEditedTextToConfig())
|
||||
{
|
||||
saveEditedTextToConfig();
|
||||
return;
|
||||
}
|
||||
edit_target_ = EditTarget::None;
|
||||
enterPage(page_before_compose_);
|
||||
@@ -5008,6 +5545,10 @@ void Runtime::confirmSettingPopup()
|
||||
app()->setBleEnabled(setting_popup_ble_enabled_);
|
||||
#endif
|
||||
app()->saveConfig();
|
||||
if (setting_popup_owner_ == Page::RadioSettings)
|
||||
{
|
||||
app()->applyMeshConfig();
|
||||
}
|
||||
if (setting_popup_owner_ == Page::DeviceSettings)
|
||||
{
|
||||
app()->applyPositionConfig();
|
||||
@@ -5103,6 +5644,8 @@ void Runtime::adjustSettingPopup(int delta)
|
||||
current + delta, 0, static_cast<int>(kMeshtasticChannelNumMax)));
|
||||
break;
|
||||
}
|
||||
case RadioSettingItem::MtPrimaryKey:
|
||||
break;
|
||||
case RadioSettingItem::MtOverrideFrequency:
|
||||
if (cfg.meshtastic_config.override_frequency_mhz <= 0.0f)
|
||||
{
|
||||
@@ -5176,6 +5719,7 @@ void Runtime::adjustSettingPopup(int delta)
|
||||
static_cast<int>(cfg.meshcore_config.meshcore_channel_slot) + delta, 0, 15));
|
||||
break;
|
||||
case RadioSettingItem::McChannelName:
|
||||
case RadioSettingItem::McChannelKey:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -5458,26 +6002,19 @@ void Runtime::cycleComposeMode()
|
||||
if (usesPhysicalTextInput())
|
||||
{
|
||||
(void)commitPhysicalComposePreedit(true);
|
||||
if (compose_mode_ == ComposeMode::Num)
|
||||
{
|
||||
compose_mode_ = ComposeMode::AbcLower;
|
||||
}
|
||||
else if (compose_mode_ == ComposeMode::AbcLower || compose_mode_ == ComposeMode::AbcUpper)
|
||||
{
|
||||
compose_mode_ = ComposeMode::Cn;
|
||||
}
|
||||
else
|
||||
{
|
||||
compose_mode_ = ComposeMode::Num;
|
||||
}
|
||||
}
|
||||
else if (compose_mode_ == ComposeMode::Num)
|
||||
|
||||
if (compose_mode_ == ComposeMode::Num)
|
||||
{
|
||||
compose_mode_ = ComposeMode::AbcLower;
|
||||
}
|
||||
else if (compose_mode_ == ComposeMode::AbcLower || compose_mode_ == ComposeMode::AbcUpper)
|
||||
else if (compose_mode_ == ComposeMode::AbcLower)
|
||||
{
|
||||
compose_mode_ = ComposeMode::Cn;
|
||||
compose_mode_ = ComposeMode::AbcUpper;
|
||||
}
|
||||
else if (compose_mode_ == ComposeMode::AbcUpper)
|
||||
{
|
||||
compose_mode_ = editAllowsChineseInput() ? ComposeMode::Cn : ComposeMode::Sym;
|
||||
}
|
||||
else if (compose_mode_ == ComposeMode::Cn)
|
||||
{
|
||||
@@ -5521,6 +6058,37 @@ bool Runtime::handlePhysicalComposeText(char ch)
|
||||
}
|
||||
|
||||
compose_focus_ = ComposeFocus::Body;
|
||||
if (compose_mode_ == ComposeMode::Sym)
|
||||
{
|
||||
(void)commitPhysicalComposePreedit(true);
|
||||
const char* chars = phoneSymbolCycleCharsForKey(ch);
|
||||
const size_t char_count = std::strlen(chars);
|
||||
if (char_count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint32_t now = nowMs();
|
||||
const bool can_cycle = compose_physical_last_key_ == ch &&
|
||||
compose_len_ > 0 &&
|
||||
(now - compose_abc_last_tap_ms_) <= kComposeMultiTapWindowMs;
|
||||
if (can_cycle)
|
||||
{
|
||||
compose_abc_tap_index_ = (compose_abc_tap_index_ + 1U) % char_count;
|
||||
compose_buffer_[compose_len_ - 1U] = chars[compose_abc_tap_index_];
|
||||
}
|
||||
else
|
||||
{
|
||||
compose_abc_tap_index_ = 0;
|
||||
appendChar(compose_buffer_, sizeof(compose_buffer_), compose_len_, chars[0]);
|
||||
}
|
||||
|
||||
compose_physical_last_key_ = ch;
|
||||
compose_abc_last_tap_ms_ = now;
|
||||
compose_abc_last_group_index_ = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (compose_mode_ == ComposeMode::Cn)
|
||||
{
|
||||
if (ch == '0')
|
||||
@@ -5760,6 +6328,15 @@ void Runtime::appendComposeChar(char ch)
|
||||
return;
|
||||
}
|
||||
|
||||
if (editUsesDirectTextEntry())
|
||||
{
|
||||
appendChar(compose_buffer_,
|
||||
sizeof(compose_buffer_),
|
||||
compose_len_,
|
||||
isAlphaAscii(ch) ? applyComposeAlphaCase(compose_mode_, ch) : ch);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((isAlphaComposeMode(compose_mode_) || compose_mode_ == ComposeMode::Cn) && isAlphaAscii(ch))
|
||||
{
|
||||
appendChar(compose_preedit_, sizeof(compose_preedit_), compose_preedit_len_, upperAscii(ch));
|
||||
@@ -5835,28 +6412,58 @@ void Runtime::activateComposeAction()
|
||||
compose_focus_ = ComposeFocus::Body;
|
||||
break;
|
||||
case ComposeAction::Send:
|
||||
sendComposeMessage();
|
||||
if (edit_target_ == EditTarget::Message)
|
||||
{
|
||||
sendComposeMessage();
|
||||
}
|
||||
else
|
||||
{
|
||||
finishTextEdit(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Runtime::saveEditedTextToConfig()
|
||||
bool Runtime::saveEditedTextToConfig()
|
||||
{
|
||||
if (!app())
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& cfg = app()->getConfig();
|
||||
bool mesh_config_changed = false;
|
||||
switch (edit_target_)
|
||||
{
|
||||
case EditTarget::MeshCoreChannelName:
|
||||
copyText(cfg.meshcore_config.meshcore_channel_name, compose_buffer_);
|
||||
mesh_config_changed = true;
|
||||
break;
|
||||
case EditTarget::MeshtasticPrimaryKey:
|
||||
if (!applyMeshtasticPrimaryKeyText(cfg.meshtastic_config, compose_buffer_))
|
||||
{
|
||||
showTransientPopup("INVALID KEY", "MT PSK");
|
||||
return false;
|
||||
}
|
||||
mesh_config_changed = true;
|
||||
break;
|
||||
case EditTarget::MeshCoreChannelKey:
|
||||
if (!applyMeshCoreChannelKeyText(cfg.meshcore_config, compose_buffer_))
|
||||
{
|
||||
showTransientPopup("INVALID KEY", "MC KEY");
|
||||
return false;
|
||||
}
|
||||
mesh_config_changed = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
commitConfig();
|
||||
if (mesh_config_changed)
|
||||
{
|
||||
app()->applyMeshConfig();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Runtime::formatTime(char* out_time, size_t out_len, char* out_date, size_t date_len) const
|
||||
@@ -6419,14 +7026,38 @@ bool Runtime::editUsesHexCharset() const
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Runtime::editUsesKeyCharset() const
|
||||
{
|
||||
return edit_target_ == EditTarget::MeshtasticPrimaryKey ||
|
||||
edit_target_ == EditTarget::MeshCoreChannelKey;
|
||||
}
|
||||
|
||||
bool Runtime::editUsesDirectTextEntry() const
|
||||
{
|
||||
return edit_target_ == EditTarget::MeshCoreChannelName ||
|
||||
editUsesKeyCharset();
|
||||
}
|
||||
|
||||
bool Runtime::editAllowsChineseInput() const
|
||||
{
|
||||
return edit_target_ == EditTarget::Message;
|
||||
}
|
||||
|
||||
bool Runtime::editAcceptsTextInput() const
|
||||
{
|
||||
return edit_target_ == EditTarget::Message ||
|
||||
edit_target_ == EditTarget::MeshCoreChannelName ||
|
||||
editUsesKeyCharset();
|
||||
}
|
||||
|
||||
bool Runtime::usesSmartCompose() const
|
||||
{
|
||||
return edit_target_ == EditTarget::Message && !usesPhysicalTextInput();
|
||||
return editAcceptsTextInput() && !usesPhysicalTextInput();
|
||||
}
|
||||
|
||||
bool Runtime::usesPhysicalTextInput() const
|
||||
{
|
||||
return host_.physical_text_input && edit_target_ == EditTarget::Message;
|
||||
return host_.physical_text_input && editAcceptsTextInput();
|
||||
}
|
||||
|
||||
bool Runtime::usesFullVirtualKeyboard() const
|
||||
|
||||
Reference in new issue
Block a user