Skip to content

Commit 4090fa6

Browse files
authored
feat: Quick Settings tiles to instantly control VoLTE setting (#253)
1 parent e3a03f4 commit 4090fa6

File tree

9 files changed

+285
-3
lines changed

9 files changed

+285
-3
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
android:label="@string/app_name"
1212
android:supportsRtl="true"
1313
android:theme="@style/Theme.EnableVoLTE"
14-
tools:targetApi="31"
14+
tools:targetApi="33"
1515
android:localeConfig="@xml/locales_config">
1616
<activity
1717
android:name=".HomeActivity"
@@ -23,6 +23,50 @@
2323
<category android:name="android.intent.category.LAUNCHER" />
2424
</intent-filter>
2525
</activity>
26+
<service
27+
android:name=".SIM1VoLTEConfigToggleQSTileService"
28+
android:exported="true"
29+
android:label="@string/qs_toggle_tile_title_sim_1"
30+
android:icon="@drawable/ic_launcher_foreground"
31+
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
32+
<intent-filter>
33+
<action android:name="android.service.quicksettings.action.QS_TILE" />
34+
</intent-filter>
35+
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:value="true" />
36+
</service>
37+
<service
38+
android:name=".SIM2VoLTEConfigToggleQSTileService"
39+
android:exported="true"
40+
android:label="@string/qs_toggle_tile_title_sim_2"
41+
android:icon="@drawable/ic_launcher_foreground"
42+
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
43+
<intent-filter>
44+
<action android:name="android.service.quicksettings.action.QS_TILE" />
45+
</intent-filter>
46+
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:value="true" />
47+
</service>
48+
<service
49+
android:name=".SIM1IMSStatusQSTileService"
50+
android:exported="true"
51+
android:label="@string/qs_status_tile_title_sim_1"
52+
android:icon="@drawable/ic_launcher_foreground"
53+
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
54+
<intent-filter>
55+
<action android:name="android.service.quicksettings.action.QS_TILE" />
56+
</intent-filter>
57+
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:value="true" />
58+
</service>
59+
<service
60+
android:name=".SIM2IMSStatusQSTileService"
61+
android:exported="true"
62+
android:label="@string/qs_status_tile_title_sim_2"
63+
android:icon="@drawable/ic_launcher_foreground"
64+
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
65+
<intent-filter>
66+
<action android:name="android.service.quicksettings.action.QS_TILE" />
67+
</intent-filter>
68+
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:value="true" />
69+
</service>
2670

2771
<provider
2872
android:name="rikka.shizuku.ShizukuProvider"

app/src/main/java/dev/bluehouse/enablevolte/HomeActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ fun PixelIMSApp() {
182182
}
183183
}
184184
},
185-
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.primary),
185+
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.primary),
186186
)
187187
},
188188
bottomBar = {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package dev.bluehouse.enablevolte
2+
3+
import android.service.quicksettings.Tile
4+
import android.service.quicksettings.TileService
5+
import org.lsposed.hiddenapibypass.HiddenApiBypass
6+
import java.lang.IllegalStateException
7+
8+
class SIM1IMSStatusQSTileService : IMSStatusQSTileService(0)
9+
class SIM2IMSStatusQSTileService : IMSStatusQSTileService(1)
10+
11+
open class IMSStatusQSTileService(private val simSlotIndex: Int) : TileService() {
12+
private val TAG = "SIM${simSlotIndex}IMSStatusQSTileService"
13+
14+
init {
15+
HiddenApiBypass.addHiddenApiExemptions("L")
16+
HiddenApiBypass.addHiddenApiExemptions("I")
17+
}
18+
19+
private val moder: SubscriptionModer? get() {
20+
val carrierModer = CarrierModer(this.applicationContext)
21+
22+
try {
23+
if (checkShizukuPermission(0) == ShizukuStatus.GRANTED && carrierModer.deviceSupportsIMS) {
24+
val sub = carrierModer.getActiveSubscriptionInfoForSimSlotIndex(this.simSlotIndex)
25+
?: return null
26+
return SubscriptionModer(sub.subscriptionId)
27+
}
28+
} catch (_: IllegalStateException) {}
29+
return null
30+
}
31+
private val imsActivated: Boolean? get() {
32+
/*
33+
* true: VoLTE enabled
34+
* false: VoLTE disabled
35+
* null: cannot determine status (Shizuku not running or permission not granted, SIM slot not active, ...)
36+
*/
37+
val moder = this.moder ?: return null
38+
try {
39+
return moder.isIMSRegistered
40+
} catch (_: IllegalStateException) {}
41+
return null
42+
}
43+
44+
override fun onTileAdded() {
45+
super.onTileAdded()
46+
if (this.imsActivated == null) {
47+
qsTile.state = Tile.STATE_UNAVAILABLE
48+
}
49+
}
50+
51+
private fun refreshStatus() {
52+
val imsActivated = this.imsActivated
53+
qsTile.state = when (imsActivated) {
54+
true -> Tile.STATE_ACTIVE
55+
false -> Tile.STATE_INACTIVE
56+
null -> Tile.STATE_UNAVAILABLE
57+
}
58+
qsTile.subtitle = getString(
59+
when (imsActivated) {
60+
true -> R.string.registered
61+
false -> R.string.unregistered
62+
null -> R.string.unknown
63+
},
64+
)
65+
qsTile.updateTile()
66+
}
67+
68+
override fun onStartListening() {
69+
super.onStartListening()
70+
this.refreshStatus()
71+
}
72+
73+
override fun onClick() {
74+
super.onClick()
75+
moder?.restartIMSRegistration()
76+
this.refreshStatus()
77+
}
78+
}

app/src/main/java/dev/bluehouse/enablevolte/Moder.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ open class Moder {
7777
}
7878

7979
class CarrierModer(private val context: Context) : Moder() {
80+
fun getActiveSubscriptionInfoForSimSlotIndex(index: Int): SubscriptionInfo? {
81+
val sub = this.loadCachedInterface { sub }
82+
return sub.getActiveSubscriptionInfoForSimSlotIndex(index, null, null)
83+
}
84+
8085
val subscriptions: List<SubscriptionInfo>
8186
get() {
8287
val sub = this.loadCachedInterface { sub }
@@ -260,6 +265,9 @@ class SubscriptionModer(val subscriptionId: Int) : Moder() {
260265
return config.get(key)
261266
}
262267

268+
val simSlotIndex: Int
269+
get() = this.loadCachedInterface { sub }.getSlotIndex(subscriptionId)
270+
263271
val isVoLteConfigEnabled: Boolean
264272
get() = this.getBooleanValue(CarrierConfigManager.KEY_CARRIER_VOLTE_AVAILABLE_BOOL)
265273

app/src/main/java/dev/bluehouse/enablevolte/Utils.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ fun checkShizukuPermission(code: Int): ShizukuStatus {
3333
}
3434

3535
val SubscriptionInfo.uniqueName: String
36-
get() = "${this.subscriptionId} - ${this.displayName}"
36+
get() = "${this.displayName} (SIM ${this.simSlotIndex + 1})"
3737

3838
fun getLatestAppVersion(handler: (String) -> Unit) {
3939
"https://api.github.com/repos/kyujin-cho/pixel-volte-patch/releases"
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package dev.bluehouse.enablevolte
2+
3+
import android.service.quicksettings.Tile
4+
import android.service.quicksettings.TileService
5+
import android.telephony.CarrierConfigManager
6+
import org.lsposed.hiddenapibypass.HiddenApiBypass
7+
import java.lang.IllegalStateException
8+
9+
class SIM1VoLTEConfigToggleQSTileService : VoLTEConfigToggleQSTileService(0)
10+
class SIM2VoLTEConfigToggleQSTileService : VoLTEConfigToggleQSTileService(1)
11+
12+
open class VoLTEConfigToggleQSTileService(private val simSlotIndex: Int) : TileService() {
13+
private val TAG = "SIM${simSlotIndex}VoLTEConfigToggleQSTileService"
14+
15+
init {
16+
HiddenApiBypass.addHiddenApiExemptions("L")
17+
HiddenApiBypass.addHiddenApiExemptions("I")
18+
}
19+
20+
private val moder: SubscriptionModer? get() {
21+
val carrierModer = CarrierModer(this.applicationContext)
22+
23+
try {
24+
if (checkShizukuPermission(0) == ShizukuStatus.GRANTED && carrierModer.deviceSupportsIMS) {
25+
carrierModer.subscriptions
26+
val sub = carrierModer.getActiveSubscriptionInfoForSimSlotIndex(this.simSlotIndex)
27+
?: return null
28+
return SubscriptionModer(sub.subscriptionId)
29+
}
30+
} catch (_: IllegalStateException) {}
31+
return null
32+
}
33+
34+
private val volteEnabled: Boolean? get() {
35+
/*
36+
* true: VoLTE enabled
37+
* false: VoLTE disabled
38+
* null: cannot determine status (Shizuku not running or permission not granted, SIM slot not active, ...)
39+
*/
40+
val moder = this.moder ?: return null
41+
try {
42+
return moder.isVoLteConfigEnabled
43+
} catch (_: IllegalStateException) {}
44+
return null
45+
}
46+
47+
override fun onTileAdded() {
48+
super.onTileAdded()
49+
if (this.volteEnabled == null) {
50+
qsTile.state = Tile.STATE_UNAVAILABLE
51+
}
52+
}
53+
54+
override fun onStartListening() {
55+
super.onStartListening()
56+
qsTile.state = when (this.volteEnabled) {
57+
true -> Tile.STATE_ACTIVE
58+
false -> Tile.STATE_INACTIVE
59+
null -> Tile.STATE_UNAVAILABLE
60+
}
61+
qsTile.subtitle = getString(
62+
when (this.volteEnabled) {
63+
true -> R.string.enabled
64+
false -> R.string.disabled
65+
null -> R.string.unknown
66+
},
67+
)
68+
qsTile.updateTile()
69+
}
70+
71+
private fun toggleVoLTEStatus() {
72+
val moder = this.moder ?: return
73+
val volteEnabled = this.volteEnabled ?: return
74+
moder.updateCarrierConfig(CarrierConfigManager.KEY_CARRIER_VOLTE_AVAILABLE_BOOL, !volteEnabled)
75+
moder.restartIMSRegistration()
76+
qsTile.state = if (volteEnabled) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE
77+
qsTile.subtitle = getString(if (volteEnabled) R.string.disabled else R.string.enabled)
78+
qsTile.updateTile()
79+
}
80+
81+
// Called when the user taps on your tile in an active or inactive state.
82+
override fun onClick() {
83+
super.onClick()
84+
if (isLocked) {
85+
unlockAndRun { toggleVoLTEStatus() }
86+
} else {
87+
toggleVoLTEStatus()
88+
}
89+
}
90+
}

app/src/main/java/dev/bluehouse/enablevolte/pages/Config.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package dev.bluehouse.enablevolte.pages
22

3+
import android.app.StatusBarManager
4+
import android.content.ComponentName
5+
import android.graphics.drawable.Icon
36
import android.os.Build
7+
import android.os.Build.VERSION
48
import android.telephony.CarrierConfigManager
59
import android.util.Log
610
import android.widget.Toast
@@ -71,6 +75,8 @@ fun Config(navController: NavController, subId: Int) {
7175
var reversedConfigurableItems by rememberSaveable { mutableStateOf<Map<String, String>>(mapOf()) }
7276
var loading by rememberSaveable { mutableStateOf(true) }
7377
val scope = rememberCoroutineScope()
78+
val simSlotIndex = moder.simSlotIndex
79+
val statusBarManager: StatusBarManager = context.getSystemService(StatusBarManager::class.java)
7480

7581
fun loadFlags() {
7682
Log.d(TAG, "loadFlags")
@@ -337,6 +343,40 @@ fun Config(navController: NavController, subId: Int) {
337343
}
338344
}
339345

346+
if (VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
347+
HeaderText(text = stringResource(R.string.qstile))
348+
ClickablePropertyView(
349+
label = stringResource(R.string.add_status_tile),
350+
value = "",
351+
) {
352+
statusBarManager.requestAddTileService(
353+
ComponentName(
354+
context,
355+
// TODO: what happens if someone tries to use this feature from a triple(or even dual)-SIM phone?
356+
Class.forName("dev.bluehouse.enablevolte.SIM${simSlotIndex + 1}IMSStatusQSTileService"),
357+
),
358+
context.getString(R.string.qs_status_tile_title, (simSlotIndex + 1).toString()),
359+
Icon.createWithResource(context, R.drawable.ic_launcher_foreground),
360+
{},
361+
{},
362+
)
363+
}
364+
ClickablePropertyView(
365+
label = stringResource(R.string.add_toggle_tile),
366+
value = "",
367+
) {
368+
statusBarManager.requestAddTileService(
369+
ComponentName(
370+
context,
371+
Class.forName("dev.bluehouse.enablevolte.SIM${simSlotIndex + 1}VoLTEConfigToggleQSTileService"),
372+
),
373+
context.getString(R.string.qs_toggle_tile_title, (simSlotIndex + 1).toString()),
374+
Icon.createWithResource(context, R.drawable.ic_launcher_foreground),
375+
{},
376+
{},
377+
)
378+
}
379+
}
340380
HeaderText(text = stringResource(R.string.miscellaneous))
341381
ClickablePropertyView(
342382
label = stringResource(R.string.reset_all_settings),

app/src/main/res/values-zh-rCN/strings.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
<string name="permissions_capabilities"><![CDATA[权限与兼容性]]></string>
3434
<string name="registered">已注册</string>
3535
<string name="unregistered">未注册</string>
36+
<string name="enabled">Enabled</string>
37+
<string name="disabled">Disabled</string>
3638
<string name="newer_version_available">有新版本 %1$s 可用!</string>
3739
<string name="feature_toggles">启用/禁用功能</string>
3840
<string name="cosmetic_toggles">显示设置</string>
@@ -67,6 +69,15 @@
6769
<string name="loaded">已加载 %1$s 共 %2$s</string>
6870
<string name="edit_value">更改值</string>
6971
<string name="search">搜索</string>
72+
<string name="qstile">Quick Settings Tile</string>
73+
<string name="qs_status_tile_title">IMS Status (SIM %1$s)</string>
74+
<string name="qs_status_tile_title_sim_1">IMS Status (SIM 1)</string>
75+
<string name="qs_status_tile_title_sim_2">IMS Status (SIM 2)</string>
76+
<string name="qs_toggle_tile_title">VoLTE Config (SIM %1$s)</string>
77+
<string name="qs_toggle_tile_title_sim_1">VoLTE Config (SIM 1)</string>
78+
<string name="qs_toggle_tile_title_sim_2">VoLTE Config (SIM 2)</string>
79+
<string name="add_status_tile">Add IMS status display tile</string>
80+
<string name="add_toggle_tile">Add VoLTE config toggle tile</string>
7081
<string name="sim_config">SIM 配置</string>
7182
<string name="config_dump_viewer">配置转储查看器</string>
7283
</resources>

app/src/main/res/values/strings.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
<string name="permissions_capabilities"><![CDATA[Permissions & Capabilities]]></string>
3333
<string name="registered">Registered</string>
3434
<string name="unregistered">Unregistered</string>
35+
<string name="enabled">Enabled</string>
36+
<string name="disabled">Disabled</string>
3537
<string name="newer_version_available">Newer version %1$s available!</string>
3638
<string name="feature_toggles">Enable/Disable Feature</string>
3739
<string name="cosmetic_toggles">Cosmetic Toggles</string>
@@ -66,6 +68,15 @@
6668
<string name="loaded">Loaded %1$s of %2$s</string>
6769
<string name="edit_value">Edit Value</string>
6870
<string name="search">Search</string>
71+
<string name="qstile">Quick Settings Tile</string>
72+
<string name="qs_status_tile_title">IMS Status (SIM %1$s)</string>
73+
<string name="qs_status_tile_title_sim_1">IMS Status (SIM 1)</string>
74+
<string name="qs_status_tile_title_sim_2">IMS Status (SIM 2)</string>
75+
<string name="qs_toggle_tile_title">VoLTE Config (SIM %1$s)</string>
76+
<string name="qs_toggle_tile_title_sim_1">VoLTE Config (SIM 1)</string>
77+
<string name="qs_toggle_tile_title_sim_2">VoLTE Config (SIM 2)</string>
78+
<string name="add_status_tile">Add IMS status display tile</string>
79+
<string name="add_toggle_tile">Add VoLTE config toggle tile</string>
6980
<string name="sim_config">SIM Config</string>
7081
<string name="config_dump_viewer">Config Dump Viewer</string>
7182
</resources>

0 commit comments

Comments
 (0)