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.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.truphone.lpa.LocalProfileInfo
import com.truphone.lpa.impl.ProfileKey.*
import com.truphone.lpad.progress.Progress
import im.angry.openeuicc.R
@ -78,7 +79,7 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
}
withContext(Dispatchers.Main) {
adapter.profiles = profiles.filter { it[PROFILE_CLASS.name] != "0" }
adapter.profiles = profiles.filter { it.profileClass != LocalProfileInfo.Clazz.Testing }
adapter.notifyDataSetChanged()
binding.swipeRefresh.isRefreshing = false
}
@ -134,9 +135,9 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
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
binding.name.text = getName()
@ -147,20 +148,18 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
R.string.disabled
}
)
binding.provider.text = profile[PROVIDER_NAME.name]
binding.iccid.text = profile[ICCID_LITTLE.name]!!
binding.provider.text = profile.providerName
binding.iccid.text = profile.iccidLittleEndian
binding.iccid.transformationMethod = PasswordTransformationMethod.getInstance()
}
private fun isEnabled(): Boolean =
profile[STATE.name]?.lowercase() == "enabled"
profile.state == LocalProfileInfo.State.Enabled
private fun getName(): String =
if (profile[NICKNAME.name].isNullOrEmpty()) {
profile[NAME.name]
} else {
profile[NICKNAME.name]
}!!
profile.nickName.ifEmpty {
profile.name
}
private fun showOptionsMenu() {
PopupMenu(binding.root.context, binding.profileMenu).apply {
@ -179,20 +178,20 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesCh
private fun onMenuItemClicked(item: MenuItem): Boolean =
when (item.itemId) {
R.id.enable -> {
enableOrDisableProfile(profile[ICCID.name]!!, true)
enableOrDisableProfile(profile.iccid, true)
true
}
R.id.disable -> {
enableOrDisableProfile(profile[ICCID.name]!!, false)
enableOrDisableProfile(profile.iccid, false)
true
}
R.id.rename -> {
ProfileRenameFragment.newInstance(slotId, profile[ICCID.name]!!, getName())
ProfileRenameFragment.newInstance(slotId, profile.iccid, getName())
.show(childFragmentManager, ProfileRenameFragment.TAG)
true
}
R.id.delete -> {
ProfileDeleteFragment.newInstance(slotId, profile[ICCID.name]!!, getName())
ProfileDeleteFragment.newInstance(slotId, profile.iccid, getName())
.show(childFragmentManager, ProfileDeleteFragment.TAG)
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 {
val binding =
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.library' version '7.1.3' 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) {

View File

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

View File

@ -21,7 +21,7 @@ public interface LocalProfileAssistant {
void downloadProfile(String matchingId, DownloadProgress progress) throws Exception;
List<Map<String, String>> getProfiles();
List<LocalProfileInfo> getProfiles();
/**
* Gets the EID from the eUICC
@ -38,5 +38,6 @@ public interface LocalProfileAssistant {
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.lpa.ApduChannel;
import com.truphone.lpa.LocalProfileAssistant;
import com.truphone.lpa.LocalProfileInfo;
import com.truphone.lpa.apdu.ApduUtils;
import com.truphone.lpa.apdu.NotificationType;
import com.truphone.lpa.progress.DownloadProgress;
@ -90,8 +91,7 @@ public class LocalProfileAssistantImpl implements LocalProfileAssistant {
}
@Override
public List<Map<String, String>> getProfiles() {
public List<LocalProfileInfo> getProfiles() {
return new ListProfilesWorker(apduChannel).run();
}