简介:这是一份面向安卓开发者的SIM卡管家项目资源,聚焦系统级SIM卡数据管理场景,适合想学习系统能力扩展、反射调用隐藏API的进阶开发者。项目利用反射技术调用安卓隐藏API,具备对SIM卡联系人、短信两个维度的增删改查与导出能力,并可查看SIM卡上的相关信息;针对双卡双待机型,还给出主卡槽使用注意事项,降低读卡失败概率。压缩包为zip格式,共738个文件,整体约7.69MB,目录中包含XML布局与配置、Java源码、class与dex编译产物、Gradle脚本、jar依赖库及apk安装包,能较好还原安卓工程从源码、资源编译到安装包生成的链路。已有564人学习浏览。阅读源码时,可重点借鉴反射隐藏API的调用结构,并借助工程内大量编译中间文件理解安卓资源编译与构建流程,便于迁移到其他系统能力扩展场景,也可作为研究隐藏系统接口的入门参考。
1. 手机越做越轻,SIM卡反而成了被忽略的存储介质
SIM卡在手机上退化为鉴权工具,但在工控设备、老人机、短信猫、车联网终端里,它依然是联系人号码和短信的物理载体。项目标题里的“SIM卡管家”,本质上是把一张SIM卡当作一个微型数据库——通过串口或读卡器和它通信,对卡上的联系人(ADN)和短信(SMS)记录执行增删改查。看起来像杂活,实际做起来既有文件系统知识,又有编码转换和AT命令时序的细节,还涉及GSM 11.11标准里EF文件结构与3GPP TS 27.007命令集的对应关系。本文从文件系统原理入手,用一个可运行的Python串口方案,把联系人、短信的四类操作逐一落地,最后补上几个只有踩过坑才知道的边界条件。适合正在做短消息硬件网关、SIM卡测试工装或设备端号码预置的工程师参考。
2. SIM卡文件系统与AT命令的映射关系:先搞清数据存在哪
2.1 SIM卡内部不是二维表,是MF/DF/EF三层目录结构
SIM卡的操作系统通常基于Java Card或Native OS,逻辑上采用ISO 7816-4定义的文件系统:主文件MF是根目录,DF是应用专用目录,EF是基本文件,真正存用户数据的只有EF。联系人和短信分属于不同EF文件:EF_ADN(Abbreviated Dialing Numbers)存储电话号码和对应名称,EF_SMS和EF_SMSP则存放收到的短信与短信参数,EF_EXT1是ADN的扩展记录,用于超长号码。一个常见误区是把“SIM卡联系人”当成一个列表,实际上固话号码和手机号码的存储格式有差异,长度超限时SIM卡会把部分号码拆分到EXT1,查询时再拼接返回。
AT命令层面,运营商模块和读卡器固件把对EF_ADN和EF_SMS的访问封装成了电话簿命令和短信命令。对应关系大致为:AT+CPBR对应读,AT+CPBW对应写,AT+CPBF对应查找,AT+CPBD对应删除,而短信读用AT+CMGL,删用AT+CMGD,写用AT+CMGW。这套命令家族来自3GPP TS 27.007,是所有支持GSM/UMTS/LTE模块共用的高层接口。
2.2 为什么选择AT命令而不直接发APDU
SIM卡的底层访问本质是APDU,即命令APDU和响应APDU的交换。APDU可以直接操作任意EF文件,因此能读运营商专用的IMSI、ICCID等,但代价是必须自己处理CLA/INS/P1/P2参数和状态字(SW1/SW2),并且不同读卡器的透明通道格式不一致。做“SIM卡管家”这种上层应用,除非要读卡片私有文件,否则直接用AT命令更稳。
# 通过AT+CRSM访问EF_ADN的例子,返回的是十六进制字节流 import serial ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=2) cmd = 'AT+CRSM=192,28474,0,0,12\r\n' # SELECT EF_ADN ser.write(cmd.encode('ascii')) import time time.sleep(0.5) resp = ser.read(ser.in_waiting).decode('utf-8', errors='ignore') print(resp)参数说明:192是SELECT命令的CLA+P1组合,28474是EF_ADN的FID;返回的+CRSM响应内容就是EF文件数据。直接用APDU需要应对不同厂商卡片的文件权限错误,AT+CRSM虽然慢一点,可移植性好很多。日常开发建议以AT命令为主,APDU留到排查数据错乱时再用来做底层验证。
2.3 硬件选型与最小实验环境
常见做法是使用USB转串口的GSM模块(SIM800、SIM7600系列),或者PC/SC读卡器。前者好处是能同时测短信发送,后者优势是读取稳定、不必插SIM卡在手机上。为了验证本文代码,推荐串口模块加一张正常使用的SIM卡,注意模块通电前先插卡,部分模组带电插卡容易损坏SIM卡触点。
# 检查串口设备是否识别,Linux下一般是ttyUSB0 ls /dev/ttyUSB* # 用python交互环境快速测通AT通道 python3 -c "import serial; s=serial.Serial('/dev/ttyUSB0',115200,timeout=2); s.write(b'AT\r\n'); print(s.read(64).decode())"命令说明:第一步确认操作系统识别到设备节点,第二步发送不带参数的AT指令,返回“OK”即说明模块与串口配置无误。这里有一个容易踩的坑:某些模块上电后默认波特率不是115200,而是9600或自动波特率,所以连不上时先排除波特率。
2.4 锁定存储位置可以避免“写错地方”
联系人和短信在SIM卡上都有对应的“存储介质”代号,电话簿的存储位置用AT+CPBS选择,SIM卡对应“SM”。不少模块出厂默认位置是“ME”(模块内存),如果不开这条命令,后续操作会落在模块内存而不是SIM卡上,导致拔出SIM卡看不到数据。
ser.write(b'AT+CPBS="SM"\r\n') resp = ser.read(ser.in_waiting).decode()这步操作虽然简单,却经常被当成多余动作跳过。另外,AT+CPBS还支持“FD”(固定拨号)等特殊存储区,固定拨号区写入受PIN2限制,普通写入会报错。
3. 联系人增删改查:用AT+CPBR/AT+CPBW打通SIM卡电话簿
3.1 四类操作与命令的对应关系表
接触SIM卡联系人时,先明确每条命令的语义和使用边界。从数据操作角度可以画一张简单映射:
| 操作类型 | 数据库类比 | AT命令 | 关键参数 | 常见报错 |
|---|---|---|---|---|
| 查询列表 | SELECT | AT+CPBR=1,250 | 起始索引,结束索引 | +CME ERROR: 22 |
| 精确查找 | WHERE name LIKE | AT+CPBF="关键词" | 支持*和?通配 | +CME ERROR: 22 |
| 新增记录 | INSERT | AT+CPBW=,"号码",145,"姓名" | 索引可省略表示追加 | +CME ERROR: 20 |
| 更新记录 | UPDATE | AT+CPBW=索引,"新号码",145,"新姓名" | 索引必须存在 | +CME ERROR: 21 |
| 删除记录 | DELETE | AT+CPBD=索引 | 单条删除 | +CME ERROR: 21 |
从表格可见,联系人的“改”本质上是对指定索引重新执行写入,SD卡管家在实现更新时不必区分新增和修改,统一用AT+CPBW即可。
3.2 实现一个可用的PhoneBookManager类
下面给出一个简单的Python类,覆盖读、查、增、改、删五个操作。用pyserial库做串口通信,保留原始串口句柄的灵活度,不去包装过多业务逻辑。
import serial import time class PhoneBookManager: def __init__(self, port, baudrate=115200): self.ser = serial.Serial(port, baudrate, timeout=2) def _at(self, command, delay=0.6): self.ser.write((command + '\r\n').encode('ascii')) time.sleep(delay) return self.ser.read(self.ser.in_waiting).decode('utf-8', errors='ignore') def select_sim(self): return self._at('AT+CPBS="SM"') def read_all(self): resp = self._at('AT+CPBR=1,250') contacts = [] for line in resp.splitlines(): if line.startswith('+CPBR:'): parts = line.split(':')[1].strip() idx, number, ctype, name = parts.split(',', 3) contacts.append({'index': int(idx), 'number': number, 'type': ctype, 'name': name.strip('"')}) return contacts def search(self, keyword): resp = self._at(f'AT+CPBF="{keyword}"') return [line for line in resp.splitlines() if line.startswith('+CPBF:')] def write_contact(self, number, name, index=None): ctype = 145 if number.startswith('+') else 129 if index is None: cmd = f'AT+CPBW=,"{number}",{ctype},"{name}"' else: cmd = f'AT+CPBW={index},"{number}",{ctype},"{name}"' return self._at(cmd) def delete_contact(self, index): return self._at(f'AT+CPBD={index}')代码逻辑说明:_at方法统一处理命令发送和读回,给每次命令留出固定延时用于模块处理。read_all通过AT+CPBR=1,250尽力读取全部索引,250是大多数SIM卡的EF_ADN容量上限。write_contact根据号码是否带+号自动选用145(国际格式)或129(国内格式),索引为空时让模块自动分配,指定索引就是修改既有记录。删除命令只传索引。
参数说明里最容易被忽略的是name字段的长度限制,常见做法是GSM 7bit编码下不超过60字符,使用UCS2编码时按字节计数,超出后模块直接返回error。
3.3 一个容易翻车的细节:名称编码
SIM卡EF_ADN记录内部用7bit字符编码存储名称,但模块在AT接口上一般兼容ASCII和UCS2。写中文联系人时,模块处于Text模式还是PDU模式会影响结果。稳妥做法是把模块切换到UCS2编码再写名称:
# 切到UCS2编码模式 self._at('AT+CSCS="UCS2"') resp = self.write_contact('+8613800138000', '4F60597D') # 两个字的UCS2编码切到UCS2后,写入的姓名必须是十六进制UCS2码元。很多初学者在这一步栽跟头,因为切编码模式会影响后面所有字符串参数,包括AT+CPBF的查询条件。建议写完联系人后把模块切回AT+CSCS="GSM",避免影响后续短信命令的参数编码。
3.4 批量操作时的索引管理策略
批量导入通讯录时,直接依赖模块自动分配索引,管理上容易失控。比如先删除再重新导入,索引会连续递增而不是从空位补起。实用做法是先用AT+CPBR=1,250读取全量,构造一个在用的索引集合,再人工指定第一个空位写入。
used_indices = set(c['index'] for c in manager.read_all()) free_index = next(i for i in range(1, 251) if i not in used_indices) print(manager.write_contact('+8610000', 'TEST', index=free_index))这么做的好处是让后续删除和更新有确定索引可依,不会出现模块内部索引错位。另一个应对方式是用AT+CPBR循环确认当前最大索引,配合容量检查控制批量任务节奏。
4. 短信增删改查:从AT+CMGL到PDU编码的完整闭环
4.1 短信的“改”不是UPDATE,是“重写”
短信在SIM卡EF_SMS中的存储方式与联系人不同,它是按TPDU记录存储,长度可变、记录个数有限(常见SIM卡存30到40条),没有“修改某一条短信内容”的本地命令。所以短信的“改”实际由“删除旧短信+写入新短信”组合完成。这个区别直接决定了短信管家在实现更新接口时,必须先确认新短信长度不超过剩余空间,再执行删除,否则会因空间不足丢失数据。
短信操作命令核心如下:
| 操作 | 命令 | 备注 |
|---|---|---|
| 列出所有短信 | AT+CMGL=4 | 4表示“全部” |
| 读取指定索引 | AT+CMGR=索引 | 返回单个PDU或TEXT记录 |
| 写入但不发送 | AT+CMGW=长度 | 后接PDU内容 |
| 删除短信 | AT+CMGD=索引 | 部分模块支持传多个索引 |
| 新短信通知 | AT+CNMI=2,1,0,0,0 | 模块主动上报+CMTI |
短信报文的格式分为TEXT模式和PDU模式。TEXT模式人类可读,但中文支持依赖模块内置字符集,处理不好会乱码,排错困难;PDU模式完全自己解析,跨模块稳定性最高,推荐“管家”类工具直接选PDU。
4.2 PDU模式下写入一条短信的最小实现
在PDU模式下,一条从SIM卡发出的短信内容由SMSC信息长度、TPDU首字节、发送方号码、协议标识、编码方案、有效期、用户数据长度、用户数据组成。写SIM卡时SMSC通常传空短消息中心(长度00),模块会自动填入卡上的短信中心号码。
def encode_pdu(destination, message): # 构造一个只支持7bit ASCII的简单PDU pdu = '00' # SMSC为空, 使用SIM卡默认 pdu += '11' # MTI=1, VP=1 # 目标地址转为Semi-octet格式 dest = destination.replace('+', '') if len(dest) % 2 != 0: dest += 'F' pdu += '0D' + '91' + dest pdu += '00' # 协议标识 pdu += '00' # 7bit编码方案 # 用户数据7bit打包 encoded = '7B05C8D2F4CC' # 示意,实际应替换为正文的7bit打包结果 pdu += '00' + encoded return pdu pdu = encode_pdu('+8613800138000', 'hello') cmd = f'AT+CMGW={len(pdu) // 2}\r\n' ser.write(cmd.encode('ascii')) time.sleep(0.2) ser.write(pdu.encode('ascii'))PDU编码说明:第一字节0D是后续地址长度,91表示国际格式,号码使用半字节反转。7bit编码部分需要专门打包算法,把字符压缩到7位。中文短信改用UCS2时,用户数据先用UTF-16BE编码,再转成十六进制,编码方案名换成08,长度按字节数而非字符数计算。
4.3 读取全量短信并解析索引与状态
使用AT+CMGL=4在TEXT模式下返回的每个块里,+CMGL: "REC READ","号码","日期"记录了索引和状态,之后是短信正文。在PDU模式下,+CMGL: 索引,状态,长度后面就是十六进制PDU内容。实际写代码时,用TEXT模式读取列表方便预览,处理逻辑再切换回PDU模式。
resp = manager._at('AT+CMGL="ALL"') lines = resp.splitlines() for i, line in enumerate(lines): if line.startswith('+CMGL:'): meta = line.split(':')[1].strip().split(',') idx = meta[0] number = meta[2] print(f'Index: {idx}, From: {number}, Content: {lines[i+1]}')这种方式的局限是遇到中英文混合短信、状态报告短信时会解析错位,更好的做法是用正则把每条+CMGL块拆分出来。模块返回的换行符不统一也是一个典型问题,有的用\r\n,有的用\n,实际项目里可以先统一替换。
4.4 异步新短信通知是“管家”的关键体验
轮询AT+CMGL=4能拿到新短信,但轮询间隔短会增加模块负载,间隔长则消息不及时。合理用法是打开新短信通知,让模块主动上报+CMTI: "SM",索引。这组命令配置在模块启动后执行一次,Pyserial持续监听即可。
manager._at('AT+CNMI=2,1,0,0,0') time.sleep(0.3) manager.ser.reset_input_buffer() while True: line = manager.ser.readline().decode(errors='ignore') if line.startswith('+CMTI'): idx = line.split(',')[1].strip() resp = manager._at(f'AT+CMGR={idx}') print(resp)参数说明:AT+CNMI的第二个参数设为1表示把新短信主动推送到串口,第三个参数设为0表示不缓存旧短信。开启后模块会周期性发送URC,所以主循环里要先清空输入缓冲区,防止启动时的残留数据干扰。稳定运行后建议加上异常处理,串口断开时自动重连。
4.5 短信删除时机的选择
删除短信要避免在模块收到新短信的同一时刻执行,某些固件有命令互斥锁,此时会返回busy。保守做法是删除前先读取一次当前短信列表,确认目标索引仍然存在再删。批量删除用循环调用AT+CMGD,不要试图一条AT指令删除多条,部分模块不支持逗号分隔索引。
5. 工程化收尾:从AT指令调试到一个能交差的“SIM卡管家”
做“管家”类工具,最容易被低估的是“数据一致性验证”。联系人和短信的增删改查都执行成功后,建议主动做一次回读校验,把写入的值和读取的值逐字段比对。实操上可以写一个简单的verify方法,例如写入后立即按索引读取,对比号码和名称字符串是否一致。对短信而言,需要额外解析日期时间戳,确保模块时区设置正确,否则会出现入库时间偏移8小时的诡异问题。
AT+CRSM绕过上层接口直接读EF原始字节,是排查“写进去了但读不出来”的杀手锏。用AT+CRSM=192,28474,0,0,12选中EF_ADN,再用AT+CRSM=176,28474,0,0,12读取记录。读回来的十六进制串里能看到号码的BCD编码和名称的7bit码流,对照GSM 11.11的格式逐字节核对,能快速区分问题是模块命令传参错误还是SIM卡上的数据本身写错。
最后的实践建议:优先选择带PC/SC驱动的读卡器做交付,而不是依赖串口模块,因为PC/SC读卡器的电源控制和卡插入检测更适合反复插拔的测试场景;代码里所有超时时间用配置项而不是魔法数字;操作SIM卡前先读一下EF_ADN的容量,很多新卡的容量从250条降到180条,默认范围会触发越界错误。把验证脚本纳入自动化测试,每次版本发布前对同一张测试卡跑一轮增删改查全流程,这是避免交付后线上数据错乱最划算的办法。
本文还有配套的精品资源,点击获取