remove unused hook

This commit is contained in:
Kavish Devar
2026-03-30 17:52:19 +05:30
parent 2cd35a7e77
commit 28c5510417

View File

@@ -2,22 +2,9 @@ package me.kavishdevar.librepods.utils
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.ParcelUuid
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.net.toUri
import io.github.libxposed.api.XposedInterface
import io.github.libxposed.api.XposedInterface.AfterHookCallback
@@ -26,6 +13,7 @@ import io.github.libxposed.api.XposedModuleInterface
import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam
import io.github.libxposed.api.annotations.AfterInvocation
import io.github.libxposed.api.annotations.XposedHooker
import kotlin.jvm.java
private const val TAG = "AirPodsHook"
private lateinit var module: KotlinModule
@@ -67,17 +55,6 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul
hook(updateIconMethod, BluetoothIconHooker::class.java)
Log.i(TAG, "Successfully hooked updateIcon method in Bluetooth settings")
try {
val displayPreferenceMethod = headerControllerClass.getDeclaredMethod(
"displayPreference",
param.classLoader.loadClass("androidx.preference.PreferenceScreen"))
hook(displayPreferenceMethod, BluetoothSettingsAirPodsHooker::class.java)
Log.i(TAG, "Successfully hooked displayPreference for AirPods button injection")
} catch (e: Exception) {
Log.e(TAG, "Failed to hook displayPreference: ${e.message}", e)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to hook Bluetooth icon handler: ${e.message}", e)
}
@@ -96,110 +73,12 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul
hook(updateIconMethod, BluetoothIconHooker::class.java)
Log.i(TAG, "Successfully hooked updateIcon method in Bluetooth settings")
try {
val displayPreferenceMethod = headerControllerClass.getDeclaredMethod(
"displayPreference",
param.classLoader.loadClass("androidx.preference.PreferenceScreen"))
hook(displayPreferenceMethod, BluetoothSettingsAirPodsHooker::class.java)
Log.i(TAG, "Successfully hooked displayPreference for AirPods button injection")
} catch (e: Exception) {
Log.e(TAG, "Failed to hook displayPreference: ${e.message}", e)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to hook Bluetooth icon handler: ${e.message}", e)
}
}
}
@XposedHooker
class BluetoothSettingsAirPodsHooker : XposedInterface.Hooker {
companion object {
private const val AIRPODS_UUID = "74ec2172-0bad-4d01-8f77-997b2be0722a"
private const val LIBREPODS_PREFERENCE_KEY = "librepods_open_preference"
private const val ACTION_SET_ANC_MODE = "me.kavishdevar.librepods.SET_ANC_MODE"
private const val EXTRA_ANC_MODE = "anc_mode"
private const val ANC_MODE_OFF = 1
private const val ANC_MODE_NOISE_CANCELLATION = 2
private const val ANC_MODE_TRANSPARENCY = 3
private const val ANC_MODE_ADAPTIVE = 4
private var currentAncMode = ANC_MODE_NOISE_CANCELLATION
@JvmStatic
@AfterInvocation
fun afterDisplayPreference(callback: AfterHookCallback) {
try {
val controller = callback.thisObject!!
val preferenceScreen = callback.args[0]!!
val context = preferenceScreen.javaClass.getMethod("getContext").invoke(preferenceScreen) as Context
val deviceField = controller.javaClass.getDeclaredField("mCachedDevice")
deviceField.isAccessible = true
val cachedDevice = deviceField.get(controller) ?: return
val getDeviceMethod = cachedDevice.javaClass.getMethod("getDevice")
val bluetoothDevice = getDeviceMethod.invoke(cachedDevice) ?: return
val uuidsMethod = bluetoothDevice.javaClass.getMethod("getUuids")
val uuids = uuidsMethod.invoke(bluetoothDevice) as? Array<ParcelUuid>
if (uuids != null) {
val isAirPods = uuids.any { it.uuid.toString() == AIRPODS_UUID }
if (isAirPods) {
Log.i(TAG, "AirPods device detected in settings, injecting controls")
val findPreferenceMethod = preferenceScreen.javaClass.getMethod("findPreference", CharSequence::class.java)
val existingPref = findPreferenceMethod.invoke(preferenceScreen, LIBREPODS_PREFERENCE_KEY)
if (existingPref != null) {
Log.i(TAG, "LIBREPODS button already exists, skipping")
return
}
val preferenceClass = preferenceScreen.javaClass.classLoader.loadClass("androidx.preference.Preference")
val preference = preferenceClass.getConstructor(Context::class.java).newInstance(context)
val setKeyMethod = preferenceClass.getMethod("setKey", String::class.java)
setKeyMethod.invoke(preference, LIBREPODS_PREFERENCE_KEY)
val setTitleMethod = preferenceClass.getMethod("setTitle", CharSequence::class.java)
setTitleMethod.invoke(preference, "Open LibrePods")
val setSummaryMethod = preferenceClass.getMethod("setSummary", CharSequence::class.java)
setSummaryMethod.invoke(preference, "Control AirPods features")
val setIconMethod = preferenceClass.getMethod("setIcon", Int::class.java)
setIconMethod.invoke(preference, android.R.drawable.ic_menu_manage)
val setOrderMethod = preferenceClass.getMethod("setOrder", Int::class.java)
setOrderMethod.invoke(preference, 1000)
val intent = Intent().apply {
setClassName("me.kavishdevar.librepods", "me.kavishdevar.librepods.MainActivity")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val setIntentMethod = preferenceClass.getMethod("setIntent", Intent::class.java)
setIntentMethod.invoke(preference, intent)
val addPreferenceMethod = preferenceScreen.javaClass.getMethod("addPreference", preferenceClass)
addPreferenceMethod.invoke(preferenceScreen, preference)
Log.i(TAG, "Successfully added Open LIBREPODS button to AirPods settings")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error in BluetoothSettingsAirPodsHooker: ${e.message}", e)
e.printStackTrace()
}
}
}
}
@XposedHooker
class BluetoothIconHooker : XposedInterface.Hooker {
companion object {
@@ -267,497 +146,4 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul
override fun getApplicationInfo(): ApplicationInfo {
return super.applicationInfo
}
companion object {
private const val ANC_MODE_OFF = 1
private const val ANC_MODE_NOISE_CANCELLATION = 2
private const val ANC_MODE_TRANSPARENCY = 3
private const val ANC_MODE_ADAPTIVE = 4
private var currentANCMode = ANC_MODE_NOISE_CANCELLATION
private const val ACTION_SET_ANC_MODE = "me.kavishdevar.librepods.SET_ANC_MODE"
private const val EXTRA_ANC_MODE = "anc_mode"
private const val ANIMATION_DURATION = 250L
private fun addAirPodsControlsToDialog(volumeDialog: Any) {
try {
val contextField = volumeDialog.javaClass.getDeclaredField("mContext")
contextField.isAccessible = true
val context = contextField.get(volumeDialog) as Context
val dialogViewField = volumeDialog.javaClass.getDeclaredField("mDialogView")
dialogViewField.isAccessible = true
val dialogView = dialogViewField.get(volumeDialog) as ViewGroup
val dialogRowsViewField = volumeDialog.javaClass.getDeclaredField("mDialogRowsView")
dialogRowsViewField.isAccessible = true
val dialogRowsView = dialogRowsViewField.get(volumeDialog) as ViewGroup
Log.d(TAG, "Found dialogRowsView: ${dialogRowsView.javaClass.name}")
val existingContainer = dialogView.findViewWithTag<View>("airpods_container")
if (existingContainer != null) {
Log.d(TAG, "AirPods container already exists, ensuring visibility state")
val drawer = existingContainer.findViewWithTag<View>("airpods_drawer_container")
drawer?.visibility = View.GONE
drawer?.alpha = 0f
drawer?.translationY = 0f
val button = existingContainer.findViewWithTag<ImageButton>("airpods_button")
button?.visibility = View.VISIBLE
button?.alpha = 1f
if (button != null) {
updateMainButtonIcon(context, button, currentANCMode)
}
return
}
val newAirPodsButton = ImageButton(context).apply {
tag = "airpods_button"
try {
val airPodsPackage = context.createPackageContext(
"me.kavishdevar.librepods",
Context.CONTEXT_IGNORE_SECURITY
)
val airPodsIconRes = airPodsPackage.resources.getIdentifier(
"airpods", "drawable", "me.kavishdevar.librepods")
if (airPodsIconRes != 0) {
val airPodsDrawable = airPodsPackage.resources.getDrawable(
airPodsIconRes, airPodsPackage.theme)
setImageDrawable(airPodsDrawable)
} else {
setImageResource(android.R.drawable.ic_media_play)
Log.d(TAG, "Using fallback icon because airpods icon resource not found")
}
} catch (e: Exception) {
setImageResource(android.R.drawable.ic_media_play)
Log.e(TAG, "Failed to load AirPods icon: ${e.message}")
}
val shape = GradientDrawable()
shape.shape = GradientDrawable.RECTANGLE
shape.setColor(Color.BLACK)
background = shape
imageTintList = ColorStateList.valueOf(Color.WHITE)
scaleType = ImageView.ScaleType.CENTER_INSIDE
setPadding(24, 24, 24, 24)
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
90
)
params.gravity = Gravity.CENTER
params.setMargins(0, 0, 0, 0)
layoutParams = params
setOnClickListener {
Log.d(TAG, "AirPods button clicked, toggling drawer")
val container = findAirPodsContainer(this)
val drawerContainer = container?.findViewWithTag<View>("airpods_drawer_container")
if (drawerContainer != null && container != null) {
if (drawerContainer.visibility == View.VISIBLE) {
hideAirPodsDrawer(container, this, drawerContainer)
} else {
showAirPodsDrawer(container, this, drawerContainer)
}
} else {
Log.e(TAG, "Could not find container or drawer for toggle")
}
}
contentDescription = "AirPods Settings"
}
val airPodsContainer = FrameLayout(context).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
tag = "airpods_container"
}
newAirPodsButton.setOnLongClickListener {
Log.d(TAG, "AirPods button long-pressed, opening QuickSettingsDialogActivity")
val intent = Intent().apply {
setClassName("me.kavishdevar.librepods", "me.kavishdevar.librepods.QuickSettingsDialogActivity")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
try {
val dismissMethod = volumeDialog.javaClass.getMethod("dismissH")
dismissMethod.invoke(volumeDialog)
} catch (e: Exception) {
Log.w(TAG, "Could not dismiss volume dialog: ${e.message}")
}
true
}
val airPodsDrawer = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = Gravity.TOP
}
tag = "airpods_drawer_container"
visibility = View.GONE
alpha = 0f
val drawerShape = GradientDrawable()
drawerShape.shape = GradientDrawable.RECTANGLE
drawerShape.setColor(Color.BLACK)
background = drawerShape
setPadding(16, 8, 16, 8)
}
val buttonContainer = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = Gravity.TOP
}
tag = "airpods_button_container"
}
val modes = listOf(ANC_MODE_OFF, ANC_MODE_TRANSPARENCY, ANC_MODE_ADAPTIVE, ANC_MODE_NOISE_CANCELLATION)
for (mode in modes) {
val modeOption = createAncModeOption(context, mode, mode == currentANCMode, newAirPodsButton)
airPodsDrawer.addView(modeOption)
}
buttonContainer.addView(newAirPodsButton)
airPodsContainer.addView(airPodsDrawer)
airPodsContainer.addView(buttonContainer)
val settingsViewField = try {
val field = volumeDialog.javaClass.getDeclaredField("mSettingsView")
field.isAccessible = true
field.get(volumeDialog) as? View
} catch (e: Exception) {
Log.e(TAG, "Failed to get settings view field: ${e.message}")
null
}
if (settingsViewField != null && settingsViewField.parent is ViewGroup) {
val settingsParent = settingsViewField.parent as ViewGroup
val settingsIndex = findViewIndexInParent(settingsParent, settingsViewField)
if (settingsIndex >= 0) {
settingsParent.addView(airPodsContainer, settingsIndex)
Log.i(TAG, "Added AirPods controls before settings button")
} else {
settingsParent.addView(airPodsContainer)
Log.i(TAG, "Added AirPods controls to the end of settings parent")
}
} else {
dialogView.addView(airPodsContainer)
Log.i(TAG, "Fallback: Added AirPods controls to dialog view")
}
updateMainButtonIcon(context, newAirPodsButton, currentANCMode)
Log.i(TAG, "Successfully added AirPods button and drawer to volume dialog")
} catch (e: Exception) {
Log.e(TAG, "Error adding AirPods button to volume panel: ${e.message}")
e.printStackTrace()
}
}
private fun findViewIndexInParent(parent: ViewGroup, view: View): Int {
for (i in 0 until parent.childCount) {
if (parent.getChildAt(i) == view) {
return i
}
}
return -1
}
private fun updateMainButtonIcon(context: Context, button: ImageButton, mode: Int) {
try {
val pkgContext = context.createPackageContext(
"me.kavishdevar.librepods",
Context.CONTEXT_IGNORE_SECURITY
)
val resName = when (mode) {
ANC_MODE_OFF -> "noise_cancellation"
ANC_MODE_TRANSPARENCY -> "transparency"
ANC_MODE_ADAPTIVE -> "adaptive"
ANC_MODE_NOISE_CANCELLATION -> "noise_cancellation"
else -> "noise_cancellation"
}
val resId = pkgContext.resources.getIdentifier(
resName, "drawable", "me.kavishdevar.librepods"
)
if (resId != 0) {
val drawable = pkgContext.resources.getDrawable(resId, pkgContext.theme)
button.setImageDrawable(drawable)
button.setColorFilter(Color.WHITE)
} else {
button.setImageResource(getIconResourceForMode(mode))
button.setColorFilter(Color.WHITE)
}
} catch (e: Exception) {
button.setImageResource(getIconResourceForMode(mode))
button.setColorFilter(Color.WHITE)
}
}
private fun createAncModeOption(context: Context, mode: Int, isSelected: Boolean, mainButton: ImageButton): LinearLayout {
return LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 6, 0, 6)
}
gravity = Gravity.CENTER
setPadding(24, 16, 24, 16)
tag = "anc_mode_${mode}"
val icon = ImageView(context).apply {
layoutParams = LinearLayout.LayoutParams(60, 60).apply {
gravity = Gravity.CENTER
}
tag = "mode_icon_$mode"
try {
val packageContext = context.createPackageContext(
"me.kavishdevar.librepods",
Context.CONTEXT_IGNORE_SECURITY
)
val resourceName = when (mode) {
ANC_MODE_OFF -> "noise_cancellation"
ANC_MODE_TRANSPARENCY -> "transparency"
ANC_MODE_ADAPTIVE -> "adaptive"
ANC_MODE_NOISE_CANCELLATION -> "noise_cancellation"
else -> "noise_cancellation"
}
val resourceId = packageContext.resources.getIdentifier(
resourceName, "drawable", "me.kavishdevar.librepods"
)
if (resourceId != 0) {
val drawable = packageContext.resources.getDrawable(
resourceId, packageContext.theme
)
setImageDrawable(drawable)
} else {
setImageResource(getIconResourceForMode(mode))
}
} catch (e: Exception) {
setImageResource(getIconResourceForMode(mode))
Log.e(TAG, "Failed to load custom drawable for mode $mode: ${e.message}")
}
if (isSelected) {
setColorFilter(Color.BLACK)
} else {
setColorFilter(Color.WHITE)
}
}
addView(icon)
background = if (isSelected) {
createSelectedBackground(context)
} else {
null
}
setOnClickListener {
Log.d(TAG, "ANC mode selected: $mode (was: $currentANCMode)")
val container = findAirPodsContainer(this)
val drawerContainer = container?.findViewWithTag<View>("airpods_drawer_container")
if (currentANCMode == mode) {
if (drawerContainer != null && container != null) {
hideAirPodsDrawer(container, mainButton, drawerContainer)
}
return@setOnClickListener
}
currentANCMode = mode
val parentDrawer = parent as? ViewGroup
if (parentDrawer != null) {
for (i in 0 until parentDrawer.childCount) {
val child = parentDrawer.getChildAt(i) as? LinearLayout
if (child != null && child.tag.toString().startsWith("anc_mode_")) {
val childModeStr = child.tag.toString().substringAfter("anc_mode_")
val childMode = childModeStr.toIntOrNull() ?: -1
val childIcon = child.findViewWithTag<ImageView>("mode_icon_${childMode}")
if (childMode == mode) {
child.background = createSelectedBackground(context)
childIcon?.setColorFilter(Color.BLACK)
} else {
child.background = null
childIcon?.setColorFilter(Color.WHITE)
}
}
}
}
val intent = Intent(ACTION_SET_ANC_MODE).apply {
setPackage("me.kavishdevar.librepods")
putExtra(EXTRA_ANC_MODE, mode)
}
context.sendBroadcast(intent)
Log.d(TAG, "Sent broadcast to change ANC mode to: ${getLabelForMode(currentANCMode)}")
updateMainButtonIcon(context, mainButton, mode)
if (drawerContainer != null && container != null) {
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
hideAirPodsDrawer(container, mainButton, drawerContainer)
}, 50)
}
}
}
}
private fun createSelectedBackground(context: Context): GradientDrawable {
return GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
setColor(Color.WHITE)
cornerRadius = 50f
}
}
private fun findAirPodsContainer(view: View): ViewGroup? {
var current: View? = view
while (current != null) {
if (current is ViewGroup && current.tag == "airpods_container") {
return current
}
val parent = current.parent
if (parent is ViewGroup && parent.tag == "airpods_container") {
return parent
}
current = parent as? View
}
Log.w(TAG, "Could not find airpods_container ancestor")
return null
}
private fun showAirPodsDrawer(container: ViewGroup, mainButton: ImageButton, drawerContainer: View) {
Log.d(TAG, "Showing AirPods drawer")
val selectedModeView = drawerContainer.findViewWithTag<View>("anc_mode_$currentANCMode")
val selectedModeIcon = selectedModeView?.findViewWithTag<ImageView>("mode_icon_$currentANCMode")
val buttonContainer = container.findViewWithTag<View>("airpods_button_container")
if (selectedModeView == null || selectedModeIcon == null) {
Log.e(TAG, "Cannot find selected mode view or icon for show animation")
drawerContainer.alpha = 0f
drawerContainer.visibility = View.VISIBLE
drawerContainer.animate()
.alpha(1f)
.setDuration(ANIMATION_DURATION)
.start()
buttonContainer?.animate()
?.alpha(0f)
?.setDuration(ANIMATION_DURATION / 2)
?.setStartDelay(ANIMATION_DURATION / 2)
?.withEndAction {
buttonContainer.visibility = View.GONE
}
?.start()
return
}
drawerContainer.measure(
View.MeasureSpec.makeMeasureSpec(container.width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
val drawerHeight = drawerContainer.measuredHeight
drawerContainer.alpha = 0f
drawerContainer.visibility = View.VISIBLE
drawerContainer.translationY = -drawerHeight.toFloat()
drawerContainer.animate()
.translationY(0f)
.alpha(1f)
.setDuration(ANIMATION_DURATION)
.setInterpolator(DecelerateInterpolator())
.start()
buttonContainer?.animate()
?.alpha(0f)
?.setDuration(ANIMATION_DURATION / 2)
?.setStartDelay(ANIMATION_DURATION / 3)
?.withEndAction {
buttonContainer.visibility = View.GONE
}
?.start()
}
private fun hideAirPodsDrawer(container: ViewGroup, mainButton: ImageButton, drawerContainer: View) {
Log.d(TAG, "Hiding AirPods drawer")
val buttonContainer = container.findViewWithTag<View>("airpods_button_container")
if (buttonContainer != null && buttonContainer.visibility != View.VISIBLE) {
buttonContainer.alpha = 0f
buttonContainer.visibility = View.VISIBLE
}
buttonContainer?.animate()
?.alpha(1f)
?.setDuration(ANIMATION_DURATION / 2)
?.start()
drawerContainer.animate()
.translationY(-drawerContainer.height.toFloat())
.alpha(0f)
.setDuration(ANIMATION_DURATION)
.setInterpolator(AccelerateInterpolator())
.setStartDelay(ANIMATION_DURATION / 4)
.withEndAction {
drawerContainer.visibility = View.GONE
drawerContainer.translationY = 0f
}
.start()
}
private fun getIconResourceForMode(mode: Int): Int {
return when (mode) {
ANC_MODE_OFF -> android.R.drawable.ic_lock_silent_mode
ANC_MODE_TRANSPARENCY -> android.R.drawable.ic_lock_silent_mode_off
ANC_MODE_ADAPTIVE -> android.R.drawable.ic_menu_compass
ANC_MODE_NOISE_CANCELLATION -> android.R.drawable.ic_lock_idle_charging
else -> android.R.drawable.ic_lock_silent_mode_off
}
}
private fun getLabelForMode(mode: Int): String {
return when (mode) {
ANC_MODE_OFF -> "Off"
ANC_MODE_TRANSPARENCY -> "Transparency"
ANC_MODE_ADAPTIVE -> "Adaptive"
ANC_MODE_NOISE_CANCELLATION -> "Noise Cancellation"
else -> "Unknown"
}
}
}
}