Fix problem of multiple stacked copies of "Select SIM" dialog

The SimDialogActivity is used to ask the user questions about which SIM
card to use for various services like calls, SMS, and data. In some
cases of SIM changes (eg when a SIM is added or removed), the telephony
stack sends a broadcast that SimSelectNotification listens for so it can
pop up a general "SIM cards changed" notification, and we additionally
want to bring up an interruptive dialog to ask the user a specific
question. This might happen for instance when we want to ask the user's
permission to turn on data on a SIM.

Recent DSDS changes in the telephony stack have meant that we
accidentally create several stacked copies of this dialog, because they
send several broadcast updates as information about SIMs asynchronously
changes. For instance, we might initially detect a SIM with a generic
name of "CARD 1", and shortly after discover the actual carrier name. So
what we really want is to put up the dialog, and update it as
information changes.

This CL makes SimDialogActivity use launchMode="singleTop" so that
additional copies of the activity won't be launched. Then it internally
enforces only showing one dialog per type of request (calls, SMS, data,
or preferred sim). If we get a request for a dialog that already exists,
we just update it instead of creating a new one for that type. So there
can still be a stack of more than one dialog, but each one will be
asking a different question.

This also refactors the monolithic, somewhat confusing code for showing
the various types of dialogs into a more clearly separated class
hierarchy, and switches to using DialogFragment for the dialog.

Fixes: 126596081
Test: manual (start with device in DSDS mode with 2 subs, remove SIM
card and re-insert it)

Change-Id: I0dbc41dc3b15015389823a24df10bbff08ec6615
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6d3c207..a73a022 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2666,10 +2666,12 @@
             android:excludeFromRecents="true"
             android:exported="false" />
 
-        <activity android:name=".sim.SimDialogActivity"
-                android:theme="@style/Theme.AlertDialog"
-                android:label="@string/sim_settings_title"
-                android:excludeFromRecents="true">
+        <activity
+            android:name=".sim.SimDialogActivity"
+            android:theme="@style/Theme.AlertDialog"
+            android:label="@string/sim_settings_title"
+            android:launchMode="singleTop"
+            android:excludeFromRecents="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
diff --git a/src/com/android/settings/sim/CallsSimListDialogFragment.java b/src/com/android/settings/sim/CallsSimListDialogFragment.java
new file mode 100644
index 0000000..bb5a003
--- /dev/null
+++ b/src/com/android/settings/sim/CallsSimListDialogFragment.java
@@ -0,0 +1,59 @@
+/*
+ * 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.settings.sim;
+
+import android.content.Context;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Specialized version of SimListDialogFragment that fetches a list of SIMs which support calls.
+ */
+public class CallsSimListDialogFragment extends SimListDialogFragment {
+    @Override
+    protected List<SubscriptionInfo> getCurrentSubscriptions() {
+        final Context context = getContext();
+        final SubscriptionManager subscriptionManager = context.getSystemService(
+                SubscriptionManager.class);
+        final TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
+        final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+        final List<PhoneAccountHandle> phoneAccounts =
+                telecomManager.getCallCapablePhoneAccounts();
+        final List<SubscriptionInfo> result = new ArrayList<>();
+
+        if (phoneAccounts == null) {
+            return result;
+        }
+        for (PhoneAccountHandle handle : phoneAccounts) {
+            final PhoneAccount phoneAccount = telecomManager.getPhoneAccount(handle);
+            final int subId = telephonyManager.getSubIdForPhoneAccount(phoneAccount);
+
+            if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                continue;
+            }
+            result.add(subscriptionManager.getActiveSubscriptionInfo(subId));
+        }
+        return result;
+    }
+}
diff --git a/src/com/android/settings/sim/PreferredSimDialogFragment.java b/src/com/android/settings/sim/PreferredSimDialogFragment.java
new file mode 100644
index 0000000..5b81e62
--- /dev/null
+++ b/src/com/android/settings/sim/PreferredSimDialogFragment.java
@@ -0,0 +1,101 @@
+/*
+ * 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.settings.sim;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.R;
+
+/**
+ * Presents a dialog asking the user if they want to update all services to use a given "preferred"
+ * SIM. Typically this would be used in a case where a device goes from having multiple SIMs down to
+ * only one.
+ */
+public class PreferredSimDialogFragment extends SimDialogFragment implements
+        DialogInterface.OnClickListener {
+    private static final String TAG = "PreferredSimDialogFrag";
+
+    public static PreferredSimDialogFragment newInstance() {
+        final PreferredSimDialogFragment fragment = new PreferredSimDialogFragment();
+        final Bundle args = initArguments(SimDialogActivity.PREFERRED_PICK,
+                R.string.sim_preferred_title);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        final AlertDialog dialog = new AlertDialog.Builder(getContext())
+                .setTitle(getTitleResId())
+                .setPositiveButton(R.string.yes, this)
+                .setNegativeButton(R.string.no, null)
+                .create();
+        updateDialog(dialog);
+        return dialog;
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int buttonClicked) {
+        if (buttonClicked != DialogInterface.BUTTON_POSITIVE) {
+            return;
+        }
+        final SimDialogActivity activity = (SimDialogActivity) getActivity();
+        final SubscriptionInfo info = getPreferredSubscription();
+        if (info != null) {
+            activity.onSubscriptionSelected(getDialogType(), info.getSubscriptionId());
+        }
+    }
+
+    public SubscriptionInfo getPreferredSubscription() {
+        final Activity activity = getActivity();
+        final int slotId = activity.getIntent().getIntExtra(SimDialogActivity.PREFERRED_SIM,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        return getSubscriptionManager().getActiveSubscriptionInfoForSimSlotIndex(slotId);
+    }
+
+    private void updateDialog(AlertDialog dialog) {
+        final SubscriptionInfo info = getPreferredSubscription();
+        if (info == null) {
+            dismiss();
+            return;
+        }
+        final String message =
+                getContext().getString(R.string.sim_preferred_message, info.getDisplayName());
+        dialog.setMessage(message);
+    }
+
+    @Override
+    public void updateDialog() {
+        updateDialog((AlertDialog) getDialog());
+    }
+
+    @VisibleForTesting
+    protected SubscriptionManager getSubscriptionManager() {
+        return getContext().getSystemService(SubscriptionManager.class);
+    }
+}
diff --git a/src/com/android/settings/sim/SimDialogActivity.java b/src/com/android/settings/sim/SimDialogActivity.java
index 487dace..d721efd 100644
--- a/src/com/android/settings/sim/SimDialogActivity.java
+++ b/src/com/android/settings/sim/SimDialogActivity.java
@@ -16,37 +16,30 @@
 
 package com.android.settings.sim;
 
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.res.Resources;
+import android.content.Intent;
 import android.os.Bundle;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
-import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.ListAdapter;
-import android.widget.TextView;
+import android.util.Log;
 import android.widget.Toast;
 
-import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
 
 import com.android.settings.R;
 
-import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 
-public class SimDialogActivity extends Activity {
+/**
+ * This activity provides singleton semantics per dialog type for showing various kinds of
+ * dialogs asking the user to make choices about which SIM to use for various services
+ * (calls, SMS, and data).
+ */
+public class SimDialogActivity extends FragmentActivity {
     private static String TAG = "SimDialogActivity";
 
     public static String PREFERRED_SIM = "preferred_sim";
@@ -60,276 +53,118 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        final int dialogType = getIntent().getIntExtra(DIALOG_TYPE_KEY, INVALID_PICK);
+        showOrUpdateDialog();
+    }
 
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        setIntent(intent);
+        showOrUpdateDialog();
+    }
+
+    private void showOrUpdateDialog() {
+        final int dialogType = getIntent().getIntExtra(DIALOG_TYPE_KEY, INVALID_PICK);
+        final String tag = Integer.toString(dialogType);
+        final FragmentManager fragmentManager = getSupportFragmentManager();
+        SimDialogFragment fragment = (SimDialogFragment) fragmentManager.findFragmentByTag(tag);
+
+        if (fragment == null) {
+            fragment = createFragment(dialogType);
+            fragment.show(fragmentManager, tag);
+        } else {
+            fragment.updateDialog();
+        }
+    }
+
+    private SimDialogFragment createFragment(int dialogType) {
+        switch(dialogType) {
+            case DATA_PICK:
+                return SimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_data,
+                        false /* includeAskEveryTime */);
+            case CALLS_PICK:
+                return CallsSimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_calls,
+                        true /* includeAskEveryTime */);
+            case SMS_PICK:
+                return SimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_sms,
+                        false /* includeAskEveryTime */);
+            case PREFERRED_PICK:
+                if (!getIntent().hasExtra(PREFERRED_SIM)) {
+                    throw new IllegalArgumentException("Missing required extra " + PREFERRED_SIM);
+                }
+                return PreferredSimDialogFragment.newInstance();
+            default:
+                throw new IllegalArgumentException( "Invalid dialog type " + dialogType + " sent.");
+        }
+    }
+
+    public void onSubscriptionSelected(int dialogType, int subId) {
+        if (getSupportFragmentManager().findFragmentByTag(Integer.toString(dialogType)) == null) {
+            Log.w(TAG, "onSubscriptionSelected ignored because stored fragment was null");
+            return;
+        }
         switch (dialogType) {
             case DATA_PICK:
+                setDefaultDataSubId(subId);
+                break;
             case CALLS_PICK:
+                setDefaultCallsSubId(subId);
+                break;
             case SMS_PICK:
-                createDialog(this, dialogType).show();
+                setDefaultSmsSubId(subId);
                 break;
             case PREFERRED_PICK:
-                displayPreferredDialog(getIntent().getIntExtra(PREFERRED_SIM, 0));
+                setPreferredSim(subId);
                 break;
             default:
-                throw new IllegalArgumentException("Invalid dialog type " + dialogType + " sent.");
-        }
-
-    }
-
-    private void displayPreferredDialog(final int slotId) {
-        final Resources res = getResources();
-        final Context context = getApplicationContext();
-        final SubscriptionInfo sir = SubscriptionManager.from(context)
-                .getActiveSubscriptionInfoForSimSlotIndex(slotId);
-
-        if (sir != null) {
-            AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
-            alertDialogBuilder.setTitle(R.string.sim_preferred_title);
-            alertDialogBuilder.setMessage(res.getString(
-                        R.string.sim_preferred_message, sir.getDisplayName()));
-
-            alertDialogBuilder.setPositiveButton(R.string.yes, new
-                    DialogInterface.OnClickListener() {
-                @Override
-                public void onClick(DialogInterface dialog, int id) {
-                    final int subId = sir.getSubscriptionId();
-                    PhoneAccountHandle phoneAccountHandle =
-                            subscriptionIdToPhoneAccountHandle(subId);
-                    setDefaultDataSubId(context, subId);
-                    setDefaultSmsSubId(context, subId);
-                    setUserSelectedOutgoingPhoneAccount(phoneAccountHandle);
-                    finish();
-                }
-            });
-            alertDialogBuilder.setNegativeButton(R.string.no, new
-                    DialogInterface.OnClickListener() {
-                @Override
-                public void onClick(DialogInterface dialog,int id) {
-                    finish();
-                }
-            });
-
-            alertDialogBuilder.create().show();
-        } else {
-            finish();
+                throw new IllegalArgumentException(
+                        "Invalid dialog type " + dialogType + " sent.");
         }
     }
 
-    private static void setDefaultDataSubId(final Context context, final int subId) {
-        final SubscriptionManager subscriptionManager = SubscriptionManager.from(context);
-        final TelephonyManager telephonyManager = TelephonyManager.from(context)
-                .createForSubscriptionId(subId);
+    public void onFragmentDismissed(SimDialogFragment simDialogFragment) {
+        final List<Fragment> fragments = getSupportFragmentManager().getFragments();
+        if (fragments.size() == 1 && fragments.get(0) == simDialogFragment) {
+            finishAndRemoveTask();
+        }
+    }
+
+    private void setDefaultDataSubId(final int subId) {
+        final SubscriptionManager subscriptionManager = getSystemService(SubscriptionManager.class);
+        final TelephonyManager telephonyManager = getSystemService(
+                TelephonyManager.class).createForSubscriptionId(subId);
         subscriptionManager.setDefaultDataSubId(subId);
         telephonyManager.setDataEnabled(true);
-        Toast.makeText(context, R.string.data_switch_started, Toast.LENGTH_LONG).show();
+        Toast.makeText(this, R.string.data_switch_started, Toast.LENGTH_LONG).show();
     }
 
-    private static void setDefaultSmsSubId(final Context context, final int subId) {
-        final SubscriptionManager subscriptionManager = SubscriptionManager.from(context);
-        subscriptionManager.setDefaultSmsSubId(subId);
-    }
-
-    private void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle phoneAccount) {
-        final TelecomManager telecomManager = TelecomManager.from(this);
+    private void setDefaultCallsSubId(final int subId) {
+        final PhoneAccountHandle phoneAccount = subscriptionIdToPhoneAccountHandle(subId);
+        final TelecomManager telecomManager = getSystemService(TelecomManager.class);
         telecomManager.setUserSelectedOutgoingPhoneAccount(phoneAccount);
     }
 
+    private void setDefaultSmsSubId(final int subId) {
+        final SubscriptionManager subscriptionManager = getSystemService(SubscriptionManager.class);
+        subscriptionManager.setDefaultSmsSubId(subId);
+    }
+
+    private void setPreferredSim(final int subId) {
+        setDefaultDataSubId(subId);
+        setDefaultSmsSubId(subId);
+        setDefaultCallsSubId(subId);
+    }
+
     private PhoneAccountHandle subscriptionIdToPhoneAccountHandle(final int subId) {
-        final TelecomManager telecomManager = TelecomManager.from(this);
-        final TelephonyManager telephonyManager = TelephonyManager.from(this);
-        final Iterator<PhoneAccountHandle> phoneAccounts =
-                telecomManager.getCallCapablePhoneAccounts().listIterator();
+        final TelecomManager telecomManager = getSystemService(TelecomManager.class);
+        final TelephonyManager telephonyManager = getSystemService(TelephonyManager.class);
 
-        while (phoneAccounts.hasNext()) {
-            final PhoneAccountHandle phoneAccountHandle = phoneAccounts.next();
-            final PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle);
+        for (PhoneAccountHandle handle : telecomManager.getCallCapablePhoneAccounts()) {
+            final PhoneAccount phoneAccount = telecomManager.getPhoneAccount(handle);
             if (subId == telephonyManager.getSubIdForPhoneAccount(phoneAccount)) {
-                return phoneAccountHandle;
+                return handle;
             }
         }
-
         return null;
     }
-
-    public Dialog createDialog(final Context context, final int id) {
-        final ArrayList<String> list = new ArrayList<String>();
-        final SubscriptionManager subscriptionManager = SubscriptionManager.from(context);
-        final List<SubscriptionInfo> subInfoList =
-            subscriptionManager.getActiveSubscriptionInfoList(true);
-        final int selectableSubInfoLength = subInfoList == null ? 0 : subInfoList.size();
-
-        final DialogInterface.OnClickListener selectionListener =
-                new DialogInterface.OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialog, int value) {
-
-                        final SubscriptionInfo sir;
-
-                        switch (id) {
-                            case DATA_PICK:
-                                sir = subInfoList.get(value);
-                                setDefaultDataSubId(context, sir.getSubscriptionId());
-                                break;
-                            case CALLS_PICK:
-                                final TelecomManager telecomManager =
-                                        TelecomManager.from(context);
-                                final List<PhoneAccountHandle> phoneAccountsList =
-                                        telecomManager.getCallCapablePhoneAccounts();
-                                setUserSelectedOutgoingPhoneAccount(
-                                        value < 1 ? null : phoneAccountsList.get(value - 1));
-                                break;
-                            case SMS_PICK:
-                                sir = subInfoList.get(value);
-                                setDefaultSmsSubId(context, sir.getSubscriptionId());
-                                break;
-                            default:
-                                throw new IllegalArgumentException("Invalid dialog type "
-                                        + id + " in SIM dialog.");
-                        }
-
-                        finish();
-                    }
-                };
-
-        Dialog.OnKeyListener keyListener = new Dialog.OnKeyListener() {
-            @Override
-            public boolean onKey(DialogInterface arg0, int keyCode,
-                    KeyEvent event) {
-                    if (keyCode == KeyEvent.KEYCODE_BACK) {
-                        finish();
-                    }
-                    return true;
-                }
-            };
-
-        ArrayList<SubscriptionInfo> callsSubInfoList = new ArrayList<SubscriptionInfo>();
-        if (id == CALLS_PICK) {
-            final TelecomManager telecomManager = TelecomManager.from(context);
-            final TelephonyManager telephonyManager = TelephonyManager.from(context);
-            final Iterator<PhoneAccountHandle> phoneAccounts =
-                    telecomManager.getCallCapablePhoneAccounts().listIterator();
-
-            list.add(getResources().getString(R.string.sim_calls_ask_first_prefs_title));
-            callsSubInfoList.add(null);
-            while (phoneAccounts.hasNext()) {
-                final PhoneAccount phoneAccount =
-                        telecomManager.getPhoneAccount(phoneAccounts.next());
-                list.add((String)phoneAccount.getLabel());
-                int subId = telephonyManager.getSubIdForPhoneAccount(phoneAccount);
-                if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-                    final SubscriptionInfo sir = SubscriptionManager.from(context)
-                            .getActiveSubscriptionInfo(subId);
-                    callsSubInfoList.add(sir);
-                } else {
-                    callsSubInfoList.add(null);
-                }
-            }
-        } else {
-            for (int i = 0; i < selectableSubInfoLength; ++i) {
-                final SubscriptionInfo sir = subInfoList.get(i);
-                CharSequence displayName = sir.getDisplayName();
-                if (displayName == null) {
-                    displayName = "";
-                }
-                list.add(displayName.toString());
-            }
-        }
-
-        String[] arr = list.toArray(new String[0]);
-
-        AlertDialog.Builder builder = new AlertDialog.Builder(context);
-
-        ListAdapter adapter = new SelectAccountListAdapter(
-                id == CALLS_PICK ? callsSubInfoList : subInfoList,
-                builder.getContext(),
-                R.layout.select_account_list_item,
-                arr, id);
-
-        switch (id) {
-            case DATA_PICK:
-                builder.setTitle(R.string.select_sim_for_data);
-                break;
-            case CALLS_PICK:
-                builder.setTitle(R.string.select_sim_for_calls);
-                break;
-            case SMS_PICK:
-                builder.setTitle(R.string.select_sim_for_sms);
-                break;
-            default:
-                throw new IllegalArgumentException("Invalid dialog type "
-                        + id + " in SIM dialog.");
-        }
-
-        Dialog dialog = builder.setAdapter(adapter, selectionListener).create();
-        dialog.setOnKeyListener(keyListener);
-
-        dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
-            @Override
-            public void onCancel(DialogInterface dialogInterface) {
-                finish();
-            }
-        });
-
-        return dialog;
-
-    }
-
-    private class SelectAccountListAdapter extends ArrayAdapter<String> {
-        private Context mContext;
-        private int mResId;
-        private int mDialogId;
-        private final float OPACITY = 0.54f;
-        private List<SubscriptionInfo> mSubInfoList;
-
-        public SelectAccountListAdapter(List<SubscriptionInfo> subInfoList,
-                Context context, int resource, String[] arr, int dialogId) {
-            super(context, resource, arr);
-            mContext = context;
-            mResId = resource;
-            mDialogId = dialogId;
-            mSubInfoList = subInfoList;
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            LayoutInflater inflater = (LayoutInflater)
-                    mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-            View rowView;
-            final ViewHolder holder;
-
-            if (convertView == null) {
-                // Cache views for faster scrolling
-                rowView = inflater.inflate(mResId, null);
-                holder = new ViewHolder();
-                holder.title = (TextView) rowView.findViewById(R.id.title);
-                holder.summary = (TextView) rowView.findViewById(R.id.summary);
-                holder.icon = (ImageView) rowView.findViewById(R.id.icon);
-                rowView.setTag(holder);
-            } else {
-                rowView = convertView;
-                holder = (ViewHolder) rowView.getTag();
-            }
-
-            final SubscriptionInfo sir = mSubInfoList.get(position);
-            if (sir == null) {
-                holder.title.setText(getItem(position));
-                holder.summary.setText("");
-                holder.icon.setImageDrawable(getResources()
-                        .getDrawable(R.drawable.ic_live_help));
-                holder.icon.setAlpha(OPACITY);
-            } else {
-                holder.title.setText(sir.getDisplayName());
-                holder.summary.setText(sir.getNumber());
-                holder.icon.setImageBitmap(sir.createIconBitmap(mContext));
-            }
-            return rowView;
-        }
-
-        private class ViewHolder {
-            TextView title;
-            TextView summary;
-            ImageView icon;
-        }
-    }
 }
diff --git a/src/com/android/settings/sim/SimDialogFragment.java b/src/com/android/settings/sim/SimDialogFragment.java
new file mode 100644
index 0000000..10815fd
--- /dev/null
+++ b/src/com/android/settings/sim/SimDialogFragment.java
@@ -0,0 +1,57 @@
+/*
+ * 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.settings.sim;
+
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+
+/** Common functionality for showing a dialog in SimDialogActivity. */
+public abstract class SimDialogFragment extends DialogFragment {
+    private static final String TAG = "SimDialogFragment";
+
+    private static final String KEY_TITLE_ID = "title_id";
+    private static final String KEY_DIALOG_TYPE = "dialog_type";
+
+    protected static Bundle initArguments(int dialogType, int titleResId) {
+        final Bundle args = new Bundle();
+        args.putInt(KEY_DIALOG_TYPE, dialogType);
+        args.putInt(KEY_TITLE_ID, titleResId);
+        return args;
+    }
+
+    public int getDialogType() {
+        return getArguments().getInt(KEY_DIALOG_TYPE);
+    }
+
+    public int getTitleResId() {
+        return getArguments().getInt(KEY_TITLE_ID);
+    }
+
+    @Override
+    public void onDismiss(@NonNull DialogInterface dialog) {
+        super.onDismiss(dialog);
+        final SimDialogActivity activity = (SimDialogActivity) getActivity();
+        if (activity != null && !activity.isFinishing()) {
+            activity.onFragmentDismissed(this);
+        }
+    }
+
+    public abstract void updateDialog();
+}
diff --git a/src/com/android/settings/sim/SimListDialogFragment.java b/src/com/android/settings/sim/SimListDialogFragment.java
new file mode 100644
index 0000000..f78c4e7
--- /dev/null
+++ b/src/com/android/settings/sim/SimListDialogFragment.java
@@ -0,0 +1,183 @@
+/*
+ * 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.settings.sim;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Shows a dialog consisting of a list of SIMs (aka subscriptions), possibly including an additional
+ * entry indicating "ask me every time".
+ */
+public class SimListDialogFragment extends SimDialogFragment implements
+        DialogInterface.OnClickListener {
+    protected static final String KEY_INCLUDE_ASK_EVERY_TIME = "include_ask_every_time";
+
+    protected SelectSubscriptionAdapter mAdapter;
+    @VisibleForTesting
+    List<SubscriptionInfo>  mSubscriptions;
+
+    public static SimListDialogFragment newInstance(int dialogType, int titleResId,
+            boolean includeAskEveryTime) {
+        final SimListDialogFragment fragment = new SimListDialogFragment();
+        final Bundle args = initArguments(dialogType, titleResId);
+        args.putBoolean(KEY_INCLUDE_ASK_EVERY_TIME, includeAskEveryTime);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        mSubscriptions = new ArrayList<>();
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+        builder.setTitle(getTitleResId());
+
+        mAdapter = new SelectSubscriptionAdapter(builder.getContext(), mSubscriptions);
+
+        setAdapter(builder);
+        final Dialog dialog = builder.create();
+        updateDialog();
+        return dialog;
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int selectionIndex) {
+        if (selectionIndex >= 0 && selectionIndex < mSubscriptions.size()) {
+            int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+            final SubscriptionInfo subscription = mSubscriptions.get(selectionIndex);
+            if (subscription != null) {
+                subId = subscription.getSubscriptionId();
+            }
+            final SimDialogActivity activity = (SimDialogActivity) getActivity();
+            activity.onSubscriptionSelected(getDialogType(), subId);
+        }
+    }
+
+    protected List<SubscriptionInfo> getCurrentSubscriptions() {
+        final SubscriptionManager manager = getContext().getSystemService(
+                SubscriptionManager.class);
+        return manager.getActiveSubscriptionInfoList(true);
+    }
+
+    @Override
+    public void updateDialog() {
+        List<SubscriptionInfo> currentSubscriptions = getCurrentSubscriptions();
+        if (currentSubscriptions == null) {
+            dismiss();
+            return;
+        }
+        if (getArguments().getBoolean(KEY_INCLUDE_ASK_EVERY_TIME)) {
+            final List<SubscriptionInfo> tmp = new ArrayList<>(currentSubscriptions.size() + 1);
+            tmp.add(null);
+            tmp.addAll(currentSubscriptions);
+            currentSubscriptions = tmp;
+        }
+        if (currentSubscriptions.equals(mSubscriptions)) {
+            return;
+        }
+        mSubscriptions.clear();
+        mSubscriptions.addAll(currentSubscriptions);
+        mAdapter.notifyDataSetChanged();
+    }
+
+    @VisibleForTesting
+    void setAdapter(AlertDialog.Builder builder) {
+        builder.setAdapter(mAdapter, this);
+    }
+
+    private static class SelectSubscriptionAdapter extends BaseAdapter {
+        private Context mContext;
+        private LayoutInflater mInflater;
+        List<SubscriptionInfo> mSubscriptions;
+
+        public SelectSubscriptionAdapter(Context context, List<SubscriptionInfo> subscriptions) {
+            mSubscriptions = subscriptions;
+            mContext = context;
+        }
+
+        @Override
+        public int getCount() {
+            return mSubscriptions.size();
+        }
+
+        @Override
+        public SubscriptionInfo getItem(int position) {
+            return mSubscriptions.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            final SubscriptionInfo info = mSubscriptions.get(position);
+            if (info == null) {
+                return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+            }
+            return info.getSubscriptionId();
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                if (mInflater == null) {
+                    mInflater = LayoutInflater.from(parent.getContext());
+                }
+                convertView = mInflater.inflate(R.layout.select_account_list_item, parent, false);
+            }
+            final SubscriptionInfo sub = getItem(position);
+
+            final TextView title = convertView.findViewById(R.id.title);
+            final TextView summary = convertView.findViewById(R.id.summary);
+            final ImageView icon = convertView.findViewById(R.id.icon);
+
+            if (sub == null) {
+                title.setText(R.string.sim_calls_ask_first_prefs_title);
+                summary.setText("");
+                icon.setImageDrawable(mContext.getDrawable(R.drawable.ic_help));
+                icon.setImageTintList(
+                        Utils.getColorAttr(mContext, android.R.attr.textColorSecondary));
+            } else {
+                title.setText(sub.getDisplayName());
+                summary.setText(sub.getNumber());
+                icon.setImageBitmap(sub.createIconBitmap(mContext));
+
+            }
+            return convertView;
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/sim/PreferredSimDialogFragmentTest.java b/tests/robotests/src/com/android/settings/sim/PreferredSimDialogFragmentTest.java
new file mode 100644
index 0000000..0b85c37
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/sim/PreferredSimDialogFragmentTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.settings.sim;
+
+import static com.android.settings.sim.SimDialogActivity.PREFERRED_PICK;
+import static com.android.settings.sim.SimDialogActivity.PREFERRED_SIM;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.DialogInterface;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowAlertDialogCompat.class)
+public class PreferredSimDialogFragmentTest extends
+        SimDialogFragmentTestBase<PreferredSimDialogFragment> {
+
+    @Override
+    public void setUp() {
+        super.setUp();
+        setDialogType(PREFERRED_PICK);
+        mFragment = spy(PreferredSimDialogFragment.newInstance());
+        doReturn(mSubscriptionManager).when(mFragment).getSubscriptionManager();
+    }
+
+    @Test
+    public void onCreateDialog_noSims_dismissed() {
+        when(mSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(anyInt()))
+                .thenReturn(null);
+        mIntent.putExtra(PREFERRED_SIM, 0);
+        startDialog();
+        verify(mFragment).dismiss();
+    }
+
+    @Test
+    public void onCreateDialog_oneSimWrongSlotArgument_dismissed() {
+        when(mSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(1)).thenReturn(null);
+        mIntent.putExtra(PREFERRED_SIM, 1);
+        startDialog();
+        verify(mFragment).dismiss();
+    }
+
+    @Test
+    public void onCreateDialog_twoSimsSelectFirst_correctMessage() {
+        mIntent.putExtra(PREFERRED_SIM, 0);
+
+        final AlertDialog alertDialog = startDialog();
+        final ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(alertDialog);
+        final String message = (String) shadowDialog.getMessage();
+        assertThat(message).contains(SIM1_NAME);
+        assertThat(message).doesNotContain(SIM2_NAME);
+    }
+
+    @Test
+    public void onCreateDialog_twoSimsSelectSecond_correctMessage() {
+        mIntent.putExtra(PREFERRED_SIM, 1);
+
+        final AlertDialog alertDialog = startDialog();
+        final ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(alertDialog);
+        final String message = (String) shadowDialog.getMessage();
+        assertThat(message).contains(SIM2_NAME);
+        assertThat(message).doesNotContain(SIM1_NAME);
+    }
+
+    @Test
+    public void onClick_yesClicked_callsOnSubscriptionSelected() {
+        mIntent.putExtra(PREFERRED_SIM, 0);
+
+        final AlertDialog alertDialog = startDialog();
+
+        final SimDialogActivity activity = (SimDialogActivity) spy(mFragment.getActivity());
+        doReturn(activity).when(mFragment).getActivity();
+        doNothing().when(activity).onSubscriptionSelected(anyInt(), anyInt());
+
+        mFragment.onClick(alertDialog, DialogInterface.BUTTON_POSITIVE);
+        verify(activity).onSubscriptionSelected(PREFERRED_PICK, SIM1_ID);
+    }
+
+    @Test
+    public void onClick_noClicked_doesNotCallOnSubscriptionSelected() {
+        mIntent.putExtra(PREFERRED_SIM, 0);
+
+        final AlertDialog alertDialog = startDialog();
+
+        final SimDialogActivity activity = (SimDialogActivity) spy(mFragment.getActivity());
+        doReturn(activity).when(mFragment).getActivity();
+        doNothing().when(activity).onSubscriptionSelected(anyInt(), anyInt());
+
+        mFragment.onClick(alertDialog, DialogInterface.BUTTON_NEGATIVE);
+        verify(activity, never()).onSubscriptionSelected(anyInt(), anyInt());
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/sim/SimDialogFragmentTestBase.java b/tests/robotests/src/com/android/settings/sim/SimDialogFragmentTestBase.java
new file mode 100644
index 0000000..904b831
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/sim/SimDialogFragmentTestBase.java
@@ -0,0 +1,76 @@
+/*
+ * 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.settings.sim;
+
+import static com.android.settings.sim.SimDialogActivity.DIALOG_TYPE_KEY;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+public abstract class SimDialogFragmentTestBase<T extends SimDialogFragment> {
+    protected static final int SIM1_ID = 111;
+    protected static final int SIM2_ID = 222;
+    protected static final String SIM1_NAME = "sim111";
+    protected static final String SIM2_NAME = "sim222";
+
+    @Mock
+    protected SubscriptionManager mSubscriptionManager;
+    @Mock
+    protected SubscriptionInfo mSim1;
+    @Mock
+    protected SubscriptionInfo mSim2;
+
+    protected T mFragment;
+    protected Intent mIntent;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mIntent = new Intent();
+
+        when(mSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(0)).thenReturn(mSim1);
+        when(mSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(1)).thenReturn(mSim2);
+
+        when(mSim1.getSubscriptionId()).thenReturn(SIM1_ID);
+        when(mSim1.getDisplayName()).thenReturn(SIM1_NAME);
+        when(mSim2.getSubscriptionId()).thenReturn(SIM2_ID);
+        when(mSim2.getDisplayName()).thenReturn(SIM2_NAME);
+    }
+
+    protected void setDialogType(int dialogType) {
+        mIntent.putExtra(DIALOG_TYPE_KEY, dialogType);
+    }
+
+    protected AlertDialog startDialog() {
+        final FragmentController controller = FragmentController.of(mFragment,
+                SimDialogActivity.class, mIntent);
+        controller.create(0 /* containerViewId */, null /* bundle */).start().visible();
+        return ShadowAlertDialogCompat.getLatestAlertDialog();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/sim/SimListDialogFragmentTest.java b/tests/robotests/src/com/android/settings/sim/SimListDialogFragmentTest.java
new file mode 100644
index 0000000..2b33ebe
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/sim/SimListDialogFragmentTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.settings.sim;
+
+import static com.android.settings.sim.SimDialogActivity.DATA_PICK;
+import static com.android.settings.sim.SimDialogActivity.SMS_PICK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.telephony.SubscriptionManager;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.R;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowAlertDialogCompat.class)
+public class SimListDialogFragmentTest extends SimDialogFragmentTestBase<SimListDialogFragment> {
+
+    @Test
+    public void onCreateDialog_noSubscriptions_dismissed() {
+        final int dialogType = DATA_PICK;
+        setDialogType(dialogType);
+        mFragment = spy(SimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_data,
+                false /* includeAskEveryTime */));
+        doReturn(null).when(mFragment).getCurrentSubscriptions();
+        startDialog();
+        verify(mFragment).dismiss();
+    }
+
+    @Test
+    public void onCreateDialog_twoSubscriptionsNoAskEveryTime_twoSubsForDisplay() {
+        final int dialogType = DATA_PICK;
+        setDialogType(dialogType);
+        mFragment = spy(SimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_data,
+                false /* includeAskEveryTime */));
+        doReturn(Arrays.asList(mSim1, mSim2)).when(mFragment).getCurrentSubscriptions();
+        // Avoid problems robolectric has with our real adapter.
+        doNothing().when(mFragment).setAdapter(any());
+        final AlertDialog alertDialog = startDialog();
+        assertThat(mFragment.mSubscriptions).hasSize(2);
+
+        final SimDialogActivity activity = (SimDialogActivity) spy(mFragment.getActivity());
+        doReturn(activity).when(mFragment).getActivity();
+        doNothing().when(activity).onSubscriptionSelected(anyInt(), anyInt());
+
+        mFragment.onClick(alertDialog, 1);
+        verify(activity).onSubscriptionSelected(dialogType, SIM2_ID);
+    }
+
+    @Test
+    public void onCreateDialog_twoSubscriptionsAskEveryTime_threeSubsForDisplay() {
+        final int dialogType = SMS_PICK;
+        setDialogType(dialogType);
+        mFragment = spy(SimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_sms,
+                true /* includeAskEveryTime */));
+        doReturn(Arrays.asList(mSim1, mSim2)).when(mFragment).getCurrentSubscriptions();
+        // Avoid problems robolectric has with our real adapter.
+        doNothing().when(mFragment).setAdapter(any());
+        final AlertDialog alertDialog = startDialog();
+        assertThat(mFragment.mSubscriptions).hasSize(3);
+        assertThat(mFragment.mSubscriptions.get(0)).isNull();
+
+        final SimDialogActivity activity = (SimDialogActivity) spy(mFragment.getActivity());
+        doReturn(activity).when(mFragment).getActivity();
+        doNothing().when(activity).onSubscriptionSelected(anyInt(), anyInt());
+
+        mFragment.onClick(alertDialog, 0);
+        verify(activity).onSubscriptionSelected(dialogType,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+    }
+}