最近弄了个短信自动填充功能,一开始觉得很简单,不就是动态注册个广播接收器去监听短信消息不就可以了吗?结果没这么简单,问题就出在机型的适配上。小米的短信权限、荣耀、OPPO的短信监听都是坑,暂时就用这三个手机测了,其他的遇到了再补充。
下面简单讲讲:
短信属于隐私权限,Android 6.0后需要动态申请权限。首先在manifest里面注册权限:
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
在需要用的地方,动态申请下:
String[] smsPermission = {Manifest.permission.READ_SMS, Manifest.permission.RECEIVE_SMS};
本来这样权限问题就搞定了,但是在小米手机上就不行。小米手机会把短信归类到通知类权限里:

在 ContextCompat.checkSelfPermission 的时候会直接返回true,并且不会弹出权限对话框,而是在实际使用的时候才会咨询用户,按理说好像和我们逻辑没有冲突,但是在使用receiver进行监听前,不是得确保有权限么?实际效果也是,在没有权限时,不能获取到短信的广播。
在网上找了找办法,好像也没多少博文,但是大致有了思路:不是用的时候才申请么?那我就先用一下,再去用receiver监听。下面是方法:
// 读取一下试试,能读取到就有权限
boolean flag = false;
try {
Uri uri = Uri.parse("content://sms/inbox");
ContentResolver cr = context.getContentResolver();
String[] projection = new String[]{"_id"};
Cursor cur = cr.query(uri, projection, null, null, "date desc");
if (null != cur) {
cur.close();
}
lag = true;
}catch (Exception e) {
e.printStackTrace();
}
这里仅针对小米手机啊,对小米手机的判断我只是用 android.os.Build.MANUFACTURER 简单判断了下,如果有更高要求请查找资料。
这里也是网上随便找了个代码,能用,但是在荣耀手机上却是偶尔能收到一次,后面几次就收不到了,打了log也没进入到onReceive中,这就很离奇了,排查了很久。同样的代码,在小米手机上是没问题的,那就只可能是适配问题了。
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.SmsMessage;
import android.util.Log;
public class SmsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//Toast.makeText(context, "收到信息", Toast.LENGTH_LONG).show();
Log.d("SmsReceiver", "onReceive: " + intent.getAction());
if(intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")){
//intent.getExtras()方法就是从过滤后的意图中获取携带的数据,
// 这里携带的是以“pdus”为key、短信内容为value的键值对
// android设备接收到的SMS是pdu形式的
Bundle bundle = intent.getExtras();
SmsMessage msg;
if (null != bundle){
//生成一个数组,将短信内容赋值进去
Object[] smsObg = (Object[]) bundle.get("pdus");
//遍历pdus数组,将每一次访问得到的数据方法object中
for (Object object:smsObg){
//获取短信
msg = SmsMessage.createFromPdu((byte[])object);
//获取短信内容
String content = msg.getDisplayMessageBody();
Log.d("SmsReceiver", "onReceive: content = " + content);
//获取短信发送方地址
String from = msg.getOriginatingAddress();
Log.d("SmsReceiver", "onReceive: from = " + from);
// TODO ...
}
}
}
}
}
使用方法:
// 使用广播进行监听
IntentFilter smsFilter = new IntentFilter();
smsFilter.addAction("android.provider.Telephony.SMS_RECEIVED");
smsFilter.addAction("android.provider.Telephony.SMS_DELIVER");
if (smsReceiver == null) {
smsReceiver = new SmsReceiver();
}
smsReceiver.setCallbackContext(callbackContext);
context.registerReceiver(smsReceiver, smsFilter);
接触监听,最好在收到短信的时候就取消注册广播:
context.unregisterReceiver(smsReceiver);
本来小米荣耀都搞定了,给测试一测,结果又不行了。收不到广播,用下面的ContentObserver还总拿不到对的数据。找了下资料,发现OPPO手机需要在短信APP进行设置。
ps. 后面发现华为、荣耀都是这样,会对验证码进行保护。可以使用ContentObserver 监听,能触发onChange,但是拿不到Uri,不过可以使用查询,拿到倒叙的第一条数据,取出其中的date属性,比对监听时的时间,如果短信两分钟有效,那就看看第一条数据是不是在两分钟内,如果不是,那就是没拿到,问题就出在用户开启了短信验证码保护,可以提示用户自行输入验证码(毕竟这个不是我们的锅)。
解决方法:
在短信 -> 短信设置里面 -> 打开禁止后台应用读取验证码
既然上面的方法没用了,只能找新的办法喽,网上很多提供了两种办法,第二种就是通过ContentResolver去监听短信添加的更新动作,其实也和广播类似,代码如下:
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.provider.Telephony;
import android.util.Log;
import androidx.annotation.RequiresApi;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class ReadSmsObserver extends ContentObserver {
private final Context context;
public ReadSmsObserver(Handler handler, Context context) {
super(handler);
this.context = context;
}
private static final String SMS_INBOX_URI = "content://sms/inbox";//API level>=23,可直接使用Telephony.Sms.Inbox.CONTENT_URI,用于获取cusor
// private static final String SMS_URI = "content://sms";//API level>=23,可直接使用Telephony.Sms.CONTENT_URI,用于注册内容观察者
private static final String[] PROJECTION = new String[]{
Telephony.Sms._ID,
Telephony.Sms.ADDRESS,
Telephony.Sms.BODY,
Telephony.Sms.DATE
};
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange);
Log.d("ReadSmsObserver", "onChange: ");
// 当收到短信时调用一次,当短信显示到屏幕上时又调用一次,所以需要return掉一次调用
if(uri.toString().equals("content://sms/raw")){
return;
}
// 读取短信收件箱,只读取未读短信,即read=0,并按照默认排序
Cursor cursor = context.getContentResolver().query(Uri.parse(SMS_INBOX_URI), PROJECTION,
Telephony.Sms.READ + "=?", new String[]{"0"}, Telephony.Sms.Inbox.DEFAULT_SORT_ORDER);
if (cursor == null) return;
// 获取倒序的第一条短信
if (cursor.moveToFirst()) {
// 读取短信发送人
String address = cursor.getString(cursor.getColumnIndex(Telephony.Sms.ADDRESS));
Log.d("ReadSmsObserver", "onChange: address = " + address);
// 读取短息内容
String smsBody = cursor.getString(cursor.getColumnIndex(Telephony.Sms.BODY));
Log.d("ReadSmsObserver", "onChange: smsBody = " + smsBody);
// TODO 传递出去,最好切下线程
}
// 关闭cursor的方法
cursor.close();
}
}
用的时候要注册和取消注册:
// 使用ContentResolver进行监听
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (smsObserver == null) {
smsObserver = new ReadSmsObserver(new SmsHandler(), context);
}
smsObserver.setCallbackContext(callbackContext);
context.getContentResolver().registerContentObserver(
Uri.parse("content://sms/"), true, smsObserver);
}
取消注册:
context.getContentResolver().unregisterContentObserver(smsObserver);
很神奇啊,每次使用ContentObserver去监听短信变化,明明onChange触发了,但是去拿短信就是拿不到最新的,开了上面的设置也不行,弄了好久。
最后想的解决办法是,两种方式同时监听,在onChange触发后等待三秒钟(开始试了1s还不行),看看有没有onReceive,如果有就直接使用onReceive的短信,如果没有再验证onChange内拿到的短信,看看是不是有效时间内的,连倒叙第一个都在有效时间外,那就是没拿到了,直接舍弃了。
思路是这样,做起来不麻烦,勇哥handler就可以解决,读者自行处理吧。
这些机型的兼容性搞起来真头疼,上面两种方法可以兼容起来使用,收到一条短信后直接取消注册就行了。
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%
我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("
有没有办法在这个简单的get方法中添加超时选项?我正在使用法拉第3.3。Faraday.get(url)四处寻找,我只能先发起连接后应用超时选项,然后应用超时选项。或者有什么简单的方法?这就是我现在正在做的:conn=Faraday.newresponse=conn.getdo|req|req.urlurlreq.options.timeout=2#2secondsend 最佳答案 试试这个:conn=Faraday.newdo|conn|conn.options.timeout=20endresponse=conn.get(url
我有一个驼峰式字符串,例如:JustAString。我想按照以下规则形成长度为4的字符串:抓取所有大写字母;如果超过4个大写字母,只保留前4个;如果少于4个大写字母,则将最后大写字母后的字母大写并添加字母,直到长度变为4。以下是可能发生的3种情况:ThisIsMyString将产生TIMS(大写字母);ThisIsOneVeryLongString将产生TIOV(前4个大写字母);MyString将生成MSTR(大写字母+tr大写)。我设法用这个片段解决了前两种情况:str.scan(/[A-Z]/).first(4).join但是,我不太确定如何最好地修改上面的代码片段以处理最后一种
我有一个存储主机名的Ruby数组server_names。如果我打印出来,它看起来像这样:["hostname.abc.com","hostname2.abc.com","hostname3.abc.com"]相当标准。我想要做的是获取这些服务器的IP(可能将它们存储在另一个变量中)。看起来IPSocket类可以做到这一点,但我不确定如何使用IPSocket类遍历它。如果它只是尝试像这样打印出IP:server_names.eachdo|name|IPSocket::getaddress(name)pnameend它提示我没有提供服务器名称。这是语法问题还是我没有正确使用类?输出:ge
我想获取模块中定义的所有常量的值:moduleLettersA='apple'.freezeB='boy'.freezeendconstants给了我常量的名字:Letters.constants(false)#=>[:A,:B]如何获取它们的值的数组,即["apple","boy"]? 最佳答案 为了做到这一点,请使用mapLetters.constants(false).map&Letters.method(:const_get)这将返回["a","b"]第二种方式:Letters.constants(false).map{|c
我安装了ruby版本管理器,并将RVM安装的ruby实现设置为默认值,这样'哪个ruby'显示'~/.rvm/ruby-1.8.6-p383/bin/ruby'但是当我在emacs中打开inf-ruby缓冲区时,它使用安装在/usr/bin中的ruby。有没有办法让emacs像shell一样尊重ruby的路径?谢谢! 最佳答案 我创建了一个emacs扩展来将rvm集成到emacs中。如果您有兴趣,可以在这里获取:http://github.com/senny/rvm.el
假设我有这个范围:("aaaaa".."zzzzz")如何在不事先/每次生成整个项目的情况下从范围中获取第N个项目? 最佳答案 一种快速简便的方法:("aaaaa".."zzzzz").first(42).last#==>"aaabp"如果出于某种原因你不得不一遍又一遍地这样做,或者如果你需要避免为前N个元素构建中间数组,你可以这样写:moduleEnumerabledefskip(n)returnto_enum:skip,nunlessblock_given?each_with_indexdo|item,index|yieldit
我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur