瀏覽代碼

Implement profile renaming

Peter Cai 3 年之前
父節點
當前提交
715f19166f

+ 2 - 0
.idea/misc.xml

@@ -8,8 +8,10 @@
         <entry key="app/src/main/res/layout/euicc_profile.xml" value="0.19375" />
         <entry key="app/src/main/res/layout/fragment_euicc.xml" value="0.19375" />
         <entry key="app/src/main/res/layout/fragment_profile_download.xml" value="0.19375" />
+        <entry key="app/src/main/res/layout/fragment_profile_rename.xml" value="0.19375" />
         <entry key="app/src/main/res/menu/activity_main_slot_spinner.xml" value="0.19375" />
         <entry key="app/src/main/res/menu/fragment_profile_download.xml" value="0.19375" />
+        <entry key="app/src/main/res/menu/fragment_profile_rename.xml" value="0.19375" />
         <entry key="app/src/main/res/menu/profile_options.xml" value="0.19375" />
       </map>
     </option>

+ 14 - 6
app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt

@@ -128,12 +128,8 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
 
         fun setProfile(profile: Map<String, String>) {
             this.profile = profile
-            binding.name.text =
-                if (profile[NICKNAME.name].isNullOrEmpty()) {
-                    profile[NAME.name]
-                } else {
-                    profile[NICKNAME.name]
-                }
+            binding.name.text = getName()
+
             binding.state.setText(
                 if (isEnabled()) {
                     R.string.enabled
@@ -149,6 +145,13 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
         private fun isEnabled(): Boolean =
             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 {
                 setOnMenuItemClickListener(::onMenuItemClicked)
@@ -166,6 +169,11 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
                     enableProfile(profile[ICCID.name]!!)
                     true
                 }
+                R.id.rename -> {
+                    ProfileRenameFragment.newInstance(slotId, profile[ICCID.name]!!, getName())
+                        .show(childFragmentManager, ProfileRenameFragment.TAG)
+                    true
+                }
                 else -> false
             }
     }

+ 109 - 0
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")
+        }
+    }
+}

+ 60 - 0
app/src/main/res/layout/fragment_profile_rename.xml

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.appcompat.widget.Toolbar
+        android:id="@+id/toolbar"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:theme="@style/Theme.OpenEUICC"
+        android:background="?attr/colorPrimary"
+        android:elevation="4dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintWidth_percent="1"
+        app:navigationIcon="?homeAsUpIndicator" />
+
+    <View
+        android:id="@+id/guideline"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:orientation="vertical"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toBottomOf="@id/toolbar"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <ProgressBar
+        android:id="@+id/progress"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:layout_constraintTop_toBottomOf="@id/toolbar"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/guideline"
+        style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
+
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/profile_rename_new_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginVertical="15dp"
+        android:hint="@string/profile_rename_new_name"
+        style="@style/Widget.OpenEUICC.Input"
+        app:layout_constraintTop_toBottomOf="@id/toolbar"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintWidth_percent=".8">
+
+        <com.google.android.material.textfield.TextInputEditText
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:theme="@style/Theme.OpenEUICC.Input.Cursor"/>
+
+    </com.google.android.material.textfield.TextInputLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 9 - 0
app/src/main/res/menu/fragment_profile_rename.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item
+        android:id="@+id/ok"
+        android:icon="@drawable/ic_check_black"
+        android:title="@string/rename"
+        app:showAsAction="ifRoom"/>
+</menu>

+ 4 - 0
app/src/main/res/menu/profile_options.xml

@@ -4,6 +4,10 @@
         android:id="@+id/enable"
         android:title="@string/enable"/>
 
+    <item
+        android:id="@+id/rename"
+        android:title="@string/rename"/>
+
     <item
         android:id="@+id/delete"
         android:title="@string/delete"/>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -10,6 +10,7 @@
 
     <string name="enable">Enable</string>
     <string name="delete">Delete</string>
+    <string name="rename">Rename</string>
 
     <string name="toast_profile_enabled">eSIM profile switched. Please wait for a while when the card is restarting.</string>
     <string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
@@ -20,4 +21,6 @@
     <string name="profile_download_scan">Scan QR Code</string>
     <string name="profile_download_ok">Download</string>
     <string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</string>
+
+    <string name="profile_rename_new_name">New nickname</string>
 </resources>

+ 2 - 0
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);
 }

+ 5 - 0
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();
     }

+ 67 - 0
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);
+        }
+    }
+}