diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b7fd2991547..9a88e987ce5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -102,6 +102,8 @@ + + + + diff --git a/src/com/android/settings/network/telephony/DeleteSimProfileConfirmationDialog.java b/src/com/android/settings/network/telephony/DeleteSimProfileConfirmationDialog.java new file mode 100644 index 00000000000..52c5cc10b4d --- /dev/null +++ b/src/com/android/settings/network/telephony/DeleteSimProfileConfirmationDialog.java @@ -0,0 +1,83 @@ +/* + * 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.network.telephony; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.telephony.SubscriptionInfo; + +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.core.instrumentation.InstrumentedDialogFragment; + +public class DeleteSimProfileConfirmationDialog extends InstrumentedDialogFragment implements + DialogInterface.OnClickListener { + public static final String TAG = "confirm_delete_sim"; + public static final String KEY_SUBSCRIPTION_INFO = "subscription_info"; + private SubscriptionInfo mInfo; + + public static DeleteSimProfileConfirmationDialog newInstance(SubscriptionInfo info) { + final DeleteSimProfileConfirmationDialog dialog = + new DeleteSimProfileConfirmationDialog(); + final Bundle args = new Bundle(); + args.putParcelable(KEY_SUBSCRIPTION_INFO, info); + dialog.setArguments(args); + return dialog; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + mInfo = getArguments().getParcelable(KEY_SUBSCRIPTION_INFO); + Context context = getContext(); + final String message = context.getString(R.string.mobile_network_erase_sim_dialog_body, + mInfo.getCarrierName(), mInfo.getCarrierName()); + return new AlertDialog.Builder(context) + .setTitle(R.string.mobile_network_erase_sim_dialog_title) + .setMessage(message) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.mobile_network_erase_sim_dialog_ok, this) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + beginDeletionWithProgress(); + } + } + + @VisibleForTesting + void beginDeletionWithProgress() { + final DeleteSimProfileProgressDialog progress = + DeleteSimProfileProgressDialog.newInstance(mInfo.getSubscriptionId()); + progress.setTargetFragment(getTargetFragment(), 0); + progress.show(getFragmentManager(), DeleteSimProfileProgressDialog.TAG); + } + + @Override + public int getMetricsCategory() { + // TODO(b/131519375) - use a real id here once it's been created in the metrics proto + return 0; + } +} diff --git a/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.java b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.java new file mode 100644 index 00000000000..22ff2b6a09a --- /dev/null +++ b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.java @@ -0,0 +1,74 @@ +/* + * 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.network.telephony; + +import android.content.Context; +import android.telephony.SubscriptionInfo; + +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.network.SubscriptionUtil; + +/** This controls a preference allowing the user to delete the profile for an eSIM. */ +public class DeleteSimProfilePreferenceController extends BasePreferenceController { + + private SubscriptionInfo mSubscriptionInfo; + private Fragment mParentFragment; + + public DeleteSimProfilePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + public void init(int subscriptionId, Fragment parentFragment) { + mParentFragment = parentFragment; + + for (SubscriptionInfo info : SubscriptionUtil.getAvailableSubscriptions( + mContext)) { + if (info.getSubscriptionId() == subscriptionId && info.isEmbedded()) { + mSubscriptionInfo = info; + break; + } + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final Preference pref = screen.findPreference(getPreferenceKey()); + pref.setOnPreferenceClickListener(p -> { + final DeleteSimProfileConfirmationDialog dialogFragment = + DeleteSimProfileConfirmationDialog.newInstance(mSubscriptionInfo); + dialogFragment.setTargetFragment(mParentFragment, 0); + dialogFragment.show(mParentFragment.getFragmentManager(), + DeleteSimProfileConfirmationDialog.TAG); + return true; + }); + } + + @Override + public int getAvailabilityStatus() { + if (mSubscriptionInfo != null) { + return AVAILABLE; + } else { + return CONDITIONALLY_UNAVAILABLE; + } + } + +} diff --git a/src/com/android/settings/network/telephony/DeleteSimProfileProgressDialog.java b/src/com/android/settings/network/telephony/DeleteSimProfileProgressDialog.java new file mode 100644 index 00000000000..15f4b2223db --- /dev/null +++ b/src/com/android/settings/network/telephony/DeleteSimProfileProgressDialog.java @@ -0,0 +1,120 @@ +/* + * 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.network.telephony; + +import android.app.Activity; +import android.app.Dialog; +import android.app.PendingIntent; +import android.app.ProgressDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.telephony.euicc.EuiccManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +public class DeleteSimProfileProgressDialog extends InstrumentedDialogFragment { + public static final String TAG = "delete_sim_progress"; + + // Note that this must be listed in AndroidManfiest.xml in a tag + @VisibleForTesting + static final String PENDING_INTENT = + "com.android.settings.DELETE_SIM_PROFILE_RESULT"; + private static final int PENDING_INTENT_REQUEST_CODE = 1; + private static final String KEY_SUBSCRIPTION_ID = "subscription_id"; + @VisibleForTesting + static final String KEY_DELETE_STARTED = "delete_started"; + + private boolean mDeleteStarted; + private BroadcastReceiver mReceiver; + + public static DeleteSimProfileProgressDialog newInstance(int subscriptionId) { + final DeleteSimProfileProgressDialog dialog = new DeleteSimProfileProgressDialog(); + final Bundle args = new Bundle(); + args.putInt(KEY_SUBSCRIPTION_ID, subscriptionId); + dialog.setArguments(args); + return dialog; + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_DELETE_STARTED, mDeleteStarted); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + if (savedInstanceState != null) { + mDeleteStarted = savedInstanceState.getBoolean(KEY_DELETE_STARTED, false); + } + final Context context = getContext(); + final ProgressDialog progressDialog = new ProgressDialog(context); + progressDialog.setMessage( + context.getString(R.string.mobile_network_erase_sim_dialog_progress)); + + mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + dismiss(); + final Activity activity = getActivity(); + if (activity != null && !activity.isFinishing()) { + activity.finish(); + } + } + }; + context.registerReceiver(mReceiver, new IntentFilter(PENDING_INTENT)); + + if (!mDeleteStarted) { + final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, + PENDING_INTENT_REQUEST_CODE, new Intent(PENDING_INTENT), + PendingIntent.FLAG_ONE_SHOT); + + final EuiccManager euiccManager = context.getSystemService(EuiccManager.class); + final int subId = getArguments().getInt(KEY_SUBSCRIPTION_ID); + euiccManager.deleteSubscription(subId, pendingIntent); + mDeleteStarted = true; + } + + return progressDialog; + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + if (mReceiver != null) { + final Context context = getContext(); + if (context != null) { + context.unregisterReceiver(mReceiver); + } + mReceiver = null; + } + } + + @Override + public int getMetricsCategory() { + // TODO(b/131519375) - use a real id here once it's been created in the metrics proto + return 0; + } +} diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index eb00b9f64f9..e18971d43d6 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -138,6 +138,7 @@ public class MobileNetworkSettings extends RestrictedDashboardFragment { use(BillingCyclePreferenceController.class).init(mSubId); use(MmsMessagePreferenceController.class).init(mSubId); use(DisabledSubscriptionController.class).init(getLifecycle(), mSubId); + use(DeleteSimProfilePreferenceController.class).init(mSubId, this); } use(MobileDataPreferenceController.class).init(getFragmentManager(), mSubId); use(RoamingPreferenceController.class).init(getFragmentManager(), mSubId); diff --git a/tests/robotests/src/com/android/settings/network/telephony/DeleteSimProfileConfirmationDialogTest.java b/tests/robotests/src/com/android/settings/network/telephony/DeleteSimProfileConfirmationDialogTest.java new file mode 100644 index 00000000000..9b6f5511e0d --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/telephony/DeleteSimProfileConfirmationDialogTest.java @@ -0,0 +1,79 @@ +/* + * 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.network.telephony; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.telephony.SubscriptionInfo; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; + +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.androidx.fragment.FragmentController; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowAlertDialogCompat.class) +public class DeleteSimProfileConfirmationDialogTest { + @Mock + private SubscriptionInfo mSubscriptionInfo; + + private DeleteSimProfileConfirmationDialog mDialogFragment; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mDialogFragment = spy(DeleteSimProfileConfirmationDialog.newInstance(mSubscriptionInfo)); + doNothing().when(mDialogFragment).beginDeletionWithProgress(); + } + + @Test + public void showDialog_dialogCancelled_deleteNotCalled() { + FragmentController.setupFragment(mDialogFragment, FragmentActivity.class, + 0 /* containerViewId */, + null /* bundle */); + final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick(); + verify(mDialogFragment, never()).beginDeletionWithProgress(); + } + + @Test + public void showDialog_dialogOk_deleteWasCalled() { + FragmentController.setupFragment(mDialogFragment, FragmentActivity.class, + 0 /* containerViewId */, + null /* bundle */); + final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick(); + verify(mDialogFragment).beginDeletionWithProgress(); + } +} diff --git a/tests/robotests/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceControllerTest.java new file mode 100644 index 00000000000..21fd19bf50b --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceControllerTest.java @@ -0,0 +1,111 @@ +/* + * 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.network.telephony; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.telephony.SubscriptionInfo; +import android.telephony.euicc.EuiccManager; + +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.network.SubscriptionUtil; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.Arrays; + +@RunWith(RobolectricTestRunner.class) +public class DeleteSimProfilePreferenceControllerTest { + private static final String PREF_KEY = "delete_profile_key"; + private static final int SUB_ID = 1234; + private static final int OTHER_ID = 5678; + + @Mock + private Fragment mFragment; + + @Mock + private SubscriptionInfo mSubscriptionInfo; + @Mock + private PreferenceScreen mScreen; + + private Context mContext; + private Preference mPreference; + private DeleteSimProfilePreferenceController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + + SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscriptionInfo)); + when(mSubscriptionInfo.getSubscriptionId()).thenReturn(SUB_ID); + when(mSubscriptionInfo.isEmbedded()).thenReturn(true); + + mPreference = new Preference(mContext); + mPreference.setKey(PREF_KEY); + when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference); + + mController = new DeleteSimProfilePreferenceController(mContext, PREF_KEY); + } + + @After + public void tearDown() { + SubscriptionUtil.setAvailableSubscriptionsForTesting(null); + } + + @Test + public void getAvailabilityStatus_noSubs_notAvailable() { + SubscriptionUtil.setAvailableSubscriptionsForTesting(new ArrayList<>()); + mController.init(SUB_ID, mFragment); + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void getAvailabilityStatus_physicalSim_notAvailable() { + when(mSubscriptionInfo.isEmbedded()).thenReturn(false); + mController.init(SUB_ID, mFragment); + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void getAvailabilityStatus_unknownSim_notAvailable() { + when(mSubscriptionInfo.getSubscriptionId()).thenReturn(OTHER_ID); + mController.init(SUB_ID, mFragment); + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void getAvailabilityStatus_knownEsim_isAvailable() { + mController.init(SUB_ID, mFragment); + assertThat(mController.isAvailable()).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/network/telephony/DeleteSimProfileProgressDialogTest.java b/tests/robotests/src/com/android/settings/network/telephony/DeleteSimProfileProgressDialogTest.java new file mode 100644 index 00000000000..aebcc461ea2 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/telephony/DeleteSimProfileProgressDialogTest.java @@ -0,0 +1,139 @@ +/* + * 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.network.telephony; + +import static com.android.settings.network.telephony.DeleteSimProfileProgressDialog.KEY_DELETE_STARTED; +import static com.android.settings.network.telephony.DeleteSimProfileProgressDialog.PENDING_INTENT; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doNothing; +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.app.Dialog; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.telephony.euicc.EuiccManager; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; + +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowAlertDialogCompat.class) +public class DeleteSimProfileProgressDialogTest { + private static final int SUB_ID = 111; + + @Mock + private FragmentActivity mActivity; + @Mock + private Fragment mTargetFragment; + @Mock + private EuiccManager mEuiccManager; + + private Context mContext; + private DeleteSimProfileProgressDialog mDialogFragment; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + + when(mContext.getSystemService(EuiccManager.class)).thenReturn(mEuiccManager); + mDialogFragment = spy(DeleteSimProfileProgressDialog.newInstance(SUB_ID)); + when(mDialogFragment.getContext()).thenReturn(mContext); + when(mDialogFragment.getTargetFragment()).thenReturn(mTargetFragment); + when(mDialogFragment.getActivity()).thenReturn(mActivity); + } + + @Test + public void onCreateDialog_firstShowing_deleteStartedAndRecordedInOutState() { + mDialogFragment.onCreateDialog(null); + verify(mEuiccManager).deleteSubscription(eq(SUB_ID), notNull()); + + final Bundle outState = new Bundle(); + mDialogFragment.onSaveInstanceState(outState); + assertThat(outState.containsKey(KEY_DELETE_STARTED)).isTrue(); + assertThat(outState.getBoolean(KEY_DELETE_STARTED)).isTrue(); + } + + @Test + public void showDialog_secondShowing_deleteNotStarted() { + final Bundle inState = new Bundle(); + inState.putBoolean(KEY_DELETE_STARTED, true); + mDialogFragment.onCreateDialog(inState); + + verify(mEuiccManager, never()).deleteSubscription(anyInt(), any()); + + final Bundle outState = new Bundle(); + mDialogFragment.onSaveInstanceState(outState); + assertThat(outState.containsKey(KEY_DELETE_STARTED)).isTrue(); + assertThat(outState.getBoolean(KEY_DELETE_STARTED)).isTrue(); + } + + @Test + public void showDialog_pendingIntentReceiverFired_activityFinished() { + mDialogFragment.onCreateDialog(null); + + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass( + PendingIntent.class); + verify(mEuiccManager).deleteSubscription(eq(SUB_ID), intentCaptor.capture()); + assertThat(intentCaptor.getValue()).isNotNull(); + + final ArgumentCaptor receiverCaptor = ArgumentCaptor.forClass( + BroadcastReceiver.class); + verify(mContext).registerReceiver(receiverCaptor.capture(), any(IntentFilter.class)); + + doNothing().when(mDialogFragment).dismiss(); + receiverCaptor.getValue().onReceive(mContext, new Intent(PENDING_INTENT)); + verify(mDialogFragment).dismiss(); + verify(mActivity).finish(); + } + + @Test + public void onDismiss_receiverUnregistered() { + Dialog dialog = mDialogFragment.onCreateDialog(null); + final ArgumentCaptor receiverCaptor = ArgumentCaptor.forClass( + BroadcastReceiver.class); + verify(mContext).registerReceiver(receiverCaptor.capture(), any(IntentFilter.class)); + + mDialogFragment.onDismiss(dialog); + verify(mContext).unregisterReceiver(eq(receiverCaptor.getValue())); + } +}