Android dual SIM card API
Question
There are several questions about accessing dual SIM features through the Android SDK, all of which are answered with brief statements that such features are unsupported in Android.
In spite of this, dual SIM phones do exist, and applications like MultiSim seem to be able to detect this in some kind of manufacturer-independent way.
So, beginning with that acknowledgement, let me try to ask some more pointed questions:
- Does "Android SDK does not support multiple SIM features" mean that these features do not exist, or that it is merely a bad idea to try to use them?
- Is there an Android content provider or an internal package (com.android...) that provides SIM card information? (TelephonyManager, as far as I can see in the docs and the code, has no mention of multiple SIM cards)
- Is there any report of any manufacturer exposing multiple SIM features to developers?
- If I were to look for undocumented functionality from a manufacturer, how would I go about that?
(By the way, the point of all of this is merely to implement this algorithm: send an SMS with SIM card 1; if delivery fails, switch to SIM card 2 and resend the message that way.)
Solution
You can use MultiSim
library to get details from multi-sim devices.
Available info from each sim card: IMEI, IMSI, SIM Serial Number, SIM State, SIM operator code, SIM operator name, SIM country iso, network operator code, network operator name, network operator iso, network type, roaming status.
Just add the lines below in your app-level Gradle script:
dependencies {
compile 'com.kirianov.multisim:multisim:2.0@aar'
}
Don't forget add required permission in AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
Use similar code in your code:
MultiSimTelephonyManager multiSimTelephonyManager = new MultiSimTelephonyManager(this);
// or
MultiSimTelephonyManager multiSimTelephonyManager = new MultiSimTelephonyManager(this, new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateInfo();
}
});
public void updateInfo() {
// for update UI
runOnUiThread(new Runnable() {
@Override
public void run() {
multiSimTelephonyManager.update();
useInfo();
}
}
// for update background information
multiSimTelephonyManager.update();
useInfo();
}
public void useInfo() {
// get number of slots:
if (multiSimTelephonyManager != null) {
multiSimTelephonyManager.sizeSlots();
}
// get info from each slot:
if (multiSimTelephonyManager != null) {
for(int i = 0; i < multiSimTelephonyManager.sizeSlots(); i++) {
multiSimTelephonyManager.getSlot(i).getImei();
multiSimTelephonyManager.getSlot(i).getImsi();
multiSimTelephonyManager.getSlot(i).getSimSerialNumber();
multiSimTelephonyManager.getSlot(i).getSimState();
multiSimTelephonyManager.getSlot(i).getSimOperator();
multiSimTelephonyManager.getSlot(i).getSimOperatorName();
multiSimTelephonyManager.getSlot(i).getSimCountryIso();
multiSimTelephonyManager.getSlot(i).getNetworkOperator();
multiSimTelephonyManager.getSlot(i).getNetworkOperatorName();
multiSimTelephonyManager.getSlot(i).getNetworkCountryIso();
multiSimTelephonyManager.getSlot(i).getNetworkType();
multiSimTelephonyManager.getSlot(i).isNetworkRoaming();
}
}
}
// or for devices above android 6.0
MultiSimTelephonyManager multiSimTelephonyManager = new MultiSimTelephonyManager(MyActivity.this, broadcastReceiverChange);
Usage:
// get info about slot 'i' by methods:
multiSimTelephonyManager.getSlot(i).
Force update info:
// force update phone info (needed on devices above android 6.0 after confirm permissions request)
multiSimTelephonyManager.update();
Handle of permissions request (6.0+):
// in YourActivity for update info after confirm permissions request on devices above android 6.0
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (multiSimTelephonyManager != null) {
multiSimTelephonyManager.update();
}
}
OTHER TIPS
there are 3 different categories ...
- features supported and documented
- Features available and un-documented
- features unavailable
So the dual sim features are available but not documented and hence not officially supported.
Having said that it doesn't mean that it will not be usable , It just means that android(or for that matter google or even manufaturer) is not liable to support your apps functionality.
But it might just work , for eg the contacts is a similar thing.
You might then ask up how would everyone know about the features if in case its not documented.. Hey android is open source .. go look into code and find it for yourself . Thats what I guess the multi sim developers did.
Android does not support multiple SIM features before API 22. But from Android 5.1 (API level 22) onwards, Android started supporting multiple SIMs. More details on Android Documentation
Reference from this Original Answer
MultiSim library sources now are not available on VCS
It's sources is still available here https://mvnrepository.com/artifact/com.kirianov.multisim/multisim
I have rewrote it on Kotlin for personal usage
class Slot {
var imei: String? = null
var imsi: String? = null
var simState = -1
val simStates = hashSetOf<Int>()
var simSerialNumber: String? = null
var simOperator: String? = null
var simCountryIso: String? = null
fun setSimState(state: Int?) {
if (state == null) {
simState = -1
return
}
simState = state
}
private fun compare(slot: Slot?): Boolean {
return if (slot != null) {
imei == slot.imei && imsi == slot.imsi && simSerialNumber == slot.simSerialNumber
} else false
}
fun indexIn(slots: List<Slot>?): Int {
if (slots == null) {
return -1
}
for (i in slots.indices) {
if (compare(slots[i])) {
return i
}
}
return -1
}
fun containsIn(slots: List<Slot>?): Boolean {
if (slots == null) {
return false
}
for (slot in slots) {
if (compare(slot)) {
return true
}
}
return false
}
override fun toString(): String {
return "Slot(" +
"imei=$imei, " +
"imsi=$imsi, " +
"simState=$simState, " +
"simStates=$simStates, " +
"simSerialNumber=$simSerialNumber, " +
"simOperator=$simOperator, " +
"simCountryIso=$simCountryIso" +
")"
}
}
Also i remove native lib dependencies
import android.Manifest
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.telephony.SubscriptionManager
import android.text.TextUtils
import domain.shadowss.extension.areGranted
import domain.shadowss.extension.isLollipopMR1Plus
import domain.shadowss.extension.isMarshmallowPlus
import domain.shadowss.extension.isOreoPlus
import domain.shadowss.model.Slot
import org.jetbrains.anko.telephonyManager
import timber.log.Timber
import java.lang.ref.WeakReference
import java.lang.reflect.Modifier
import java.util.*
/**
* https://mvnrepository.com/artifact/com.kirianov.multisim/multisim
*/
@Suppress("MemberVisibilityCanBePrivate")
class MultiSimManager(context: Context) {
private val reference = WeakReference(context)
val slots = arrayListOf<Slot>()
@Synchronized get
@Suppress("unused")
val dualMcc: Pair<String?, String?>
@SuppressLint("MissingPermission")
@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
get() = reference.get()?.run {
if (isLollipopMR1Plus()) {
if (areGranted(Manifest.permission.READ_PHONE_STATE)) {
val subscriptionManager =
getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager
val list = subscriptionManager.activeSubscriptionInfoList
return list.getOrNull(0)?.mcc?.toString()
?.padStart(3, '0') to list.getOrNull(1)?.mcc?.toString()
?.padStart(3, '0')
}
}
null to null
} ?: null to null
@Synchronized
@SuppressLint("MissingPermission")
fun updateData(): String? = reference.get()?.run {
if (areGranted(Manifest.permission.READ_PHONE_STATE)) {
val error = try {
var slotNumber = 0
while (true) {
val slot = touchSlot(slotNumber)
if (slot == null) {
for (i in slotNumber until slots.size) {
slots.removeAt(i)
}
break
}
if (slot.containsIn(slots) && slot.indexIn(slots) < slotNumber) {
// protect from Alcatel infinity bug
break
}
slots.apply {
when {
size > slotNumber -> {
removeAt(slotNumber)
add(slotNumber, slot)
}
size == slotNumber -> add(slot)
}
}
slotNumber++
}
null
} catch (e: Throwable) {
Timber.e(e)
e.toString()
}
// below is my custom logic only which was found on practice
slots.removeAll { it.imsi == null || it.simOperator?.trim()?.isEmpty() != false }
val imsi = arrayListOf<String?>()
slots.forEachReversedWithIndex { i, slot ->
if (imsi.contains(slot.imsi)) {
slots.removeAt(i)
} else {
imsi.add(slot.imsi)
slot.simStates.apply {
clear()
addAll(slots.filter { it.imsi == slot.imsi }.map { it.simState })
}
}
}
error
} else {
slots.clear()
null
}
}
@Suppress("SpellCheckingInspection", "DEPRECATION")
@SuppressLint("MissingPermission", "HardwareIds")
private fun touchSlot(slotNumber: Int): Slot? = reference.get()?.run {
val slot = Slot()
val telephonyManager = telephonyManager
Timber.v("telephonyManager [$telephonyManager] ${telephonyManager.deviceId}")
val subscriberIdIntValue = ArrayList<String>()
val subscriberIdIntIndex = ArrayList<Int>()
for (i in 0..99) {
val subscriber = runMethodReflect(
telephonyManager,
"android.telephony.TelephonyManager",
"getSubscriberId",
arrayOf(i),
null
) as? String
if (subscriber != null && !subscriberIdIntValue.contains(subscriber)) {
subscriberIdIntValue.add(subscriber)
subscriberIdIntIndex.add(i)
}
}
var subIdInt =
if (subscriberIdIntIndex.size > slotNumber) subscriberIdIntIndex[slotNumber] else null
if (subIdInt == null) {
try {
subIdInt = runMethodReflect(
telephonyManager,
"android.telephony.TelephonyManager",
"getSubId",
arrayOf(slotNumber),
null
).toString().toInt()
} catch (ignored: Throwable) {
}
}
Timber.v("subIdInt $subIdInt")
val subscriberIdLongValue = ArrayList<String>()
val subscriberIdLongIndex = ArrayList<Long>()
for (i in 0L until 100L) {
val subscriber = runMethodReflect(
telephonyManager,
"android.telephony.TelephonyManager",
"getSubscriberId",
arrayOf(i),
null
) as? String
runMethodReflect(
telephonyManager,
"android.telephony.TelephonyManagerSprd",
"getSubInfoForSubscriber",
arrayOf(i),
null
) ?: continue
if (subscriber != null && !subscriberIdLongValue.contains(subscriber)) {
subscriberIdLongValue.add(subscriber)
subscriberIdLongIndex.add(i)
}
}
if (subscriberIdLongIndex.size <= 0) {
for (i in 0L until 100L) {
val subscriber = runMethodReflect(
telephonyManager,
"android.telephony.TelephonyManager",
"getSubscriberId",
arrayOf(i),
null
) as? String
if (subscriber != null && !subscriberIdLongValue.contains(subscriber)) {
subscriberIdLongValue.add(subscriber)
subscriberIdLongIndex.add(i)
}
}
}
var subIdLong =
if (subscriberIdLongIndex.size > slotNumber) subscriberIdLongIndex[slotNumber] else null
if (subIdLong == null) {
subIdLong = runMethodReflect(
telephonyManager,
"android.telephony.TelephonyManager",
"getSubId",
arrayOf(slotNumber),
null
) as? Long
}
Timber.v("subIdLong $subIdLong")
val listParamsSubs = ArrayList<Any?>()
if (subIdInt != null && !listParamsSubs.contains(subIdInt)) {
listParamsSubs.add(subIdInt)
}
if (subIdLong != null && !listParamsSubs.contains(subIdLong)) {
listParamsSubs.add(subIdLong)
}
if (!listParamsSubs.contains(slotNumber)) {
listParamsSubs.add(slotNumber)
}
val objectParamsSubs = listParamsSubs.toTypedArray()
for (i in objectParamsSubs.indices) {
Timber.v("SPAM PARAMS_SUBS [$i]=[${objectParamsSubs[i]}]")
}
val listParamsSlot = ArrayList<Any?>()
if (!listParamsSlot.contains(slotNumber)) {
listParamsSlot.add(slotNumber)
}
if (subIdInt != null && !listParamsSlot.contains(subIdInt)) {
listParamsSlot.add(subIdInt)
}
if (subIdLong != null && !listParamsSlot.contains(subIdLong)) {
listParamsSlot.add(subIdLong)
}
val objectParamsSlot = listParamsSlot.toTypedArray()
for (i in objectParamsSlot.indices) {
Timber.v("SPAM PARAMS_SLOT [$i]=[${objectParamsSlot[i]}]")
}
// firstly all Int params, then all Long params
Timber.v("------------------------------------------")
Timber.v("SLOT [$slotNumber]")
if (isMarshmallowPlus()) {
slot.imei = telephonyManager.getDeviceId(slotNumber)
}
if (slot.imei == null) {
slot.imei = iterateMethods("getDeviceId", objectParamsSlot) as? String
}
if (slot.imei == null) {
slot.imei = runMethodReflect(
null,
"com.android.internal.telephony.Phone",
null,
null,
"GEMINI_SIM_" + (slotNumber + 1)
) as? String
}
if (slot.imei == null) {
slot.imei = runMethodReflect(
getSystemService("phone" + (slotNumber + 1)),
null,
"getDeviceId",
null,
null
) as? String
}
Timber.v("IMEI [${slot.imei}]")
if (slot.imei == null) {
when (slotNumber) {
0 -> {
slot.imei = if (isOreoPlus()) {
telephonyManager.imei
} else {
telephonyManager.deviceId
}
slot.imsi = telephonyManager.subscriberId
slot.simState = telephonyManager.simState
slot.simOperator = telephonyManager.simOperator
slot.simSerialNumber = telephonyManager.simSerialNumber
slot.simCountryIso = telephonyManager.simCountryIso
return slot
}
}
}
if (slot.imei == null) {
return null
}
slot.setSimState(iterateMethods("getSimState", objectParamsSlot) as? Int)
Timber.v("SIMSTATE [${slot.simState}]")
slot.imsi = iterateMethods("getSubscriberId", objectParamsSubs) as? String
Timber.v("IMSI [${slot.imsi}]")
slot.simSerialNumber = iterateMethods("getSimSerialNumber", objectParamsSubs) as? String
Timber.v("SIMSERIALNUMBER [${slot.simSerialNumber}]")
slot.simOperator = iterateMethods("getSimOperator", objectParamsSubs) as? String
Timber.v("SIMOPERATOR [${slot.simOperator}]")
slot.simCountryIso = iterateMethods("getSimCountryIso", objectParamsSubs) as? String
Timber.v("SIMCOUNTRYISO [${slot.simCountryIso}]")
Timber.v("------------------------------------------")
return slot
}
@SuppressLint("WrongConstant")
private fun iterateMethods(methodName: String?, methodParams: Array<Any?>): Any? =
reference.get()?.run {
if (methodName == null || methodName.isEmpty()) {
return null
}
val telephonyManager = telephonyManager
val instanceMethods = ArrayList<Any?>()
val multiSimTelephonyManagerExists = telephonyManager.toString()
.startsWith("android.telephony.MultiSimTelephonyManager")
for (methodParam in methodParams) {
if (methodParam == null) {
continue
}
val objectMulti = if (multiSimTelephonyManagerExists) {
runMethodReflect(
null,
"android.telephony.MultiSimTelephonyManager",
"getDefault",
arrayOf(methodParam),
null
)
} else {
telephonyManager
}
if (!instanceMethods.contains(objectMulti)) {
instanceMethods.add(objectMulti)
}
}
if (!instanceMethods.contains(telephonyManager)) {
instanceMethods.add(telephonyManager)
}
val telephonyManagerEx = runMethodReflect(
null,
"com.mediatek.telephony.TelephonyManagerEx",
"getDefault",
null,
null
)
if (!instanceMethods.contains(telephonyManagerEx)) {
instanceMethods.add(telephonyManagerEx)
}
val phoneMsim = getSystemService("phone_msim")
if (!instanceMethods.contains(phoneMsim)) {
instanceMethods.add(phoneMsim)
}
if (!instanceMethods.contains(null)) {
instanceMethods.add(null)
}
var result: Any?
for (methodSuffix in suffixes) {
for (className in classNames) {
for (instanceMethod in instanceMethods) {
for (methodParam in methodParams) {
if (methodParam == null) {
continue
}
result = runMethodReflect(
instanceMethod,
className,
methodName + methodSuffix,
if (multiSimTelephonyManagerExists) null else arrayOf(methodParam),
null
)
if (result != null) {
return result
}
}
}
}
}
return null
}
private fun runMethodReflect(
instanceInvoke: Any?,
classInvokeName: String?,
methodName: String?,
methodParams: Array<Any>?,
field: String?
): Any? {
var result: Any? = null
try {
val classInvoke = when {
classInvokeName != null -> Class.forName(classInvokeName)
instanceInvoke != null -> instanceInvoke.javaClass
else -> return null
}
if (field != null) {
val fieldReflect = classInvoke.getField(field)
val accessible = fieldReflect.isAccessible
fieldReflect.isAccessible = true
result = fieldReflect.get(null).toString()
fieldReflect.isAccessible = accessible
} else {
var classesParams: Array<Class<*>?>? = null
if (methodParams != null) {
classesParams = arrayOfNulls(methodParams.size)
for (i in methodParams.indices) {
classesParams[i] = when {
methodParams[i] is Int -> Int::class.javaPrimitiveType
methodParams[i] is Long -> Long::class.javaPrimitiveType
methodParams[i] is Boolean -> Boolean::class.javaPrimitiveType
else -> methodParams[i].javaClass
}
}
}
val method = if (classesParams != null) {
classInvoke.getDeclaredMethod(methodName.toString(), *classesParams)
} else {
classInvoke.getDeclaredMethod(methodName.toString())
}
val accessible = method.isAccessible
method.isAccessible = true
result = if (methodParams != null) {
method.invoke(instanceInvoke ?: classInvoke, *methodParams)
} else {
method.invoke(instanceInvoke ?: classInvoke)
}
method.isAccessible = accessible
}
} catch (ignored: Throwable) {
}
return result
}
@Suppress("unused")
val allMethodsAndFields: String
get() = """
Default: ${reference.get()?.telephonyManager}${'\n'}
${printAllMethodsAndFields("android.telephony.TelephonyManager")}
${printAllMethodsAndFields("android.telephony.MultiSimTelephonyManager")}
${printAllMethodsAndFields("android.telephony.MSimTelephonyManager")}
${printAllMethodsAndFields("com.mediatek.telephony.TelephonyManager")}
${printAllMethodsAndFields("com.mediatek.telephony.TelephonyManagerEx")}
${printAllMethodsAndFields("com.android.internal.telephony.ITelephony")}
""".trimIndent()
private fun printAllMethodsAndFields(className: String): String {
val builder = StringBuilder()
builder.append("========== $className\n")
try {
val cls = Class.forName(className)
for (method in cls.methods) {
val params = method.parameterTypes.map { it.name }
builder.append(
"M: ${method.name} [${params.size}](${TextUtils.join(
",",
params
)}) -> ${method.returnType} ${if (Modifier.isStatic(method.modifiers)) "(static)" else ""}\n"
)
}
for (field in cls.fields) {
builder.append("F: ${field.name} ${field.type}\n")
}
} catch (e: Throwable) {
builder.append("E: $e\n")
}
return builder.toString()
}
companion object {
private val classNames = arrayOf(
null,
"android.telephony.TelephonyManager",
"android.telephony.MSimTelephonyManager",
"android.telephony.MultiSimTelephonyManager",
"com.mediatek.telephony.TelephonyManagerEx",
"com.android.internal.telephony.Phone",
"com.android.internal.telephony.PhoneFactory"
)
private val suffixes = arrayOf(
"",
"Gemini",
"Ext",
"Ds",
"ForSubscription",
"ForPhone"
)
}
}
May be it wiil be helpful for somebody
You can use SubscriptionInfo
class to achieve it,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
val mSubscriptionManager = getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager
// val mSubscriptionManager = SubscriptionManager.from(baseContext)
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
val subscriptions = mSubscriptionManager.activeSubscriptionInfoList
//loop through number of SIMS inserted
for (subscriptionInfo in subscriptions) {
//the number of this subscription if the calling app has been granted the READ_PHONE_NUMBERS permission, or an empty string otherwise
Log.v("SIM", subscriptionInfo.number)
//the ISO country code
Log.v("SIM", subscriptionInfo.countryIso)
//the name displayed to the user that identifies Subscription provider name
Log.v("SIM", subscriptionInfo.carrierName.toString())
//the name displayed to the user that identifies this subscription
Log.v("SIM", subscriptionInfo.displayName.toString())
//The MCC, as a string. This value may be null.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.v("SIM", subscriptionInfo.mccString.toString())
} else {
Log.v("SIM", subscriptionInfo.mcc.toString())
}
//The MNC, as a string. This value may be null.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.v("SIM", subscriptionInfo.mncString.toString())
} else {
Log.v("SIM", subscriptionInfo.mnc.toString())
}
}
}
}
<receiver
android:name=".SimChangedReceiver"
android:enabled="true"
android:process=":remote" >
<intent-filter>
<action android:name="android.intent.action.SIM_STATE_CHANGED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
SimChangedReceiver class
public class SimChangedReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equalsIgnoreCase("android.intent.action.SIM_STATE_CHANGED")) {
Log.d("SimChangedReceiver", "--> SIM state changed <--");
// do code whatever u want to apply action //
}
}
}
this is work for dual sim also , and you don't need to call this receiver because it will run remotely