A standardized framework for implementing custom Quick Settings (QS) tiles with built-in haptic feedback, permission handling, and root/shell fallback.
All QS tiles in the application inherit from BaseTileService, which abstracts away the complexity of tile lifecycle, IPC caching, and secure settings management.
- Immediate Feedback: Triggers haptics via
HapticUtiland updates the tile state to "Working..." immediately on click. - Permission Guard: Automatically checks
hasFeaturePermission()and disables the tile if required permissions (likeWRITE_SECURE_SETTINGS) are missing. - Settings Fallback: Provides protected methods to read/write
SecureandGlobalsettings with automatic fallback to Shell/Root commands if the standard Android API fails. - Background Scopes: Integrated
CoroutineScopefor offloading work from the UI thread.
Every tile must implement:
onTileClick(): The actual logic performed when the tile is toggled.getTileLabel(): The primary text on the tile.getTileSubtitle(): Secondary text (e.g., "On", "Off", or "Permission missing").getTileState(): ReturnsSTATE_ACTIVEorSTATE_INACTIVE.hasFeaturePermission(): Logic to check if the tile can be used.
abstract class BaseTileService : TileService() {
override fun onClick() {
// 1. Immediate Haptic Feedback
HapticUtil.performHapticForService(this)
if (!hasFeaturePermission() || isProcessing) return
// 2. Visual "Processing" State
isProcessing = true
updateTile()
// 3. Offload Work
serviceScope.launch {
onTileClick()
withContext(Dispatchers.Main) {
isProcessing = false
updateTile()
}
}
}
}This pattern ensures tiles work even on devices where the standard API is restricted.
protected fun putSecureInt(key: String, value: Int) {
try {
Settings.Secure.putInt(contentResolver, key, value)
} catch (e: Exception) {
// Fallback to Shell if the app doesn't have WRITE_SECURE_SETTINGS
ShellUtils.runCommand(this, "settings put secure $key $value")
}
}class MonoAudioTileService : BaseTileService() {
override fun getTileLabel() = "Mono Audio"
override fun getTileSubtitle() = if (isEnabled()) "On" else "Off"
override fun getTileState() = if (isEnabled()) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
override fun onTileClick() {
val newState = if (isEnabled()) 0 else 1
putSecureInt("master_mono", newState)
}
override fun hasFeaturePermission() = PermissionUtils.canWriteSecureSettings(this) || ShellUtils.isAvailable(this)
}Each tile service must be declared in the AndroidManifest.xml with the appropriate permission and intent-filter.
<service
android:name=".services.tiles.MonoAudioTileService"
android:icon="@drawable/ic_mono"
android:label="Mono Audio"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>Per user-defined rules, all added QS tiles should also be exposed in the "Quick Settings Tiles" section of the app's settings UI, allowing users to discover and learn how to add them to their system panel.