如何在Android中以编程方式读取设备中的短信消息?

299

我想从设备中检索短信消息并将其展示出来。


@David Freitas 受信任的链接 +1 - Shahzad Imam
3
@DavidFreitas,这个链接无法打开,请问你能否分享最新的链接? - Khobaib
3
@Khobaib,像往常一样,互联网上的东西是瞬息万变的。我在archive.org上找到了一份副本http://stackoverflow.com/a/19966227/40961,感谢他们(我最近捐赠了一些钱来支持他们的运营)。但我们应该考虑将https://web.archive.org/web/20121022021217/http://mobdev.olin.edu/mobdevwiki/FrontPage/Tutorials/SMS%20Messaging页面的内容转换为Markdown语法,放在这个问题的答案中。可能需要一个小时的工作量。 - David d C e Freitas
13个回答

176

使用内容提供程序("content://sms/inbox")来读取收件箱中的短信。

// public static final String INBOX = "content://sms/inbox";
// public static final String SENT = "content://sms/sent";
// public static final String DRAFT = "content://sms/draft";
Cursor cursor = getContentResolver().query(Uri.parse("content://sms/inbox"), null, null, null, null);

if (cursor.moveToFirst()) { // must check the result to prevent exception
    do {
       String msgData = "";
       for(int idx=0;idx<cursor.getColumnCount();idx++)
       {
           msgData += " " + cursor.getColumnName(idx) + ":" + cursor.getString(idx);
       }
       // use msgData
    } while (cursor.moveToNext());
} else {
   // empty box, no SMS
}

请添加READ_SMS权限。


8
谢谢!你拼错了“getColumnName”,除此之外,它的效果很好。哦,如果有人要使用这个,请不要忘记添加android.permission.READ_SMS权限。 - qwerty
5
这是否也使用了@CommonsWare在他对被接受的答案的评论中指定的未记录的API? - Krishnabhadra
1
注意!不要像我一样错过了 moveToFirst - Oleksandr Pryimak
4
可以的。它使用未公开的“content://sms/inbox”内容提供程序。 - pm_labs
1
问题:使用这个是否授予开发者读取短信收件箱中每一条消息的权限? - Aditya M P
显示剩余4条评论

86
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        final String myPackageName = getPackageName();
        if (!Telephony.Sms.getDefaultSmsPackage(this).equals(myPackageName)) {

            Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT);
            intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, myPackageName);
            startActivityForResult(intent, 1);
        }else {
            List<Sms> lst = getAllSms();
        }
    }else {
        List<Sms> lst = getAllSms();
    }

将应用设置为默认的短信应用

    @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 1) {
    if (resultCode == RESULT_OK) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            final String myPackageName = getPackageName();
            if (Telephony.Sms.getDefaultSmsPackage(mActivity).equals(myPackageName)) {

                List<Sms> lst = getAllSms();
            }
        }
    }
}
}

获取短信的函数

public List<Sms> getAllSms() {
    List<Sms> lstSms = new ArrayList<Sms>();
    Sms objSms = new Sms();
    Uri message = Uri.parse("content://sms/");
    ContentResolver cr = mActivity.getContentResolver();

    Cursor c = cr.query(message, null, null, null, null);
    mActivity.startManagingCursor(c);
    int totalSMS = c.getCount();

    if (c.moveToFirst()) {
        for (int i = 0; i < totalSMS; i++) {

            objSms = new Sms();
            objSms.setId(c.getString(c.getColumnIndexOrThrow("_id")));
            objSms.setAddress(c.getString(c
                    .getColumnIndexOrThrow("address")));
            objSms.setMsg(c.getString(c.getColumnIndexOrThrow("body")));
            objSms.setReadState(c.getString(c.getColumnIndex("read")));
            objSms.setTime(c.getString(c.getColumnIndexOrThrow("date")));
            if (c.getString(c.getColumnIndexOrThrow("type")).contains("1")) {
                objSms.setFolderName("inbox");
            } else {
                objSms.setFolderName("sent");
            }

            lstSms.add(objSms);
            c.moveToNext();
        }
    }
    // else {
    // throw new RuntimeException("You have no SMS");
    // }
    c.close();

    return lstSms;
}

Sms类如下:

public class Sms{
private String _id;
private String _address;
private String _msg;
private String _readState; //"0" for have not read sms and "1" for have read sms
private String _time;
private String _folderName;

public String getId(){
return _id;
}
public String getAddress(){
return _address;
}
public String getMsg(){
return _msg;
}
public String getReadState(){
return _readState;
}
public String getTime(){
return _time;
}
public String getFolderName(){
return _folderName;
}


public void setId(String id){
_id = id;
}
public void setAddress(String address){
_address = address;
}
public void setMsg(String msg){
_msg = msg;
}
public void setReadState(String readState){
_readState = readState;
}
public void setTime(String time){
_time = time;
}
public void setFolderName(String folderName){
_folderName = folderName;
}

}

别忘了在你的AndroidManifest.xml文件中定义权限。

<uses-permission android:name="android.permission.READ_SMS" />

3
这是一段不错的代码。只有一个问题,时间是以毫秒为单位获取的。我认为最好将其转换为人类可读格式,例如 String receiveDayTime = Functions.dateFromMilisec(Long.valueOf(c.getColumnIndexOrThrow("date")), "hh:mm a MMM dd, yyyy"); - Bibaswann Bandyopadhyay
1
为什么要用getter和setter来制作所有东西的目的,我真的不明白为什么不直接使用关联数组或类,其元素可以直接访问。 - michnovka
1
@TomasNavara:检查这段代码以理解getter和setter的使用。http://pastebin.com/Nh8YXtyJ - Bugs Happen
mActivity未定义。这是什么意思? - dthree
你可以使用Context或ActivityName.this代替mActivity。 - Atif Mahmood
显示剩余2条评论

62

这是一个简单的过程。你可以在源码SMSPopup中看到一个很好的例子。

请查看以下方法:

SmsMmsMessage getSmsDetails(Context context, long ignoreThreadId, boolean unreadOnly)
long findMessageId(Context context, long threadId, long _timestamp, int messageType
void setMessageRead(Context context, long messageId, int messageType)
void deleteMessage(Context context, long messageId, long threadId, int messageType)

这是读取的方法:

SmsMmsMessage getSmsDetails(Context context,
                            long ignoreThreadId, boolean unreadOnly)
{
   String SMS_READ_COLUMN = "read";
   String WHERE_CONDITION = unreadOnly ? SMS_READ_COLUMN + " = 0" : null;
   String SORT_ORDER = "date DESC";
   int count = 0;
   // Log.v(WHERE_CONDITION);
   if (ignoreThreadId > 0) {
      // Log.v("Ignoring sms threadId = " + ignoreThreadId);
      WHERE_CONDITION += " AND thread_id != " + ignoreThreadId;
   }
   Cursor cursor = context.getContentResolver().query(
                      SMS_INBOX_CONTENT_URI,
                      new String[] { "_id", "thread_id", "address", "person", "date", "body" },
                      WHERE_CONDITION,
                      null,
                      SORT_ORDER);
   if (cursor != null) {
      try {
         count = cursor.getCount();
         if (count > 0) {
            cursor.moveToFirst();
            // String[] columns = cursor.getColumnNames();
            // for (int i=0; i<columns.length; i++) {
            // Log.v("columns " + i + ": " + columns[i] + ": " + cursor.getString(i));
            // }                                         
            long messageId = cursor.getLong(0);
            long threadId = cursor.getLong(1);
            String address = cursor.getString(2);
            long contactId = cursor.getLong(3);
            String contactId_string = String.valueOf(contactId);
            long timestamp = cursor.getLong(4);

            String body = cursor.getString(5);                             
            if (!unreadOnly) {
                count = 0;
            }

            SmsMmsMessage smsMessage = new SmsMmsMessage(context, address,
                          contactId_string, body, timestamp,
                          threadId, count, messageId, SmsMmsMessage.MESSAGE_TYPE_SMS);
            return smsMessage;
         }
      } finally {
         cursor.close();
      }
   }               
   return null;
}

50
这不是Android SDK的一部分。这段代码错误地假设所有设备都支持这个未经记录和不受支持的内容提供程序。Google已明确表示依赖此内容不是一个好主意:http://android-developers.blogspot.com/2010/05/be-careful-with-content-providers.html。 - CommonsWare
1
@Janusz:没有记录和支持的方法可以在所有设备上的所有短信客户端中运行。 - CommonsWare
9
@CommonsWare 那听起来很悲伤。也许只能继续使用这个API了。 - Janusz
@Omer 你有没有想法如何统计每个联系人的短信数量? - user868935
4
代码已经移动。搜索SmsPopupUtils.java会在Google代码中为我提供一个新链接。万一他们再次移动或彻底停止使用,这里有一个备用链接 - http://pastebin.com/iPt7MLyM - KalEl

40

从API 19开始,您可以利用Telephony Class进行操作;由于硬编码的值在每个设备上检索消息可能不起作用,因为内容提供程序Uri会随着设备和制造商而变化。

public void getAllSms(Context context) {

    ContentResolver cr = context.getContentResolver();
    Cursor c = cr.query(Telephony.Sms.CONTENT_URI, null, null, null, null);
    int totalSMS = 0;
    if (c != null) {
        totalSMS = c.getCount();
        if (c.moveToFirst()) {
            for (int j = 0; j < totalSMS; j++) {
                String smsDate = c.getString(c.getColumnIndexOrThrow(Telephony.Sms.DATE));
                String number = c.getString(c.getColumnIndexOrThrow(Telephony.Sms.ADDRESS));
                String body = c.getString(c.getColumnIndexOrThrow(Telephony.Sms.BODY));
                Date dateFormat= new Date(Long.valueOf(smsDate));
                String type;
                switch (Integer.parseInt(c.getString(c.getColumnIndexOrThrow(Telephony.Sms.TYPE)))) {
                    case Telephony.Sms.MESSAGE_TYPE_INBOX:
                        type = "inbox";
                        break;
                    case Telephony.Sms.MESSAGE_TYPE_SENT:
                        type = "sent";
                        break;
                    case Telephony.Sms.MESSAGE_TYPE_OUTBOX:
                        type = "outbox";
                        break;
                    default:
                        break;
                }


                c.moveToNext();
            }
        }

        c.close();

    } else {
        Toast.makeText(this, "No message to show!", Toast.LENGTH_SHORT).show();
    }
}

17
似乎是唯一一个不使用未记录的API和不引用第三方库的答案。 - Ishamael
2
不要忘记调用c.close(); - Cícero Moura
你好,我们可以假设这段代码在API19及以上的所有设备上都能正常工作吗? - Sardar Agabejli
1
如果我们使用硬编码值,比如“contenturi:sms”,它在每个设备上都不同,但是如果我们使用Telephony类,我们可以直接访问该设备的短信数据库的内容URI或路径。这是一个指向短信数据库的帮助类。 - Manoj Perumarath
使用这个Conversation类来读取https://developer.android.com/reference/android/provider/Telephony.Sms.Conversations,如果你找不到,可以将其作为一个问题发布,祝编码愉快 :) - Manoj Perumarath
显示剩余3条评论

25

这篇文章有点旧,但这里有另一个简单的解决方案,可用于获取Android中与SMS内容提供程序相关的数据:

使用这个库:https://github.com/EverythingMe/easy-content-providers

  • 获取所有SMS

TelephonyProvider telephonyProvider = new TelephonyProvider(context);
List<Sms> smses = telephonyProvider.getSms(Filter.ALL).getList();

每个SMS都有所有字段,因此您可以获取所需的任何信息:
地址、内容、接收日期、类型(收件箱、已发送、草稿等)、线程ID等等

  • 获取所有MMS

  • List<Mms> mmses = telephonyProvider.getMms(Filter.ALL).getList();
    
  • 收集所有的Thread

    List<Thread> threads = telephonyProvider.getThreads().getList();
    
  • 获取所有会话

  • List<Conversation> conversations = telephonyProvider.getConversations().getList();
    
    它适用于ListCursor,并且有一个样例应用程序可以看到它的外观和工作方式。
    实际上,它支持所有Android内容提供程序,如:联系人、通话记录、日历等。 全部选项的完整文档:https://github.com/EverythingMe/easy-content-providers/wiki/Android-providers 希望它也有所帮助 :)

    2
    源代码和GitHub上的示例非常有用。这是大多数常见提供商的良好封装/外观。谢谢。 - m3nda

    23

    目前已经有多种答案可供选择,但我认为它们都遗漏了这个问题的一个重要部分。在从内部数据库或其表中读取数据之前,我们必须了解数据存储方式,然后才能找到上述问题的解决方案:

    如何在Android中以编程方式读取设备中的短信?

    在Android中,短信表看起来像这样:

    enter image description here

    现在您可以从数据库中选择任何内容。在我们的情况下,我们只需要

    id、address和body

    在阅读短信时:

    1.请求权限。

    int REQUEST_PHONE_CALL = 1;
    
       if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_SMS) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_SMS}, REQUEST_PHONE_CALL);
            }
    
    或者
     <uses-permission android:name="android.permission.READ_SMS" />
    

    2.现在你的代码应该像这样

    // Create Inbox box URI
    Uri inboxURI = Uri.parse("content://sms/inbox");
    
    // List required columns
    String[] reqCols = new String[]{"_id", "address", "body"};
    
    // Get Content Resolver object, which will deal with Content Provider
    ContentResolver cr = getContentResolver();
    
    // Fetch Inbox SMS Message from Built-in Content Provider
    Cursor c = cr.query(inboxURI, reqCols, null, null, null);
    
    // Attached Cursor with adapter and display in listview
    adapter = new SimpleCursorAdapter(this, R.layout.a1_row, c,
            new String[]{"body", "address"}, new int[]{
            R.id.A1_txt_Msg, R.id.A1_txt_Number});
    lst.setAdapter(adapter);
    

    我希望这会有所帮助。 谢谢。


    21

    步骤 1:首先,我们需要在清单文件中添加权限,例如:

    <uses-permission android:name="android.permission.RECEIVE_SMS" android:protectionLevel="signature" />
    <uses-permission android:name="android.permission.READ_SMS" />
    

    步骤2:然后添加服务短信接收器类,用于接收短信。

    <receiver android:name="com.aquadeals.seller.services.SmsReceiver">
        <intent-filter>
            <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
        </intent-filter>
    </receiver>
    

    步骤三:添加运行时权限

    private boolean checkAndRequestPermissions()
    {
        int sms = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS);
    
        if (sms != PackageManager.PERMISSION_GRANTED)
        {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_SMS}, REQUEST_ID_MULTIPLE_PERMISSIONS);
            return false;
        }
        return true;
    }
    

    步骤 4:在您的应用程序中添加这些类并进行测试接口类

    public interface SmsListener {
       public void messageReceived(String messageText);
    }
    

    SmsReceiver.java

    public class SmsReceiver extends BroadcastReceiver {
        private static SmsListener mListener;
        public Pattern p = Pattern.compile("(|^)\\d{6}");
        @Override
        public void onReceive(Context context, Intent intent) {
            Bundle data  = intent.getExtras();
            Object[] pdus = (Object[]) data.get("pdus");
            for(int i=0;i<pdus.length;i++)
            {
                SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) pdus[i]);
                String sender = smsMessage.getDisplayOriginatingAddress();
                String phoneNumber = smsMessage.getDisplayOriginatingAddress();
                String senderNum = phoneNumber ;
                String messageBody = smsMessage.getMessageBody();
                try{
                    if(messageBody!=null){
                        Matcher m = p.matcher(messageBody);
                        if(m.find()) {
                            mListener.messageReceived(m.group(0));
                        }
                    }
                }
                catch(Exception e){}
            }
        }
        public static void bindListener(SmsListener listener) {
            mListener = listener; 
        }
    }
    

    这个模式是做什么的? - Mark Buikema
    那个(“com.aquadeals.seller.services.SmsReceiver”)是常用的服务名称吗? - m3nda
    我的应用需要定位权限,所以我已经包含了它。如果您不想要这个权限,那也没问题 @ZamSunk - Venkatesh
    @AnjaniMittal,发生了什么事情,你能告诉我吗? - Venkatesh
    1
    我正在尝试制作一个应用程序,即使应用程序已被关闭,也会向用户弹出短信内容。 - Anjani Mittal
    显示剩余6条评论

    7
    Google Play服务有两个API可用于简化基于短信的验证过程。 SMS Retriever API 提供了完全自动化的用户体验,无需用户手动输入验证码,也不需要任何额外的应用程序权限,应尽可能使用。但是,它要求您在消息正文中放置自定义哈希代码,因此您必须同时控制服务器端
    • 消息要求 - 11位哈希码,唯一标识您的应用程序
    • 发送者要求 - 无
    • 用户交互 - 无
    在Android应用中请求短信验证 在服务器上执行短信验证 SMS User Consent API 不需要自定义哈希代码,但需要用户批准您的应用程序请求访问包含验证码的消息。为了最小化向用户展示错误消息的可能性,“SMS用户同意”将过滤掉来自用户联系人列表中发件人的消息。
    • 消息要求 - 包含至少一个数字的4-10位字母数字代码
    • 发件人要求 - 发件人不能在用户的联系人列表中
    • 用户交互 - 一次轻点即可批准

    SMS用户同意API是Google Play服务的一部分。要使用它,您至少需要这些库的版本17.0.0

    implementation "com.google.android.gms:play-services-auth:17.0.0"
    implementation "com.google.android.gms:play-services-auth-api-phone:17.1.0"
    

    步骤1:开始监听短信消息

    SMS用户同意将监听包含一次性代码的传入短信消息,持续时间最长为五分钟。它不会查看在其启动之前发送的任何消息。如果您知道将发送一次性代码的电话号码,则可以指定senderPhoneNumber,否则null将匹配任何号码。

     smsRetriever.startSmsUserConsent(senderPhoneNumber /* or null */)
    

    第二步:请求读取消息的许可

    一旦您的应用程序收到包含一次性代码的消息,它将通过广播通知。此时,您没有获得读取消息的许可 - 而是给您一个Intent,您可以开始提示用户授权。在您的BroadcastReceiver内,使用extras中的Intent显示提示。 当您启动该意图时,它将提示用户允许读取单个消息。他们将看到他们将与您的应用程序共享的整个文本。

    val consentIntent = extras.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
    startActivityForResult(consentIntent, SMS_CONSENT_REQUEST)
    

    enter image description here

    步骤3:解析一次性代码并完成短信验证
    当用户点击“允许”时,就该读取信息了!在onActivityResult中,您可以从数据中获取完整的短信消息文本:
    val message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
    

    你需要解析短信并将一次性验证码传递给后端!

    包含至少一个数字的4-10位字母数字代码。您能解释一下这是什么意思吗?这是指整个消息的长度应为4-10个字符还是只是短信代码? - Zeeshan Shabbir
    谢谢你也。 - Levon Petrosyan
    这仅适用于OTP验证,对于读取手机内的所有其他消息,如所有短信等,有什么新的API吗?请告诉我。祝编码愉快! :) - Manoj Perumarath
    我们总是遇到超时错误。请帮我。 - Manikandan K

    4

    最简单的函数

    为了读取短信,我编写了一个返回Conversation对象的函数:

    class Conversation(val number: String, val message: List<Message>)
    class Message(val number: String, val body: String, val date: Date)
    
    fun getSmsConversation(context: Context, number: String? = null, completion: (conversations: List<Conversation>?) -> Unit) {
            val cursor = context.contentResolver.query(Telephony.Sms.CONTENT_URI, null, null, null, null)
    
            val numbers = ArrayList<String>()
            val messages = ArrayList<Message>()
            var results = ArrayList<Conversation>()
    
            while (cursor != null && cursor.moveToNext()) {
                val smsDate = cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms.DATE))
                val number = cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms.ADDRESS))
                val body = cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms.BODY))
    
                numbers.add(number)
                messages.add(Message(number, body, Date(smsDate.toLong())))
            }
    
            cursor?.close()
    
            numbers.forEach { number ->
                if (results.find { it.number == number } == null) {
                    val msg = messages.filter { it.number == number }
                    results.add(Conversation(number = number, message = msg))
                }
            }
    
            if (number != null) {
                results = results.filter { it.number == number } as ArrayList<Conversation>
            }
    
            completion(results)
        }
    

    使用:

    getSmsConversation(this){ conversations ->
        conversations.forEach { conversation ->
            println("Number: ${conversation.number}")
            println("Message One: ${conversation.message[0].body}")
            println("Message Two: ${conversation.message[1].body}")
        }
    }
    

    或者只获取特定号码的对话:

    getSmsConversation(this, "+33666494128"){ conversations ->
        conversations.forEach { conversation ->
            println("Number: ${conversation.number}")
            println("Message One: ${conversation.message[0].body}")
            println("Message Two: ${conversation.message[1].body}")
        }
    }
    

    3

    使用Kotlin读取短信的代码:

    1- 在AndroidManifest.xml中添加以下权限:

        <uses-permission android:name="android.permission.RECEIVE_SMS"/>
    

    2-创建一个广播接收器类:

    package utils.broadcastreceivers
    
    import android.content.BroadcastReceiver
    import android.content.Context
    import android.content.Intent
    import android.telephony.SmsMessage
    import android.util.Log
    
    class MySMSBroadCastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        var body = ""
        val bundle = intent?.extras
        val pdusArr = bundle!!.get("pdus") as Array<Any>
        var messages: Array<SmsMessage?>  = arrayOfNulls(pdusArr.size)
    
     // if SMSis Long and contain more than 1 Message we'll read all of them
        for (i in pdusArr.indices) {
            messages[i] = SmsMessage.createFromPdu(pdusArr[i] as ByteArray)
        }
          var MobileNumber: String? = messages[0]?.originatingAddress
           Log.i(TAG, "MobileNumber =$MobileNumber")         
           val bodyText = StringBuilder()
            for (i in messages.indices) {
                bodyText.append(messages[i]?.messageBody)
            }
            body = bodyText.toString()
            if (body.isNotEmpty()){
           // Do something, save SMS in DB or variable , static object or .... 
                           Log.i("Inside Receiver :" , "body =$body")
            }
        }
     }
    

    3-如果Android 6及以上版本,请获取短信权限:

       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && 
        ActivityCompat.checkSelfPermission(context!!,
                Manifest.permission.RECEIVE_SMS
            ) != PackageManager.PERMISSION_GRANTED
        ) { // Needs permission
    
                requestPermissions(arrayOf(Manifest.permission.RECEIVE_SMS),
                PERMISSIONS_REQUEST_READ_SMS
            )
    
        } else { // Permission has already been granted
    
        }
    

    4- 将此请求代码添加到Activity或片段中:

     companion object {
        const val PERMISSIONS_REQUEST_READ_SMS = 100
       }
    

    5- 覆盖检查权限请求结果函数:

     override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<out String>,
        grantResults: IntArray
    ) {
        when (requestCode) {
    
            PERMISSIONS_REQUEST_READ_SMS -> {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Log.i("BroadCastReceiver", "PERMISSIONS_REQUEST_READ_SMS Granted")
                } else {
                    //  toast("Permission must be granted  ")
                }
            }
        }
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接