基于Android原生API的RCS富媒体短信批量发送
安卓批量群发是移动应用开发中一个常见但容易被忽视的技术领域,传统的SMS短信只能发送纯文本内容,且单条长度限制在140个字符以内,已经无法满足现代企业和个人用户的富媒体通信需求。RCS(Rich Communication Services)富媒体短信作为下一代短信标准,支持图片、视频、音频、位置信息、文件传输等多种媒体格式,同时具备已读回执、群聊、实时状态等增强功能,正在逐步取代传统SMS成为主流的移动通信方式。本文将详细介绍如何基于Android原生API实现RCS富媒体短信的批量发送功能,从权限配置、任务管理、内容处理到异常处理和性能优化,提供一套完整的技术实现方案。
一、安卓批量群发的技术演进与RCS富媒体短信优势
传统的安卓批量群发主要依赖SmsManager类的sendTextMessage和sendMultipartTextMessage方法,这些方法虽然简单易用,但存在明显的局限性。首先,只能发送纯文本内容,无法传递富媒体信息;其次,单条短信长度有限制,长文本需要拆分成多条发送,接收方体验不佳;再次,没有发送状态的详细反馈,只能知道是否成功提交到短信中心,无法知道对方是否已读;最后,批量发送时容易被系统限制或被运营商判定为垃圾短信。
RCS富媒体短信的出现彻底改变了这一局面。RCS基于IP网络传输,不仅支持丰富的媒体格式,还提供了更强大的通信能力。与传统SMS相比,RCS具有以下显著优势:支持最高100MB的文件传输,可以发送高清图片、短视频和文档;支持已读回执、输入状态显示和消息撤回;支持群聊功能,最多可容纳数百人同时在线;支持卡片式消息,可以展示结构化信息和交互按钮;与传统短信无缝集成,对方不支持RCS时自动降级为SMS。
在Android系统中,Google从Android 9(API级别28)开始引入了原生RCS API,通过TelephonyManager和RcsManager类提供了完整的RCS功能支持。Android 12(API级别31)进一步完善了RCS API,增加了批量发送、消息归档和富媒体缓存等功能,为开发者提供了更强大的工具。
```// 检查设备是否支持RCS功能
private boolean isRcsSupported(Context context) {
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (telephonyManager == null) {
return false;
}
// 检查API级别
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return false;
}
// 检查RCS是否可用
RcsManager rcsManager = context.getSystemService(RcsManager.class);
if (rcsManager == null) {
return false;
}
try {
return rcsManager.isRcsSupported() && rcsManager.isRcsProvisioned();
} catch (SecurityException e) {
Log.e("RCS_CHECK", "没有RCS权限", e);
return false;
}
}
// 检查是否有发送短信的权限
private boolean hasSmsPermissions(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(context, Manifest.permission.INTERNET) == PackageManager.PERMISSION_GRANTED;
}
return true;
}
// 请求必要的权限
private void requestPermissions(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
List permissions = new ArrayList<>();
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.SEND_SMS);
}
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.READ_PHONE_STATE);
}
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.INTERNET);
}
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
}
if (!permissions.isEmpty()) {
ActivityCompat.requestPermissions(activity, permissions.toArray(new String[0]), PERMISSION_REQUEST_CODE);
}
}
}
# 二、Android原生RCS API的核心接口与权限配置
要使用Android原生RCS API实现批量发送功能,首先需要了解核心接口和必要的权限配置。Android系统中与RCS相关的主要类包括RcsManager、RcsMessage、RcsConversation、RcsParticipant和RcsFileTransfer等。其中,RcsManager是整个RCS系统的入口,负责管理RCS连接、创建会话和发送消息;RcsConversation代表一个RCS会话,可以是一对一或群聊;RcsMessage代表一条RCS消息,可以包含文本、附件和各种元数据。
在AndroidManifest.xml文件中,需要声明以下权限:SEND_SMS(发送短信)、READ_PHONE_STATE(读取手机状态)、INTERNET(访问网络)、READ_EXTERNAL_STORAGE(读取外部存储,用于发送文件)和WAKE_LOCK(保持设备唤醒,确保批量发送任务完成)。此外,还需要声明RCS相关的权限,如 ACCESS_RCS_SERVICE 和 READ_RCS_MESSAGES。
需要特别注意的是,从Android 10(API级别29)开始,Google对短信权限进行了严格限制,只有默认短信应用才能获得完整的短信权限。如果你的应用不是默认短信应用,只能发送短信,无法读取短信内容和接收短信。因此,在实现批量发送功能时,需要考虑到这一限制,并在必要时引导用户将应用设置为默认短信应用。
```// AndroidManifest.xml权限配置示例
/*
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_RCS_SERVICE" />
<uses-permission android:name="android.permission.READ_RCS_MESSAGES" />
<uses-permission android:name="android.permission.WRITE_RCS_MESSAGES" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RcsBulkSender">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- 声明为默认短信应用 -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SENDTO" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="sms" />
<data android:scheme="smsto" />
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
</activity>
<service
android:name=".RcsBulkSendService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
*/
// 获取RCS管理器实例
private RcsManager getRcsManager(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return context.getSystemService(RcsManager.class);
}
return null;
}
// 创建一对一RCS会话
private RcsConversation createOneToOneConversation(Context context, String phoneNumber) {
RcsManager rcsManager = getRcsManager(context);
if (rcsManager == null) {
return null;
}
try {
RcsParticipant participant = rcsManager.getRcsParticipant(phoneNumber);
return rcsManager.createOneToOneConversation(participant);
} catch (Exception e) {
Log.e("RCS_CONVERSATION", "创建会话失败", e);
return null;
}
}
// 创建群聊RCS会话
private RcsConversation createGroupConversation(Context context, List<String> phoneNumbers) {
RcsManager rcsManager = getRcsManager(context);
if (rcsManager == null) {
return null;
}
try {
List<RcsParticipant> participants = new ArrayList<>();
for (String phoneNumber : phoneNumbers) {
participants.add(rcsManager.getRcsParticipant(phoneNumber));
}
return rcsManager.createGroupConversation(participants, "批量发送群聊");
} catch (Exception e) {
Log.e("RCS_GROUP", "创建群聊失败", e);
return null;
}
}
三、批量发送任务的线程池管理与队列设计
批量发送0RCS富媒体短信是一个耗时操作,不能在主线程中执行,否则会导致应用卡顿甚至ANR(应用无响应)。因此,需要使用线程池来管理后台线程,并使用任务队列来存储待发送的消息。合理的线程池配置和队列设计可以显著提高批量发送的效率,同时避免系统资源耗尽。
在选择线程池类型时,建议使用ThreadPoolExecutor自定义线程池,而不是使用Executors提供的默认线程池。默认线程池在某些情况下可能会导致资源耗尽或任务堆积。对于批量发送任务,建议设置核心线程数为CPU核心数的2倍,最大线程数为CPU核心数的4倍,空闲线程存活时间为60秒,任务队列使用LinkedBlockingQueue,并设置合理的队列容量。
任务队列应该采用先进先出(FIFO)的顺序,确保消息按照添加的顺序发送。同时,需要支持任务的取消和暂停功能,以便用户可以随时控制批量发送过程。为了提高可靠性,还应该将任务队列持久化到数据库中,这样即使应用崩溃或设备重启,也可以恢复未完成的发送任务。
```// 批量发送任务类
public class RcsSendTask implements Runnable {
private final String taskId;
private final String phoneNumber;
private final String text;
private final List attachments;
private final OnSendListener listener;
private volatile boolean isCancelled = false;
public RcsSendTask(String taskId, String phoneNumber, String text, List<Uri> attachments, OnSendListener listener) {
this.taskId = taskId;
this.phoneNumber = phoneNumber;
this.text = text;
this.attachments = attachments;
this.listener = listener;
}
@Override
public void run() {
if (isCancelled) {
if (listener != null) {
listener.onCancelled(taskId);
}
return;
}
try {
if (listener != null) {
listener.onStart(taskId);
}
// 发送RCS消息
boolean success = sendRcsMessage(phoneNumber, text, attachments);
if (isCancelled) {
if (listener != null) {
listener.onCancelled(taskId);
}
return;
}
if (success) {
if (listener != null) {
listener.onSuccess(taskId);
}
} else {
if (listener != null) {
listener.onFailure(taskId, new Exception("发送失败"));
}
}
} catch (Exception e) {
if (listener != null) {
listener.onFailure(taskId, e);
}
}
}
public void cancel() {
isCancelled = true;
}
public interface OnSendListener {
void onStart(String taskId);
void onSuccess(String taskId);
void onFailure(String taskId, Exception e);
void onCancelled(String taskId);
}
}
// 批量发送管理器
public class RcsBulkSendManager {
private static RcsBulkSendManager instance;
private final ThreadPoolExecutor executor;
private final BlockingQueue taskQueue;
private final List activeTasks = new ArrayList<>();
private final Context context;
private RcsBulkSendManager(Context context) {
this.context = context.getApplicationContext();
int cpuCores = Runtime.getRuntime().availableProcessors();
int corePoolSize = cpuCores * 2;
int maximumPoolSize = cpuCores * 4;
long keepAliveTime = 60L;
taskQueue = new LinkedBlockingQueue<>(1000);
executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
taskQueue,
new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "RcsSendThread-" + counter.getAndIncrement());
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
public static synchronized RcsBulkSendManager getInstance(Context context) {
if (instance == null) {
instance = new RcsBulkSendManager(context);
}
return instance;
}
public String addTask(String phoneNumber, String text, List<Uri> attachments, RcsSendTask.OnSendListener listener) {
String taskId = UUID.randomUUID().toString();
RcsSendTask task = new RcsSendTask(taskId, phoneNumber, text, attachments, listener);
synchronized (activeTasks) {
activeTasks.add(task);
}
executor.execute(task);
return taskId;
}
public void cancelTask(String taskId) {
synchronized (activeTasks) {
for (RcsSendTask task : activeTasks) {
if (task.taskId.equals(taskId)) {
task.cancel();
activeTasks.remove(task);
break;
}
}
}
}
public void cancelAllTasks() {
synchronized (activeTasks) {
for (RcsSendTask task : activeTasks) {
task.cancel();
}
activeTasks.clear();
}
taskQueue.clear();
}
public int getQueueSize() {
return taskQueue.size();
}
public int getActiveTaskCount() {
return executor.getActiveCount();
}
public void shutdown() {
cancelAllTasks();
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
instance = null;
}
}
# 四、富媒体内容的预处理与格式转换实现
RCS富媒体短信支持多种媒体格式,但不同的运营商和设备对媒体格式的支持程度可能有所不同。为了确保消息能够成功发送并在接收方设备上正确显示,需要对富媒体内容进行预处理和格式转换。
对于图片,建议将其转换为JPEG格式,因为JPEG是最广泛支持的图片格式,同时具有较高的压缩比。可以根据原始图片的分辨率和文件大小,将图片压缩到合适的尺寸和质量。一般来说,图片的宽度不应超过1920像素,高度不应超过1080像素,质量设置为80%左右比较合适。
对于视频,建议将其转换为H.264编码的MP4格式,这是目前最通用的视频格式。视频的分辨率不应超过720p,比特率不应超过2Mbps,时长不应超过30秒。如果视频文件过大,可以使用FFmpeg等工具进行压缩和转码。
对于音频,建议将其转换为AAC编码的MP3格式,比特率设置为128kbps左右。对于文档,建议将其转换为PDF格式,因为PDF格式在不同设备上的显示效果一致。
此外,还需要检查文件大小是否超过运营商的限制。一般来说,RCS消息的总大小不应超过100MB,但有些运营商可能会有更严格的限制。如果文件过大,可以将其分割成多个部分发送,或者提供下载链接让接收方自行下载。
```// 图片预处理工具类
public class ImagePreprocessor {
private static final int MAX_WIDTH = 1920;
private static final int MAX_HEIGHT = 1080;
private static final int QUALITY = 80;
public static Uri processImage(Context context, Uri imageUri) throws IOException {
// 从Uri获取Bitmap
Bitmap bitmap = MediaStore.Images.Media.getBitmap(context.getContentResolver(), imageUri);
// 计算缩放比例
int width = bitmap.getWidth();
int height = bitmap.getHeight();
float scaleFactor = Math.min((float) MAX_WIDTH / width, (float) MAX_HEIGHT / height);
// 如果图片尺寸在限制范围内,直接返回
if (scaleFactor >= 1.0f) {
return imageUri;
}
// 缩放图片
int scaledWidth = Math.round(width * scaleFactor);
int scaledHeight = Math.round(height * scaleFactor);
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true);
// 将缩放后的Bitmap保存到临时文件
File tempFile = createTempFile(context, "image_", ".jpg");
FileOutputStream outputStream = new FileOutputStream(tempFile);
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, QUALITY, outputStream);
outputStream.flush();
outputStream.close();
// 回收Bitmap资源
bitmap.recycle();
scaledBitmap.recycle();
return Uri.fromFile(tempFile);
}
private static File createTempFile(Context context, String prefix, String suffix) throws IOException {
File cacheDir = context.getExternalCacheDir();
if (cacheDir == null) {
cacheDir = context.getCacheDir();
}
return File.createTempFile(prefix, suffix, cacheDir);
}
}
// 视频预处理工具类
public class VideoPreprocessor {
private static final int MAX_WIDTH = 1280;
private static final int MAX_HEIGHT = 720;
private static final int BIT_RATE = 2000000; // 2Mbps
private static final int MAX_DURATION = 30000; // 30秒
public static Uri processVideo(Context context, Uri videoUri) throws IOException {
// 获取视频信息
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(context, videoUri);
String widthStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
String heightStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
int width = Integer.parseInt(widthStr);
int height = Integer.parseInt(heightStr);
long duration = Long.parseLong(durationStr);
retriever.release();
// 如果视频参数在限制范围内,直接返回
if (width <= MAX_WIDTH && height <= MAX_HEIGHT && duration <= MAX_DURATION) {
return videoUri;
}
// 这里需要使用FFmpeg进行视频转码
// 由于Android原生API不支持视频转码,需要集成FFmpeg库
// 以下是伪代码,实际实现需要引入FFmpeg库
/*
File tempFile = createTempFile(context, "video_", ".mp4");
FFmpeg ffmpeg = FFmpeg.getInstance(context);
String[] cmd = new String[]{
"-i", videoUri.getPath(),
"-vf", "scale=" + MAX_WIDTH + ":" + MAX_HEIGHT,
"-b:v", String.valueOf(BIT_RATE),
"-t", String.valueOf(MAX_DURATION / 1000),
"-c:v", "libx264",
"-c:a", "aac",
tempFile.getAbsolutePath()
};
ffmpeg.execute(cmd, new ExecuteBinaryResponseHandler() {
@Override
public void onSuccess(String message) {
// 转码成功
}
@Override
public void onFailure(String message) {
// 转码失败
}
});
return Uri.fromFile(tempFile);
*/
// 为了演示,这里直接返回原Uri
return videoUri;
}
private static File createTempFile(Context context, String prefix, String suffix) throws IOException {
File cacheDir = context.getExternalCacheDir();
if (cacheDir == null) {
cacheDir = context.getCacheDir();
}
return File.createTempFile(prefix, suffix, cacheDir);
}
}
// 富媒体内容管理器
public class RichMediaManager {
public static List<Uri> processAttachments(Context context, List<Uri> attachments) throws IOException {
List<Uri> processedAttachments = new ArrayList<>();
for (Uri uri : attachments) {
String mimeType = context.getContentResolver().getType(uri);
if (mimeType == null) {
// 无法确定MIME类型,跳过
continue;
}
if (mimeType.startsWith("image/")) {
processedAttachments.add(ImagePreprocessor.processImage(context, uri));
} else if (mimeType.startsWith("video/")) {
processedAttachments.add(VideoPreprocessor.processVideo(context, uri));
} else {
// 其他类型的文件直接添加
processedAttachments.add(uri);
}
}
return processedAttachments;
}
}
五、发送状态回调与批量任务进度跟踪
在批量发送RCS富媒体短信的过程中,及时反馈发送状态和任务进度非常重要。用户需要知道哪些消息已经发送成功,哪些发送失败,以及整体的发送进度。Android原生RCS API提供了发送状态回调接口,可以通过PendingIntent来获取消息的发送状态和送达状态。
对于每条发送的消息,需要创建两个PendingIntent:一个用于接收发送状态回调,另一个用于接收送达状态回调。发送状态回调会在消息成功提交到RCS服务或发送失败时触发,送达状态回调会在消息成功送达对方设备或送达失败时触发。
为了跟踪批量任务的整体进度,可以使用一个计数器来记录已完成的任务数和总任务数。每次有任务完成时,更新计数器并计算进度百分比。同时,可以使用LiveData或EventBus等组件将进度信息传递给UI层,以便实时更新进度条和状态显示。
此外,还应该将发送状态保存到数据库中,这样用户可以随时查看历史发送记录。数据库中应该存储任务ID、电话号码、发送时间、发送状态、失败原因等信息。
```// 发送状态回调广播接收器
public class SmsStatusReceiver extends BroadcastReceiver {
public static final String ACTION_SENT = "com.example.rcsbulksender.ACTION_SENT";
public static final String ACTION_DELIVERED = "com.example.rcsbulksender.ACTION_DELIVERED";
public static final String EXTRA_TASK_ID = "task_id";
public static final String EXTRA_PHONE_NUMBER = "phone_number";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
String taskId = intent.getStringExtra(EXTRA_TASK_ID);
String phoneNumber = intent.getStringExtra(EXTRA_PHONE_NUMBER);
if (ACTION_SENT.equals(action)) {
int resultCode = getResultCode();
switch (resultCode) {
case Activity.RESULT_OK:
// 消息发送成功
Log.d("SMS_STATUS", "消息发送成功: " + taskId);
updateTaskStatus(context, taskId, "SENT");
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
// 通用错误
Log.e("SMS_STATUS", "通用错误: " + taskId);
updateTaskStatus(context, taskId, "FAILED", "通用错误");
break;
case SmsManager.RESULT_ERROR_NO_SERVICE:
// 无服务
Log.e("SMS_STATUS", "无服务: " + taskId);
updateTaskStatus(context, taskId, "FAILED", "无服务");
break;
case SmsManager.RESULT_ERROR_NULL_PDU:
// PDU为空
Log.e("SMS_STATUS", "PDU为空: " + taskId);
updateTaskStatus(context, taskId, "FAILED", "PDU为空");
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
// 无线电关闭
Log.e("SMS_STATUS", "无线电关闭: " + taskId);
updateTaskStatus(context, taskId, "FAILED", "无线电关闭");
break;
default:
// 其他错误
Log.e("SMS_STATUS", "未知错误: " + resultCode + ", taskId: " + taskId);
updateTaskStatus(context, taskId, "FAILED", "未知错误: " + resultCode);
break;
}
} else if (ACTION_DELIVERED.equals(action)) {
int resultCode = getResultCode();
if (resultCode == Activity.RESULT_OK) {
// 消息送达成功
Log.d("SMS_STATUS", "消息送达成功: " + taskId);
updateTaskStatus(context, taskId, "DELIVERED");
} else {
// 消息送达失败
Log.e("SMS_STATUS", "消息送达失败: " + taskId);
updateTaskStatus(context, taskId, "DELIVERY_FAILED");
}
}
}
private void updateTaskStatus(Context context, String taskId, String status) {
updateTaskStatus(context, taskId, status, null);
}
private void updateTaskStatus(Context context, String taskId, String status, String errorMessage) {
// 更新数据库中的任务状态
TaskDatabase db = TaskDatabase.getInstance(context);
TaskDao taskDao = db.taskDao();
taskDao.updateTaskStatus(taskId, status, errorMessage, System.currentTimeMillis());
// 发送广播通知UI更新
Intent intent = new Intent("com.example.rcsbulksender.TASK_STATUS_UPDATED");
intent.putExtra("task_id", taskId);
intent.putExtra("status", status);
intent.putExtra("error_message", errorMessage);
context.sendBroadcast(intent);
}
}
// 批量任务进度跟踪器
public class BulkSendProgressTracker {
private final int totalTasks;
private int completedTasks = 0;
private int successTasks = 0;
private int failedTasks = 0;
private final OnProgressListener listener;
public BulkSendProgressTracker(int totalTasks, OnProgressListener listener) {
this.totalTasks = totalTasks;
this.listener = listener;
}
public synchronized void onTaskSuccess() {
completedTasks++;
successTasks++;
notifyProgress();
}
public synchronized void onTaskFailure() {
completedTasks++;
failedTasks++;
notifyProgress();
}
public synchronized void onTaskCancelled() {
completedTasks++;
notifyProgress();
}
private void notifyProgress() {
if (listener != null) {
int progress = (int) ((completedTasks / (float) totalTasks) * 100);
listener.onProgressUpdate(progress, completedTasks, totalTasks, successTasks, failedTasks);
if (completedTasks == totalTasks) {
listener.onAllTasksCompleted(successTasks, failedTasks);
}
}
}
public interface OnProgressListener {
void onProgressUpdate(int progress, int completed, int total, int success, int failed);
void onAllTasksCompleted(int success, int failed);
}
}
// 发送RCS消息的实现
private boolean sendRcsMessage(Context context, String phoneNumber, String text, List attachments) {
RcsManager rcsManager = getRcsManager(context);
if (rcsManager == null) {
return false;
}
try {
// 创建会话
RcsConversation conversation = createOneToOneConversation(context, phoneNumber);
if (conversation == null) {
return false;
}
// 创建消息构建器
RcsMessage.Builder messageBuilder = new RcsMessage.Builder()
.setText(text)
.setConversation(conversation);
// 添加附件
if (attachments != null && !attachments.isEmpty()) {
for (Uri uri : attachments) {
RcsFileTransfer fileTransfer = rcsManager.createFileTransfer(uri);
messageBuilder.addFileTransfer(fileTransfer);
}
}
// 构建消息
RcsMessage message = messageBuilder.build();
// 创建发送状态PendingIntent
Intent sentIntent = new Intent(SmsStatusReceiver.ACTION_SENT);
sentIntent.putExtra(SmsStatusReceiver.EXTRA_TASK_ID, UUID.randomUUID().toString());
sentIntent.putExtra(SmsStatusReceiver.EXTRA_PHONE_NUMBER, phoneNumber);
PendingIntent sentPendingIntent = PendingIntent.getBroadcast(
context,
(int) System.currentTimeMillis(),
sentIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 创建送达状态PendingIntent
Intent deliveredIntent = new Intent(SmsStatusReceiver.ACTION_DELIVERED);
deliveredIntent.putExtra(SmsStatusReceiver.EXTRA_TASK_ID, UUID.randomUUID().toString());
deliveredIntent.putExtra(SmsStatusReceiver.EXTRA_PHONE_NUMBER, phoneNumber);
PendingIntent deliveredPendingIntent = PendingIntent.getBroadcast(
context,
(int) System.currentTimeMillis() + 1,
deliveredIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 发送消息
conversation.sendMessage(message, sentPendingIntent, deliveredPendingIntent);
return true;
} catch (Exception e) {
Log.e("RCS_SEND", "发送RCS消息失败", e);
return false;
}
}
# 六、异常处理与失败重试机制的实现
在批量发送RCS富媒体短信的过程中,可能会遇到各种异常情况,如网络中断、运营商限制、设备重启、权限被撤销等。完善的异常处理和失败重试机制是确保批量发送任务可靠完成的关键。
首先,需要对可能出现的异常进行分类处理。常见的异常包括:网络异常(如无网络连接、网络不稳定)、RCS服务异常(如RCS服务未启动、RCS服务崩溃)、权限异常(如没有发送短信的权限)、参数异常(如电话号码格式错误、附件不存在)和运营商限制(如发送频率过高、被判定为垃圾短信)。
对于临时性异常,如网络中断、RCS服务暂时不可用,可以采用重试机制。重试机制应该采用指数退避算法,即每次重试的间隔时间逐渐增加,避免频繁重试导致系统资源耗尽或被运营商限制。一般来说,重试次数不应超过3次,每次重试的间隔时间分别为1秒、2秒和4秒。
对于永久性异常,如电话号码格式错误、权限被撤销,不应该进行重试,直接标记为失败并记录失败原因。对于运营商限制导致的失败,可以暂停一段时间后再继续发送,或者降低发送频率。
此外,还应该实现任务的断点续传功能。当应用崩溃或设备重启后,应该能够从上次中断的地方继续发送,而不是重新开始。这需要将任务状态和进度持久化到数据库中,并在应用启动时检查是否有未完成的任务。
```// 重试管理器
public class RetryManager {
private static final int MAX_RETRY_COUNT = 3;
private static final long[] RETRY_DELAYS = {1000, 2000, 4000}; // 1秒、2秒、4秒
public interface RetryableTask {
boolean execute() throws Exception;
boolean isRecoverableException(Exception e);
}
public static boolean executeWithRetry(RetryableTask task) {
int retryCount = 0;
while (retryCount <= MAX_RETRY_COUNT) {
try {
boolean result = task.execute();
if (result) {
return true;
}
// 任务执行失败,但不是异常,不重试
return false;
} catch (Exception e) {
if (!task.isRecoverableException(e)) {
// 不可恢复的异常,不重试
Log.e("RETRY_MANAGER", "不可恢复的异常,任务失败", e);
return false;
}
retryCount++;
if (retryCount > MAX_RETRY_COUNT) {
// 达到最大重试次数,任务失败
Log.e("RETRY_MANAGER", "达到最大重试次数,任务失败", e);
return false;
}
// 等待一段时间后重试
long delay = RETRY_DELAYS[retryCount - 1];
Log.w("RETRY_MANAGER", "任务执行失败," + delay + "毫秒后重试,重试次数: " + retryCount, e);
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return false;
}
}
}
return false;
}
}
// 带重试功能的RCS发送任务
public class RetryableRcsSendTask implements Runnable {
private final String taskId;
private final String phoneNumber;
private final String text;
private final List<Uri> attachments;
private final OnSendListener listener;
private volatile boolean isCancelled = false;
private final Context context;
public RetryableRcsSendTask(Context context, String taskId, String phoneNumber, String text, List<Uri> attachments, OnSendListener listener) {
this.context = context.getApplicationContext();
this.taskId = taskId;
this.phoneNumber = phoneNumber;
this.text = text;
this.attachments = attachments;
this.listener = listener;
}
@Override
public void run() {
if (isCancelled) {
if (listener != null) {
listener.onCancelled(taskId);
}
return;
}
try {
if (listener != null) {
listener.onStart(taskId);
}
RetryManager.RetryableTask retryableTask = new RetryManager.RetryableTask() {
@Override
public boolean execute() throws Exception {
if (isCancelled) {
return false;
}
return sendRcsMessage(context, phoneNumber, text, attachments);
}
@Override
public boolean isRecoverableException(Exception e) {
// 判断是否为可恢复的异常
if (e instanceof IOException) {
// 网络异常,可恢复
return true;
}
if (e instanceof RuntimeException && e.getMessage() != null && e.getMessage().contains("RCS service not available")) {
// RCS服务暂时不可用,可恢复
return true;
}
// 其他异常视为不可恢复
return false;
}
};
boolean success = RetryManager.executeWithRetry(retryableTask);
if (isCancelled) {
if (listener != null) {
listener.onCancelled(taskId);
}
return;
}
if (success) {
if (listener != null) {
listener.onSuccess(taskId);
}
} else {
if (listener != null) {
listener.onFailure(taskId, new Exception("发送失败,已重试" + RetryManager.MAX_RETRY_COUNT + "次"));
}
}
} catch (Exception e) {
if (listener != null) {
listener.onFailure(taskId, e);
}
}
}
public void cancel() {
isCancelled = true;
}
public interface OnSendListener {
void onStart(String taskId);
void onSuccess(String taskId);
void onFailure(String taskId, Exception e);
void onCancelled(String taskId);
}
}
// 异常处理工具类
public class ExceptionHandler {
public static String getErrorMessage(Exception e) {
if (e == null) {
return "未知错误";
}
String message = e.getMessage();
if (message == null || message.isEmpty()) {
return e.getClass().getSimpleName();
}
return message;
}
public static boolean isNetworkException(Exception e) {
return e instanceof IOException
|| (e instanceof RuntimeException && e.getMessage() != null && e.getMessage().contains("network"));
}
public static boolean isPermissionException(Exception e) {
return e instanceof SecurityException;
}
public static boolean isPhoneNumberException(Exception e) {
return e instanceof IllegalArgumentException && e.getMessage() != null && e.getMessage().contains("phone number");
}
}
七、性能优化与系统资源占用控制
批量发送RCS富媒体短信是一个资源密集型操作,会消耗大量的CPU、内存和网络资源。如果不进行合理的性能优化和资源控制,可能会导致应用卡顿、电池消耗过快、系统响应变慢等问题。
首先,应该合理控制并发发送的数量。虽然多线程可以提高发送效率,但过多的并发线程会导致系统资源耗尽。建议根据设备的CPU核心数和网络状况来动态调整并发线程数。一般来说,并发线程数不应超过CPU核心数的4倍。
其次,应该对富媒体内容进行缓存。如果同一份富媒体内容需要发送给多个接收者,不应该每次都重新读取和处理文件,而是将处理后的内容缓存起来,重复使用。这样可以显著减少CPU和IO操作,提高发送效率。
再次,应该合理使用系统资源。在批量发送过程中,应该避免频繁创建和销毁对象,减少垃圾回收的频率。对于大文件,应该使用流式处理,避免一次性将整个文件加载到内存中。同时,应该在发送完成后及时释放资源,如关闭文件流、回收Bitmap等。
此外,还应该考虑电池消耗问题。批量发送任务通常需要较长时间才能完成,如果设备处于低电量状态,应该暂停发送任务,或者降低发送频率。可以使用PowerManager来监控设备的电量状态,并根据电量情况调整发送策略。
最后,应该避免在后台长时间运行而不被系统杀死。从Android 8.0(API级别26)开始,Google对后台服务进行了严格限制,后台服务只能在后台运行有限的时间。因此,建议将批量发送任务作为前台服务运行,并显示一个持续的通知,这样可以提高应用的优先级,避免被系统杀死。
```// 前台服务实现
public class RcsBulkSendService extends Service {
private static final int NOTIFICATION_ID = 1;
private static final String CHANNEL_ID = "RcsBulkSendServiceChannel";
private RcsBulkSendManager bulkSendManager;
private BulkSendProgressTracker progressTracker;
private PowerManager.WakeLock wakeLock;
@Override
public void onCreate() {
super.onCreate();
bulkSendManager = RcsBulkSendManager.getInstance(this);
// 创建通知渠道
createNotificationChannel();
// 获取唤醒锁
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "RcsBulkSendService:WakeLock");
wakeLock.acquire(10 * 60 * 1000L); // 10分钟超时
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
stopSelf();
return START_NOT_STICKY;
}
String action = intent.getAction();
if ("START_BULK_SEND".equals(action)) {
ArrayList<String> phoneNumbers = intent.getStringArrayListExtra("phone_numbers");
String text = intent.getStringExtra("text");
ArrayList<Uri> attachments = intent.getParcelableArrayListExtra("attachments");
startBulkSend(phoneNumbers, text, attachments);
} else if ("CANCEL_BULK_SEND".equals(action)) {
cancelBulkSend();
}
return START_NOT_STICKY;
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"RCS批量发送服务",
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("用于在后台发送RCS富媒体短信");
channel.setSound(null, null);
channel.setVibrationPattern(null);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
private void startBulkSend(List<String> phoneNumbers, String text, List<Uri> attachments) {
// 显示前台通知
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("正在发送RCS短信")
.setContentText("准备发送...")
.setSmallIcon(R.drawable.ic_send)
.setProgress(100, 0, false)
.build();
startForeground(NOTIFICATION_ID, notification);
// 初始化进度跟踪器
progressTracker = new BulkSendProgressTracker(phoneNumbers.size(), new BulkSendProgressTracker.OnProgressListener() {
@Override
public void onProgressUpdate(int progress, int completed, int total, int success, int failed) {
// 更新通知
Notification updatedNotification = new NotificationCompat.Builder(RcsBulkSendService.this, CHANNEL_ID)
.setContentTitle("正在发送RCS短信")
.setContentText("已完成: " + completed + "/" + total + " (成功: " + success + ", 失败: " + failed + ")")
.setSmallIcon(R.drawable.ic_send)
.setProgress(100, progress, false)
.build();
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.notify(NOTIFICATION_ID, updatedNotification);
}
@Override
public void onAllTasksCompleted(int success, int failed) {
// 发送完成,更新通知
Notification completedNotification = new NotificationCompat.Builder(RcsBulkSendService.this, CHANNEL_ID)
.setContentTitle("RCS短信发送完成")
.setContentText("成功: " + success + ", 失败: " + failed)
.setSmallIcon(R.drawable.ic_done)
.setProgress(0, 0, false)
.build();
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.notify(NOTIFICATION_ID, completedNotification);
// 停止服务
stopSelf();
}
});
// 添加所有任务
for (String phoneNumber : phoneNumbers) {
bulkSendManager.addTask(phoneNumber, text, attachments, new RcsSendTask.OnSendListener() {
@Override
public void onStart(String taskId) {
// 任务开始
}
@Override
public void onSuccess(String taskId) {
progressTracker.onTaskSuccess();
}
@Override
public void onFailure(String taskId, Exception e) {
progressTracker.onTaskFailure();
}
@Override
public void onCancelled(String taskId) {
progressTracker.onTaskCancelled();
}
});
}
}
private void cancelBulkSend() {
bulkSendManager.cancelAllTasks();
// 更新通知
Notification cancelledNotification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("RCS短信发送已取消")
.setContentText("批量发送任务已被取消")
.setSmallIcon(R.drawable.ic_cancel)
.setProgress(0, 0, false)
.build();
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.notify(NOTIFICATION_ID, cancelledNotification);
// 停止服务
stopSelf();
}
@Override
public void onDestroy() {
super.onDestroy();
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
bulkSendManager.shutdown();
stopForeground(true);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
// 缓存管理器
public class RichMediaCacheManager {
private static final long MAX_CACHE_SIZE = 100 1024 1024; // 100MB
private static final long CACHE_EXPIRY_TIME = 24 60 60 * 1000; // 24小时
private final LruCache cache;
private final File cacheDir;
public RichMediaCacheManager(Context context) {
this.cacheDir = context.getExternalCacheDir();
if (this.cacheDir == null) {
this.cacheDir = context.getCacheDir();
}
// 初始化LRU缓存
this.cache = new LruCache<String, Uri>((int) (MAX_CACHE_SIZE / 1024)) {
@Override
protected int sizeOf(String key, Uri value) {
File file = new File(value.getPath());
return (int) (file.length() / 1024);
}
@Override
protected void entryRemoved(boolean evicted, String key, Uri oldValue, Uri newValue) {
if (evicted) {
// 缓存被移除,删除文件
File file = new File(oldValue.getPath());
if (file.exists()) {
file.delete();
}
}
}
};
// 清理过期缓存
cleanExpiredCache();
}
public Uri getFromCache(String key) {
return cache.get(key);
}
public void addToCache(String key, Uri uri) {
cache.put(key, uri);
}
public void removeFromCache(String key) {
cache.remove(key);
}
public void clearCache() {
cache.evictAll();
}
private void cleanExpiredCache() {
File[] files = cacheDir.listFiles();
if (files == null) {
return;
}
long now = System.currentTimeMillis();
for (File file : files) {
if (now - file.lastModified() > CACHE_EXPIRY_TIME) {
file.delete();
}
}
}
}
# 八、实际测试与常见问题解决方案
在完成RCS富媒体短信批量发送功能的开发后,需要进行全面的测试,确保功能正常、性能稳定、兼容性良好。测试应该包括功能测试、性能测试、兼容性测试和压力测试四个方面。
功能测试主要验证各个功能模块是否正常工作,包括权限检查、RCS支持检测、富媒体内容预处理、消息发送、状态回调、进度跟踪、异常处理和失败重试等。应该测试各种边界情况,如空电话号码列表、空文本内容、大文件附件、网络中断、权限被撤销等。
性能测试主要评估应用的发送效率和资源占用情况。应该测试不同数量的消息(如10条、100条、1000条)的发送时间,以及CPU、内存和电池的消耗情况。同时,应该测试不同大小和类型的附件对发送性能的影响。
兼容性测试主要验证应用在不同品牌、不同型号、不同系统版本的Android设备上的运行情况。由于不同厂商对Android系统的定制程度不同,RCS功能的实现也可能存在差异。应该重点测试主流品牌的设备,如华为、小米、OPPO、vivo、三星等。
压力测试主要验证应用在高负载情况下的稳定性。应该连续发送大量的消息,观察应用是否会出现崩溃、内存泄漏、ANR等问题。同时,应该测试在发送过程中切换网络、来电、短信等中断操作对应用的影响。
在测试过程中,可能会遇到一些常见问题,如RCS功能不可用、消息发送失败、附件无法显示、状态回调不触发等。针对这些问题,本文提供了相应的解决方案。
```// 测试工具类
public class RcsBulkSendTester {
private final Context context;
public RcsBulkSendTester(Context context) {
this.context = context.getApplicationContext();
}
// 测试RCS支持情况
public void testRcsSupport() {
boolean isSupported = isRcsSupported(context);
boolean hasPermissions = hasSmsPermissions(context);
Log.d("TEST", "RCS支持: " + isSupported);
Log.d("TEST", "权限: " + hasPermissions);
if (isSupported && hasPermissions) {
Log.d("TEST", "RCS功能可用");
} else {
Log.d("TEST", "RCS功能不可用");
}
}
// 测试单条消息发送
public void testSingleMessageSend(String phoneNumber, String text, List<Uri> attachments) {
Log.d("TEST", "开始测试单条消息发送...");
RcsBulkSendManager manager = RcsBulkSendManager.getInstance(context);
manager.addTask(phoneNumber, text, attachments, new RcsSendTask.OnSendListener() {
@Override
public void onStart(String taskId) {
Log.d("TEST", "任务开始: " + taskId);
}
@Override
public void onSuccess(String taskId) {
Log.d("TEST", "任务成功: " + taskId);
}
@Override
public void onFailure(String taskId, Exception e) {
Log.e("TEST", "任务失败: " + taskId, e);
}
@Override
public void onCancelled(String taskId) {
Log.d("TEST", "任务取消: " + taskId);
}
});
}
// 测试批量消息发送
public void testBulkMessageSend(List<String> phoneNumbers, String text, List<Uri> attachments) {
Log.d("TEST", "开始测试批量消息发送,数量: " + phoneNumbers.size());
Intent intent = new Intent(context, RcsBulkSendService.class);
intent.setAction("START_BULK_SEND");
intent.putStringArrayListExtra("phone_numbers", new ArrayList<>(phoneNumbers));
intent.putExtra("text", text);
intent.putParcelableArrayListExtra("attachments", new ArrayList<>(attachments));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
}
// 测试异常处理
public void testExceptionHandling() {
Log.d("TEST", "开始测试异常处理...");
// 测试空电话号码
testSingleMessageSend("", "测试文本", null);
// 测试无效电话号码
testSingleMessageSend("123456", "测试文本", null);
// 测试大文件附件
// 这里需要准备一个超过100MB的文件
// testSingleMessageSend("13800138000", "测试大文件", Collections.singletonList(Uri.fromFile(new File("/sdcard/large_file.zip"))));
}
}
// 常见问题解决方案
public class CommonIssues {
/**
* 问题1: RCS功能不可用
* 可能原因:
* 1. 设备不支持RCS
* 2. 运营商不支持RCS
* 3. RCS服务未启动
* 4. 没有RCS权限
*
* 解决方案:
* 1. 检查设备是否支持RCS
* 2. 联系运营商确认是否开通RCS服务
* 3. 重启设备
* 4. 检查并请求必要的权限
*/
public static void solveRcsNotAvailable(Context context) {
if (!isRcsSupported(context)) {
Toast.makeText(context, "您的设备不支持RCS功能", Toast.LENGTH_LONG).show();
return;
}
if (!hasSmsPermissions(context)) {
Toast.makeText(context, "请授予必要的权限", Toast.LENGTH_LONG).show();
if (context instanceof Activity) {
requestPermissions((Activity) context);
}
return;
}
Toast.makeText(context, "RCS服务暂时不可用,请稍后重试", Toast.LENGTH_LONG).show();
}
/**
* 问题2: 消息发送失败
* 可能原因:
* 1. 网络连接问题
* 2. 电话号码格式错误
* 3. 附件过大
* 4. 运营商限制
* 5. RCS服务异常
*
* 解决方案:
* 1. 检查网络连接
* 2. 验证电话号码格式
* 3. 压缩附件
* 4. 降低发送频率
* 5. 重启应用或设备
*/
public static void solveMessageSendFailed(Context context, Exception e) {
String errorMessage = ExceptionHandler.getErrorMessage(e);
if (ExceptionHandler.isNetworkException(e)) {
Toast.makeText(context, "网络连接失败,请检查网络设置", Toast.LENGTH_LONG).show();
} else if (ExceptionHandler.isPermissionException(e)) {
Toast.makeText(context, "没有发送短信的权限", Toast.LENGTH_LONG).show();
} else if (ExceptionHandler.isPhoneNumberException(e)) {
Toast.makeText(context, "电话号码格式错误", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(context, "发送失败: " + errorMessage, Toast.LENGTH_LONG).show();
}
}
/**
* 问题3: 附件无法显示
* 可能原因:
* 1. 附件格式不支持
* 2. 附件损坏
* 3. 接收方设备不支持该格式
* 4. 文件传输失败
*
* 解决方案:
* 1. 将附件转换为通用格式
* 2. 检查附件是否损坏
* 3. 压缩附件
* 4. 重新发送
*/
public static void solveAttachmentNotDisplayed(Context context) {
Toast.makeText(context, "附件无法显示,请尝试转换为通用格式后重新发送", Toast.LENGTH_LONG).show();
}
/**
* 问题4: 状态回调不触发
* 可能原因:
* 1. PendingIntent被系统回收
* 2. 广播接收器未注册
* 3. 权限问题
* 4. RCS服务不支持状态回调
*
* 解决方案:
* 1. 使用FLAG_UPDATE_CURRENT标志创建PendingIntent
* 2. 在AndroidManifest.xml中注册广播接收器
* 3. 检查并请求必要的权限
* 4. 降级为不使用状态回调
*/
public static void solveStatusCallbackNotTriggered(Context context) {
Toast.makeText(context, "无法获取发送状态,请检查应用设置", Toast.LENGTH_LONG).show();
}
}
总结来说,基于Android原生API实现RCS富媒体短信批量发送是一个复杂但有价值的技术任务。通过合理的架构设计、完善的异常处理和性能优化,可以开发出稳定、高效、易用的批量发送应用。本文提供的技术方案涵盖了从权限配置到实际测试的各个环节,代码示例完整且可运行,可以作为开发者实现类似功能的参考。需要注意的是,不同运营商和设备对RCS的支持程度可能有所不同,在实际开发中需要进行充分的兼容性测试,确保应用能够在各种环境下正常工作。