使用短信验证设备的电话号码

26

我想通过让Android设备发送短信到自己,并自动检查是否已接收该短信来验证设备的电话号码。我应该如何做?

1个回答

68

首先,这将需要两个权限;一个用于发送短信,另一个用于接收短信。以下内容应该放在您的AndroidManifest.xml文件中,在<manifest>标签之间,但位于<application>标签之外。

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

这两个权限都属于危险权限,因此如果您的应用程序在Marshmallow(API级别23)或更高版本上运行,并且具有23+的targetSdkVersion,则需要相应地处理它们。有关如何在运行时请求这些权限的信息可以在此开发人员页面找到。

您需要的Java类位于android.telephony包中;具体来说是android.telephony.SmsManagerandroid.telephony.SmsMessage。确保您已正确导入这两个类。

要发送外发短信,您将使用SmsManagersendTextMessage()方法,其签名如下:

sendTextMessage(String destinationAddress, String scAddress, String text,
                PendingIntent sentIntent, PendingIntent deliveryIntent)

在这个方法调用中只需要两个参数 - destinationAddresstext;第一个是电话号码,第二个是短信内容。其余参数可以传入null。例如:

String number = "1234567890";
String message = "Verification message.";
SmsManager sm = SmsManager.getDefault();
sm.sendTextMessage(number, null, message, null, null);
重点是保持消息文本相对较短,因为如果文本长度超过单条消息的字符限制,sendTextMessage()通常会悄悄失败。
要接收和读取传入消息,您需要使用"android.provider.Telephony.SMS_RECEIVED"操作注册一个带有IntentFilterBroadcastReceiver。这个接收器可以在清单中静态注册,也可以在运行时动态注册到Context上。
  • 在清单中静态注册接收器类将允许你的应用程序在接收之前被杀死时接收传入的消息。然而,要将结果放在所需位置可能需要一些额外的工作。在<application>标签之间:

    <receiver
        android:name=".SmsReceiver"
        android:enabled="false">
        <intent-filter>
            <action android:name="android.provider.Telephony.SMS_RECEIVED" />
        </intent-filter>
    </receiver>
    

    PackageManager#setComponentEnabledSetting()方法可以用于需要时启用和禁用这个<receiver>

    Context上动态注册接收器实例可能在代码方面更容易管理,因为接收器类可以被设置为注册它的任何组件的内部类,因此可以直接访问该组件的成员。但是,这种方法可能不像静态注册那样可靠,因为一些不同的事情可能会阻止接收器获取广播;例如,您的应用程序进程被杀死,用户导航到注册的Activity之外等。

    SmsReceiver receiver = new SmsReceiver();
    IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
    registerReceiver(receiver, filter);
    

    请务必在适当的时候取消注册接收器。


在接收器的onReceive()方法中,实际消息作为一个byte数组的数组附加到Intent作为额外信息。解码细节因Android版本而异,但结果是一个单独的SmsMessage对象,其中包含您需要的电话号码和消息。

class SmsReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        SmsMessage msg;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            SmsMessage[] msgs = Telephony.Sms.Intents.getMessagesFromIntent(intent);
            msg = msgs[0];
        } else {
            Object pdus[] = (Object[]) intent.getExtras().get("pdus");
            msg = SmsMessage.createFromPdu((byte[]) pdus[0]);
        }

        String number = msg.getOriginatingAddress();
        String message = msg.getMessageBody();
        ...
    }
}
在此时,您只需将此处的number与传递给sendTextMessage()调用的号码进行比较。建议使用PhoneNumberUtils.compare()进行比较,因为在接收器中检索到的号码可能与地址的号码格式不同。
注意:
  • 此处演示的示例正在使用一个单独的消息,因此消息文本应限制在相对较短的长度范围内。如果出于某种原因您确实想发送更长的消息,则可以改用sendMultipartTextMessage()方法。您需要先拆分文本,使用SmsManager#divideMessage()方法并将生成的ArrayList传递给该方法,而不是消息String。要在接收器中重新组装完整的消息,您需要将每个byte[]解码为SmsMessage,然后连接消息正文。
  • 自KitKat(API级别19)以来,如果您的应用程序不是默认的消息应用程序,则这里使用的消息将被系统和默认应用程序保存到SMS提供程序中,并且因此可供使用该提供程序的任何其他应用程序使用。你无法做太多关于它的事情,但如果你真的想避免它,那么这个同样的技巧可以用于数据短信,这不会触发默认应用程序,并且不会保存到提供程序中。
  • 为此,需要使用sendDataMessage()方法,该方法将需要一个额外的short参数用于(任意)端口号,并将消息作为byte[]而不是String传递。要进行过滤的操作是"android.intent.action.DATA_SMS_RECEIVED",并且过滤器将需要设置数据方案和权限(主机和端口)。在清单中,它看起来像:

    <intent-filter>
        <action android:name="android.intent.action.DATA_SMS_RECEIVED" /> 
        <data
            android:scheme="sms"
            android:host="localhost"
            android:port="1234" /> 
    </intent-filter>
    

    IntentFilter类中有相应的方法来动态地设置过滤规则。

    解码SmsMessage的方式与以前相同,但是可以使用getUserData()来获取消息的byte[],而不是getMessageBody()

  • 在KitKat之前,应用程序需要自己编写发送的短信,因此如果您不想记录任何信息,则可以在这些版本上停止执行该操作。

    可以拦截传入的消息,并在主短信应用程序接收和写入它们之前中止其广播。为此,将过滤器的优先级设置为最高,并在接收器中调用abortBroadcast()方法。在静态选项中,将android:priority="999"属性添加到开头的<intent-filter>标记中。动态地,IntentFilter#setPriority()方法也可以实现相同的效果。

    这并不完全可靠,因为另一个应用程序始终可能具有比您更高的优先级。

  • 在这些示例中,我忽略了使用广播者权限来保护接收器,部分原因是为了简单和清晰,另一部分原因是这种方式的性质不会让您暴露于任何可能造成损害的欺骗行为中。但是,如果您想包括这个功能,则只需要在静态选项的开头的<receiver>标记中添加android:permission="android.permission.BROADCAST_SMS"属性。对于动态选项,请使用registerReceiver()方法的四参数重载,将此权限String作为第三个参数传递,并将第四个参数设为null


这个答案也可以用于 Stackoverflow 上关于如何避免短信进入手机收件箱的问题。 - Mohsen Emami
android:enabled="false" 应该改为 true,而不是 false 吗? - Mohsen Emami
@MohsenEmami 这个例子假设您只想在验证时短暂启用接收器。否则,它将不必要地运行每个接收到的消息。正如我在下一句中提到的那样:“PackageManager#setComponentEnabledSetting()方法可用于根据需要启用和禁用此<receiver>。”如果您需要始终启用它,则可以删除该属性设置。 - Mike M.
1
我更喜欢阅读/撰写详细的答案,而这个答案中的细节是典范。它包含了你需要知道的一切,整个过程都有。非常感谢您,先生。 - Lalit Fauzdar

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