xiongzhu 11 luni în urmă
părinte
comite
61335b64c4

+ 11 - 11
app/src/main/java/com/example/uicceditor/ApduChannel.kt

@@ -5,13 +5,6 @@ import android.telephony.TelephonyManager
 import android.util.Log
 
 
-class ApduResponse(rawStr: String) {
-    val data: String = rawStr.substring(0, rawStr.length - 4)
-    val sw: String = rawStr.substring(rawStr.length - 4)
-    val sw1: String = sw.substring(0, 2)
-    val sw2: String = sw.substring(2)
-}
-
 class ApduChannel(private val telephonyManager: TelephonyManager, aid: String) {
     companion object {
         const val TAG = "APDU"
@@ -25,6 +18,13 @@ class ApduChannel(private val telephonyManager: TelephonyManager, aid: String) {
     private val channelId: Int
     private var closed: Boolean = false
 
+    class ApduResponse(rawStr: String) {
+        val data: String = rawStr.substring(0, rawStr.length - 4)
+        val sw: String = rawStr.substring(rawStr.length - 4)
+        val sw1: String = sw.substring(0, 2)
+        val sw2: String = sw.substring(2)
+    }
+
     init {
         val res = telephonyManager.iccOpenLogicalChannel(aid, 0x04)
         if (res.status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR) {
@@ -58,7 +58,7 @@ class ApduChannel(private val telephonyManager: TelephonyManager, aid: String) {
         if (res.sw != "9000") {
             throw Exception("Failed to read ICCID")
         }
-        return decICCID(res.data)
+        return SimEncoder.decICCID(res.data)
     }
 
     fun writeICCID(iccid: String) {
@@ -68,7 +68,7 @@ class ApduChannel(private val telephonyManager: TelephonyManager, aid: String) {
 //        }
         select(SIMView.FID_MF)
         select(SIMView.FID_EF_ICCID)
-        writeBinary(encICCID(iccid))
+        writeBinary(SimEncoder.encICCID(iccid))
     }
 
     fun readIMSI(): String {
@@ -76,11 +76,11 @@ class ApduChannel(private val telephonyManager: TelephonyManager, aid: String) {
         if (res.sw != "9000") {
             throw Exception("Failed to read IMSI")
         }
-        return decIMSI(res.data)!!
+        return SimEncoder.decIMSI(res.data)!!
     }
 
     fun writeIMSI(imsi: String) {
-        val res = execute(0x00, 0xE2, 0xA2, 0x00, 0x09, encIMSI(imsi))
+        val res = execute(0x00, 0xE2, 0xA2, 0x00, 0x09, SimEncoder.encIMSI(imsi))
         if (res.sw != "9000") {
             throw Exception("Failed to write IMSI")
         }

+ 12 - 21
app/src/main/java/com/example/uicceditor/EncodeException.kt

@@ -1,4 +1,4 @@
-package com.example.uicceditor;
+package com.example.uicceditor
 
 /*
  * Copyright (C) 2006 The Android Open Source Project
@@ -20,33 +20,24 @@ package com.example.uicceditor;
 /**
  * {@hide}
  */
-public class EncodeException extends Exception {
+class EncodeException : Exception {
+    var error: Int = ERROR_UNENCODABLE
+        private set
 
-    private int mError = ERROR_UNENCODABLE;
+    constructor() : super()
 
-    public static final int ERROR_UNENCODABLE = 0;
-    public static final int ERROR_EXCEED_SIZE = 1;
-
-    public EncodeException() {
-        super();
-    }
 
+    constructor(s: String?) : super(s)
 
-    public EncodeException(String s) {
-        super(s);
+    constructor(s: String?, error: Int) : super(s) {
+        this.error = error
     }
 
-    public EncodeException(String s, int error) {
-        super(s);
-        mError = error;
-    }
 
+    constructor(c: Char) : super("Unencodable char: '$c'")
 
-    public EncodeException(char c) {
-        super("Unencodable char: '" + c + "'");
-    }
-
-    public int getError() {
-        return mError;
+    companion object {
+        const val ERROR_UNENCODABLE: Int = 0
+        const val ERROR_EXCEED_SIZE: Int = 1
     }
 }

+ 0 - 241
app/src/main/java/com/example/uicceditor/Encoder.kt

@@ -1,241 +0,0 @@
-package com.example.uicceditor
-
-import android.util.Log
-import java.math.BigDecimal
-import java.math.RoundingMode
-
-fun String.stripF(): String {
-    return this.replace(Regex("^[fF]+"), "").replace(Regex("[fF]+$"), "")
-}
-
-fun String.swapNibbles(): String {
-    val sb = StringBuilder()
-    var i = 0
-    while (i < length) {
-        sb.append(this[i + 1]).append(this[i])
-        i += 2
-    }
-    return sb.toString()
-}
-
-fun asciiToHex(ascii: String): String {
-    return ascii.toByteArray().joinToString("") { "%02x".format(it) }
-}
-
-fun decIMSI(ef: String): String? {
-    if (ef.length < 4) {
-        return null
-    }
-    var l = ef.substring(0, 2).toInt(16) * 2
-    l -= 1
-    val swapped: String = ef.substring(2).swapNibbles().stripF()
-    if (swapped.isEmpty()) {
-        return null
-    }
-    val oe = (swapped[0].toString().toInt() shr 3) and 1
-    if (oe == 0) {
-        l -= 1
-    }
-    if (l != swapped.length - 1) {
-        return null
-    }
-    val imsi = swapped.substring(1)
-    return imsi
-}
-
-fun encIMSI(imsi: String): String {
-    val l = BigDecimal(((imsi.length + 1) / 2f).toDouble()).setScale(0, RoundingMode.HALF_UP)
-        .toInt()
-    val oe = imsi.length and 1
-    val ei =
-        String.format("%02x", l) +
-                (String.format("%01x", (oe shl 3) or 1) + imsi.padEnd(
-                    15,
-                    'f'
-                )).swapNibbles()
-    return ei
-}
-
-fun decICCID(ef: String): String {
-    return ef.swapNibbles().stripF()
-}
-
-fun encICCID(iccid: String): String {
-    return iccid.padEnd(20, 'f').swapNibbles()
-}
-
-/*
-* NPI (Numbering Plan Identification): This indicates the numbering plan used for the phone number. Common values include:
-    0x01: ISDN/telephony numbering plan (E.164/E.163)
-    0x02: Data numbering plan (X.121)
-    0x03: Telex numbering plan
-  ToN (Type of Number): This indicates the type of the phone number. Common values include:
-    0x00: Unknown
-    0x01: International number
-    0x02: National number
-    0x03: Network-specific number
-    0x04: Subscriber number
-    0x05: Alphanumeric (coded according to the GSM 7-bit default alphabet)
-    0x06: Abbreviated number
-* */
-fun decMSISDN(efMsisdn: String): String? {
-    println("ef_msisdn: $efMsisdn")
-    // Convert from String to ByteArray
-    val efMsisdnBytes = efMsisdn.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
-
-    // Make sure mandatory fields are present
-    if (efMsisdnBytes.size < 14) {
-        Log.e("DecMSISDN", "EF.MSISDN is too short")
-        return null
-    }
-
-    // Skip optional Alpha Identifier
-    val xlen = efMsisdnBytes.size - 14
-    val msisdnLhv = efMsisdnBytes.copyOfRange(xlen, efMsisdnBytes.size)
-
-    // Parse the length (in bytes) of the BCD encoded number
-    var bcdLen = msisdnLhv[0].toInt()
-    // BCD length = length of dial num (max. 10 bytes) + 1 byte ToN and NPI
-    if (bcdLen == 0xff) {
-        return null
-    } else if (bcdLen > 11 || bcdLen < 1) {
-        Log.e("DecMSISDN", "Length of MSISDN ($bcdLen bytes) is out of range")
-        return null
-    }
-
-    // Parse ToN / NPI
-    val ton = (msisdnLhv[1].toInt() shr 4) and 0x07
-    val npi = msisdnLhv[1].toInt() and 0x0f
-    bcdLen -= 1
-
-    // No MSISDN?
-    if (bcdLen == 0) {
-        return null
-    }
-
-    val msisdn =
-        msisdnLhv.copyOfRange(2, 2 + bcdLen).joinToString("") { "%02x".format(it) }
-            .swapNibbles()
-            .stripF()
-    // International number 10.5.118/3GPP TS 24.008
-    val formattedMsisdn = if (ton == 0x01) "+$msisdn" else msisdn
-    Log.i("DecMSISDN", "EF: $efMsisdn, MSISDN: $formattedMsisdn, NPI: $npi, ToN: $ton")
-    return formattedMsisdn
-}
-
-fun encMSISDN(msisdn: String, npi: Int = 0x01, ton: Int = 0x03): String {
-    // If no MSISDN is supplied then encode the file contents as all "ff"
-    if (msisdn.isEmpty() || msisdn == "+") {
-        return "ff".repeat(14)
-    }
-
-    var msisdnVar = msisdn
-    var tonVar = ton
-
-    // Leading '+' indicates International Number
-    if (msisdnVar[0] == '+') {
-        msisdnVar = msisdnVar.substring(1)
-        tonVar = 0x01
-    }
-
-    // An MSISDN must not exceed 20 digits
-    if (msisdnVar.length > 20) {
-        throw IllegalArgumentException("msisdn must not be longer than 20 digits")
-    }
-
-    // Append 'f' padding if number of digits is odd
-    if (msisdnVar.length % 2 > 0) {
-        msisdnVar += 'f'
-    }
-
-    // BCD length also includes NPI/ToN header
-    val bcdLen = msisdnVar.length / 2 + 1
-    val npiTon = (npi and 0x0f) or ((tonVar and 0x07) shl 4) or 0x80
-    val bcd = msisdnVar.swapNibbles().padEnd(10 * 2, 'f')  // pad to 10 octets
-
-    return String.format("%02x", bcdLen) + String.format("%02x", npiTon) + bcd + "ff".repeat(2)
-}
-
-fun decPLMN(efPlmn: String): String? {
-    if (efPlmn.length < 6) {
-        return null
-    }
-    return efPlmn.stripF().chunked(6).joinToString(",") {
-        val mcc = decMcc(it)
-        val mnc = decMnc(it)
-        "$mcc$mnc"
-    }
-}
-
-fun decPLMNwAcT(efPlmn: String): String? {
-    if (efPlmn.length < 10) {
-        return null
-    }
-    return efPlmn.chunked(10)
-        .filter { !it.substring(0, 6).matches(Regex("^f{3,}", RegexOption.IGNORE_CASE)) }
-        .joinToString(",") {
-            val mcc = decMcc(it)
-            val mnc = decMnc(it)
-            val act = it.substring(6)
-            "$mcc$mnc:$act"
-        }
-}
-
-fun encPLMN(plmn: String): String {
-    return plmn.split(",", ",").map { it.trim() }
-        .filter { it.matches(Regex("\\d{5,6}")) }
-        .joinToString("") { encMccMnc(it) }
-}
-
-fun encPLMNwAcT(plmn: String): String {
-    return plmn.split(",", ",").map { it.trim() }
-        .filter { it.matches(Regex("\\d{5,6}:\\d{4}")) }
-        .joinToString("") {
-            it.split(":")
-                .let { (plmn, act) ->
-                    "${encMccMnc(plmn)}${act}"
-                }
-        }
-}
-
-fun decMcc(plmn: String): String {
-    val digit1 = plmn[1]  // 1st byte, LSB
-    val digit2 = plmn[0]  // 1st byte, MSB
-    val digit3 = plmn[3]  // 2nd byte, LSB
-    val res = "$digit1$digit2$digit3"
-    return res.stripF()
-}
-
-fun decMnc(plmn: String): String {
-    val digit1 = plmn[5]  // 3rd byte, LSB
-    val digit2 = plmn[4]  // 3rd byte, MSB
-    val digit3 = plmn[2]  // 2nd byte, MSB
-    val res = "$digit1$digit2$digit3"
-    return res.stripF()
-}
-
-fun encMccMnc(mccmnc: String): String {
-    if (!mccmnc.matches(Regex("^\\d{5,6}$"))) {
-        throw IllegalArgumentException("mccmnc must be 5 or 6 digits")
-    }
-    // Make sure there are no excess whitespaces in the input parameters
-    var mccVar = mccmnc.substring(0, 3)
-    var mncVar = mccmnc.substring(3)
-
-    // Make sure that MCC/MNC are correctly padded with leading zeros or 'F', depending on the length
-    mncVar = when (mncVar.length) {
-        0 -> "FFF"
-        1 -> "0$mncVar" + "F"
-        2 -> mncVar + "F"
-        else -> mncVar
-    }
-
-    mccVar = when (mccVar.length) {
-        0 -> "FFF"
-        1 -> "00$mccVar"
-        2 -> "0$mccVar"
-        else -> mccVar
-    }
-
-    return "${mccVar[1]}${mccVar[0]}${mncVar[2]}${mccVar[2]}${mncVar[1]}${mncVar[0]}"
-}

+ 27 - 16
app/src/main/java/com/example/uicceditor/MainActivity.kt

@@ -100,19 +100,19 @@ class MainActivity : AppCompatActivity() {
             apduChannel.select(SIMView.FID_MF)
             apduChannel.select(SIMView.FID_DF_TELECOM)
             apduChannel.select(SIMView.FID_EF_MSISDN)
-            val msisdn = decMSISDN(apduChannel.readRecord(1, 28).substring(28)) ?: ""
+            val msisdn = SimEncoder.decMSISDN(apduChannel.readRecord(1, 28).substring(28)) ?: ""
             binding.etMsisdn.setText(msisdn)
             apduChannel.close()
         }
 
         binding.btnWriteMsisdn.setOnClickListener {
             val msisdn = binding.etMsisdn.text.toString()
-            Log.i(TAG, "MSISDN: ${encMSISDN(msisdn)}")
+            Log.i(TAG, "MSISDN: ${SimEncoder.encMSISDN(msisdn)}")
             val apduChannel = ApduChannel(telephonyManager, AID)
             apduChannel.select(SIMView.FID_MF)
             apduChannel.select(SIMView.FID_DF_TELECOM)
             apduChannel.select(SIMView.FID_EF_MSISDN)
-            apduChannel.writeRecord(1, encMSISDN(msisdn).padStart(56, 'F'))
+            apduChannel.writeRecord(1, SimEncoder.encMSISDN(msisdn).padStart(56, 'F'))
             apduChannel.close()
 
 //            val subscriptionManager =
@@ -129,7 +129,7 @@ class MainActivity : AppCompatActivity() {
                 0x01,
                 0x04,
                 0x1C,
-                encMSISDN(msisdn).padStart(56, 'F')
+                SimEncoder.encMSISDN(msisdn).padStart(56, 'F')
             )
             apduChannel.close()
         }
@@ -139,12 +139,12 @@ class MainActivity : AppCompatActivity() {
             apduChannel.select(SIMView.FID_MF)
             apduChannel.select(SIMView.FID_DF_GSM)
             apduChannel.select(SIMView.FID_EF_PLMNSEL)
-            binding.etPlmn.setText(decPLMN(apduChannel.readBinary(60)))
+            binding.etPlmn.setText(SimEncoder.decPLMN(apduChannel.readBinary(60)))
             apduChannel.close()
         }
 
         binding.btnWritePlmn.setOnClickListener {
-            val plmn = encPLMN(binding.etPlmn.text.toString())
+            val plmn = SimEncoder.encPLMN(binding.etPlmn.text.toString())
             Log.i(TAG, "btnWritePlmn: $plmn")
             val apduChannel = ApduChannel(telephonyManager, AID)
             apduChannel.select(SIMView.FID_MF)
@@ -161,16 +161,16 @@ class MainActivity : AppCompatActivity() {
                 Toast.makeText(this, "Invalid PLMN", Toast.LENGTH_SHORT).show()
                 return@setOnClickListener
             }
-            val plmnHex = encPLMN(plmn.joinToString(","))
+            val plmnHex = SimEncoder.encPLMN(plmn.joinToString(","))
             Log.i(TAG, "plmnHex: $plmnHex")
 
-            val plmnwactHex = encPLMNwAcT(plmn.flatMap {
+            val plmnwactHex = SimEncoder.encPLMNwAcT(plmn.flatMap {
                 listOf("${it}:4000,$it:8000,$it:0080")
             }.joinToString(","))
             Log.i(TAG, "plmnwactHex: $plmnwactHex")
 
             val fplmn =
-                encPLMN("46000,46001,46002,46006,46007,46011,46012,46015,46020")
+                SimEncoder.encPLMN("46000,46001,46002,46006,46007,46011,46012,46015,46020")
 
             val apduChannel = ApduChannel(telephonyManager, AID)
             apduChannel.select(SIMView.FID_MF)
@@ -211,7 +211,7 @@ class MainActivity : AppCompatActivity() {
             apduChannel.select(SIMView.FID_MF)
             apduChannel.select(SIMView.FID_DF_GSM)
             apduChannel.select(SIMView.FID_EF_EHPLMN)
-            binding.etEhplmn.setText(decPLMN(apduChannel.readBinary(12)))
+            binding.etEhplmn.setText(SimEncoder.decPLMN(apduChannel.readBinary(12)))
             apduChannel.close()
         }
 
@@ -220,12 +220,12 @@ class MainActivity : AppCompatActivity() {
             apduChannel.select(SIMView.FID_MF)
             apduChannel.select(SIMView.FID_DF_GSM)
             apduChannel.select(SIMView.FID_EF_FPLMN)
-            binding.etFplmn.setText(decPLMN(apduChannel.readBinary(30)))
+            binding.etFplmn.setText(SimEncoder.decPLMN(apduChannel.readBinary(30)))
             apduChannel.close()
         }
 
         binding.btnWriteFplmn.setOnClickListener {
-            val plmn = encPLMN(binding.etFplmn.text.toString())
+            val plmn = SimEncoder.encPLMN(binding.etFplmn.text.toString())
             Log.i(TAG, "btnWritePlmn: $plmn")
             val apduChannel = ApduChannel(telephonyManager, AID)
             apduChannel.select(SIMView.FID_MF)
@@ -240,7 +240,18 @@ class MainActivity : AppCompatActivity() {
             apduChannel.select(SIMView.FID_MF)
             apduChannel.select(SIMView.FID_DF_GSM)
             apduChannel.select(SIMView.FID_EF_PLMNWACT)
-            binding.etPlmnwact.setText(decPLMNwAcT(apduChannel.readBinary(120)))
+            binding.etPlmnwact.setText(SimEncoder.decPLMNwAcT(apduChannel.readBinary(120)))
+            apduChannel.close()
+        }
+
+        binding.btnWritePlmnwact.setOnClickListener {
+            val plmn = SimEncoder.encPLMNwAcT(binding.etPlmnwact.text.toString())
+            Log.i(TAG, "btnWritePlmn: $plmn")
+            val apduChannel = ApduChannel(telephonyManager, AID)
+            apduChannel.select(SIMView.FID_MF)
+            apduChannel.select(SIMView.FID_DF_GSM)
+            apduChannel.select(SIMView.FID_EF_PLMNWACT)
+            apduChannel.writeBinary(plmn.padEnd(240, 'f'))
             apduChannel.close()
         }
 
@@ -249,7 +260,7 @@ class MainActivity : AppCompatActivity() {
             apduChannel.select(SIMView.FID_MF)
             apduChannel.select(SIMView.FID_DF_GSM)
             apduChannel.select(SIMView.FID_EF_OPLMNWACT)
-            binding.etOplmnwact.setText(decPLMNwAcT(apduChannel.readBinary(60)))
+            binding.etOplmnwact.setText(SimEncoder.decPLMNwAcT(apduChannel.readBinary(60)))
             apduChannel.close()
         }
 
@@ -258,7 +269,7 @@ class MainActivity : AppCompatActivity() {
             apduChannel.select(SIMView.FID_MF)
             apduChannel.select(SIMView.FID_DF_GSM)
             apduChannel.select(SIMView.FID_EF_HPLMNWACT)
-            binding.etHplmnwact.setText(decPLMNwAcT(apduChannel.readBinary(20)))
+            binding.etHplmnwact.setText(SimEncoder.decPLMNwAcT(apduChannel.readBinary(20)))
             apduChannel.close()
         }
 
@@ -270,7 +281,7 @@ class MainActivity : AppCompatActivity() {
             val sender = binding.etSender.text.toString()
             val body = binding.etSms.text.toString()
             if (sender.isNotEmpty() && body.isNotEmpty()) {
-                val smsManager = (getSystemService("sms") as SmsManager).createForSubscriptionId(12)
+                val smsManager = (getSystemService("sms") as SmsManager)
                 val pendingIntent = PendingIntent.getBroadcast(
                     this,
                     1,

+ 244 - 0
app/src/main/java/com/example/uicceditor/SimEncoder.kt

@@ -0,0 +1,244 @@
+package com.example.uicceditor
+
+import android.util.Log
+import java.math.BigDecimal
+import java.math.RoundingMode
+
+object SimEncoder {
+    fun String.stripF(): String {
+        return this.replace(Regex("^[fF]+"), "").replace(Regex("[fF]+$"), "")
+    }
+
+    fun String.swapNibbles(): String {
+        val sb = StringBuilder()
+        var i = 0
+        while (i < length) {
+            sb.append(this[i + 1]).append(this[i])
+            i += 2
+        }
+        return sb.toString()
+    }
+
+    fun asciiToHex(ascii: String): String {
+        return ascii.toByteArray().joinToString("") { "%02x".format(it) }
+    }
+
+    fun decIMSI(ef: String): String? {
+        if (ef.length < 4) {
+            return null
+        }
+        var l = ef.substring(0, 2).toInt(16) * 2
+        l -= 1
+        val swapped: String = ef.substring(2).swapNibbles().stripF()
+        if (swapped.isEmpty()) {
+            return null
+        }
+        val oe = (swapped[0].toString().toInt() shr 3) and 1
+        if (oe == 0) {
+            l -= 1
+        }
+        if (l != swapped.length - 1) {
+            return null
+        }
+        val imsi = swapped.substring(1)
+        return imsi
+    }
+
+    fun encIMSI(imsi: String): String {
+        val l = BigDecimal(((imsi.length + 1) / 2f).toDouble()).setScale(0, RoundingMode.HALF_UP)
+            .toInt()
+        val oe = imsi.length and 1
+        val ei =
+            String.format("%02x", l) +
+                    (String.format("%01x", (oe shl 3) or 1) + imsi.padEnd(
+                        15,
+                        'f'
+                    )).swapNibbles()
+        return ei
+    }
+
+    fun decICCID(ef: String): String {
+        return ef.swapNibbles().stripF()
+    }
+
+    fun encICCID(iccid: String): String {
+        return iccid.padEnd(20, 'f').swapNibbles()
+    }
+
+    /*
+    * NPI (Numbering Plan Identification): This indicates the numbering plan used for the phone number. Common values include:
+        0x01: ISDN/telephony numbering plan (E.164/E.163)
+        0x02: Data numbering plan (X.121)
+        0x03: Telex numbering plan
+      ToN (Type of Number): This indicates the type of the phone number. Common values include:
+        0x00: Unknown
+        0x01: International number
+        0x02: National number
+        0x03: Network-specific number
+        0x04: Subscriber number
+        0x05: Alphanumeric (coded according to the GSM 7-bit default alphabet)
+        0x06: Abbreviated number
+    * */
+    fun decMSISDN(efMsisdn: String): String? {
+        println("ef_msisdn: $efMsisdn")
+        // Convert from String to ByteArray
+        val efMsisdnBytes = efMsisdn.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
+
+        // Make sure mandatory fields are present
+        if (efMsisdnBytes.size < 14) {
+            Log.e("DecMSISDN", "EF.MSISDN is too short")
+            return null
+        }
+
+        // Skip optional Alpha Identifier
+        val xlen = efMsisdnBytes.size - 14
+        val msisdnLhv = efMsisdnBytes.copyOfRange(xlen, efMsisdnBytes.size)
+
+        // Parse the length (in bytes) of the BCD encoded number
+        var bcdLen = msisdnLhv[0].toInt()
+        // BCD length = length of dial num (max. 10 bytes) + 1 byte ToN and NPI
+        if (bcdLen == 0xff) {
+            return null
+        } else if (bcdLen > 11 || bcdLen < 1) {
+            Log.e("DecMSISDN", "Length of MSISDN ($bcdLen bytes) is out of range")
+            return null
+        }
+
+        // Parse ToN / NPI
+        val ton = (msisdnLhv[1].toInt() shr 4) and 0x07
+        val npi = msisdnLhv[1].toInt() and 0x0f
+        bcdLen -= 1
+
+        // No MSISDN?
+        if (bcdLen == 0) {
+            return null
+        }
+
+        val msisdn =
+            msisdnLhv.copyOfRange(2, 2 + bcdLen).joinToString("") { "%02x".format(it) }
+                .swapNibbles()
+                .stripF()
+        // International number 10.5.118/3GPP TS 24.008
+        val formattedMsisdn = if (ton == 0x01) "+$msisdn" else msisdn
+        Log.i("DecMSISDN", "EF: $efMsisdn, MSISDN: $formattedMsisdn, NPI: $npi, ToN: $ton")
+        return formattedMsisdn
+    }
+
+    fun encMSISDN(msisdn: String, npi: Int = 0x01, ton: Int = 0x03): String {
+        // If no MSISDN is supplied then encode the file contents as all "ff"
+        if (msisdn.isEmpty() || msisdn == "+") {
+            return "ff".repeat(14)
+        }
+
+        var msisdnVar = msisdn
+        var tonVar = ton
+
+        // Leading '+' indicates International Number
+        if (msisdnVar[0] == '+') {
+            msisdnVar = msisdnVar.substring(1)
+            tonVar = 0x01
+        }
+
+        // An MSISDN must not exceed 20 digits
+        if (msisdnVar.length > 20) {
+            throw IllegalArgumentException("msisdn must not be longer than 20 digits")
+        }
+
+        // Append 'f' padding if number of digits is odd
+        if (msisdnVar.length % 2 > 0) {
+            msisdnVar += 'f'
+        }
+
+        // BCD length also includes NPI/ToN header
+        val bcdLen = msisdnVar.length / 2 + 1
+        val npiTon = (npi and 0x0f) or ((tonVar and 0x07) shl 4) or 0x80
+        val bcd = msisdnVar.swapNibbles().padEnd(10 * 2, 'f')  // pad to 10 octets
+
+        return String.format("%02x", bcdLen) + String.format("%02x", npiTon) + bcd + "ff".repeat(2)
+    }
+
+    fun decPLMN(efPlmn: String): String? {
+        if (efPlmn.length < 6) {
+            return null
+        }
+        return efPlmn.stripF().chunked(6).joinToString(",") {
+            val mcc = decMcc(it)
+            val mnc = decMnc(it)
+            "$mcc$mnc"
+        }
+    }
+
+    fun decPLMNwAcT(efPlmn: String): String? {
+        if (efPlmn.length < 10) {
+            return null
+        }
+        return efPlmn.chunked(10)
+            .filter { !it.substring(0, 6).matches(Regex("^f{3,}", RegexOption.IGNORE_CASE)) }
+            .joinToString(",") {
+                val mcc = decMcc(it)
+                val mnc = decMnc(it)
+                val act = it.substring(6)
+                "$mcc$mnc:$act"
+            }
+    }
+
+    fun encPLMN(plmn: String): String {
+        return plmn.split(",", ",").map { it.trim() }
+            .filter { it.matches(Regex("\\d{5,6}")) }
+            .joinToString("") { encMccMnc(it) }
+    }
+
+    fun encPLMNwAcT(plmn: String): String {
+        return plmn.split(",", ",").map { it.trim() }
+            .filter { it.matches(Regex("\\d{5,6}:\\d{4}")) }
+            .joinToString("") {
+                it.split(":")
+                    .let { (plmn, act) ->
+                        "${encMccMnc(plmn)}${act}"
+                    }
+            }
+    }
+
+    fun decMcc(plmn: String): String {
+        val digit1 = plmn[1]  // 1st byte, LSB
+        val digit2 = plmn[0]  // 1st byte, MSB
+        val digit3 = plmn[3]  // 2nd byte, LSB
+        val res = "$digit1$digit2$digit3"
+        return res.stripF()
+    }
+
+    fun decMnc(plmn: String): String {
+        val digit1 = plmn[5]  // 3rd byte, LSB
+        val digit2 = plmn[4]  // 3rd byte, MSB
+        val digit3 = plmn[2]  // 2nd byte, MSB
+        val res = "$digit1$digit2$digit3"
+        return res.stripF()
+    }
+
+    fun encMccMnc(mccmnc: String): String {
+        if (!mccmnc.matches(Regex("^\\d{5,6}$"))) {
+            throw IllegalArgumentException("mccmnc must be 5 or 6 digits")
+        }
+        // Make sure there are no excess whitespaces in the input parameters
+        var mccVar = mccmnc.substring(0, 3)
+        var mncVar = mccmnc.substring(3)
+
+        // Make sure that MCC/MNC are correctly padded with leading zeros or 'F', depending on the length
+        mncVar = when (mncVar.length) {
+            0 -> "FFF"
+            1 -> "0$mncVar" + "F"
+            2 -> mncVar + "F"
+            else -> mncVar
+        }
+
+        mccVar = when (mccVar.length) {
+            0 -> "FFF"
+            1 -> "00$mccVar"
+            2 -> "0$mccVar"
+            else -> mccVar
+        }
+
+        return "${mccVar[1]}${mccVar[0]}${mncVar[2]}${mccVar[2]}${mncVar[1]}${mncVar[0]}"
+    }
+}
+

+ 93 - 89
app/src/main/java/com/example/uicceditor/SmsUtils.kt

@@ -1,127 +1,131 @@
-package com.example.uicceditor;
+package com.example.uicceditor
 
-import android.content.Intent;
-import android.telephony.PhoneNumberUtils;
-import android.telephony.SmsManager;
-import android.util.Log;
+import android.content.Intent
+import android.telephony.PhoneNumberUtils
+import android.telephony.SmsManager
+import android.util.Log
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.UnsupportedEncodingException
+import java.nio.charset.StandardCharsets
+import java.util.Calendar
+import java.util.GregorianCalendar
 
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.lang.reflect.Method;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-
-public class SmsUtils {
+object SmsUtils {
     // 获取对应的Intent数据
-    public static Intent getSmsIntent(String number, String body) {
-        SmsManager smsManager = SmsManager.getDefault();
+    fun getSmsIntent(number: String, body: String?): Intent {
+        val smsManager = SmsManager.getDefault()
         // 防止短信过长
-        ArrayList<String> messages = smsManager.divideMessage(body);
-        int size = messages.size();
-        Object[] objArray = new Object[size];
-        for (int i = 0; i < size; ++i) {
-            byte[] pduu = createFakeSms(number, messages.get(i));
-            objArray[i] = pduu;
+        val messages = smsManager.divideMessage(body)
+        val size = messages.size
+        val objArray = arrayOfNulls<Any>(size)
+        for (i in 0 until size) {
+            val pduu = createFakeSms(number, messages[i])
+            objArray[i] = pduu
         }
-        Intent intent = new Intent();
-        intent.setAction("android.provider.Telephony.SMS_DELIVER");
-        intent.putExtra("pdus", objArray);
-        intent.putExtra("format", "3gpp");
-        intent.putExtra("subscription", 12);
-        return intent;
+        val intent = Intent()
+        intent.setAction("android.provider.Telephony.SMS_DELIVER")
+        intent.putExtra("pdus", objArray)
+        intent.putExtra("format", "3gpp")
+        intent.putExtra("subscription", 12)
+        return intent
     }
 
     // 创建pdu
-    public static byte[] createFakeSms(String sender, String body) {
-        byte[] pdu = null;
-        byte[] scBytes = PhoneNumberUtils.networkPortionToCalledPartyBCD("0000000000");
-        byte[] senderBytes = PhoneNumberUtils.networkPortionToCalledPartyBCD(sender);
-        int lsmcs = scBytes.length;
+    fun createFakeSms(sender: String, body: String): ByteArray? {
+        var pdu: ByteArray? = null
+        val scBytes = PhoneNumberUtils.networkPortionToCalledPartyBCD("0000000000")
+        val senderBytes = PhoneNumberUtils.networkPortionToCalledPartyBCD(sender)
+        val lsmcs = scBytes.size
         // 时间处理,包括年月日时分秒以及时区和夏令时
-        byte[] dateBytes = new byte[7];
-        Calendar calendar = new GregorianCalendar();
-        dateBytes[0] = reverseByte((byte) (calendar.get(Calendar.YEAR)));
-        dateBytes[1] = reverseByte((byte) (calendar.get(Calendar.MONTH) + 1));
-        dateBytes[2] = reverseByte((byte) (calendar.get(Calendar.DAY_OF_MONTH)));
-        dateBytes[3] = reverseByte((byte) (calendar.get(Calendar.HOUR_OF_DAY)));
-        dateBytes[4] = reverseByte((byte) (calendar.get(Calendar.MINUTE)));
-        dateBytes[5] = reverseByte((byte) (calendar.get(Calendar.SECOND)));
-        dateBytes[6] = reverseByte((byte) ((calendar.get(Calendar.ZONE_OFFSET)
-                + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000 * 15)));
+        val dateBytes = ByteArray(7)
+        val calendar: Calendar = GregorianCalendar()
+        dateBytes[0] = reverseByte(calendar[Calendar.YEAR].toByte())
+        dateBytes[1] = reverseByte((calendar[Calendar.MONTH] + 1).toByte())
+        dateBytes[2] = reverseByte(calendar[Calendar.DAY_OF_MONTH].toByte())
+        dateBytes[3] = reverseByte(calendar[Calendar.HOUR_OF_DAY].toByte())
+        dateBytes[4] = reverseByte(calendar[Calendar.MINUTE].toByte())
+        dateBytes[5] = reverseByte(calendar[Calendar.SECOND].toByte())
+        dateBytes[6] = reverseByte(
+            ((calendar[Calendar.ZONE_OFFSET]
+                    + calendar[Calendar.DST_OFFSET]) / (60 * 1000 * 15)).toByte()
+        )
         try {
-            ByteArrayOutputStream bo = new ByteArrayOutputStream();
-            bo.write(lsmcs);// 短信服务中心长度
-            bo.write(scBytes);// 短信服务中心号码
-            bo.write(0x04);
-            bo.write((byte) sender.length());// 发送方号码长度
-            bo.write(senderBytes);// 发送方号码
-            bo.write(0x00);// 协议标示,00为普通GSM,点对点方式
+            val bo = ByteArrayOutputStream()
+            bo.write(lsmcs) // 短信服务中心长度
+            bo.write(scBytes) // 短信服务中心号码
+            bo.write(0x04)
+            bo.write(sender.length.toByte().toInt()) // 发送方号码长度
+            bo.write(senderBytes) // 发送方号码
+            bo.write(0x00) // 协议标示,00为普通GSM,点对点方式
             try {
-                String className = "com.android.internal.telephony.GsmAlphabet";
-                Class<?> clazz = Class.forName(className);
-                Method method = clazz.getMethod("stringToGsm7BitPacked", new Class[]{String.class});
-                method.setAccessible(true);
-                byte[] bodybytes = (byte[]) method.invoke(null, body);
+                val className = "com.android.internal.telephony.GsmAlphabet"
+                val clazz = Class.forName(className)
+                val method = clazz.getMethod(
+                    "stringToGsm7BitPacked", *arrayOf<Class<*>>(
+                        String::class.java
+                    )
+                )
+                method.isAccessible = true
+                val bodybytes = method.invoke(null, body) as ByteArray
 
-                byte[] bodybytes1 = GsmAlphabet.stringToGsm7BitPacked(body);
+                val bodybytes1 = GsmAlphabet.stringToGsm7BitPacked(body)
 
                 // compare the two byte arrays
-                if (bodybytes1.length != bodybytes.length) {
-                    System.out.println("The two byte arrays are not the same length.");
+                if (bodybytes1.size != bodybytes.size) {
+                    println("The two byte arrays are not the same length.")
                 } else {
-                    boolean same = true;
-                    for (int i = 0; i < bodybytes1.length; i++) {
+                    var same = true
+                    for (i in bodybytes1.indices) {
                         if (bodybytes1[i] != bodybytes[i]) {
-                            same = false;
-                            break;
+                            same = false
+                            break
                         }
                     }
-                    Log.d("stringToGsm7BitPacked", "createFakeSms: " + same);
+                    Log.d("stringToGsm7BitPacked", "createFakeSms: $same")
                 }
 
-                bo.write(0xf1); // encoding: 0 for default 7bit
-                bo.write(dateBytes);
-                bo.write(bodybytes);
-            } catch (Exception e) {
+                bo.write(0xf1) // encoding: 0 for default 7bit
+                bo.write(dateBytes)
+                bo.write(bodybytes)
+            } catch (e: Exception) {
                 // 下面是UCS-2编码的处理,中文短信就需要用此种方式
-                byte[] bodyBytes = encodeUCS2(body, null);
-                bo.write(0x08); // encoding: 8 for UCS-2
-                bo.write(dateBytes);
-                bo.write(bodyBytes);// 其中encodeUCS2是从系统中复制过来的,并不是我写的
+                val bodyBytes = encodeUCS2(body, null)
+                bo.write(0x08) // encoding: 8 for UCS-2
+                bo.write(dateBytes)
+                bo.write(bodyBytes) // 其中encodeUCS2是从系统中复制过来的,并不是我写的
                 // 源码具体位置在
                 // frameworks/base/telephony/java/com/android/internal/telephony/gsm/SmsMessage.java
             }
 
-            pdu = bo.toByteArray();
-        } catch (IOException e) {
+            pdu = bo.toByteArray()
+        } catch (e: IOException) {
         }
-        return pdu;
+        return pdu
     }
 
-    private static byte reverseByte(byte b) {
-        return (byte) ((b & 0xF0) >> 4 | (b & 0x0F) << 4);
+    private fun reverseByte(b: Byte): Byte {
+        return ((b.toInt() and 0xF0) shr 4 or ((b.toInt() and 0x0F) shl 4)).toByte()
     }
 
-    private static byte[] encodeUCS2(String message, byte[] header) throws UnsupportedEncodingException {
-        byte[] userData, textPart;
-        textPart = message.getBytes(StandardCharsets.UTF_16BE);
+    @Throws(UnsupportedEncodingException::class)
+    private fun encodeUCS2(message: String, header: ByteArray?): ByteArray {
+        val userData: ByteArray
+        val textPart = message.toByteArray(StandardCharsets.UTF_16BE)
 
         if (header != null) {
             // Need 1 byte for UDHL
-            userData = new byte[header.length + textPart.length + 1];
+            userData = ByteArray(header.size + textPart.size + 1)
 
-            userData[0] = (byte) header.length;
-            System.arraycopy(header, 0, userData, 1, header.length);
-            System.arraycopy(textPart, 0, userData, header.length + 1, textPart.length);
+            userData[0] = header.size.toByte()
+            System.arraycopy(header, 0, userData, 1, header.size)
+            System.arraycopy(textPart, 0, userData, header.size + 1, textPart.size)
         } else {
-            userData = textPart;
+            userData = textPart
         }
-        byte[] ret = new byte[userData.length + 1];
-        ret[0] = (byte) (userData.length & 0xff);
-        System.arraycopy(userData, 0, ret, 1, userData.length);
-        return ret;
+        val ret = ByteArray(userData.size + 1)
+        ret[0] = (userData.size and 0xff).toByte()
+        System.arraycopy(userData, 0, ret, 1, userData.size)
+        return ret
     }
 }

+ 3 - 3
app/src/main/res/layout/activity_main.xml

@@ -253,7 +253,7 @@
                     android:id="@+id/et_plmnwact"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:digits="0123456789,"
+                    android:digits="0123456789,:"
                     android:lines="1" />
             </com.google.android.material.textfield.TextInputLayout>
 
@@ -288,7 +288,7 @@
                     android:id="@+id/et_oplmnwact"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:digits="0123456789,"
+                    android:digits="0123456789,:"
                     android:lines="1" />
             </com.google.android.material.textfield.TextInputLayout>
 
@@ -323,7 +323,7 @@
                     android:id="@+id/et_hplmnwact"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:digits="0123456789,"
+                    android:digits="0123456789,:"
                     android:lines="1" />
             </com.google.android.material.textfield.TextInputLayout>