Add Fastboot/EDL reboot modes and SIM contact import/export

This commit is contained in:
Luxferre
2026-10-25 07:56:44 +02:00
parent a1e4f6ef8c
commit a117e74452
17 changed files with 864 additions and 37 deletions
+325 -25
View File
@@ -25,6 +25,13 @@ enum class ViewMode {
Edit = 3,
PickRecent = 4,
ConfirmDelete = 5,
SelectSim = 6,
};
enum class SimOp {
ImportAll = 0,
ExportAll = 1,
ExportSingle = 2,
};
enum class TextCaseMode {
@@ -153,12 +160,19 @@ int runContactsApp(int argc, char** argv) {
ViewMode mode = ViewMode::List;
ViewMode prevModeBeforeEdit = ViewMode::List;
ViewMode prevModeBeforeSim = ViewMode::List;
SimOp pendingSimOp = SimOp::ImportAll;
std::string searchQuery;
int selected = 0;
int detailSelected = 0;
int optionSelected = 0;
int confirmSelected = 0;
int recentSelected = 0;
int simSelected = 0;
int activeGwSlot = 1;
std::string statusMsg;
bool statusIsError = false;
int64_t statusUntilMs = 0;
// Editor state
bool editIsNew = true;
@@ -360,44 +374,66 @@ int runContactsApp(int argc, char** argv) {
if (mode == ViewMode::Options) {
// Popup options menu overlay
const char* optLabels[6] = {
const char* optLabels[8] = {
"New Contact",
"Add from Recent",
"Import from SIM",
"Export to SIM",
"Call Contact",
"Send SMS",
"Edit Contact",
"Delete Contact"
};
const char* optIcons[6] = {
const char* optIcons[8] = {
kIconPlus,
kIconClock,
kIconSim,
kIconSim,
kIconPhone,
kIconSms,
kIconEdit,
kIconTrash
};
int optCount = filtered.empty() ? 2 : 6;
int optCount = filtered.empty() ? (book.savedContacts().empty() ? 3 : 4) : 8;
constexpr int kMaxOptVis = 6;
int visCount = std::min(optCount, kMaxOptVis);
int optScroll = 0;
if (optionSelected >= visCount) {
optScroll = optionSelected - visCount + 1;
}
if (optScroll > std::max(0, optCount - visCount)) {
optScroll = std::max(0, optCount - visCount);
}
int menuRowH = 34;
int menuH = optCount * menuRowH + 4;
int menuH = visCount * menuRowH + 4;
int menuY = kWinHeight - kSoftbarH - menuH;
XSetForeground(dpy, gc, theme.spec.bgFooter);
XFillRectangle(dpy, pix, gc, 8, menuY, kWinWidth - 16, menuH);
XSetForeground(dpy, gc, theme.spec.selBg);
XDrawRectangle(dpy, pix, gc, 8, menuY, kWinWidth - 17, menuH - 1);
for (int i = 0; i < optCount; ++i) {
for (int i = 0; i < visCount; ++i) {
int idx = optScroll + i;
if (idx >= optCount) break;
int ry = menuY + 2 + i * menuRowH;
bool isSel = (i == optionSelected);
bool isSel = (idx == optionSelected);
if (isSel) {
XSetForeground(dpy, gc, theme.spec.selBg);
XFillRectangle(dpy, pix, gc, 10, ry, kWinWidth - 20, menuRowH);
XSetForeground(dpy, gc, theme.spec.accent);
XFillRectangle(dpy, pix, gc, 10, ry, 4, menuRowH);
}
XftColor* icCol = (i == 5) ? &colRed : (isSel ? &colYellow : &colGreen);
drawPango(layoutIcon, icCol, 18, ry, menuRowH, optIcons[i]);
XftColor* icCol = (idx == 7) ? &colRed : (isSel ? &colYellow : &colGreen);
drawPango(layoutIcon, icCol, 18, ry, menuRowH, optIcons[idx]);
drawPango(layoutMain, isSel ? &colWhite : &colLight, 42, ry, menuRowH,
optLabels[i], 180);
optLabels[idx], 180);
}
if (optCount > visCount) {
int sbH = std::max(14, ((menuH - 4) * visCount) / optCount);
int sbY = menuY + 2 +
(((menuH - 4) - sbH) * optScroll) / (optCount - visCount);
XSetForeground(dpy, gc, theme.spec.divider);
XFillRectangle(dpy, pix, gc, kWinWidth - 12, sbY, 2, sbH);
}
drawSoftbar("Select", "OK", "Back", &colYellow);
} else {
@@ -421,23 +457,25 @@ int runContactsApp(int argc, char** argv) {
drawPango(layoutMain, &colWhite, 34, 4, 26, c.name, 198);
drawPango(layoutSmall, &colYellow, 34, 30, 20, c.number, 198);
const char* actLabels[6] = {
const char* actLabels[7] = {
"Call Contact",
"Send SMS",
"Read Messages",
"Open in Dialer",
"Copy to SIM",
"Edit Contact",
"Delete Contact"
};
const char* actIcons[6] = {
const char* actIcons[7] = {
kIconPhone,
kIconSms,
kIconList,
kIconPhoneLog,
kIconSim,
kIconEdit,
kIconTrash
};
constexpr int kActCount = 6;
constexpr int kActCount = 7;
constexpr int kActVis = 5;
int actListY = kCardH;
int actListH = kWinHeight - kCardH - kSoftbarH;
@@ -462,10 +500,10 @@ int runContactsApp(int argc, char** argv) {
XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH);
}
XftColor* icCol =
(idx == 5) ? &colRed : (isSel ? &colYellow : &colGreen);
(idx == 6) ? &colRed : (isSel ? &colYellow : &colGreen);
drawPango(layoutIcon, icCol, 10, ry, rowH, actIcons[idx]);
drawPango(layoutMain,
(idx == 5 && !isSel) ? &colRed : (isSel ? &colWhite : &colLight),
(idx == 6 && !isSel) ? &colRed : (isSel ? &colWhite : &colLight),
34, ry, rowH, actLabels[idx], 196);
}
@@ -608,6 +646,57 @@ int runContactsApp(int argc, char** argv) {
}
drawSoftbar("Delete", "Select", "Cancel", &colRed);
} else if (mode == ViewMode::SelectSim) {
XSetForeground(dpy, gc, theme.spec.bgHeader);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH);
XSetForeground(dpy, gc, theme.spec.border);
XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1);
drawPango(layoutIcon, &colYellow, 8, 0, kHeaderH, kIconSim);
const char* simTitle =
(pendingSimOp == SimOp::ImportAll)
? "Import from SIM"
: ((pendingSimOp == SimOp::ExportSingle) ? "Copy to SIM"
: "Export to SIM");
drawPango(layoutMain, &colWhite, 32, 0, kHeaderH, simTitle, 200);
drawPango(layoutSmall, &colMuted, 12, 48, 22, "Select SIM card slot:",
216);
for (int i = 0; i < 2; ++i) {
int slotNum = i + 1;
int ry = 78 + i * 54;
bool isSel = (i == simSelected);
XSetForeground(dpy, gc, isSel ? theme.spec.selBg : theme.spec.bgButton);
XFillRectangle(dpy, pix, gc, 8, ry, kWinWidth - 16, 46);
if (isSel) {
XSetForeground(dpy, gc, theme.spec.accent);
XFillRectangle(dpy, pix, gc, 8, ry, 4, 46);
}
drawPango(layoutIcon, isSel ? &colYellow : &colGreen, 16, ry, 46,
kIconSim);
std::string label =
std::string("SIM ") + std::to_string(slotNum) +
((activeGwSlot == slotNum) ? " [Active]" : "");
drawPango(layoutMain, isSel ? &colWhite : &colLight, 42, ry, 46, label,
182);
}
drawSoftbar("Select", "OK", "Cancel", &colGreen);
}
if (!statusMsg.empty() &&
(mode == ViewMode::List || mode == ViewMode::Detail)) {
int bannerH = 26;
int bannerY = kWinHeight - kSoftbarH - bannerH;
XSetForeground(dpy, gc, theme.spec.bgFooter);
XFillRectangle(dpy, pix, gc, 0, bannerY, kWinWidth, bannerH);
XSetForeground(dpy, gc,
statusIsError ? colRed.pixel : theme.spec.accent);
XFillRectangle(dpy, pix, gc, 0, bannerY, kWinWidth, 1);
drawPango(layoutIcon, statusIsError ? &colRed : &colYellow, 8, bannerY,
bannerH, kIconSim);
drawPango(layoutSmall, statusIsError ? &colRed : &colWhite, 30, bannerY,
bannerH, statusMsg, 204);
}
if (visible) {
@@ -620,7 +709,7 @@ int runContactsApp(int argc, char** argv) {
drawPango(layoutIcon, &colGreen, 0, 0, 20,
std::string(kIconBook) + kIconUser + kIconSearch + kIconPlus +
kIconEdit + kIconTrash + kIconSms + kIconList + kIconCheck +
kIconPhone + kIconClock);
kIconPhone + kIconClock + kIconSim);
render();
XSync(dpy, False);
@@ -810,6 +899,159 @@ int runContactsApp(int argc, char** argv) {
lastTapMs = now;
};
auto setStatus = [&](const std::string& msg, bool isErr, int durationMs = 3500) {
statusMsg = msg;
statusIsError = isErr;
statusUntilMs = (durationMs > 0) ? (nowMs() + durationMs) : 0;
};
auto performSimOperation = [&](int slotNum) {
mode = (pendingSimOp == SimOp::ExportSingle) ? ViewMode::Detail
: ViewMode::List;
if (pendingSimOp == SimOp::ImportAll) {
setStatus("Reading SIM " + std::to_string(slotNum) + " contacts...",
false, 0);
render();
int rc = 0;
std::string out = runCaptureCmd(
"doas /usr/libexec/msm-modem-uim-selection sim-contacts-import " +
std::to_string(slotNum) + " 2>&1",
&rc);
if (out.rfind("ERROR:", 0) == 0) {
std::string err = trimStr(out.substr(6));
setStatus(err.empty() ? "SIM import failed" : err, true);
render();
return;
}
std::vector<std::pair<std::string, std::string>> rows;
std::stringstream ss(out);
std::string line;
while (std::getline(ss, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
size_t tab = line.find('\t');
if (tab == std::string::npos) continue;
std::string num = trimStr(line.substr(0, tab));
std::string name = trimStr(line.substr(tab + 1));
if (!num.empty()) {
rows.push_back({num, name.empty() ? num : name});
}
}
if (rows.empty()) {
setStatus("No contacts on SIM " + std::to_string(slotNum), false);
} else {
int imported = book.importContactsBatch(rows);
contactsStamp.checkAndUpdate(getContactsFilePath());
updateFilter();
if (imported < 0) {
setStatus("Failed to save imported contacts", true);
} else {
setStatus("Imported " + std::to_string(imported) + " from SIM " +
std::to_string(slotNum),
false);
}
}
render();
} else {
std::string srcFile = getContactsFilePath();
std::string tmpSingle;
if (pendingSimOp == SimOp::ExportSingle) {
if (selected < 0 || selected >= static_cast<int>(filtered.size())) {
return;
}
tmpSingle = "/tmp/sim_single_" + std::to_string(::getpid()) + ".tsv";
std::ofstream tf(tmpSingle, std::ios::trunc);
tf << filtered[selected].number << "\t" << filtered[selected].name
<< "\n";
tf.close();
srcFile = tmpSingle;
} else if (book.savedContacts().empty()) {
setStatus("Phonebook is empty", true);
render();
return;
}
setStatus("Writing to SIM " + std::to_string(slotNum) + "...", false, 0);
render();
int rc = 0;
std::string out = runCaptureCmd(
"doas /usr/libexec/msm-modem-uim-selection sim-contacts-export " +
std::to_string(slotNum) + " " + shellQuote(srcFile) + " 2>&1",
&rc);
if (!tmpSingle.empty()) {
::unlink(tmpSingle.c_str());
}
if (out.rfind("ERROR:", 0) == 0) {
std::string err = trimStr(out.substr(6));
setStatus(err.empty() ? "SIM export failed" : err, true);
render();
return;
}
int exported = 0;
std::stringstream ss(out);
std::string line;
while (std::getline(ss, line)) {
if (line.rfind("EXPORTED=", 0) == 0) {
exported = std::atoi(line.c_str() + 9);
}
}
if (exported > 0) {
if (pendingSimOp == SimOp::ExportSingle) {
setStatus("Copied contact to SIM " + std::to_string(slotNum), false);
} else {
setStatus("Exported " + std::to_string(exported) + " to SIM " +
std::to_string(slotNum),
false);
}
} else {
setStatus("SIM " + std::to_string(slotNum) + " write failed", true);
}
render();
}
};
auto beginSimOperation = [&](SimOp op) {
pendingSimOp = op;
prevModeBeforeSim =
(op == SimOp::ExportSingle) ? ViewMode::Detail : ViewMode::List;
std::string st = runCaptureCmd(
"doas /usr/libexec/msm-modem-uim-selection status 2>/dev/null");
bool s1Present = false;
bool s2Present = false;
int gwSlot = 1;
std::stringstream ss(st);
std::string line;
while (std::getline(ss, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
if (line == "SLOT1_STATE=present") {
s1Present = true;
} else if (line == "SLOT2_STATE=present") {
s2Present = true;
} else if (line.rfind("PRIMARY_GW_SLOT=", 0) == 0) {
int v = std::atoi(line.c_str() + 16);
if (v == 1 || v == 2) {
gwSlot = v;
}
}
}
activeGwSlot = gwSlot;
if (s1Present && s2Present) {
simSelected = (gwSlot == 2) ? 1 : 0;
mode = ViewMode::SelectSim;
render();
} else if (s1Present) {
performSimOperation(1);
} else if (s2Present) {
performSimOperation(2);
} else {
mode = prevModeBeforeSim;
setStatus("No SIM card detected", true);
render();
}
};
auto executeDetailAction = [&](int actIdx) {
if (selected < 0 || selected >= static_cast<int>(filtered.size())) {
return;
@@ -842,11 +1084,14 @@ int runContactsApp(int argc, char** argv) {
runAsyncArgv("xdialer", c.number.c_str());
}
break;
case 4: // Edit Contact
case 4: // Copy to SIM
beginSimOperation(SimOp::ExportSingle);
break;
case 5: // Edit Contact
beginEditContact(c);
render();
break;
case 5: // Delete Contact
case 6: // Delete Contact
confirmSelected = 0;
mode = ViewMode::ConfirmDelete;
render();
@@ -865,6 +1110,10 @@ int runContactsApp(int argc, char** argv) {
beginNewContact("");
mode = ViewMode::PickRecent;
render();
} else if (optIdx == 2) {
beginSimOperation(SimOp::ImportAll);
} else if (optIdx == 3) {
beginSimOperation(SimOp::ExportAll);
}
return;
}
@@ -880,18 +1129,24 @@ int runContactsApp(int argc, char** argv) {
mode = ViewMode::PickRecent;
render();
break;
case 2: // Call
case 2: // Import from SIM
beginSimOperation(SimOp::ImportAll);
break;
case 3: // Export to SIM
beginSimOperation(SimOp::ExportAll);
break;
case 4: // Call
executeDetailAction(0);
break;
case 3: // Send SMS
case 5: // Send SMS
executeDetailAction(1);
break;
case 4: // Edit
executeDetailAction(4);
break;
case 5: // Delete
case 6: // Edit
executeDetailAction(5);
break;
case 7: // Delete
executeDetailAction(6);
break;
}
};
@@ -1072,7 +1327,8 @@ int runContactsApp(int argc, char** argv) {
break;
}
} else if (mode == ViewMode::Options) {
int optCount = filtered.empty() ? 2 : 6;
int optCount =
filtered.empty() ? (book.savedContacts().empty() ? 3 : 4) : 8;
switch (sym) {
case XK_Up:
case XK_KP_Up:
@@ -1098,7 +1354,7 @@ int runContactsApp(int argc, char** argv) {
break;
}
} else if (mode == ViewMode::Detail) {
constexpr int kActCount = 6;
constexpr int kActCount = 7;
switch (sym) {
case XK_Up:
case XK_KP_Up:
@@ -1317,10 +1573,49 @@ int runContactsApp(int argc, char** argv) {
default:
break;
}
} else if (mode == ViewMode::SelectSim) {
switch (sym) {
case XK_Up:
case XK_KP_Up:
case XK_Down:
case XK_KP_Down:
simSelected = 1 - simSelected;
render();
break;
case XK_1:
case XK_KP_1:
simSelected = 0;
performSimOperation(1);
break;
case XK_2:
case XK_KP_2:
simSelected = 1;
performSimOperation(2);
break;
case XK_Return:
case XK_KP_Enter:
performSimOperation(simSelected + 1);
break;
case XK_BackSpace:
case XK_Left:
case XK_KP_Left:
mode = prevModeBeforeSim;
render();
break;
default:
break;
}
}
}
}
if (visible && !statusMsg.empty() && statusUntilMs > 0 &&
nowMs() >= statusUntilMs) {
statusMsg.clear();
statusUntilMs = 0;
render();
}
if (visible && softkeyPending != 0 && nowMs() >= softkeyDeadlineMs) {
int comb = softkeyPending | querySoftkeysDown(dpy);
softkeyPending = 0;
@@ -1366,6 +1661,8 @@ int runContactsApp(int argc, char** argv) {
}
mode = ViewMode::List;
render();
} else if (mode == ViewMode::SelectSim) {
performSimOperation(simSelected + 1);
}
} else if (comb == 2) {
// Right softkey
@@ -1404,6 +1701,9 @@ int runContactsApp(int argc, char** argv) {
} else if (mode == ViewMode::ConfirmDelete) {
mode = ViewMode::Detail;
render();
} else if (mode == ViewMode::SelectSim) {
mode = prevModeBeforeSim;
render();
}
}
}
+72
View File
@@ -809,6 +809,78 @@ class ContactBook {
return true;
}
int importContactsBatch(
const std::vector<std::pair<std::string, std::string>>& items) {
if (items.empty()) {
return 0;
}
std::string path = getContactsFilePath();
ensureParentDir(path);
std::vector<std::pair<std::string, std::string>> rows;
{
std::ifstream in(path);
std::string line;
while (std::getline(in, line)) {
if (line.empty()) continue;
size_t tab = line.find('\t');
if (tab == std::string::npos) continue;
std::string num = trimStr(line.substr(0, tab));
std::string name = trimStr(line.substr(tab + 1));
if (!num.empty() && !name.empty()) {
rows.push_back({num, name});
}
}
}
int count = 0;
for (const auto& item : items) {
std::string newNumber = trimStr(item.first);
std::string newName = trimStr(item.second);
for (char& ch : newName) {
if (ch == '\t' || ch == '\r' || ch == '\n') {
ch = ' ';
}
}
std::string newNorm = normalizePhone(newNumber);
if (newNorm.empty()) {
continue;
}
if (newName.empty()) {
newName = newNumber;
}
bool replaced = false;
for (auto& r : rows) {
if (r.first == newNumber ||
(!newNorm.empty() && normalizePhone(r.first) == newNorm)) {
r.first = newNumber;
r.second = newName;
replaced = true;
break;
}
}
if (!replaced) {
rows.push_back({newNumber, newName});
}
++count;
}
if (count <= 0) {
return 0;
}
std::ofstream out(path, std::ios::trunc);
if (!out.is_open()) {
return -1;
}
for (const auto& r : rows) {
out << r.first << "\t" << r.second << "\n";
}
out.close();
load();
return count;
}
bool deleteContact(const std::string& number, const std::string& name) {
std::string path = getContactsFilePath();
std::vector<std::pair<std::string, std::string>> rows;