ModemConfig: Register subchange with SubscriptionManager instead

DEFAULT_SUBSCRIPTION_CHANGED is not delivered often enough. When
swapping the same SIM between slots the selected subscription doesn't
change yet the firmware for the slot might need to be updated.
Registering this handler provides the ability to do so, but has to be
registered from a background service that consequently is to be started
by a BootReceiver.

Signed-off-by: Marijn Suijten <marijns95@gmail.com>
This commit is contained in:
Marijn Suijten 2020-05-11 14:55:12 +02:00
commit 05d50c9738
3 changed files with 69 additions and 26 deletions

View file

@ -5,14 +5,21 @@
android:versionCode="1"
android:versionName="1.0">
<application android:directBootAware="true">
<receiver android:name=".ModemConfigReceiver">
<application
android:directBootAware="true"
android:persistent="true">
<receiver
android:name=".BootReceiver"
android:enabled="true">
<intent-filter>
<action android:name="android.telephony.action.DEFAULT_SUBSCRIPTION_CHANGED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service android:name=".ModemConfigService" />
</application>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
</manifest>

View file

@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 Marijn Suijten
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sony.opentelephony.modemconfig
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
context.startService(Intent(context, ModemConfigService::class.java))
}
}

View file

@ -0,0 +1,184 @@
/*
* Copyright (C) 2020 Marijn Suijten
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sony.opentelephony.modemconfig
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import android.text.TextUtils
import android.util.Log
import org.xmlpull.v1.XmlPullParser
private const val TAG = "ModemConfigReceiver"
private const val VERBOSE = false
private val newlineMatch = Regex("[\n\r]")
class ModemConfigService : Service() {
private class ProviderFilter(val sim_config_id: String,
val mcc: String? = null,
val mnc: String? = null,
val gid1: String? = null,
val imsi: Regex? = null,
val sp: Regex? = null
) {
override fun toString() = "[mcc: $mcc, mnc: $mnc, gid1: $gid1, imsi: ${imsi?.pattern}, " +
"sp: ${sp?.pattern}] => $sim_config_id"
}
// TODO: Do a lateinit here?
private /*lateinit*/ var providers: List<ProviderFilter>? = null
private fun getProviders(context: Context): List<ProviderFilter> {
if (providers != null)
return providers!!
val list = arrayListOf<ProviderFilter>()
val currentMap = hashMapOf<String, String>()
var simConfigId: String? = null
// TODO: Kotlin .use {} block doesn't work here, while it does in regular apps...
/*
modemconfig/ModemConfigReceiver.kt:63:70: error: unresolved reference. None of the following
candidates is applicable because of receiver type mismatch:
@InlineOnly public inline fun <T : Closeable?, R> ???.use(block: (???) -> ???): ???
defined in kotlin.io
context.resources.getXml(R.xml.service_provider_sim_configs).use {
*/
val xml = context.resources.getXml(R.xml.service_provider_sim_configs)
try {
while (xml.next() != XmlPullParser.END_DOCUMENT) {
if (xml.eventType == XmlPullParser.END_TAG &&
xml.name == "service_provider_sim_config") {
// When a node is closed, collect all elements and create a filter entry:
list.add(ProviderFilter(
simConfigId!!,
currentMap["mcc"],
currentMap["mnc"],
currentMap["gid1"],
currentMap["imsi"]?.let { Regex(it) },
currentMap["sp"]?.let { Regex(it) }
))
currentMap.clear()
continue
}
if (xml.eventType != XmlPullParser.START_TAG)
continue
if (xml.name == "service_provider_sim_config") {
// Start collection for this config id:
simConfigId = xml.getAttributeValue(null, "sim_config_id")
if (TextUtils.isEmpty(simConfigId))
throw Exception("Unexpected empty sim_config_id attribute!")
currentMap.clear()
} else if (simConfigId != null) {
// If not in a service_provider_sim_config element, and a valid simConfigId
// /exists parse all members/subelements:
val text = cleanString(xml.nextText())
if (currentMap.containsKey(xml.name))
throw Exception("Already parsed ${xml.name}!")
currentMap[xml.name] = text
}
}
} finally {
xml.close()
}
providers = list
return list
}
private fun cleanString(str: String) = str.replace(newlineMatch, "").trim()
private fun findConfigurationName(context: Context, tm: TelephonyManager): String? {
val operator = tm.simOperator
val mcc = operator.substring(0, 3)
val mnc = operator.substring(3)
val sp = cleanString(tm.simOperatorName)
val imsi = tm.subscriberId
val gid1 = tm.groupIdLevel1
val iccid2 = tm.simSerialNumber
if (VERBOSE) Log.v(TAG, "Matching providers against: $sp $mcc/$mnc, imsi: $imsi" +
", gid: $gid1, iccid2: $iccid2")
val result = getProviders(context).filter f@{ info ->
if (info.mcc != null && info.mcc != mcc)
return@f false
if (info.mnc != null && info.mnc != mnc)
return@f false
if (info.gid1 != null && !gid1.startsWith(info.gid1))
return@f false
if (info.imsi != null && !info.imsi.matches(imsi))
return@f false
if (info.sp != null && !info.sp.matches(sp))
return@f false
true
}
return if (result.isEmpty()) {
Log.w(TAG, "No matching config found")
null
} else {
if (result.size > 1)
Log.w(TAG, "Multiple matches found: ${result}, using last")
val match = result.last()
Log.i(TAG, "Matched with $match")
match.sim_config_id
}
}
private fun handleSubscription(sub: SubscriptionInfo) {
if (VERBOSE) Log.v(TAG, "Checking sub $sub")
val globalTm = getSystemService(TelephonyManager::class.java)
val tm = globalTm!!.createForSubscriptionId(sub.subscriptionId)
val name = findConfigurationName(this, tm)
}
override fun onCreate() {
Log.e(TAG, "Starting")
val sm = getSystemService(SubscriptionManager::class.java)
?: throw Exception("Expected SubscriptionManager, got null")
// Our callback is invoked once on .add too; no need to run the contents manually at startup
sm.addOnSubscriptionsChangedListener(
object : SubscriptionManager.OnSubscriptionsChangedListener() {
// Even though this callback is invoked a lot (when sims are changed), apply no
// caching at all. The modem-switcher itself makes sure to not needlessly flash
// firmware - let it handle the validation.
override fun onSubscriptionsChanged() {
sm.activeSubscriptionInfoList?.forEach(::handleSubscription)
}
})
}
override fun onBind(intent: Intent?): IBinder? = null
}