diff --git a/overlay/rootfs/usr/lib/dwm/plugins/contacts.so b/overlay/rootfs/usr/lib/dwm/plugins/contacts.so index bf638a5..5d53cc7 100755 Binary files a/overlay/rootfs/usr/lib/dwm/plugins/contacts.so and b/overlay/rootfs/usr/lib/dwm/plugins/contacts.so differ diff --git a/overlay/rootfs/usr/libexec/msm-modem-uim-selection b/overlay/rootfs/usr/libexec/msm-modem-uim-selection index 8f9cdfc..fc9f43a 100755 --- a/overlay/rootfs/usr/libexec/msm-modem-uim-selection +++ b/overlay/rootfs/usr/libexec/msm-modem-uim-selection @@ -77,6 +77,9 @@ detect_qmicli_modem() { if timeout 3 qmicli --silent -pd qrtr://0 --uim-noop >/dev/null 2>&1; then QMICLI_MODEM="timeout 4 qmicli --silent -pd qrtr://0" return 0 + elif timeout 3 qmicli --silent -d qrtr://0 --uim-noop >/dev/null 2>&1; then + QMICLI_MODEM="timeout 4 qmicli --silent -d qrtr://0" + return 0 elif [ -e "/dev/modem" ]; then if timeout 3 qmicli --silent -pd /dev/modem --uim-noop >/dev/null 2>&1; then QMICLI_MODEM="timeout 4 qmicli --silent -pd /dev/modem" @@ -1181,6 +1184,151 @@ case "$cmd" in fi fi + tmp_contacts="/tmp/sim_import_$$.tsv" + rm -f "$tmp_contacts" + : > "$tmp_contacts" + + # Method 1: Fast direct QRTR QMI UIM EF_ADN read (supports SIM 1 & SIM 2 without switching GW session) + if command -v python3 >/dev/null 2>&1; then + python3 - "$target_slot" > "$tmp_contacts" 2>/dev/null << 'PY' || true +import socket, struct, select, sys + +slot = int(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].isdigit() else 1 +AF_QIPCRTR = 42 +QRTR_PORT_CTRL = 0xfffffffe +QRTR_TYPE_NEW_SERVER = 4 +QRTR_TYPE_NEW_LOOKUP = 10 +QMI_SERVICE_UIM = 11 + +ls = socket.socket(AF_QIPCRTR, socket.SOCK_DGRAM) +node, _ = ls.getsockname() +ls.sendto(struct.pack("= 20: + cmd, srv, inst, srv_node, srv_port = struct.unpack("= 4: + res, err = struct.unpack("= 9: + _, _, _, rs, rc = struct.unpack("= 14 and rc >= 1: + session_type = stype + tlv_f = tf + rec_size = rs + rec_count = min(rc, 500) + break + if tlv_f: + break + +if not tlv_f: + s.close() + sys.exit(1) + +tlv_session = struct.pack("= 4: + res, err = struct.unpack("= 2: + clen = struct.unpack("= 14): + continue + alpha_len = len(raw) - 14 + alpha = raw[:alpha_len] + bcd_len = raw[alpha_len] + ton_npi = raw[alpha_len + 1] + if bcd_len == 0 or bcd_len > 11 or bcd_len == 0xFF: + continue + num = "+" if ((ton_npi >> 4) & 0x07) == 1 else "" + for b in raw[alpha_len + 2 : alpha_len + 2 + min(10, bcd_len - 1)]: + if b == 0xFF: break + lo = b & 0x0F + hi = (b >> 4) & 0x0F + if lo < len(bcd_map): num += bcd_map[lo] + else: break + if hi == 0x0F: break + if hi < len(bcd_map): num += bcd_map[hi] + else: break + if not num or num == "+": + continue + while alpha and alpha[-1] == 0xFF: + alpha = alpha[:-1] + if not alpha: + name = num + elif alpha[0] == 0x80: + u16 = alpha[1:] + if len(u16) % 2 == 1: u16 = u16[:-1] + name = u16.decode("utf-16-be", errors="replace").replace("\t", " ").replace("\n", " ").strip() + else: + name = "".join(chr(b) for b in alpha if 32 <= b < 127 and b != 9).strip() + print(f"{num}\t{name or num}") +s.close() +PY + fi + + if [ -s "$tmp_contacts" ]; then + cat "$tmp_contacts" + rm -f "$tmp_contacts" + exit 0 + fi + orig_gw_slot="$PRIMARY_GW_SLOT" orig_gw_aid="" if [ "$orig_gw_slot" = "1" ]; then @@ -1210,57 +1358,7 @@ case "$cmd" in fi fi - tmp_contacts="/tmp/sim_import_$$.tsv" - rm -f "$tmp_contacts" - : > "$tmp_contacts" - - # Method 1: Standard 3GPP AT+CPBS="SM" / AT+CPBR - at_info="$("$0" at auto 'AT+CMEE=2;+CSCS="GSM";+CPBS="SM";+CPBR=?' 2>/dev/null || true)" - max_idx="$(printf "%s\n" "$at_info" | sed -n 's/.*+CPBR:[[:space:]]*([0-9]*-\([0-9]*\)).*/\1/p' | head -n1)" - if [ -n "$max_idx" ] && [ "$max_idx" -gt 0 ] 2>/dev/null; then - if [ "$max_idx" -gt 500 ]; then - max_idx=500 - fi - start_idx=1 - while [ "$start_idx" -le "$max_idx" ]; do - end_idx=$((start_idx + 49)) - if [ "$end_idx" -gt "$max_idx" ]; then - end_idx="$max_idx" - fi - chunk="$("$0" at auto "AT+CPBR=${start_idx},${end_idx}" 2>/dev/null || true)" - if [ -n "$chunk" ]; then - printf "%s\n" "$chunk" | awk ' - /^\+CPBR:[[:space:]]*[0-9]+,/ { - line = $0 - sub(/^\+CPBR:[[:space:]]*[0-9]+,[[:space:]]*"/, "", line) - q1 = index(line, "\"") - if (q1 <= 0) next - num = substr(line, 1, q1 - 1) - rest = substr(line, q1 + 1) - sub(/^[[:space:]]*,[[:space:]]*/, "", rest) - c2 = index(rest, ",") - if (c2 <= 0) next - ton = substr(rest, 1, c2 - 1) + 0 - name = substr(rest, c2 + 1) - sub(/^[[:space:]]*"/, "", name) - sub(/"[[:space:]]*$/, "", name) - gsub(/[\t\r\n]/, " ", name) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", name) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", num) - if (num == "") next - if (ton == 145 && substr(num, 1, 1) != "+") { - num = "+" num - } - if (name == "") name = num - printf "%s\t%s\n", num, name - } - ' >> "$tmp_contacts" - fi - start_idx=$((end_idx + 1)) - done - fi - - # Method 2: Direct QMI UIM EF_ADN (3F00:7F10:6F3A) record fallback + # Method 2: Direct qmicli UIM EF_ADN (3F00-7F10-6F3A) record fallback if [ ! -s "$tmp_contacts" ]; then adn_attr="$($QMICLI_MODEM --uim-get-file-attributes=0x3F00,0x7F10,0x6F3A 2>/dev/null || true)" reclen="$(printf "%s\n" "$adn_attr" | awk '/Record (size|length):/ {gsub(/[^0-9]/, "", $NF); print $NF + 0; exit}')" @@ -1271,9 +1369,9 @@ case "$cmd" in fi rec=1 while [ "$rec" -le "$reccount" ]; do - raw_rec="$($QMICLI_MODEM --uim-read-record="file=0x3F00,0x7F10,0x6F3A,record-number=${rec},record-length=${reclen}" 2>/dev/null || true)" + raw_rec="$($QMICLI_MODEM --uim-read-record="file=0x3F00-0x7F10-0x6F3A,record-number=${rec},record-length=${reclen}" 2>/dev/null || true)" if [ -n "$raw_rec" ]; then - printf "%s\n" "$raw_rec" | awk -v rlen="$reclen" ' + printf "%s\n" "$raw_rec" | awk ' function hex2val(h, i, c, v, res) { res = 0 h = toupper(h) @@ -1291,15 +1389,21 @@ case "$cmd" in if (n == 11) return "#" return "" } - /^[[:space:]]*([0-9A-Fa-f]{2}:)+[0-9A-Fa-f]{2}[[:space:]]*$/ { + /Read result:/ { in_res = 1; full = ""; next } + in_res && /^[[:space:]]*[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2})*:?[[:space:]]*$/ { line = $0 gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) - n = split(line, hex, ":") - if (n < 14) next + sub(/:$/, "", line) + full = (full == "") ? line : (full ":" line) + } + END { + if (full == "") exit + n = split(full, hex, ":") + if (n < 14) exit alpha_len = n - 14 bcd_len = hex2val(hex[alpha_len + 1]) ton_npi = hex2val(hex[alpha_len + 2]) - if (bcd_len <= 0 || bcd_len > 11 || bcd_len == 255) next + if (bcd_len <= 0 || bcd_len > 11 || bcd_len == 255) exit num = "" if (int(ton_npi / 16) % 8 == 1) { num = "+" @@ -1318,7 +1422,7 @@ case "$cmd" in if (d2 == "") break num = num d2 } - if (num == "" || num == "+") next + if (num == "" || num == "+") exit name = "" for (i = 1; i <= alpha_len; i++) { b = hex2val(hex[i]) @@ -1396,64 +1500,234 @@ case "$cmd" in fi fi - orig_gw_slot="$PRIMARY_GW_SLOT" - orig_gw_aid="" - if [ "$orig_gw_slot" = "1" ]; then - orig_gw_aid="$SLOT1_AID" - elif [ "$orig_gw_slot" = "2" ]; then - orig_gw_aid="$SLOT2_AID" - fi - switched_gw=0 - restore_gw_slot_exp() { - if [ "$switched_gw" = "1" ]; then - $QMICLI_MODEM --uim-change-provisioning-session='activate=no,session-type=primary-gw-provisioning' >/dev/null 2>&1 || true - if [ -n "$orig_gw_slot" ] && [ "$orig_gw_slot" != "0" ] && [ -n "$orig_gw_aid" ]; then - $QMICLI_MODEM --uim-change-provisioning-session="slot=$orig_gw_slot,activate=yes,session-type=primary-gw-provisioning,aid=$orig_gw_aid" >/dev/null 2>&1 || true - fi - switched_gw=0 - fi - } - trap restore_gw_slot_exp EXIT INT TERM + if command -v python3 >/dev/null 2>&1; then + exp_out="$(python3 - "$target_slot" "$src_tsv" << 'PY' +import socket, struct, select, sys - if [ "$PRIMARY_GW_SLOT" != "$target_slot" ] && [ -n "$target_aid" ]; then - if [ "$PRIMARY_GW_SLOT" != "0" ]; then - $QMICLI_MODEM --uim-change-provisioning-session='activate=no,session-type=primary-gw-provisioning' >/dev/null 2>&1 || true +slot = int(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].isdigit() else 1 +tsv_path = sys.argv[2] + +rows = [] +with open(tsv_path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + line = line.rstrip("\r\n") + if "\t" not in line: continue + num, name = line.split("\t", 1) + num = "".join(c for c in num if c in "+0123456789*#") + name = name.strip() + if num: + rows.append((num, name or num)) + +if not rows: + print("ERROR: No contacts to export") + sys.exit(1) + +AF_QIPCRTR = 42 +QRTR_PORT_CTRL = 0xfffffffe +QRTR_TYPE_NEW_SERVER = 4 +QRTR_TYPE_NEW_LOOKUP = 10 +QMI_SERVICE_UIM = 11 + +ls = socket.socket(AF_QIPCRTR, socket.SOCK_DGRAM) +node, _ = ls.getsockname() +ls.sendto(struct.pack("= 20: + cmd, srv, inst, srv_node, srv_port = struct.unpack("= 4: + res, err = struct.unpack("= 9: + _, _, _, rs, rc = struct.unpack("= 14 and rc >= 1: + session_type = stype + tlv_f = tf + rec_size = rs + rec_count = min(rc, 500) + break + if tlv_f: + break + +if not tlv_f: + s.close() + print(f"ERROR: SIM {slot} phonebook not ready") + sys.exit(1) + +tlv_session = struct.pack("= 9 else norm + +for rnum in range(1, rec_count + 1): + tlv_rec = struct.pack("= 4: + res, err = struct.unpack("= 2: + clen = struct.unpack("= 14): + continue + alpha_len = len(raw) - 14 + bcd_len = raw[alpha_len] + ton_npi = raw[alpha_len + 1] + if bcd_len == 0 or bcd_len > 11 or bcd_len == 0xFF: + empty_slots.append(rnum) + continue + num = "+" if ((ton_npi >> 4) & 0x07) == 1 else "" + for b in raw[alpha_len + 2 : alpha_len + 2 + min(10, bcd_len - 1)]: + if b == 0xFF: break + lo = b & 0x0F + hi = (b >> 4) & 0x0F + if lo < len(bcd_map_str): num += bcd_map_str[lo] + else: break + if hi == 0x0F: break + if hi < len(bcd_map_str): num += bcd_map_str[hi] + else: break + if not num or num == "+": + empty_slots.append(rnum) + continue + existing_by_norm[num] = rnum + existing_by_sfx[sfx_key(num)] = rnum + +def encode_adn(name, num, rsize): + alpha_len = rsize - 14 + try: + nb = name.encode("ascii")[:alpha_len] + alpha = nb + b"\xFF" * (alpha_len - len(nb)) + except UnicodeEncodeError: + u16 = name.encode("utf-16-be")[:max(0, (alpha_len - 1) // 2 * 2)] + alpha = b"\x80" + u16 + b"\xFF" * (alpha_len - 1 - len(u16)) + ton_npi = 0x91 if num.startswith("+") else 0x81 + digits = [c for c in num if c != "+"][:20] + if not digits: return None + bcd = bytearray([0xFF] * 10) + bmap = {str(i): i for i in range(10)} + bmap["*"] = 0x0A + bmap["#"] = 0x0B + for idx, ch in enumerate(digits): + nib = bmap.get(ch, 0x0F) + bi = idx // 2 + if idx % 2 == 0: + bcd[bi] = (bcd[bi] & 0xF0) | nib + else: + bcd[bi] = (bcd[bi] & 0x0F) | (nib << 4) + bcd_len = 1 + (len(digits) + 1) // 2 + return alpha + bytes([bcd_len, ton_npi]) + bytes(bcd) + b"\xFF\xFF" + +exported = 0 +empty_idx = 0 +for num, name in rows: + trec = existing_by_norm.get(num) or existing_by_sfx.get(sfx_key(num)) + if not trec: + if empty_idx < len(empty_slots): + trec = empty_slots[empty_idx] + empty_idx += 1 + else: + break + rdata = encode_adn(name, num, rec_size) + if not rdata: continue + wval = struct.pack("= 4: + res, err = struct.unpack("= len(empty_slots): + print(f"ERROR: SIM {slot} phonebook is full") + sys.exit(1) +print(f"EXPORTED={exported}") +print(f"SLOT={slot}") +PY +)" + if printf "%s\n" "$exp_out" | grep -q "^EXPORTED="; then + printf "%s\n" "$exp_out" + exit 0 fi - if $QMICLI_MODEM --uim-change-provisioning-session="slot=$target_slot,activate=yes,session-type=primary-gw-provisioning,aid=$target_aid" >/dev/null 2>&1; then - switched_gw=1 - sleep 1 + if printf "%s\n" "$exp_out" | grep -q "^ERROR:"; then + printf "%s\n" "$exp_out" + exit 1 fi fi - at_info="$("$0" at auto 'AT+CMEE=2;+CSCS="GSM";+CPBS="SM";+CPBR=?' 2>/dev/null || true)" - if printf "%s\n" "$at_info" | grep -qE "ERROR"; then - echo "ERROR: SIM $target_slot phonebook not ready" - exit 1 - fi - tlen="$(printf "%s\n" "$at_info" | sed -n 's/.*+CPBR:[[:space:]]*([0-9]*-[0-9]*),[0-9]*,\([0-9]*\).*/\1/p' | head -n1)" - if [ -z "$tlen" ] || [ "$tlen" -le 0 ] 2>/dev/null; then - tlen=14 - fi - - exported=0 - while IFS="$(printf '\t')" read -r raw_num raw_name; do - num="$(printf "%s" "$raw_num" | tr -cd '+0-9*#')" - name="$(printf "%s" "$raw_name" | tr -d '"\r\n\t\\' | cut -c1-"$tlen")" - [ -n "$num" ] || continue - [ -n "$name" ] || name="$(printf "%s" "$num" | cut -c1-"$tlen")" - ton=129 - case "$num" in - +*) ton=145 ;; - esac - res="$("$0" at auto "AT+CPBW=,\"${num}\",${ton},\"${name}\"" 2>/dev/null || true)" - if printf "%s\n" "$res" | grep -q "OK"; then - exported=$((exported + 1)) - fi - done < "$src_tsv" - - echo "EXPORTED=${exported}" - echo "SLOT=${target_slot}" - exit 0 + echo "ERROR: SIM $target_slot phonebook not ready" + exit 1 ;; *) diff --git a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so index bf638a5..5d53cc7 100755 Binary files a/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so and b/prebuilt/sxmo-x11/usr/lib/dwm/plugins/contacts.so differ diff --git a/src/dwm-plugins/contacts_plugin.cpp b/src/dwm-plugins/contacts_plugin.cpp index 6a6f03c..dfe5ac7 100644 --- a/src/dwm-plugins/contacts_plugin.cpp +++ b/src/dwm-plugins/contacts_plugin.cpp @@ -908,34 +908,41 @@ int runContactsApp(int argc, char** argv) { setStatus("Reading SIM " + std::to_string(slotNum) + " contacts...", false, 0); render(); - int rc = 0; - std::string out = runCaptureCmd( - "doas -n /usr/libexec/msm-modem-uim-selection sim-contacts-import " + - std::to_string(slotNum) + " 2>&1", - &rc); - if (visible) { - XRaiseWindow(dpy, win); - grabPluginKeyboard(dpy, win); - } - 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> rows; - std::stringstream ss(out); - std::string line; - while (std::getline(ss, line)) { - if (!line.empty() && line.back() == '\r') { - line.pop_back(); + std::string qErr; + bool ok = qrtrReadSimContacts(slotNum, rows, qErr); + if (!ok) { + int rc = 0; + std::string out = runCaptureCmd( + "doas -n /usr/libexec/msm-modem-uim-selection sim-contacts-import " + + std::to_string(slotNum) + " 2>&1", + &rc); + if (visible) { + XRaiseWindow(dpy, win); + grabPluginKeyboard(dpy, win); } - 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 (out.rfind("ERROR:", 0) == 0) { + std::string err = trimStr(out.substr(6)); + setStatus(err.empty() ? (qErr.empty() ? "SIM import failed" : qErr) + : err, + true); + render(); + return; + } + 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()) { @@ -954,49 +961,65 @@ int runContactsApp(int argc, char** argv) { } render(); } else { - std::string srcFile = getContactsFilePath(); - std::string tmpSingle; + std::vector> toExport; if (pendingSimOp == SimOp::ExportSingle) { if (selected < 0 || selected >= static_cast(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; + toExport.push_back({filtered[selected].number, filtered[selected].name}); + } else { + if (book.savedContacts().empty()) { + setStatus("Phonebook is empty", true); + render(); + return; + } + for (const auto& c : book.savedContacts()) { + toExport.push_back({c.number, c.name}); + } } + setStatus("Writing to SIM " + std::to_string(slotNum) + "...", false, 0); render(); - int rc = 0; - std::string out = runCaptureCmd( - "doas -n /usr/libexec/msm-modem-uim-selection sim-contacts-export " + - std::to_string(slotNum) + " " + shellQuote(srcFile) + " 2>&1", - &rc); - if (visible) { - XRaiseWindow(dpy, win); - grabPluginKeyboard(dpy, win); - } - 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); + std::string qErr; + bool ok = qrtrWriteSimContacts(slotNum, toExport, exported, qErr); + if (!ok && exported <= 0) { + std::string srcFile = getContactsFilePath(); + std::string tmpSingle; + if (pendingSimOp == SimOp::ExportSingle) { + tmpSingle = "/tmp/sim_single_" + std::to_string(::getpid()) + ".tsv"; + std::ofstream tf(tmpSingle, std::ios::trunc); + tf << toExport[0].first << "\t" << toExport[0].second << "\n"; + tf.close(); + srcFile = tmpSingle; + } + int rc = 0; + std::string out = runCaptureCmd( + "doas -n /usr/libexec/msm-modem-uim-selection sim-contacts-export " + + std::to_string(slotNum) + " " + shellQuote(srcFile) + " 2>&1", + &rc); + if (visible) { + XRaiseWindow(dpy, win); + grabPluginKeyboard(dpy, win); + } + if (!tmpSingle.empty()) { + ::unlink(tmpSingle.c_str()); + } + if (out.rfind("ERROR:", 0) == 0) { + std::string err = trimStr(out.substr(6)); + setStatus(err.empty() ? (qErr.empty() ? "SIM export failed" : qErr) + : err, + true); + render(); + return; + } + 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) { @@ -1008,7 +1031,10 @@ int runContactsApp(int argc, char** argv) { false); } } else { - setStatus("SIM " + std::to_string(slotNum) + " write failed", true); + setStatus(qErr.empty() + ? ("SIM " + std::to_string(slotNum) + " write failed") + : qErr, + true); } render(); } diff --git a/src/dwm-plugins/phone_data.h b/src/dwm-plugins/phone_data.h index 37c9a88..f5fa076 100644 --- a/src/dwm-plugins/phone_data.h +++ b/src/dwm-plugins/phone_data.h @@ -1762,6 +1762,628 @@ inline void cancelModemUssd() { "|| mmcli -m any --3gpp-ussd-cancel >/dev/null 2>&1"); } +inline bool decodeEfAdnRecord(const uint8_t* raw, size_t len, + std::string& outNum, std::string& outName) { + if (!raw || len < 14) return false; + size_t alphaLen = len - 14; + uint8_t bcdLen = raw[alphaLen]; + uint8_t tonNpi = raw[alphaLen + 1]; + if (bcdLen == 0 || bcdLen > 11 || bcdLen == 0xFF) return false; + + outNum.clear(); + if (((tonNpi >> 4) & 0x07) == 1) { + outNum.push_back('+'); + } + auto nibToChar = [](uint8_t nib) -> char { + if (nib <= 9) return static_cast('0' + nib); + if (nib == 0x0A) return '*'; + if (nib == 0x0B) return '#'; + if (nib == 0x0C) return 'p'; + return '\0'; + }; + size_t bcdBytes = std::min(10, bcdLen - 1); + for (size_t i = 0; i < bcdBytes; ++i) { + uint8_t b = raw[alphaLen + 2 + i]; + if (b == 0xFF) break; + char d1 = nibToChar(b & 0x0F); + if (!d1) break; + outNum.push_back(d1); + uint8_t hi = (b >> 4) & 0x0F; + if (hi == 0x0F) break; + char d2 = nibToChar(hi); + if (!d2) break; + outNum.push_back(d2); + } + if (outNum.empty() || outNum == "+") return false; + + size_t aEnd = alphaLen; + while (aEnd > 0 && raw[aEnd - 1] == 0xFF) { + --aEnd; + } + outName.clear(); + if (aEnd > 0 && raw[0] == 0x80) { + for (size_t i = 1; i + 1 < aEnd; i += 2) { + uint16_t cp = (static_cast(raw[i]) << 8) | + static_cast(raw[i + 1]); + if (cp == 0xFFFF || cp == 0) break; + if (cp < 0x80) { + if (cp >= 32 && cp != '\t') outName.push_back(static_cast(cp)); + } else if (cp < 0x800) { + outName.push_back(static_cast(0xC0 | (cp >> 6))); + outName.push_back(static_cast(0x80 | (cp & 0x3F))); + } else { + outName.push_back(static_cast(0xE0 | (cp >> 12))); + outName.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F))); + outName.push_back(static_cast(0x80 | (cp & 0x3F))); + } + } + } else { + for (size_t i = 0; i < aEnd; ++i) { + uint8_t b = raw[i]; + if (b == 0xFF) break; + if (b >= 32 && b < 127 && b != '\t') { + outName.push_back(static_cast(b)); + } + } + } + outName = trimStr(outName); + if (outName.empty()) outName = outNum; + return true; +} + +inline bool encodeEfAdnRecord(const std::string& rawNum, + const std::string& rawName, size_t recSize, + std::vector& outRec) { + if (recSize < 14) return false; + outRec.assign(recSize, 0xFF); + size_t alphaLen = recSize - 14; + + std::string cleanNum = normalizePhone(rawNum); + if (cleanNum.empty()) return false; + bool isIntl = (cleanNum.front() == '+'); + uint8_t tonNpi = isIntl ? 0x91 : 0x81; + + std::vector nibs; + for (char c : cleanNum) { + if (c == '+') continue; + if (c >= '0' && c <= '9') nibs.push_back(static_cast(c - '0')); + else if (c == '*') nibs.push_back(0x0A); + else if (c == '#') nibs.push_back(0x0B); + if (nibs.size() >= 20) break; + } + if (nibs.empty()) return false; + + std::string name = trimStr(rawName); + if (name.empty()) name = cleanNum; + bool pureAscii = true; + for (unsigned char uc : name) { + if (uc >= 0x80) { + pureAscii = false; + break; + } + } + if (pureAscii) { + size_t n = std::min(name.size(), alphaLen); + for (size_t i = 0; i < n; ++i) { + unsigned char uc = static_cast(name[i]); + outRec[i] = (uc >= 32 && uc < 127) ? uc : static_cast(' '); + } + } else if (alphaLen >= 3) { + outRec[0] = 0x80; + size_t outPos = 1; + size_t i = 0; + while (i < name.size() && outPos + 1 < alphaLen) { + uint32_t cp = 0; + unsigned char c0 = static_cast(name[i]); + if (c0 < 0x80) { + cp = c0; + i += 1; + } else if ((c0 & 0xE0) == 0xC0 && i + 1 < name.size()) { + cp = ((c0 & 0x1F) << 6) | + (static_cast(name[i + 1]) & 0x3F); + i += 2; + } else if ((c0 & 0xF0) == 0xE0 && i + 2 < name.size()) { + cp = ((c0 & 0x0F) << 12) | + ((static_cast(name[i + 1]) & 0x3F) << 6) | + (static_cast(name[i + 2]) & 0x3F); + i += 3; + } else { + i += 1; + continue; + } + if (cp <= 0xFFFF) { + outRec[outPos++] = static_cast((cp >> 8) & 0xFF); + outRec[outPos++] = static_cast(cp & 0xFF); + } + } + } + + size_t bcdBytes = (nibs.size() + 1) / 2; + outRec[alphaLen] = static_cast(1 + bcdBytes); + outRec[alphaLen + 1] = tonNpi; + for (size_t i = 0; i < nibs.size(); ++i) { + size_t bIdx = alphaLen + 2 + (i / 2); + if ((i % 2) == 0) { + outRec[bIdx] = (outRec[bIdx] & 0xF0) | (nibs[i] & 0x0F); + } else { + outRec[bIdx] = (outRec[bIdx] & 0x0F) | ((nibs[i] & 0x0F) << 4); + } + } + return true; +} + +inline bool openQrtrUimSocket(int& outSock, uint32_t& outNode, + uint32_t& outPort) { + constexpr int kAfQipcrtr = 42; + constexpr uint32_t kQrtrPortCtrl = 0xfffffffeu; + constexpr uint32_t kQrtrTypeNewServer = 4; + constexpr uint32_t kQrtrTypeNewLookup = 10; + constexpr uint32_t kQmiServiceUim = 11; + + struct SockAddrQrtr { + unsigned short sq_family; + uint32_t sq_node; + uint32_t sq_port; + }; + struct QrtrCtrlPkt { + uint32_t cmd; + uint32_t service; + uint32_t instance; + uint32_t node; + uint32_t port; + } __attribute__((packed)); + + int lfd = ::socket(kAfQipcrtr, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (lfd < 0) return false; + + SockAddrQrtr localSq {}; + socklen_t sl = sizeof(localSq); + if (::getsockname(lfd, reinterpret_cast(&localSq), &sl) < 0) { + ::close(lfd); + return false; + } + SockAddrQrtr ctrlSq {}; + ctrlSq.sq_family = kAfQipcrtr; + ctrlSq.sq_node = localSq.sq_node; + ctrlSq.sq_port = kQrtrPortCtrl; + + QrtrCtrlPkt lookup {}; + lookup.cmd = kQrtrTypeNewLookup; + lookup.service = kQmiServiceUim; + lookup.instance = 0; + if (::sendto(lfd, &lookup, sizeof(lookup), 0, + reinterpret_cast(&ctrlSq), + sizeof(ctrlSq)) < 0) { + ::close(lfd); + return false; + } + + bool found = false; + for (int i = 0; i < 16; ++i) { + struct pollfd pfd { lfd, POLLIN, 0 }; + if (::poll(&pfd, 1, 80) <= 0) break; + QrtrCtrlPkt pkt {}; + ssize_t n = ::recv(lfd, &pkt, sizeof(pkt), 0); + if (n >= static_cast(sizeof(pkt)) && + pkt.cmd == kQrtrTypeNewServer) { + if (pkt.service == kQmiServiceUim && pkt.port != 0) { + outNode = pkt.node; + outPort = pkt.port; + found = true; + } + if (pkt.service == 0 && pkt.node == 0 && pkt.port == 0) { + break; + } + } + } + ::close(lfd); + if (!found) return false; + + outSock = ::socket(kAfQipcrtr, SOCK_DGRAM | SOCK_CLOEXEC, 0); + return outSock >= 0; +} + +inline bool queryQrtrUimAdnGeometry(int sfd, uint32_t uimNode, uint32_t uimPort, + int slotNum, uint8_t& outSessionType, + std::vector& outFileTlvVal, + uint16_t& outRecSize, + uint16_t& outRecCount) { + struct SockAddrQrtr { + unsigned short sq_family; + uint32_t sq_node; + uint32_t sq_port; + } uimSq { 42, uimNode, uimPort }; + + uint8_t sessionCandidates[2] = { + static_cast((slotNum == 2) ? 7 : 6), + 0 + }; + struct FilePathSpec { + uint16_t fileId; + uint8_t pathLen; + uint8_t pathBytes[6]; + } fileSpecs[2] = { + { 0x6F3A, 4, { 0x00, 0x3F, 0x10, 0x7F, 0x00, 0x00 } }, + { 0x4F3A, 6, { 0x00, 0x3F, 0x10, 0x7F, 0x3A, 0x5F } } + }; + + uint16_t txnId = 1; + for (uint8_t stype : sessionCandidates) { + for (const auto& fs : fileSpecs) { + std::vector fval; + fval.push_back(static_cast(fs.fileId & 0xFF)); + fval.push_back(static_cast((fs.fileId >> 8) & 0xFF)); + fval.push_back(fs.pathLen); + for (uint8_t k = 0; k < fs.pathLen; ++k) { + fval.push_back(fs.pathBytes[k]); + } + + std::vector req(7); + req[0] = 0x00; + req[1] = static_cast(txnId & 0xFF); + req[2] = static_cast((txnId >> 8) & 0xFF); + ++txnId; + req[3] = 0x24; + req[4] = 0x00; + + // TLV 0x01: Session + req.push_back(0x01); + req.push_back(0x02); + req.push_back(0x00); + req.push_back(stype); + req.push_back(0x00); + + // TLV 0x02: File + req.push_back(0x02); + req.push_back(static_cast(fval.size() & 0xFF)); + req.push_back(static_cast((fval.size() >> 8) & 0xFF)); + req.insert(req.end(), fval.begin(), fval.end()); + + uint16_t tlvTotal = static_cast(req.size() - 7); + req[5] = static_cast(tlvTotal & 0xFF); + req[6] = static_cast((tlvTotal >> 8) & 0xFF); + + if (::sendto(sfd, req.data(), req.size(), 0, + reinterpret_cast(&uimSq), + sizeof(uimSq)) < 0) { + continue; + } + struct pollfd pfd { sfd, POLLIN, 0 }; + if (::poll(&pfd, 1, 120) <= 0) continue; + uint8_t resp[512]; + ssize_t rlen = ::recv(sfd, resp, sizeof(resp), 0); + if (rlen < 14 || resp[0] != 0x02 || resp[3] != 0x24) continue; + + bool okResult = false; + uint16_t rSize = 0; + uint16_t rCount = 0; + size_t pos = 7; + while (pos + 3 <= static_cast(rlen)) { + uint8_t tt = resp[pos]; + uint16_t tl = static_cast(resp[pos + 1]) | + (static_cast(resp[pos + 2]) << 8); + pos += 3; + if (pos + tl > static_cast(rlen)) break; + if (tt == 0x02 && tl >= 4) { + uint16_t res = static_cast(resp[pos]) | + (static_cast(resp[pos + 1]) << 8); + uint16_t err = static_cast(resp[pos + 2]) | + (static_cast(resp[pos + 3]) << 8); + if (res == 0 && err == 0) okResult = true; + } else if (tt == 0x11 && tl >= 9) { + rSize = static_cast(resp[pos + 5]) | + (static_cast(resp[pos + 6]) << 8); + rCount = static_cast(resp[pos + 7]) | + (static_cast(resp[pos + 8]) << 8); + } + pos += tl; + } + if (okResult && rSize >= 14 && rCount >= 1) { + outSessionType = stype; + outFileTlvVal = fval; + outRecSize = rSize; + outRecCount = std::min(rCount, 500); + return true; + } + } + } + return false; +} + +inline bool qrtrReadSimRecord(int sfd, uint32_t uimNode, uint32_t uimPort, + uint16_t txnId, uint8_t sessionType, + const std::vector& fileTlvVal, + uint16_t recNum, uint16_t recSize, + std::vector& outData) { + struct SockAddrQrtr { + unsigned short sq_family; + uint32_t sq_node; + uint32_t sq_port; + } uimSq { 42, uimNode, uimPort }; + + std::vector req(7); + req[0] = 0x00; + req[1] = static_cast(txnId & 0xFF); + req[2] = static_cast((txnId >> 8) & 0xFF); + req[3] = 0x21; // QMI_UIM_READ_RECORD + req[4] = 0x00; + + // TLV 0x01: Session + req.push_back(0x01); + req.push_back(0x02); + req.push_back(0x00); + req.push_back(sessionType); + req.push_back(0x00); + + // TLV 0x02: File + req.push_back(0x02); + req.push_back(static_cast(fileTlvVal.size() & 0xFF)); + req.push_back(static_cast((fileTlvVal.size() >> 8) & 0xFF)); + req.insert(req.end(), fileTlvVal.begin(), fileTlvVal.end()); + + // TLV 0x03: Record (record_number u16 LE, record_length u16 LE) + req.push_back(0x03); + req.push_back(0x04); + req.push_back(0x00); + req.push_back(static_cast(recNum & 0xFF)); + req.push_back(static_cast((recNum >> 8) & 0xFF)); + req.push_back(static_cast(recSize & 0xFF)); + req.push_back(static_cast((recSize >> 8) & 0xFF)); + + uint16_t tlvTotal = static_cast(req.size() - 7); + req[5] = static_cast(tlvTotal & 0xFF); + req[6] = static_cast((tlvTotal >> 8) & 0xFF); + + if (::sendto(sfd, req.data(), req.size(), 0, + reinterpret_cast(&uimSq), + sizeof(uimSq)) < 0) { + return false; + } + struct pollfd pfd { sfd, POLLIN, 0 }; + if (::poll(&pfd, 1, 120) <= 0) return false; + uint8_t resp[512]; + ssize_t rlen = ::recv(sfd, resp, sizeof(resp), 0); + if (rlen < 14 || resp[0] != 0x02 || resp[3] != 0x21) return false; + + bool okResult = false; + outData.clear(); + size_t pos = 7; + while (pos + 3 <= static_cast(rlen)) { + uint8_t tt = resp[pos]; + uint16_t tl = static_cast(resp[pos + 1]) | + (static_cast(resp[pos + 2]) << 8); + pos += 3; + if (pos + tl > static_cast(rlen)) break; + if (tt == 0x02 && tl >= 4) { + uint16_t res = static_cast(resp[pos]) | + (static_cast(resp[pos + 1]) << 8); + uint16_t err = static_cast(resp[pos + 2]) | + (static_cast(resp[pos + 3]) << 8); + if (res == 0 && err == 0) okResult = true; + } else if (tt == 0x11 && tl >= 2) { + uint16_t clen = static_cast(resp[pos]) | + (static_cast(resp[pos + 1]) << 8); + if (2 + clen <= tl) { + outData.assign(resp + pos + 2, resp + pos + 2 + clen); + } + } + pos += tl; + } + return okResult && !outData.empty(); +} + +inline bool qrtrWriteSimRecord(int sfd, uint32_t uimNode, uint32_t uimPort, + uint16_t txnId, uint8_t sessionType, + const std::vector& fileTlvVal, + uint16_t recNum, + const std::vector& recData) { + struct SockAddrQrtr { + unsigned short sq_family; + uint32_t sq_node; + uint32_t sq_port; + } uimSq { 42, uimNode, uimPort }; + + std::vector req(7); + req[0] = 0x00; + req[1] = static_cast(txnId & 0xFF); + req[2] = static_cast((txnId >> 8) & 0xFF); + req[3] = 0x23; // QMI_UIM_WRITE_RECORD + req[4] = 0x00; + + // TLV 0x01: Session + req.push_back(0x01); + req.push_back(0x02); + req.push_back(0x00); + req.push_back(sessionType); + req.push_back(0x00); + + // TLV 0x02: File + req.push_back(0x02); + req.push_back(static_cast(fileTlvVal.size() & 0xFF)); + req.push_back(static_cast((fileTlvVal.size() >> 8) & 0xFF)); + req.insert(req.end(), fileTlvVal.begin(), fileTlvVal.end()); + + // TLV 0x03: Write Record (record_number u16 LE, data_len u16 LE, data[]) + uint16_t wTlvLen = static_cast(4 + recData.size()); + req.push_back(0x03); + req.push_back(static_cast(wTlvLen & 0xFF)); + req.push_back(static_cast((wTlvLen >> 8) & 0xFF)); + req.push_back(static_cast(recNum & 0xFF)); + req.push_back(static_cast((recNum >> 8) & 0xFF)); + uint16_t dLen = static_cast(recData.size()); + req.push_back(static_cast(dLen & 0xFF)); + req.push_back(static_cast((dLen >> 8) & 0xFF)); + req.insert(req.end(), recData.begin(), recData.end()); + + uint16_t tlvTotal = static_cast(req.size() - 7); + req[5] = static_cast(tlvTotal & 0xFF); + req[6] = static_cast((tlvTotal >> 8) & 0xFF); + + if (::sendto(sfd, req.data(), req.size(), 0, + reinterpret_cast(&uimSq), + sizeof(uimSq)) < 0) { + return false; + } + struct pollfd pfd { sfd, POLLIN, 0 }; + if (::poll(&pfd, 1, 200) <= 0) return false; + uint8_t resp[512]; + ssize_t rlen = ::recv(sfd, resp, sizeof(resp), 0); + if (rlen < 14 || resp[0] != 0x02 || resp[3] != 0x23) return false; + + bool okResult = false; + size_t pos = 7; + while (pos + 3 <= static_cast(rlen)) { + uint8_t tt = resp[pos]; + uint16_t tl = static_cast(resp[pos + 1]) | + (static_cast(resp[pos + 2]) << 8); + pos += 3; + if (pos + tl > static_cast(rlen)) break; + if (tt == 0x02 && tl >= 4) { + uint16_t res = static_cast(resp[pos]) | + (static_cast(resp[pos + 1]) << 8); + uint16_t err = static_cast(resp[pos + 2]) | + (static_cast(resp[pos + 3]) << 8); + if (res == 0 && err == 0) okResult = true; + } + pos += tl; + } + return okResult; +} + +inline bool qrtrReadSimContacts( + int slotNum, std::vector>& outContacts, + std::string& outErr) { + outContacts.clear(); + outErr.clear(); + + int sfd = -1; + uint32_t uimNode = 0, uimPort = 0; + if (!openQrtrUimSocket(sfd, uimNode, uimPort)) { + outErr = "Modem UIM service unavailable"; + return false; + } + + uint8_t sessionType = 0; + std::vector fileTlvVal; + uint16_t recSize = 0, recCount = 0; + if (!queryQrtrUimAdnGeometry(sfd, uimNode, uimPort, slotNum, sessionType, + fileTlvVal, recSize, recCount)) { + ::close(sfd); + outErr = "SIM " + std::to_string(slotNum) + " phonebook not ready"; + return false; + } + + std::vector raw; + for (uint16_t rnum = 1; rnum <= recCount; ++rnum) { + if (qrtrReadSimRecord(sfd, uimNode, uimPort, static_cast(10 + rnum), + sessionType, fileTlvVal, rnum, recSize, raw)) { + std::string num, name; + if (decodeEfAdnRecord(raw.data(), raw.size(), num, name)) { + outContacts.push_back({num, name}); + } + } + } + ::close(sfd); + return true; +} + +inline bool qrtrWriteSimContacts( + int slotNum, + const std::vector>& contactsToWrite, + int& outExported, std::string& outErr) { + outExported = 0; + outErr.clear(); + if (contactsToWrite.empty()) { + outErr = "No contacts to export"; + return false; + } + + int sfd = -1; + uint32_t uimNode = 0, uimPort = 0; + if (!openQrtrUimSocket(sfd, uimNode, uimPort)) { + outErr = "Modem UIM service unavailable"; + return false; + } + + uint8_t sessionType = 0; + std::vector fileTlvVal; + uint16_t recSize = 0, recCount = 0; + if (!queryQrtrUimAdnGeometry(sfd, uimNode, uimPort, slotNum, sessionType, + fileTlvVal, recSize, recCount)) { + ::close(sfd); + outErr = "SIM " + std::to_string(slotNum) + " phonebook not ready"; + return false; + } + + std::unordered_map existingByNorm; + std::unordered_map existingBySuffix; + std::vector emptySlots; + std::vector raw; + + for (uint16_t rnum = 1; rnum <= recCount; ++rnum) { + if (qrtrReadSimRecord(sfd, uimNode, uimPort, static_cast(10 + rnum), + sessionType, fileTlvVal, rnum, recSize, raw)) { + std::string num, name; + if (decodeEfAdnRecord(raw.data(), raw.size(), num, name)) { + std::string norm = normalizePhone(num); + if (!norm.empty()) { + existingByNorm[norm] = rnum; + std::string sfx = phoneSuffixKey(norm); + if (!sfx.empty()) existingBySuffix[sfx] = rnum; + } + } else { + emptySlots.push_back(rnum); + } + } + } + + size_t emptyIdx = 0; + uint16_t txnId = static_cast(1000); + bool simFull = false; + + for (const auto& kv : contactsToWrite) { + std::string norm = normalizePhone(kv.first); + if (norm.empty()) continue; + + uint16_t targetRec = 0; + auto it = existingByNorm.find(norm); + if (it != existingByNorm.end()) { + targetRec = it->second; + } else { + std::string sfx = phoneSuffixKey(norm); + auto sit = sfx.empty() ? existingBySuffix.end() + : existingBySuffix.find(sfx); + if (sit != existingBySuffix.end()) { + targetRec = sit->second; + } else if (emptyIdx < emptySlots.size()) { + targetRec = emptySlots[emptyIdx++]; + } else { + simFull = true; + break; + } + } + + std::vector recData; + if (!encodeEfAdnRecord(kv.first, kv.second, recSize, recData)) { + continue; + } + if (qrtrWriteSimRecord(sfd, uimNode, uimPort, txnId++, sessionType, + fileTlvVal, targetRec, recData)) { + ++outExported; + existingByNorm[norm] = targetRec; + std::string sfx = phoneSuffixKey(norm); + if (!sfx.empty()) existingBySuffix[sfx] = targetRec; + } + } + + ::close(sfd); + if (outExported == 0 && simFull) { + outErr = "SIM " + std::to_string(slotNum) + " phonebook is full"; + return false; + } + return outExported > 0; +} + } // namespace bananarchy #endif // DWM_PLUGINS_PHONE_DATA_H