diff --git a/.idea/misc.xml b/.idea/misc.xml index 9c06351..2008b36 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,8 +8,10 @@ + + diff --git a/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt index 106b7ef..4d21fdd 100644 --- a/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt +++ b/app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt @@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.truphone.lpa.impl.ProfileKey.* import com.truphone.lpad.progress.Progress import im.angry.openeuicc.R import im.angry.openeuicc.databinding.EuiccProfileBinding @@ -77,7 +78,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh } withContext(Dispatchers.Main) { - adapter.profiles = profiles.filter { it["PROFILE_CLASS"] != "0" } + adapter.profiles = profiles.filter { it[PROFILE_CLASS.name] != "0" } adapter.notifyDataSetChanged() binding.swipeRefresh.isRefreshing = false } @@ -127,8 +128,8 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh fun setProfile(profile: Map) { this.profile = profile - // TODO: The library is not exposing the nicknames. Expose them so that we can do something here. - binding.name.text = profile["NAME"] + binding.name.text = getName() + binding.state.setText( if (isEnabled()) { R.string.enabled @@ -136,13 +137,20 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh R.string.disabled } ) - binding.provider.text = profile["PROVIDER_NAME"] - binding.iccid.text = profile["ICCID"] + binding.provider.text = profile[PROVIDER_NAME.name] + binding.iccid.text = profile[ICCID.name] binding.iccid.transformationMethod = PasswordTransformationMethod.getInstance() } private fun isEnabled(): Boolean = - profile["STATE"]?.lowercase() == "enabled" + profile[STATE.name]?.lowercase() == "enabled" + + private fun getName(): String = + if (profile[NICKNAME.name].isNullOrEmpty()) { + profile[NAME.name] + } else { + profile[NICKNAME.name] + }!! private fun showOptionsMenu() { PopupMenu(binding.root.context, binding.profileMenu).apply { @@ -158,7 +166,12 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh private fun onMenuItemClicked(item: MenuItem): Boolean = when (item.itemId) { R.id.enable -> { - enableProfile(profile["ICCID"]!!) + enableProfile(profile[ICCID.name]!!) + true + } + R.id.rename -> { + ProfileRenameFragment.newInstance(slotId, profile[ICCID.name]!!, getName()) + .show(childFragmentManager, ProfileRenameFragment.TAG) true } else -> false diff --git a/app/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt new file mode 100644 index 0000000..e068582 --- /dev/null +++ b/app/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt @@ -0,0 +1,109 @@ +package im.angry.openeuicc.ui + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import im.angry.openeuicc.R +import im.angry.openeuicc.databinding.FragmentProfileRenameBinding +import im.angry.openeuicc.util.setWidthPercent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.lang.Exception +import java.lang.RuntimeException + +class ProfileRenameFragment : DialogFragment(), EuiccFragmentMarker { + companion object { + const val TAG = "ProfileRenameFragment" + + fun newInstance(slotId: Int, iccid: String, currentName: String): ProfileRenameFragment { + val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId) + instance.requireArguments().apply { + putString("iccid", iccid) + putString("currentName", currentName) + } + return instance + } + } + + private var _binding: FragmentProfileRenameBinding? = null + private val binding get() = _binding!! + + private var renaming = false + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfileRenameBinding.inflate(inflater, container, false) + binding.toolbar.inflateMenu(R.menu.fragment_profile_rename) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.toolbar.apply { + setTitle(R.string.rename) + setNavigationOnClickListener { + if (!renaming) dismiss() + } + setOnMenuItemClickListener { + if (!renaming) rename() + true + } + } + } + + override fun onStart() { + super.onStart() + binding.profileRenameNewName.editText!!.setText(requireArguments().getString("currentName")) + } + + override fun onResume() { + super.onResume() + setWidthPercent(95) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).also { + it.window?.requestFeature(Window.FEATURE_NO_TITLE) + it.setCanceledOnTouchOutside(false) + } + } + + private fun rename() { + val name = binding.profileRenameNewName.editText!!.text.toString().trim() + + renaming = true + binding.progress.isIndeterminate = true + binding.progress.visibility = View.VISIBLE + + lifecycleScope.launch { + try { + doRename(name) + } catch (e: Exception) { + Log.d(TAG, "Failed to rename profile") + Log.d(TAG, Log.getStackTraceString(e)) + } finally { + if (parentFragment is EuiccProfilesChangedListener) { + (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() + } + dismiss() + } + } + } + + private suspend fun doRename(name: String) = withContext(Dispatchers.IO) { + if (!channel.lpa.setNickname(requireArguments().getString("iccid"), name)) { + throw RuntimeException("Profile nickname not changed") + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_profile_rename.xml b/app/src/main/res/layout/fragment_profile_rename.xml new file mode 100644 index 0000000..9ac391f --- /dev/null +++ b/app/src/main/res/layout/fragment_profile_rename.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_profile_rename.xml b/app/src/main/res/menu/fragment_profile_rename.xml new file mode 100644 index 0000000..bde850f --- /dev/null +++ b/app/src/main/res/menu/fragment_profile_rename.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/profile_options.xml b/app/src/main/res/menu/profile_options.xml index 7929308..658b4e4 100644 --- a/app/src/main/res/menu/profile_options.xml +++ b/app/src/main/res/menu/profile_options.xml @@ -4,6 +4,10 @@ android:id="@+id/enable" android:title="@string/enable"/> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1597d22..9197709 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,6 +10,7 @@ Enable Delete + Rename eSIM profile switched. Please wait for a while when the card is restarting. Cannot switch to new eSIM profile. @@ -20,4 +21,6 @@ Scan QR Code Download Failed to download eSIM. Check your activation / QR code. + + New nickname \ No newline at end of file diff --git a/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/LocalProfileAssistant.java b/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/LocalProfileAssistant.java index f61a7c3..7cdfda3 100644 --- a/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/LocalProfileAssistant.java +++ b/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/LocalProfileAssistant.java @@ -37,4 +37,6 @@ public interface LocalProfileAssistant { String allocateProfile(String mcc); void processPendingNotifications(); + + boolean setNickname(String iccid, String nickname); } \ No newline at end of file diff --git a/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/ListProfilesWorker.java b/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/ListProfilesWorker.java index 21b7d5c..546c18f 100644 --- a/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/ListProfilesWorker.java +++ b/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/ListProfilesWorker.java @@ -42,6 +42,7 @@ class ListProfilesWorker { profileMap.put(ProfileKey.STATE.name(), LocalProfileAssistantImpl.DISABLED_STATE.equals(info.getProfileState().toString()) ? "Disabled" : "Enabled"); profileMap.put(ProfileKey.ICCID.name(), info.getIccid().toString()); profileMap.put(ProfileKey.NAME.name(), (info.getProfileName()!=null)?info.getProfileName().toString():""); + profileMap.put(ProfileKey.NICKNAME.name(), (info.getProfileNickname()!=null)?info.getProfileNickname().toString():""); profileMap.put(ProfileKey.PROVIDER_NAME.name(), (info.getServiceProviderName()!=null)?info.getServiceProviderName().toString():""); profileMap.put(ProfileKey.ISDP_AID.name(), (info.getIsdpAid()!=null)?info.getIsdpAid().toString():""); profileMap.put(ProfileKey.PROFILE_CLASS.name(), (info.getProfileClass()!=null)?info.getProfileClass().toString():""); diff --git a/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/LocalProfileAssistantImpl.java b/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/LocalProfileAssistantImpl.java index a6c92a9..d221093 100644 --- a/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/LocalProfileAssistantImpl.java +++ b/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/LocalProfileAssistantImpl.java @@ -136,6 +136,11 @@ public class LocalProfileAssistantImpl implements LocalProfileAssistant { } + @Override + public boolean setNickname(String iccid, String nickname) { + return new SetNicknameWorker(iccid, nickname, apduChannel).run(); + } + public void smdsRetrieveEvents(Progress progress) { // return new SmdsRetrieveEvents(); } diff --git a/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/ProfileKey.java b/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/ProfileKey.java index 135e8cc..7409c80 100644 --- a/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/ProfileKey.java +++ b/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/ProfileKey.java @@ -4,9 +4,10 @@ public enum ProfileKey { ICCID, STATE, NAME, + NICKNAME, PROVIDER_NAME, ISDP_AID, PROFILE_CLASS, PROFILE_STATE - + } diff --git a/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/SetNicknameWorker.java b/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/SetNicknameWorker.java new file mode 100644 index 0000000..b4f04e1 --- /dev/null +++ b/libs/lpad-sm-dp-plus-connector/src/main/java/com/truphone/lpa/impl/SetNicknameWorker.java @@ -0,0 +1,67 @@ +package com.truphone.lpa.impl; + +import com.truphone.lpa.ApduChannel; +import com.truphone.lpa.apdu.ApduUtils; +import com.truphone.rsp.dto.asn1.rspdefinitions.SetNicknameResponse; +import com.truphone.util.LogStub; +import com.truphone.util.Util; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SetNicknameWorker { + private static final Logger LOG = Logger.getLogger(ListProfilesWorker.class.getName()); + + private final String iccid; + private final String nickname; + private final ApduChannel apduChannel; + + SetNicknameWorker(String iccid, String nickname, ApduChannel apduChannel) { + this.apduChannel = apduChannel; + this.iccid = iccid; + this.nickname = nickname; + } + + boolean run() { + if (LogStub.getInstance().isDebugEnabled()) { + LogStub.getInstance().logDebug(LOG, LogStub.getInstance().getTag() + " - Renaming profile: " + iccid); + } + + String apdu = ApduUtils.setNicknameApdu(iccid, Util.byteArrayToHexString(nickname.getBytes(), "")); + String eResponse = apduChannel.transmitAPDU(apdu); + + try { + InputStream is = new ByteArrayInputStream(Hex.decodeHex(eResponse.toCharArray())); + SetNicknameResponse response = new SetNicknameResponse(); + + response.decode(is); + + if ("0".equals(response.getSetNicknameResult().toString())) { + if (LogStub.getInstance().isDebugEnabled()) { + LogStub.getInstance().logDebug(LOG, LogStub.getInstance().getTag() + " - Profile renamed: " + iccid); + } + return true; + } else { + if (LogStub.getInstance().isDebugEnabled()) { + LogStub.getInstance().logDebug(LOG, LogStub.getInstance().getTag() + " - Profile not renamed: " + iccid); + } + return false; + } + } catch (IOException ioe) { + LOG.log(Level.SEVERE, LogStub.getInstance().getTag() + " - iccid: " + iccid + " profile failed to be renamed"); + + throw new RuntimeException("Unable to rename profile: " + iccid + ", response: " + eResponse); + } catch (DecoderException e) { + LOG.log(Level.SEVERE, LogStub.getInstance().getTag() + " - " + e.getMessage(), e); + LOG.log(Level.SEVERE, LogStub.getInstance().getTag() + " - iccid: " + iccid + " profile failed to be renamed. Exception in Decoder:" + e.getMessage()); + + throw new RuntimeException("Unable to rename profile: " + iccid + ", response: " + eResponse); + } + } +}