mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36:12 -04:00
Add preferences for AirPods Identity and Encryption Keys
This commit introduces settings for users to input their AirPods' Identity Resolving Key (IRK) and Encryption Key. These keys, obtainable via a MacBook, allow CAPod to reliably identify the AirPods and decrypt detailed status information. Key changes: - Added new preferences in General Settings for IRK and Encryption Key. - Implemented a custom dialog for key input, including validation and a link to a guide. - Added a Moshi adapter for serializing/deserializing `ByteArray` to/from Base64 for storing the keys. - Included new string resources for labels, descriptions, and explanations related to the keys. - Added new icons for the key preferences.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package eu.darken.capod.main.ui.settings.general
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.UIConverter
|
||||
|
||||
class AirPodKeyInputDialog(private val context: Context) {
|
||||
fun create(
|
||||
mode: Mode,
|
||||
current: String,
|
||||
onKey: (String) -> Unit,
|
||||
onGuide: () -> Unit,
|
||||
): AlertDialog {
|
||||
val inputEditText = TextInputEditText(context).apply {
|
||||
setText(current)
|
||||
}
|
||||
val inputLayout =
|
||||
TextInputLayout(context, null, com.google.android.material.R.attr.textInputOutlinedStyle).apply {
|
||||
hint = context.getString(
|
||||
R.string.general_example_label,
|
||||
"FE-D0-1C-54-11-81-BC-BC-87-D2-C4-3F-31-64-5F-EE"
|
||||
)
|
||||
addView(inputEditText)
|
||||
val padding = UIConverter.convertDpToPixels(context, 24f)
|
||||
setPadding(padding, padding / 2, padding, 0)
|
||||
}
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(context).apply {
|
||||
setView(inputLayout)
|
||||
when (mode) {
|
||||
Mode.IRK -> {
|
||||
setTitle(R.string.settings_maindevice_identitykey_label)
|
||||
setMessage(R.string.settings_maindevice_identitykey_explanation)
|
||||
}
|
||||
|
||||
Mode.ENC -> {
|
||||
setTitle(R.string.settings_maindevice_encryptionkey_label)
|
||||
setMessage(R.string.settings_maindevice_encryptionkey_explanation)
|
||||
}
|
||||
}
|
||||
|
||||
setPositiveButton(R.string.general_save_action) { _, _ -> onKey(inputEditText.text.toString()) }
|
||||
setNegativeButton(R.string.general_cancel_action) { _, _ -> }
|
||||
setNeutralButton(R.string.general_guide_action) { _, _ -> onGuide() }
|
||||
}.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
val saveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
|
||||
val keyRegex = Regex("^([0-9A-F]{2}-){15}[0-9A-F]{2}$")
|
||||
fun validateInput() {
|
||||
val isValid = inputEditText.text.toString().matches(keyRegex)
|
||||
saveButton.isEnabled = isValid || inputEditText.length() == 0
|
||||
}
|
||||
inputEditText.doOnTextChanged { _, _, _, _ -> validateInput() }
|
||||
validateInput()
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
IRK,
|
||||
ENC,
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.WebpageTool
|
||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||
import eu.darken.capod.common.preferences.PercentSliderPreference
|
||||
import eu.darken.capod.common.uix.PreferenceFragment3
|
||||
@@ -25,6 +26,7 @@ class GeneralSettingsFragment : PreferenceFragment3() {
|
||||
|
||||
@Inject lateinit var generalSettings: GeneralSettings
|
||||
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||
@Inject lateinit var webpageTool: WebpageTool
|
||||
|
||||
override val settings: GeneralSettings
|
||||
get() = generalSettings
|
||||
@@ -35,6 +37,8 @@ class GeneralSettingsFragment : PreferenceFragment3() {
|
||||
private val scanModePref by lazy { findPreference<ListPreference>(generalSettings.scannerMode.key)!! }
|
||||
private val mainDeviceAddressPref by lazy { findPreference<Preference>(generalSettings.mainDeviceAddress.key)!! }
|
||||
private val mainDeviceModelPref by lazy { findPreference<Preference>(generalSettings.mainDeviceModel.key)!! }
|
||||
private val mainDeviceIdentityKeyPref by lazy { findPreference<Preference>(generalSettings.mainDeviceIdentityKey.key)!! }
|
||||
private val mainDeviceEncryptionKeyPref by lazy { findPreference<Preference>(generalSettings.mainDeviceEncryptionKey.key)!! }
|
||||
|
||||
override fun onPreferencesCreated() {
|
||||
monitorModePref.apply {
|
||||
@@ -45,9 +49,32 @@ class GeneralSettingsFragment : PreferenceFragment3() {
|
||||
entries = ScannerMode.values().map { getString(it.labelRes) }.toTypedArray()
|
||||
entryValues = ScannerMode.values().map { settings.scannerMode.rawWriter(it) as String }.toTypedArray()
|
||||
}
|
||||
mainDeviceIdentityKeyPref.setOnPreferenceClickListener {
|
||||
AirPodKeyInputDialog(requireContext()).create(
|
||||
mode = AirPodKeyInputDialog.Mode.IRK,
|
||||
current = generalSettings.mainDeviceIdentityKey.value?.toHumanReadable() ?: "",
|
||||
onKey = { generalSettings.mainDeviceIdentityKey.value = it.fromHumanReadable() },
|
||||
onGuide = { webpageTool.open("https://github.com/d4rken-org/capod/wiki/airpod-Keys") }
|
||||
).show()
|
||||
true
|
||||
}
|
||||
mainDeviceEncryptionKeyPref.setOnPreferenceClickListener {
|
||||
AirPodKeyInputDialog(requireContext()).create(
|
||||
mode = AirPodKeyInputDialog.Mode.ENC,
|
||||
current = generalSettings.mainDeviceEncryptionKey.value?.toHumanReadable() ?: "",
|
||||
onKey = { generalSettings.mainDeviceEncryptionKey.value = it.fromHumanReadable() },
|
||||
onGuide = { webpageTool.open("https://github.com/d4rken-org/capod/wiki/airpod-Keys") }
|
||||
).show()
|
||||
true
|
||||
}
|
||||
|
||||
super.onPreferencesCreated()
|
||||
}
|
||||
|
||||
private fun ByteArray.toHumanReadable(): String = joinToString("-") { "%02X".format(it) }
|
||||
|
||||
private fun String.fromHumanReadable(): ByteArray = split("-").map { it.toInt(16).toByte() }.toByteArray()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
vm.bondedDevices.observe2 { devices ->
|
||||
mainDeviceAddressPref.setOnPreferenceClickListener {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7 14C5.9 14 5 13.1 5 12S5.9 10 7 10 9 10.9 9 12 8.1 14 7 14M12.6 10C11.8 7.7 9.6 6 7 6C3.7 6 1 8.7 1 12S3.7 18 7 18C9.6 18 11.8 16.3 12.6 14H16V18H20V14H23V10H12.6Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21 18H15V15H13.3C12.2 17.4 9.7 19 7 19C3.1 19 0 15.9 0 12S3.1 5 7 5C9.7 5 12.2 6.6 13.3 9H24V15H21V18M17 16H19V13H22V11H11.9L11.7 10.3C11 8.3 9.1 7 7 7C4.2 7 2 9.2 2 12S4.2 17 7 17C9.1 17 11 15.7 11.7 13.7L11.9 13H17V16M7 15C5.3 15 4 13.7 4 12S5.3 9 7 9 10 10.3 10 12 8.7 15 7 15M7 11C6.4 11 6 11.4 6 12S6.4 13 7 13 8 12.6 8 12 7.6 11 7 11Z" />
|
||||
</vector>
|
||||
@@ -7,6 +7,11 @@
|
||||
<string name="general_upgrade_action">Upgrade</string>
|
||||
<string name="general_check_action">Check</string>
|
||||
<string name="general_close_action">Close</string>
|
||||
<string name="general_save_action">Save</string>
|
||||
<string name="general_guide_action">Guide</string>
|
||||
<string name="general_continue_action">Continue</string>
|
||||
|
||||
<string name="general_example_label">E.g.: %s</string>
|
||||
|
||||
<string name="upgrade_capod_label">Upgrade CAPod</string>
|
||||
<string name="upgrade_capod_description">Get additional features and support the developer.</string>
|
||||
@@ -49,6 +54,13 @@
|
||||
<string name="settings_maindevice_address_none">None</string>
|
||||
<string name="settings_maindevice_model_label">Your device model</string>
|
||||
<string name="settings_maindevice_model_description">The model of your main device. This helps the app recognize your device when it is not connected to your phone.</string>
|
||||
<string name="settings_maindevice_identitykey_label">Identity Key</string>
|
||||
<string name="settings_maindevice_identitykey_description">Your device\'s Identity Resolving Key (IRK), which helps CAPod identify it among nearby devices.</string>
|
||||
<string name="settings_maindevice_identitykey_explanation">AirPods frequently change their Bluetooth address for privacy. The IRK helps CAPod recognize your device. You need one-time access to a MacBook.</string>
|
||||
<string name="settings_maindevice_encryptionkey_label">Encryption Key</string>
|
||||
<string name="settings_maindevice_encryptionkey_description">Your device\'s encryption key, which allows CAPod to retrieve detailed status information.</string>
|
||||
<string name="settings_maindevice_encryptionkey_explanation">AirPods send a status message, part of which is encrypted. The encryption key allows CAPod to decrypt the full message. You need one-time access to a MacBook.</string>
|
||||
|
||||
<string name="settings_onepod_mode_label">One pod mode</string>
|
||||
<string name="settings_onepod_mode_description">Wearing both pods is not required, wearing a single pod is sufficient to trigger reactions.</string>
|
||||
<string name="settings_popup_caseopen_label">Show case popup</string>
|
||||
@@ -134,6 +146,4 @@
|
||||
<string name="onboarding_body3">CAPod has no ads and does not collect your data.</string>
|
||||
<string name="onboarding_body4">You can upgrade to CAPod Pro to gain extra features and support development.</string>
|
||||
|
||||
<string name="general_continue_action">Continue</string>
|
||||
|
||||
</resources>
|
||||
@@ -41,6 +41,18 @@
|
||||
android:key="core.maindevice.model"
|
||||
android:summary="@string/settings_maindevice_model_description"
|
||||
android:title="@string/settings_maindevice_model_label" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_key_outline_24"
|
||||
android:key="core.maindevice.identitykey"
|
||||
android:summary="@string/settings_maindevice_identitykey_description"
|
||||
android:title="@string/settings_maindevice_identitykey_label" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_key_24"
|
||||
android:key="core.maindevice.encryptionkey"
|
||||
android:summary="@string/settings_maindevice_encryptionkey_description"
|
||||
android:title="@string/settings_maindevice_encryptionkey_label" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
|
||||
Reference in New Issue
Block a user