diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c1d6a588569..189e1b6ea9c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -511,6 +511,11 @@ + + See less + + + Turn off %1$s? + + Turn off SIM? + + Turning off SIM + + Can\'t disable carrier + + Something went wrong and your carrier could not be disabled. + Connect to device diff --git a/src/com/android/settings/SidecarFragment.java b/src/com/android/settings/SidecarFragment.java new file mode 100644 index 00000000000..1a69c03f48d --- /dev/null +++ b/src/com/android/settings/SidecarFragment.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2020 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; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.CallSuper; +import androidx.annotation.IntDef; + +import com.android.settingslib.utils.ThreadUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * A headless fragment encapsulating a long-running action such as a network RPC surviving rotation. + * + *

Subclasses should implement their own state machine, updating the state on each state change + * via {@link #setState(int, int)}. They can define their own states, however, it is suggested that + * the pre-defined {@link @State} constants are used and customizations are implemented via + * substates. Custom states must be outside the range of pre-defined states. + * + *

It is safe to update the state at any time, but state updates must originate from the main + * thread. + * + *

A listener can be attached that receives state updates while it's registered. Note that state + * change events can occur at any point in time and hence a registered listener should unregister if + * it cannot act upon the state change (typically a non-resumed fragment). + * + *

Listeners can receive state changes for the same state/substate combination, so listeners + * should make sure to be idempotent during state change events. + * + *

If a SidecarFragment is only relevant during the lifetime of another fragment (for example, a + * sidecar performing a details request for a DetailsFragment), that fragment needs to become the + * managing fragment of the sidecar. + * + *

Managing fragment responsibilities

+ * + *
    + *
  1. Instantiates the sidecar fragment when necessary, preferably in {@link #onStart}. + *
  2. Removes the sidecar fragment when it's no longer used or when itself is removed. Removal of + * the managing fragment can be detected by checking {@link #isRemoving} in {@link #onStop}. + *
    + *
  3. Registers as a listener in {@link #onResume()}, unregisters in {@link #onPause()}. + *
  4. Starts the long-running operation by calling into the sidecar. + *
  5. Receives state updates via {@link Listener#onStateChange(SidecarFragment)} and updates the + * UI accordingly. + *
+ * + *

Managing fragment example

+ * + *
+ *     public class MainFragment implements SidecarFragment.Listener {
+ *         private static final String TAG_SOME_SIDECAR = ...;
+ *         private static final String KEY_SOME_SIDECAR_STATE = ...;
+ *
+ *         private SomeSidecarFragment mSidecar;
+ *
+ *         @Override
+ *         public void onStart() {
+ *             super.onStart();
+ *             Bundle args = ...; // optional args
+ *             mSidecar = SidecarFragment.get(getFragmentManager(), TAG_SOME_SIDECAR,
+ *                     SidecarFragment.class, args);
+ *         }
+ *
+ *         @Override
+ *         public void onResume() {
+ *             mSomeSidecar.addListener(this);
+ *         }
+ *
+ *         @Override
+ *         public void onPause() {
+ *             mSomeSidecar.removeListener(this):
+ *         }
+ *     }
+ * 
+ */ +public class SidecarFragment extends Fragment { + + private static final String TAG = "SidecarFragment"; + + /** + * Get an instance of this sidecar. + * + *

Will return the existing instance if one is already present. Note that the args will not + * be used in this situation, so args must be constant for any particular fragment manager and + * tag. + */ + @SuppressWarnings("unchecked") + protected static T get( + FragmentManager fm, String tag, Class clazz, Bundle args) { + T fragment = (T) fm.findFragmentByTag(tag); + if (fragment == null) { + try { + fragment = clazz.newInstance(); + } catch (java.lang.InstantiationException e) { + throw new InstantiationException("Unable to create fragment", e); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Unable to create fragment", e); + } + if (args != null) { + fragment.setArguments(args); + } + fm.beginTransaction().add(fragment, tag).commit(); + // No real harm in doing this here - get() should generally only be called from onCreate + // which is on the main thread - and it allows us to start running the sidecar on this + // instance immediately rather than having to wait until the transaction commits. + fm.executePendingTransactions(); + } + + return fragment; + } + + /** State definitions. @see {@link #getState} */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({State.INIT, State.RUNNING, State.SUCCESS, State.ERROR}) + public @interface State { + /** Initial idling state. */ + int INIT = 0; + + /** The long-running operation is in progress. */ + int RUNNING = 1; + + /** The long-running operation has succeeded. */ + int SUCCESS = 2; + + /** The long-running operation has failed. */ + int ERROR = 3; + } + + /** Substate definitions. @see {@link #getSubstate} */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + Substate.UNUSED, + Substate.RUNNING_BIND_SERVICE, + Substate.RUNNING_GET_ACTIVATION_CODE, + }) + public @interface Substate { + // Unknown/unused substate. + int UNUSED = 0; + int RUNNING_BIND_SERVICE = 1; + int RUNNING_GET_ACTIVATION_CODE = 2; + + // Future tags: 3+ + } + + /** **************************************** */ + private Set mListeners = new CopyOnWriteArraySet<>(); + + // Used to track whether onCreate has been called yet. + private boolean mCreated; + + @State private int mState; + @Substate private int mSubstate; + + /** A listener receiving state change events. */ + public interface Listener { + + /** + * Called upon any state or substate change. + * + *

The new state can be queried through {@link #getState} and {@link #getSubstate}. + * + *

Called from the main thread. + * + * @param fragment the SidecarFragment that changed its state + */ + void onStateChange(SidecarFragment fragment); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + mCreated = true; + setState(State.INIT, Substate.UNUSED); + } + + @Override + public void onDestroy() { + mCreated = false; + super.onDestroy(); + } + + /** + * Registers a listener that will receive subsequent state changes. + * + *

A {@link Listener#onStateChange(SidecarFragment)} event is fired as part of this call + * unless {@link #onCreate} has not yet been called (which means that it's unsafe to access this + * fragment as it has not been setup or restored completely). In that case, the future call to + * onCreate will trigger onStateChange on registered listener. + * + *

Must be called from the main thread. + * + * @param listener a listener, or null for unregistering the current listener + */ + public void addListener(Listener listener) { + ThreadUtils.ensureMainThread(); + mListeners.add(listener); + if (mCreated) { + notifyListener(listener); + } + } + + /** + * Removes a previously registered listener. + * + * @return {@code true} if the listener was removed, {@code false} if there was no such listener + * registered. + */ + public boolean removeListener(Listener listener) { + ThreadUtils.ensureMainThread(); + return mListeners.remove(listener); + } + + /** Returns the current state. */ + @State + public int getState() { + return mState; + } + + /** Returns the current substate. */ + @Substate + public int getSubstate() { + return mSubstate; + } + + /** + * Resets the sidecar to its initial state. + * + *

Implementers can override this method to perform additional reset tasks, but must call the + * super method. + */ + @CallSuper + public void reset() { + setState(State.INIT, Substate.UNUSED); + } + + /** + * Updates the state and substate and notifies the registered listener. + * + *

Must be called from the main thread. + * + * @param state the state to transition to + * @param substate the substate to transition to + */ + protected void setState(@State int state, @Substate int substate) { + ThreadUtils.ensureMainThread(); + + mState = state; + mSubstate = substate; + notifyAllListeners(); + printState(); + } + + private void notifyAllListeners() { + for (Listener listener : mListeners) { + notifyListener(listener); + } + } + + private void notifyListener(Listener listener) { + listener.onStateChange(this); + } + + /** Prints the state of the sidecar. */ + public void printState() { + StringBuilder sb = + new StringBuilder("SidecarFragment.setState(): Sidecar Class: ") + .append(getClass().getCanonicalName()); + sb.append(", State: "); + switch (mState) { + case SidecarFragment.State.INIT: + sb.append("State.INIT"); + break; + case SidecarFragment.State.RUNNING: + sb.append("State.RUNNING"); + break; + case SidecarFragment.State.SUCCESS: + sb.append("State.SUCCESS"); + break; + case SidecarFragment.State.ERROR: + sb.append("State.ERROR"); + break; + default: + sb.append(mState); + break; + } + switch (mSubstate) { + case SidecarFragment.Substate.UNUSED: + sb.append(", Substate.UNUSED"); + break; + default: + sb.append(", ").append(mSubstate); + break; + } + + Log.v(TAG, sb.toString()); + } + + @Override + public String toString() { + return String.format( + Locale.US, + "SidecarFragment[mState=%d, mSubstate=%d]: %s", + mState, + mSubstate, + super.toString()); + } + + /** The State of the sidecar status. */ + public static final class States { + public static final States SUCCESS = States.create(State.SUCCESS, Substate.UNUSED); + public static final States ERROR = States.create(State.ERROR, Substate.UNUSED); + + @State public final int state; + @Substate public final int substate; + + /** Creates a new sidecar state. */ + public static States create(@State int state, @Substate int substate) { + return new States(state, substate); + } + + public States(@State int state, @Substate int substate) { + this.state = state; + this.substate = substate; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof States)) { + return false; + } + States other = (States) o; + return this.state == other.state && this.substate == other.substate; + } + + @Override + public int hashCode() { + return state * 31 + substate; + } + } +} diff --git a/src/com/android/settings/network/SubscriptionUtil.java b/src/com/android/settings/network/SubscriptionUtil.java index e61cc36d37e..ac21e12c232 100644 --- a/src/com/android/settings/network/SubscriptionUtil.java +++ b/src/com/android/settings/network/SubscriptionUtil.java @@ -21,6 +21,7 @@ import static android.telephony.UiccSlotInfo.CARD_STATE_INFO_PRESENT; import static com.android.internal.util.CollectionUtils.emptyIfNull; +import android.annotation.Nullable; import android.content.Context; import android.os.ParcelUuid; import android.telephony.SubscriptionInfo; @@ -30,6 +31,8 @@ import android.telephony.UiccSlotInfo; import androidx.annotation.VisibleForTesting; +import com.android.settings.network.telephony.ToggleSubscriptionDialogActivity; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -278,6 +281,33 @@ public class SubscriptionUtil { } } + /** Starts a dialog activity to handle SIM enabling/disabling. */ + public static void startToggleSubscriptionDialogActivity( + Context context, int subId, boolean enable) { + context.startActivity(ToggleSubscriptionDialogActivity.getIntent(context, subId, enable)); + } + + /** + * Finds and returns a subscription with a specific subscription ID. + * @param subscriptionManager The ProxySubscriptionManager for accessing subscription + * information + * @param subId The id of subscription to be returned + * @return the {@code SubscriptionInfo} whose ID is {@code subId}. It returns null if the + * {@code subId} is {@code SubscriptionManager.INVALID_SUBSCRIPTION_ID} or no such + * {@code SubscriptionInfo} is found. + */ + @Nullable + public static SubscriptionInfo getSubById(SubscriptionManager subscriptionManager, int subId) { + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + return null; + } + return subscriptionManager + .getAllSubscriptionInfoList() + .stream() + .filter(subInfo -> subInfo.getSubscriptionId() == subId) + .findFirst() + .get(); + } /** * Whether a subscription is visible to API caller. If it's a bundled opportunistic diff --git a/src/com/android/settings/network/SwitchToEuiccSubscriptionSidecar.java b/src/com/android/settings/network/SwitchToEuiccSubscriptionSidecar.java new file mode 100644 index 00000000000..2eaa0ec0a40 --- /dev/null +++ b/src/com/android/settings/network/SwitchToEuiccSubscriptionSidecar.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 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; + +import android.app.FragmentManager; +import android.app.PendingIntent; + +import com.android.settings.SidecarFragment; +import com.android.settings.network.telephony.EuiccOperationSidecar; + +/** A headless fragment encapsulating long-running eSIM enabling/disabling operations. */ +public class SwitchToEuiccSubscriptionSidecar extends EuiccOperationSidecar { + private static final String TAG = "SwitchToEuiccSubscriptionSidecar"; + private static final String ACTION_SWITCH_TO_SUBSCRIPTION = + "com.android.settings.network.switchToSubscription"; + + private PendingIntent mCallbackIntent; + + /** Returns a SwitchToEuiccSubscriptionSidecar sidecar instance. */ + public static SwitchToEuiccSubscriptionSidecar get(FragmentManager fm) { + return SidecarFragment.get( + fm, TAG, SwitchToEuiccSubscriptionSidecar.class, null /* args */); + } + + @Override + public String getReceiverAction() { + return ACTION_SWITCH_TO_SUBSCRIPTION; + } + + /** Returns the pendingIntent of the eSIM operations. */ + public PendingIntent getCallbackIntent() { + return mCallbackIntent; + } + + /** Starts calling EuiccManager#switchToSubscription to enable/disable the eSIM profile. */ + public void run(int subscriptionId) { + setState(State.RUNNING, Substate.UNUSED); + mCallbackIntent = createCallbackIntent(); + mEuiccManager.switchToSubscription(subscriptionId, mCallbackIntent); + } +} diff --git a/src/com/android/settings/network/telephony/EuiccOperationSidecar.java b/src/com/android/settings/network/telephony/EuiccOperationSidecar.java new file mode 100644 index 00000000000..3bda9c57c7c --- /dev/null +++ b/src/com/android/settings/network/telephony/EuiccOperationSidecar.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2020 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.Manifest; +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.os.SystemClock; +import android.telephony.euicc.EuiccManager; +import android.util.Log; + +import com.android.settings.SidecarFragment; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * The sidecar base class that an Euicc sidecar can extend from. The extended class should implement + * getReceiverAction() to return the action string for the broadcast receiver. The extended class + * should implement its own get() function to return an instance of that class, and implement the + * functional class like run() to actually trigger the function in EuiccManager. + */ +public abstract class EuiccOperationSidecar extends SidecarFragment { + private static final String TAG = "EuiccOperationSidecar"; + private static final int REQUEST_CODE = 0; + private static final String EXTRA_OP_ID = "op_id"; + private static AtomicInteger sCurrentOpId = + new AtomicInteger((int) SystemClock.elapsedRealtime()); + + protected EuiccManager mEuiccManager; + + private int mResultCode; + private int mDetailedCode; + private Intent mResultIntent; + private int mOpId; + + protected final BroadcastReceiver mReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (getReceiverAction().equals(intent.getAction()) + && mOpId == intent.getIntExtra(EXTRA_OP_ID, -1)) { + mResultCode = getResultCode(); + /* TODO: This relies on our LUI and LPA to coexist, should think about how + to generalize this further. */ + mDetailedCode = + intent.getIntExtra( + EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, + 0 /* defaultValue*/); + mResultIntent = intent; + Log.i( + TAG, + String.format( + "Result code : %d; detailed code : %d", + mResultCode, mDetailedCode)); + onActionReceived(); + } + } + }; + + /** + * This is called when the broadcast action is received. The subclass may override this to + * perform different logic. The broadcast result code may be obtained with {@link + * #getResultCode()} and the Intent may be obtained with {@link #getResultIntent()}. + */ + protected void onActionReceived() { + if (mResultCode == EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) { + setState(State.SUCCESS, Substate.UNUSED); + } else { + setState(State.ERROR, mResultCode); + } + } + + /** + * The extended class should implement it to return a string for the broadcast action. The class + * should be unique across all the child classes. + */ + protected abstract String getReceiverAction(); + + protected PendingIntent createCallbackIntent() { + mOpId = sCurrentOpId.incrementAndGet(); + Intent intent = new Intent(getReceiverAction()); + intent.putExtra(EXTRA_OP_ID, mOpId); + return PendingIntent.getBroadcast( + getContext(), REQUEST_CODE, intent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mEuiccManager = (EuiccManager) getContext().getSystemService(Context.EUICC_SERVICE); + + getContext() + .getApplicationContext() + .registerReceiver( + mReceiver, + new IntentFilter(getReceiverAction()), + Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS, + null); + } + + @Override + public void onDestroy() { + getContext().getApplicationContext().unregisterReceiver(mReceiver); + super.onDestroy(); + } + + public int getResultCode() { + return mResultCode; + } + + public int getDetailedCode() { + return mDetailedCode; + } + + public Intent getResultIntent() { + return mResultIntent; + } +} diff --git a/src/com/android/settings/network/telephony/SubscriptionActionDialogActivity.java b/src/com/android/settings/network/telephony/SubscriptionActionDialogActivity.java new file mode 100644 index 00000000000..491a776afaa --- /dev/null +++ b/src/com/android/settings/network/telephony/SubscriptionActionDialogActivity.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 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.R; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; + +/** The base class for subscription action dialogs */ +public class SubscriptionActionDialogActivity extends Activity { + + private static final String TAG = "SubscriptionActionDialogActivity"; + + private ProgressDialog mProgressDialog; + private AlertDialog mErrorDialog; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** + * Displays a loading dialog. + * + * @param message The string content should be displayed in the progress dialog. + */ + protected void showProgressDialog(String message) { + if (mProgressDialog == null) { + mProgressDialog = ProgressDialog.show(this, null, message); + mProgressDialog.setCanceledOnTouchOutside(false); + mProgressDialog.setCancelable(false); + } + mProgressDialog.setMessage(message); + mProgressDialog.show(); + } + + /** Dismisses the loading dialog. */ + protected void dismissProgressDialog() { + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + } + } + + /** + * Displays an error dialog to indicate the subscription action failure. + * + * @param title The title of the error dialog. + * @param message The body text of the error dialog. + * @param positiveOnClickListener The callback function after users confirm with the error. + */ + protected void showErrorDialog( + String title, String message, DialogInterface.OnClickListener positiveOnClickListener) { + if (mErrorDialog == null) { + mErrorDialog = + new AlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setPositiveButton( + R.string.ok, + (dialog, which) -> { + positiveOnClickListener.onClick(dialog, which); + dismissErrorDialog(); + }) + .create(); + } + mErrorDialog.setMessage(message); + mErrorDialog.show(); + } + + /** Dismisses the error dialog. */ + protected void dismissErrorDialog() { + if (mErrorDialog != null) { + mErrorDialog.dismiss(); + } + } +} diff --git a/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java b/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java new file mode 100644 index 00000000000..062c9846b2a --- /dev/null +++ b/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2020 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.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.SidecarFragment; +import com.android.settings.network.SubscriptionUtil; +import com.android.settings.network.SwitchToEuiccSubscriptionSidecar; + +/** This dialog activity handles both eSIM and pSIM subscriptions enabling and disabling. */ +public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogActivity + implements SidecarFragment.Listener { + + private static final String TAG = "ToggleSubscriptionDialogActivity"; + + private static final String ARG_SUB_ID = "sub_id"; + private static final String ARG_enable = "enable"; + + /** + * Returns an intent of ToggleSubscriptionDialogActivity. + * @param context The context used to start the ToggleSubscriptionDialogActivity. + * @param subId The subscription ID of the subscription needs to be toggled. + * @param enable Whether the activity should enable or disable the subscription. + */ + public static Intent getIntent(Context context, int subId, boolean enable) { + Intent intent = new Intent(context, ToggleSubscriptionDialogActivity.class); + intent.putExtra(ARG_SUB_ID, subId); + intent.putExtra(ARG_enable, enable); + return intent; + } + + private SubscriptionManager mSubscriptionManager; + private SubscriptionInfo mSubInfo; + private SwitchToEuiccSubscriptionSidecar mSwitchToEuiccSubscriptionSidecar; + private AlertDialog mToggleSimConfirmDialog; + private boolean mEnable; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + int subId = intent.getIntExtra(ARG_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID); + mSubscriptionManager = getSystemService(SubscriptionManager.class); + + UserManager userManager = getSystemService(UserManager.class); + if (!userManager.isAdminUser()) { + Log.e(TAG, "It is not the admin user. Unable to toggle subscription."); + finish(); + return; + } + + if (!SubscriptionManager.isUsableSubscriptionId(subId)) { + Log.e(TAG, "The subscription id is not usable."); + finish(); + return; + } + + mSubInfo = SubscriptionUtil.getSubById(mSubscriptionManager, subId); + mSwitchToEuiccSubscriptionSidecar = + SwitchToEuiccSubscriptionSidecar.get(getFragmentManager()); + mEnable = intent.getBooleanExtra(ARG_enable, true); + + if (mEnable) { + handleEnablingSubAction(); + } else { + handleDisablingSubAction(); + } + } + + @Override + protected void onResume() { + super.onResume(); + mSwitchToEuiccSubscriptionSidecar.addListener(this); + } + + @Override + protected void onPause() { + mSwitchToEuiccSubscriptionSidecar.removeListener(this); + super.onPause(); + } + + @Override + public void onStateChange(SidecarFragment fragment) { + if (fragment == mSwitchToEuiccSubscriptionSidecar) { + handleSwitchToEuiccSubscriptionSidecarStateChange(); + } + } + + private void handleSwitchToEuiccSubscriptionSidecarStateChange() { + switch (mSwitchToEuiccSubscriptionSidecar.getState()) { + case SidecarFragment.State.SUCCESS: + Log.i( + TAG, + String.format( + "Successfully %s the eSIM profile.", + mEnable ? "enable" : "disable")); + mSwitchToEuiccSubscriptionSidecar.reset(); + dismissProgressDialog(); + finish(); + break; + case SidecarFragment.State.ERROR: + Log.i( + TAG, + String.format( + "Failed to %s the eSIM profile.", mEnable ? "enable" : "disable")); + mSwitchToEuiccSubscriptionSidecar.reset(); + dismissProgressDialog(); + showErrorDialog( + getString(R.string.privileged_action_disable_fail_title), + getString(R.string.privileged_action_disable_fail_text), + (dialog, which) -> finish()); + break; + } + } + + /* Handles the enabling SIM action. */ + private void handleEnablingSubAction() { + Log.i(TAG, "handleEnableSub"); + // TODO(b/160819390): Implement enabling eSIM/pSIM profile. + } + + /* Handles the disabling SIM action. */ + private void handleDisablingSubAction() { + showToggleSimConfirmDialog( + (dialog, which) -> { + if (mSubInfo.isEmbedded()) { + Log.i(TAG, "Disabling the eSIM profile."); + showProgressDialog( + getString(R.string.privileged_action_disable_sub_dialog_progress)); + mSwitchToEuiccSubscriptionSidecar.run( + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + return; + } + Log.i(TAG, "Disabling the pSIM profile."); + // TODO(b/160819390): Implement disabling pSIM profile. + }); + } + + /* Displays the SIM toggling confirmation dialog. */ + private void showToggleSimConfirmDialog( + DialogInterface.OnClickListener positiveOnClickListener) { + if (mToggleSimConfirmDialog == null) { + mToggleSimConfirmDialog = + new AlertDialog.Builder(this) + .setTitle(getToggleSimConfirmDialogTitle()) + .setPositiveButton( + R.string.yes, + (dialog, which) -> { + positiveOnClickListener.onClick(dialog, which); + dismissToggleSimConfirmDialog(); + }) + .setNegativeButton( + R.string.cancel, + (dialog, which) -> { + dismissToggleSimConfirmDialog(); + finish(); + }) + .create(); + } + mToggleSimConfirmDialog.show(); + } + + /* Dismisses the SIM toggling confirmation dialog. */ + private void dismissToggleSimConfirmDialog() { + if (mToggleSimConfirmDialog != null) { + mToggleSimConfirmDialog.dismiss(); + } + } + + /* Returns the title of toggling SIM confirmation dialog. */ + private String getToggleSimConfirmDialogTitle() { + if (mEnable) { + // TODO(b/160819390): Handle the case for enabling SIM. + return null; + } + return mSubInfo == null || TextUtils.isEmpty(mSubInfo.getDisplayName()) + ? getString(R.string.privileged_action_disable_sub_dialog_title_without_carrier) + : getString( + R.string.privileged_action_disable_sub_dialog_title, + mSubInfo.getDisplayName()); + } +}