blob: ce7b53037b9189da098ceb4c0c0f02910b087c1e [file] [log] [blame]
/*
* Copyright (C) 2021 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.telephonyprovider.cts;
import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
import static android.provider.Telephony.ServiceStateTable.DATA_NETWORK_TYPE;
import static android.provider.Telephony.ServiceStateTable.DATA_REG_STATE;
import static android.provider.Telephony.ServiceStateTable.DUPLEX_MODE;
import static android.provider.Telephony.ServiceStateTable.VOICE_REG_STATE;
import static android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_HOME;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeTrue;
import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcel;
import android.provider.Telephony;
import android.telephony.AccessNetworkConstants;
import android.telephony.NetworkRegistrationInfo;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import com.android.compatibility.common.util.ApiTest;
import com.android.compatibility.common.util.PollingCheck;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@SmallTest
public class ServiceStateTest {
private static final int DEFAULT_TIMEOUT = 1000;
// Keep the same as ServiceStateProvider#SERVICE_STATE which is NOT same as
// Telephony.ServiceStateTable.AUTHORITY;
private static final String SERVICE_STATE = "service_state";
private ContentResolver mContentResolver;
private TelephonyManager mTelephonyManager;
private int mSubId;
private @Nullable ServiceState mInitialServiceState;
@Before
public void setUp() {
assumeTrue(hasTelephonyFeature());
mContentResolver = getInstrumentation().getContext().getContentResolver();
mTelephonyManager =
getInstrumentation().getContext().getSystemService(TelephonyManager.class);
mSubId = SubscriptionManager.getDefaultSubscriptionId();
mInitialServiceState = mTelephonyManager.getServiceState();
}
@After
public void tearDown() {
if (!hasTelephonyFeature()) {
return;
}
// Recover the initial ServiceState to remove the impact of manual ServiceState insertion.
if (mInitialServiceState != null) {
insertServiceState(mInitialServiceState);
}
}
/**
* Verifies that the ServiceStateTable CONTENT_URI and AUTHORITY is valid.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#AUTHORITY",
"android.provider.Telephony.ServiceStateTable#AUTHORCONTENT_URI"})
public void testUriAndAuthority() {
Uri uri = Telephony.ServiceStateTable.CONTENT_URI;
assertThat(uri).isEqualTo(Uri.parse("content://service-state/"));
String authority = Telephony.ServiceStateTable.AUTHORITY;
assertThat(authority).isEqualTo("service-state");
}
/**
* Verifies that the voice reg state is valid and matches ServiceState#getState().
* This test case requires an active subscription.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#VOICE_REG_STATE"})
public void testGetVoiceRegState_query() {
if (!isSubscriptionActive(mSubId)) {
return;
}
try (Cursor cursor = mContentResolver.query(Telephony.ServiceStateTable.CONTENT_URI,
new String[]{VOICE_REG_STATE}, null, null)) {
assertThat(cursor.getCount()).isEqualTo(1);
cursor.moveToNext();
int voiceRegState = cursor.getInt(cursor.getColumnIndex(VOICE_REG_STATE));
assertThat(voiceRegState).isEqualTo(mTelephonyManager.getServiceState().getState());
}
}
/**
* Verifies that when voice reg state did not change, the observer should not receive the
* notification.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#VOICE_REG_STATE"})
public void testGetVoiceRegState_noChangeObserved() throws Exception {
ServiceState oldSS = new ServiceState();
oldSS.setStateOutOfService();
oldSS.setState(ServiceState.STATE_OUT_OF_SERVICE);
ServiceState copyOfOldSS = new ServiceState();
copyOfOldSS.setStateOutOfService();
copyOfOldSS.setState(ServiceState.STATE_OUT_OF_SERVICE);
// set additional fields which is not related to voice reg state
copyOfOldSS.setChannelNumber(65536);
copyOfOldSS.setIsManualSelection(true);
verifyNotificationObservedWhenFieldChanged(
VOICE_REG_STATE, oldSS, copyOfOldSS, false /*expectChange*/);
}
/**
* Verifies that when voice reg state changed, the observer should receive the notification.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#VOICE_REG_STATE"})
public void testGetVoiceRegState_changeObserved() throws Exception {
ServiceState oldSS = new ServiceState();
oldSS.setStateOutOfService();
oldSS.setState(ServiceState.STATE_OUT_OF_SERVICE);
ServiceState newSS = new ServiceState();
newSS.setStateOutOfService();
newSS.setState(ServiceState.STATE_POWER_OFF);
verifyNotificationObservedWhenFieldChanged(
VOICE_REG_STATE, oldSS, newSS, true /*expectChange*/);
}
/**
* Verifies that the data network type is valid and matches ServiceState#getDataNetworkType().
* This test case requires an active subscription.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#DATA_NETWORK_TYPE"})
public void testGetDataNetworkType_query() {
if (!isSubscriptionActive(mSubId)) {
return;
}
try (Cursor cursor = mContentResolver.query(Telephony.ServiceStateTable.CONTENT_URI,
new String[]{DATA_NETWORK_TYPE}, null, null)) {
assertThat(cursor.getCount()).isEqualTo(1);
cursor.moveToNext();
int dataNetworkType = cursor.getInt(cursor.getColumnIndex(DATA_NETWORK_TYPE));
assertThat(dataNetworkType).isEqualTo(
mTelephonyManager.getServiceState().getDataNetworkType());
}
}
/**
* Verifies that when data network type did not change, the observer should not receive the
* notification.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#DATA_NETWORK_TYPE"})
public void testDataNetworkType_noChangeObserved() throws Exception {
ServiceState oldSS = new ServiceState();
oldSS.setStateOutOfService();
ServiceState copyOfOldSS = new ServiceState();
copyOfOldSS.setStateOutOfService();
// Add a DOMAIN_CS NRI which should not update DataNetworkType
NetworkRegistrationInfo nri = new NetworkRegistrationInfo.Builder()
.setDomain(NetworkRegistrationInfo.DOMAIN_CS)
.setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
.setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_GPRS)
.setRegistrationState(REGISTRATION_STATE_HOME)
.build();
copyOfOldSS.addNetworkRegistrationInfo(nri);
verifyNotificationObservedWhenFieldChanged(
DATA_NETWORK_TYPE, oldSS, copyOfOldSS, false /*expectChange*/);
}
/**
* Verifies that when data network type changed, the observer should receive the notification.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#DATA_NETWORK_TYPE"})
public void testDataNetworkType_changeObserved() throws Exception {
// While we don't have a method to directly set dataNetworkType, we emulate a ServiceState
// change that will trigger the change of dataNetworkType, according to the logic in
// ServiceState#getDataNetworkType()
ServiceState oldSS = new ServiceState();
oldSS.setStateOutOfService();
ServiceState newSS = new ServiceState();
newSS.setStateOutOfService();
NetworkRegistrationInfo nri = new NetworkRegistrationInfo.Builder()
.setDomain(NetworkRegistrationInfo.DOMAIN_PS)
.setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
.setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
.setRegistrationState(REGISTRATION_STATE_HOME)
.build();
newSS.addNetworkRegistrationInfo(nri);
verifyNotificationObservedWhenFieldChanged(
DATA_NETWORK_TYPE, oldSS, newSS, true /*expectChange*/);
}
/**
* Verifies that the duplex mode is valid and matches ServiceState#getDuplexMode().
* This test case requires an active subscription.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#DUPLEX_MODE"})
public void testGetDuplexMode_query() {
if (!isSubscriptionActive(mSubId)) {
return;
}
try (Cursor cursor = mContentResolver.query(Telephony.ServiceStateTable.CONTENT_URI,
new String[]{DUPLEX_MODE}, null, null)) {
assertThat(cursor.getCount()).isEqualTo(1);
cursor.moveToNext();
int duplexMode = cursor.getInt(cursor.getColumnIndex(DUPLEX_MODE));
assertThat(duplexMode).isEqualTo(
mTelephonyManager.getServiceState().getDuplexMode());
}
}
/**
* Verifies that even we have duplex mode change, the observer should not receive the
* notification (duplex mode is a poll-only field).
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#DUPLEX_MODE"})
public void testGetDuplexMode_noChangeObserved() throws Exception {
ServiceState oldSS = new ServiceState();
oldSS.setStateOutOfService();
ServiceState newSS = new ServiceState();
newSS.setStateOutOfService();
// Add NRI to trigger SS with duplex mode updated
NetworkRegistrationInfo nri = new NetworkRegistrationInfo.Builder()
.setDomain(NetworkRegistrationInfo.DOMAIN_PS)
.setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
.setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE)
.build();
newSS.addNetworkRegistrationInfo(nri);
newSS.setChannelNumber(65536); // EutranBand.BAND_65, DUPLEX_MODE_FDD
verifyNotificationObservedWhenFieldChanged(
DUPLEX_MODE, oldSS, newSS, false /*expectChange*/);
}
/**
* Verifies that the data reg state is valid and matches ServiceState#getDataRegState()
* This test case requires an active subscription.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#DATA_REG_STATE"})
public void testGetDataRegState_query() {
if (!isSubscriptionActive(mSubId)) {
return;
}
try (Cursor cursor = mContentResolver.query(Telephony.ServiceStateTable.CONTENT_URI,
new String[]{DATA_REG_STATE}, null, null)) {
assertThat(cursor.getCount()).isEqualTo(1);
cursor.moveToNext();
int dataRegState = cursor.getInt(cursor.getColumnIndex(DATA_REG_STATE));
assertThat(dataRegState).isEqualTo(
mTelephonyManager.getServiceState().getDataRegState());
}
}
/**
* Verifies that when data reg state did not change, the observer should not receive the
* notification.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#DATA_REG_STATE"})
public void testGetDataRegState_noChangeObserved() throws Exception {
ServiceState oldSS = new ServiceState();
oldSS.setStateOutOfService();
oldSS.setState(ServiceState.STATE_OUT_OF_SERVICE);
ServiceState copyOfOldSS = new ServiceState();
copyOfOldSS.setStateOutOfService();
copyOfOldSS.setState(ServiceState.STATE_OUT_OF_SERVICE);
// set additional fields which is not related to data reg state
copyOfOldSS.setChannelNumber(65536);
copyOfOldSS.setIsManualSelection(true);
verifyNotificationObservedWhenFieldChanged(
DATA_REG_STATE, oldSS, copyOfOldSS, false /*expectChange*/);
}
/**
* Verifies that when data reg state changed, the observer should receive the notification.
*/
@Test
@ApiTest(apis = {"android.provider.Telephony.ServiceStateTable#DATA_REG_STATE"})
public void testGetDataRegState_changeObserved() throws Exception {
ServiceState oldSS = new ServiceState();
oldSS.setStateOutOfService();
ServiceState newSS = new ServiceState();
newSS.setStateOutOfService();
newSS.setStateOff();
verifyNotificationObservedWhenFieldChanged(
DATA_REG_STATE, oldSS, newSS, true /*expectChange*/);
}
/**
* Verifies that when query location protected fields without permissions, those fields are not
* available to apps with targetSdkVersion S+.
*/
@Test
public void testLocationProtectedFields_noPermission() {
try (Cursor cursor = mContentResolver.query(Telephony.ServiceStateTable.CONTENT_URI,
null, null, null)) {
assertThat(cursor.getCount()).isEqualTo(1);
cursor.moveToNext();
assertThat(cursor.getColumnIndex("network_id")).isEqualTo(-1);
assertThat(cursor.getColumnIndex("system_id")).isEqualTo(-1);
assertThrows(IllegalArgumentException.class,
() -> mContentResolver.query(Telephony.ServiceStateTable.CONTENT_URI,
new String[]{"network_id"}, null, null));
assertThrows(IllegalArgumentException.class,
() -> mContentResolver.query(Telephony.ServiceStateTable.CONTENT_URI,
new String[]{"system_id"}, null, null));
}
}
/**
* Insert new ServiceState over the old ServiceState and expect the observer receiving the
* notification over the observed field change.
*/
private void verifyNotificationObservedWhenFieldChanged(String field, ServiceState oldSS,
ServiceState newSS, boolean expectChange) throws Exception {
final Uri uriForSubAndField =
Telephony.ServiceStateTable.getUriForSubscriptionIdAndField(mSubId, field);
insertServiceState(oldSS);
RecordingContentObserver observer = new RecordingContentObserver();
mContentResolver.registerContentObserver(uriForSubAndField, false, observer);
assertWithMessage("Observer is NOT empty in the beginning.").that(
observer.mObserved).isEmpty();
insertServiceState(newSS);
if (expectChange) {
// Only verify we did receive the notification for the expected field, instead of the
// number of notifications we received to remove flakiness for different cases.
PollingCheck.check(
"Expect notification when " + field + " updated.",
DEFAULT_TIMEOUT, () -> observer.mObserved.contains(uriForSubAndField));
} else {
// Let the bullets fly for a while before we check the target.
try {
Thread.sleep(DEFAULT_TIMEOUT);
} catch (InterruptedException ignored) {
}
// Fields in ServiceState are not orthogonal. In case we do receive notification(s),
// further check if it is for the expected field.
assertWithMessage("Unexpected notification for " + field).that(
observer.mObserved).doesNotContain(uriForSubAndField);
}
mContentResolver.unregisterContentObserver(observer);
}
// Manually insert the ServiceState into table to test the notification.
private void insertServiceState(ServiceState state) {
ContentValues values = getContentValuesForServiceState(state);
SystemUtil.runWithShellPermissionIdentity(
() -> mContentResolver.insert(
Telephony.ServiceStateTable.getUriForSubscriptionId(mSubId), values),
Manifest.permission.MODIFY_PHONE_STATE);
}
private boolean isSubscriptionActive(int subId) {
SubscriptionManager sm = getInstrumentation().getContext().getSystemService(
SubscriptionManager.class);
return sm != null && sm.isActiveSubscriptionId(subId);
}
// Copied from ServiceStateProvider#getContentValuesForServiceState
private static ContentValues getContentValuesForServiceState(ServiceState state) {
ContentValues values = new ContentValues();
final Parcel p = Parcel.obtain();
state.writeToParcel(p, 0);
values.put(SERVICE_STATE, p.marshall());
return values;
}
private static class RecordingContentObserver extends ContentObserver {
List<Uri> mObserved = new CopyOnWriteArrayList<>();
RecordingContentObserver() {
super(new Handler(Looper.getMainLooper()));
}
@Override
public void onChange(boolean selfChange, @Nullable Uri uri) {
mObserved.add(uri);
}
}
private static boolean hasTelephonyFeature() {
return getInstrumentation().getContext().getPackageManager().hasSystemFeature(
FEATURE_TELEPHONY);
}
// org.junit.Assume.assertThrows is not available until JUnit 4.13
private static void assertThrows(Class<? extends Exception> exceptionClass, Runnable r) {
try {
r.run();
Assert.fail("Expected " + exceptionClass + " to be thrown.");
} catch (Exception exception) {
assertThat(exception).isInstanceOf(exceptionClass);
}
}
}