DO NOT MERGE - Merge pi-dev@5234907 into stage-aosp-master
Bug: 120848293
Change-Id: I2516ba0c0bd728d9132b9551179180c8132153d2
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ce774d8..f68b72e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -22,10 +22,6 @@
android:sharedUserLabel="@string/phoneAppLabel"
>
- <uses-sdk
- android:minSdkVersion="23"
- android:targetSdkVersion="26" />
-
<original-package android:name="com.android.phone" />
<protected-broadcast android:name="android.telecom.action.TTY_PREFERRED_MODE_CHANGED" />
@@ -92,6 +88,10 @@
<protected-broadcast android:name= "android.telephony.action.SIM_SLOT_STATUS_CHANGED" />
<protected-broadcast android:name= "android.telephony.action.SUBSCRIPTION_CARRIER_IDENTITY_CHANGED" />
<protected-broadcast android:name= "android.telephony.action.SUBSCRIPTION_PRECISE_CARRIER_IDENTITY_CHANGED" />
+ <protected-broadcast android:name= "android.telephony.action.NETWORK_COUNTRY_CHANGED" />
+
+ <!-- For Vendor Debugging in Telephony -->
+ <protected-broadcast android:name="android.telephony.debug.action.DEBUG_EVENT" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.CALL_PHONE" />
@@ -194,6 +194,7 @@
<uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
<uses-permission android:name="android.permission.BIND_TELEPHONY_DATA_SERVICE" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+ <uses-permission android:name="android.permission.READ_PRECISE_PHONE_STATE" />
<application android:name="PhoneApp"
android:persistent="true"
diff --git a/ecc/README.md b/ecc/README.md
index b5dc538..304cdfb 100644
--- a/ecc/README.md
+++ b/ecc/README.md
@@ -30,4 +30,4 @@
4. Make TeleService
5. Push TeleService.apk to system/priv-app/TeleService
6. Reboot device
-7. run 'atest TeleServiceTests:IsoToEccProtobufRepositoryTest#testEccDataContent'
+7. run 'atest TeleServiceTests:EccDataTest#testEccDataContent'
diff --git a/ecc/gen_eccdata.sh b/ecc/gen_eccdata.sh
index 4f3a097..e4dd745 100755
--- a/ecc/gen_eccdata.sh
+++ b/ecc/gen_eccdata.sh
@@ -57,4 +57,4 @@
echo " 1. make TeleService"
echo " 2. push TeleService.apk to system/priv-app/TeleService"
echo " 3. reboot device"
-echo " 4. run 'atest TeleServiceTests:IsoToEccProtobufRepositoryTest#testEccDataContent'"
+echo " 4. run 'atest TeleServiceTests:EccDataTest#testEccDataContent'"
diff --git a/res/layout/emergency_shortcut_buttons_group.xml b/res/layout/emergency_shortcut_buttons_group.xml
index 7911f30..54563c9 100644
--- a/res/layout/emergency_shortcut_buttons_group.xml
+++ b/res/layout/emergency_shortcut_buttons_group.xml
@@ -77,4 +77,4 @@
android:divider="@drawable/emergency_shortcuts_divider"
android:showDividers="middle">
</LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/res/layout/pref_dialog_editpin.xml b/res/layout/pref_dialog_editpin.xml
index a278690..94cdadf 100644
--- a/res/layout/pref_dialog_editpin.xml
+++ b/res/layout/pref_dialog_editpin.xml
@@ -22,7 +22,7 @@
android:orientation="vertical"
android:padding="?android:attr/dialogPreferredPadding">
- <TextView android:id="@+android:id/message"
+ <TextView android:id="@android:id/message"
style="?android:attr/textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/res/values/config.xml b/res/values/config.xml
index b1f8ae8..6b6bf04 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -254,6 +254,7 @@
<!-- Intent action to launch target emergency app. -->
<string name="config_emergency_app_intent" translatable="false"></string>
- <!-- Flag indicating whether shortcut view of promoted emergency numbers should be enabled. -->
- <bool name="config_emergency_shortcut_view_enabled">false</bool>
+ <!-- The country list that shortcut view can be enabled. -->
+ <string-array name="config_countries_to_enable_shortcut_view" translatable="false">
+ </string-array>
</resources>
diff --git a/res/xml/phone_account_settings.xml b/res/xml/phone_account_settings.xml
index ae3e9d9..d230328 100644
--- a/res/xml/phone_account_settings.xml
+++ b/res/xml/phone_account_settings.xml
@@ -29,6 +29,7 @@
android:order="1" />
<PreferenceScreen
+ android:key="phone_accounts_all_calling_accounts"
android:title="@string/phone_accounts_all_calling_accounts"
android:summary="@string/phone_accounts_all_calling_accounts_summary"
android:persistent="false"
diff --git a/sip/src/com/android/services/telephony/sip/SipUtil.java b/sip/src/com/android/services/telephony/sip/SipUtil.java
index d674225..ff38754 100644
--- a/sip/src/com/android/services/telephony/sip/SipUtil.java
+++ b/sip/src/com/android/services/telephony/sip/SipUtil.java
@@ -134,7 +134,7 @@
.setAddress(sipUri)
.setShortDescription(sipAddress)
.setIcon(Icon.createWithResource(
- context.getResources(), R.drawable.ic_dialer_sip_black_24dp))
+ context, R.drawable.ic_dialer_sip_black_24dp))
.setExtras(phoneAccountExtras)
.setSupportedUriSchemes(supportedUriSchemes);
diff --git a/src/com/android/phone/CallFeaturesSetting.java b/src/com/android/phone/CallFeaturesSetting.java
index c1bbf2d..6c69a33 100644
--- a/src/com/android/phone/CallFeaturesSetting.java
+++ b/src/com/android/phone/CallFeaturesSetting.java
@@ -38,6 +38,7 @@
import android.telephony.CarrierConfigManager;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
+import android.telephony.ims.ProvisioningManager;
import android.telephony.ims.feature.ImsFeature;
import android.util.Log;
import android.view.MenuItem;
@@ -214,10 +215,32 @@
}
};
+ private final ProvisioningManager.Callback mProvisioningCallback =
+ new ProvisioningManager.Callback() {
+ @Override
+ public void onProvisioningIntChanged(int item, int value) {
+ if (item == ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED
+ || item == ImsConfig.ConfigConstants.VLT_SETTING_ENABLED
+ || item == ImsConfig.ConfigConstants.LVC_SETTING_ENABLED) {
+ updateVtWfc();
+ }
+ }
+ };
+
@Override
protected void onPause() {
super.onPause();
listenPhoneState(false);
+
+ // Remove callback for provisioning changes.
+ try {
+ if (mImsMgr != null) {
+ mImsMgr.getConfigInterface().removeConfigCallback(
+ mProvisioningCallback.getBinder());
+ }
+ } catch (ImsException e) {
+ Log.w(LOG_TAG, "onPause: Unable to remove callback for provisioning changes");
+ }
}
@Override
@@ -236,6 +259,17 @@
TelephonyManager telephonyManager = getSystemService(TelephonyManager.class)
.createForSubscriptionId(mPhone.getSubId());
+ // Note: The PhoneAccountSettingsActivity accessible via the
+ // android.telecom.action.CHANGE_PHONE_ACCOUNTS intent is accessible directly from
+ // the AOSP Dialer settings page on multi-sim devices.
+ // Where a device does NOT make the PhoneAccountSettingsActivity directly accessible from
+ // its Dialer app, this check must be modified in the device's AOSP branch to ensure that
+ // the PhoneAccountSettingsActivity is always accessible.
+ if (telephonyManager.isMultiSimEnabled()) {
+ Preference phoneAccountSettingsPreference = findPreference(PHONE_ACCOUNT_SETTINGS_KEY);
+ getPreferenceScreen().removePreference(phoneAccountSettingsPreference);
+ }
+
PreferenceScreen prefSet = getPreferenceScreen();
mVoicemailSettingsScreen =
(PreferenceScreen) findPreference(VOICEMAIL_SETTING_SCREEN_PREF_KEY);
@@ -299,7 +333,24 @@
}
}
}
+ updateVtWfc();
+ // Register callback for provisioning changes.
+ try {
+ if (mImsMgr != null) {
+ mImsMgr.getConfigInterface().addConfigCallback(mProvisioningCallback);
+ }
+ } catch (ImsException e) {
+ Log.w(LOG_TAG, "onResume: Unable to register callback for provisioning changes.");
+ }
+ }
+
+ private void updateVtWfc() {
+ PreferenceScreen prefSet = getPreferenceScreen();
+ TelephonyManager telephonyManager = getSystemService(TelephonyManager.class)
+ .createForSubscriptionId(mPhone.getSubId());
+ PersistableBundle carrierConfig =
+ PhoneGlobals.getInstance().getCarrierConfigForSubId(mPhone.getSubId());
if (mImsMgr.isVtEnabledByPlatform() && mImsMgr.isVtProvisionedOnDevice()
&& (carrierConfig.getBoolean(
CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS)
@@ -309,6 +360,7 @@
? mImsMgr.isVtEnabledByUser() : false;
mEnableVideoCalling.setChecked(currentValue);
mEnableVideoCalling.setOnPreferenceChangeListener(this);
+ prefSet.addPreference(mEnableVideoCalling);
} else {
prefSet.removePreference(mEnableVideoCalling);
}
@@ -324,6 +376,7 @@
mButtonWifiCalling.setTitle(resolutions.get(0).loadLabel(pm));
mButtonWifiCalling.setSummary(null);
mButtonWifiCalling.setIntent(intent);
+ prefSet.addPreference(mButtonWifiCalling);
} else {
prefSet.removePreference(mButtonWifiCalling);
}
@@ -352,6 +405,7 @@
}
}
mButtonWifiCalling.setSummary(resId);
+ prefSet.addPreference(mButtonWifiCalling);
}
try {
diff --git a/src/com/android/phone/EccShortcutAdapter.java b/src/com/android/phone/EccShortcutAdapter.java
index e14b90a..19b1fec 100644
--- a/src/com/android/phone/EccShortcutAdapter.java
+++ b/src/com/android/phone/EccShortcutAdapter.java
@@ -17,6 +17,7 @@
package com.android.phone;
import android.content.Context;
+import android.telephony.emergency.EmergencyNumber;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
@@ -25,9 +26,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.phone.ecc.CountryEccInfo;
-import com.android.phone.ecc.EccInfo;
-
import com.google.common.collect.LinkedListMultimap;
import java.util.ArrayList;
@@ -35,15 +33,11 @@
/**
* An abstract adapter between ECC data and the view contains ECC shortcuts.
- * This adapter will convert given {@link CountryEccInfo} to number string, description string and
- * icon resource id for each {@link EccInfo}.
+ * This adapter prepares description and icon for every promoted emergency number.
* The subclass should implements {@link #inflateView} to provide the view for an ECC data, when the
* view container calls {@link #getView}.
*/
public abstract class EccShortcutAdapter extends BaseAdapter {
- // GSM default emergency number, used when country's fallback ECC(112 or 911) not available.
- private static final String FALLBACK_EMERGENCY_NUMBER = "112";
-
private List<EccDisplayMaterial> mEccDisplayMaterialList;
private CharSequence mPoliceDescription;
@@ -90,16 +84,16 @@
* Get a View that display the given ECC data: number, description and iconRes.
*
* @param convertView The old view to reuse, if possible. Note: You should check that this view
- * is non-null and of an appropriate type before using. If it is not possible
- * to convert this view to display the correct data, this method can create a
- * new view. Heterogeneous lists can specify their number of view types, so
- * that this View is always of the right type (see {@link
- * BaseAdapter#getViewTypeCount()} and {@link
- * BaseAdapter#getItemViewType(int)}).
- * @param parent The parent that this view will eventually be attached to.
- * @param number The number of the ECC shortcut to display in the view.
+ * is non-null and of an appropriate type before using. If it is not possible
+ * to convert this view to display the correct data, this method can create a
+ * new view. Heterogeneous lists can specify their number of view types, so
+ * that this View is always of the right type (see {@link
+ * BaseAdapter#getViewTypeCount()} and {@link
+ * BaseAdapter#getItemViewType(int)}).
+ * @param parent The parent that this view will eventually be attached to.
+ * @param number The number of the ECC shortcut to display in the view.
* @param description The description of the ECC shortcut to display in the view.
- * @param iconRes The icon resource ID represent for the ECC shortcut.
+ * @param iconRes The icon resource ID represent for the ECC shortcut.
* @return A View corresponding to the data at the specified position.
*/
public abstract View inflateView(View convertView, ViewGroup parent, CharSequence number,
@@ -110,89 +104,68 @@
* be display by the short container View.
*
* @param context The context used to access resources.
- * @param countryEccInfo Updated country ECC info.
+ * @param phoneInfo Information of the phone to make an emergency call.
*/
- public void updateCountryEccInfo(@NonNull Context context, CountryEccInfo countryEccInfo) {
+ public void updateCountryEccInfo(@NonNull Context context,
+ @Nullable ShortcutViewUtils.PhoneInfo phoneInfo) {
List<EccDisplayMaterial> displayMaterials = new ArrayList<>();
- final EccInfo.Type[] orderedMustHaveTypes =
- { EccInfo.Type.POLICE, EccInfo.Type.AMBULANCE, EccInfo.Type.FIRE };
-
- String fallback = null;
- EccInfo[] eccInfoList = null;
- if (countryEccInfo != null) {
- fallback = countryEccInfo.getFallbackEcc();
- eccInfoList = countryEccInfo.getEccInfoList();
- }
- if (TextUtils.isEmpty(fallback)) {
- fallback = FALLBACK_EMERGENCY_NUMBER;
- }
-
- // Finding matched ECC for each must have types.
- // Using LinkedListMultimap to prevent duplicated keys.
- // LinkedListMultimap also preserve the insertion order of keys (ECC number) and values
- // (matched types of the ECC number), which follows the order in orderedMustHaveTypes.
- LinkedListMultimap<String, EccInfo.Type> eccList = LinkedListMultimap.create();
- for (EccInfo.Type type : orderedMustHaveTypes) {
- String number = null;
- if (eccInfoList != null) {
- number = pickEccNumberForType(type, eccInfoList);
+ try {
+ if (phoneInfo == null) {
+ return;
}
- if (number == null) {
- number = fallback;
- }
- // append type for exist number, otherwise insert a new entry.
- eccList.put(number, type);
- }
- // prepare display material for picked ECC
- for (String number : eccList.keySet()) {
- EccDisplayMaterial material = prepareDisplayMaterialForEccInfo(context,
- new EccInfo(number, eccList.asMap().get(number)));
- if (material != null) {
- displayMaterials.add(material);
- }
- }
-
- mEccDisplayMaterialList = displayMaterials;
- notifyDataSetChanged();
- }
-
- private @Nullable String pickEccNumberForType(@NonNull EccInfo.Type targetType,
- @NonNull EccInfo[] eccInfoList) {
- EccInfo pickedEccInfo = null;
- for (EccInfo eccInfo : eccInfoList) {
- if (eccInfo.containsType(targetType)) {
- // An ECC is more suitable for a type if the ECC has fewer other types.
- if (pickedEccInfo == null
- || eccInfo.getTypesCount() < pickedEccInfo.getTypesCount()) {
- pickedEccInfo = eccInfo;
+ LinkedListMultimap<String, Integer> emergencyNumbers = LinkedListMultimap.create();
+ for (int category : ShortcutViewUtils.PROMOTED_CATEGORIES) {
+ String number = pickEmergencyNumberForCategory(category,
+ phoneInfo.getPromotedEmergencyNumbers());
+ if (number != null) {
+ emergencyNumbers.put(number, category);
}
}
+
+ // prepare display material for picked ECC
+ for (String number : emergencyNumbers.keySet()) {
+ EccDisplayMaterial material = prepareEccMaterial(context, number,
+ emergencyNumbers.get(number));
+ if (material != null) {
+ displayMaterials.add(material);
+ }
+ }
+ } finally {
+ mEccDisplayMaterialList = displayMaterials;
+ notifyDataSetChanged();
}
- if (pickedEccInfo != null) {
- return pickedEccInfo.getNumber();
+ }
+
+ @Nullable
+ private String pickEmergencyNumberForCategory(int category,
+ @NonNull List<EmergencyNumber> emergencyNumbers) {
+ for (EmergencyNumber number : emergencyNumbers) {
+ if ((number.getEmergencyServiceCategoryBitmask() & category) != 0) {
+ return number.getNumber();
+ }
}
return null;
}
- private @Nullable EccDisplayMaterial prepareDisplayMaterialForEccInfo(@NonNull Context context,
- @NonNull EccInfo eccInfo) {
+ @Nullable
+ private EccDisplayMaterial prepareEccMaterial(@NonNull Context context, @NonNull String number,
+ @NonNull List<Integer> categories) {
EccDisplayMaterial material = new EccDisplayMaterial();
- material.number = eccInfo.getNumber();
- EccInfo.Type[] types = eccInfo.getTypes();
- for (EccInfo.Type type : types) {
+ material.number = number;
+ for (int category : categories) {
CharSequence description;
- switch (type) {
- case POLICE:
+ switch (category) {
+ case EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE:
description = mPoliceDescription;
material.iconRes = R.drawable.ic_local_police_gm2_24px;
break;
- case AMBULANCE:
+ case EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_AMBULANCE:
description = mAmbulanceDescription;
material.iconRes = R.drawable.ic_local_hospital_gm2_24px;
break;
- case FIRE:
+ case EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_FIRE_BRIGADE:
description = mFireDescription;
material.iconRes = R.drawable.ic_local_fire_department_gm2_24px;
break;
@@ -200,6 +173,7 @@
// ignore unknown types
continue;
}
+
if (TextUtils.isEmpty(material.description)) {
material.description = description;
} else {
@@ -209,10 +183,10 @@
material.description, description);
}
}
+
if (TextUtils.isEmpty(material.description) || material.iconRes == 0) {
return null;
}
return material;
}
-
}
diff --git a/src/com/android/phone/EmergencyDialer.java b/src/com/android/phone/EmergencyDialer.java
index c89ddc6..65edcf9 100644
--- a/src/com/android/phone/EmergencyDialer.java
+++ b/src/com/android/phone/EmergencyDialer.java
@@ -83,9 +83,6 @@
import com.android.phone.common.dialpad.DialpadKeyButton;
import com.android.phone.common.util.ViewUtil;
import com.android.phone.common.widget.ResizingTextEditText;
-import com.android.phone.ecc.CountryEccInfo;
-import com.android.phone.ecc.EccInfoHelper;
-import com.android.phone.ecc.IsoToEccProtobufRepository;
import java.util.ArrayList;
import java.util.List;
@@ -235,7 +232,7 @@
private View mEmergencyShortcutView;
private View mDialpadView;
- private EccInfoHelper mEccInfoHelper;
+ private ShortcutViewUtils.PhoneInfo mPhoneInfo;
private List<EmergencyShortcutButton> mEmergencyShortcutButtonList;
private EccShortcutAdapter mShortcutAdapter;
@@ -363,14 +360,10 @@
configMgr.getConfigForSubId(SubscriptionManager.getDefaultVoiceSubscriptionId());
mIsShortcutViewEnabled = false;
+ mPhoneInfo = null;
if (canEnableShortcutView(carrierConfig)) {
- TelephonyManager tm = getSystemService(TelephonyManager.class);
- String countryIso = tm.getNetworkCountryIso();
- if (TextUtils.isEmpty(countryIso)) {
- Log.d(LOG_TAG, "Unable to determine the country of current network.");
- } else if (!EccInfoHelper.isCountryEccInfoAvailable(this, countryIso)) {
- Log.d(LOG_TAG, "ECC info is unavailable.");
- } else {
+ mPhoneInfo = ShortcutViewUtils.pickPreferredPhone(this);
+ if (mPhoneInfo != null) {
mIsShortcutViewEnabled = true;
}
}
@@ -469,7 +462,6 @@
mEmergencyInfoGroup = (EmergencyInfoGroup) findViewById(R.id.emergency_info_button);
if (mIsShortcutViewEnabled) {
- mEccInfoHelper = new EccInfoHelper(IsoToEccProtobufRepository.getInstance());
setupEmergencyShortcutsView();
}
}
@@ -611,11 +603,8 @@
if (!TextUtils.isEmpty(phoneNumber)) {
if (DBG) Log.d(LOG_TAG, "dial emergency number: " + Rlog.pii(LOG_TAG, phoneNumber));
- Bundle extras = new Bundle();
- extras.putInt(TelecomManager.EXTRA_CALL_SOURCE,
- ParcelableCallAnalytics.CALL_SOURCE_EMERGENCY_SHORTCUT);
- TelecomManager tm = (TelecomManager) getSystemService(TELECOM_SERVICE);
- tm.placeCall(Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null), extras);
+ placeCall(phoneNumber, ParcelableCallAnalytics.CALL_SOURCE_EMERGENCY_SHORTCUT,
+ mPhoneInfo);
} else {
Log.d(LOG_TAG, "emergency number is empty");
}
@@ -769,30 +758,9 @@
updateTheme(lockScreenColors.supportsDarkText());
}
- if (mIsShortcutViewEnabled && mEccInfoHelper != null) {
- final Context context = this;
- mEccInfoHelper.getCountryEccInfoAsync(context,
- new EccInfoHelper.CountryEccInfoResultCallback() {
- @Override
- public void onSuccess(String iso, CountryEccInfo countryEccInfo) {
- Log.d(LOG_TAG, "Retrieve ECC info success, country ISO: "
- + Rlog.pii(LOG_TAG, iso));
- updateLocationAndEccInfo(iso, countryEccInfo);
- }
-
- @Override
- public void onDetectCountryFailed() {
- Log.w(LOG_TAG, "Cannot detect current country.");
- updateLocationAndEccInfo(null, null);
- }
-
- @Override
- public void onRetrieveCountryEccInfoFailed(String iso) {
- Log.w(LOG_TAG, "Retrieve ECC info failed, country ISO: "
- + Rlog.pii(LOG_TAG, iso));
- updateLocationAndEccInfo(iso, null);
- }
- });
+ if (mIsShortcutViewEnabled) {
+ mPhoneInfo = ShortcutViewUtils.pickPreferredPhone(this);
+ updateLocationAndEccInfo();
}
}
@@ -842,10 +810,6 @@
}
private boolean canEnableShortcutView(PersistableBundle carrierConfig) {
- if (!getResources().getBoolean(R.bool.config_emergency_shortcut_view_enabled)) {
- // Disables shortcut view by project.
- return false;
- }
if (!carrierConfig.getBoolean(
CarrierConfigManager.KEY_SUPPORT_EMERGENCY_DIALER_SHORTCUT_BOOL)) {
Log.d(LOG_TAG, "Disables shortcut view by carrier requirement");
@@ -897,7 +861,20 @@
// nothing and just returns input number.
mLastNumber = PhoneNumberUtils.convertToEmergencyNumber(this, mLastNumber);
- if (PhoneNumberUtils.isLocalEmergencyNumber(this, mLastNumber)) {
+ boolean isEmergencyNumber = false;
+ ShortcutViewUtils.PhoneInfo phoneToMakeCall = null;
+ if (mPhoneInfo != null) {
+ isEmergencyNumber = mPhoneInfo.hasPromotedEmergencyNumber(mLastNumber);
+ if (isEmergencyNumber) {
+ phoneToMakeCall = mPhoneInfo;
+ }
+ }
+ if (!isEmergencyNumber) {
+ TelephonyManager tm = getSystemService(TelephonyManager.class);
+ isEmergencyNumber = tm.isCurrentEmergencyNumber(mLastNumber);
+ }
+
+ if (isEmergencyNumber) {
if (DBG) Log.d(LOG_TAG, "placing call to " + mLastNumber);
// place the call if it is a valid number
@@ -910,11 +887,8 @@
mMetricsWriter.writeMetricsForMakingCall(MetricsWriter.CALL_SOURCE_DIALPAD,
MetricsWriter.PHONE_NUMBER_TYPE_EMERGENCY, isShortcutNumber(mLastNumber));
- Bundle extras = new Bundle();
- extras.putInt(TelecomManager.EXTRA_CALL_SOURCE,
- ParcelableCallAnalytics.CALL_SOURCE_EMERGENCY_DIALPAD);
- TelecomManager tm = (TelecomManager) getSystemService(TELECOM_SERVICE);
- tm.placeCall(Uri.fromParts(PhoneAccount.SCHEME_TEL, mLastNumber, null), extras);
+ placeCall(mLastNumber, ParcelableCallAnalytics.CALL_SOURCE_EMERGENCY_DIALPAD,
+ phoneToMakeCall);
} else {
if (DBG) Log.d(LOG_TAG, "rejecting bad requested number " + mLastNumber);
@@ -928,6 +902,27 @@
mDigits.getText().delete(0, mDigits.getText().length());
}
+ private void placeCall(String number, int callSource, ShortcutViewUtils.PhoneInfo phone) {
+ Bundle extras = new Bundle();
+ extras.putInt(TelecomManager.EXTRA_CALL_SOURCE, callSource);
+ /**
+ * This is used for Telecom and Telephony to tell modem user's intent is emergency call,
+ * when the dialed number is ambiguous and identified as both emergency number and any
+ * other non-emergency number; e.g. in some situation, 611 could be both an emergency
+ * number in a country and a non-emergency number of a carrier's customer service hotline.
+ */
+ extras.putBoolean(TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL, true);
+
+ if (phone != null && phone.getPhoneAccountHandle() != null) {
+ // Requests to dial through the specified phone.
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+ phone.getPhoneAccountHandle());
+ }
+
+ TelecomManager tm = this.getSystemService(TelecomManager.class);
+ tm.placeCall(Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null), extras);
+ }
+
/**
* Plays the specified tone for TONE_LENGTH_MS milliseconds.
*
@@ -1071,7 +1066,7 @@
AsyncTask<Void, Void, Boolean> showWfcWarningTask = new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... voids) {
- TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
+ TelephonyManager tm = getSystemService(TelephonyManager.class);
boolean isWfcAvailable = tm.isWifiCallingAvailable();
ServiceState ss = tm.getServiceState();
boolean isCellAvailable =
@@ -1143,14 +1138,15 @@
mEmergencyShortcutButtonList = new ArrayList<>();
setupEmergencyCallShortcutButton();
- updateLocationAndEccInfo(null, null);
+ updateLocationAndEccInfo();
switchView(mEmergencyShortcutView, mDialpadView, false);
}
- private void setLocationInfo(String countryIso) {
+ private void setLocationInfo() {
final View locationInfo = findViewById(R.id.location_info);
+ String countryIso = mPhoneInfo != null ? mPhoneInfo.getCountryIso() : null;
String countryName = null;
if (!TextUtils.isEmpty(countryIso)) {
Locale locale = Locale.getDefault();
@@ -1227,11 +1223,11 @@
mShortcutAdapter.registerDataSetObserver(mShortcutDataSetObserver);
}
- private void updateLocationAndEccInfo(String iso, CountryEccInfo countryEccInfo) {
+ private void updateLocationAndEccInfo() {
if (!isFinishing() && !isDestroyed()) {
- setLocationInfo(iso);
+ setLocationInfo();
if (mShortcutAdapter != null) {
- mShortcutAdapter.updateCountryEccInfo(this, countryEccInfo);
+ mShortcutAdapter.updateCountryEccInfo(this, mPhoneInfo);
}
}
}
diff --git a/src/com/android/phone/MobileNetworkSettings.java b/src/com/android/phone/MobileNetworkSettings.java
index 9d6108e..0f48f75 100644
--- a/src/com/android/phone/MobileNetworkSettings.java
+++ b/src/com/android/phone/MobileNetworkSettings.java
@@ -57,6 +57,7 @@
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.euicc.EuiccManager;
+import android.telephony.ims.ProvisioningManager;
import android.telephony.ims.feature.ImsFeature;
import android.text.TextUtils;
import android.util.Log;
@@ -485,7 +486,7 @@
public void onIntentUpdate(Intent intent) {
if (!mUnavailable) {
- updateCurrentTab(intent);
+ updateCurrentTab(intent.getExtras());
}
}
@@ -583,8 +584,7 @@
}
};
- private int getSlotIdFromIntent(Intent intent) {
- Bundle data = intent.getExtras();
+ private int getSlotIdFromBundle(Bundle data) {
int subId = -1;
if (data != null) {
subId = data.getInt(Settings.EXTRA_SUB_ID, -1);
@@ -754,8 +754,8 @@
mEmptyTabContent);
}
- private void updateCurrentTab(Intent intent) {
- int slotId = getSlotIdFromIntent(intent);
+ private void updateCurrentTab(Bundle data) {
+ int slotId = getSlotIdFromBundle(data);
if (slotId >= 0 && mTabHost != null && mTabHost.getCurrentTab() != slotId) {
mTabHost.setCurrentTab(slotId);
}
@@ -768,6 +768,9 @@
// If advanced fields are already expanded, we save it and expand it
// when it's re-created.
outState.putBoolean(EXPAND_ADVANCED_FIELDS, mExpandAdvancedFields);
+
+ // Save subId of currently shown tab.
+ outState.putInt(Settings.EXTRA_SUB_ID, mSubId);
}
@Override
@@ -851,7 +854,12 @@
getActivity().setContentView(R.layout.telephony_disallowed_preference_screen);
} else {
initializeSubscriptions();
- updateCurrentTab(getActivity().getIntent());
+
+ if (savedInstanceState != null) {
+ updateCurrentTab(savedInstanceState);
+ } else {
+ updateCurrentTab(getActivity().getIntent().getExtras());
+ }
}
}
@@ -884,6 +892,18 @@
}
}
+ private final ProvisioningManager.Callback mProvisioningCallback =
+ new ProvisioningManager.Callback() {
+ @Override
+ public void onProvisioningIntChanged(int item, int value) {
+ if (item == ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED
+ || item == ImsConfig.ConfigConstants.VLT_SETTING_ENABLED
+ || item == ImsConfig.ConfigConstants.LVC_SETTING_ENABLED) {
+ updateBody();
+ }
+ }
+ };
+
@Override
public void onDestroy() {
super.onDestroy();
@@ -933,6 +953,15 @@
context.getContentResolver().registerContentObserver(ENFORCE_MANAGED_URI, false,
mDpcEnforcedContentObserver);
+ // Register callback for provisioning changes.
+ try {
+ if (mImsMgr != null) {
+ mImsMgr.getConfigInterface().addConfigCallback(mProvisioningCallback);
+ }
+ } catch (ImsException e) {
+ Log.w(LOG_TAG, "onResume: Unable to register callback for provisioning changes.");
+ }
+
Log.i(LOG_TAG, "onResume:-");
}
@@ -1325,6 +1354,17 @@
final Context context = getActivity();
context.unregisterReceiver(mPhoneChangeReceiver);
context.getContentResolver().unregisterContentObserver(mDpcEnforcedContentObserver);
+
+ // Remove callback for provisioning changes.
+ try {
+ if (mImsMgr != null) {
+ mImsMgr.getConfigInterface().removeConfigCallback(
+ mProvisioningCallback.getBinder());
+ }
+ } catch (ImsException e) {
+ Log.w(LOG_TAG, "onPause: Unable to remove callback for provisioning changes");
+ }
+
if (DBG) log("onPause:-");
}
@@ -1854,10 +1894,23 @@
return;
}
+ // See what Telecom thinks the SIM call manager is.
final PhoneAccountHandle simCallManager =
TelecomManager.from(getContext()).getSimCallManager();
- if (simCallManager != null) {
+ // Check which SIM call manager is for the current sub ID.
+ PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(mSubId);
+ String currentSubSimCallManager = null;
+ if (carrierConfig != null) {
+ currentSubSimCallManager = carrierConfig.getString(
+ CarrierConfigManager.KEY_DEFAULT_SIM_CALL_MANAGER_STRING);
+ }
+
+ // Only try to configure the phone account if this is the sim call manager for the
+ // current sub.
+ if (simCallManager != null
+ && simCallManager.getComponentName().flattenToString().equals(
+ currentSubSimCallManager)) {
Intent intent = MobileNetworkSettings.buildPhoneAccountConfigureIntent(
getContext(), simCallManager);
PackageManager pm = getContext().getPackageManager();
diff --git a/src/com/android/phone/PhoneApp.java b/src/com/android/phone/PhoneApp.java
index 333e0ec..df151bf 100644
--- a/src/com/android/phone/PhoneApp.java
+++ b/src/com/android/phone/PhoneApp.java
@@ -19,7 +19,6 @@
import android.app.Application;
import android.os.UserHandle;
-import com.android.phone.ecc.IsoToEccProtobufRepository;
import com.android.services.telephony.TelecomAccountRegistry;
/**
@@ -41,10 +40,5 @@
TelecomAccountRegistry.getInstance(this).setupOnBoot();
}
-
- new Thread(() -> {
- // Preload ECC table in background.
- IsoToEccProtobufRepository.getInstance().loadMappingTable(PhoneApp.this);
- }).start();
}
}
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index ccbb4d0..3d82e74 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -44,6 +44,7 @@
import android.provider.Settings;
import android.telecom.TelecomManager;
import android.telephony.CarrierConfigManager;
+import android.telephony.DebugEventReporter;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
@@ -283,6 +284,9 @@
// getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_VOICE_CALLS);
if (mCM == null) {
+ // Initialize DebugEventReporter early so that it can be used
+ DebugEventReporter.initialize(this);
+
// Inject telephony component factory if configured using other jars.
XmlResourceParser parser = getResources().getXml(R.xml.telephony_injection);
TelephonyComponentFactory.getInstance().injectTheComponentFactory(parser);
@@ -594,14 +598,22 @@
airplaneMode = AIRPLANE_ON;
}
handleAirplaneModeChange(context, airplaneMode);
- } else if ((action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) &&
- (mPUKEntryActivity != null)) {
- // if an attempt to un-PUK-lock the device was made, while we're
- // receiving this state change notification, notify the handler.
- // NOTE: This is ONLY triggered if an attempt to un-PUK-lock has
- // been attempted.
- mHandler.sendMessage(mHandler.obtainMessage(EVENT_SIM_STATE_CHANGED,
- intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE)));
+ } else if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) {
+ // re-register as it may be a new IccCard
+ int phoneId = intent.getIntExtra(PhoneConstants.PHONE_KEY,
+ SubscriptionManager.INVALID_PHONE_INDEX);
+ if (SubscriptionManager.isValidPhoneId(phoneId)) {
+ PhoneUtils.unregisterIccStatus(mHandler, phoneId);
+ PhoneUtils.registerIccStatus(mHandler, EVENT_SIM_NETWORK_LOCKED, phoneId);
+ }
+ if (mPUKEntryActivity != null) {
+ // if an attempt to un-PUK-lock the device was made, while we're
+ // receiving this state change notification, notify the handler.
+ // NOTE: This is ONLY triggered if an attempt to un-PUK-lock has
+ // been attempted.
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_SIM_STATE_CHANGED,
+ intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE)));
+ }
} else if (action.equals(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED)) {
String newPhone = intent.getStringExtra(PhoneConstants.PHONE_NAME_KEY);
Log.d(LOG_TAG, "Radio technology switched. Now " + newPhone + " is active.");
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 26b5f94..f7f6683 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -35,6 +35,7 @@
import android.net.Uri;
import android.os.AsyncResult;
import android.os.Binder;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -52,11 +53,11 @@
import android.os.WorkSource;
import android.preference.PreferenceManager;
import android.provider.Settings;
-import android.service.carrier.CarrierIdentifier;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telephony.CarrierConfigManager;
+import android.telephony.CarrierRestrictionRules;
import android.telephony.CellInfo;
import android.telephony.CellInfoGsm;
import android.telephony.CellInfoWcdma;
@@ -86,6 +87,7 @@
import android.telephony.data.ApnSetting;
import android.telephony.emergency.EmergencyNumber;
import android.telephony.gsm.GsmCellLocation;
+import android.telephony.ims.ProvisioningManager;
import android.telephony.ims.aidl.IImsCapabilityCallback;
import android.telephony.ims.aidl.IImsConfig;
import android.telephony.ims.aidl.IImsConfigCallback;
@@ -93,6 +95,8 @@
import android.telephony.ims.aidl.IImsRcsFeature;
import android.telephony.ims.aidl.IImsRegistration;
import android.telephony.ims.aidl.IImsRegistrationCallback;
+import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.stub.ImsConfigImplBase;
import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.text.TextUtils;
import android.util.ArraySet;
@@ -110,6 +114,7 @@
import com.android.internal.telephony.CellNetworkScanResult;
import com.android.internal.telephony.CommandException;
import com.android.internal.telephony.DefaultPhoneNotifier;
+import com.android.internal.telephony.HalVersion;
import com.android.internal.telephony.INumberVerificationCallback;
import com.android.internal.telephony.ITelephony;
import com.android.internal.telephony.IccCard;
@@ -130,7 +135,9 @@
import com.android.internal.telephony.SmsApplication.SmsApplicationData;
import com.android.internal.telephony.SubscriptionController;
import com.android.internal.telephony.TelephonyPermissions;
+import com.android.internal.telephony.emergency.EmergencyNumberTracker;
import com.android.internal.telephony.euicc.EuiccConnector;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
import com.android.internal.telephony.uicc.IccIoResult;
import com.android.internal.telephony.uicc.IccUtils;
import com.android.internal.telephony.uicc.SIMRecords;
@@ -153,9 +160,11 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
/**
* Implementation of the ITelephony interface.
@@ -229,6 +238,8 @@
private static final int EVENT_CMD_MODEM_REBOOT_DONE = 65;
private static final int CMD_REQUEST_CELL_INFO_UPDATE = 66;
private static final int EVENT_REQUEST_CELL_INFO_UPDATE_DONE = 67;
+ private static final int CMD_REQUEST_ENABLE_MODEM = 68;
+ private static final int EVENT_ENABLE_MODEM_DONE = 69;
// Parameters of select command.
private static final int SELECT_COMMAND = 0xA4;
@@ -255,6 +266,10 @@
private static final String PREF_CARRIERS_ALPHATAG_PREFIX = "carrier_alphtag_";
private static final String PREF_CARRIERS_NUMBER_PREFIX = "carrier_number_";
private static final String PREF_CARRIERS_SUBSCRIBER_PREFIX = "carrier_subscriber_";
+ private static final String PREF_PROVISION_IMS_MMTEL_PREFIX = "provision_ims_mmtel_";
+
+ // String to store multi SIM allowed
+ private static final String PREF_MULTI_SIM_RESTRICTED = "multisim_restricted";
// The AID of ISD-R.
private static final String ISDR_AID = "A0000005591010FFFFFFFF8900000100";
@@ -840,10 +855,10 @@
case CMD_SET_ALLOWED_CARRIERS:
request = (MainThreadRequest) msg.obj;
+ CarrierRestrictionRules argument =
+ (CarrierRestrictionRules) request.argument;
onCompleted = obtainMessage(EVENT_SET_ALLOWED_CARRIERS_DONE, request);
- defaultPhone.setAllowedCarriers(
- (List<CarrierIdentifier>) request.argument,
- onCompleted, request.workSource);
+ defaultPhone.setAllowedCarriers(argument, onCompleted, request.workSource);
break;
case EVENT_SET_ALLOWED_CARRIERS_DONE:
@@ -852,19 +867,19 @@
if (ar.exception == null && ar.result != null) {
request.result = ar.result;
} else {
- if (ar.result == null) {
- loge("setAllowedCarriers: Empty response");
- } else if (ar.exception instanceof CommandException) {
- loge("setAllowedCarriers: CommandException: " +
- ar.exception);
+ request.result = TelephonyManager.SET_CARRIER_RESTRICTION_ERROR;
+ if (ar.exception instanceof CommandException) {
+ loge("setAllowedCarriers: CommandException: " + ar.exception);
+ CommandException.Error error =
+ ((CommandException) (ar.exception)).getCommandError();
+ if (error == CommandException.Error.REQUEST_NOT_SUPPORTED) {
+ request.result =
+ TelephonyManager.SET_CARRIER_RESTRICTION_NOT_SUPPORTED;
+ }
} else {
loge("setAllowedCarriers: Unknown exception");
}
}
- // Result cannot be null. Return -1 on error.
- if (request.result == null) {
- request.result = new int[]{-1};
- }
notifyRequester(request);
break;
@@ -880,6 +895,8 @@
if (ar.exception == null && ar.result != null) {
request.result = ar.result;
} else {
+ request.result = new IllegalStateException(
+ "Failed to get carrier restrictions");
if (ar.result == null) {
loge("getAllowedCarriers: Empty response");
} else if (ar.exception instanceof CommandException) {
@@ -889,10 +906,6 @@
loge("getAllowedCarriers: Unknown exception");
}
}
- // Result cannot be null. Return empty list of CarrierIdentifier.
- if (request.result == null) {
- request.result = new ArrayList<CarrierIdentifier>(0);
- }
notifyRequester(request);
break;
@@ -1082,6 +1095,20 @@
case EVENT_CMD_MODEM_REBOOT_DONE:
handleNullReturnEvent(msg, "rebootModem");
break;
+ case CMD_REQUEST_ENABLE_MODEM:
+ request = (MainThreadRequest) msg.obj;
+ boolean enable = (boolean) request.argument;
+ onCompleted = obtainMessage(EVENT_ENABLE_MODEM_DONE, request);
+ PhoneConfigurationManager.getInstance()
+ .enablePhone(request.phone, enable, onCompleted);
+ break;
+ case EVENT_ENABLE_MODEM_DONE:
+ ar = (AsyncResult) msg.obj;
+ request = (MainThreadRequest) ar.userObj;
+ request.result = (ar.exception == null);
+ updateModemStateMetrics();
+ notifyRequester(request);
+ break;
default:
Log.w(LOG_TAG, "MainThreadHandler: unexpected message code: " + msg.what);
break;
@@ -1818,9 +1845,21 @@
public Bundle getCellLocation(String callingPackage) {
mApp.getSystemService(AppOpsManager.class)
.checkPackage(Binder.getCallingUid(), callingPackage);
- if (!LocationAccessPolicy.canAccessCellLocation(mApp, callingPackage,
- Binder.getCallingUid(), Binder.getCallingPid(), true)) {
- return null;
+
+ LocationAccessPolicy.LocationPermissionResult locationResult =
+ LocationAccessPolicy.checkLocationPermission(mApp,
+ new LocationAccessPolicy.LocationPermissionQuery.Builder()
+ .setCallingPackage(callingPackage)
+ .setCallingPid(Binder.getCallingPid())
+ .setCallingUid(Binder.getCallingUid())
+ .setMethod("getCellLocation")
+ .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+ .build());
+ switch (locationResult) {
+ case DENIED_HARD:
+ throw new SecurityException("Not allowed to access cell location");
+ case DENIED_SOFT:
+ return new Bundle();
}
WorkSource workSource = getWorkSource(Binder.getCallingUid());
@@ -1970,9 +2009,21 @@
public List<CellInfo> getAllCellInfo(String callingPackage) {
mApp.getSystemService(AppOpsManager.class)
.checkPackage(Binder.getCallingUid(), callingPackage);
- if (!LocationAccessPolicy.canAccessCellLocation(mApp,
- callingPackage, Binder.getCallingUid(), Binder.getCallingPid(), true)) {
- return null;
+
+ LocationAccessPolicy.LocationPermissionResult locationResult =
+ LocationAccessPolicy.checkLocationPermission(mApp,
+ new LocationAccessPolicy.LocationPermissionQuery.Builder()
+ .setCallingPackage(callingPackage)
+ .setCallingPid(Binder.getCallingPid())
+ .setCallingUid(Binder.getCallingUid())
+ .setMethod("getAllCellInfo")
+ .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+ .build());
+ switch (locationResult) {
+ case DENIED_HARD:
+ throw new SecurityException("Not allowed to access cell info");
+ case DENIED_SOFT:
+ return new ArrayList<>();
}
final int targetSdk = getTargetSdk(callingPackage);
@@ -2013,9 +2064,21 @@
int subId, ICellInfoCallback cb, String callingPackage, WorkSource workSource) {
mApp.getSystemService(AppOpsManager.class)
.checkPackage(Binder.getCallingUid(), callingPackage);
- if (!LocationAccessPolicy.canAccessCellLocation(mApp, callingPackage,
- Binder.getCallingUid(), Binder.getCallingPid(), true)) {
- return;
+
+ LocationAccessPolicy.LocationPermissionResult locationResult =
+ LocationAccessPolicy.checkLocationPermission(mApp,
+ new LocationAccessPolicy.LocationPermissionQuery.Builder()
+ .setCallingPackage(callingPackage)
+ .setCallingPid(Binder.getCallingPid())
+ .setCallingUid(Binder.getCallingUid())
+ .setMethod("requestCellInfoUpdate")
+ .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+ .build());
+ switch (locationResult) {
+ case DENIED_HARD:
+ throw new SecurityException("Not allowed to access cell info");
+ case DENIED_SOFT:
+ return;
}
final Phone phone = getPhone(subId);
@@ -2720,9 +2783,9 @@
public void registerImsRegistrationCallback(int subId, IImsRegistrationCallback c)
throws RemoteException {
enforceReadPrivilegedPermission("registerImsRegistrationCallback");
- // TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
final long token = Binder.clearCallingIdentity();
try {
+ // TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
ImsManager.getInstance(mApp, getSlotIndexOrException(subId))
.addRegistrationCallbackForSubscription(c, subId);
} finally {
@@ -2733,10 +2796,21 @@
@Override
public void unregisterImsRegistrationCallback(int subId, IImsRegistrationCallback c) {
enforceReadPrivilegedPermission("unregisterImsRegistrationCallback");
- // TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
- Binder.withCleanCallingIdentity(() ->
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ throw new IllegalArgumentException("Invalid Subscription ID: " + subId);
+ }
+ Binder.withCleanCallingIdentity(() -> {
+ try {
+ // TODO: Refactor to remove ImsManager dependence and query through ImsPhone.
ImsManager.getInstance(mApp, getSlotIndexOrException(subId))
- .removeRegistrationCallbackForSubscription(c, subId));
+ .removeRegistrationCallbackForSubscription(c, subId);
+ } catch (IllegalArgumentException e) {
+ Log.i(LOG_TAG, "unregisterImsRegistrationCallback: " + subId
+ + "is inactive, ignoring unregister.");
+ // If the subscription is no longer active, just return, since the callback
+ // will already have been removed internally.
+ }
+ });
}
@Override
@@ -2756,10 +2830,22 @@
@Override
public void unregisterMmTelCapabilityCallback(int subId, IImsCapabilityCallback c) {
enforceReadPrivilegedPermission("unregisterMmTelCapabilityCallback");
- // TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
- Binder.withCleanCallingIdentity(() ->
+
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ throw new IllegalArgumentException("Invalid Subscription ID: " + subId);
+ }
+ Binder.withCleanCallingIdentity(() -> {
+ try {
+ // TODO: Refactor to remove ImsManager dependence and query through ImsPhone.
ImsManager.getInstance(mApp, getSlotIndexOrException(subId))
- .removeCapabilitiesCallbackForSubscription(c, subId));
+ .removeCapabilitiesCallbackForSubscription(c, subId);
+ } catch (IllegalArgumentException e) {
+ Log.i(LOG_TAG, "unregisterMmTelCapabilityCallback: " + subId
+ + "is inactive, ignoring unregister.");
+ // If the subscription is no longer active, just return, since the callback
+ // will already have been removed internally.
+ }
+ });
}
@Override
@@ -2773,6 +2859,9 @@
} catch (ImsException e) {
Log.w(LOG_TAG, "IMS isCapable - service unavailable: " + e.getMessage());
return false;
+ } catch (IllegalArgumentException e) {
+ Log.i(LOG_TAG, "isCapable: " + subId + " is inactive, returning false.");
+ return false;
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -2904,8 +2993,10 @@
final long identity = Binder.clearCallingIdentity();
try {
// TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
+ boolean isRoaming = TelephonyManager.from(
+ getPhone(subId).getContext()).isNetworkRoaming(subId);
ImsManager.getInstance(mApp,
- getSlotIndexOrException(subId)).setWfcNonPersistent(isCapable, mode);
+ getSlotIndexOrException(subId)).setWfcNonPersistent(isCapable, mode, isRoaming);
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -2999,9 +3090,7 @@
try {
// TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
ImsManager.getInstance(mApp, getSlotIndexOrException(subId))
- .getConfigInterface().addConfigCallback(callback);
- } catch (ImsException e) {
- throw new IllegalArgumentException(e.getMessage());
+ .addProvisioningCallbackForSubscription(callback, subId);
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -3011,27 +3100,267 @@
public void unregisterImsProvisioningChangedCallback(int subId, IImsConfigCallback callback) {
enforceReadPrivilegedPermission("unregisterImsProvisioningChangedCallback");
final long identity = Binder.clearCallingIdentity();
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ throw new IllegalArgumentException("Invalid Subscription ID: " + subId);
+ }
try {
// TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
ImsManager.getInstance(mApp, getSlotIndexOrException(subId))
- .getConfigInterface().removeConfigCallback(callback);
- } catch (ImsException e) {
- throw new IllegalArgumentException(e.getMessage());
+ .removeProvisioningCallbackForSubscription(callback, subId);
+ } catch (IllegalArgumentException e) {
+ Log.i(LOG_TAG, "unregisterImsProvisioningChangedCallback: " + subId
+ + "is inactive, ignoring unregister.");
+ // If the subscription is no longer active, just return, since the callback will already
+ // have been removed internally.
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
+ public void setImsProvisioningStatusForCapability(int subId, int capability, int tech,
+ boolean isProvisioned) {
+ if (tech != ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN
+ && tech != ImsRegistrationImplBase.REGISTRATION_TECH_LTE) {
+ throw new IllegalArgumentException("Registration technology '" + tech + "' is invalid");
+ }
+ TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(mApp, subId,
+ "setProvisioningStatusForCapability");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ // TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
+ Phone phone = getPhone(subId);
+ if (phone == null) {
+ loge("setImsProvisioningStatusForCapability: phone instance null for subid "
+ + subId);
+ return;
+ }
+ if (!doesImsCapabilityRequireProvisioning(phone.getContext(), subId, capability)) {
+ return;
+ }
+
+ // this capability requires provisioning, route to the correct API.
+ ImsManager ims = ImsManager.getInstance(mApp, getSlotIndex(subId));
+ switch (capability) {
+ case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE: {
+ if (tech == ImsRegistrationImplBase.REGISTRATION_TECH_LTE) {
+ ims.setVolteProvisioned(isProvisioned);
+ } else if (tech == ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN) {
+ ims.setWfcProvisioned(isProvisioned);
+ }
+ break;
+ }
+ case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO: {
+ // There is currently no difference in VT provisioning type.
+ ims.setVtProvisioned(isProvisioned);
+ break;
+ }
+ case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT: {
+ // There is no "deprecated" UT provisioning mechanism through ImsConfig, so
+ // change the capability of the feature instead if needed.
+ if (isMmTelCapabilityProvisionedInCache(subId, capability, tech)
+ == isProvisioned) {
+ // No change in provisioning.
+ return;
+ }
+ cacheMmTelCapabilityProvisioning(subId, capability, tech, isProvisioned);
+ try {
+ ims.changeMmTelCapability(capability, tech, isProvisioned);
+ } catch (ImsException e) {
+ loge("setImsProvisioningStatusForCapability: couldn't change UT capability"
+ + ", Exception" + e.getMessage());
+ }
+ break;
+ }
+ default: {
+ throw new IllegalArgumentException("Tried to set provisioning for capability '"
+ + capability + "', which does not require provisioning.");
+ }
+ }
+
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public boolean getImsProvisioningStatusForCapability(int subId, int capability, int tech) {
+ if (tech != ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN
+ && tech != ImsRegistrationImplBase.REGISTRATION_TECH_LTE) {
+ throw new IllegalArgumentException("Registration technology '" + tech + "' is invalid");
+ }
+ enforceReadPrivilegedPermission("getProvisioningStatusForCapability");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ // TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
+ Phone phone = getPhone(subId);
+ if (phone == null) {
+ loge("getImsProvisioningStatusForCapability: phone instance null for subid "
+ + subId);
+ // We will fail with "true" as the provisioning status because this is the default
+ // if we do not require provisioning.
+ return true;
+ }
+
+ if (!doesImsCapabilityRequireProvisioning(phone.getContext(), subId, capability)) {
+ return true;
+ }
+
+ ImsManager ims = ImsManager.getInstance(mApp, getSlotIndex(subId));
+ switch (capability) {
+ case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE: {
+ if (tech == ImsRegistrationImplBase.REGISTRATION_TECH_LTE) {
+ return ims.isVolteProvisionedOnDevice();
+ } else if (tech == ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN) {
+ return ims.isWfcProvisionedOnDevice();
+ }
+ // This should never happen, since we are checking tech above to make sure it
+ // is either LTE or IWLAN.
+ throw new IllegalArgumentException("Invalid radio technology for voice "
+ + "capability.");
+ }
+ case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO: {
+ // There is currently no difference in VT provisioning type.
+ return ims.isVtProvisionedOnDevice();
+ }
+ case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT: {
+ // There is no "deprecated" UT provisioning mechanism, so get from shared prefs.
+ return isMmTelCapabilityProvisionedInCache(subId, capability, tech);
+ }
+ default: {
+ throw new IllegalArgumentException("Tried to get provisioning for capability '"
+ + capability + "', which does not require provisioning.");
+ }
+ }
+
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public boolean isMmTelCapabilityProvisionedInCache(int subId, int capability, int tech) {
+ if (tech != ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN
+ && tech != ImsRegistrationImplBase.REGISTRATION_TECH_LTE) {
+ throw new IllegalArgumentException("Registration technology '" + tech + "' is invalid");
+ }
+ enforceReadPrivilegedPermission("isMmTelCapabilityProvisionedInCache");
+ int provisionedBits = getMmTelCapabilityProvisioningBitfield(subId, tech);
+ return (provisionedBits & capability) > 0;
+ }
+
+ @Override
+ public void cacheMmTelCapabilityProvisioning(int subId, int capability, int tech,
+ boolean isProvisioned) {
+ if (tech != ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN
+ && tech != ImsRegistrationImplBase.REGISTRATION_TECH_LTE) {
+ throw new IllegalArgumentException("Registration technology '" + tech + "' is invalid");
+ }
+ TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(mApp, subId,
+ "setProvisioningStatusForCapability");
+ int provisionedBits = getMmTelCapabilityProvisioningBitfield(subId, tech);
+ // If the current provisioning status for capability already matches isProvisioned,
+ // do nothing.
+ if (((provisionedBits & capability) > 0) == isProvisioned) {
+ return;
+ }
+ if (isProvisioned) {
+ setMmTelCapabilityProvisioningBitfield(subId, tech, (provisionedBits | capability));
+ } else {
+ setMmTelCapabilityProvisioningBitfield(subId, tech, (provisionedBits & ~capability));
+ }
+ }
+
+ /**
+ * @return the bitfield containing the MmTel provisioning for the provided subscription and
+ * technology. The bitfield should mirror the bitfield defined by
+ * {@link MmTelFeature.MmTelCapabilities.MmTelCapability}.
+ */
+ private int getMmTelCapabilityProvisioningBitfield(int subId, int tech) {
+ String key = getMmTelProvisioningKey(subId, tech);
+ // Default is no capabilities are provisioned.
+ return mTelephonySharedPreferences.getInt(key, 0 /*default*/);
+ }
+
+ /**
+ * Sets the MmTel capability provisioning bitfield (defined by
+ * {@link MmTelFeature.MmTelCapabilities.MmTelCapability}) for the subscription and
+ * technology specified.
+ *
+ * Note: This is a synchronous command and should not be called on UI thread.
+ */
+ private void setMmTelCapabilityProvisioningBitfield(int subId, int tech, int newField) {
+ final SharedPreferences.Editor editor = mTelephonySharedPreferences.edit();
+ String key = getMmTelProvisioningKey(subId, tech);
+ editor.putInt(key, newField);
+ editor.commit();
+ }
+
+ private static String getMmTelProvisioningKey(int subId, int tech) {
+ // resulting key is provision_ims_mmtel_{subId}_{tech}
+ return PREF_PROVISION_IMS_MMTEL_PREFIX + subId + "_" + tech;
+ }
+
+ /**
+ * Query CarrierConfig to see if the specified capability requires provisioning for the
+ * carrier associated with the subscription id.
+ */
+ private boolean doesImsCapabilityRequireProvisioning(Context context, int subId,
+ int capability) {
+ CarrierConfigManager configManager = new CarrierConfigManager(context);
+ PersistableBundle c = configManager.getConfigForSubId(subId);
+ boolean requireUtProvisioning = c.getBoolean(
+ // By default, this config is true (even if there is no SIM). We also check to make
+ // sure the subscription needs provisioning here, so we do not need to check for
+ // the no-SIM case, where we would normally shortcut this to false.
+ CarrierConfigManager.KEY_CARRIER_SUPPORTS_SS_OVER_UT_BOOL, true)
+ && c.getBoolean(CarrierConfigManager.KEY_CARRIER_UT_PROVISIONING_REQUIRED_BOOL,
+ false);
+ boolean requireVoiceVtProvisioning = c.getBoolean(
+ CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL, false);
+
+ // First check to make sure that the capability requires provisioning.
+ switch (capability) {
+ case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE:
+ // intentional fallthrough
+ case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO: {
+ if (requireVoiceVtProvisioning) {
+ // Voice and Video requires provisioning
+ return true;
+ }
+ break;
+ }
+ case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT: {
+ if (requireUtProvisioning) {
+ // UT requires provisioning
+ return true;
+ }
+ break;
+ }
+ }
+ return false;
+ }
+
+ @Override
public int getImsProvisioningInt(int subId, int key) {
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ throw new IllegalArgumentException("Invalid Subscription id '" + subId + "'");
+ }
enforceReadPrivilegedPermission("getImsProvisioningInt");
final long identity = Binder.clearCallingIdentity();
try {
// TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
- return ImsManager.getInstance(mApp,
- getSlotIndexOrException(subId)).getConfigInterface().getConfigInt(key);
+ int slotId = getSlotIndex(subId);
+ if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+ Log.w(LOG_TAG, "getImsProvisioningInt: called with an inactive subscription '"
+ + subId + "' for key:" + key);
+ return ImsConfigImplBase.CONFIG_RESULT_UNKNOWN;
+ }
+ return ImsManager.getInstance(mApp, slotId).getConfigInterface().getConfigInt(key);
} catch (ImsException e) {
- throw new IllegalArgumentException(e.getMessage());
+ Log.w(LOG_TAG, "getImsProvisioningInt: ImsService is not available for subscription '"
+ + subId + "' for key:" + key);
+ return ImsConfigImplBase.CONFIG_RESULT_UNKNOWN;
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -3039,14 +3368,24 @@
@Override
public String getImsProvisioningString(int subId, int key) {
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ throw new IllegalArgumentException("Invalid Subscription id '" + subId + "'");
+ }
enforceReadPrivilegedPermission("getImsProvisioningString");
final long identity = Binder.clearCallingIdentity();
try {
// TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
- return ImsManager.getInstance(mApp,
- getSlotIndexOrException(subId)).getConfigInterface().getConfigString(key);
+ int slotId = getSlotIndex(subId);
+ if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+ Log.w(LOG_TAG, "getImsProvisioningString: called for an inactive subscription id '"
+ + subId + "' for key:" + key);
+ return ProvisioningManager.STRING_QUERY_RESULT_ERROR_GENERIC;
+ }
+ return ImsManager.getInstance(mApp, slotId).getConfigInterface().getConfigString(key);
} catch (ImsException e) {
- throw new IllegalArgumentException(e.getMessage());
+ Log.w(LOG_TAG, "getImsProvisioningString: ImsService is not available for sub '"
+ + subId + "' for key:" + key);
+ return ProvisioningManager.STRING_QUERY_RESULT_ERROR_NOT_READY;
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -3054,15 +3393,25 @@
@Override
public int setImsProvisioningInt(int subId, int key, int value) {
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ throw new IllegalArgumentException("Invalid Subscription id '" + subId + "'");
+ }
TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(mApp, subId,
"setImsProvisioningInt");
final long identity = Binder.clearCallingIdentity();
try {
// TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
- return ImsManager.getInstance(mApp,
- getSlotIndexOrException(subId)).getConfigInterface().setConfig(key, value);
+ int slotId = getSlotIndex(subId);
+ if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+ Log.w(LOG_TAG, "setImsProvisioningInt: called with an inactive subscription id '"
+ + subId + "' for key:" + key);
+ return ImsConfigImplBase.CONFIG_RESULT_FAILED;
+ }
+ return ImsManager.getInstance(mApp, slotId).getConfigInterface().setConfig(key, value);
} catch (ImsException e) {
- throw new IllegalArgumentException(e.getMessage());
+ Log.w(LOG_TAG, "setImsProvisioningInt: ImsService unavailable for sub '" + subId
+ + "' for key:" + key);
+ return ImsConfigImplBase.CONFIG_RESULT_FAILED;
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -3070,15 +3419,25 @@
@Override
public int setImsProvisioningString(int subId, int key, String value) {
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ throw new IllegalArgumentException("Invalid Subscription id '" + subId + "'");
+ }
TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(mApp, subId,
"setImsProvisioningString");
final long identity = Binder.clearCallingIdentity();
try {
// TODO: Refactor to remove ImsManager dependence and query through ImsPhone directly.
- return ImsManager.getInstance(mApp,
- getSlotIndexOrException(subId)).getConfigInterface().setConfig(key, value);
+ int slotId = getSlotIndex(subId);
+ if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+ Log.w(LOG_TAG, "setImsProvisioningString: called with an inactive subscription id '"
+ + subId + "' for key:" + key);
+ return ImsConfigImplBase.CONFIG_RESULT_FAILED;
+ }
+ return ImsManager.getInstance(mApp, slotId).getConfigInterface().setConfig(key, value);
} catch (ImsException e) {
- throw new IllegalArgumentException(e.getMessage());
+ Log.w(LOG_TAG, "setImsProvisioningString: ImsService unavailable for sub '" + subId
+ + "' for key:" + key);
+ return ImsConfigImplBase.CONFIG_RESULT_FAILED;
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -3087,7 +3446,15 @@
private int getSlotIndexOrException(int subId) throws IllegalArgumentException {
int slotId = SubscriptionManager.getSlotIndex(subId);
if (!SubscriptionManager.isValidSlotIndex(slotId)) {
- throw new IllegalArgumentException("Invalid Subscription Id.");
+ throw new IllegalArgumentException("Invalid Subscription Id, subId=" + subId);
+ }
+ return slotId;
+ }
+
+ private int getSlotIndex(int subId) {
+ int slotId = SubscriptionManager.getSlotIndex(subId);
+ if (!SubscriptionManager.isValidSlotIndex(slotId)) {
+ return SubscriptionManager.INVALID_SIM_SLOT_INDEX;
}
return slotId;
}
@@ -3860,9 +4227,24 @@
* Scans for available networks.
*/
@Override
- public CellNetworkScanResult getCellNetworkScanResults(int subId) {
+ public CellNetworkScanResult getCellNetworkScanResults(int subId, String callingPackage) {
TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
mApp, subId, "getCellNetworkScanResults");
+ LocationAccessPolicy.LocationPermissionResult locationResult =
+ LocationAccessPolicy.checkLocationPermission(mApp,
+ new LocationAccessPolicy.LocationPermissionQuery.Builder()
+ .setCallingPackage(callingPackage)
+ .setCallingPid(Binder.getCallingPid())
+ .setCallingUid(Binder.getCallingUid())
+ .setMethod("getCellNetworkScanResults")
+ .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+ .build());
+ switch (locationResult) {
+ case DENIED_HARD:
+ throw new SecurityException("Not allowed to access scan results -- location");
+ case DENIED_SOFT:
+ return null;
+ }
long identity = Binder.clearCallingIdentity();
try {
@@ -3885,17 +4267,29 @@
*/
@Override
public int requestNetworkScan(int subId, NetworkScanRequest request, Messenger messenger,
- IBinder binder) {
+ IBinder binder, String callingPackage) {
TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
mApp, subId, "requestNetworkScan");
- final long identity = Binder.clearCallingIdentity();
- try {
- return mNetworkScanRequestTracker.startNetworkScan(
- request, messenger, binder, getPhone(subId));
- } finally {
- Binder.restoreCallingIdentity(identity);
+ LocationAccessPolicy.LocationPermissionResult locationResult =
+ LocationAccessPolicy.checkLocationPermission(mApp,
+ new LocationAccessPolicy.LocationPermissionQuery.Builder()
+ .setCallingPackage(callingPackage)
+ .setCallingPid(Binder.getCallingPid())
+ .setCallingUid(Binder.getCallingUid())
+ .setMethod("requestNetworkScan")
+ .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+ .build());
+ switch (locationResult) {
+ case DENIED_HARD:
+ throw new SecurityException("Not allowed to request network scan -- location");
+ case DENIED_SOFT:
+ return -1;
}
+
+ return mNetworkScanRequestTracker.startNetworkScan(
+ request, messenger, binder, getPhone(subId),
+ callingPackage);
}
/**
@@ -3948,8 +4342,9 @@
*/
@Override
public int getPreferredNetworkType(int subId) {
- TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
- mApp, subId, "getPreferredNetworkType");
+ TelephonyPermissions
+ .enforeceCallingOrSelfReadPrivilegedPhoneStatePermissionOrCarrierPrivilege(
+ mApp, subId, "getPreferredNetworkType");
final long identity = Binder.clearCallingIdentity();
try {
@@ -4796,51 +5191,29 @@
}
@Override
- public String getLocaleFromDefaultSim() {
+ public String getSimLocaleForSubscriber(int subId) {
+ enforceReadPrivilegedPermission("getSimLocaleForSubscriber, subId: " + subId);
+ final Phone phone = getPhone(subId);
+ if (phone == null) {
+ log("getSimLocaleForSubscriber, invalid subId");
+ return null;
+ }
final long identity = Binder.clearCallingIdentity();
try {
- // We query all subscriptions instead of just the active ones, because
- // this might be called early on in the provisioning flow when the
- // subscriptions potentially aren't active yet.
- final List<SubscriptionInfo> slist = getAllSubscriptionInfoList();
- if (slist == null || slist.isEmpty()) {
- return null;
- }
-
- // This function may be called very early, say, from the setup wizard, at
- // which point we won't have a default subscription set. If that's the case
- // we just choose the first, which will be valid in "most cases".
- final int defaultSubId = getDefaultSubscription();
- SubscriptionInfo info = null;
- if (defaultSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
- info = slist.get(0);
- } else {
- for (SubscriptionInfo item : slist) {
- if (item.getSubscriptionId() == defaultSubId) {
- info = item;
- break;
- }
- }
-
- if (info == null) {
- return null;
- }
- }
-
+ final SubscriptionInfo info = mSubscriptionController.getActiveSubscriptionInfo(subId,
+ phone.getContext().getOpPackageName());
// Try and fetch the locale from the carrier properties or from the SIM language
// preferences (EF-PL and EF-LI)...
final int mcc = info.getMcc();
- final Phone defaultPhone = getPhone(info.getSubscriptionId());
String simLanguage = null;
- if (defaultPhone != null) {
- final Locale localeFromDefaultSim = defaultPhone.getLocaleFromSimAndCarrierPrefs();
- if (localeFromDefaultSim != null) {
- if (!localeFromDefaultSim.getCountry().isEmpty()) {
- if (DBG) log("Using locale from default SIM:" + localeFromDefaultSim);
- return localeFromDefaultSim.toLanguageTag();
- } else {
- simLanguage = localeFromDefaultSim.getLanguage();
- }
+ final Locale localeFromDefaultSim = phone.getLocaleFromSimAndCarrierPrefs();
+ if (localeFromDefaultSim != null) {
+ if (!localeFromDefaultSim.getCountry().isEmpty()) {
+ if (DBG) log("Using locale from subId: " + subId + " locale: "
+ + localeFromDefaultSim);
+ return localeFromDefaultSim.toLanguageTag();
+ } else {
+ simLanguage = localeFromDefaultSim.getLanguage();
}
}
@@ -4850,7 +5223,7 @@
// determined from the SIM MCC to provide an exact locale.
final Locale mccLocale = MccTable.getLocaleFromMcc(mApp, mcc, simLanguage);
if (mccLocale != null) {
- if (DBG) log("No locale from default SIM, using mcc locale:" + mccLocale);
+ if (DBG) log("No locale from SIM, using mcc locale:" + mccLocale);
return mccLocale.toLanguageTag();
}
@@ -4957,6 +5330,31 @@
return null;
}
+ LocationAccessPolicy.LocationPermissionResult fineLocationResult =
+ LocationAccessPolicy.checkLocationPermission(mApp,
+ new LocationAccessPolicy.LocationPermissionQuery.Builder()
+ .setCallingPackage(callingPackage)
+ .setCallingPid(Binder.getCallingPid())
+ .setCallingUid(Binder.getCallingUid())
+ .setMethod("getServiceStateForSubscriber")
+ .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+ .build());
+
+ LocationAccessPolicy.LocationPermissionResult coarseLocationResult =
+ LocationAccessPolicy.checkLocationPermission(mApp,
+ new LocationAccessPolicy.LocationPermissionQuery.Builder()
+ .setCallingPackage(callingPackage)
+ .setCallingPid(Binder.getCallingPid())
+ .setCallingUid(Binder.getCallingUid())
+ .setMethod("getServiceStateForSubscriber")
+ .setMinSdkVersionForCoarse(Build.VERSION_CODES.Q)
+ .build());
+ // We don't care about hard or soft here -- all we need to know is how much info to scrub.
+ boolean hasFinePermission =
+ fineLocationResult == LocationAccessPolicy.LocationPermissionResult.ALLOWED;
+ boolean hasCoarsePermission =
+ coarseLocationResult == LocationAccessPolicy.LocationPermissionResult.ALLOWED;
+
final long identity = Binder.clearCallingIdentity();
try {
final Phone phone = getPhone(subId);
@@ -4964,7 +5362,13 @@
return null;
}
- return phone.getServiceState();
+ ServiceState ss = phone.getServiceState();
+
+ // Scrub out the location info in ServiceState depending on what level of access
+ // the caller has.
+ if (hasFinePermission) return ss;
+ if (hasCoarsePermission) return ss.sanitizeLocationInfo(false);
+ return ss.sanitizeLocationInfo(true);
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -5235,27 +5639,26 @@
/**
* {@hide}
- * Set the allowed carrier list for slotIndex
+ * Set the allowed carrier list and the excluded carrier list, indicating the priority between
+ * the two lists.
* Require system privileges. In the future we may add this to carrier APIs.
*
- * @return The number of carriers set successfully, should match length of carriers
+ * @return Integer with the result of the operation, as defined in {@link TelephonyManager}.
*/
@Override
- public int setAllowedCarriers(int slotIndex, List<CarrierIdentifier> carriers) {
+ @TelephonyManager.SetCarrierRestrictionResult
+ public int setAllowedCarriers(CarrierRestrictionRules carrierRestrictionRules) {
enforceModifyPermission();
WorkSource workSource = getWorkSource(Binder.getCallingUid());
- if (carriers == null) {
- throw new NullPointerException("carriers cannot be null");
+ if (carrierRestrictionRules == null) {
+ throw new NullPointerException("carrier restriction cannot be null");
}
final long identity = Binder.clearCallingIdentity();
try {
- int[] subIds = SubscriptionManager.getSubId(slotIndex);
- int subId = (subIds != null ? subIds[0] : SubscriptionManager.INVALID_SUBSCRIPTION_ID);
- int[] retVal = (int[]) sendRequest(CMD_SET_ALLOWED_CARRIERS, carriers, subId,
+ return (int) sendRequest(CMD_SET_ALLOWED_CARRIERS, carrierRestrictionRules,
workSource);
- return retVal[0];
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -5263,23 +5666,29 @@
/**
* {@hide}
- * Get the allowed carrier list for slotIndex.
+ * Get the allowed carrier list and the excluded carrier list, including the priority between
+ * the two lists.
* Require system privileges. In the future we may add this to carrier APIs.
*
- * @return List of {@link android.service.telephony.CarrierIdentifier}; empty list
- * means all carriers are allowed.
+ * @return {@link android.telephony.CarrierRestrictionRules}
*/
@Override
- public List<CarrierIdentifier> getAllowedCarriers(int slotIndex) {
+ public CarrierRestrictionRules getAllowedCarriers() {
enforceReadPrivilegedPermission("getAllowedCarriers");
WorkSource workSource = getWorkSource(Binder.getCallingUid());
final long identity = Binder.clearCallingIdentity();
try {
- int[] subIds = SubscriptionManager.getSubId(slotIndex);
- int subId = (subIds != null ? subIds[0] : SubscriptionManager.INVALID_SUBSCRIPTION_ID);
- return (List<CarrierIdentifier>) sendRequest(CMD_GET_ALLOWED_CARRIERS, null, subId,
- workSource);
+ Object response = sendRequest(CMD_GET_ALLOWED_CARRIERS, null, workSource);
+ if (response instanceof CarrierRestrictionRules) {
+ return (CarrierRestrictionRules) response;
+ }
+ // Response is an Exception of some kind,
+ // which is signalled to the user as a NULL retval
+ return null;
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "getAllowedCarriers. Exception ex=" + e);
+ return null;
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -5671,13 +6080,43 @@
}
@Override
- public UiccCardInfo[] getUiccCardsInfo() {
- enforceReadPrivilegedPermission("getUiccCardsInfo");
+ public List<UiccCardInfo> getUiccCardsInfo(String callingPackage) {
+ try {
+ enforceReadPrivilegedPermission("getUiccCardsInfo");
+ } catch (SecurityException e) {
+ // even without READ_PRIVILEGED_PHONE_STATE, we allow the call to continue if the caller
+ // has carrier privileges on an active UICC
+ if (checkCarrierPrivilegesForPackageAnyPhone(callingPackage)
+ != TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
+ throw new SecurityException("Caller does not have carrier privileges on any UICC");
+ }
+ }
final long identity = Binder.clearCallingIdentity();
try {
- ArrayList<UiccCardInfo> cards = UiccController.getInstance().getAllUiccCardInfos();
- return cards.toArray(new UiccCardInfo[cards.size()]);
+ UiccController uiccController = UiccController.getInstance();
+ ArrayList<UiccCardInfo> cardInfos = uiccController.getAllUiccCardInfos();
+
+ ApplicationInfo ai = mApp.getPackageManager().getApplicationInfo(callingPackage, 0);
+ if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+ // Remove private info if the caller doesn't have access
+ ArrayList<UiccCardInfo> filteredInfos = new ArrayList<>();
+ for (UiccCardInfo cardInfo : cardInfos) {
+ UiccCard card = uiccController.getUiccCard(cardInfo.getSlotIndex());
+ UiccProfile profile = card.getUiccProfile();
+ if (profile.getCarrierPrivilegeStatus(mApp.getPackageManager(), callingPackage)
+ != TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
+ filteredInfos.add(cardInfo.getUnprivileged());
+ } else {
+ filteredInfos.add(cardInfo);
+ }
+ }
+ return filteredInfos;
+ }
+ return cardInfos;
+ } catch (PackageManager.NameNotFoundException e) {
+ // This should not happen since we pass the package info in from TelephonyManager
+ throw new SecurityException("Invalid calling package.");
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -5757,11 +6196,6 @@
@Override
public int getCardIdForDefaultEuicc(int subId, String callingPackage) {
- if (!TelephonyPermissions.checkCallingOrSelfReadPhoneState(
- mApp, subId, callingPackage, "getCardIdForDefaultEuicc")) {
- return TelephonyManager.INVALID_CARD_ID;
- }
-
final long identity = Binder.clearCallingIdentity();
try {
return UiccController.getInstance().getCardIdForDefaultEuicc();
@@ -6060,75 +6494,50 @@
}
}
- private Map<Integer, List<EmergencyNumber>> getEmergencyNumberListInternal() {
- Map<Integer, List<EmergencyNumber>> emergencyNumberListInternal = new HashMap<>();
+ /**
+ * Update emergency number list for test mode.
+ */
+ @Override
+ public void updateEmergencyNumberListTestMode(int action, EmergencyNumber num) {
+ TelephonyPermissions.enforceShellOnly(Binder.getCallingUid(),
+ "updateEmergencyNumberListTestMode");
- for (Phone phone: PhoneFactory.getPhones()) {
- if (phone.getEmergencyNumberTracker() != null
- && phone.getEmergencyNumberTracker().getEmergencyNumberList() != null) {
- emergencyNumberListInternal.put(
- phone.getSubId(),
- phone.getEmergencyNumberTracker().getEmergencyNumberList());
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ for (Phone phone: PhoneFactory.getPhones()) {
+ EmergencyNumberTracker tracker = phone.getEmergencyNumberTracker();
+ if (tracker != null) {
+ tracker.executeEmergencyNumberTestModeCommand(action, num);
+ }
}
+ } finally {
+ Binder.restoreCallingIdentity(identity);
}
- return emergencyNumberListInternal;
- }
-
- private List<EmergencyNumber> getEmergencyNumberListFromEccList(int subscriptionId) {
- SubscriptionManager sm = (SubscriptionManager) mApp.getSystemService(
- Context.TELEPHONY_SUBSCRIPTION_SERVICE);
- List<EmergencyNumber> emergencyNumberList = new ArrayList<>();
- int slotId = sm.getSlotIndex(subscriptionId);
-
- String ecclist = (slotId <= 0) ? "ril.ecclist" : ("ril.ecclist" + slotId);
- String emergencyNumbers = SystemProperties.get(ecclist, "");
- if (TextUtils.isEmpty(emergencyNumbers)) {
- // then read-only ecclist property since old RIL only uses this
- emergencyNumbers = SystemProperties.get("ro.ril.ecclist");
- }
- if (!TextUtils.isEmpty(emergencyNumbers)) {
- // searches through the comma-separated list for a match,
- // return true if one is found.
- for (String emergencyNum : emergencyNumbers.split(",")) {
- emergencyNumberList.add(new EmergencyNumber(emergencyNum, "", "",
- EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_UNSPECIFIED, 0));
- }
- }
- emergencyNumbers = ((slotId < 0) ? "112,911,000,08,110,118,119,999" : "112,911");
- for (String emergencyNum : emergencyNumbers.split(",")) {
- emergencyNumberList.add(new EmergencyNumber(emergencyNum, "", "",
- EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_UNSPECIFIED, 0));
- }
- EmergencyNumber.mergeSameNumbersInEmergencyNumberList(emergencyNumberList);
- return emergencyNumberList;
}
/**
- * Get Emergency number list based on EccList. This util is used for solving backward
- * compatibility if device does not support the 1.4 IRadioIndication HAL that reports
- * emergency number list.
- *
- * @return Map including the key as the active subscription ID (Note: if there is no active
- * subscription, the key is {@link SubscriptionManager#getDefaultSubscriptionId})
- * and the value as the list of {@link EmergencyNumber}.
+ * Get the full emergency number list for test mode.
*/
- private Map<Integer, List<EmergencyNumber>> getEmergencyNumberListFromEccList() {
- Map<Integer, List<EmergencyNumber>> results = new HashMap<>();
- SubscriptionManager sm = (SubscriptionManager) mApp.getSystemService(
- Context.TELEPHONY_SUBSCRIPTION_SERVICE);
- int[] activeSubscriptionIds = sm.getActiveSubscriptionIdList();
+ @Override
+ public List<String> getEmergencyNumberListTestMode() {
+ TelephonyPermissions.enforceShellOnly(Binder.getCallingUid(),
+ "getEmergencyNumberListTestMode");
- if (activeSubscriptionIds.length == 0) {
- int defaultSubscriptionId = getDefaultSubscription();
- results.put(defaultSubscriptionId,
- getEmergencyNumberListFromEccList(defaultSubscriptionId));
- } else {
- for (int activeSubscriptionId : activeSubscriptionIds) {
- results.put(activeSubscriptionId,
- getEmergencyNumberListFromEccList(activeSubscriptionId));
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ Set<String> emergencyNumbers = new HashSet<>();
+ for (Phone phone: PhoneFactory.getPhones()) {
+ EmergencyNumberTracker tracker = phone.getEmergencyNumberTracker();
+ if (tracker != null) {
+ for (EmergencyNumber num : tracker.getEmergencyNumberList()) {
+ emergencyNumbers.add(num.getNumber());
+ }
+ }
}
+ return new ArrayList<>(emergencyNumbers);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
}
- return results;
}
@Override
@@ -6150,4 +6559,128 @@
}
return null;
}
+
+ /**
+ * Enable or disable a modem stack.
+ */
+ @Override
+ public boolean enableModemForSlot(int slotIndex, boolean enable) {
+ enforceModifyPermission();
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ Phone phone = PhoneFactory.getPhone(slotIndex);
+ if (phone == null) {
+ return false;
+ } else {
+ return (Boolean) sendRequest(CMD_REQUEST_ENABLE_MODEM, enable, phone, null);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void setMultisimCarrierRestriction(boolean isMultisimCarrierRestricted) {
+ enforceModifyPermission();
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mTelephonySharedPreferences.edit()
+ .putBoolean(PREF_MULTI_SIM_RESTRICTED, isMultisimCarrierRestricted)
+ .commit();
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public boolean isMultisimCarrierRestricted() {
+ enforceReadPrivilegedPermission("isMultisimCarrierRestricted");
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ // If the device has less than 2 SIM cards, indicate that multisim is restricted.
+ int numPhysicalSlots = UiccController.getInstance().getUiccSlots().length;
+ if (numPhysicalSlots < 2) {
+ loge("isMultisimCarrierRestricted: requires at least 2 cards");
+ return true;
+ }
+
+ // Default value is false. Multi SIM is allowed unless explicitly restricted.
+ return mTelephonySharedPreferences.getBoolean(PREF_MULTI_SIM_RESTRICTED, false);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Switch configs to enable multi-sim or switch back to single-sim
+ * @param numOfSims number of active sims we want to switch to
+ */
+ @Override
+ public void switchMultiSimConfig(int numOfSims) {
+ TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
+ mApp, SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, "switchMultiSimConfig");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mPhoneConfigurationManager.switchMultiSimConfig(numOfSims);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Get whether reboot is required or not after making changes to modem configurations.
+ * Return value defaults to true
+ */
+ @Override
+ public boolean isRebootRequiredForModemConfigChange() {
+ enforceReadPrivilegedPermission("isRebootRequiredForModemConfigChange");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return mPhoneConfigurationManager.isRebootRequiredForModemConfigChange();
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ private void updateModemStateMetrics() {
+ TelephonyMetrics metrics = TelephonyMetrics.getInstance();
+ // TODO: check the state for each modem if the api is ready.
+ metrics.updateEnabledModemBitmap((1 << TelephonyManager.from(mApp).getPhoneCount()) - 1);
+ }
+
+ @Override
+ public int[] getSlotsMapping() {
+ enforceReadPrivilegedPermission("getSlotsMapping");
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ int phoneCount = TelephonyManager.getDefault().getPhoneCount();
+ // All logical slots should have a mapping to a physical slot.
+ int[] logicalSlotsMapping = new int[phoneCount];
+ UiccSlotInfo[] slotInfos = getUiccSlotsInfo();
+ for (int i = 0; i < slotInfos.length; i++) {
+ if (SubscriptionManager.isValidPhoneId(slotInfos[i].getLogicalSlotIdx())) {
+ logicalSlotsMapping[slotInfos[i].getLogicalSlotIdx()] = i;
+ }
+ }
+ return logicalSlotsMapping;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Get the IRadio HAL Version
+ */
+ @Override
+ public int getRadioHalVersion() {
+ Phone phone = getDefaultPhone();
+ if (phone == null) return -1;
+ HalVersion hv = phone.getHalVersion();
+ if (hv.equals(HalVersion.UNKNOWN)) return -1;
+ return hv.major * 100 + hv.minor;
+ }
}
diff --git a/src/com/android/phone/PhoneUtils.java b/src/com/android/phone/PhoneUtils.java
index 3306d4b..d3f780f 100644
--- a/src/com/android/phone/PhoneUtils.java
+++ b/src/com/android/phone/PhoneUtils.java
@@ -19,7 +19,6 @@
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
-import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
@@ -1296,6 +1295,34 @@
}
/**
+ * Register ICC status for all phones.
+ */
+ static final void registerIccStatus(Handler handler, int event, int phoneId) {
+ Phone[] phones = PhoneFactory.getPhones();
+ IccCard sim = phones[phoneId].getIccCard();
+ if (sim != null) {
+ if (VDBG) {
+ Log.v(LOG_TAG, "register for ICC status, phone " + phones[phoneId].getPhoneId());
+ }
+ sim.registerForNetworkLocked(handler, event, phones[phoneId]);
+ }
+ }
+
+ /**
+ * Unregister ICC status for a specific phone.
+ */
+ static final void unregisterIccStatus(Handler handler, int phoneId) {
+ Phone[] phones = PhoneFactory.getPhones();
+ IccCard sim = phones[phoneId].getIccCard();
+ if (sim != null) {
+ if (VDBG) {
+ Log.v(LOG_TAG, "unregister for ICC status, phone " + phones[phoneId].getPhoneId());
+ }
+ sim.unregisterForNetworkLocked(handler);
+ }
+ }
+
+ /**
* Set the radio power on/off state for all phones.
*
* @param enabled true means on, false means off.
diff --git a/src/com/android/phone/ShortcutViewUtils.java b/src/com/android/phone/ShortcutViewUtils.java
new file mode 100644
index 0000000..595ea86
--- /dev/null
+++ b/src/com/android/phone/ShortcutViewUtils.java
@@ -0,0 +1,303 @@
+/*
+ * 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 android.content.Context;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.emergency.EmergencyNumber;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+class ShortcutViewUtils {
+ private static final String LOG_TAG = "ShortcutViewUtils";
+
+ // Emergency services which will be promoted on the shortcut view.
+ static final int[] PROMOTED_CATEGORIES = {
+ EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE,
+ EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_AMBULANCE,
+ EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_FIRE_BRIGADE,
+ };
+
+ static final int PROMOTED_CATEGORIES_BITMASK;
+
+ static {
+ int bitmask = 0;
+ for (int category : PROMOTED_CATEGORIES) {
+ bitmask |= category;
+ }
+ PROMOTED_CATEGORIES_BITMASK = bitmask;
+ }
+
+ // Info and emergency call capability of every phone.
+ static class PhoneInfo {
+ private final PhoneAccountHandle mHandle;
+ private final boolean mCanPlaceEmergencyCall;
+ private final int mSubId;
+ private final String mCountryIso;
+ private final List<EmergencyNumber> mPromotedEmergencyNumbers;
+
+ private PhoneInfo(int subId, String countryIso,
+ List<EmergencyNumber> promotedEmergencyNumbers) {
+ this(null, true, subId, countryIso, promotedEmergencyNumbers);
+ }
+
+ private PhoneInfo(PhoneAccountHandle handle, boolean canPlaceEmergencyCall, int subId,
+ String countryIso, List<EmergencyNumber> promotedEmergencyNumbers) {
+ mHandle = handle;
+ mCanPlaceEmergencyCall = canPlaceEmergencyCall;
+ mSubId = subId;
+ mCountryIso = countryIso;
+ mPromotedEmergencyNumbers = promotedEmergencyNumbers;
+ }
+
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mHandle;
+ }
+
+ public boolean canPlaceEmergencyCall() {
+ return mCanPlaceEmergencyCall;
+ }
+
+ public int getSubId() {
+ return mSubId;
+ }
+
+ public String getCountryIso() {
+ return mCountryIso;
+ }
+
+ public List<EmergencyNumber> getPromotedEmergencyNumbers() {
+ return mPromotedEmergencyNumbers;
+ }
+
+ public boolean isSufficientForEmergencyCall(@NonNull Context context) {
+ // Checking mCountryIso because the emergency number list is not reliable to be
+ // suggested to users if the device didn't camp to any network. In this case, users
+ // can still try to dial emergency numbers with dial pad.
+ return mCanPlaceEmergencyCall && mPromotedEmergencyNumbers != null
+ && isSupportedCountry(context, mCountryIso);
+ }
+
+ public boolean hasPromotedEmergencyNumber(String number) {
+ for (EmergencyNumber emergencyNumber : mPromotedEmergencyNumbers) {
+ if (emergencyNumber.getNumber().equalsIgnoreCase(number)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ if (mHandle != null) {
+ sb.append("handle=").append(mHandle.getId()).append(", ");
+ }
+ sb.append("subId=").append(mSubId)
+ .append(", canPlaceEmergencyCall=").append(mCanPlaceEmergencyCall)
+ .append(", networkCountryIso=").append(mCountryIso);
+ if (mPromotedEmergencyNumbers != null) {
+ sb.append(", emergencyNumbers=");
+ for (EmergencyNumber emergencyNumber : mPromotedEmergencyNumbers) {
+ sb.append(emergencyNumber.getNumber()).append(":")
+ .append(emergencyNumber).append(",");
+ }
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Picks a preferred phone (SIM slot) which is sufficient for emergency call and can provide
+ * promoted emergency numbers.
+ *
+ * A promoted emergency number should be dialed out over the preferred phone. Other emergency
+ * numbers should be still dialable over the system default phone.
+ *
+ * @return A preferred phone and its promoted emergency number, or null if no phone/promoted
+ * emergency numbers available.
+ */
+ @Nullable
+ static PhoneInfo pickPreferredPhone(@NonNull Context context) {
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+ if (telephonyManager.getPhoneCount() <= 0) {
+ Log.w(LOG_TAG, "No phone available!");
+ return null;
+ }
+
+ Map<Integer, List<EmergencyNumber>> promotedLists =
+ getPromotedEmergencyNumberLists(telephonyManager);
+ if (promotedLists == null || promotedLists.isEmpty()) {
+ return null;
+ }
+
+ // For a multi-phone device, tries the default phone account.
+ TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
+ PhoneAccountHandle defaultHandle = telecomManager.getDefaultOutgoingPhoneAccount(
+ PhoneAccount.SCHEME_TEL);
+ if (defaultHandle != null) {
+ PhoneInfo phone = loadPhoneInfo(defaultHandle, telephonyManager, telecomManager,
+ promotedLists);
+ if (phone.isSufficientForEmergencyCall(context)) {
+ return phone;
+ }
+ Log.w(LOG_TAG, "Default PhoneAccount is insufficient for emergency call: "
+ + phone.toString());
+ } else {
+ Log.w(LOG_TAG, "Missing default PhoneAccount! Is this really a phone device?");
+ }
+
+ // Looks for any one phone which supports emergency call.
+ List<PhoneAccountHandle> allHandles = telecomManager.getCallCapablePhoneAccounts();
+ if (allHandles != null && !allHandles.isEmpty()) {
+ for (PhoneAccountHandle handle : allHandles) {
+ PhoneInfo phone = loadPhoneInfo(handle, telephonyManager, telecomManager,
+ promotedLists);
+ if (phone.isSufficientForEmergencyCall(context)) {
+ return phone;
+ } else {
+ if (Log.isLoggable(LOG_TAG, Log.DEBUG)) {
+ Log.d(LOG_TAG, "PhoneAccount " + phone.toString()
+ + " is insufficient for emergency call.");
+ }
+ }
+ }
+ }
+
+ Log.w(LOG_TAG, "No PhoneAccount available for emergency call!");
+ return null;
+ }
+
+ private static boolean isSupportedCountry(@NonNull Context context, String countryIso) {
+ if (TextUtils.isEmpty(countryIso)) {
+ return false;
+ }
+
+ String[] countrysToEnableShortcutView = context.getResources().getStringArray(
+ R.array.config_countries_to_enable_shortcut_view);
+ for (String supportedCountry : countrysToEnableShortcutView) {
+ if (countryIso.equalsIgnoreCase(supportedCountry)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static PhoneInfo loadPhoneInfo(@NonNull PhoneAccountHandle handle,
+ @NonNull TelephonyManager telephonyManager, @NonNull TelecomManager telecomManager,
+ Map<Integer, List<EmergencyNumber>> promotedLists) {
+ boolean canPlaceEmergencyCall = false;
+ int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ String countryIso = null;
+ List<EmergencyNumber> emergencyNumberList = null;
+
+ PhoneAccount phoneAccount = telecomManager.getPhoneAccount(handle);
+ if (phoneAccount != null) {
+ canPlaceEmergencyCall = phoneAccount.hasCapabilities(
+ PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+ subId = telephonyManager.getSubIdForPhoneAccount(phoneAccount);
+ }
+
+ TelephonyManager subTelephonyManager = telephonyManager.createForSubscriptionId(subId);
+ if (subTelephonyManager != null) {
+ countryIso = subTelephonyManager.getNetworkCountryIso();
+ }
+
+ if (promotedLists != null) {
+ emergencyNumberList = promotedLists.get(subId);
+ }
+
+ return new PhoneInfo(handle, canPlaceEmergencyCall, subId, countryIso, emergencyNumberList);
+ }
+
+ @NonNull
+ private static Map<Integer, List<EmergencyNumber>> getPromotedEmergencyNumberLists(
+ @NonNull TelephonyManager telephonyManager) {
+ Map<Integer, List<EmergencyNumber>> allLists =
+ telephonyManager.getCurrentEmergencyNumberList();
+ if (allLists == null || allLists.isEmpty()) {
+ Log.w(LOG_TAG, "Unable to retrieve emergency number lists!");
+ return new ArrayMap<>();
+ }
+
+ boolean isDebugLoggable = Log.isLoggable(LOG_TAG, Log.DEBUG);
+ Map<Integer, List<EmergencyNumber>> promotedEmergencyNumberLists = new ArrayMap<>();
+ for (Map.Entry<Integer, List<EmergencyNumber>> entry : allLists.entrySet()) {
+ if (entry.getKey() == null || entry.getValue() == null) {
+ continue;
+ }
+ List<EmergencyNumber> emergencyNumberList = entry.getValue();
+ if (isDebugLoggable) {
+ Log.d(LOG_TAG, "Emergency numbers of " + entry.getKey());
+ }
+
+ // The list of promoted emergency numbers which will be visible on shortcut view.
+ List<EmergencyNumber> promotedList = new ArrayList<>();
+ // A temporary list for non-prioritized emergency numbers.
+ List<EmergencyNumber> tempList = new ArrayList<>();
+
+ for (EmergencyNumber emergencyNumber : emergencyNumberList) {
+ boolean isPromotedCategory = (emergencyNumber.getEmergencyServiceCategoryBitmask()
+ & PROMOTED_CATEGORIES_BITMASK) != 0;
+
+ // Emergency numbers in DATABASE are prioritized for shortcut view since they were
+ // well-categorized.
+ boolean isFromPrioritizedSource =
+ (emergencyNumber.getEmergencyNumberSourceBitmask()
+ & EmergencyNumber.EMERGENCY_NUMBER_SOURCE_DATABASE) != 0;
+ if (isDebugLoggable) {
+ Log.d(LOG_TAG, " " + emergencyNumber
+ + (isPromotedCategory ? "M" : "")
+ + (isFromPrioritizedSource ? "P" : ""));
+ }
+
+ if (isPromotedCategory) {
+ if (isFromPrioritizedSource) {
+ promotedList.add(emergencyNumber);
+ } else {
+ tempList.add(emergencyNumber);
+ }
+ }
+ }
+ // Puts numbers in temp list after prioritized numbers.
+ promotedList.addAll(tempList);
+
+ if (!promotedList.isEmpty()) {
+ promotedEmergencyNumberLists.put(entry.getKey(), promotedList);
+ }
+ }
+
+ if (promotedEmergencyNumberLists.isEmpty()) {
+ Log.w(LOG_TAG, "No promoted emergency number found!");
+ }
+ return promotedEmergencyNumberLists;
+ }
+}
diff --git a/src/com/android/phone/TelephonyShellCommand.java b/src/com/android/phone/TelephonyShellCommand.java
index 9250118..956e0e2 100644
--- a/src/com/android/phone/TelephonyShellCommand.java
+++ b/src/com/android/phone/TelephonyShellCommand.java
@@ -22,11 +22,14 @@
import android.os.ShellCommand;
import android.os.UserHandle;
import android.telephony.SubscriptionManager;
+import android.telephony.emergency.EmergencyNumber;
import android.util.Log;
import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.emergency.EmergencyNumberTracker;
import java.io.PrintWriter;
+import java.util.ArrayList;
/**
* Takes actions based on the adb commands given by "adb shell cmd phone ...". Be careful, no
@@ -44,6 +47,7 @@
private static final String IMS_SUBCOMMAND = "ims";
private static final String SMS_SUBCOMMAND = "sms";
private static final String NUMBER_VERIFICATION_SUBCOMMAND = "numverify";
+ private static final String EMERGENCY_NUMBER_TEST_MODE = "emergency-number-test-mode";
private static final String IMS_SET_CARRIER_SERVICE = "set-ims-service";
private static final String IMS_GET_CARRIER_SERVICE = "get-ims-service";
@@ -79,6 +83,8 @@
}
case NUMBER_VERIFICATION_SUBCOMMAND:
return handleNumberVerificationCommand();
+ case EMERGENCY_NUMBER_TEST_MODE:
+ return handleEmergencyNumberTestModeCommand();
default: {
return handleDefaultCommands(cmd);
}
@@ -95,8 +101,11 @@
pw.println(" IMS Commands.");
pw.println(" sms");
pw.println(" SMS Commands.");
+ pw.println(" emergency-number-test-mode");
+ pw.println(" Emergency Number Test Mode Commands.");
onHelpIms();
onHelpSms();
+ onHelpEmergencyNumber();
}
private void onHelpIms() {
@@ -147,6 +156,20 @@
pw.println(" 1 if the call would have been intercepted, 0 otherwise.");
}
+ private void onHelpEmergencyNumber() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("Emergency Number Test Mode Commands:");
+ pw.println(" emergency-number-test-mode ");
+ pw.println(" Add(-a), Clear(-c), Print (-p) or Remove(-r) the emergency number list in"
+ + " the test mode");
+ pw.println(" -a <emergency number address>: add an emergency number address for the"
+ + " test mode, only allows '0'-'9', '*', or '#'.");
+ pw.println(" -c: clear the emergency number list in the test mode.");
+ pw.println(" -r <emergency number address>: remove an existing emergency number"
+ + " address added by the test mode, only allows '0'-'9', '*', or '#'.");
+ pw.println(" -p: get the full emergency number list in the test mode.");
+ }
+
private int handleImsCommand() {
String arg = getNextArg();
if (arg == null) {
@@ -172,6 +195,91 @@
return -1;
}
+ private int handleEmergencyNumberTestModeCommand() {
+ PrintWriter errPw = getErrPrintWriter();
+ String opt = getNextOption();
+ if (opt == null) {
+ onHelpEmergencyNumber();
+ return 0;
+ }
+
+ switch (opt) {
+ case "-a": {
+ String emergencyNumberCmd = getNextArgRequired();
+ if (emergencyNumberCmd == null
+ || !EmergencyNumber.validateEmergencyNumberAddress(emergencyNumberCmd)) {
+ errPw.println("An emergency number (only allow '0'-'9', '*', or '#') needs"
+ + " to be specified after -a in the command ");
+ return -1;
+ }
+ try {
+ mInterface.updateEmergencyNumberListTestMode(
+ EmergencyNumberTracker.ADD_EMERGENCY_NUMBER_TEST_MODE,
+ new EmergencyNumber(emergencyNumberCmd, "", "",
+ EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_UNSPECIFIED,
+ new ArrayList<String>(),
+ EmergencyNumber.EMERGENCY_NUMBER_SOURCE_TEST,
+ EmergencyNumber.EMERGENCY_CALL_ROUTING_UNKNOWN));
+ } catch (RemoteException ex) {
+ Log.w(LOG_TAG, "emergency-number-test-mode -a " + emergencyNumberCmd
+ + ", error " + ex.getMessage());
+ errPw.println("Exception: " + ex.getMessage());
+ return -1;
+ }
+ break;
+ }
+ case "-c": {
+ try {
+ mInterface.updateEmergencyNumberListTestMode(
+ EmergencyNumberTracker.RESET_EMERGENCY_NUMBER_TEST_MODE, null);
+ } catch (RemoteException ex) {
+ Log.w(LOG_TAG, "emergency-number-test-mode -c " + "error " + ex.getMessage());
+ errPw.println("Exception: " + ex.getMessage());
+ return -1;
+ }
+ break;
+ }
+ case "-r": {
+ String emergencyNumberCmd = getNextArgRequired();
+ if (emergencyNumberCmd == null
+ || !EmergencyNumber.validateEmergencyNumberAddress(emergencyNumberCmd)) {
+ errPw.println("An emergency number (only allow '0'-'9', '*', or '#') needs"
+ + " to be specified after -r in the command ");
+ return -1;
+ }
+ try {
+ mInterface.updateEmergencyNumberListTestMode(
+ EmergencyNumberTracker.REMOVE_EMERGENCY_NUMBER_TEST_MODE,
+ new EmergencyNumber(emergencyNumberCmd, "", "",
+ EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_UNSPECIFIED,
+ new ArrayList<String>(),
+ EmergencyNumber.EMERGENCY_NUMBER_SOURCE_TEST,
+ EmergencyNumber.EMERGENCY_CALL_ROUTING_UNKNOWN));
+ } catch (RemoteException ex) {
+ Log.w(LOG_TAG, "emergency-number-test-mode -r " + emergencyNumberCmd
+ + ", error " + ex.getMessage());
+ errPw.println("Exception: " + ex.getMessage());
+ return -1;
+ }
+ break;
+ }
+ case "-p": {
+ try {
+ getOutPrintWriter().println(mInterface.getEmergencyNumberListTestMode());
+ } catch (RemoteException ex) {
+ Log.w(LOG_TAG, "emergency-number-test-mode -p " + "error " + ex.getMessage());
+ errPw.println("Exception: " + ex.getMessage());
+ return -1;
+ }
+ break;
+ }
+ default:
+ onHelpEmergencyNumber();
+ break;
+ }
+ return 0;
+ }
+
private int handleNumberVerificationCommand() {
String arg = getNextArg();
if (arg == null) {
diff --git a/src/com/android/phone/settings/PhoneAccountSettingsFragment.java b/src/com/android/phone/settings/PhoneAccountSettingsFragment.java
index 670f98d..ca45b31 100644
--- a/src/com/android/phone/settings/PhoneAccountSettingsFragment.java
+++ b/src/com/android/phone/settings/PhoneAccountSettingsFragment.java
@@ -44,7 +44,7 @@
"phone_accounts_accounts_list_category_key";
private static final String DEFAULT_OUTGOING_ACCOUNT_KEY = "default_outgoing_account";
- private static final String ALL_CALLING_ACCOUNTS_KEY = "phone_account_all_calling_accounts";
+ private static final String ALL_CALLING_ACCOUNTS_KEY = "phone_accounts_all_calling_accounts";
private static final String SIP_SETTINGS_CATEGORY_PREF_KEY =
"phone_accounts_sip_settings_category_key";
@@ -71,11 +71,21 @@
private PreferenceCategory mAccountList;
private AccountSelectionPreference mDefaultOutgoingAccount;
+ private Preference mAllCallingAccounts;
private ListPreference mUseSipCalling;
private SwitchPreference mSipReceiveCallsPreference;
private SipPreferences mSipPreferences;
+ private final SubscriptionManager.OnSubscriptionsChangedListener
+ mOnSubscriptionsChangeListener =
+ new SubscriptionManager.OnSubscriptionsChangedListener() {
+ @Override
+ public void onSubscriptionsChanged() {
+ updateAccounts();
+ }
+ };
+
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
@@ -122,34 +132,11 @@
*/
mAccountList = (PreferenceCategory) getPreferenceScreen().findPreference(
ACCOUNTS_LIST_CATEGORY_KEY);
- List<PhoneAccountHandle> allNonSimAccounts =
- getCallingAccounts(false /* includeSims */, true /* includeDisabled */);
- // Check to see if we should show the entire section at all.
- if (shouldShowConnectionServiceList(allNonSimAccounts)) {
- List<PhoneAccountHandle> enabledAccounts =
- getCallingAccounts(true /* includeSims */, false /* includeDisabled */);
- // Initialize the account list with the set of enabled & SIM accounts.
- initAccountList(enabledAccounts);
+ mDefaultOutgoingAccount = (AccountSelectionPreference)
+ getPreferenceScreen().findPreference(DEFAULT_OUTGOING_ACCOUNT_KEY);
+ mAllCallingAccounts = getPreferenceScreen().findPreference(ALL_CALLING_ACCOUNTS_KEY);
- mDefaultOutgoingAccount = (AccountSelectionPreference)
- getPreferenceScreen().findPreference(DEFAULT_OUTGOING_ACCOUNT_KEY);
- mDefaultOutgoingAccount.setListener(this);
-
- // Only show the 'Make Calls With..." option if there are multiple accounts.
- if (enabledAccounts.size() > 1) {
- updateDefaultOutgoingAccountsModel();
- } else {
- mAccountList.removePreference(mDefaultOutgoingAccount);
- }
-
- Preference allAccounts = getPreferenceScreen().findPreference(ALL_CALLING_ACCOUNTS_KEY);
- // If there are no third party (nonSim) accounts, then don't show enable/disable dialog.
- if (allNonSimAccounts.isEmpty() && allAccounts != null) {
- mAccountList.removePreference(allAccounts);
- }
- } else {
- getPreferenceScreen().removePreference(mAccountList);
- }
+ updateAccounts();
if (isPrimaryUser() && SipUtil.isVoipSupported(getActivity())) {
mSipPreferences = new SipPreferences(getActivity());
@@ -183,6 +170,16 @@
getPreferenceScreen().removePreference(
getPreferenceScreen().findPreference(SIP_SETTINGS_CATEGORY_PREF_KEY));
}
+
+ SubscriptionManager.from(getActivity()).addOnSubscriptionsChangedListener(
+ mOnSubscriptionsChangeListener);
+ }
+
+ @Override
+ public void onPause() {
+ SubscriptionManager.from(getActivity()).removeOnSubscriptionsChangedListener(
+ mOnSubscriptionsChangeListener);
+ super.onPause();
}
/**
@@ -391,6 +388,40 @@
return mTelephonyManager.isMultiSimEnabled() || allNonSimAccounts.size() > 0;
}
+ private void updateAccounts() {
+ if (mAccountList != null) {
+ mAccountList.removeAll();
+ List<PhoneAccountHandle> allNonSimAccounts =
+ getCallingAccounts(false /* includeSims */, true /* includeDisabled */);
+ // Check to see if we should show the entire section at all.
+ if (shouldShowConnectionServiceList(allNonSimAccounts)) {
+ List<PhoneAccountHandle> enabledAccounts =
+ getCallingAccounts(true /* includeSims */, false /* includeDisabled */);
+ // Initialize the account list with the set of enabled & SIM accounts.
+ initAccountList(enabledAccounts);
+
+ mDefaultOutgoingAccount.setListener(this);
+ // Only show the 'Make Calls With..." option if there are multiple accounts.
+ if (enabledAccounts.size() > 1) {
+ mAccountList.addPreference(mDefaultOutgoingAccount);
+ updateDefaultOutgoingAccountsModel();
+ } else {
+ mAccountList.removePreference(mDefaultOutgoingAccount);
+ }
+
+ // If there are no third party (nonSim) accounts,
+ // then don't show enable/disable dialog.
+ if (!allNonSimAccounts.isEmpty()) {
+ mAccountList.addPreference(mAllCallingAccounts);
+ } else {
+ mAccountList.removePreference(mAllCallingAccounts);
+ }
+ } else {
+ getPreferenceScreen().removePreference(mAccountList);
+ }
+ }
+ }
+
private List<PhoneAccountHandle> getCallingAccounts(
boolean includeSims, boolean includeDisabledAccounts) {
PhoneAccountHandle emergencyAccountHandle = getEmergencyPhoneAccount();
diff --git a/src/com/android/services/telephony/ConferenceParticipantConnection.java b/src/com/android/services/telephony/ConferenceParticipantConnection.java
index 82baa92..1f330f9 100644
--- a/src/com/android/services/telephony/ConferenceParticipantConnection.java
+++ b/src/com/android/services/telephony/ConferenceParticipantConnection.java
@@ -33,11 +33,6 @@
* Represents a participant in a conference call.
*/
public class ConferenceParticipantConnection extends Connection {
- /**
- * RFC5767 states that a SIP URI with an unknown number should use an address of
- * {@code anonymous@anonymous.invalid}. E.g. the host name is anonymous.invalid.
- */
- private static final String ANONYMOUS_INVALID_HOST = "anonymous.invalid";
/**
* The user entity URI For the conference participant.
@@ -65,7 +60,7 @@
mParentConnection = parentConnection;
- int presentation = getParticipantPresentation(participant);
+ int presentation = participant.getParticipantPresentation();
Uri address;
if (presentation != PhoneConstants.PRESENTATION_ALLOWED) {
address = null;
@@ -161,53 +156,7 @@
setConnectionCapabilities(capabilities);
}
- /**
- * Determines the number presentation for a conference participant. Per RFC5767, if the host
- * name contains {@code anonymous.invalid} we can assume that there is no valid caller ID
- * information for the caller, otherwise we'll assume that the URI can be shown.
- *
- * @param participant The conference participant.
- * @return The number presentation.
- */
- private int getParticipantPresentation(ConferenceParticipant participant) {
- Uri address = participant.getHandle();
- if (address == null) {
- return PhoneConstants.PRESENTATION_RESTRICTED;
- }
- String number = address.getSchemeSpecificPart();
- // If no number, bail early and set restricted presentation.
- if (TextUtils.isEmpty(number)) {
- return PhoneConstants.PRESENTATION_RESTRICTED;
- }
- // Per RFC3261, the host name portion can also potentially include extra information:
- // E.g. sip:anonymous1@anonymous.invalid;legid=1
- // In this case, hostName will be anonymous.invalid and there is an extra parameter for
- // legid=1.
- // Parameters are optional, and the address (e.g. test@test.com) will always be the first
- // part, with any parameters coming afterwards.
- String hostParts[] = number.split("[;]");
- String addressPart = hostParts[0];
-
- // Get the number portion from the address part.
- // This will typically be formatted similar to: 6505551212@test.com
- String numberParts[] = addressPart.split("[@]");
-
- // If we can't parse the host name out of the URI, then there is probably other data
- // present, and is likely a valid SIP URI.
- if (numberParts.length != 2) {
- return PhoneConstants.PRESENTATION_ALLOWED;
- }
- String hostName = numberParts[1];
-
- // If the hostname portion of the SIP URI is the invalid host string, presentation is
- // restricted.
- if (hostName.equals(ANONYMOUS_INVALID_HOST)) {
- return PhoneConstants.PRESENTATION_RESTRICTED;
- }
-
- return PhoneConstants.PRESENTATION_ALLOWED;
- }
/**
* Attempts to build a tel: style URI from a conference participant.
@@ -311,6 +260,10 @@
sb.append(Log.pii(mParentConnection.getAddress()));
sb.append(" state:");
sb.append(Connection.stateToString(getState()));
+ sb.append(" connectTime:");
+ sb.append(getConnectTimeMillis());
+ sb.append(" connectElapsedTime:");
+ sb.append(getConnectElapsedTimeMillis());
sb.append("]");
return sb.toString();
diff --git a/src/com/android/services/telephony/ImsConference.java b/src/com/android/services/telephony/ImsConference.java
index 5722834..cada504 100644
--- a/src/com/android/services/telephony/ImsConference.java
+++ b/src/com/android/services/telephony/ImsConference.java
@@ -29,11 +29,13 @@
import android.telecom.Log;
import android.telecom.PhoneAccountHandle;
import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.CarrierConfigManager;
import android.telephony.PhoneNumberUtils;
import android.util.Pair;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.CallStateException;
import com.android.internal.telephony.Phone;
@@ -49,6 +51,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
/**
* Represents an IMS conference call.
@@ -68,6 +71,13 @@
public class ImsConference extends Conference implements Holdable {
/**
+ * Abstracts out fetching a feature flag. Makes testing easier.
+ */
+ public interface FeatureFlagProxy {
+ boolean isUsingSinglePartyCallEmulation();
+ }
+
+ /**
* Listener used to respond to changes to conference participants. At the conference level we
* are most concerned with handling destruction of a conference participant.
*/
@@ -241,6 +251,25 @@
private final Object mUpdateSyncRoot = new Object();
private boolean mIsHoldable;
+ private boolean mCouldManageConference;
+ private FeatureFlagProxy mFeatureFlagProxy;
+ private boolean mIsEmulatingSinglePartyCall = false;
+ /**
+ * Where {@link #mIsEmulatingSinglePartyCall} is {@code true}, contains the
+ * {@link ConferenceParticipantConnection#getUserEntity()} and
+ * {@link ConferenceParticipantConnection#getEndpoint()} of the single participant which this
+ * conference pretends to be.
+ */
+ private Pair<Uri, Uri> mLoneParticipantIdentity = null;
+
+ /**
+ * The {@link ConferenceParticipantConnection#getUserEntity()} and
+ * {@link ConferenceParticipantConnection#getEndpoint()} of the conference host as they appear
+ * in the CEP. This is determined when we scan the first conference event package.
+ * It is possible that this will be {@code null} for carriers which do not include the host
+ * in the CEP.
+ */
+ private Pair<Uri, Uri> mHostParticipantIdentity = null;
public void updateConferenceParticipantsAfterCreation() {
if (mConferenceHost != null) {
@@ -254,19 +283,21 @@
/**
* Initializes a new {@link ImsConference}.
- *
- * @param telephonyConnectionService The connection service responsible for adding new
+ * @param telephonyConnectionService The connection service responsible for adding new
* conferene participants.
* @param conferenceHost The telephony connection hosting the conference.
* @param phoneAccountHandle The phone account handle associated with the conference.
+ * @param featureFlagProxy
*/
public ImsConference(TelecomAccountRegistry telecomAccountRegistry,
- TelephonyConnectionServiceProxy telephonyConnectionService,
- TelephonyConnection conferenceHost, PhoneAccountHandle phoneAccountHandle) {
+ TelephonyConnectionServiceProxy telephonyConnectionService,
+ TelephonyConnection conferenceHost, PhoneAccountHandle phoneAccountHandle,
+ FeatureFlagProxy featureFlagProxy) {
super(phoneAccountHandle);
mTelecomAccountRegistry = telecomAccountRegistry;
+ mFeatureFlagProxy = featureFlagProxy;
// Specify the connection time of the conference to be the connection time of the original
// connection.
@@ -509,6 +540,8 @@
@Override
public void onConnectionAdded(android.telecom.Connection connection) {
// No-op
+ Log.d(this, "connection added: " + connection
+ + ", time: " + connection.getConnectTimeMillis());
}
@Override
@@ -561,16 +594,25 @@
}
/**
- * Updates the manage conference capability of the conference. Where there are one or more
- * conference event package participants, the conference management is permitted. Where there
- * are no conference event package participants, conference management is not permitted.
+ * Updates the manage conference capability of the conference.
+ *
+ * The following cases are handled:
+ * <ul>
+ * <li>There is only a single participant in the conference -- manage conference is
+ * disabled.</li>
+ * <li>There is more than one participant in the conference -- manage conference is
+ * enabled.</li>
+ * <li>No conference event package data is available -- manage conference is disabled.</li>
+ * </ul>
* <p>
* Note: We add and remove {@link Connection#CAPABILITY_CONFERENCE_HAS_NO_CHILDREN} to ensure
* that the conference is represented appropriately on Bluetooth devices.
*/
private void updateManageConference() {
boolean couldManageConference = can(Connection.CAPABILITY_MANAGE_CONFERENCE);
- boolean canManageConference = !mConferenceParticipantConnections.isEmpty();
+ boolean canManageConference = mFeatureFlagProxy.isUsingSinglePartyCallEmulation()
+ ? mConferenceParticipantConnections.size() > 1
+ : mConferenceParticipantConnections.size() != 0;
Log.v(this, "updateManageConference was :%s is:%s", couldManageConference ? "Y" : "N",
canManageConference ? "Y" : "N");
@@ -649,7 +691,8 @@
* @param parent The connection which was notified of the conference participant.
* @param participants The conference participant information.
*/
- private void handleConferenceParticipantsUpdate(
+ @VisibleForTesting
+ public void handleConferenceParticipantsUpdate(
TelephonyConnection parent, List<ConferenceParticipant> participants) {
if (participants == null) {
@@ -668,64 +711,102 @@
// update adds new participants, and the second does something like update the status of one
// of the participants, we can get into a situation where the participant is added twice.
synchronized (mUpdateSyncRoot) {
+ int oldParticipantCount = mConferenceParticipantConnections.size();
boolean newParticipantsAdded = false;
boolean oldParticipantsRemoved = false;
ArrayList<ConferenceParticipant> newParticipants = new ArrayList<>(participants.size());
HashSet<Pair<Uri,Uri>> participantUserEntities = new HashSet<>(participants.size());
- // Add any new participants and update existing.
- for (ConferenceParticipant participant : participants) {
- Pair<Uri,Uri> userEntity = new Pair<>(participant.getHandle(),
- participant.getEndpoint());
+ // Determine if the conference event package represents a single party conference.
+ // A single party conference is one where there is no other participant other than the
+ // conference host and one other participant.
+ boolean isSinglePartyConference = participants.stream()
+ .filter(p -> {
+ Pair<Uri, Uri> pIdent = new Pair<>(p.getHandle(), p.getEndpoint());
+ return !Objects.equals(mHostParticipantIdentity, pIdent);
+ })
+ .count() == 1;
- participantUserEntities.add(userEntity);
- if (!mConferenceParticipantConnections.containsKey(userEntity)) {
- // Some carriers will also include the conference host in the CEP. We will
- // filter that out here.
- if (!isParticipantHost(mConferenceHostAddress, participant.getHandle())) {
- createConferenceParticipantConnection(parent, participant);
- newParticipants.add(participant);
- newParticipantsAdded = true;
+ // We will only process the CEP data if:
+ // 1. We're not emulating a single party call.
+ // 2. We're emulating a single party call and the CEP contains more than just the
+ // single party
+ if ((mIsEmulatingSinglePartyCall && !isSinglePartyConference) ||
+ !mIsEmulatingSinglePartyCall) {
+ // Add any new participants and update existing.
+ for (ConferenceParticipant participant : participants) {
+ Pair<Uri, Uri> userEntity = new Pair<>(participant.getHandle(),
+ participant.getEndpoint());
+
+ participantUserEntities.add(userEntity);
+ if (!mConferenceParticipantConnections.containsKey(userEntity)) {
+ // Some carriers will also include the conference host in the CEP. We will
+ // filter that out here.
+ if (!isParticipantHost(mConferenceHostAddress, participant.getHandle())) {
+ createConferenceParticipantConnection(parent, participant);
+ newParticipants.add(participant);
+ newParticipantsAdded = true;
+ } else {
+ // Track the identity of the conference host; its useful to know when
+ // we look at the CEP in the future.
+ mHostParticipantIdentity = userEntity;
+ }
+ } else {
+ ConferenceParticipantConnection connection =
+ mConferenceParticipantConnections.get(userEntity);
+ Log.i(this,
+ "handleConferenceParticipantsUpdate: updateState, participant = %s",
+ participant);
+ connection.updateState(participant.getState());
+ connection.setVideoState(parent.getVideoState());
}
- } else {
- ConferenceParticipantConnection connection =
- mConferenceParticipantConnections.get(userEntity);
- Log.i(this, "handleConferenceParticipantsUpdate: updateState, participant = %s",
- participant);
- connection.updateState(participant.getState());
- connection.setVideoState(parent.getVideoState());
+ }
+
+ // Set state of new participants.
+ if (newParticipantsAdded) {
+ // Set the state of the new participants at once and add to the conference
+ for (ConferenceParticipant newParticipant : newParticipants) {
+ ConferenceParticipantConnection connection =
+ mConferenceParticipantConnections.get(new Pair<>(
+ newParticipant.getHandle(),
+ newParticipant.getEndpoint()));
+ connection.updateState(newParticipant.getState());
+ connection.setVideoState(parent.getVideoState());
+ }
+ }
+
+ // Finally, remove any participants from the conference that no longer exist in the
+ // conference event package data.
+ Iterator<Map.Entry<Pair<Uri, Uri>, ConferenceParticipantConnection>> entryIterator =
+ mConferenceParticipantConnections.entrySet().iterator();
+ while (entryIterator.hasNext()) {
+ Map.Entry<Pair<Uri, Uri>, ConferenceParticipantConnection> entry =
+ entryIterator.next();
+
+ if (!participantUserEntities.contains(entry.getKey())) {
+ ConferenceParticipantConnection participant = entry.getValue();
+ participant.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
+ participant.removeConnectionListener(mParticipantListener);
+ mTelephonyConnectionService.removeConnection(participant);
+ removeConnection(participant);
+ entryIterator.remove();
+ oldParticipantsRemoved = true;
+ }
}
}
- // Set state of new participants.
- if (newParticipantsAdded) {
- // Set the state of the new participants at once and add to the conference
- for (ConferenceParticipant newParticipant : newParticipants) {
- ConferenceParticipantConnection connection =
- mConferenceParticipantConnections.get(new Pair<>(
- newParticipant.getHandle(),
- newParticipant.getEndpoint()));
- connection.updateState(newParticipant.getState());
- connection.setVideoState(parent.getVideoState());
- }
- }
-
- // Finally, remove any participants from the conference that no longer exist in the
- // conference event package data.
- Iterator<Map.Entry<Pair<Uri, Uri>, ConferenceParticipantConnection>> entryIterator =
- mConferenceParticipantConnections.entrySet().iterator();
- while (entryIterator.hasNext()) {
- Map.Entry<Pair<Uri, Uri>, ConferenceParticipantConnection> entry =
- entryIterator.next();
-
- if (!participantUserEntities.contains(entry.getKey())) {
- ConferenceParticipantConnection participant = entry.getValue();
- participant.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
- participant.removeConnectionListener(mParticipantListener);
- mTelephonyConnectionService.removeConnection(participant);
- removeConnection(participant);
- entryIterator.remove();
- oldParticipantsRemoved = true;
+ int newParticipantCount = mConferenceParticipantConnections.size();
+ Log.v(this, "handleConferenceParticipantsUpdate: oldParticipantCount=%d, "
+ + "newParticipantcount=%d", oldParticipantCount, newParticipantCount);
+ // If the single party call emulation fature flag is enabled, we can potentially treat
+ // the conference as a single party call when there is just one participant.
+ if (mFeatureFlagProxy.isUsingSinglePartyCallEmulation()) {
+ if (oldParticipantCount > 1 && newParticipantCount == 1) {
+ // If number of participants goes to 1, emulate a single party call.
+ startEmulatingSinglePartyCall();
+ } else if (mIsEmulatingSinglePartyCall && !isSinglePartyConference) {
+ // Number of participants increased, so stop emulating a single party call.
+ stopEmulatingSinglePartyCall();
}
}
@@ -738,6 +819,89 @@
}
/**
+ * Called after {@link #startEmulatingSinglePartyCall()} to cause the conference to appear as
+ * if it is a conference again.
+ * 1. Tell telecom we're a conference again.
+ * 2. Restore {@link Connection#CAPABILITY_MANAGE_CONFERENCE} capability.
+ * 3. Null out the name/address.
+ */
+ private void stopEmulatingSinglePartyCall() {
+ Log.i(this, "stopEmulatingSinglePartyCall: conference now has more than one"
+ + " participant; make it look conference-like again.");
+ mIsEmulatingSinglePartyCall = false;
+
+ if (mCouldManageConference) {
+ int currentCapabilities = getConnectionCapabilities();
+ currentCapabilities |= Connection.CAPABILITY_MANAGE_CONFERENCE;
+ setConnectionCapabilities(currentCapabilities);
+ }
+
+ // Null out the address/name so it doesn't look like a single party call
+ setAddress(null, TelecomManager.PRESENTATION_UNKNOWN);
+ setCallerDisplayName(null, TelecomManager.PRESENTATION_UNKNOWN);
+
+ // Copy the conference connect time back to the previous lone participant.
+ ConferenceParticipantConnection loneParticipant =
+ mConferenceParticipantConnections.get(mLoneParticipantIdentity);
+ if (loneParticipant != null) {
+ Log.d(this,
+ "stopEmulatingSinglePartyCall: restored lone participant connect time");
+ loneParticipant.setConnectTimeMillis(getConnectionTime());
+ loneParticipant.setConnectionStartElapsedRealTime(getConnectionStartElapsedRealTime());
+ }
+
+ // Tell Telecom its a conference again.
+ setConferenceState(true);
+ }
+
+ /**
+ * Called when a conference drops to a single participant. Causes this conference to present
+ * itself to Telecom as if it was a single party call.
+ * 1. Remove the participant from Telecom and from local tracking; when we get a new CEP in
+ * the future we'll just re-add the participant anyways.
+ * 2. Tell telecom we're not a conference.
+ * 3. Remove {@link Connection#CAPABILITY_MANAGE_CONFERENCE} capability.
+ * 4. Set the name/address to that of the single participant.
+ */
+ private void startEmulatingSinglePartyCall() {
+ Log.i(this, "startEmulatingSinglePartyCall: conference has a single "
+ + "participant; downgrade to single party call.");
+
+ mIsEmulatingSinglePartyCall = true;
+ Iterator<ConferenceParticipantConnection> valueIterator =
+ mConferenceParticipantConnections.values().iterator();
+ if (valueIterator.hasNext()) {
+ ConferenceParticipantConnection entry = valueIterator.next();
+
+ // Set the conference name/number to that of the remaining participant.
+ setAddress(entry.getAddress(), entry.getAddressPresentation());
+ setCallerDisplayName(entry.getCallerDisplayName(),
+ entry.getCallerDisplayNamePresentation());
+ setConnectionStartElapsedRealTime(entry.getConnectElapsedTimeMillis());
+ setConnectionTime(entry.getConnectTimeMillis());
+ mLoneParticipantIdentity = new Pair<>(entry.getUserEntity(), entry.getEndpoint());
+
+ // Remove the participant from Telecom. It'll get picked up in a future CEP update
+ // again anyways.
+ entry.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED,
+ DisconnectCause.REASON_EMULATING_SINGLE_CALL));
+ entry.removeConnectionListener(mParticipantListener);
+ mTelephonyConnectionService.removeConnection(entry);
+ removeConnection(entry);
+ valueIterator.remove();
+ }
+
+ // Have Telecom pretend its not a conference.
+ setConferenceState(false);
+
+ // Remove manage conference capability.
+ mCouldManageConference = can(Connection.CAPABILITY_MANAGE_CONFERENCE);
+ int currentCapabilities = getConnectionCapabilities();
+ currentCapabilities &= ~Connection.CAPABILITY_MANAGE_CONFERENCE;
+ setConnectionCapabilities(currentCapabilities);
+ }
+
+ /**
* Creates a new {@link ConferenceParticipantConnection} to represent a
* {@link ConferenceParticipant}.
* <p>
@@ -755,8 +919,13 @@
ConferenceParticipantConnection connection = new ConferenceParticipantConnection(
parent.getOriginalConnection(), participant);
connection.addConnectionListener(mParticipantListener);
- connection.setConnectTimeMillis(parent.getConnectTimeMillis());
-
+ if (participant.getConnectTime() == 0) {
+ connection.setConnectTimeMillis(parent.getConnectTimeMillis());
+ connection.setConnectionStartElapsedRealTime(parent.getConnectElapsedTimeMillis());
+ } else {
+ connection.setConnectTimeMillis(participant.getConnectTime());
+ connection.setConnectionStartElapsedRealTime(participant.getConnectElapsedTime());
+ }
Log.i(this, "createConferenceParticipantConnection: participant=%s, connection=%s",
participant, connection);
@@ -919,6 +1088,7 @@
c.updateState();
// Copy the connect time from the conferenceHost
c.setConnectTimeMillis(mConferenceHost.getConnectTimeMillis());
+ c.setConnectionStartElapsedRealTime(mConferenceHost.getConnectElapsedTimeMillis());
mTelephonyConnectionService.addExistingConnection(phoneAccountHandle, c);
mTelephonyConnectionService.addConnectionToConferenceController(c);
} // CDMA case not applicable for SRVCC
@@ -1000,8 +1170,7 @@
setStatusHints(new StatusHints(
context.getString(R.string.status_hint_label_wifi_call),
Icon.createWithResource(
- context.getResources(),
- R.drawable.ic_signal_wifi_4_bar_24dp),
+ context, R.drawable.ic_signal_wifi_4_bar_24dp),
null /* extras */));
}
} else {
diff --git a/src/com/android/services/telephony/ImsConferenceController.java b/src/com/android/services/telephony/ImsConferenceController.java
index 971dd7b..9902700 100644
--- a/src/com/android/services/telephony/ImsConferenceController.java
+++ b/src/com/android/services/telephony/ImsConferenceController.java
@@ -93,6 +93,8 @@
*/
private final TelephonyConnectionServiceProxy mConnectionService;
+ private final ImsConference.FeatureFlagProxy mFeatureFlagProxy;
+
/**
* List of known {@link TelephonyConnection}s.
*/
@@ -110,11 +112,14 @@
* Creates a new instance of the Ims conference controller.
*
* @param connectionService The current connection service.
+ * @param featureFlagProxy
*/
public ImsConferenceController(TelecomAccountRegistry telecomAccountRegistry,
- TelephonyConnectionServiceProxy connectionService) {
+ TelephonyConnectionServiceProxy connectionService,
+ ImsConference.FeatureFlagProxy featureFlagProxy) {
mConnectionService = connectionService;
mTelecomAccountRegistry = telecomAccountRegistry;
+ mFeatureFlagProxy = featureFlagProxy;
}
/**
@@ -372,7 +377,7 @@
}
ImsConference conference = new ImsConference(mTelecomAccountRegistry, mConnectionService,
- conferenceHostConnection, phoneAccountHandle);
+ conferenceHostConnection, phoneAccountHandle, mFeatureFlagProxy);
conference.setState(conferenceHostConnection.getState());
conference.addListener(mConferenceListener);
conference.updateConferenceParticipantsAfterCreation();
diff --git a/src/com/android/services/telephony/RadioOnHelper.java b/src/com/android/services/telephony/RadioOnHelper.java
index cd08289..288c72c 100644
--- a/src/com/android/services/telephony/RadioOnHelper.java
+++ b/src/com/android/services/telephony/RadioOnHelper.java
@@ -93,13 +93,12 @@
* get an onServiceStateChanged() callback when the radio successfully comes up.
*/
private void powerOnRadio() {
- Log.d(this, "powerOnRadio().");
// If airplane mode is on, we turn it off the same way that the Settings activity turns it
// off.
if (Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 0) > 0) {
- Log.d(this, "==> Turning off airplane mode.");
+ Log.d(this, "==> Turning off airplane mode for emergency call.");
// Change the system setting
Settings.Global.putInt(mContext.getContentResolver(),
diff --git a/src/com/android/services/telephony/RadioOnStateListener.java b/src/com/android/services/telephony/RadioOnStateListener.java
index 91a7d77..729f6a9 100644
--- a/src/com/android/services/telephony/RadioOnStateListener.java
+++ b/src/com/android/services/telephony/RadioOnStateListener.java
@@ -46,16 +46,16 @@
}
// Number of times to retry the call, and time between retry attempts.
+ // not final for testing
private static int MAX_NUM_RETRIES = 5;
+ // not final for testing
private static long TIME_BETWEEN_RETRIES_MILLIS = 5000; // msec
// Handler message codes; see handleMessage()
- @VisibleForTesting
- public static final int MSG_START_SEQUENCE = 1;
+ private static final int MSG_START_SEQUENCE = 1;
@VisibleForTesting
public static final int MSG_SERVICE_STATE_CHANGED = 2;
- @VisibleForTesting
- public static final int MSG_RETRY_TIMEOUT = 3;
+ private static final int MSG_RETRY_TIMEOUT = 3;
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
diff --git a/src/com/android/services/telephony/TelecomAccountRegistry.java b/src/com/android/services/telephony/TelecomAccountRegistry.java
index 228eed1..b29499f 100644
--- a/src/com/android/services/telephony/TelecomAccountRegistry.java
+++ b/src/com/android/services/telephony/TelecomAccountRegistry.java
@@ -46,6 +46,7 @@
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsException;
import android.telephony.ims.ImsMmTelManager;
import android.telephony.ims.feature.MmTelFeature;
import android.telephony.ims.stub.ImsRegistrationImplBase;
@@ -67,7 +68,7 @@
* Owns all data we have registered with Telecom including handling dynamic addition and
* removal of SIMs and SIP accounts.
*/
-public final class TelecomAccountRegistry {
+public class TelecomAccountRegistry {
private static final boolean DBG = false; /* STOP SHIP if true */
// This icon is the one that is used when the Slot ID that we have for a particular SIM
@@ -84,6 +85,7 @@
private boolean mIsRttCapable;
private MmTelFeature.MmTelCapabilities mMmTelCapabilities;
private ImsMmTelManager.CapabilityCallback mMmtelCapabilityCallback;
+ private ImsMmTelManager mMmTelManager;
private final boolean mIsDummy;
private boolean mIsVideoCapable;
private boolean mIsVideoPresenceSupported;
@@ -106,51 +108,58 @@
mPhoneCapabilitiesNotifier = new PstnPhoneCapabilitiesNotifier((Phone) mPhone,
this);
- if (!mIsDummy) {
- ImsMmTelManager manager;
- try {
- manager = ImsMmTelManager.createForSubscriptionId(mContext, getSubId());
- } catch (IllegalArgumentException e) {
- Log.i(this, "Not registering Mmtel listener because the subid is invalid");
- return;
- }
-
- boolean isImsVoiceCapable = manager.isCapable(
- ImsRegistrationImplBase.REGISTRATION_TECH_LTE,
- MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE)
- || manager.isCapable(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN,
- MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
-
- if (!isImsVoiceCapable) {
- Log.i(this, "Not registering MmTel listener because"
- + " voice over IMS isn't supported");
- return;
- }
-
- mMmtelCapabilityCallback =
- new ImsMmTelManager.CapabilityCallback() {
- @Override
- public void onCapabilitiesStatusChanged(
- MmTelFeature.MmTelCapabilities capabilities) {
- mMmTelCapabilities = capabilities;
- updateRttCapability();
- }
- };
- manager.registerMmTelCapabilityCallback(mContext.getMainExecutor(),
- mMmtelCapabilityCallback);
+ if (mIsDummy || isEmergency) {
+ // For dummy and emergency entries, there is no sub ID that can be assigned, so do
+ // not register for capabilities callbacks.
+ return;
}
+
+ try {
+ mMmTelManager = ImsMmTelManager.createForSubscriptionId(getSubId());
+ } catch (IllegalArgumentException e) {
+ Log.i(this, "Not registering MmTel capabilities listener because the subid '"
+ + getSubId() + "' is invalid: " + e.getMessage());
+ return;
+ }
+
+ mMmtelCapabilityCallback = new ImsMmTelManager.CapabilityCallback() {
+ @Override
+ public void onCapabilitiesStatusChanged(
+ MmTelFeature.MmTelCapabilities capabilities) {
+ mMmTelCapabilities = capabilities;
+ updateRttCapability();
+ }
+ };
+
+ registerMmTelCapabilityCallback();
}
void teardown() {
mIncomingCallNotifier.teardown();
mPhoneCapabilitiesNotifier.teardown();
- if (mMmtelCapabilityCallback != null) {
- try {
- ImsMmTelManager.createForSubscriptionId(mContext, getSubId())
- .unregisterMmTelCapabilityCallback(mMmtelCapabilityCallback);
- } catch (IllegalArgumentException e) {
- // TODO (breadley): Tearing down may fail if the sim has been removed.
- }
+ if (mMmTelManager != null && mMmtelCapabilityCallback != null) {
+ mMmTelManager.unregisterMmTelCapabilityCallback(mMmtelCapabilityCallback);
+ }
+ }
+
+ private void registerMmTelCapabilityCallback() {
+ if (mMmTelManager == null || mMmtelCapabilityCallback == null) {
+ // The subscription id associated with this account is invalid or not associated
+ // with a subscription. Do not register in this case.
+ return;
+ }
+
+ try {
+ mMmTelManager.registerMmTelCapabilityCallback(mContext.getMainExecutor(),
+ mMmtelCapabilityCallback);
+ } catch (ImsException e) {
+ Log.w(this, "registerMmTelCapabilityCallback: registration failed, no ImsService"
+ + " available. Exception: " + e.getMessage());
+ return;
+ } catch (IllegalArgumentException e) {
+ Log.w(this, "registerMmTelCapabilityCallback: registration failed, invalid"
+ + " subscription, Exception" + e.getMessage());
+ return;
}
}
@@ -258,7 +267,15 @@
capabilities |= PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS;
}
- mIsVideoCapable = mPhone.isVideoEnabled();
+ if (PhoneGlobals.getInstance().phoneMgr.isRttEnabled(subId)
+ && isImsVoiceAvailable()) {
+ capabilities |= PhoneAccount.CAPABILITY_RTT;
+ mIsRttCapable = true;
+ } else {
+ mIsRttCapable = false;
+ }
+
+ mIsVideoCapable = mPhone.isVideoEnabled() && !mIsRttCapable;
boolean isVideoEnabledByPlatform = ImsManager.getInstance(mPhone.getContext(),
mPhone.getPhoneId()).isVtEnabledByPlatform();
@@ -308,12 +325,6 @@
extras.putBoolean(PhoneAccount.EXTRA_PLAY_CALL_RECORDING_TONE, true);
}
- if (PhoneGlobals.getInstance().phoneMgr.isRttEnabled(subId)
- && isImsVoiceAvailable()) {
- capabilities |= PhoneAccount.CAPABILITY_RTT;
- mIsRttCapable = true;
- }
-
extras.putBoolean(PhoneAccount.EXTRA_SUPPORTS_VIDEO_CALLING_FALLBACK,
mContext.getResources()
.getBoolean(R.bool.config_support_video_calling_fallback));
@@ -575,21 +586,14 @@
}
public void updateRttCapability() {
- // In the rare case that mMmTelCapabilities hasn't been set yet, try fetching it
- // directly.
- boolean hasVoiceAvailability;
- if (mMmTelCapabilities != null) {
- hasVoiceAvailability = mMmTelCapabilities.isCapable(
- MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
- } else {
- hasVoiceAvailability = isImsVoiceAvailable();
- }
+ boolean hasVoiceAvailability = isImsVoiceAvailable();
boolean isRttSupported = PhoneGlobals.getInstance().phoneMgr
.isRttEnabled(mPhone.getSubId());
boolean isRttEnabled = hasVoiceAvailability && isRttSupported;
if (isRttEnabled != mIsRttCapable) {
+ Log.i(this, "updateRttCapability - changed, new value: " + isRttEnabled);
mAccount = registerPstnPhoneAccount(mIsEmergency, mIsDummy);
}
}
@@ -659,11 +663,17 @@
MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
}
- ImsMmTelManager manager = ImsMmTelManager.createForSubscriptionId(
- mContext, getSubId());
- return manager.isAvailable(ImsRegistrationImplBase.REGISTRATION_TECH_LTE,
+ if (mMmTelManager == null) {
+ // The Subscription is invalid, so IMS is unavailable.
+ return false;
+ }
+
+ // In the rare case that mMmTelCapabilities hasn't been set, try fetching it
+ // directly and register callback.
+ registerMmTelCapabilityCallback();
+ return mMmTelManager.isAvailable(ImsRegistrationImplBase.REGISTRATION_TECH_LTE,
MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE)
- || manager.isAvailable(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN,
+ || mMmTelManager.isAvailable(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN,
MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
}
}
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index d2ba8f1..85f0d48 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -46,6 +46,7 @@
import com.android.internal.telephony.Call;
import com.android.internal.telephony.CallFailCause;
import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.Call.HoldingRequestState;
import com.android.internal.telephony.Connection.Capability;
import com.android.internal.telephony.Connection.PostDialListener;
import com.android.internal.telephony.Phone;
@@ -952,6 +953,7 @@
// instead of actually putting it on hold.
if (ringingCall.getState() != Call.State.WAITING) {
phone.switchHoldingAndActive();
+ mOriginalConnection.getCall().updateHoldingRequestState(HoldingRequestState.STARTED);
}
// TODO: Cdma calls are slightly different.
@@ -2107,8 +2109,7 @@
setStatusHints(new StatusHints(
context.getString(labelId),
Icon.createWithResource(
- context.getResources(),
- R.drawable.ic_signal_wifi_4_bar_24dp),
+ context, R.drawable.ic_signal_wifi_4_bar_24dp),
null /* extras */));
} else {
setStatusHints(null);
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 8ae7b8a..6d7c1f0 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -40,6 +40,7 @@
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.telephony.emergency.EmergencyNumber;
import android.text.TextUtils;
import android.util.Pair;
@@ -52,6 +53,7 @@
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.RIL;
import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
import com.android.internal.telephony.imsphone.ImsPhone;
import com.android.internal.telephony.imsphone.ImsPhoneConnection;
@@ -66,9 +68,12 @@
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Queue;
import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
/**
* Service for making GSM and CDMA connections.
*/
@@ -144,7 +149,10 @@
new CdmaConferenceController(this);
private final ImsConferenceController mImsConferenceController =
new ImsConferenceController(TelecomAccountRegistry.getInstance(this),
- mTelephonyConnectionServiceProxy);
+ mTelephonyConnectionServiceProxy,
+ // FeatureFlagProxy; used to determine if standalone call emulation is enabled.
+ // TODO: Move to carrier config
+ () -> true);
private ComponentName mExpectedComponentName = null;
private RadioOnHelper mRadioOnHelper;
@@ -201,26 +209,47 @@
};
// TelephonyManager Proxy interface for testing
+ @VisibleForTesting
public interface TelephonyManagerProxy {
int getPhoneCount();
boolean hasIccCard(int slotId);
+ boolean isCurrentEmergencyNumber(String number);
+ Map<Integer, List<EmergencyNumber>> getCurrentEmergencyNumberList();
}
- private TelephonyManagerProxy mTelephonyManagerProxy = new TelephonyManagerProxy() {
- private final TelephonyManager sTelephonyManager = TelephonyManager.getDefault();
+ private TelephonyManagerProxy mTelephonyManagerProxy;
+
+ private class TelephonyManagerProxyImpl implements TelephonyManagerProxy {
+ private final TelephonyManager mTelephonyManager;
+
+
+ TelephonyManagerProxyImpl(Context context) {
+ mTelephonyManager = new TelephonyManager(context);
+ }
@Override
public int getPhoneCount() {
- return sTelephonyManager.getPhoneCount();
+ return mTelephonyManager.getPhoneCount();
}
@Override
public boolean hasIccCard(int slotId) {
- return sTelephonyManager.hasIccCard(slotId);
+ return mTelephonyManager.hasIccCard(slotId);
}
- };
+
+ @Override
+ public boolean isCurrentEmergencyNumber(String number) {
+ return mTelephonyManager.isCurrentEmergencyNumber(number);
+ }
+
+ @Override
+ public Map<Integer, List<EmergencyNumber>> getCurrentEmergencyNumberList() {
+ return mTelephonyManager.getCurrentEmergencyNumberList();
+ }
+ }
//PhoneFactory proxy interface for testing
+ @VisibleForTesting
public interface PhoneFactoryProxy {
Phone getPhone(int index);
Phone getDefaultPhone();
@@ -279,6 +308,7 @@
public void onCreate() {
super.onCreate();
Log.initLogging(this);
+ setTelephonyManagerProxy(new TelephonyManagerProxyImpl(getApplicationContext()));
mExpectedComponentName = new ComponentName(this, this.getClass());
mEmergencyTonePlayer = new EmergencyTonePlayer(this);
TelecomAccountRegistry.getInstance(this).setTelephonyConnectionService(this);
@@ -316,7 +346,8 @@
if (PhoneAccount.SCHEME_VOICEMAIL.equals(scheme)) {
// TODO: We don't check for SecurityException here (requires
// CALL_PRIVILEGED permission).
- final Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
+ final Phone phone = getPhoneForAccount(request.getAccountHandle(),
+ false /* isEmergencyCall */, null /* not an emergency call */);
if (phone == null) {
Log.d(this, "onCreateOutgoingConnection, phone is null");
return Connection.createFailedConnection(
@@ -354,7 +385,8 @@
"Unable to parse number"));
}
- final Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
+ final Phone phone = getPhoneForAccount(request.getAccountHandle(),
+ false /* isEmergencyCall*/, null /* not an emergency call */);
if (phone != null && CDMA_ACTIVATION_CODE_REGEX_PATTERN.matcher(number).matches()) {
// Obtain the configuration for the outgoing phone's SIM. If the outgoing number
// matches the *228 regex pattern, fail the call. This number is used for OTASP, and
@@ -378,10 +410,15 @@
}
}
+ final boolean isEmergencyNumber = mTelephonyManagerProxy.isCurrentEmergencyNumber(number);
+ // Find out if this is a test emergency number
+ final boolean isTestEmergencyNumber = isEmergencyNumberTestNumber(number);
+
// Convert into emergency number if necessary
// This is required in some regions (e.g. Taiwan).
- if (!PhoneNumberUtils.isLocalEmergencyNumber(this, number)) {
- final Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
+ if (isEmergencyNumber) {
+ final Phone phone = getPhoneForAccount(request.getAccountHandle(), false,
+ handle.getSchemeSpecificPart());
// We only do the conversion if the phone is not in service. The un-converted
// emergency numbers will go to the correct destination when the phone is in-service,
// so they will only need the special emergency call setup when the phone is out of
@@ -398,9 +435,6 @@
}
final String numberToDial = number;
- final boolean isEmergencyNumber =
- PhoneNumberUtils.isLocalEmergencyNumber(this, numberToDial);
-
final boolean isAirplaneModeOn = Settings.Global.getInt(getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 0) > 0;
@@ -427,7 +461,12 @@
@Override
public boolean isOkToCall(Phone phone, int serviceState) {
- if (isEmergencyNumber) {
+ // HAL 1.4 introduced a new variant of dial for emergency calls, which includes
+ // an isTesting parameter. For HAL 1.4+, do not wait for IN_SERVICE, this will
+ // be handled at the RIL/vendor level by emergencyDial(...).
+ boolean waitForInServiceToDialEmergency = isTestEmergencyNumber
+ && phone.getHalVersion().less(RIL.RADIO_HAL_VERSION_1_4);
+ if (isEmergencyNumber && !waitForInServiceToDialEmergency) {
// We currently only look to make sure that the radio is on before dialing.
// We should be able to make emergency calls at any time after the radio has
// been powered on and isn't in the UNAVAILABLE state, even if it is
@@ -435,9 +474,10 @@
return (phone.getState() == PhoneConstants.State.OFFHOOK)
|| phone.getServiceState().getState() != ServiceState.STATE_POWER_OFF;
} else {
- // It is not an emergency number, so wait until we are in service and ready
- // to make calls. This can happen when we power down the radio on bluetooth
- // to save power on watches.
+ // Wait until we are in service and ready to make calls. This can happen
+ // when we power down the radio on bluetooth to save power on watches or if
+ // it is a test emergency number and we have to wait for the device to move
+ // IN_SERVICE before the call can take place over normal routing.
return (phone.getState() == PhoneConstants.State.OFFHOOK)
|| serviceState == ServiceState.STATE_IN_SERVICE;
}
@@ -459,7 +499,9 @@
}
// Get the right phone object from the account data passed in.
- final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber);
+ final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber,
+ /* Note: when not an emergency, handle can be null for unknown callers */
+ handle == null ? null : handle.getSchemeSpecificPart());
Connection resultConnection = getTelephonyConnection(request, numberToDial,
isEmergencyNumber, handle, phone);
// If there was a failure, the resulting connection will not be a TelephonyConnection,
@@ -475,6 +517,24 @@
}
}
+ private boolean isEmergencyNumberTestNumber(String number) {
+ Map<Integer, List<EmergencyNumber>> list =
+ mTelephonyManagerProxy.getCurrentEmergencyNumberList();
+ // Do not worry about which subscription the test emergency call is on yet, only detect that
+ // it is an emergency.
+ for (Integer sub : list.keySet()) {
+ for (EmergencyNumber eNumber : list.get(sub)) {
+ if (number.equals(eNumber.getNumber())
+ && eNumber.isFromSources(EmergencyNumber.EMERGENCY_NUMBER_SOURCE_TEST)) {
+ Log.i(this, "isEmergencyNumberTestNumber: " + number + " has been detected as "
+ + "a test emergency number.,");
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Whether the cellular radio is power off because the device is on Bluetooth.
*/
@@ -503,8 +563,9 @@
if (isRadioReady) {
// Get the right phone object since the radio has been turned on
// successfully.
- final Phone phone = getPhoneForAccount(request.getAccountHandle(),
- isEmergencyNumber);
+ final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber,
+ /* Note: when not an emergency, handle can be null for unknown callers */
+ handle == null ? null : handle.getSchemeSpecificPart());
// If the PhoneType of the Phone being used is different than the Default Phone, then we
// need create a new Connection using that PhoneType and replace it in Telecom.
if (phone.getPhoneType() != originalPhoneType) {
@@ -722,7 +783,9 @@
"Treat as an Emergency Call.");
isEmergency = true;
}
- Phone phone = getPhoneForAccount(accountHandle, isEmergency);
+ Phone phone = getPhoneForAccount(accountHandle, isEmergency,
+ /* Note: when not an emergency, handle can be null for unknown callers */
+ request.getAddress() == null ? null : request.getAddress().getSchemeSpecificPart());
if (phone == null) {
return Connection.createFailedConnection(
DisconnectCauseUtil.toTelecomDisconnectCause(
@@ -833,7 +896,9 @@
"Treat as an Emergency Call.");
isEmergency = true;
}
- Phone phone = getPhoneForAccount(accountHandle, isEmergency);
+ Phone phone = getPhoneForAccount(accountHandle, isEmergency,
+ /* Note: when not an emergency, handle can be null for unknown callers */
+ request.getAddress() == null ? null : request.getAddress().getSchemeSpecificPart());
if (phone == null) {
return Connection.createFailedConnection(
DisconnectCauseUtil.toTelecomDisconnectCause(
@@ -1223,7 +1288,17 @@
return false;
}
- private Phone getPhoneForAccount(PhoneAccountHandle accountHandle, boolean isEmergency) {
+ /**
+ * Determines which {@link Phone} will be used to place the call.
+ * @param accountHandle The {@link PhoneAccountHandle} which was sent from Telecom to place the
+ * call on.
+ * @param isEmergency {@code true} if this is an emergency call, {@code false} otherwise.
+ * @param emergencyNumberAddress When {@code isEmergency} is {@code true}, will be the phone
+ * of the emergency call. Otherwise, this can be {@code null} .
+ * @return
+ */
+ private Phone getPhoneForAccount(PhoneAccountHandle accountHandle, boolean isEmergency,
+ @Nullable String emergencyNumberAddress) {
Phone chosenPhone = null;
int subId = PhoneUtils.getSubIdForPhoneAccountHandle(accountHandle);
if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
@@ -1237,7 +1312,7 @@
.getServiceState().getState())) {
Log.d(this, "getPhoneForAccount: phone for phone acct handle %s is out of service "
+ "or invalid for emergency call.", accountHandle);
- chosenPhone = getFirstPhoneForEmergencyCall();
+ chosenPhone = getPhoneForEmergencyCall(emergencyNumberAddress);
Log.d(this, "getPhoneForAccount: using subId: " +
(chosenPhone == null ? "null" : chosenPhone.getSubId()));
}
@@ -1245,6 +1320,48 @@
}
/**
+ * Get the Phone to use for an emergency call of the given emergency number address:
+ * a) If there are multiple Phones with the Subscriptions that support the emergency number
+ * address, and one of them is the default voice Phone, consider the default voice phone
+ * if 1.4 HAL is supported, or if it is available for emergency call.
+ * b) If there are multiple Phones with the Subscriptions that support the emergency number
+ * address, and none of them is the default voice Phone, use one of these Phones if 1.4 HAL
+ * is supported, or if it is available for emergency call.
+ * c) If there is no Phone that supports the emergency call for the address, use the defined
+ * Priority list to select the Phone via {@link #getFirstPhoneForEmergencyCall}.
+ */
+ public Phone getPhoneForEmergencyCall(String emergencyNumberAddress) {
+ // Find the list of available Phones for the given emergency number address
+ List<Phone> potentialEmergencyPhones = new ArrayList<>();
+ int defaultVoicePhoneId = mSubscriptionManagerProxy.getDefaultVoicePhoneId();
+ for (Phone phone : mPhoneFactoryProxy.getPhones()) {
+ if (phone.getEmergencyNumberTracker() != null) {
+ if (phone.getEmergencyNumberTracker().isEmergencyNumber(
+ emergencyNumberAddress, true)) {
+ if (phone.getHalVersion().greaterOrEqual(RIL.RADIO_HAL_VERSION_1_4)
+ || isAvailableForEmergencyCalls(phone)) {
+ // a)
+ if (phone.getPhoneId() == defaultVoicePhoneId) {
+ Log.i(this, "getPhoneForEmergencyCall, Phone Id that supports"
+ + " emergency number: " + phone.getPhoneId());
+ return phone;
+ }
+ potentialEmergencyPhones.add(phone);
+ }
+ }
+ }
+ }
+ // b)
+ if (potentialEmergencyPhones.size() > 0) {
+ Log.i(this, "getPhoneForEmergencyCall, Phone Id that supports emergency number:"
+ + potentialEmergencyPhones.get(0).getPhoneId());
+ return potentialEmergencyPhones.get(0);
+ }
+ // c)
+ return getFirstPhoneForEmergencyCall();
+ }
+
+ /**
* Retrieves the most sensible Phone to use for an emergency call using the following Priority
* list (for multi-SIM devices):
* 1) The User's SIM preference for Voice calling
diff --git a/testapps/ImsTestService/AndroidManifest.xml b/testapps/ImsTestService/AndroidManifest.xml
index 7662a42..eea54b8 100644
--- a/testapps/ImsTestService/AndroidManifest.xml
+++ b/testapps/ImsTestService/AndroidManifest.xml
@@ -19,6 +19,9 @@
coreApp="true"
package="com.android.phone.testapps.imstestapp">
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <!--Beware, declaring the below permission will cause the device to not boot unless you add
+ this app and permission to frameworks/base/data/etc/privapp-permissions-platform.xml-->
+ <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
<application
android:label="ImsTestService"
android:directBootAware="true">
diff --git a/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsCallingActivity.java b/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsCallingActivity.java
index fea2bf8..0ff6cc1 100644
--- a/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsCallingActivity.java
+++ b/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsCallingActivity.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.os.Bundle;
import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
import android.telephony.ims.ImsMmTelManager;
import android.telephony.ims.feature.MmTelFeature;
import android.telephony.ims.stub.ImsRegistrationImplBase;
@@ -162,12 +163,12 @@
mListView = (ListView) findViewById(R.id.cap_cb_list);
mListView.setAdapter(mCapabiltyEventAdapter);
try {
- mImsManager = ImsMmTelManager.createForSubscriptionId(this,
+ mImsManager = ImsMmTelManager.createForSubscriptionId(
SubscriptionManager.getDefaultVoiceSubscriptionId());
Log.i("ImsCallingActivity", "onResume");
mImsManager.registerMmTelCapabilityCallback(getMainExecutor(), mCapabilityCallback);
- } catch (IllegalArgumentException e) {
- Log.w("ImsCallingActivity", "illegal subscription ID.");
+ } catch (IllegalArgumentException | ImsException e) {
+ Log.w("ImsCallingActivity", "Exception: " + e.getMessage());
}
}
diff --git a/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsRegistrationActivity.java b/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsRegistrationActivity.java
index 3317ff1..84ec7b9 100644
--- a/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsRegistrationActivity.java
+++ b/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsRegistrationActivity.java
@@ -19,7 +19,9 @@
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
+import android.telephony.AccessNetworkConstants;
import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsException;
import android.telephony.ims.ImsMmTelManager;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.stub.ImsRegistrationImplBase;
@@ -149,8 +151,8 @@
private static final Map<Integer, String> REG_TECH_STRING = new ArrayMap<>(2);
static {
REG_TECH_STRING.put(ImsRegistrationImplBase.REGISTRATION_TECH_NONE, "NONE");
- REG_TECH_STRING.put(ImsRegistrationImplBase.REGISTRATION_TECH_LTE, "LTE");
- REG_TECH_STRING.put(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, "IWLAN");
+ REG_TECH_STRING.put(AccessNetworkConstants.TransportType.WWAN, "WWAN");
+ REG_TECH_STRING.put(AccessNetworkConstants.TransportType.WLAN, "WLAN");
}
@@ -177,10 +179,10 @@
mListView = (ListView) findViewById(R.id.reg_cb_list);
mListView.setAdapter(mRegItemAdapter);
try {
- mImsManager = ImsMmTelManager.createForSubscriptionId(this,
+ mImsManager = ImsMmTelManager.createForSubscriptionId(
SubscriptionManager.getDefaultVoiceSubscriptionId());
mImsManager.registerImsRegistrationCallback(getMainExecutor(), mRegistrationCallback);
- } catch (IllegalArgumentException e) {
+ } catch (IllegalArgumentException | ImsException e) {
Log.w("ImsCallingActivity", "illegal subscription ID.");
}
diff --git a/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml b/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml
index 5145a63..6dd8bc2 100644
--- a/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml
+++ b/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml
@@ -73,7 +73,6 @@
android:layout_height="50dip">
</Button>
-
<ScrollView
android:id="@+id/return_value_wrapper"
android:layout_width="fill_parent"
diff --git a/testapps/TelephonyRegistryTestApp/AndroidManifest.xml b/testapps/TelephonyRegistryTestApp/AndroidManifest.xml
index 708ea66..550c9f0 100644
--- a/testapps/TelephonyRegistryTestApp/AndroidManifest.xml
+++ b/testapps/TelephonyRegistryTestApp/AndroidManifest.xml
@@ -16,9 +16,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.phone.testapps.telephonyregistry">
+ <uses-sdk android:minSdkVersion="25"
+ android:targetSdkVersion="25"/>
+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.READ_PRECISE_PHONE_STATE"/>
<application android:label="TelephonyRegistryTestApp">
<activity
android:name=".TelephonyRegistryTestApp"
diff --git a/testapps/TelephonyRegistryTestApp/src/com/android/phone/testapps/telephonyregistry/TelephonyRegistryTestApp.java b/testapps/TelephonyRegistryTestApp/src/com/android/phone/testapps/telephonyregistry/TelephonyRegistryTestApp.java
index 74cafcd..96f8bf7 100644
--- a/testapps/TelephonyRegistryTestApp/src/com/android/phone/testapps/telephonyregistry/TelephonyRegistryTestApp.java
+++ b/testapps/TelephonyRegistryTestApp/src/com/android/phone/testapps/telephonyregistry/TelephonyRegistryTestApp.java
@@ -24,6 +24,7 @@
import android.telephony.CellInfo;
import android.telephony.CellLocation;
import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.util.SparseArray;
import android.widget.Button;
@@ -76,6 +77,11 @@
notify("onSrvccStateChanged", srvccState);
}
+ @Override
+ public void onServiceStateChanged(ServiceState state) {
+ notify("onServiceStateChanged", state);
+ }
+
private void notify(String method, Object data) {
Notification.Builder builder = new Notification.Builder(TelephonyRegistryTestApp.this,
NOTIFICATION_CHANNEL);
diff --git a/tests/src/com/android/phone/LocationAccessPolicyTest.java b/tests/src/com/android/phone/LocationAccessPolicyTest.java
new file mode 100644
index 0000000..9938bf2
--- /dev/null
+++ b/tests/src/com/android/phone/LocationAccessPolicyTest.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2018 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 org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.os.Build;
+import android.os.UserHandle;
+import android.telephony.LocationAccessPolicy;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public class LocationAccessPolicyTest {
+ private static class Scenario {
+ static class Builder {
+ private int mAppSdkLevel;
+ private boolean mAppHasFineManifest = false;
+ private boolean mAppHasCoarseManifest = false;
+ private int mFineAppOp = AppOpsManager.MODE_IGNORED;
+ private int mCoarseAppOp = AppOpsManager.MODE_IGNORED;
+ private boolean mIsDynamicLocationEnabled;
+ private LocationAccessPolicy.LocationPermissionQuery mQuery;
+ private LocationAccessPolicy.LocationPermissionResult mExpectedResult;
+ private String mName;
+
+ public Builder setAppSdkLevel(int appSdkLevel) {
+ mAppSdkLevel = appSdkLevel;
+ return this;
+ }
+
+ public Builder setAppHasFineManifest(boolean appHasFineManifest) {
+ mAppHasFineManifest = appHasFineManifest;
+ return this;
+ }
+
+ public Builder setAppHasCoarseManifest(
+ boolean appHasCoarseManifest) {
+ mAppHasCoarseManifest = appHasCoarseManifest;
+ return this;
+ }
+
+ public Builder setFineAppOp(int fineAppOp) {
+ mFineAppOp = fineAppOp;
+ return this;
+ }
+
+ public Builder setCoarseAppOp(int coarseAppOp) {
+ mCoarseAppOp = coarseAppOp;
+ return this;
+ }
+
+ public Builder setIsDynamicLocationEnabled(
+ boolean isDynamicLocationEnabled) {
+ mIsDynamicLocationEnabled = isDynamicLocationEnabled;
+ return this;
+ }
+
+ public Builder setQuery(
+ LocationAccessPolicy.LocationPermissionQuery query) {
+ mQuery = query;
+ return this;
+ }
+
+ public Builder setExpectedResult(
+ LocationAccessPolicy.LocationPermissionResult expectedResult) {
+ mExpectedResult = expectedResult;
+ return this;
+ }
+
+ public Builder setName(String name) {
+ mName = name;
+ return this;
+ }
+
+ public Scenario build() {
+ return new Scenario(mAppSdkLevel, mAppHasFineManifest, mAppHasCoarseManifest,
+ mFineAppOp, mCoarseAppOp, mIsDynamicLocationEnabled, mQuery,
+ mExpectedResult, mName);
+ }
+ }
+ int appSdkLevel;
+ boolean appHasFineManifest;
+ boolean appHasCoarseManifest;
+ int fineAppOp;
+ int coarseAppOp;
+ boolean isDynamicLocationEnabled;
+ LocationAccessPolicy.LocationPermissionQuery query;
+ LocationAccessPolicy.LocationPermissionResult expectedResult;
+ String name;
+
+ private Scenario(int appSdkLevel, boolean appHasFineManifest, boolean appHasCoarseManifest,
+ int fineAppOp, int coarseAppOp,
+ boolean isDynamicLocationEnabled,
+ LocationAccessPolicy.LocationPermissionQuery query,
+ LocationAccessPolicy.LocationPermissionResult expectedResult,
+ String name) {
+ this.appSdkLevel = appSdkLevel;
+ this.appHasFineManifest = appHasFineManifest;
+ this.appHasCoarseManifest = appHasFineManifest || appHasCoarseManifest;
+ this.fineAppOp = fineAppOp;
+ this.coarseAppOp = coarseAppOp == AppOpsManager.MODE_ALLOWED ? coarseAppOp : fineAppOp;
+ this.isDynamicLocationEnabled = isDynamicLocationEnabled;
+ this.query = query;
+ this.expectedResult = expectedResult;
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ @Mock Context mContext;
+ @Mock AppOpsManager mAppOpsManager;
+ @Mock LocationManager mLocationManager;
+ @Mock PackageManager mPackageManager;
+ Scenario mScenario;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mockContextSystemService(AppOpsManager.class, mAppOpsManager);
+ mockContextSystemService(LocationManager.class, mLocationManager);
+ mockContextSystemService(PackageManager.class, mPackageManager);
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ }
+
+ private <T> void mockContextSystemService(Class<T> clazz , T obj) {
+ when(mContext.getSystemServiceName(eq(clazz))).thenReturn(clazz.getSimpleName());
+ when(mContext.getSystemService(clazz.getSimpleName())).thenReturn(obj);
+ }
+
+ public LocationAccessPolicyTest(Scenario scenario) {
+ mScenario = scenario;
+ }
+
+
+ @Test
+ public void test() {
+ setupScenario(mScenario);
+ assertEquals(mScenario.expectedResult,
+ LocationAccessPolicy.checkLocationPermission(mContext, mScenario.query));
+ }
+
+ private void setupScenario(Scenario s) {
+ when(mContext.checkPermission(eq(Manifest.permission.ACCESS_FINE_LOCATION),
+ anyInt(), anyInt())).thenReturn(s.appHasFineManifest
+ ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
+
+ when(mContext.checkPermission(eq(Manifest.permission.ACCESS_COARSE_LOCATION),
+ anyInt(), anyInt())).thenReturn(s.appHasCoarseManifest
+ ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
+
+ when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OP_FINE_LOCATION),
+ anyInt(), anyString()))
+ .thenReturn(s.fineAppOp);
+ when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OP_COARSE_LOCATION),
+ anyInt(), anyString()))
+ .thenReturn(s.coarseAppOp);
+
+ if (s.isDynamicLocationEnabled) {
+ when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class))).thenReturn(true);
+ when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL),
+ anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_GRANTED);
+ } else {
+ when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class)))
+ .thenReturn(false);
+ when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL),
+ anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_DENIED);
+ }
+
+ ApplicationInfo fakeAppInfo = new ApplicationInfo();
+ fakeAppInfo.targetSdkVersion = s.appSdkLevel;
+
+ try {
+ when(mPackageManager.getApplicationInfo(anyString(), anyInt()))
+ .thenReturn(fakeAppInfo);
+ } catch (Exception e) {
+ // this is a formality
+ }
+ }
+
+ private static LocationAccessPolicy.LocationPermissionQuery.Builder getDefaultQueryBuilder() {
+ return new LocationAccessPolicy.LocationPermissionQuery.Builder()
+ .setMethod("test")
+ .setCallingPackage("com.android.test")
+ .setCallingPid(10001)
+ .setCallingUid(10001);
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Scenario> getScenarios() {
+ List<Scenario> scenarios = new ArrayList<>();
+ scenarios.add(new Scenario.Builder()
+ .setName("System location is off")
+ .setAppHasFineManifest(true)
+ .setFineAppOp(AppOpsManager.MODE_ALLOWED)
+ .setAppSdkLevel(Build.VERSION_CODES.P)
+ .setIsDynamicLocationEnabled(false)
+ .setQuery(getDefaultQueryBuilder()
+ .setMinSdkVersionForFine(Build.VERSION_CODES.N)
+ .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
+ .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_SOFT)
+ .build());
+
+ scenarios.add(new Scenario.Builder()
+ .setName("App on latest SDK level has all proper permissions for fine")
+ .setAppHasFineManifest(true)
+ .setFineAppOp(AppOpsManager.MODE_ALLOWED)
+ .setAppSdkLevel(Build.VERSION_CODES.P)
+ .setIsDynamicLocationEnabled(true)
+ .setQuery(getDefaultQueryBuilder()
+ .setMinSdkVersionForFine(Build.VERSION_CODES.N)
+ .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
+ .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
+ .build());
+
+ scenarios.add(new Scenario.Builder()
+ .setName("App on older SDK level missing permissions for fine but has coarse")
+ .setAppHasCoarseManifest(true)
+ .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
+ .setAppSdkLevel(Build.VERSION_CODES.JELLY_BEAN)
+ .setIsDynamicLocationEnabled(true)
+ .setQuery(getDefaultQueryBuilder()
+ .setMinSdkVersionForFine(Build.VERSION_CODES.M)
+ .setMinSdkVersionForCoarse(Build.VERSION_CODES.JELLY_BEAN).build())
+ .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
+ .build());
+
+ scenarios.add(new Scenario.Builder()
+ .setName("App on latest SDK level missing fine app ops permission")
+ .setAppHasFineManifest(true)
+ .setFineAppOp(AppOpsManager.MODE_ERRORED)
+ .setAppSdkLevel(Build.VERSION_CODES.P)
+ .setIsDynamicLocationEnabled(true)
+ .setQuery(getDefaultQueryBuilder()
+ .setMinSdkVersionForFine(Build.VERSION_CODES.N)
+ .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
+ .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
+ .build());
+
+ scenarios.add(new Scenario.Builder()
+ .setName("App has coarse permission but fine permission isn't being enforced yet")
+ .setAppHasCoarseManifest(true)
+ .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
+ .setAppSdkLevel(LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1)
+ .setIsDynamicLocationEnabled(true)
+ .setQuery(getDefaultQueryBuilder()
+ .setMinSdkVersionForFine(
+ LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1)
+ .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
+ .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
+ .build());
+
+ scenarios.add(new Scenario.Builder()
+ .setName("App on latest SDK level has coarse but missing fine when fine is req.")
+ .setAppHasCoarseManifest(true)
+ .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
+ .setAppSdkLevel(Build.VERSION_CODES.P)
+ .setIsDynamicLocationEnabled(true)
+ .setQuery(getDefaultQueryBuilder()
+ .setMinSdkVersionForFine(Build.VERSION_CODES.P)
+ .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
+ .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
+ .build());
+
+ scenarios.add(new Scenario.Builder()
+ .setName("App on latest SDK level has MODE_IGNORED for app ops on fine")
+ .setAppHasCoarseManifest(true)
+ .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
+ .setFineAppOp(AppOpsManager.MODE_IGNORED)
+ .setAppSdkLevel(Build.VERSION_CODES.P)
+ .setIsDynamicLocationEnabled(true)
+ .setQuery(getDefaultQueryBuilder()
+ .setMinSdkVersionForFine(Build.VERSION_CODES.P)
+ .setMinSdkVersionForCoarse(Build.VERSION_CODES.O).build())
+ .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
+ .build());
+
+ scenarios.add(new Scenario.Builder()
+ .setName("App has no permissions but it's sdk level grandfathers it in")
+ .setAppSdkLevel(Build.VERSION_CODES.N)
+ .setIsDynamicLocationEnabled(true)
+ .setQuery(getDefaultQueryBuilder()
+ .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+ .setMinSdkVersionForCoarse(Build.VERSION_CODES.O).build())
+ .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
+ .build());
+
+ scenarios.add(new Scenario.Builder()
+ .setName("App on latest SDK level has proper permissions for coarse")
+ .setAppHasCoarseManifest(true)
+ .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
+ .setAppSdkLevel(Build.VERSION_CODES.P)
+ .setIsDynamicLocationEnabled(true)
+ .setQuery(getDefaultQueryBuilder()
+ .setMinSdkVersionForCoarse(Build.VERSION_CODES.P).build())
+ .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
+ .build());
+ return scenarios;
+ }
+}
diff --git a/tests/src/com/android/phone/ecc/EccDataTest.java b/tests/src/com/android/phone/ecc/EccDataTest.java
new file mode 100644
index 0000000..8f4abc5
--- /dev/null
+++ b/tests/src/com/android/phone/ecc/EccDataTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 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.ecc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.phone.ecc.nano.ProtobufEccData;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Unit tests for eccdata.
+ */
+@RunWith(AndroidJUnit4.class)
+public class EccDataTest extends TelephonyTestBase {
+ @Test
+ public void testEccDataContent() throws IOException {
+ InputStream eccData = new GZIPInputStream(new BufferedInputStream(
+ InstrumentationRegistry.getTargetContext().getAssets().open("eccdata")));
+ ProtobufEccData.AllInfo allEccMessages = ProtobufEccData.AllInfo.parseFrom(
+ readInputStreamToByteArray(eccData));
+ eccData.close();
+
+ HashSet loadedIsos = new HashSet(300);
+ HashSet loadedNumbers = new HashSet(5);
+
+ for (ProtobufEccData.CountryInfo countryInfo : allEccMessages.countries) {
+ assertThat(countryInfo.isoCode).isNotEmpty();
+ assertThat(countryInfo.isoCode).isEqualTo(countryInfo.isoCode.toUpperCase().trim());
+ assertThat(loadedIsos.contains(countryInfo.isoCode)).isFalse();
+ loadedIsos.add(countryInfo.isoCode);
+
+ loadedNumbers.clear();
+ for (ProtobufEccData.EccInfo eccInfo : countryInfo.eccs) {
+ assertThat(eccInfo.phoneNumber).isNotEmpty();
+ assertThat(eccInfo.phoneNumber).isEqualTo(eccInfo.phoneNumber.trim());
+ assertThat(loadedNumbers.contains(eccInfo.phoneNumber)).isFalse();
+ assertThat(eccInfo.types).isNotEmpty();
+ loadedNumbers.add(eccInfo.phoneNumber);
+ }
+ }
+ }
+
+ /**
+ * Util function to convert inputStream to byte array before parsing proto data.
+ */
+ private static byte[] readInputStreamToByteArray(InputStream inputStream) throws IOException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ int nRead;
+ byte[] data = new byte[16 * 1024]; // Read 16k chunks
+ while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+ buffer.flush();
+ return buffer.toByteArray();
+ }
+}
diff --git a/tests/src/com/android/phone/ecc/IsoToEccProtobufRepositoryTest.java b/tests/src/com/android/phone/ecc/IsoToEccProtobufRepositoryTest.java
deleted file mode 100644
index f6e5ba2..0000000
--- a/tests/src/com/android/phone/ecc/IsoToEccProtobufRepositoryTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2018 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.ecc;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-import android.util.Log;
-
-import com.android.TelephonyTestBase;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.HashSet;
-import java.util.Map;
-
-/**
- * Unit tests for IsoToEccProtobufRepository.
- */
-
-@RunWith(AndroidJUnit4.class)
-public class IsoToEccProtobufRepositoryTest extends TelephonyTestBase {
- private static final String LOG_TAG = "IsoToEccProtobufRepositoryTest";
-
- @Test
- public void testEccDataContent() {
- IsoToEccProtobufRepository repository = IsoToEccProtobufRepository.getInstance();
- repository.loadMappingTable(InstrumentationRegistry.getTargetContext());
- Map<String, CountryEccInfo> eccTable = repository.getEccTable();
- HashSet loadedIsos = new HashSet(300);
- HashSet loadedNumbers = new HashSet(5);
-
- assertThat(eccTable).isNotEmpty();
- for (Map.Entry<String, CountryEccInfo> entry : eccTable.entrySet()) {
- String countryIso = entry.getKey();
- CountryEccInfo countryEccInfo = entry.getValue();
- EccInfo[] eccInfoList = countryEccInfo.getEccInfoList();
- if (eccInfoList.length > 0) {
- Log.i(LOG_TAG, "Verifying country " + countryIso + " with "
- + eccInfoList.length + " ecc(s)");
- } else {
- Log.w(LOG_TAG, "Verifying country " + countryIso + " with no ecc");
- }
-
- assertThat(countryIso).isNotEmpty();
- assertThat(countryIso).isEqualTo(countryIso.toUpperCase().trim());
- assertThat(loadedIsos.contains(countryIso)).isFalse();
- loadedIsos.add(countryIso);
-
- assertThat(countryEccInfo.getFallbackEcc()).isNotEmpty();
-
- if (eccInfoList.length != 0) {
- loadedNumbers.clear();
- for (EccInfo eccInfo : eccInfoList) {
- String eccNumber = eccInfo.getNumber();
- assertThat(eccNumber).isNotEmpty();
- assertThat(eccNumber).isEqualTo(eccNumber.trim());
- assertThat(eccInfo.getTypes()).isNotEmpty();
- assertThat(loadedNumbers.contains(eccNumber)).isFalse();
- loadedNumbers.add(eccNumber);
- }
- }
- }
- }
-}
diff --git a/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java b/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java
index 229bdee..aa832aa 100644
--- a/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java
+++ b/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java
@@ -47,6 +47,9 @@
private TelecomAccountRegistry mTelecomAccountRegistry;
+ @Mock
+ private TelecomAccountRegistry mMockTelecomAccountRegistry;
+
private TestTelephonyConnection mTestTelephonyConnectionA;
private TestTelephonyConnection mTestTelephonyConnectionB;
@@ -63,7 +66,7 @@
mTestTelephonyConnectionB = new TestTelephonyConnection();
mControllerTest = new ImsConferenceController(mTelecomAccountRegistry,
- mMockTelephonyConnectionServiceProxy);
+ mMockTelephonyConnectionServiceProxy, () -> false);
}
/**
@@ -78,7 +81,6 @@
@Test
@SmallTest
public void testConferenceable() {
-
mControllerTest.add(mTestTelephonyConnectionB);
mControllerTest.add(mTestTelephonyConnectionA);
@@ -112,7 +114,6 @@
@Test
@SmallTest
public void testMergeMultiPartyCalls() {
-
when(mTestTelephonyConnectionA.mMockRadioConnection.getPhoneType())
.thenReturn(PhoneConstants.PHONE_TYPE_IMS);
when(mTestTelephonyConnectionB.mMockRadioConnection.getPhoneType())
diff --git a/tests/src/com/android/services/telephony/ImsConferenceTest.java b/tests/src/com/android/services/telephony/ImsConferenceTest.java
new file mode 100644
index 0000000..56a6240
--- /dev/null
+++ b/tests/src/com/android/services/telephony/ImsConferenceTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 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.services.telephony;
+
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.times;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.net.Uri;
+import android.os.Looper;
+import android.telecom.ConferenceParticipant;
+import android.telecom.Connection;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.internal.telephony.PhoneConstants;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.Arrays;
+
+public class ImsConferenceTest {
+ @Mock
+ private TelephonyConnectionServiceProxy mMockTelephonyConnectionServiceProxy;
+
+ @Mock
+ private TelecomAccountRegistry mMockTelecomAccountRegistry;
+
+ @Mock
+ private com.android.internal.telephony.Connection mOriginalConnection;
+
+ private TestTelephonyConnection mConferenceHost;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ mConferenceHost = new TestTelephonyConnection();
+ mConferenceHost.setManageImsConferenceCallSupported(true);
+ }
+
+ @Test
+ @SmallTest
+ public void testSinglePartyEmulation() {
+ ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+ mMockTelephonyConnectionServiceProxy, mConferenceHost,
+ null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+
+ ConferenceParticipant participant1 = new ConferenceParticipant(
+ Uri.parse("tel:6505551212"),
+ "A",
+ Uri.parse("sip:6505551212@testims.com"),
+ Connection.STATE_ACTIVE);
+ ConferenceParticipant participant2 = new ConferenceParticipant(
+ Uri.parse("tel:6505551213"),
+ "A",
+ Uri.parse("sip:6505551213@testims.com"),
+ Connection.STATE_ACTIVE);
+ imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+ Arrays.asList(participant1, participant2));
+ assertEquals(2, imsConference.getNumberOfParticipants());
+
+ // Because we're pretending its a single party, there should be no participants any more.
+ imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+ Arrays.asList(participant1));
+ assertEquals(0, imsConference.getNumberOfParticipants());
+
+ // Back to 2!
+ imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+ Arrays.asList(participant1, participant2));
+ assertEquals(2, imsConference.getNumberOfParticipants());
+ }
+
+ @Test
+ @SmallTest
+ public void testNormalConference() {
+ ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+ mMockTelephonyConnectionServiceProxy, mConferenceHost,
+ null /* phoneAccountHandle */, () -> false /* featureFlagProxy */);
+
+ ConferenceParticipant participant1 = new ConferenceParticipant(
+ Uri.parse("tel:6505551212"),
+ "A",
+ Uri.parse("sip:6505551212@testims.com"),
+ Connection.STATE_ACTIVE);
+ ConferenceParticipant participant2 = new ConferenceParticipant(
+ Uri.parse("tel:6505551213"),
+ "A",
+ Uri.parse("sip:6505551213@testims.com"),
+ Connection.STATE_ACTIVE);
+ imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+ Arrays.asList(participant1, participant2));
+ assertEquals(2, imsConference.getNumberOfParticipants());
+
+ // Not emulating single party; should still be one.
+ imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+ Arrays.asList(participant1));
+ assertEquals(1, imsConference.getNumberOfParticipants());
+ }
+}
diff --git a/tests/src/com/android/services/telephony/RadioOnStateListenerTest.java b/tests/src/com/android/services/telephony/RadioOnStateListenerTest.java
index fb214cc..d9de2e8 100644
--- a/tests/src/com/android/services/telephony/RadioOnStateListenerTest.java
+++ b/tests/src/com/android/services/telephony/RadioOnStateListenerTest.java
@@ -16,17 +16,25 @@
package com.android.services.telephony;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import android.os.AsyncResult;
import android.os.Handler;
-import android.telephony.ServiceState;
import android.support.test.runner.AndroidJUnit4;
-import android.support.test.filters.FlakyTest;
+import android.telephony.ServiceState;
import android.test.suitebuilder.annotation.SmallTest;
import com.android.TelephonyTestBase;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
-import com.android.internal.telephony.ServiceStateTracker;
import org.junit.After;
import org.junit.Before;
@@ -34,16 +42,6 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.when;
-
/**
* Tests the RadioOnStateListener, which listens to one Phone and waits until its service
* state changes to accepting emergency calls or in service. If it can not find a tower to camp onto
@@ -52,21 +50,26 @@
@RunWith(AndroidJUnit4.class)
public class RadioOnStateListenerTest extends TelephonyTestBase {
- private static final long TIMEOUT_MS = 100;
+ private static final long TIMEOUT_MS = 1000;
@Mock Phone mMockPhone;
@Mock RadioOnStateListener.Callback mCallback;
RadioOnStateListener mListener;
+ @Override
@Before
public void setUp() throws Exception {
super.setUp();
mListener = new RadioOnStateListener();
}
+ @Override
@After
public void tearDown() throws Exception {
+ // Wait for the queue to clear...
+ waitForHandlerAction(mListener.getHandler(), TIMEOUT_MS /*ms timeout*/);
mListener.getHandler().removeCallbacksAndMessages(null);
+ mListener = null;
super.tearDown();
}
@@ -86,8 +89,9 @@
}
/**
- * {@link RadioOnStateListener.Callback#isOkToCall(int)} returns true, so we are expecting
- * {@link RadioOnStateListener.Callback#onComplete(boolean)} to return true.
+ * {@link RadioOnStateListener.Callback#isOkToCall(Phone, int)} returns true, so we are
+ * expecting {@link RadioOnStateListener.Callback#onComplete(RadioOnStateListener, boolean)} to
+ * return true.
*/
@Test
@SmallTest
@@ -107,8 +111,9 @@
}
/**
- * We never receive a {@link RadioOnStateListener.Callback#onComplete(boolean)} because
- * {@link RadioOnStateListener.Callback#isOkToCall(int)} returns false.
+ * We never receive a
+ * {@link RadioOnStateListener.Callback#onComplete(RadioOnStateListener, boolean)} because
+ * {@link RadioOnStateListener.Callback#isOkToCall(Phone, int)} returns false.
*/
@Test
@SmallTest
@@ -129,27 +134,27 @@
}
/**
- * Tests {@link RadioOnStateListener.Callback#isOkToCall(int)} returning false and hitting the
- * max number of retries. This should result in
- * {@link RadioOnStateListener.Callback#onComplete(boolean)} returning false.
+ * Tests {@link RadioOnStateListener.Callback#isOkToCall(Phone, int)} returning false and
+ * hitting the max number of retries. This should result in
+ * {@link RadioOnStateListener.Callback#onComplete(RadioOnStateListener, boolean)} returning
+ * false.
*/
@Test
- @FlakyTest
+ @SmallTest
public void testTimeout_RetryFailure() {
ServiceState state = new ServiceState();
state.setState(ServiceState.STATE_POWER_OFF);
when(mMockPhone.getState()).thenReturn(PhoneConstants.State.IDLE);
when(mMockPhone.getServiceState()).thenReturn(state);
when(mCallback.isOkToCall(eq(mMockPhone), anyInt())).thenReturn(false);
- mListener.setTimeBetweenRetriesMillis(50);
+ mListener.setTimeBetweenRetriesMillis(0/*ms*/);
mListener.setMaxNumRetries(2);
// Wait for the timer to expire and check state manually in onRetryTimeout
mListener.waitForRadioOn(mMockPhone, mCallback);
- waitForHandlerActionDelayed(mListener.getHandler(), TIMEOUT_MS, 500);
+ waitForHandlerActionDelayed(mListener.getHandler(), TIMEOUT_MS, TIMEOUT_MS /*delay*/);
verify(mCallback).onComplete(eq(mListener), eq(false));
verify(mMockPhone, times(2)).setRadioPower(eq(true));
}
-
}
diff --git a/tests/src/com/android/services/telephony/TestTelephonyConnection.java b/tests/src/com/android/services/telephony/TestTelephonyConnection.java
index 39e4579..c064ef6 100644
--- a/tests/src/com/android/services/telephony/TestTelephonyConnection.java
+++ b/tests/src/com/android/services/telephony/TestTelephonyConnection.java
@@ -17,10 +17,12 @@
package com.android.services.telephony;
import android.content.Context;
+import android.content.res.Resources;
import android.os.Bundle;
import android.telecom.PhoneAccountHandle;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -28,6 +30,7 @@
import com.android.internal.telephony.Call;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -50,6 +53,9 @@
@Mock
Context mMockContext;
+ @Mock
+ Resources mMockResources;
+
private Phone mMockPhone;
private int mNotifyPhoneAccountChangedCount = 0;
private List<String> mLastConnectionEvents = new ArrayList<>();
@@ -66,14 +72,18 @@
mMockPhone = mock(Phone.class);
mMockContext = mock(Context.class);
+ mOriginalConnection = mock(Connection.class);
// Set up mMockRadioConnection and mMockPhone to contain an active call
when(mMockRadioConnection.getState()).thenReturn(Call.State.ACTIVE);
when(mMockRadioConnection.getCall()).thenReturn(mMockCall);
+ when(mMockRadioConnection.getPhoneType()).thenReturn(PhoneConstants.PHONE_TYPE_IMS);
doNothing().when(mMockRadioConnection).addListener(any(Connection.Listener.class));
doNothing().when(mMockRadioConnection).addPostDialListener(
any(Connection.PostDialListener.class));
when(mMockPhone.getRingingCall()).thenReturn(mMockCall);
- when(mMockPhone.getContext()).thenReturn(null);
+ when(mMockPhone.getContext()).thenReturn(mMockContext);
+ when(mMockContext.getResources()).thenReturn(mMockResources);
+ when(mMockResources.getBoolean(anyInt())).thenReturn(false);
when(mMockPhone.getDefaultPhone()).thenReturn(mMockPhone);
when(mMockCall.getState()).thenReturn(Call.State.ACTIVE);
when(mMockCall.getPhone()).thenReturn(mMockPhone);