mirror of
https://github.com/pelgraine/Meck.git
synced 2026-06-27 05:11:13 +02:00
ed039aa711
The "SMS Inbox" badge on the Phone & SMS landing screen previously showed the number of conversations regardless of read state. It now shows the number of unread received messages and disappears when there are none. Read state is tracked persistently, so it survives reboots and modem power-cycles (the flag lives in each record on the SD card). SMSStore: - Add a `read` byte to SMSRecord, reusing one of the reserved bytes so the record stays 256 bytes and every existing field offset is unchanged (existing .sms files remain readable). - saveMessage marks sent messages read and received messages unread. - loadConversations scans each file and reports unreadCount (received messages with read=0); preview / last-message behaviour is unchanged. - markConversationRead / markFileRead flip received-unread records to read in place (open "r+", rewrite only the affected records). - begin() runs a one-time migration on first boot that marks all pre-existing history read, gated by a marker file (/sms/rdmig.dat), so old messages do not all appear as unread after the update. SMSScreen: - Landing-screen badge sums unreadCount across conversations, hidden at zero. - Opening a conversation marks it read and clears its in-RAM count. - A message arriving while its conversation is open is marked read. - Inbox reloads on the landing screen as well as the inbox view, so the badge updates live when a message arrives.
274 lines
7.3 KiB
C++
274 lines
7.3 KiB
C++
#ifdef HAS_4G_MODEM
|
|
|
|
#include "SMSStore.h"
|
|
#include <Mesh.h> // For MESH_DEBUG_PRINTLN
|
|
#include "target.h" // For SDCARD_CS macro
|
|
|
|
// Global singleton
|
|
SMSStore smsStore;
|
|
|
|
void SMSStore::begin() {
|
|
// Ensure SMS directory exists
|
|
if (!SD.exists(SMS_DIR)) {
|
|
SD.mkdir(SMS_DIR);
|
|
MESH_DEBUG_PRINTLN("[SMSStore] created %s", SMS_DIR);
|
|
}
|
|
_ready = true;
|
|
|
|
// One-time migration: history saved before read-tracking existed has read=0,
|
|
// which would otherwise all show as unread. Mark it read once, gated by a
|
|
// marker file so genuine unread state survives later reboots.
|
|
if (!SD.exists(SMS_READ_MIGRATED)) {
|
|
migrateExistingAsRead();
|
|
File m = SD.open(SMS_READ_MIGRATED, FILE_WRITE);
|
|
if (m) m.close();
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
}
|
|
|
|
MESH_DEBUG_PRINTLN("[SMSStore] ready");
|
|
}
|
|
|
|
void SMSStore::phoneToFilename(const char* phone, char* out, size_t outLen) {
|
|
// Convert phone number to safe filename: strip non-alphanumeric, prefix with dir
|
|
// e.g. "+1234567890" -> "/sms/p1234567890.sms"
|
|
char safe[SMS_PHONE_LEN];
|
|
int j = 0;
|
|
for (int i = 0; phone[i] && j < SMS_PHONE_LEN - 1; i++) {
|
|
char c = phone[i];
|
|
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
|
|
safe[j++] = c;
|
|
}
|
|
}
|
|
safe[j] = '\0';
|
|
snprintf(out, outLen, "%s/p%s.sms", SMS_DIR, safe);
|
|
}
|
|
|
|
bool SMSStore::saveMessage(const char* phone, const char* body, bool isSent, uint32_t timestamp) {
|
|
if (!_ready) return false;
|
|
|
|
char filepath[64];
|
|
phoneToFilename(phone, filepath, sizeof(filepath));
|
|
|
|
// Build record
|
|
SMSRecord rec;
|
|
memset(&rec, 0, sizeof(rec));
|
|
rec.timestamp = timestamp;
|
|
rec.isSent = isSent ? 1 : 0;
|
|
rec.read = isSent ? 1 : 0; // sent messages are never unread; received start unread
|
|
rec.bodyLen = strlen(body);
|
|
if (rec.bodyLen >= SMS_BODY_LEN) rec.bodyLen = SMS_BODY_LEN - 1;
|
|
strncpy(rec.phone, phone, SMS_PHONE_LEN - 1);
|
|
strncpy(rec.body, body, SMS_BODY_LEN - 1);
|
|
|
|
// Append to file
|
|
File f = SD.open(filepath, FILE_APPEND);
|
|
if (!f) {
|
|
// Try creating
|
|
f = SD.open(filepath, FILE_WRITE);
|
|
if (!f) {
|
|
MESH_DEBUG_PRINTLN("[SMSStore] can't open %s", filepath);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
size_t written = f.write((uint8_t*)&rec, sizeof(rec));
|
|
f.close();
|
|
|
|
// Release SD CS
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
|
|
return written == sizeof(rec);
|
|
}
|
|
|
|
int SMSStore::loadConversations(SMSConversation* out, int maxCount) {
|
|
if (!_ready) return 0;
|
|
|
|
File dir = SD.open(SMS_DIR);
|
|
if (!dir || !dir.isDirectory()) return 0;
|
|
|
|
int count = 0;
|
|
File entry;
|
|
while ((entry = dir.openNextFile()) && count < maxCount) {
|
|
const char* name = entry.name();
|
|
// Only process .sms files
|
|
if (!strstr(name, ".sms")) { entry.close(); continue; }
|
|
|
|
size_t fileSize = entry.size();
|
|
if (fileSize < sizeof(SMSRecord)) { entry.close(); continue; }
|
|
|
|
int numRecords = fileSize / sizeof(SMSRecord);
|
|
|
|
// Scan all records: count unread received messages, keep the last for preview
|
|
SMSRecord rec;
|
|
SMSRecord lastRec;
|
|
memset(&lastRec, 0, sizeof(lastRec));
|
|
int unread = 0;
|
|
bool haveLast = false;
|
|
for (int i = 0; i < numRecords; i++) {
|
|
entry.seek((size_t)i * sizeof(SMSRecord));
|
|
if (entry.read((uint8_t*)&rec, sizeof(SMSRecord)) != sizeof(SMSRecord)) continue;
|
|
if (rec.isSent == 0 && rec.read == 0) unread++;
|
|
lastRec = rec;
|
|
haveLast = true;
|
|
}
|
|
if (!haveLast) { entry.close(); continue; }
|
|
|
|
SMSConversation& conv = out[count];
|
|
memset(&conv, 0, sizeof(SMSConversation));
|
|
strncpy(conv.phone, lastRec.phone, SMS_PHONE_LEN - 1);
|
|
strncpy(conv.preview, lastRec.body, 39);
|
|
conv.preview[39] = '\0';
|
|
conv.lastTimestamp = lastRec.timestamp;
|
|
conv.messageCount = numRecords;
|
|
conv.unreadCount = unread;
|
|
conv.valid = true;
|
|
|
|
count++;
|
|
entry.close();
|
|
}
|
|
dir.close();
|
|
|
|
// Release SD CS
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
|
|
// Sort by most recent (simple bubble sort, small N)
|
|
for (int i = 0; i < count - 1; i++) {
|
|
for (int j = 0; j < count - 1 - i; j++) {
|
|
if (out[j].lastTimestamp < out[j + 1].lastTimestamp) {
|
|
SMSConversation tmp = out[j];
|
|
out[j] = out[j + 1];
|
|
out[j + 1] = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
int SMSStore::loadMessages(const char* phone, SMSMessage* out, int maxCount) {
|
|
if (!_ready) return 0;
|
|
|
|
char filepath[64];
|
|
phoneToFilename(phone, filepath, sizeof(filepath));
|
|
|
|
File f = SD.open(filepath, FILE_READ);
|
|
if (!f) return 0;
|
|
|
|
size_t fileSize = f.size();
|
|
int numRecords = fileSize / sizeof(SMSRecord);
|
|
|
|
// Load from end of file (most recent N messages), in chronological order
|
|
int startIdx = numRecords > maxCount ? numRecords - maxCount : 0;
|
|
|
|
// Read chronologically (oldest first) for chat-style display
|
|
SMSRecord rec;
|
|
int outIdx = 0;
|
|
for (int i = startIdx; i < numRecords && outIdx < maxCount; i++) {
|
|
f.seek(i * sizeof(SMSRecord));
|
|
if (f.read((uint8_t*)&rec, sizeof(SMSRecord)) != sizeof(SMSRecord)) continue;
|
|
|
|
out[outIdx].timestamp = rec.timestamp;
|
|
out[outIdx].isSent = rec.isSent != 0;
|
|
out[outIdx].valid = true;
|
|
strncpy(out[outIdx].phone, rec.phone, SMS_PHONE_LEN - 1);
|
|
strncpy(out[outIdx].body, rec.body, SMS_BODY_LEN - 1);
|
|
outIdx++;
|
|
}
|
|
|
|
f.close();
|
|
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
|
|
return outIdx;
|
|
}
|
|
|
|
bool SMSStore::deleteConversation(const char* phone) {
|
|
if (!_ready) return false;
|
|
|
|
char filepath[64];
|
|
phoneToFilename(phone, filepath, sizeof(filepath));
|
|
|
|
bool ok = SD.remove(filepath);
|
|
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
|
|
return ok;
|
|
}
|
|
|
|
int SMSStore::getMessageCount(const char* phone) {
|
|
if (!_ready) return 0;
|
|
|
|
char filepath[64];
|
|
phoneToFilename(phone, filepath, sizeof(filepath));
|
|
|
|
File f = SD.open(filepath, FILE_READ);
|
|
if (!f) return 0;
|
|
|
|
int count = f.size() / sizeof(SMSRecord);
|
|
f.close();
|
|
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
|
|
return count;
|
|
}
|
|
|
|
void SMSStore::markFileRead(const char* filepath) {
|
|
// In-place flag update: open read+write without truncating ("r+").
|
|
// Caller releases SDCARD_CS afterwards.
|
|
File f = SD.open(filepath, "r+");
|
|
if (!f) return;
|
|
|
|
size_t fileSize = f.size();
|
|
int numRecords = fileSize / sizeof(SMSRecord);
|
|
|
|
SMSRecord rec;
|
|
for (int i = 0; i < numRecords; i++) {
|
|
f.seek((size_t)i * sizeof(SMSRecord));
|
|
if (f.read((uint8_t*)&rec, sizeof(SMSRecord)) != sizeof(SMSRecord)) continue;
|
|
if (rec.isSent == 0 && rec.read == 0) {
|
|
rec.read = 1;
|
|
f.seek((size_t)i * sizeof(SMSRecord));
|
|
f.write((uint8_t*)&rec, sizeof(SMSRecord));
|
|
}
|
|
}
|
|
|
|
f.close();
|
|
}
|
|
|
|
void SMSStore::markConversationRead(const char* phone) {
|
|
if (!_ready) return;
|
|
|
|
char filepath[64];
|
|
phoneToFilename(phone, filepath, sizeof(filepath));
|
|
markFileRead(filepath);
|
|
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
}
|
|
|
|
void SMSStore::migrateExistingAsRead() {
|
|
File dir = SD.open(SMS_DIR);
|
|
if (!dir || !dir.isDirectory()) return;
|
|
|
|
File entry;
|
|
while ((entry = dir.openNextFile())) {
|
|
const char* name = entry.name();
|
|
if (!strstr(name, ".sms")) { entry.close(); continue; }
|
|
|
|
// name() may be a bare basename or a full path depending on core version
|
|
char fullpath[64];
|
|
if (name[0] == '/') {
|
|
strncpy(fullpath, name, sizeof(fullpath) - 1);
|
|
fullpath[sizeof(fullpath) - 1] = '\0';
|
|
} else {
|
|
snprintf(fullpath, sizeof(fullpath), "%s/%s", SMS_DIR, name);
|
|
}
|
|
entry.close();
|
|
|
|
markFileRead(fullpath);
|
|
}
|
|
dir.close();
|
|
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
}
|
|
|
|
#endif // HAS_4G_MODEM
|