Add service-state content provider

Add the ServiceStateProvider which clients can use to get the current
ServiceState given a subId and be notified when the service state
changes.

Test: runtest --path ServiceStateProviderTest.java
Bug: 33756364
Change-Id: Iecfee79fca45accbf701bc7729229c60ecc0cdbb
Merged-In: Iecfee79fca45accbf701bc7729229c60ecc0cdbb
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a2d0bda..1ca6c9a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -48,6 +48,13 @@
                   android:singleUser="true"
                   android:multiprocess="false" />
 
+        <provider android:name="ServiceStateProvider"
+                  android:authorities="service-state"
+                  android:exported="true"
+                  android:singleUser="true"
+                  android:writePermission="android.permission.MODIFY_PHONE_STATE"
+                  android:multiprocess="false" />
+
         <!-- This is a singleton provider that is used by all users.
              A new instance is not created for each user. And the db is shared
              as well. -->
diff --git a/src/com/android/providers/telephony/ServiceStateProvider.java b/src/com/android/providers/telephony/ServiceStateProvider.java
new file mode 100644
index 0000000..8662d39
--- /dev/null
+++ b/src/com/android/providers/telephony/ServiceStateProvider.java
@@ -0,0 +1,373 @@
+/* //device/content/providers/telephony/TelephonyProvider.java
+**
+** Copyright 2016, 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 com.android.providers.telephony;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MatrixCursor.RowBuilder;
+import android.net.Uri;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.SubscriptionController;
+
+import java.lang.NumberFormatException;
+import java.util.HashMap;
+import java.util.Objects;
+
+import static android.provider.Telephony.ServiceStateTable.getUriForSubId;
+import static android.provider.Telephony.ServiceStateTable.getUriForSubIdAndField;
+
+import static android.provider.Telephony.ServiceStateTable;
+import static android.provider.Telephony.ServiceStateTable.CONTENT_URI;
+
+import static android.provider.Telephony.ServiceStateTable.VOICE_REG_STATE;
+import static android.provider.Telephony.ServiceStateTable.DATA_REG_STATE;
+import static android.provider.Telephony.ServiceStateTable.VOICE_ROAMING_TYPE;
+import static android.provider.Telephony.ServiceStateTable.DATA_ROAMING_TYPE;
+import static android.provider.Telephony.ServiceStateTable.VOICE_OPERATOR_ALPHA_LONG;
+import static android.provider.Telephony.ServiceStateTable.VOICE_OPERATOR_ALPHA_SHORT;
+import static android.provider.Telephony.ServiceStateTable.VOICE_OPERATOR_NUMERIC;
+import static android.provider.Telephony.ServiceStateTable.DATA_OPERATOR_ALPHA_LONG;
+import static android.provider.Telephony.ServiceStateTable.DATA_OPERATOR_ALPHA_SHORT;
+import static android.provider.Telephony.ServiceStateTable.DATA_OPERATOR_NUMERIC;
+import static android.provider.Telephony.ServiceStateTable.IS_MANUAL_NETWORK_SELECTION;
+import static android.provider.Telephony.ServiceStateTable.RIL_VOICE_RADIO_TECHNOLOGY;
+import static android.provider.Telephony.ServiceStateTable.RIL_DATA_RADIO_TECHNOLOGY;
+import static android.provider.Telephony.ServiceStateTable.CSS_INDICATOR;
+import static android.provider.Telephony.ServiceStateTable.NETWORK_ID;
+import static android.provider.Telephony.ServiceStateTable.SYSTEM_ID;
+import static android.provider.Telephony.ServiceStateTable.CDMA_ROAMING_INDICATOR;
+import static android.provider.Telephony.ServiceStateTable.CDMA_DEFAULT_ROAMING_INDICATOR;
+import static android.provider.Telephony.ServiceStateTable.CDMA_ERI_ICON_INDEX;
+import static android.provider.Telephony.ServiceStateTable.CDMA_ERI_ICON_MODE;
+import static android.provider.Telephony.ServiceStateTable.IS_EMERGENCY_ONLY;
+import static android.provider.Telephony.ServiceStateTable.IS_DATA_ROAMING_FROM_REGISTRATION;
+import static android.provider.Telephony.ServiceStateTable.IS_USING_CARRIER_AGGREGATION;
+
+
+public class ServiceStateProvider extends ContentProvider {
+    private static final String TAG = "ServiceStateProvider";
+
+    public static final String AUTHORITY = ServiceStateTable.AUTHORITY;
+    public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+    private final HashMap<Integer, ServiceState> mServiceStates = new HashMap<>();
+    private static final String[] sColumns = {
+        VOICE_REG_STATE,
+        DATA_REG_STATE,
+        VOICE_ROAMING_TYPE,
+        DATA_ROAMING_TYPE,
+        VOICE_OPERATOR_ALPHA_LONG,
+        VOICE_OPERATOR_ALPHA_SHORT,
+        VOICE_OPERATOR_NUMERIC,
+        DATA_OPERATOR_ALPHA_LONG,
+        DATA_OPERATOR_ALPHA_SHORT,
+        DATA_OPERATOR_NUMERIC,
+        IS_MANUAL_NETWORK_SELECTION,
+        RIL_VOICE_RADIO_TECHNOLOGY,
+        RIL_DATA_RADIO_TECHNOLOGY,
+        CSS_INDICATOR,
+        NETWORK_ID,
+        SYSTEM_ID,
+        CDMA_ROAMING_INDICATOR,
+        CDMA_DEFAULT_ROAMING_INDICATOR,
+        CDMA_ERI_ICON_INDEX,
+        CDMA_ERI_ICON_MODE,
+        IS_EMERGENCY_ONLY,
+        IS_DATA_ROAMING_FROM_REGISTRATION,
+        IS_USING_CARRIER_AGGREGATION,
+    };
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @VisibleForTesting
+    public ServiceState getServiceState(int subId) {
+        return mServiceStates.get(subId);
+    }
+
+    @VisibleForTesting
+    public int getDefaultSubId() {
+        return SubscriptionController.getInstance().getDefaultSubId();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        if (uri.isPathPrefixMatch(CONTENT_URI)) {
+            // Parse the subId
+            int subId = 0;
+            try {
+                subId = Integer.parseInt(uri.getLastPathSegment());
+            } catch (NumberFormatException e) {
+                Log.d(TAG, "insert: no subId provided in uri");
+                throw e;
+            }
+            Log.d(TAG, "subId=" + subId);
+
+            // handle DEFAULT_SUBSCRIPTION_ID
+            if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+                subId = getDefaultSubId();
+            }
+
+            // create the new service state
+            ServiceState newSS = new ServiceState();
+            newSS.setVoiceRegState(values.getAsInteger(VOICE_REG_STATE));
+            newSS.setDataRegState(values.getAsInteger(DATA_REG_STATE));
+            newSS.setVoiceOperatorName(values.getAsString(VOICE_OPERATOR_ALPHA_LONG),
+                        values.getAsString(VOICE_OPERATOR_ALPHA_SHORT),
+                        values.getAsString(VOICE_OPERATOR_NUMERIC));
+            newSS.setDataOperatorName(values.getAsString(DATA_OPERATOR_ALPHA_LONG),
+                    values.getAsString(DATA_OPERATOR_ALPHA_SHORT),
+                    values.getAsString(DATA_OPERATOR_NUMERIC));
+            newSS.setIsManualSelection(values.getAsBoolean(IS_MANUAL_NETWORK_SELECTION));
+            newSS.setRilVoiceRadioTechnology(values.getAsInteger(RIL_VOICE_RADIO_TECHNOLOGY));
+            newSS.setRilDataRadioTechnology(values.getAsInteger(RIL_DATA_RADIO_TECHNOLOGY));
+            newSS.setCssIndicator(values.getAsInteger(CSS_INDICATOR));
+            newSS.setSystemAndNetworkId(values.getAsInteger(SYSTEM_ID),
+                    values.getAsInteger(NETWORK_ID));
+            newSS.setCdmaRoamingIndicator(values.getAsInteger(CDMA_ROAMING_INDICATOR));
+            newSS.setCdmaDefaultRoamingIndicator(
+                    values.getAsInteger(CDMA_DEFAULT_ROAMING_INDICATOR));
+            newSS.setCdmaEriIconIndex(values.getAsInteger(CDMA_ERI_ICON_INDEX));
+            newSS.setCdmaEriIconMode(values.getAsInteger(CDMA_ERI_ICON_MODE));
+            newSS.setEmergencyOnly(values.getAsBoolean(IS_EMERGENCY_ONLY));
+            newSS.setDataRoamingFromRegistration(
+                    values.getAsBoolean(IS_DATA_ROAMING_FROM_REGISTRATION));
+            newSS.setIsUsingCarrierAggregation(values.getAsBoolean(IS_USING_CARRIER_AGGREGATION));
+
+            // notify listeners
+            // if ss is null (e.g. first service state update) we will notify for all fields
+            ServiceState ss = getServiceState(subId);
+            notifyChangeForSubIdAndField(getContext(), ss, newSS, subId);
+            notifyChangeForSubId(getContext(), ss, newSS, subId);
+
+            // store the new service state
+            mServiceStates.put(subId, newSS);
+            return uri;
+        }
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new RuntimeException("Not supported");
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new RuntimeException("Not supported");
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new RuntimeException("Not supported");
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        if (!uri.isPathPrefixMatch(CONTENT_URI)) {
+            throw new IllegalArgumentException("Invalid URI: " + uri);
+        } else {
+            // Parse the subId
+            int subId = 0;
+            try {
+                subId = Integer.parseInt(uri.getLastPathSegment());
+            } catch (NumberFormatException e) {
+                Log.d(TAG, "query: no subId provided in uri");
+                throw e;
+            }
+            Log.d(TAG, "subId=" + subId);
+
+            // handle DEFAULT_SUBSCRIPTION_ID
+            if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+                subId = getDefaultSubId();
+            }
+
+            // Get the service state
+            ServiceState ss = getServiceState(subId);
+            if (ss == null) {
+                Log.d(TAG, "returning null");
+                return null;
+            }
+
+            // Build the result
+            final int voice_reg_state = ss.getVoiceRegState();
+            final int data_reg_state = ss.getDataRegState();
+            final int voice_roaming_type = ss.getVoiceRoamingType();
+            final int data_roaming_type = ss.getDataRoamingType();
+            final String voice_operator_alpha_long = ss.getVoiceOperatorAlphaLong();
+            final String voice_operator_alpha_short = ss.getVoiceOperatorAlphaShort();
+            final String voice_operator_numeric = ss.getVoiceOperatorNumeric();
+            final String data_operator_alpha_long = ss.getDataOperatorAlphaLong();
+            final String data_operator_alpha_short = ss.getDataOperatorAlphaShort();
+            final String data_operator_numeric = ss.getDataOperatorNumeric();
+            final int is_manual_network_selection = (ss.getIsManualSelection()) ? 1 : 0;
+            final int ril_voice_radio_technology = ss.getRilVoiceRadioTechnology();
+            final int ril_data_radio_technology = ss.getRilDataRadioTechnology();
+            final int css_indicator = ss.getCssIndicator();
+            final int network_id = ss.getNetworkId();
+            final int system_id = ss.getSystemId();
+            final int cdma_roaming_indicator = ss.getCdmaRoamingIndicator();
+            final int cdma_default_roaming_indicator = ss.getCdmaDefaultRoamingIndicator();
+            final int cdma_eri_icon_index = ss.getCdmaEriIconIndex();
+            final int cdma_eri_icon_mode = ss.getCdmaEriIconMode();
+            final int is_emergency_only = (ss.isEmergencyOnly()) ? 1 : 0;
+            final int is_data_roaming_from_registration =
+                    (ss.getDataRoamingFromRegistration()) ? 1 : 0;
+            final int is_using_carrier_aggregation = (ss.isUsingCarrierAggregation()) ? 1 : 0;
+
+            return buildSingleRowResult(projection, sColumns, new Object[] {
+                        voice_reg_state,
+                        data_reg_state,
+                        voice_roaming_type,
+                        data_roaming_type,
+                        voice_operator_alpha_long,
+                        voice_operator_alpha_short,
+                        voice_operator_numeric,
+                        data_operator_alpha_long,
+                        data_operator_alpha_short,
+                        data_operator_numeric,
+                        is_manual_network_selection,
+                        ril_voice_radio_technology,
+                        ril_data_radio_technology,
+                        css_indicator,
+                        network_id,
+                        system_id,
+                        cdma_roaming_indicator,
+                        cdma_default_roaming_indicator,
+                        cdma_eri_icon_index,
+                        cdma_eri_icon_mode,
+                        is_emergency_only,
+                        is_data_roaming_from_registration,
+                        is_using_carrier_aggregation,
+            });
+        }
+    }
+
+    private static Cursor buildSingleRowResult(String[] projection, String[] availableColumns,
+            Object[] data) {
+        if (projection == null) {
+            projection = availableColumns;
+        }
+        final MatrixCursor c = new MatrixCursor(projection, 1);
+        final RowBuilder row = c.newRow();
+        for (int i = 0; i < c.getColumnCount(); i++) {
+            final String columnName = c.getColumnName(i);
+            boolean found = false;
+            for (int j = 0; j < availableColumns.length; j++) {
+                if (availableColumns[j].equals(columnName)) {
+                    row.add(data[j]);
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                throw new IllegalArgumentException("Invalid column " + projection[i]);
+            }
+        }
+        return c;
+    }
+
+    /**
+     * Notify interested apps that certain fields of the ServiceState have changed.
+     *
+     * Apps which want to wake when specific fields change can use
+     * JobScheduler's TriggerContentUri.  This replaces the waking functionality of the implicit
+     * broadcast of ACTION_SERVICE_STATE_CHANGED for apps targetting version O.
+     *
+     * We will only notify for certain fields. This is an intentional change from the behavior of
+     * the broadcast. Listeners will be notified when the voice or data registration state or
+     * roaming type changes.
+     */
+    @VisibleForTesting
+    public static void notifyChangeForSubIdAndField(Context context, ServiceState oldSS,
+            ServiceState newSS, int subId) {
+        final boolean firstUpdate = (oldSS == null) ? true : false;
+
+        // for every field, if the field has changed values, notify via the provider
+        if (firstUpdate || voiceRegStateChanged(oldSS, newSS)) {
+            context.getContentResolver().notifyChange(
+                    getUriForSubIdAndField(subId, VOICE_REG_STATE),
+                    /* observer= */ null, /* syncToNetwork= */ false);
+        }
+        if (firstUpdate || dataRegStateChanged(oldSS, newSS)) {
+            context.getContentResolver().notifyChange(
+                    getUriForSubIdAndField(subId, DATA_REG_STATE), null, false);
+        }
+        if (firstUpdate || voiceRoamingTypeChanged(oldSS, newSS)) {
+            context.getContentResolver().notifyChange(
+                    getUriForSubIdAndField(subId, VOICE_ROAMING_TYPE), null, false);
+        }
+        if (firstUpdate || dataRoamingTypeChanged(oldSS, newSS)) {
+            context.getContentResolver().notifyChange(
+                    getUriForSubIdAndField(subId, DATA_ROAMING_TYPE), null, false);
+        }
+    }
+
+    private static boolean voiceRegStateChanged(ServiceState oldSS, ServiceState newSS) {
+        return oldSS.getVoiceRegState() != newSS.getVoiceRegState();
+    }
+
+    private static boolean dataRegStateChanged(ServiceState oldSS, ServiceState newSS) {
+        return oldSS.getDataRegState() != newSS.getDataRegState();
+    }
+
+    private static boolean voiceRoamingTypeChanged(ServiceState oldSS, ServiceState newSS) {
+        return oldSS.getVoiceRoamingType() != newSS.getVoiceRoamingType();
+    }
+
+    private static boolean dataRoamingTypeChanged(ServiceState oldSS, ServiceState newSS) {
+        return oldSS.getDataRoamingType() != newSS.getDataRoamingType();
+    }
+
+    /**
+     * Notify interested apps that the ServiceState has changed.
+     *
+     * Apps which want to wake when any field in the ServiceState has changed can use
+     * JobScheduler's TriggerContentUri.  This replaces the waking functionality of the implicit
+     * broadcast of ACTION_SERVICE_STATE_CHANGED for apps targetting version O.
+     *
+     * We will only notify for certain fields. This is an intentional change from the behavior of
+     * the broadcast. Listeners will be notified when the voice or data registration state or
+     * roaming type changes.
+     */
+    @VisibleForTesting
+    public static void notifyChangeForSubId(Context context, ServiceState oldSS, ServiceState newSS,
+            int subId) {
+        // if the voice or data registration or roaming state field has changed values, notify via
+        // the provider.
+        // If oldSS is null and newSS is not (e.g. first update of service state) this will also
+        // notify
+        if (oldSS == null || voiceRegStateChanged(oldSS, newSS) || dataRegStateChanged(oldSS, newSS)
+                || voiceRoamingTypeChanged(oldSS, newSS) || dataRoamingTypeChanged(oldSS, newSS)) {
+            context.getContentResolver().notifyChange(getUriForSubId(subId), null, false);
+        }
+    }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
index 9af28ab..89136f3 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -3,7 +3,10 @@
 
 LOCAL_MODULE_TAGS := tests
 
-LOCAL_STATIC_JAVA_LIBRARIES := mockito-target legacy-android-test
+LOCAL_STATIC_JAVA_LIBRARIES := mockito-target \
+                               legacy-android-test \
+                               compatibility-device-util \
+                               android-support-test
 
 LOCAL_JAVA_LIBRARIES := android.test.runner telephony-common
 
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 7a273fc..91e8953 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -21,7 +21,7 @@
         <uses-library android:name="android.test.runner" />
     </application>
 
-    <instrumentation android:name="android.test.InstrumentationTestRunner"
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.providers.telephony"
         android:label="Tests for TelephonyProvider">
     </instrumentation>
diff --git a/tests/src/com/android/providers/telephony/ServiceStateProviderTest.java b/tests/src/com/android/providers/telephony/ServiceStateProviderTest.java
new file mode 100644
index 0000000..980190f
--- /dev/null
+++ b/tests/src/com/android/providers/telephony/ServiceStateProviderTest.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2016 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 com.android.providers.telephony;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ProviderInfo;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import com.android.internal.telephony.CarrierActionAgent;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.GsmCdmaPhone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.ServiceStateTracker;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import static android.app.job.JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS;
+import static android.Manifest.permission.MODIFY_PHONE_STATE;
+import static android.provider.Telephony.ServiceStateTable;
+import static android.provider.Telephony.ServiceStateTable.getUriForSubId;
+import static android.provider.Telephony.ServiceStateTable.getContentValuesForServiceState;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for simple queries of ServiceStateProvider.
+ *
+ * Build, install and run the tests by running the commands below:
+ *     runtest --path <dir or file>
+ *     runtest --path <dir or file> --test-method <testMethodName>
+ *     e.g.)
+ *         runtest --path tests/src/com/android/providers/telephony/ServiceStateProviderTest.java \
+ *                 --test-method testGetServiceState
+ */
+public class ServiceStateProviderTest {
+    private static final String TAG = "ServiceStateProviderTest";
+
+    private Context mContext;
+    private MockContentResolver mContentResolver;
+    private ServiceState testServiceState;
+    private ServiceState testServiceStateForSubId1;
+
+    private final String[] testProjection =
+    {
+        ServiceStateTable.VOICE_REG_STATE,
+        ServiceStateTable.DATA_REG_STATE,
+        ServiceStateTable.VOICE_OPERATOR_ALPHA_LONG,
+        ServiceStateTable.VOICE_OPERATOR_ALPHA_SHORT,
+        ServiceStateTable.VOICE_OPERATOR_NUMERIC,
+        ServiceStateTable.DATA_OPERATOR_ALPHA_LONG,
+        ServiceStateTable.DATA_OPERATOR_ALPHA_SHORT,
+        ServiceStateTable.DATA_OPERATOR_NUMERIC,
+        ServiceStateTable.IS_MANUAL_NETWORK_SELECTION,
+        ServiceStateTable.RIL_VOICE_RADIO_TECHNOLOGY,
+        ServiceStateTable.RIL_DATA_RADIO_TECHNOLOGY,
+        ServiceStateTable.CSS_INDICATOR,
+        ServiceStateTable.NETWORK_ID,
+        ServiceStateTable.SYSTEM_ID,
+        ServiceStateTable.CDMA_ROAMING_INDICATOR,
+        ServiceStateTable.CDMA_DEFAULT_ROAMING_INDICATOR,
+        ServiceStateTable.CDMA_ERI_ICON_INDEX,
+        ServiceStateTable.CDMA_ERI_ICON_MODE,
+        ServiceStateTable.IS_EMERGENCY_ONLY,
+        ServiceStateTable.IS_DATA_ROAMING_FROM_REGISTRATION,
+        ServiceStateTable.IS_USING_CARRIER_AGGREGATION,
+    };
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = mock(Context.class);
+        mContentResolver = new MockContentResolver() {
+            @Override
+            public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+                throw new RuntimeException("notifyChange!");
+            }
+        };
+        doReturn(mContentResolver).when(mContext).getContentResolver();
+
+        testServiceState = new ServiceState();
+        testServiceState.setStateOutOfService();
+        testServiceStateForSubId1 = new ServiceState();
+        testServiceStateForSubId1.setStateOff();
+
+        // Mock out the actual phone state
+        ServiceStateProvider provider = new ServiceStateProvider() {
+            @Override
+            public ServiceState getServiceState(int subId) {
+                if (subId == 1) {
+                    return testServiceStateForSubId1;
+                } else {
+                    return testServiceState;
+                }
+            }
+
+            @Override
+            public int getDefaultSubId() {
+                return 0;
+            }
+        };
+        ProviderInfo providerInfo = new ProviderInfo();
+        providerInfo.authority = "service-state";
+        provider.attachInfoForTesting(mContext, providerInfo);
+        mContentResolver.addProvider("service-state", provider);
+    }
+
+    @Test
+    @SmallTest
+    public void testGetServiceStateWithDefaultSubId() {
+        // Verify that when calling with the DEFAULT_SUBSCRIPTION_ID the correct ServiceState is
+        // returned
+        // In this case the subId is set to 0 and the expected service state is
+        // testServiceState
+        verifyServiceStateForSubId(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, testServiceState);
+    }
+
+    /**
+     * Test querying the service state for a given subId
+     */
+    @Test
+    @SmallTest
+    public void testGetServiceStateForSubId() {
+        // Verify that when calling with a specific subId the correct ServiceState is returned
+        // In this case the subId is set to 1 and the expected service state is
+        // testServiceStateForSubId1
+        verifyServiceStateForSubId(1, testServiceStateForSubId1);
+    }
+
+    private void verifyServiceStateForSubId(int subId, ServiceState ss) {
+        Cursor cursor = mContentResolver.query(getUriForSubId(subId), testProjection, "", null,
+                null);
+        assertNotNull(cursor);
+        cursor.moveToFirst();
+
+        final int voiceRegState = ss.getVoiceRegState();
+        final int dataRegState = ss.getDataRegState();
+        final String voiceOperatorAlphaLong = ss.getVoiceOperatorAlphaLong();
+        final String voiceOperatorAlphaShort = ss.getVoiceOperatorAlphaShort();
+        final String voiceOperatorNumeric = ss.getVoiceOperatorNumeric();
+        final String dataOperatorAlphaLong = ss.getDataOperatorAlphaLong();
+        final String dataOperatorAlphaShort = ss.getDataOperatorAlphaShort();
+        final String dataOperatorNumeric = ss.getDataOperatorNumeric();
+        final int isManualNetworkSelection = (ss.getIsManualSelection()) ? 1 : 0;
+        final int rilVoiceRadioTechnology = ss.getRilVoiceRadioTechnology();
+        final int rilDataRadioTechnology = ss.getRilDataRadioTechnology();
+        final int cssIndicator = ss.getCssIndicator();
+        final int networkId = ss.getNetworkId();
+        final int systemId = ss.getSystemId();
+        final int cdmaRoamingIndicator = ss.getCdmaRoamingIndicator();
+        final int cdmaDefaultRoamingIndicator = ss.getCdmaDefaultRoamingIndicator();
+        final int cdmaEriIconIndex = ss.getCdmaEriIconIndex();
+        final int cdmaEriIconMode = ss.getCdmaEriIconMode();
+        final int isEmergencyOnly = (ss.isEmergencyOnly()) ? 1 : 0;
+        final int isDataRoamingFromRegistration = (ss.getDataRoamingFromRegistration()) ? 1 : 0;
+        final int isUsingCarrierAggregation = (ss.isUsingCarrierAggregation()) ? 1 : 0;
+
+        assertEquals(voiceRegState, cursor.getInt(0));
+        assertEquals(dataRegState, cursor.getInt(1));
+        assertEquals(voiceOperatorAlphaLong, cursor.getString(2));
+        assertEquals(voiceOperatorAlphaShort, cursor.getString(3));
+        assertEquals(voiceOperatorNumeric, cursor.getString(4));
+        assertEquals(dataOperatorAlphaLong, cursor.getString(5));
+        assertEquals(dataOperatorAlphaShort, cursor.getString(6));
+        assertEquals(dataOperatorNumeric, cursor.getString(7));
+        assertEquals(isManualNetworkSelection, cursor.getInt(8));
+        assertEquals(rilVoiceRadioTechnology, cursor.getInt(9));
+        assertEquals(rilDataRadioTechnology, cursor.getInt(10));
+        assertEquals(cssIndicator, cursor.getInt(11));
+        assertEquals(networkId, cursor.getInt(12));
+        assertEquals(systemId, cursor.getInt(13));
+        assertEquals(cdmaRoamingIndicator, cursor.getInt(14));
+        assertEquals(cdmaDefaultRoamingIndicator, cursor.getInt(15));
+        assertEquals(cdmaEriIconIndex, cursor.getInt(16));
+        assertEquals(cdmaEriIconMode, cursor.getInt(17));
+        assertEquals(isEmergencyOnly, cursor.getInt(18));
+        assertEquals(isDataRoamingFromRegistration, cursor.getInt(19));
+        assertEquals(isUsingCarrierAggregation, cursor.getInt(20));
+    }
+
+    /**
+     * Test that we don't notify for certain field changes. (e.g. we don't notify when the NetworkId
+     * or SystemId change) This is an intentional behavior change from the broadcast.
+     */
+    @Test
+    @SmallTest
+    public void testNoNotify() {
+        int subId = 0;
+
+        ServiceState oldSS = new ServiceState();
+        oldSS.setStateOutOfService();
+        oldSS.setSystemAndNetworkId(1, 1);
+
+        ServiceState newSS = new ServiceState();
+        newSS.setStateOutOfService();
+        newSS.setSystemAndNetworkId(0, 0);
+
+        // Test that notifyChange is not called for these fields
+        boolean notifyChangeWasCalled = false;
+        try {
+            ServiceStateProvider.notifyChangeForSubIdAndField(mContext, oldSS, newSS, subId);
+        } catch (RuntimeException e) {
+            final String message = e.getMessage();
+            if (message != null &&  message.equals("notifyChange!")) {
+                notifyChangeWasCalled = true;
+            }
+        }
+        assertFalse(notifyChangeWasCalled);
+    }
+
+    @Test
+    @SmallTest
+    public void testNotifyChanged() {
+        int subId = 0;
+
+        ServiceState oldSS = new ServiceState();
+        oldSS.setStateOutOfService();
+        oldSS.setVoiceRegState(ServiceState.STATE_OUT_OF_SERVICE);
+
+        ServiceState copyOfOldSS = new ServiceState();
+        copyOfOldSS.setStateOutOfService();
+        copyOfOldSS.setVoiceRegState(ServiceState.STATE_OUT_OF_SERVICE);
+
+        ServiceState newSS = new ServiceState();
+        newSS.setStateOutOfService();
+        newSS.setVoiceRegState(ServiceState.STATE_POWER_OFF);
+
+        // Test that notifyChange is not called with no change in notifyChangeForSubIdAndField
+        boolean notifyChangeWasCalled = false;
+        try {
+            ServiceStateProvider.notifyChangeForSubIdAndField(mContext, oldSS, copyOfOldSS, subId);
+        } catch (RuntimeException e) {
+            final String message = e.getMessage();
+            if (message != null &&  message.equals("notifyChange!")) {
+                notifyChangeWasCalled = true;
+            }
+        }
+        assertFalse(notifyChangeWasCalled);
+
+        // Test that notifyChange is not called with no change in notifyChangeForSubId
+        notifyChangeWasCalled = false;
+        try {
+            ServiceStateProvider.notifyChangeForSubId(mContext, oldSS, copyOfOldSS, subId);
+        } catch (RuntimeException e) {
+            final String message = e.getMessage();
+            if (message != null &&  message.equals("notifyChange!")) {
+                notifyChangeWasCalled = true;
+            }
+        }
+        assertFalse(notifyChangeWasCalled);
+
+        // Test that notifyChange is called by notifyChangeForSubIdAndField when the voice_reg_state
+        // changes
+        notifyChangeWasCalled = false;
+        try {
+            ServiceStateProvider.notifyChangeForSubIdAndField(mContext, oldSS, newSS, subId);
+        } catch (RuntimeException e) {
+            final String message = e.getMessage();
+            if (message != null &&  message.equals("notifyChange!")) {
+                notifyChangeWasCalled = true;
+            }
+        }
+        assertTrue(notifyChangeWasCalled);
+
+        // Test that notifyChange is called by notifyChangeForSubId when the voice_reg_state changes
+        notifyChangeWasCalled = false;
+        try {
+            ServiceStateProvider.notifyChangeForSubId(mContext, oldSS, newSS, subId);
+        } catch (RuntimeException e) {
+            final String message = e.getMessage();
+            if (message != null &&  message.equals("notifyChange!")) {
+                notifyChangeWasCalled = true;
+            }
+        }
+        assertTrue(notifyChangeWasCalled);
+    }
+}