package com.android.phone.settings; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceFragment; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; import com.android.internal.telephony.Phone; import com.android.phone.PhoneUtils; import com.android.phone.R; import com.android.phone.SubscriptionInfoHelper; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; public class PhoneAccountSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener, AccountSelectionPreference.AccountSelectionListener { private static final String ACCOUNTS_LIST_CATEGORY_KEY = "phone_accounts_accounts_list_category_key"; private static final String ALL_CALLING_ACCOUNTS_KEY = "phone_accounts_all_calling_accounts"; private static final String MAKE_AND_RECEIVE_CALLS_CATEGORY_KEY = "make_and_receive_calls_settings_category_key"; private static final String DEFAULT_OUTGOING_ACCOUNT_KEY = "default_outgoing_account"; private static final String SMART_FORWARDING_CONFIGURATION_PREF_KEY = "smart_forwarding_configuration_key"; private static final String LEGACY_ACTION_CONFIGURE_PHONE_ACCOUNT = "android.telecom.action.CONNECTION_SERVICE_CONFIGURE"; /** * Value to start ordering of phone accounts relative to other preferences. By setting this * value on the phone account listings, we ensure that anything that is ordered before * {value} in the preference XML comes before the phone account list and anything with * a value significantly larger will list after. */ private static final int ACCOUNT_ORDERING_START_VALUE = 100; private static final String LOG_TAG = PhoneAccountSettingsFragment.class.getSimpleName(); private TelecomManager mTelecomManager; private TelephonyManager mTelephonyManager; private SubscriptionManager mSubscriptionManager; private PreferenceCategory mAccountList; private AccountSelectionPreference mDefaultOutgoingAccount; private Preference mAllCallingAccounts; private PreferenceCategory mMakeAndReceiveCallsCategory; private boolean mMakeAndReceiveCallsCategoryPresent; private final SubscriptionManager.OnSubscriptionsChangedListener mOnSubscriptionsChangeListener = new SubscriptionManager.OnSubscriptionsChangedListener() { @Override public void onSubscriptionsChanged() { updateAccounts(); } }; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); mTelecomManager = getActivity().getSystemService(TelecomManager.class); mTelephonyManager = TelephonyManager.from(getActivity()); mSubscriptionManager = SubscriptionManager.from(getActivity()); } @Override public void onResume() { super.onResume(); if (getPreferenceScreen() != null) { getPreferenceScreen().removeAll(); } addPreferencesFromResource(R.xml.phone_account_settings); /** * Here we make decisions about what we will and will not display with regards to phone- * account settings. The basic settings structure is this: * (1) // Lets user pick a default account for outgoing calls * (2) * * ... * * * (3) // Lets user enable/disable third-party accounts. SIM-based accounts * // are always enabled and so aren't relevant here. * * Here are the rules that we follow: * - (1) is only shown if there are multiple enabled accounts, including SIM accounts. * This can be 2+ SIM accounts, 2+ third party accounts or any combination. * - (2) The account list only lists (a) enabled third party accounts and (b) SIM-based * accounts. However, for single-SIM devices, if the only account to show is the * SIM-based account, we don't show the list at all under the assumption that the user * already knows about the account. * - (3) Is only shown if there exist any third party accounts. If none exist, then the * option is hidden since there is nothing that can be done in it. * * By far, the most common case for users will be the single-SIM device without any * third party accounts. IOW, the great majority of users won't see any of these options. */ mAccountList = (PreferenceCategory) getPreferenceScreen().findPreference( ACCOUNTS_LIST_CATEGORY_KEY); mDefaultOutgoingAccount = (AccountSelectionPreference) getPreferenceScreen().findPreference(DEFAULT_OUTGOING_ACCOUNT_KEY); mAllCallingAccounts = getPreferenceScreen().findPreference(ALL_CALLING_ACCOUNTS_KEY); mMakeAndReceiveCallsCategory = (PreferenceCategory) getPreferenceScreen().findPreference( MAKE_AND_RECEIVE_CALLS_CATEGORY_KEY); mMakeAndReceiveCallsCategoryPresent = false; updateAccounts(); updateMakeCallsOptions(); SubscriptionManager.from(getActivity()).addOnSubscriptionsChangedListener( mOnSubscriptionsChangeListener); } @Override public void onPause() { SubscriptionManager.from(getActivity()).removeOnSubscriptionsChangedListener( mOnSubscriptionsChangeListener); super.onPause(); } /** * Handles changes to the preferences. * * @param pref The preference changed. * @param objValue The changed value. * @return True if the preference change has been handled, and false otherwise. */ @Override public boolean onPreferenceChange(Preference pref, Object objValue) { return false; } /** * Handles a phone account selection for the default outgoing phone account. * * @param pref The account selection preference which triggered the account selected event. * @param account The account selected. * @return True if the account selection has been handled, and false otherwise. */ @Override public boolean onAccountSelected(AccountSelectionPreference pref, PhoneAccountHandle account) { if (pref == mDefaultOutgoingAccount) { mTelecomManager.setUserSelectedOutgoingPhoneAccount(account); return true; } return false; } /** * Repopulate the dialog to pick up changes before showing. * * @param pref The account selection preference dialog being shown. */ @Override public void onAccountSelectionDialogShow(AccountSelectionPreference pref) { if (pref == mDefaultOutgoingAccount) { updateDefaultOutgoingAccountsModel(); } } @Override public void onAccountChanged(AccountSelectionPreference pref) {} /** * Queries the telcomm manager to update the default outgoing account selection preference * with the list of outgoing accounts and the current default outgoing account. */ private void updateDefaultOutgoingAccountsModel() { mDefaultOutgoingAccount.setModel( mTelecomManager, getCallingAccounts(true /* includeSims */, false /* includeDisabled */), mTelecomManager.getUserSelectedOutgoingPhoneAccount(), getString(R.string.phone_accounts_ask_every_time)); } private void initAccountList(List enabledAccounts) { boolean isMultiSimDevice = mTelephonyManager.isMultiSimEnabled(); // On a single-SIM device, do not list any accounts if the only account is the SIM-based // one. This is because on single-SIM devices, we do not expose SIM settings through the // account listing entry so showing it does nothing to help the user. Nor does the lack of // action match the "Settings" header above the listing. if (!isMultiSimDevice && getCallingAccounts( false /* includeSims */, false /* includeDisabled */).isEmpty()){ return; } // Obtain the list of phone accounts. List accounts = new ArrayList<>(); for (PhoneAccountHandle handle : enabledAccounts) { PhoneAccount account = mTelecomManager.getPhoneAccount(handle); if (account != null) { accounts.add(account); } } // Sort the accounts according to how we want to display them. Collections.sort(accounts, new Comparator() { @Override public int compare(PhoneAccount account1, PhoneAccount account2) { int retval = 0; // SIM accounts go first boolean isSim1 = account1.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION); boolean isSim2 = account2.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION); if (isSim1 != isSim2) { retval = isSim1 ? -1 : 1; } int subId1 = mTelephonyManager.getSubIdForPhoneAccount(account1); int subId2 = mTelephonyManager.getSubIdForPhoneAccount(account2); if (subId1 != SubscriptionManager.INVALID_SUBSCRIPTION_ID && subId2 != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { retval = (mSubscriptionManager.getSlotIndex(subId1) < mSubscriptionManager.getSlotIndex(subId2)) ? -1 : 1; } // Then order by package if (retval == 0) { String pkg1 = account1.getAccountHandle().getComponentName().getPackageName(); String pkg2 = account2.getAccountHandle().getComponentName().getPackageName(); retval = pkg1.compareTo(pkg2); } // Finally, order by label if (retval == 0) { String label1 = nullToEmpty(account1.getLabel().toString()); String label2 = nullToEmpty(account2.getLabel().toString()); retval = label1.compareTo(label2); } // Then by hashcode if (retval == 0) { retval = account1.hashCode() - account2.hashCode(); } return retval; } }); int order = ACCOUNT_ORDERING_START_VALUE; // Add an entry for each account. for (PhoneAccount account : accounts) { PhoneAccountHandle handle = account.getAccountHandle(); Intent intent = null; // SIM phone accounts use a different setting intent and are thus handled differently. if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { // For SIM-based accounts, we only expose the settings through the account list // if we are on a multi-SIM device. For single-SIM devices, the settings are // more spread out so there is no good single place to take the user, so we don't. if (isMultiSimDevice) { SubscriptionInfo subInfo = mSubscriptionManager.getActiveSubscriptionInfo( mTelephonyManager.getSubIdForPhoneAccount(account)); if (subInfo != null) { intent = new Intent(TelecomManager.ACTION_SHOW_CALL_SETTINGS); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); SubscriptionInfoHelper.addExtrasToIntent(intent, subInfo); } } } else { intent = buildPhoneAccountConfigureIntent(getActivity(), handle); } // Create the preference & add the label Preference accountPreference = new Preference(getActivity()); CharSequence accountLabel = account.getLabel(); boolean isSimAccount = account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION); accountPreference.setTitle((TextUtils.isEmpty(accountLabel) && isSimAccount) ? getString(R.string.phone_accounts_default_account_label) : accountLabel); // Add an icon. Icon icon = account.getIcon(); if (icon != null) { accountPreference.setIcon(icon.loadDrawable(getActivity())); } // Add an intent to send the user to the account's settings. if (intent != null) { accountPreference.setIntent(intent); } accountPreference.setOrder(order++); mAccountList.addPreference(accountPreference); } } private boolean shouldShowConnectionServiceList(List allNonSimAccounts) { return mTelephonyManager.isMultiSimEnabled() || allNonSimAccounts.size() > 0; } private void updateAccounts() { if (mAccountList != null) { mAccountList.removeAll(); List allNonSimAccounts = getCallingAccounts(false /* includeSims */, true /* includeDisabled */); List enabledAccounts = getCallingAccounts(true /* includeSims */, false /* includeDisabled */); // Initialize the account list with the set of enabled & SIM accounts. initAccountList(enabledAccounts); // Always show the 'Make Calls With..." option mMakeAndReceiveCallsCategory.addPreference(mDefaultOutgoingAccount); mMakeAndReceiveCallsCategoryPresent = true; mDefaultOutgoingAccount.setListener(this); updateDefaultOutgoingAccountsModel(); // 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); } } } private List getCallingAccounts( boolean includeSims, boolean includeDisabledAccounts) { PhoneAccountHandle emergencyAccountHandle = getEmergencyPhoneAccount(); List accountHandles = mTelecomManager.getCallCapablePhoneAccounts(includeDisabledAccounts); for (Iterator i = accountHandles.iterator(); i.hasNext();) { PhoneAccountHandle handle = i.next(); if (handle.equals(emergencyAccountHandle)) { // never include emergency call accounts in this piece of code. i.remove(); continue; } PhoneAccount account = mTelecomManager.getPhoneAccount(handle); if (account == null) { i.remove(); } else if (!includeSims && account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { i.remove(); } } return accountHandles; } private String nullToEmpty(String str) { return str == null ? "" : str; } private PhoneAccountHandle getEmergencyPhoneAccount() { return PhoneUtils.makePstnPhoneAccountHandleWithPrefix( (Phone) null, "" /* prefix */, true /* isEmergency */); } public static Intent buildPhoneAccountConfigureIntent( Context context, PhoneAccountHandle accountHandle) { Intent intent = buildConfigureIntent( context, accountHandle, TelecomManager.ACTION_CONFIGURE_PHONE_ACCOUNT); if (intent == null) { // If the new configuration didn't work, try the old configuration intent. intent = buildConfigureIntent( context, accountHandle, LEGACY_ACTION_CONFIGURE_PHONE_ACCOUNT); if (intent != null) { Log.w(LOG_TAG, "Phone account using old configuration intent: " + accountHandle); } } return intent; } private static Intent buildConfigureIntent( Context context, PhoneAccountHandle accountHandle, String actionStr) { if (accountHandle == null || accountHandle.getComponentName() == null || TextUtils.isEmpty(accountHandle.getComponentName().getPackageName())) { return null; } // Build the settings intent. Intent intent = new Intent(actionStr); intent.setPackage(accountHandle.getComponentName().getPackageName()); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle); // Check to see that the phone account package can handle the setting intent. PackageManager pm = context.getPackageManager(); List resolutions = pm.queryIntentActivities(intent, 0); if (resolutions.size() == 0) { intent = null; // set no intent if the package cannot handle it. } return intent; } /** * @return Whether the current user is the primary user. */ private boolean isPrimaryUser() { final UserManager userManager = (UserManager) getActivity() .getSystemService(Context.USER_SERVICE); return userManager.isPrimaryUser(); } private void updateMakeCallsOptions() { if (mMakeAndReceiveCallsCategory == null) { return; } Intent smartForwardingUiIntent = getLaunchSmartForwardingMenuIntent(); if (smartForwardingUiIntent != null) { mMakeAndReceiveCallsCategory.findPreference(SMART_FORWARDING_CONFIGURATION_PREF_KEY) .setIntent(smartForwardingUiIntent); mMakeAndReceiveCallsCategoryPresent = true; } else { mMakeAndReceiveCallsCategory.removePreference( getPreferenceScreen().findPreference(SMART_FORWARDING_CONFIGURATION_PREF_KEY)); } if (!mMakeAndReceiveCallsCategoryPresent) { getPreferenceScreen().removePreference(mMakeAndReceiveCallsCategory); } } /** * @return Smart forwarding configuration UI Intent when supported */ private Intent getLaunchSmartForwardingMenuIntent() { if (mTelephonyManager.getPhoneCount() <= 1) { return null; } final CarrierConfigManager configManager = (CarrierConfigManager) getActivity().getSystemService(Context.CARRIER_CONFIG_SERVICE); if (configManager == null) { return null; } List subscriptions = mSubscriptionManager.getActiveSubscriptionInfoList(); if (subscriptions == null) { return null; } List effectiveSubscriptions = subscriptions.stream() .filter(subInfo -> !subInfo.isOpportunistic()) .collect(Collectors.toList()); if (effectiveSubscriptions.size() < 2) { return null; } List componentNames = effectiveSubscriptions.stream() .map(subInfo -> configManager.getConfigForSubId(subInfo.getSubscriptionId())) .filter(bundle -> (bundle != null)) .map(bundle -> bundle.getString( CarrierConfigManager.KEY_SMART_FORWARDING_CONFIG_COMPONENT_NAME_STRING)) .filter(componentName -> !TextUtils.isEmpty(componentName)) .collect(Collectors.toList()); String componentNameOfMenu = null; for (String componentName : componentNames) { if (componentNameOfMenu == null) { componentNameOfMenu = componentName; } else if (!componentNameOfMenu.equals(componentName)) { Log.w(LOG_TAG, "ignore smart forward component: " + componentName); } } if (TextUtils.isEmpty(componentNameOfMenu)) { return null; } Intent intent = new Intent(Intent.ACTION_MAIN); intent.setComponent(ComponentName.unflattenFromString(componentNameOfMenu)); PackageManager pm = getActivity().getPackageManager(); List resolutions = pm.queryIntentActivities(intent, 0); if (resolutions.size() == 0) { intent = null; // set no intent if no package can handle it. } return intent; } }