refactor: Emit structured profile info from the LPA library

This commit is contained in:
Peter Cai 2022-05-03 10:11:34 -04:00
parent 5894dc9a71
commit 4b29660ef2
8 changed files with 190 additions and 127 deletions

View file

@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.truphone.lpa.LocalProfileInfo
import com.truphone.lpa.impl.ProfileKey.* import com.truphone.lpa.impl.ProfileKey.*
import com.truphone.lpad.progress.Progress import com.truphone.lpad.progress.Progress
import im.angry.openeuicc.R import im.angry.openeuicc.R
@ -78,7 +79,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
adapter.profiles = profiles.filter { it[PROFILE_CLASS.name] != "0" } adapter.profiles = profiles.filter { it.profileClass != LocalProfileInfo.Clazz.Testing }
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
binding.swipeRefresh.isRefreshing = false binding.swipeRefresh.isRefreshing = false
} }
@ -134,9 +135,9 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
binding.profileMenu.setOnClickListener { showOptionsMenu() } binding.profileMenu.setOnClickListener { showOptionsMenu() }
} }
private lateinit var profile: Map<String, String> private lateinit var profile: LocalProfileInfo
fun setProfile(profile: Map<String, String>) { fun setProfile(profile: LocalProfileInfo) {
this.profile = profile this.profile = profile
binding.name.text = getName() binding.name.text = getName()
@ -147,20 +148,18 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
R.string.disabled R.string.disabled
} }
) )
binding.provider.text = profile[PROVIDER_NAME.name] binding.provider.text = profile.providerName
binding.iccid.text = profile[ICCID_LITTLE.name]!! binding.iccid.text = profile.iccidLittleEndian
binding.iccid.transformationMethod = PasswordTransformationMethod.getInstance() binding.iccid.transformationMethod = PasswordTransformationMethod.getInstance()
} }
private fun isEnabled(): Boolean = private fun isEnabled(): Boolean =
profile[STATE.name]?.lowercase() == "enabled" profile.state == LocalProfileInfo.State.Enabled
private fun getName(): String = private fun getName(): String =
if (profile[NICKNAME.name].isNullOrEmpty()) { profile.nickName.ifEmpty {
profile[NAME.name] profile.name
} else { }
profile[NICKNAME.name]
}!!
private fun showOptionsMenu() { private fun showOptionsMenu() {
PopupMenu(binding.root.context, binding.profileMenu).apply { PopupMenu(binding.root.context, binding.profileMenu).apply {
@ -179,20 +178,20 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
private fun onMenuItemClicked(item: MenuItem): Boolean = private fun onMenuItemClicked(item: MenuItem): Boolean =
when (item.itemId) { when (item.itemId) {
R.id.enable -> { R.id.enable -> {
enableOrDisableProfile(profile[ICCID.name]!!, true) enableOrDisableProfile(profile.iccid, true)
true true
} }
R.id.disable -> { R.id.disable -> {
enableOrDisableProfile(profile[ICCID.name]!!, false) enableOrDisableProfile(profile.iccid, false)
true true
} }
R.id.rename -> { R.id.rename -> {
ProfileRenameFragment.newInstance(slotId, profile[ICCID.name]!!, getName()) ProfileRenameFragment.newInstance(slotId, profile.iccid, getName())
.show(childFragmentManager, ProfileRenameFragment.TAG) .show(childFragmentManager, ProfileRenameFragment.TAG)
true true
} }
R.id.delete -> { R.id.delete -> {
ProfileDeleteFragment.newInstance(slotId, profile[ICCID.name]!!, getName()) ProfileDeleteFragment.newInstance(slotId, profile.iccid, getName())
.show(childFragmentManager, ProfileDeleteFragment.TAG) .show(childFragmentManager, ProfileDeleteFragment.TAG)
true true
} }
@ -200,7 +199,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
} }
} }
inner class EuiccProfileAdapter(var profiles: List<Map<String, String>>) : RecyclerView.Adapter<ViewHolder>() { inner class EuiccProfileAdapter(var profiles: List<LocalProfileInfo>) : RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = val binding =
EuiccProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false) EuiccProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false)

View file

@ -3,6 +3,7 @@ plugins {
id 'com.android.application' version '7.1.3' apply false id 'com.android.application' version '7.1.3' apply false
id 'com.android.library' version '7.1.3' apply false id 'com.android.library' version '7.1.3' apply false
id 'org.jetbrains.kotlin.android' version '1.5.30' apply false id 'org.jetbrains.kotlin.android' version '1.5.30' apply false
id 'org.jetbrains.kotlin.multiplatform' version '1.6.21' apply false
} }
task clean(type: Delete) { task clean(type: Delete) {

View file

@ -1,4 +1,5 @@
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'kotlin'
configurations { configurations {
tool tool
@ -27,6 +28,7 @@ task genAsn1(type: JavaExec) {
} }
compileJava.dependsOn genAsn1 compileJava.dependsOn genAsn1
compileKotlin.dependsOn genAsn1
description = 'LPAd SM-DP+ Connector' description = 'LPAd SM-DP+ Connector'

View file

@ -21,7 +21,7 @@ public interface LocalProfileAssistant {
void downloadProfile(String matchingId, DownloadProgress progress) throws Exception; void downloadProfile(String matchingId, DownloadProgress progress) throws Exception;
List<Map<String, String>> getProfiles(); List<LocalProfileInfo> getProfiles();
/** /**
* Gets the EID from the eUICC * Gets the EID from the eUICC
@ -38,5 +38,6 @@ public interface LocalProfileAssistant {
void processPendingNotifications(); void processPendingNotifications();
boolean setNickname(String iccid, String nickname); boolean setNickname(String iccid, String nickname
);
} }

View file

@ -0,0 +1,69 @@
/*
* Copyright 2022 Peter Cai & Pierre-Hugues Husson
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation, version 2.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.truphone.lpa
import java.lang.IllegalArgumentException
import java.lang.StringBuilder
data class LocalProfileInfo(
val iccid: String,
val state: State,
val name: String,
val nickName: String,
val providerName: String,
val isdpAID: String,
val profileClass: Clazz
) {
val iccidLittleEndian by lazy {
iccidBigToLittle(iccid)
}
enum class State {
Enabled,
Disabled
}
enum class Clazz {
Testing,
Provisioning,
Operational
}
companion object {
fun stateFromString(state: String?): State =
if (state == "0") {
State.Disabled
} else {
State.Enabled
}
fun classFromString(clazz: String?): Clazz =
when (clazz) {
"0" -> Clazz.Testing
"1" -> Clazz.Provisioning
"2" -> Clazz.Operational
else -> throw IllegalArgumentException("Unknown profile class $clazz")
}
private fun iccidBigToLittle(iccid: String): String {
val builder = StringBuilder()
for (i in 0 until iccid.length / 2) {
builder.append(iccid[i * 2 + 1])
if (iccid[i * 2] != 'F') builder.append(iccid[i * 2])
}
return builder.toString()
}
}
}

View file

@ -1,107 +0,0 @@
package com.truphone.lpa.impl;
import com.truphone.rsp.dto.asn1.rspdefinitions.ProfileInfo;
import com.truphone.rsp.dto.asn1.rspdefinitions.ProfileInfoListResponse;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import com.truphone.lpa.ApduChannel;
import com.truphone.lpa.apdu.ApduUtils;
import com.truphone.util.LogStub;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
class ListProfilesWorker {
private static final Logger LOG = Logger.getLogger(ListProfilesWorker.class.getName());
private final ApduChannel apduChannel;
ListProfilesWorker(ApduChannel apduChannel) {
this.apduChannel = apduChannel;
}
List<Map<String, String>> run() {
String profilesInfo = getProfileInfoListResponse();
ProfileInfoListResponse profiles = new ProfileInfoListResponse();
List<Map<String, String>> profileList = new ArrayList<>();
try {
decodeProfiles(profilesInfo, profiles);
for (ProfileInfo info : profiles.getProfileInfoListOk().getProfileInfo()) {
Map<String, String> profileMap = new HashMap<>();
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.ICCID_LITTLE.name(), iccidBigToLittle(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():"");
profileMap.put(ProfileKey.PROFILE_STATE.name(), info.getProfileState().toString());
profileList.add(profileMap);
}
if (LogStub.getInstance().isDebugEnabled()) {
LogStub.getInstance().logDebug (LOG, LogStub.getInstance().getTag() + " - getProfiles - returning: " + profileList.toString());
}
return profileList;
} catch (DecoderException e) {
LOG.log(Level.SEVERE, LogStub.getInstance().getTag() + " - " + e.getMessage(), e);
LOG.log(Level.SEVERE, LogStub.getInstance().getTag() + " - Unable to retrieve profiles. Exception in Decoder:" + e.getMessage());
throw new RuntimeException("Unable to retrieve profiles");
} catch (IOException ioe) {
LOG.log(Level.SEVERE, LogStub.getInstance().getTag() + " - " + ioe.getMessage(), ioe);
throw new RuntimeException("Unable to retrieve profiles");
}
}
private void decodeProfiles(String profilesInfo, ProfileInfoListResponse profiles) throws DecoderException, IOException {
InputStream is = new ByteArrayInputStream(Hex.decodeHex(profilesInfo.toCharArray()));
profiles.decode(is);
if (LogStub.getInstance().isDebugEnabled()) {
LogStub.getInstance().logDebug (LOG,"Profile list object: " + profiles.toString());
}
}
private String getProfileInfoListResponse() {
if (LogStub.getInstance().isDebugEnabled()) {
LogStub.getInstance().logDebug (LOG, LogStub.getInstance().getTag() + " - Getting Profiles");
}
String apdu = ApduUtils.getProfilesInfoApdu(null);
if (LogStub.getInstance().isDebugEnabled()) {
LogStub.getInstance().logDebug (LOG,"List profiles APDU: " + apdu);
}
return apduChannel.transmitAPDU(apdu);
}
private String iccidBigToLittle(String iccid) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < iccid.length() / 2; i++) {
builder.append(iccid.charAt(i * 2 + 1));
if (iccid.charAt(i * 2) != 'F')
builder.append(iccid.charAt(i * 2));
}
return builder.toString();
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright 2022 Peter Cai & Pierre-Hugues Husson
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation, version 2.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.truphone.lpa.impl
import com.truphone.lpa.ApduChannel
import com.truphone.lpa.LocalProfileInfo
import com.truphone.rsp.dto.asn1.rspdefinitions.ProfileInfoListResponse
import com.truphone.util.LogStub
import org.apache.commons.codec.DecoderException
import org.apache.commons.codec.binary.Hex
import com.truphone.lpa.apdu.ApduUtils
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.lang.RuntimeException
import java.util.logging.Level
import java.util.logging.Logger
internal class ListProfilesWorker(private val apduChannel: ApduChannel) {
fun run(): List<LocalProfileInfo> {
val profilesInfo = profileInfoListResponse
val profiles = ProfileInfoListResponse()
val profileList: MutableList<LocalProfileInfo> = mutableListOf()
return try {
decodeProfiles(profilesInfo, profiles)
for (info in profiles.profileInfoListOk.profileInfo) {
profileList.add(
LocalProfileInfo(
iccid = info.iccid.toString(),
state = LocalProfileInfo.stateFromString(info.profileState?.toString()),
name = info.profileName?.toString() ?: "",
nickName = info.profileNickname?.toString() ?: "",
providerName = info.serviceProviderName?.toString() ?: "",
isdpAID = info.isdpAid?.toString() ?: "",
profileClass = LocalProfileInfo.classFromString(info.profileClass?.toString())
)
)
}
if (LogStub.getInstance().isDebugEnabled) {
LogStub.getInstance().logDebug(
LOG,
LogStub.getInstance().tag + " - getProfiles - returning: " + profileList.toString()
)
}
profileList
} catch (e: DecoderException) {
LOG.log(Level.SEVERE, LogStub.getInstance().tag + " - " + e.message, e)
LOG.log(
Level.SEVERE,
LogStub.getInstance().tag + " - Unable to retrieve profiles. Exception in Decoder:" + e.message
)
throw RuntimeException("Unable to retrieve profiles")
} catch (ioe: IOException) {
LOG.log(Level.SEVERE, LogStub.getInstance().tag + " - " + ioe.message, ioe)
throw RuntimeException("Unable to retrieve profiles")
}
}
@Throws(DecoderException::class, IOException::class)
private fun decodeProfiles(profilesInfo: String, profiles: ProfileInfoListResponse) {
val `is`: InputStream = ByteArrayInputStream(Hex.decodeHex(profilesInfo.toCharArray()))
profiles.decode(`is`)
if (LogStub.getInstance().isDebugEnabled) {
LogStub.getInstance().logDebug(LOG, "Profile list object: $profiles")
}
}
private val profileInfoListResponse: String
get() {
if (LogStub.getInstance().isDebugEnabled) {
LogStub.getInstance()
.logDebug(LOG, LogStub.getInstance().tag + " - Getting Profiles")
}
val apdu = ApduUtils.getProfilesInfoApdu(null)
if (LogStub.getInstance().isDebugEnabled) {
LogStub.getInstance().logDebug(LOG, "List profiles APDU: $apdu")
}
return apduChannel.transmitAPDU(apdu)
}
companion object {
private val LOG = Logger.getLogger(
ListProfilesWorker::class.java.name
)
}
}

View file

@ -3,6 +3,7 @@ package com.truphone.lpa.impl;
import com.truphone.es9plus.Es9PlusImpl; import com.truphone.es9plus.Es9PlusImpl;
import com.truphone.lpa.ApduChannel; import com.truphone.lpa.ApduChannel;
import com.truphone.lpa.LocalProfileAssistant; import com.truphone.lpa.LocalProfileAssistant;
import com.truphone.lpa.LocalProfileInfo;
import com.truphone.lpa.apdu.ApduUtils; import com.truphone.lpa.apdu.ApduUtils;
import com.truphone.lpa.apdu.NotificationType; import com.truphone.lpa.apdu.NotificationType;
import com.truphone.lpa.progress.DownloadProgress; import com.truphone.lpa.progress.DownloadProgress;
@ -90,8 +91,7 @@ public class LocalProfileAssistantImpl implements LocalProfileAssistant {
} }
@Override @Override
public List<Map<String, String>> getProfiles() { public List<LocalProfileInfo> getProfiles() {
return new ListProfilesWorker(apduChannel).run(); return new ListProfilesWorker(apduChannel).run();
} }