Android 导入 iPhone 短信的解决方案

October 18, 2019
PhonePythonAndroid

由于好久不做 Android 开发所以也好久没有写过安卓开发的文章了,前几天用了一下,赶紧让我抓住机会水一篇博客。

前几天国庆老爸换了一台华为P30 Pro,换掉了原来使用的 iPhone,通过华为的手机克隆迁移了大部分手机上的资料:通讯录,照片等等,唯独短信没有办法迁移。iOS 那边,我用iTools导出来了,但是 iTools 本身也不支持 Android 的短信导入,试了很多手机管理软件都没有把 iPhone 的短息导入到 Android 上的功能,所以只好自己动手丰衣足食。

计划很简单,就是先把短息导出来,然后写个小脚本转成Json存下来,写一个 Android App 去读取那个Json文件然后倒入到手机里面。

短信导出

iPhone 那边的导出我使用了 iTools,但是iTools 只能导出为csv和html,最后我选择了导出为 html,导出的样式如下图。html顶部是发件人的号码,收到的短信是一个灰色的气泡,发送的短信是一个绿色的气泡。导出成这个样子然后用 Python 的爬虫根据元素的class抓一下就能轻松的把号码,时间,短信内容清洗出来了。

这边放一个大概的Python代码,使用 BeautifulSoup 解析的 html 文件。因为这个方案没有普遍性所以就不具体讲这个代码了,学过html和css的同学肯定是能看懂。这个Python脚本最后会把所有的短息打包成一个 Json 文件保存起来。

  1. def read_messages(path, number):
  2. html = open(path, 'r', encoding='utf-8')
  3. soup = BeautifulSoup(html.read())
  4. body = soup.body.contents[4:-1]
  5. current_time_stamp = 0 #时间戳
  6. current_number = number #短息号码
  7. current_type = 1 #1代表是接收短息,2代表发送短信
  8. for each in body:
  9. class_name = each.attrs['class'][0]
  10. if class_name == 'date': #得到日期
  11. time_str = each.get_text().replace('Date:','')
  12. timeArray = time.strptime(time_str, "%Y-%m-%d %H:%M")
  13. current_time_stamp = int(time.mktime(timeArray))
  14. print('date', time_str, timeStamp)
  15. else: #得到短信内容
  16. if class_name == 'triangle-isosceles':
  17. print('type = 1 接收', each.get_text(),'\n')
  18. current_type = 1
  19. elif class_name == 'triangle-isosceles2':
  20. print('type = 2 发送', each.get_text(),'\n')
  21. current_type = 2
  22. messages.append({'address':current_number,
  23. 'type':current_type,
  24. 'date':current_time_stamp,
  25. 'body': each.get_text(),
  26. })
  27. print(current_number, 'done.')

Json 文件是下面这个样子,每个元素是一个短息,包括号码,短息类型,接收/发送时间和短息内容。有了这个 Json 文件就可以开始写个 App 来导入短信了。

短信导入

一开始我以为 Android 特别特别好导入短信,给个权限随随便便几行代码就行了,当时正是国庆时候 Google 都上不去,在百度上搜了一圈给的代码都差不多,就是使用 ContentResolver 类来把短信写到手机里。有一定 Android 基础的同学肯定知道 Android 的四大组件,分别是:

  • 活动(Activity)应用程序的前台显示类,也是所有安卓程序员最先接触的组件,App 所有的界面都是由 Activity 实现的。
  • 服务(Service),后台运行服务,当前台被杀掉后后台服务还能够运行并且执行操作,常用语后台下载和后台通知推送,比如要开发一个闹钟的 App的话就需要 Service,要不然的话用户如果把 App前台清掉的话闹钟就不会响了,秒表也是。
  • 广播接受者(Broadcast Receiver), 用于接收系统广播来执行一些操作,Android 在系统的一些阶段会发送一个广播向 App 告知自己的状态,比如亮屏或锁屏的时候,接收短信的时候,充电的时候,电量低的时候等等,很多毒瘤 App 就是利用广播接受者来唤醒自己的,比如注册一个接受每次亮屏的广播,这样用户每次解锁手机的时候自己的 App 就会被直接唤醒。
  • 内容提供者(Content Provider),用来跨应用储存,如果你的 App 想要读取系统的 通讯录,短信之类的信息就需要使用 Content Provider,写入短信用到的ContentResolver就是属于内容提供者的一个类。

不过由于Android Kitkat 之后的权限的变动,原本的 WRITE_SMS 权限已经被移除了,也就是说开发者已经不能直接在内容提供者上直接写短信到系统里了。在百度上查了半天终于发现了,只有 App 是一个短信类应用 (比如 Facebook Messager)的时候才能读取和写入系统的短信,并且同时只能有一个短信类 App 能够进行短信操作,这取决于用户在设置里选择的默认短信应用是谁。

但是怎么样才能把 App 变成短息类应用程序呢?
Android Developers Blog: Getting Your SMS Apps Ready for KitKat Google Android Developer博客上已经写得比较详细了。

首先需要一个 Activatity 这个不用说,然后需要两个 BroadcastReceiver,分别用来接收SMS和MMS,还需要一个Service 来响应快捷回复功能。因为我不会真正的去开发一个短信App,只是想获得短信写入的权限,所以这两个 Receiver 和 Service 我都只是新建了类但没有去实现,就像下图一样。

如果真的想要开发短信类 App 的同学可以去学习下这个文章,对于 Receiver 的讲解详细的多 2.2: Sending and Receiving SMS Messages - Part 2 · GitBook

在 manifest 文件里还需要加很多intent filter,在这里我直接把需要的贴出来,里面的注释已经写得很清楚了。

  1. <manifest>
  2. ...
  3. <application>
  4. <!-- BroadcastReceiver that listens for incoming SMS messages -->
  5. <receiver android:name=".SmsReceiver"
  6. android:permission="android.permission.BROADCAST_SMS">
  7. <intent-filter>
  8. <action android:name="android.provider.Telephony.SMS_DELIVER" />
  9. </intent-filter>
  10. </receiver>
  11. <!-- BroadcastReceiver that listens for incoming MMS messages -->
  12. <receiver android:name=".MmsReceiver"
  13. android:permission="android.permission.BROADCAST_WAP_PUSH">
  14. <intent-filter>
  15. <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
  16. <data android:mimeType="application/vnd.wap.mms-message" />
  17. </intent-filter>
  18. </receiver>
  19. <!-- Activity that allows the user to send new SMS/MMS messages -->
  20. <activity android:name=".ComposeSmsActivity" >
  21. <intent-filter>
  22. <action android:name="android.intent.action.SEND" />
  23. <action android:name="android.intent.action.SENDTO" />
  24. <category android:name="android.intent.category.DEFAULT" />
  25. <category android:name="android.intent.category.BROWSABLE" />
  26. <data android:scheme="sms" />
  27. <data android:scheme="smsto" />
  28. <data android:scheme="mms" />
  29. <data android:scheme="mmsto" />
  30. </intent-filter>
  31. </activity>
  32. <!-- Service that delivers messages from the phone "quick response" -->
  33. <service android:name=".HeadlessSmsSendService"
  34. android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
  35. android:exported="true" >
  36. <intent-filter>
  37. <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
  38. <category android:name="android.intent.category.DEFAULT" />
  39. <data android:scheme="sms" />
  40. <data android:scheme="smsto" />
  41. <data android:scheme="mms" />
  42. <data android:scheme="mmsto" />
  43. </intent-filter>
  44. </service>
  45. </application>
  46. </manifest>

如果你的manifest 没问题并且有那两个 Service 和 Broadcast Receiver 的话,那么 Android 的系统设置里的默认短信 App 就会出现你的程序了,这时只要你的 App 是默认 SMS app的话就可以向系统写入短信了。

  1. ContentResolver resolver=getContentResolver();
  2. Uri url= Uri.parse("content://sms/inbox");
  3. try {
  4. JSONArray jsonArray = new JSONArray(builder.toString());
  5. for (int i =0;i<jsonArray.length();i++){
  6. //读取json
  7. JSONObject object = jsonArray.getJSONObject(i);
  8. String address = object.getString("address");
  9. String body = object.getString("body");
  10. long time = object.getLong("date")*1000;
  11. int type = object.getInt("type");
  12. //写入
  13. ContentValues values=new ContentValues();
  14. values.put("address", address);
  15. values.put("type", type);
  16. values.put("date", time);
  17. values.put("body", body);
  18. values.put("read",1);
  19. resolver.insert(url, values);
  20. }
  21. } catch (JSONException e) {
  22. e.printStackTrace();
  23. }

在上面的代码中初始化了一个 ContentResolver 实例用于写入,Uri实例表示写入路径是收件箱,然后读取了之前保存的Json文件,包装成了一个ContentValues实例,之后把调用表Resolver的 insert 方法包装的ContentValues 实例插入就行了。

下面是对 ContentValues 的一些讲解:

  • id:短信的id
  • thread_id:对话的序号,如100,与同一个手机号互发的短信,其序号是相同的
  • address:发件人地址,即手机号,如10086
  • person:发件人,如果发件人在通讯录中则为具体姓名,陌生人为null
  • date:日期,long型,是一个时间戳
  • protocol:协议0: SMS_RPOTO短信; 1:MMS_PROTO彩信
  • read:是否阅读0未读,1已读
  • status:短信状态,1代表接收,0: complete, 64: pending,128: failed
  • type:短信类型,1代表接收2代表发送
  • body:短信具体内容

然后在补一个好看点的界面就大功告成啦,运行结束后短信就都导入进去了。

Comments

July 21, 2018 at 10:52 am

There are no comments

keyboard_arrow_up