blob: 5effaddea672900f3e6a990896da51d9a4defff9 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.telephony.cts;
import static androidx.test.InstrumentationRegistry.getContext;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.android.compatibility.common.util.BlockedNumberUtil.deleteBlockedNumber;
import static com.android.compatibility.common.util.BlockedNumberUtil.insertBlockedNumber;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeNoException;
import static org.junit.Assume.assumeTrue;
import android.Manifest;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.app.UiAutomation;
import android.app.role.RoleManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteCallback;
import android.os.SystemClock;
import android.provider.Telephony;
import android.telephony.SmsCbMessage;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.cdma.CdmaSmsCbProgramData;
import android.telephony.cts.util.DefaultSmsAppHelper;
import android.telephony.cts.util.TelephonyUtils;
import android.text.TextUtils;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import com.android.compatibility.common.util.ApiTest;
import com.android.compatibility.common.util.ShellIdentityUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* Tests for {@link android.telephony.SmsManager}.
*
* Structured so tests can be reused to test {@link android.telephony.gsm.SmsManager}
*/
public class SmsManagerTest {
private static final String TAG = "SmsManagerTest";
private static final String LONG_TEXT =
"This is a very long text. This text should be broken into three " +
"separate messages.This is a very long text. This text should be broken into " +
"three separate messages.This is a very long text. This text should be broken " +
"into three separate messages.This is a very long text. This text should be " +
"broken into three separate messages.";;
private static final String LONG_TEXT_WITH_32BIT_CHARS =
"Long dkkshsh jdjsusj kbsksbdf jfkhcu hhdiwoqiwyrygrvn?*?*!\";:'/,."
+ "__?9#9292736&4;\"$+$+((]\\[\\℅©℅™^®°¥°¥=¢£}}£∆~¶~÷|√×."
+ " 😯😆😉😇😂😀👕🎓😀👙🐕🐀🐶🐰🐩⛪⛲ ";
private static final String SMS_SEND_ACTION = "CTS_SMS_SEND_ACTION";
private static final String SMS_DELIVERY_ACTION = "CTS_SMS_DELIVERY_ACTION";
private static final String DATA_SMS_RECEIVED_ACTION = "android.intent.action.DATA_SMS_RECEIVED";
public static final String SMS_DELIVER_DEFAULT_APP_ACTION = "CTS_SMS_DELIVERY_ACTION_DEFAULT_APP";
public static final String LEGACY_SMS_APP = "android.telephony.cts.sms23";
public static final String MODERN_SMS_APP = "android.telephony.cts.sms";
private static final String SMS_RETRIEVER_APP = "android.telephony.cts.smsretriever";
private static final String SMS_RETRIEVER_ACTION = "CTS_SMS_RETRIEVER_ACTION";
private static final String FINANCIAL_SMS_APP = "android.telephony.cts.financialsms";
private TelephonyManager mTelephonyManager;
private SubscriptionManager mSubscriptionManager;
private String mDestAddr;
private String mText;
private SmsBroadcastReceiver mSendReceiver;
private SmsBroadcastReceiver mDeliveryReceiver;
private SmsBroadcastReceiver mDataSmsReceiver;
private SmsBroadcastReceiver mSmsDeliverReceiver;
private SmsBroadcastReceiver mSmsReceivedReceiver;
private SmsBroadcastReceiver mSmsRetrieverReceiver;
private PendingIntent mSentIntent;
private PendingIntent mDeliveredIntent;
private Intent mSendIntent;
private Intent mDeliveryIntent;
private Context mContext;
private Uri mBlockedNumberUri;
private boolean mTestAppSetAsDefaultSmsApp;
private boolean mDeliveryReportSupported;
private static boolean mReceivedDataSms;
private static String mReceivedText;
@Nullable
private String mOriginalDefaultSmsApp;
private static boolean sHasShellPermissionIdentity = false;
private static long sMessageId = 0L;
private static final int TIME_OUT = 1000 * 60 * 10;
private static final int NO_CALLS_TIMEOUT_MILLIS = 1000; // 1 second
@Before
public void setUp() throws Exception {
assumeTrue(getContext().getPackageManager().hasSystemFeature(
PackageManager.FEATURE_TELEPHONY_MESSAGING));
mContext = getContext();
mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
mText = "This is a test message";
executeWithShellPermissionIdentity(() -> {
mDestAddr = mSubscriptionManager.getPhoneNumber(mTelephonyManager.getSubscriptionId());
});
// exclude the networks that don't support SMS delivery report
String mccmnc = mTelephonyManager.getSimOperator();
mDeliveryReportSupported = !(CarrierCapability.NO_DELIVERY_REPORTS.contains(mccmnc));
// register receivers
mSendIntent = new Intent(SMS_SEND_ACTION).setPackage(mContext.getPackageName());
mDeliveryIntent = new Intent(SMS_DELIVERY_ACTION).setPackage(mContext.getPackageName());
IntentFilter sendIntentFilter = new IntentFilter(SMS_SEND_ACTION);
IntentFilter deliveryIntentFilter = new IntentFilter(SMS_DELIVERY_ACTION);
IntentFilter dataSmsReceivedIntentFilter = new IntentFilter(DATA_SMS_RECEIVED_ACTION);
IntentFilter smsDeliverIntentFilter = new IntentFilter(SMS_DELIVER_DEFAULT_APP_ACTION);
IntentFilter smsReceivedIntentFilter =
new IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
IntentFilter smsRetrieverIntentFilter = new IntentFilter(SMS_RETRIEVER_ACTION);
dataSmsReceivedIntentFilter.addDataScheme("sms");
dataSmsReceivedIntentFilter.addDataAuthority("localhost", "19989");
mSendReceiver = new SmsBroadcastReceiver(SMS_SEND_ACTION);
mDeliveryReceiver = new SmsBroadcastReceiver(SMS_DELIVERY_ACTION);
mDataSmsReceiver = new SmsBroadcastReceiver(DATA_SMS_RECEIVED_ACTION);
mSmsDeliverReceiver = new SmsBroadcastReceiver(SMS_DELIVER_DEFAULT_APP_ACTION);
mSmsReceivedReceiver = new SmsBroadcastReceiver(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
mSmsRetrieverReceiver = new SmsBroadcastReceiver(SMS_RETRIEVER_ACTION);
mContext.registerReceiver(mSendReceiver, sendIntentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
mContext.registerReceiver(mDeliveryReceiver, deliveryIntentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
mContext.registerReceiver(mDataSmsReceiver, dataSmsReceivedIntentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
mContext.registerReceiver(mSmsDeliverReceiver, smsDeliverIntentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
mContext.registerReceiver(mSmsReceivedReceiver, smsReceivedIntentFilter);
mContext.registerReceiver(mSmsRetrieverReceiver, smsRetrieverIntentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
mOriginalDefaultSmsApp = DefaultSmsAppHelper.getDefaultSmsApp(getContext());
DefaultSmsAppHelper.stopBeingDefaultSmsApp();
}
@After
public void tearDown() throws Exception {
if (mBlockedNumberUri != null) {
unblockNumber(mBlockedNumberUri);
mBlockedNumberUri = null;
}
if (mTestAppSetAsDefaultSmsApp) {
setDefaultSmsApp(false);
}
// unregister receivers
if (mSendReceiver != null) {
mContext.unregisterReceiver(mSendReceiver);
}
if (mDeliveryReceiver != null) {
mContext.unregisterReceiver(mDeliveryReceiver);
}
if (mDataSmsReceiver != null) {
mContext.unregisterReceiver(mDataSmsReceiver);
}
if (mSmsDeliverReceiver != null) {
mContext.unregisterReceiver(mSmsDeliverReceiver);
}
if (mSmsReceivedReceiver != null) {
mContext.unregisterReceiver(mSmsReceivedReceiver);
}
if (mSmsRetrieverReceiver != null) {
mContext.unregisterReceiver(mSmsRetrieverReceiver);
}
if (!TextUtils.isEmpty(mOriginalDefaultSmsApp)) {
assertTrue(DefaultSmsAppHelper.setDefaultSmsApp(getContext(), mOriginalDefaultSmsApp));
}
}
@Test
public void testDivideMessage() {
ArrayList<String> dividedMessages = divideMessage(LONG_TEXT);
assertNotNull(dividedMessages);
if (TelephonyUtils.isSkt(mTelephonyManager)) {
assertTrue(isComplete(dividedMessages, 5, LONG_TEXT)
|| isComplete(dividedMessages, 3, LONG_TEXT));
} else if (TelephonyUtils.isKt(mTelephonyManager)) {
assertTrue(isComplete(dividedMessages, 4, LONG_TEXT)
|| isComplete(dividedMessages, 3, LONG_TEXT));
} else {
assertTrue(isComplete(dividedMessages, 3, LONG_TEXT));
}
}
@Test
public void testDivideUnicodeMessage() {
ArrayList<String> dividedMessages = divideMessage(LONG_TEXT_WITH_32BIT_CHARS);
assertNotNull(dividedMessages);
assertTrue(isComplete(dividedMessages, 3, LONG_TEXT_WITH_32BIT_CHARS));
for (String messagePiece : dividedMessages) {
assertFalse(Character.isHighSurrogate(
messagePiece.charAt(messagePiece.length() - 1)));
}
}
private boolean isComplete(List<String> dividedMessages, int numParts, String longText) {
if (dividedMessages.size() != numParts) {
return false;
}
String actualMessage = "";
for (int i = 0; i < numParts; i++) {
actualMessage += dividedMessages.get(i);
}
return longText.equals(actualMessage);
}
@Test
public void testSmsRetriever() throws Exception {
assertFalse("[RERUN] SIM card does not provide phone number. Use a suitable SIM Card.",
TextUtils.isEmpty(mDestAddr));
String mccmnc = mTelephonyManager.getSimOperator();
int carrierId = mTelephonyManager.getSimCarrierId();
assertFalse("[RERUN] Carrier [carrier-id: " + carrierId + "] does not support "
+ "loop back messages. Use another carrier.",
CarrierCapability.UNSUPPORT_LOOP_BACK_MESSAGES.contains(carrierId));
init();
CompletableFuture<Bundle> callbackResult = new CompletableFuture<>();
mContext.startActivity(new Intent()
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setComponent(new ComponentName(
SMS_RETRIEVER_APP, SMS_RETRIEVER_APP + ".MainActivity"))
.putExtra("callback", new RemoteCallback(callbackResult::complete)));
Bundle bundle = callbackResult.get(200, TimeUnit.SECONDS);
String token = bundle.getString("token");
assertThat(bundle.getString("class"), startsWith(SMS_RETRIEVER_APP));
assertNotNull(token);
String composedText = "testprefix1" + mText + token;
sendTextMessage(mDestAddr, composedText, null, null);
assertTrue("[RERUN] SMS retriever message not received. Check signal.",
mSmsRetrieverReceiver.waitForCalls(1, TIME_OUT));
}
private void sendAndReceiveSms(boolean addMessageId, boolean defaultSmsApp) throws Exception {
// send single text sms
init();
if (addMessageId) {
long fakeMessageId = 19812L;
sendTextMessageWithMessageId(mDestAddr,
String.valueOf(SystemClock.elapsedRealtimeNanos()), mSentIntent,
mDeliveredIntent, fakeMessageId);
} else {
sendTextMessage(mDestAddr, String.valueOf(SystemClock.elapsedRealtimeNanos()),
mSentIntent, mDeliveredIntent);
}
assertTrue("[RERUN] Could not send SMS. Check signal.",
mSendReceiver.waitForCalls(1, TIME_OUT));
if (mDeliveryReportSupported) {
assertTrue("[RERUN] SMS message delivery notification not received. Check signal.",
mDeliveryReceiver.waitForCalls(1, TIME_OUT));
}
assertTrue(mSmsReceivedReceiver.waitForCalls(1, TIME_OUT));
// Received SMS should always contain a generated messageId
assertNotEquals(0L, sMessageId);
if (defaultSmsApp) {
// default app should receive SMS_DELIVER_ACTION
assertTrue(mSmsDeliverReceiver.waitForCalls(1, TIME_OUT));
} else {
// non-default app should receive only SMS_RECEIVED_ACTION
assertTrue(mSmsDeliverReceiver.verifyNoCalls(NO_CALLS_TIMEOUT_MILLIS));
}
}
private void sendAndReceiveMultipartSms(String mccmnc, boolean addMessageId,
boolean defaultSmsApp) throws Exception {
sMessageId = 0L;
int numPartsSent = sendMultipartTextMessageIfSupported(mccmnc, addMessageId);
if (numPartsSent > 0) {
assertTrue("[RERUN] Could not send multi part SMS. Check signal.",
mSendReceiver.waitForCalls(numPartsSent, TIME_OUT));
if (mDeliveryReportSupported) {
assertTrue("[RERUN] Multi part SMS message delivery notification not received. "
+ "Check signal.", mDeliveryReceiver.waitForCalls(numPartsSent, TIME_OUT));
}
assertTrue(mSmsReceivedReceiver.waitForCalls(1, TIME_OUT));
// Received SMS should contain a generated messageId
assertNotEquals(0L, sMessageId);
if (defaultSmsApp) {
// default app should receive SMS_DELIVER_ACTION
assertTrue(mSmsDeliverReceiver.waitForCalls(1, TIME_OUT));
} else {
// non-default app should receive only SMS_RECEIVED_ACTION
assertTrue(mSmsDeliverReceiver.verifyNoCalls(NO_CALLS_TIMEOUT_MILLIS));
}
} else {
// This GSM network doesn't support Multipart SMS message.
// Skip the test.
}
}
private void sendDataSms(String mccmnc) throws Exception {
if (sendDataMessageIfSupported(mccmnc)) {
assertTrue("[RERUN] Could not send data SMS. Check signal.",
mSendReceiver.waitForCalls(1, TIME_OUT));
if (mDeliveryReportSupported) {
assertTrue("[RERUN] Data SMS message delivery notification not received. " +
"Check signal.", mDeliveryReceiver.waitForCalls(1, TIME_OUT));
}
mDataSmsReceiver.waitForCalls(1, TIME_OUT);
assertTrue("[RERUN] Data SMS message not received. Check signal.", mReceivedDataSms);
assertEquals(mReceivedText, mText);
} else {
// This GSM network doesn't support Data(binary) SMS message.
// Skip the test.
}
}
@Test(timeout = 10 * 60 * 1000)
@ApiTest(apis = {
"android.telephony.SmsManager#sendTextMessage",
"android.telephony.SmsManager#sendDataMessage",
"android.telephony.SmsManager#sendMultipartTextMessage"})
public void testSendAndReceiveMessages() throws Exception {
// Test non-default SMS app
testSendAndReceiveMessages(false);
// Test default SMS app
DefaultSmsAppHelper.ensureDefaultSmsApp();
testSendAndReceiveMessages(true);
DefaultSmsAppHelper.stopBeingDefaultSmsApp();
}
private void testSendAndReceiveMessages(boolean defaultSmsApp) throws Exception {
assertFalse("[RERUN] SIM card does not provide phone number. Use a suitable SIM Card.",
TextUtils.isEmpty(mDestAddr));
String mccmnc = mTelephonyManager.getSimOperator();
int carrierId = mTelephonyManager.getSimCarrierId();
assertFalse("[RERUN] Carrier [carrier-id: " + carrierId + "] does not support "
+ "loop back messages. Use another carrier.",
CarrierCapability.UNSUPPORT_LOOP_BACK_MESSAGES.contains(carrierId));
// send/receive single text sms with and without messageId
sendAndReceiveSms(/* addMessageId= */ true, defaultSmsApp);
sendAndReceiveSms(/* addMessageId= */ false, defaultSmsApp);
if (mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
// TODO: temp workaround, OCTET encoding for EMS not properly supported
return;
}
// send/receive data sms
sendDataSms(mccmnc);
// send/receive multi part text sms with and without messageId
sendAndReceiveMultipartSms(mccmnc, /* addMessageId= */ true, defaultSmsApp);
sendAndReceiveMultipartSms(mccmnc, /* addMessageId= */ false, defaultSmsApp);
}
@Test
public void testSmsBlocking() throws Exception {
assertFalse("[RERUN] SIM card does not provide phone number. Use a suitable SIM Card.",
TextUtils.isEmpty(mDestAddr));
// disable suppressing blocking.
TelephonyUtils.endBlockSuppression(getInstrumentation());
String mccmnc = mTelephonyManager.getSimOperator();
// Setting default SMS App is needed to be able to block numbers.
setDefaultSmsApp(true);
blockNumber(mDestAddr);
// single-part SMS blocking
init();
sendTextMessage(mDestAddr, String.valueOf(SystemClock.elapsedRealtimeNanos()),
mSentIntent, mDeliveredIntent);
assertTrue("[RERUN] Could not send SMS. Check signal.",
mSendReceiver.waitForCalls(1, TIME_OUT));
assertTrue("Expected no messages to be received due to number blocking.",
mSmsReceivedReceiver.verifyNoCalls(NO_CALLS_TIMEOUT_MILLIS));
assertTrue("Expected no messages to be delivered due to number blocking.",
mSmsDeliverReceiver.verifyNoCalls(NO_CALLS_TIMEOUT_MILLIS));
// send data sms
if (!sendDataMessageIfSupported(mccmnc)) {
assertTrue("[RERUN] Could not send data SMS. Check signal.",
mSendReceiver.waitForCalls(1, TIME_OUT));
if (mDeliveryReportSupported) {
assertTrue("[RERUN] Data SMS message delivery notification not received. " +
"Check signal.", mDeliveryReceiver.waitForCalls(1, TIME_OUT));
}
assertTrue("Expected no messages to be delivered due to number blocking.",
mSmsDeliverReceiver.verifyNoCalls(NO_CALLS_TIMEOUT_MILLIS));
} else {
// This GSM network doesn't support Data(binary) SMS message.
// Skip the test.
}
// multi-part SMS blocking
int numPartsSent = sendMultipartTextMessageIfSupported(mccmnc, /* addMessageId= */ false);
if (numPartsSent > 0) {
assertTrue("[RERUN] Could not send multi part SMS. Check signal.",
mSendReceiver.waitForCalls(numPartsSent, TIME_OUT));
assertTrue("Expected no messages to be received due to number blocking.",
mSmsReceivedReceiver.verifyNoCalls(NO_CALLS_TIMEOUT_MILLIS));
assertTrue("Expected no messages to be delivered due to number blocking.",
mSmsDeliverReceiver.verifyNoCalls(NO_CALLS_TIMEOUT_MILLIS));
} else {
// This GSM network doesn't support Multipart SMS message.
// Skip the test.
}
}
@Test
public void testGetSmsMessagesForFinancialAppPermissionRequestedNotGranted() throws Exception {
CompletableFuture<Bundle> callbackResult = new CompletableFuture<>();
mContext.startActivity(new Intent()
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setComponent(new ComponentName(FINANCIAL_SMS_APP, FINANCIAL_SMS_APP + ".MainActivity"))
.putExtra("callback", new RemoteCallback(callbackResult::complete)));
Bundle bundle = callbackResult.get(500, TimeUnit.SECONDS);
assertThat(bundle.getString("class"), startsWith(FINANCIAL_SMS_APP));
assertThat(bundle.getInt("rowNum"), equalTo(-1));
}
@Test
public void testGetSmsMessagesForFinancialAppPermissionRequestedGranted() throws Exception {
CompletableFuture<Bundle> callbackResult = new CompletableFuture<>();
String ctsPackageName = getInstrumentation().getContext().getPackageName();
executeWithShellPermissionIdentity(() -> {
setModeForOps(FINANCIAL_SMS_APP,
AppOpsManager.MODE_ALLOWED,
AppOpsManager.OPSTR_SMS_FINANCIAL_TRANSACTIONS);
});
mContext.startActivity(new Intent()
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setComponent(new ComponentName(FINANCIAL_SMS_APP, FINANCIAL_SMS_APP + ".MainActivity"))
.putExtra("callback", new RemoteCallback(callbackResult::complete)));
Bundle bundle = callbackResult.get(500, TimeUnit.SECONDS);
assertThat(bundle.getString("class"), startsWith(FINANCIAL_SMS_APP));
assertThat(bundle.getInt("rowNum"), equalTo(-1));
}
@Test
public void testSmsNotPersisted_failsWithoutCarrierPermissions() throws Exception {
assertFalse("[RERUN] SIM card does not provide phone number. Use a suitable SIM Card.",
TextUtils.isEmpty(mDestAddr));
try {
getSmsManager().sendTextMessageWithoutPersisting(mDestAddr, null /*scAddress */,
mDestAddr, mSentIntent, mDeliveredIntent);
fail("We should get a SecurityException due to not having carrier privileges");
} catch (SecurityException e) {
// Success
}
}
@Test
public void testContentProviderAccessRestriction() throws Exception {
Uri dummySmsUri = null;
Context context = getInstrumentation().getContext();
ContentResolver contentResolver = context.getContentResolver();
int originalWriteSmsMode = -1;
String ctsPackageName = context.getPackageName();
try {
// Insert some test sms
originalWriteSmsMode = context.getSystemService(AppOpsManager.class)
.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_WRITE_SMS,
getPackageUid(ctsPackageName), ctsPackageName);
setModeForOps(ctsPackageName,
AppOpsManager.MODE_ALLOWED, AppOpsManager.OPSTR_WRITE_SMS);
ContentValues contentValues = new ContentValues();
contentValues.put(Telephony.TextBasedSmsColumns.ADDRESS, "addr");
contentValues.put(Telephony.TextBasedSmsColumns.READ, 1);
contentValues.put(Telephony.TextBasedSmsColumns.SUBJECT, "subj");
contentValues.put(Telephony.TextBasedSmsColumns.BODY, "created_at_"
+ new Date().toString().replace(" ", "_"));
dummySmsUri = contentResolver.insert(Telephony.Sms.CONTENT_URI, contentValues);
assertNotNull("Failed to insert test sms", dummySmsUri);
assertNotEquals("Failed to insert test sms", "0", dummySmsUri.getLastPathSegment());
testSmsAccessAboutDefaultApp(LEGACY_SMS_APP);
testSmsAccessAboutDefaultApp(MODERN_SMS_APP);
} finally {
if (dummySmsUri != null && !"/0".equals(dummySmsUri.getLastPathSegment())) {
final Uri finalDummySmsUri = dummySmsUri;
executeWithShellPermissionIdentity(() -> contentResolver.delete(finalDummySmsUri,
null, null));
}
if (originalWriteSmsMode >= 0) {
int finalOriginalWriteSmsMode = originalWriteSmsMode;
executeWithShellPermissionIdentity(() ->
setModeForOps(ctsPackageName,
finalOriginalWriteSmsMode, AppOpsManager.OPSTR_WRITE_SMS));
}
}
}
private void testSmsAccessAboutDefaultApp(String pkg)
throws Exception {
String originalSmsApp = getSmsApp();
assertNotEquals(pkg, originalSmsApp);
assertCanAccessSms(pkg);
try {
setSmsApp(pkg);
assertCanAccessSms(pkg);
} finally {
resetReadWriteSmsAppOps(pkg);
setSmsApp(originalSmsApp);
}
}
private void resetReadWriteSmsAppOps(String pkg) throws Exception {
setModeForOps(pkg, AppOpsManager.MODE_DEFAULT,
AppOpsManager.OPSTR_READ_SMS, AppOpsManager.OPSTR_WRITE_SMS);
}
private void setModeForOps(String pkg, int mode, String... ops) throws Exception {
// We cannot reset these app ops to DEFAULT via current API, so we reset them manually here
// temporarily as we will rewrite how the default SMS app is setup later.
executeWithShellPermissionIdentity(() -> {
int uid = getPackageUid(pkg);
AppOpsManager appOpsManager =
getInstrumentation().getContext().getSystemService(AppOpsManager.class);
for (String op : ops) {
appOpsManager.setUidMode(op, uid, mode);
}
});
}
private int getPackageUid(String pkg) throws PackageManager.NameNotFoundException {
return getInstrumentation().getContext().getPackageManager().getPackageUid(pkg, 0);
}
private String getSmsApp() throws Exception {
return executeWithShellPermissionIdentity(() -> getInstrumentation()
.getContext()
.getSystemService(RoleManager.class)
.getRoleHolders(RoleManager.ROLE_SMS)
.get(0));
}
private void setSmsApp(String pkg) throws Exception {
executeWithShellPermissionIdentity(() -> {
Context context = getInstrumentation().getContext();
RoleManager roleManager = context.getSystemService(RoleManager.class);
CompletableFuture<Boolean> result = new CompletableFuture<>();
if (roleManager.getRoleHoldersAsUser(RoleManager.ROLE_SMS,
context.getUser()).contains(pkg)) {
result.complete(true);
} else {
roleManager.addRoleHolderAsUser(RoleManager.ROLE_SMS, pkg,
RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, context.getUser(),
AsyncTask.THREAD_POOL_EXECUTOR, result::complete);
}
assertTrue(result.get(5, TimeUnit.SECONDS));
});
}
private <T> T executeWithShellPermissionIdentity(Callable<T> callable) throws Exception {
if (sHasShellPermissionIdentity) {
return callable.call();
}
UiAutomation uiAutomation = getInstrumentation().getUiAutomation(
UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
uiAutomation.adoptShellPermissionIdentity();
try {
sHasShellPermissionIdentity = true;
return callable.call();
} finally {
uiAutomation.dropShellPermissionIdentity();
sHasShellPermissionIdentity = false;
}
}
private void executeWithShellPermissionIdentity(RunnableWithException runnable)
throws Exception {
executeWithShellPermissionIdentity(() -> {
runnable.run();
return null;
});
}
private interface RunnableWithException {
void run() throws Exception;
}
private void assertCanAccessSms(String pkg) throws Exception {
CompletableFuture<Bundle> callbackResult = new CompletableFuture<>();
mContext.startActivity(new Intent()
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setComponent(new ComponentName(pkg, pkg + ".MainActivity"))
.putExtra("callback", new RemoteCallback(callbackResult::complete)));
Bundle bundle = callbackResult.get(20, TimeUnit.SECONDS);
assertThat(bundle.getString("class"), startsWith(pkg));
assertThat(bundle.getString("exceptionMessage"), anyOf(equalTo(null), emptyString()));
assertThat(bundle.getInt("queryCount"), greaterThan(0));
}
private void init() {
mSendReceiver.reset();
mDeliveryReceiver.reset();
mDataSmsReceiver.reset();
mSmsDeliverReceiver.reset();
mSmsReceivedReceiver.reset();
mSmsRetrieverReceiver.reset();
mReceivedDataSms = false;
sMessageId = 0L;
mSentIntent = PendingIntent.getBroadcast(mContext, 0, mSendIntent,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
mDeliveredIntent = PendingIntent.getBroadcast(mContext, 0, mDeliveryIntent,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
}
/**
* Returns the number of parts sent in the message. If Multi-part SMS is not supported,
* returns 0.
*/
private int sendMultipartTextMessageIfSupported(String mccmnc, boolean addMessageId) {
int numPartsSent = 0;
if (!CarrierCapability.UNSUPPORT_MULTIPART_SMS_MESSAGES.contains(mccmnc)) {
init();
ArrayList<String> parts = divideMessage(LONG_TEXT);
numPartsSent = parts.size();
ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>();
ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>();
for (int i = 0; i < numPartsSent; i++) {
sentIntents.add(PendingIntent.getBroadcast(mContext, 0, mSendIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED));
deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, mDeliveryIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED));
}
sendMultiPartTextMessage(mDestAddr, parts, sentIntents, deliveryIntents, addMessageId);
}
return numPartsSent;
}
private boolean sendDataMessageIfSupported(String mccmnc) {
if (!CarrierCapability.UNSUPPORT_DATA_SMS_MESSAGES.contains(mccmnc)) {
byte[] data = mText.getBytes();
short port = 19989;
init();
sendDataMessage(mDestAddr, port, data, mSentIntent, mDeliveredIntent);
return true;
}
return false;
}
@Test
public void testGetDefault() {
assertNotNull(getSmsManager());
}
@Test
public void testGetSetSmscAddress() {
String smsc = null;
try {
smsc = getSmsManager().getSmscAddress();
fail("SmsManager.getSmscAddress() should throw a SecurityException");
} catch (SecurityException e) {
// expected
}
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity("android.permission.READ_PRIVILEGED_PHONE_STATE");
try {
smsc = getSmsManager().getSmscAddress();
} catch (SecurityException se) {
fail("Caller with READ_PRIVILEGED_PHONE_STATE should be able to call API");
} finally {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
try {
getSmsManager().setSmscAddress(smsc);
fail("SmsManager.setSmscAddress() should throw a SecurityException");
} catch (SecurityException e) {
// expected
}
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity("android.permission.MODIFY_PHONE_STATE");
try {
getSmsManager().setSmscAddress(smsc);
} catch (SecurityException se) {
fail("Caller with MODIFY_PHONE_STATE should be able to call API");
} finally {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
public void testGetPremiumSmsConsent() {
try {
getSmsManager().getPremiumSmsConsent("fake package name");
fail("SmsManager.getPremiumSmsConsent() should throw a SecurityException");
} catch (SecurityException e) {
// expected
}
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity("android.permission.READ_PRIVILEGED_PHONE_STATE");
try {
getSmsManager().getPremiumSmsConsent("fake package name");
fail("Caller with permission but only phone/system uid is allowed");
} catch (SecurityException se) {
// expected
} finally {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
@Test
public void testSetPremiumSmsConsent() {
try {
getSmsManager().setPremiumSmsConsent("fake package name", 0);
fail("SmsManager.setPremiumSmsConsent() should throw a SecurityException");
} catch (SecurityException e) {
// expected
}
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity("android.permission.MODIFY_PHONE_STATE");
try {
getSmsManager().setPremiumSmsConsent("fake package name", 0);
fail("Caller with permission but only phone/system uid is allowed");
} catch (SecurityException se) {
// expected
} finally {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}
/**
* Verify that SmsManager.getSmsCapacityOnIcc requires Permission.
* <p>
* Requires Permission:
* {@link android.Manifest.permission#READ_PHONE_STATE}.
*/
@Test
public void testGetSmsCapacityOnIcc() {
try {
getSmsManager().getSmsCapacityOnIcc();
} catch (SecurityException e) {
fail("Caller with READ_PHONE_STATE should be able to call API");
}
}
@Test
public void testDisableCellBroadcastRange() {
try {
int ranType = SmsCbMessage.MESSAGE_FORMAT_3GPP;
executeWithShellPermissionIdentity(() -> {
getSmsManager().disableCellBroadcastRange(
CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT,
CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT,
ranType);
});
} catch (Exception e) {
// expected
}
}
@Test
public void testEnableCellBroadcastRange() {
try {
int ranType = SmsCbMessage.MESSAGE_FORMAT_3GPP;
executeWithShellPermissionIdentity(() -> {
getSmsManager().enableCellBroadcastRange(
CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT,
CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT,
ranType);
});
} catch (Exception e) {
// expected
}
}
@Test
public void testResetAllCellBroadcastRanges() {
try {
executeWithShellPermissionIdentity(() -> {
getSmsManager().resetAllCellBroadcastRanges();
});
} catch (Exception e) {
// expected
}
}
@Test
public void testCreateForSubscriptionId() {
int testSubId = 123;
SmsManager smsManager = mContext.getSystemService(SmsManager.class)
.createForSubscriptionId(testSubId);
assertEquals("getSubscriptionId() should be " + testSubId, testSubId,
smsManager.getSubscriptionId());
}
/**
* Verify the API will not throw any exception when READ_PRIVILEGED_PHONE_STATE is granted.
*/
@Test
public void testGetSmscIdentity() {
try {
mTelephonyManager.getHalVersion(TelephonyManager.HAL_SERVICE_RADIO);
} catch (IllegalStateException e) {
assumeNoException("Skipping test because Telephony service is null", e);
}
SmsManager smsManager = getSmsManager();
ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(smsManager,
SmsManager::getSmscIdentity, Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
}
/**
* Verify the API will throw the SecurityException or not when no permissions are granted.
*/
@Test
public void testGetSmscIdentity_Exception() {
try {
mTelephonyManager.getHalVersion(TelephonyManager.HAL_SERVICE_RADIO);
} catch (IllegalStateException e) {
assumeNoException("Skipping test because Telephony service is null", e);
}
dropShellIdentity();
try {
getSmsManager().getSmscIdentity();
fail();
} catch (SecurityException se) {
// API will throw SecurityException as no permission is granted to the caller
}
adoptShellIdentity();
}
protected ArrayList<String> divideMessage(String text) {
return getSmsManager().divideMessage(text);
}
private android.telephony.SmsManager getSmsManager() {
return android.telephony.SmsManager.getDefault();
}
protected void sendMultiPartTextMessage(String destAddr, ArrayList<String> parts,
ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents,
boolean addMessageId) {
if (addMessageId) {
long fakeMessageId = 1278;
getSmsManager().sendMultipartTextMessage(destAddr, null, parts, sentIntents,
deliveryIntents, fakeMessageId);
} else if (mContext.getOpPackageName() != null) {
getSmsManager().sendMultipartTextMessage(destAddr, null, parts, sentIntents,
deliveryIntents, mContext.getOpPackageName(), mContext.getAttributionTag());
} else {
getSmsManager().sendMultipartTextMessage(destAddr, null, parts, sentIntents,
deliveryIntents);
}
}
protected void sendDataMessage(String destAddr,short port, byte[] data, PendingIntent sentIntent, PendingIntent deliveredIntent) {
getSmsManager().sendDataMessage(destAddr, null, port, data, sentIntent, deliveredIntent);
}
protected void sendTextMessage(String destAddr, String text, PendingIntent sentIntent,
PendingIntent deliveredIntent) {
getSmsManager().sendTextMessage(destAddr, null, text, sentIntent, deliveredIntent);
}
protected void sendTextMessageWithMessageId(String destAddr, String text,
PendingIntent sentIntent, PendingIntent deliveredIntent, long messageId) {
getSmsManager().sendTextMessage(destAddr, null, text, sentIntent, deliveredIntent,
messageId);
}
private void blockNumber(String number) {
mBlockedNumberUri = insertBlockedNumber(mContext, number);
if (mBlockedNumberUri == null) {
fail("Failed to insert into blocked number provider.");
}
}
private void unblockNumber(Uri uri) {
deleteBlockedNumber(mContext, uri);
}
private void setDefaultSmsApp(boolean setToSmsApp)
throws Exception {
String command = String.format(
"appops set --user 0 %s WRITE_SMS %s",
mContext.getPackageName(),
setToSmsApp ? "allow" : "default");
assertTrue("Setting default SMS app failed : " + setToSmsApp,
executeShellCommand(command).isEmpty());
mTestAppSetAsDefaultSmsApp = setToSmsApp;
}
private String executeShellCommand(String command)
throws IOException {
ParcelFileDescriptor pfd =
getInstrumentation().getUiAutomation().executeShellCommand(command);
BufferedReader br = null;
try (InputStream in = new FileInputStream(pfd.getFileDescriptor());) {
br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String str;
StringBuilder out = new StringBuilder();
while ((str = br.readLine()) != null) {
out.append(str);
}
return out.toString();
} finally {
if (br != null) {
br.close();
}
}
}
private static class SmsBroadcastReceiver extends BroadcastReceiver {
private int mCalls;
private int mExpectedCalls;
private String mAction;
private Object mLock;
SmsBroadcastReceiver(String action) {
mAction = action;
reset();
mLock = new Object();
}
void reset() {
mExpectedCalls = Integer.MAX_VALUE;
mCalls = 0;
}
@Override
public void onReceive(Context context, Intent intent) {
if(mAction.equals(DATA_SMS_RECEIVED_ACTION)){
StringBuilder sb = new StringBuilder();
Bundle bundle = intent.getExtras();
if (bundle != null) {
Object[] obj = (Object[]) bundle.get("pdus");
String format = bundle.getString("format");
SmsMessage[] message = new SmsMessage[obj.length];
for (int i = 0; i < obj.length; i++) {
message[i] = SmsMessage.createFromPdu((byte[]) obj[i], format);
}
for (SmsMessage currentMessage : message) {
byte[] binaryContent = currentMessage.getUserData();
String readableContent = new String(binaryContent);
sb.append(readableContent);
}
}
mReceivedDataSms = true;
mReceivedText=sb.toString();
}
if (mAction.equals(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
sMessageId = intent.getLongExtra("messageId", 0L);
}
Log.i(TAG, "onReceive " + intent.getAction() + ", mAction " + mAction);
if (intent.getAction().equals(mAction)) {
synchronized (mLock) {
mCalls += 1;
mLock.notify();
}
}
}
public boolean verifyNoCalls(long timeout) throws InterruptedException {
synchronized(mLock) {
mLock.wait(timeout);
return mCalls == 0;
}
}
public boolean waitForCalls(int expectedCalls, long timeout) throws InterruptedException {
synchronized(mLock) {
mExpectedCalls = expectedCalls;
long startTime = SystemClock.elapsedRealtime();
while (mCalls < mExpectedCalls) {
long waitTime = timeout - (SystemClock.elapsedRealtime() - startTime);
if (waitTime > 0) {
mLock.wait(waitTime);
} else {
return false; // timed out
}
}
return true; // success
}
}
}
/**
* Adopts shell permission identity
*/
private static void adoptShellIdentity() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity();
}
/**
* Drop shell permission identity
*/
private static void dropShellIdentity() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
}