|  | /* | 
|  | * Copyright (C) 2019 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.phone; | 
|  |  | 
|  | import static android.provider.Telephony.ServiceStateTable; | 
|  | 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.CDMA_ROAMING_INDICATOR; | 
|  | import static android.provider.Telephony.ServiceStateTable.CONTENT_URI; | 
|  | import static android.provider.Telephony.ServiceStateTable.CSS_INDICATOR; | 
|  | 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.DATA_REG_STATE; | 
|  | import static android.provider.Telephony.ServiceStateTable.DATA_ROAMING_TYPE; | 
|  | import static android.provider.Telephony.ServiceStateTable.IS_EMERGENCY_ONLY; | 
|  | import static android.provider.Telephony.ServiceStateTable.IS_MANUAL_NETWORK_SELECTION; | 
|  | import static android.provider.Telephony.ServiceStateTable.IS_USING_CARRIER_AGGREGATION; | 
|  | import static android.provider.Telephony.ServiceStateTable.NETWORK_ID; | 
|  | import static android.provider.Telephony.ServiceStateTable.OPERATOR_ALPHA_LONG_RAW; | 
|  | import static android.provider.Telephony.ServiceStateTable.OPERATOR_ALPHA_SHORT_RAW; | 
|  | import static android.provider.Telephony.ServiceStateTable.RIL_DATA_RADIO_TECHNOLOGY; | 
|  | import static android.provider.Telephony.ServiceStateTable.RIL_VOICE_RADIO_TECHNOLOGY; | 
|  | import static android.provider.Telephony.ServiceStateTable.SERVICE_STATE; | 
|  | import static android.provider.Telephony.ServiceStateTable.SYSTEM_ID; | 
|  | 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.VOICE_REG_STATE; | 
|  | import static android.provider.Telephony.ServiceStateTable.VOICE_ROAMING_TYPE; | 
|  | import static android.provider.Telephony.ServiceStateTable.getUriForSubscriptionId; | 
|  | import static android.provider.Telephony.ServiceStateTable.getUriForSubscriptionIdAndField; | 
|  |  | 
|  | 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.os.Parcel; | 
|  | import android.telephony.ServiceState; | 
|  | import android.telephony.SubscriptionManager; | 
|  | import android.util.Log; | 
|  |  | 
|  | import com.android.internal.annotations.VisibleForTesting; | 
|  |  | 
|  | import java.util.HashMap; | 
|  |  | 
|  | /** | 
|  | * The class to provide base facility to access ServiceState related content, | 
|  | * which is stored in a SQLite database. | 
|  | */ | 
|  | 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_USING_CARRIER_AGGREGATION, | 
|  | OPERATOR_ALPHA_LONG_RAW, | 
|  | OPERATOR_ALPHA_SHORT_RAW, | 
|  | }; | 
|  |  | 
|  | @Override | 
|  | public boolean onCreate() { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the {@link ServiceState} information on specified subscription. | 
|  | * | 
|  | * @param subId whose subscriber id is returned | 
|  | * @return the {@link ServiceState} information on specified subscription. | 
|  | */ | 
|  | @VisibleForTesting | 
|  | public ServiceState getServiceState(int subId) { | 
|  | return mServiceStates.get(subId); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the system's default subscription id. | 
|  | * | 
|  | * @return the "system" default subscription id. | 
|  | */ | 
|  | @VisibleForTesting | 
|  | public int getDefaultSubId() { | 
|  | return SubscriptionManager.getDefaultSubscriptionId(); | 
|  | } | 
|  |  | 
|  | @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.e(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(); | 
|  | } | 
|  |  | 
|  | final Parcel p = Parcel.obtain(); | 
|  | final byte[] rawBytes = values.getAsByteArray(SERVICE_STATE); | 
|  | p.unmarshall(rawBytes, 0, rawBytes.length); | 
|  | p.setDataPosition(0); | 
|  |  | 
|  | // create the new service state | 
|  | final ServiceState newSS = ServiceState.CREATOR.createFromParcel(p); | 
|  |  | 
|  | // 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, using default."); | 
|  | subId = getDefaultSubId(); | 
|  | } | 
|  | 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.getOperatorAlphaLong(); | 
|  | final String voice_operator_alpha_short = ss.getOperatorAlphaShort(); | 
|  | final String voice_operator_numeric = ss.getOperatorNumeric(); | 
|  | final String data_operator_alpha_long = ss.getOperatorAlphaLong(); | 
|  | final String data_operator_alpha_short = ss.getOperatorAlphaShort(); | 
|  | final String data_operator_numeric = ss.getOperatorNumeric(); | 
|  | 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.getCdmaNetworkId(); | 
|  | final int system_id = ss.getCdmaSystemId(); | 
|  | 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_using_carrier_aggregation = (ss.isUsingCarrierAggregation()) ? 1 : 0; | 
|  | final String operator_alpha_long_raw = ss.getOperatorAlphaLongRaw(); | 
|  | final String operator_alpha_short_raw = ss.getOperatorAlphaShortRaw(); | 
|  |  | 
|  | 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_using_carrier_aggregation, | 
|  | operator_alpha_long_raw, | 
|  | operator_alpha_short_raw, | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | 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 targeting 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( | 
|  | getUriForSubscriptionIdAndField(subId, VOICE_REG_STATE), | 
|  | /* observer= */ null, /* syncToNetwork= */ false); | 
|  | } | 
|  | if (firstUpdate || dataRegStateChanged(oldSS, newSS)) { | 
|  | context.getContentResolver().notifyChange( | 
|  | getUriForSubscriptionIdAndField(subId, DATA_REG_STATE), null, false); | 
|  | } | 
|  | if (firstUpdate || voiceRoamingTypeChanged(oldSS, newSS)) { | 
|  | context.getContentResolver().notifyChange( | 
|  | getUriForSubscriptionIdAndField(subId, VOICE_ROAMING_TYPE), null, false); | 
|  | } | 
|  | if (firstUpdate || dataRoamingTypeChanged(oldSS, newSS)) { | 
|  | context.getContentResolver().notifyChange( | 
|  | getUriForSubscriptionIdAndField(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 targeting version O. | 
|  | * | 
|  | * We will only notify for certain fields. This is an intentional change from the behavior of | 
|  | * the broadcast. Listeners will only be notified when the voice/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(getUriForSubscriptionId(subId), null, false); | 
|  | } | 
|  | } | 
|  | } |