Android 手机朗读短信(文本)消息给您
使用 Android TextToSpeech API,我们可以编写一个简单的应用程序,将您的短信(文本)消息大声朗读给您听。
- 下载所有源代码 v2 (所有平台 - KitKat, Lollipop, Marshmallow, Nougat) - 98.2 KB
- 下载源代码 (原始 v1 4.3 - 5.1(lollipop)) - 98 KB
引言
我写了这篇文章中解释的应用程序,因为它是我自己想要的。我经常在下班回家之前快速给妻子发条短信。我通常不会注意到她的回复,因为我已经在开车了,而且我非常自律,开车时不会看手机。这常常导致我错过她让我顺路去商店买东西的短信。现在不会了。
观看这个 youtube 视频(仅 12 秒)以了解该应用程序(Vext)的实际运行情况 -- 在我的手机上运行(点击图片或链接)。
现在我只需启动我的 Android 应用程序,我的手机就会使用 TextToSpeech API 在开车时朗读她的(或任何 incoming SMS)消息给我听。
更新 - 已修复权限,可在所有版本上运行 (KitKat 至 Nougat)
我已经修复了 Marshmallow 和 Nougat 上短信权限的问题,现在此代码可以在所有版本上运行。获取上面的 v2 版本,并可以在任何支持 TextToSpeech 的版本上运行。有关添加代码的详细信息,请参阅下面的修复权限部分。
警告 - 请注意:Marshmallow
、Nougat
用户 -- 此时我很高兴发布这篇文章,所以我将发布适用于Android 4.3 - Lollipop (5.1.1) 的版本。 在 Lollipop 之后,Android 增加了更多短信安全措施,所以我需要添加代码来获取短信消息,并在第一次运行时提醒用户。 目前该代码在这些平台上将无法正常工作。 我将很快更新。现在,希望您能阅读,也许等您读完时,我就会修好它(一两天内 -- 目标是 2017 年 4 月 27 日)。
第一个版本很难看(基本 UI)
第一个版本不好看,因为我只想要功能,并不关心创建漂亮的 UI。 这是一个基本的空 Android 窗体(Activity),我添加了一些基本控件。 下图显示了一个 EditText 和一个按钮,您可以输入任何您想要的文本,应用程序就会为您朗读该文本。
我将该应用程序命名为 Vext("Voice-text" 的 混合词)。
派对上的乐趣
您可以在 EditText 控件中输入任何您想要的文本,按下 [Speak] 按钮,应用程序就会用设备默认的语音为您朗读。 您将成为您参加的每个派对的焦点。 :)
观看这个非常短的视频,看看应用程序如何朗读一小段话。
https://youtu.be/D5X3z7miXjQ (在新标签页中打开)
Activity 中更靠下的 SeekBar 控件(在 Windows 世界中也称为滑块)允许您设置语音的音量。 当应用程序首次启动时,它会调用 MainActivity 上的以下方法来获取最大音量。
int maxVol = am.getStreamMaxVolume(am.STREAM_MUSIC);
第一次运行时,它将默认为 3。 设置值后,我会将其存储在 SharedPreferences 中,以便每次启动应用程序时都能记住您想要的音量。
我们将在本文的后续内容中详细介绍这些细节。 以下是我们将在本文中涵盖的概念。
我们将涵盖的概念
- 接收短信,获取短信正文
- 使用 TextToSpeech API
- 在 SharedPreferences 中设置和保存 TextToSpeech 音量
调用该功能
目前,运行该应用程序是开启和关闭该功能的方式。 也就是说,启动应用程序后,以下收到的消息将被朗读给您。 停止应用程序后,您的短信将不再被朗读。
Android 小部件
将此应用程序作为 Android 小部件(可在主屏幕和锁定屏幕上使用)会很不错,我已经在开发中了。但是,Android 小部件 API 非常脆弱,难以使用。事情肯定可以更轻松。 在我尝试实现它的过程中,我遇到了无法解释的错误。 在修复这些问题之前,我们将只能满足于这个基本应用程序。
前面的介绍有点长,让我们开始看代码。 我们将首先查看 MainActivity 代码,一切都从那里开始。
MainActivity onCreate()
当 Android 应用程序启动时,MainActivity 会被加载,并调用 onCreate()
函数。 该方法为我们在应用程序启动时进行一些初始化提供了机会。 我不会展示所有 UI 组件的初始化,以便专注于本文的主题。 在 onCreate() 中我们看到的第一个有趣的用于我们的代码是
AudioManager am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
int maxVol = am.getStreamMaxVolume(am.STREAM_MUSIC);
Log.d("MainActivity", "max vol : " + String.valueOf(maxVol));
volumeSeekBar.setMax(maxVol);
设置 UI 组件(SeekBar)
此代码允许我们使用 AudioManager
获取系统音频服务的最大音量级别。 稍后,我们将使用 AudioManager 的 setStreamVolume()
方法来设置 TextToSpeech 语音的音量。 此代码部分获取可能的最大音量,以便我们设置 SeekBar(可以理解为滑块)的最大值。 通常此值为 13 到 15 左右。 这确保了用户只能使用 SeekBar 将音量设置为有效值。
设置当前音量
下一行调用了我编写的一个方法,该方法设置当前音量值。
setCurrentVolumeLevel();
该方法位于 MainActivity.java 文件的稍下方,看起来像这样:
private void setCurrentVolumeLevel() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
int volumeLevel = 0;
volumeLevel = preferences.getInt("volumeLevel", 3);
volumeSeekBar.setProgress(volumeLevel);
}
使用 SharedPreferences 简单方便
此方法向您展示了如何从 Android SharedPreferences
读取数据。 这些值只能被此应用程序读取和写入,因此它们被认为是安全的。 当然,我们只是保存和检索当前音量值,所以这无关紧要。 使用 SharedPreferences 的好处是,我们的值将在每次运行应用程序时被设置和记住。
一旦我们获得了从 PreferenceManager
获取的 preferences
对象,我们就可以查询它是否有已保存的值。 在本例中,我们正在查找一个名为 volumeLevel
的整数值。 我们使用 getInt()
方法,该方法接受我们正在查找的值的名称以及一个默认值(在本例中为 3),如果找不到该值则返回它。 第一次运行应用程序时,找不到该值,因此我们将值设置为 3。
更新 SeekBar UI
一旦我们获取了值(或默认值),我们就简单地调用 setProgress()
方法并传入该值,以确保 SeekBar UI 已使用正确的值更新。 该方法返回,我们回到 onCreate()
。
Intent 和广播
在 Android 世界中,每当您想要解析某些通用功能时,都可以使用 Intent 来实现。 例如,如果您想让用户从您的应用程序观看视频,很可能已经有一个视频服务可用。 这意味着您只需要正确设置 Intent 并请求该服务来处理操作。
对于 Vext,我决定使 TextToSpeech
功能可用的最简单方法是创建一个 BroadcastReceiver,我们可以在需要时通过 Intent 调用它。 这将使该功能在实现 Android 小部件 时更容易访问(稍后将详细介绍)。
正如我所说,您可以请求应用程序外部的功能,但在我们的情况下,我希望将服务保留在本地,并且我了解到通过创建一个继承 BroadcastReceiver
的类来实现这一点。 稍后,我们将看到这个新添加的类名为 TTSReceiver
。
在 MainActivity onCreate() 中,您可以看到我在此处初始化了一个名为 lbm
的成员变量,然后创建了一个新的 TTSReceiver
。
lbm = LocalBroadcastManager.getInstance(this);
ttsReceiver = new TTSReceiver();
这为 TextToSpeech 系统做好了准备,我们将使用它。 但要了解 TextToSpeech 在我们的应用程序中是如何实现的,让我们仔细看看 TTSReceiver 类。
public class TTSReceiver extends BroadcastReceiver {
private TextToSpeech tts;
private Set<Voice> allVoices;
private String messageBodyText;
private TextToSpeech.OnInitListener ttsListener;
@Override
public void onReceive(Context context, Intent intent) {
Log.d("MainActivity", "onReceive() fired!");
SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
int volumeLevel = 0;
volumeLevel = preferences.getInt("volumeLevel",3);
Log.d("MainActivity", "volumeLevel : " + String.valueOf(volumeLevel));
AudioManager am = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
am.setStreamVolume(am.STREAM_MUSIC, volumeLevel, 0);
messageBodyText = intent.getStringExtra("MESSAGE_BODY");
if (ttsListener == null) {
ttsListener = new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
// tts.speak(messageBodyText, TextToSpeech.QUEUE_ADD, null, "1st1");
}
};
}
if (tts == null) {
tts = new TextToSpeech(context, ttsListener);
}
tts.speak(messageBodyText, TextToSpeech.QUEUE_ADD, null, "2nd1");
}
}
这个类相当直接,我们很快就会回到 MainActivity 中通过 Intent 调用它的代码,以便您了解它是如何被调用的。 但是,首先让我们看看这里有什么。
BroadcastReceiver onRecieve()
当您的 BroadcastReceiver
被调用时,采取一些行动非常容易。 您只需覆盖 onReceive()
方法,操作系统就会在事件发生时通知您。 当您创建 Intent 并通过 LocalBroadcastManager
调用 BroadcastReceiver 时,将运行这段代码。 您可以查找有关这些基础知识的更多详细信息,但请允许我添加关于必须注册此类的信息,以便您了解系统如何找到此类。
AndroidManifest.xml
每当您添加 BroadcastReceiver 时,您还必须将该类注册到系统中,以便它知道如何找到该类。 您可以通过项目中的 AndroidManifest
来完成。 虽然我不喜欢给读者看太多代码,但我将在此处展示完整的 AndroidManifest,因为我们还需要允许一些 Android 权限,您最好现在就看到它们。 我还将加粗 TTSReceiver 定义的部分,以便您可以看到它。
AndroidManifest
的大部分是由 Android Studio 项目模板在您创建项目时生成的。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="us.raddev.vext">
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".MsgReceiver">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
<receiver android:name=".TTSReceiver">
<intent-filter>
<action android:name="us.raddev.vext.message" />
</intent-filter>
</receiver>
</application>
</manifest>
Android 权限
接收短信
如您所见,前两行加粗的文本是我们添加的权限。 第一个权限允许我们的应用程序接收短信。 这意味着用户在安装应用程序时会收到警告,告知应用程序将执行此操作。 如果用户愿意,她可以取消安装。
读取联系人信息
此应用程序还需要读取联系人信息,以便获取发送消息给您的用户的姓名。
如果您仔细看,您会看到我们的 MainActivity 也已在清单中注册(.MainActivity)。
TTSReceiver 注册一个 Action
您还可以看到 TTSReceiver 注册了一个名为 "us.raddev.text.message" 的 action。 这是我们将添加到 Intent 中的 action,以便在广播时触发该 action。
当 action 触发时,将运行 onReceive()
方法,我们可以运行我们的代码。
当 onReceive() 方法事件发生时,我们执行以下操作:
从 SharedPreferences 读取音量级别
SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); int volumeLevel = 0;
volumeLevel = preferences.getInt("volumeLevel",3);
我们将音量级别存储在一个变量中,以便用它来设置 TextToSpeech
引擎将使用的音量级别。
设置 AudioManager 音量级别
AudioManager am = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
am.setStreamVolume(am.STREAM_MUSIC, volumeLevel, 0);
读取传入的字符串:MESSAGE_BODY
messageBodyText = intent.getStringExtra("MESSAGE_BODY");
当我们在 MainActivity 的 onCreate() 中创建 Intent 时(稍后将展示代码),我们在 Intent 上设置了一个字符串,我们将用 "MESSAGE_BODY" 这个名称来引用它。 在这里,我们获取该文本并将其保存到名为 messageBodyText 的成员变量中。
这是什么文本?
这通常是收到的文本消息的正文。 但是,由于我们将此 BroadcastReceiver 用于任何 TextToSpeech 调用,因此它是我们希望被朗读的任何文本。
最后,我们准备初始化 TextToSpeech,以便它为我们朗读文本。
初始化 TextToSpeech
if (ttsListener == null) {
ttsListener = new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
// tts.speak(messageBodyText, TextToSpeech.QUEUE_ADD, null, "1st1");
}
};
}
if (tts == null) {
tts = new TextToSpeech(context, ttsListener);
}
您可以看到,我们必须初始化 TTSListener
以便使用它来初始化将实际朗读文本的 TextToSpeech
对象。 我将这些对象存储在成员变量中,以便 TTS 在应用程序生命周期内都可用。 但是,我遇到了一些奇怪的问题。
奇怪的事情,重复或不朗读
我在初始化 TextToSpeech 时遇到了一些问题。我发现如果在上面所示的 onInit() 方法中(现在已注释掉,因为它不应被使用)进行第一次调用,TextToSpeech 会重复短语。 这非常奇怪,而且具有挑战性,因为当我没有这样做时,TextToSpeech 就不会朗读我的任何文本。
变通方法,并且奏效
我最终通过一个请求朗读空字符串的初始调用来初始化 TTS,从而解决了这个问题。 是的,这似乎很荒谬,但我了解到其他人也有奇怪的重复问题。 阅读这个 StackOverflow 以获取更多信息:
TextToSpeech.OnInitListener.onInit(int) 被连续调用^
一切初始化完毕后,我们就可以调用 speak() 方法来朗读我们的文本了。
TextToSpeech Speak() 方法
tts.speak(messageBodyText, TextToSpeech.QUEUE_ADD, null, "2nd1");
一旦您拥有了这一切,做到这一点就非常容易了。 当然,您必须设置好 BroadcastReceiver,否则这将无法正常工作。 speak()
方法需要三个参数,我们应该更仔细地看一下它们以进行理解。
- messageBodyText : TTS 将要朗读的文本
- TextToSpeech.QUEUE_ADD : 如何处理多次请求朗读的情况 - 在我们的例子中,我们告诉它将每个请求添加到队列中并在可能时朗读。 您也可以使用 QUEUE_FLUSH,它将刷新(取消)其他请求并只朗读最新的一个。
- String : 在我们的例子中,我添加了 "2nd1",这是无意义的。 这只是一个用于标识消息的字符串。
既然我们已经了解了 TTSReceiver(BroadcastReceiver)的工作原理,现在我们可以更好地理解如何调用它了。 我们使用一个空字符串进行第一次调用(如前所述),来自 MainActivity.onCreate()
方法。
这是我们如何创建 Intent 并调用 TTSReceiver 来朗读文本:
Intent intent = new Intent("us.raddev.vext.message");
intent.putExtra(MESSAGE_BODY,"" );
lbm.sendBroadcast(intent);
当我们创建 Intent 时,我们提供了我们想要执行的操作的名称。 请记住,".message" 是任意选择的。 如果我愿意,它也可以是 "us.raddev.vext.elephant"。 :)
接下来,我们将我们的 Extra(字符串)添加到 Intent 中,并将其命名为 "MESSAGE_BODY"。 在这种情况下,我们添加的字符串是空字符串,所以没什么特别的。
广播我们的 Intent
最后,我们调用我们的 LocalBroadcastManager
并调用 sendBroadcast 方法和我们的 intent
。 当我们这样做时,系统会找到匹配的类(TTSReceiver),然后运行 onReceive()
方法。 就是这么简单。
您可以看到与 speak 按钮的 onclick 监听器附加了非常相似的代码。 这样,当用户在 MainActivity 上键入文本并单击按钮时,她就会听到朗读。
speakButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
String outText = textToSpeak.getText().toString();
if (outText != null) {
Intent intent = new Intent("us.raddev.vext.message");
intent.putExtra(MESSAGE_BODY,outText );
lbm.sendBroadcast(intent);
Log.d("MainActivity", "Button click ended!");
}
}
});
我已将上面重要的代码加粗。 您可以看到,这里的主要区别在于我们从 textToSpeak EditText 控件获取文本,并在调用 Intent 的 putExtra 时使用它。
这就是我们在 TTSReceiver.onReceive()
方法中检索的字符串,以便 TextToSpeech.speak()
方法知道要朗读什么。
现在一切工作原理更清楚了
现在我们都明白了,理解当 SMS 文本消息到达时我们将做什么会更容易。 唯一的区别是,我们的应用程序将朗读 SMS 文本消息中的单词。 它是如何做到的? 方式相同。 唯一额外的事情是我们获取 SMS 的正文文本并将其传递给我们的 TTSReceiver,以便它为我们朗读。
更多 BroadcastReceiver,请
在 Android 上接收 SMS 并获取正文文本非常简单。 现在您理解了 BroadcastReceiver
,这会容易得多,因为接收 SMS 也是通过 BroadcastReceiver 完成的。 您甚至可以谷歌搜索如何做到这一点。 回顾上面的 AndroidManifest,查找文本 .MsgReceiver
,您将看到我们如何注册一个名为 MsgReceiver
的类,以便在收到 incoming SMS 时运行。
这是我们的 MsgReceiver
类的样子。 同样,一切都发生在 onReceive()
方法中。
public void onReceive(Context context, Intent intent) {
if (lbm == null && ttsReceiver == null) {
lbm = LocalBroadcastManager.getInstance(context);
ttsReceiver = new TTSReceiver();
lbm.registerReceiver(ttsReceiver,new IntentFilter("us.raddev.vext.message"));
}
SmsMessage message = null;
String from = null;
message = GetMessage(intent);
from = message.getOriginatingAddress();
if (message == null){
message = GetMessage(intent);
}
from = message.getOriginatingAddress();
String body = message.getMessageBody();
Toast.makeText(context,body, Toast.LENGTH_SHORT).show();
long receivedDate = message.getTimestampMillis();
Date date = new Date(receivedDate);
DateFormat formatter = new SimpleDateFormat("MM/dd/yyyy - HH:mm:ss");
String formattedDate = formatter.format(date);
///Resolving the contact name from the contacts.
Uri lookupUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
Uri.encode(from));
Cursor c = context.getContentResolver().query(lookupUri, new String[]
{ContactsContract.Data.DISPLAY_NAME},null,null,null);
try {
Intent ttsIntent = new Intent("us.raddev.vext.message");
boolean isSuccess = c.moveToFirst();
if (isSuccess) {
// got a contact name
String displayName = c.getString(0);
String ContactName = displayName;
Log.d("MainActivity", "ContactName : " + ContactName);
ttsIntent.putExtra(MESSAGE_BODY,"Incoming from " + ContactName );
}
else{
//doesn't have name in contacts
ttsIntent.putExtra(MESSAGE_BODY,"Incoming message");
}
lbm.sendBroadcast(ttsIntent);
lbm.unregisterReceiver(ttsReceiver);
}
catch (Exception e) {
// TODO: handle exception
}finally{
c.close();
}
Intent ttsIntent = new Intent("us.raddev.vext.message");
ttsIntent.putExtra(MESSAGE_BODY,body );
lbm.sendBroadcast(ttsIntent);
lbm.unregisterReceiver(ttsReceiver);
Log.d("MainActivity", "from : " + from);
Log.d("MainActivity", "body : " + body);
Log.d("MainActivity", "receivedDate : " + formattedDate);
}
您可能只需阅读该代码和一些注释就能弄清楚一切都在做什么。
但是,请允许我给您一个摘要,说明它做了什么。
SMS MsgReceiver 摘要
- 检查成员变量以查看它们是否已初始化(或这是第一次运行)。
- 调用名为
GetMessage()
的本地方法,该方法获取实际的 SMS 消息对象。 此方法只是包装了一些内容,因为 Android SMS 已随版本变化。 - 从 SMS 消息对象中获取一些我们想要使用的信息(
originatingAddress
、textBody
)。 - 通过
originatingAddress
(from
)获取联系人(如果存在)。 - 如果联系人存在,则请求 TTSReceiver 宣布 "Incoming from <contactname>"。
- 如果联系人不存在,则请求 TTSReceiver 宣布 "incoming message"。
- 请求
TTSReceiver
朗读文本正文。
就是这样。
就是这么简单。 下载代码并在您的 Android 手机上试用。 我认为您会非常喜欢它。
修复权限
之前,我在 Marshmallow (6.x) 及更高版本上请求权限时遇到了问题。 我已经研究了如何做到这一点,添加的代码不多。
应用程序首次运行
您将此代码添加到您的 MainActivity
中,以便当用户第一次运行应用程序时,她会收到通知,告知应用程序正在请求权限,并且她需要响应以允许这些权限。 同样,这只会发生如果应用程序运行在 Marshmallow 或更高版本上。 对于 Lollipop 或更低版本,权限将以常规方式处理。
以下是用户将看到的。 在我们的例子中,我们需要两个权限(RECEIVE_SMS 和 READ_CONTACTS),因此用户会被查询两次,一次查询一个权限。
拒绝任何一个,Vext 将无法工作
如果用户拒绝任何一个,Vext 应用程序将无法工作。 通过将联系人读取移出 MsgReceiver 类可以解决这个问题,但我现在不打算担心它。 现在是全有或全无。 如果用户在未允许权限的情况下运行应用程序,并且收到了 SMS 文本,则应用程序将直接崩溃。 没有关于权限未设置的警告。 这在开发 Android 应用程序时可能会有点令人困惑,所以请牢记这一点。
这是我添加到 MainActivity 以处理权限的代码。
权限方法
我添加了一个新方法,我将其命名为 requestAllPermissions()
,并在 MainActivity.onCreate()
方法的顶部添加了一个调用,以确保在用户第一次运行应用程序时对其进行查询。
这是整个方法,非常直接:
private void requestAllPermissions() {
String permission = Manifest.permission.RECEIVE_SMS;
String permission2 = Manifest.permission.READ_CONTACTS;
int grant = ContextCompat.checkSelfPermission(this, permission);
if ( grant != PackageManager.PERMISSION_GRANTED) {
String[] permission_list = new String[2];
permission_list[0] = permission;
permission_list[1] = permission2;
ActivityCompat.requestPermissions(this, permission_list, 2);
}
}
您只需设置一个权限字符串数组,然后将其添加到 ActivityCompat.requestPermissions() 方法中,并调用该方法。 一旦这段代码运行并通过用户接受,应用程序就为这些权限设置好了。
就是这么简单。
历史
文章和代码的第二版:2017 年 4 月 25 日添加了代码,以在 Android Marshmallow 及更高版本上正确处理权限。
文章和代码的第一版 : 04/25/2017