diff --git a/.editorconfig b/.editorconfig
index 74377f1..005f2c7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -16,6 +16,5 @@ indent_size = 4
trim_trailing_whitespace = false
max_line_length = off
-[*.{py,java,r,R}]
+[*.{py,java,r,R,kt,xml,kts}]
indent_size = 4
-
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fe3884c..791481b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -11,15 +11,19 @@
-
-
+
-
+
+
+
+
+
+
+
+
@@ -93,4 +108,4 @@
-
\ No newline at end of file
+
diff --git a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt
index cf3f53d..e0ab20a 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt
@@ -1,17 +1,17 @@
/*
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
- *
+ *
* Copyright (C) 2024 Kavish Devar
- *
+ *
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
- *
+ *
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
@@ -28,7 +28,6 @@ import android.app.Service
import android.appwidget.AppWidgetManager
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
-import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothSocket
@@ -42,6 +41,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
+import android.content.res.Resources
import android.media.AudioManager
import android.os.BatteryManager
import android.os.Binder
@@ -51,6 +51,7 @@ import android.os.IBinder
import android.os.Looper
import android.os.ParcelUuid
import android.util.Log
+import android.util.TypedValue
import android.view.View
import android.widget.RemoteViews
import androidx.annotation.RequiresPermission
@@ -79,18 +80,22 @@ import me.kavishdevar.aln.utils.LongPressPackets
import me.kavishdevar.aln.utils.MediaController
import me.kavishdevar.aln.utils.Window
import me.kavishdevar.aln.widgets.BatteryWidget
+import me.kavishdevar.aln.widgets.NoiseControlWidget
import org.lsposed.hiddenapibypass.HiddenApiBypass
object ServiceManager {
private var service: AirPodsService? = null
+
@Synchronized
fun getService(): AirPodsService? {
return service
}
+
@Synchronized
fun setService(service: AirPodsService?) {
this.service = service
}
+
@OptIn(ExperimentalMaterial3Api::class)
@Synchronized
fun restartService(context: Context) {
@@ -108,12 +113,14 @@ object ServiceManager {
}
// @Suppress("unused")
-class AirPodsService: Service() {
+class AirPodsService : Service() {
private var macAddress = ""
+
inner class LocalBinder : Binder() {
fun getService(): AirPodsService = this@AirPodsService
}
+ private lateinit var sharedPreferencesLogs: SharedPreferences
private lateinit var sharedPreferences: SharedPreferences
private val packetLogKey = "packet_log"
private val _packetLogsFlow = MutableStateFlow>(emptySet())
@@ -121,24 +128,26 @@ class AirPodsService: Service() {
override fun onCreate() {
super.onCreate()
- sharedPreferences = getSharedPreferences("packet_logs", MODE_PRIVATE)
+ sharedPreferencesLogs = getSharedPreferences("packet_logs", MODE_PRIVATE)
}
private fun logPacket(packet: ByteArray, source: String) {
val packetHex = packet.joinToString(" ") { "%02X".format(it) }
val logEntry = "$source: $packetHex"
- val logs = sharedPreferences.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet() ?: mutableSetOf()
+ val logs =
+ sharedPreferencesLogs.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet()
+ ?: mutableSetOf()
logs.add(logEntry)
_packetLogsFlow.value = logs
- sharedPreferences.edit { putStringSet(packetLogKey, logs) }
+ sharedPreferencesLogs.edit { putStringSet(packetLogKey, logs) }
}
fun getPacketLogs(): Set {
- return sharedPreferences.getStringSet(packetLogKey, emptySet()) ?: emptySet()
+ return sharedPreferencesLogs.getStringSet(packetLogKey, emptySet()) ?: emptySet()
}
private fun clearPacketLogs() {
- sharedPreferences.edit { remove(packetLogKey).apply() }
+ sharedPreferencesLogs.edit { remove(packetLogKey).apply() }
}
@@ -163,18 +172,22 @@ class AirPodsService: Service() {
}
@Suppress("ClassName")
- private object bluetoothReceiver: BroadcastReceiver() {
+ private object bluetoothReceiver : BroadcastReceiver() {
@SuppressLint("MissingPermission")
override fun onReceive(context: Context?, intent: Intent) {
val bluetoothDevice =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java)
+ intent.getParcelableExtra(
+ "android.bluetooth.device.extra.DEVICE",
+ BluetoothDevice::class.java
+ )
} else {
intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE") as BluetoothDevice?
}
val action = intent.action
val context = context?.applicationContext
- val name = context?.getSharedPreferences("settings", MODE_PRIVATE)?.getString("name", bluetoothDevice?.name)
+ val name = context?.getSharedPreferences("settings", MODE_PRIVATE)
+ ?.getString("name", bluetoothDevice?.name)
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
Log.d("AirPodsService", "Received bluetooth connection broadcast")
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
@@ -204,109 +217,27 @@ class AirPodsService: Service() {
private lateinit var earReceiver: BroadcastReceiver
var widgetMobileBatteryEnabled = false
- val METADATA_UNTETHERED_LEFT_CHARGING = 13
- val METADATA_UNTETHERED_LEFT_BATTERY = 10
- val METADATA_UNTETHERED_RIGHT_CHARGING = 14
- val METADATA_UNTETHERED_RIGHT_BATTERY = 11
- val METADATA_UNTETHERED_CASE_CHARGING = 15
- val METADATA_UNTETHERED_CASE_BATTERY = 12
-
- @SuppressLint("MissingPermission")
- fun setBatteryLevels(
- leftStatus: Boolean, leftLevel: Int,
- rightStatus: Boolean, rightLevel: Int,
- caseStatus: Boolean, caseLevel: Int,
- device: BluetoothDevice
- ) {
- HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothDevice;")
-
- HiddenApiBypass.invoke(
- BluetoothDevice::class.java,
- device,
- "setMetadata",
- METADATA_UNTETHERED_LEFT_CHARGING,
- leftStatus.toString().toByteArray()
- )
- HiddenApiBypass.invoke(
- BluetoothDevice::class.java,
- device,
- "setMetadata",
- METADATA_UNTETHERED_LEFT_BATTERY,
- leftLevel.toString().toByteArray()
- )
- HiddenApiBypass.invoke(
- BluetoothDevice::class.java,
- device,
- "setMetadata",
- METADATA_UNTETHERED_RIGHT_CHARGING,
- rightStatus.toString().toByteArray()
- )
- HiddenApiBypass.invoke(
- BluetoothDevice::class.java,
- device,
- "setMetadata",
- METADATA_UNTETHERED_RIGHT_BATTERY,
- rightLevel.toString().toByteArray()
- )
- HiddenApiBypass.invoke(
- BluetoothDevice::class.java,
- device,
- "setMetadata",
- METADATA_UNTETHERED_CASE_CHARGING,
- caseStatus.toString().toByteArray()
- )
- HiddenApiBypass.invoke(
- BluetoothDevice::class.java,
- device,
- "setMetadata",
- METADATA_UNTETHERED_CASE_BATTERY,
- caseLevel.toString().toByteArray()
- )
- HiddenApiBypass.invoke(
- BluetoothDevice::class.java,
- device,
- "sendVendorSpecificHeadsetEvent",
- "+IPHONEACCEV",
- BluetoothHeadset.AT_CMD_TYPE_SET,
- 1,
- leftLevel,
- 2,
- rightLevel,
- 3,
- caseLevel
- )
-
- // Prepare the intent to broadcast vendor-specific headset event
- val intent = Intent(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT).apply {
- putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD, "+IPHONEACCEV")
- putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, BluetoothHeadset.AT_CMD_TYPE_SET)
- putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS, arrayOf(
- 1, leftLevel,
- 2, rightLevel,
- 3, caseLevel
- ))
- putExtra(BluetoothDevice.EXTRA_DEVICE, device)
- putExtra(BluetoothDevice.EXTRA_NAME, device.name)
- addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "." + 76)
- }
-
- // Send the broadcast to update the battery levels
- sendBroadcast(intent)
-
- // Broadcast battery level changes
- val batteryIntent = Intent("android.bluet9ooth.device.action.BATTERY_LEVEL_CHANGED").apply {
- putExtra(BluetoothDevice.EXTRA_DEVICE, device)
- putExtra("android.bluetooth.device.extra.BATTERY_LEVEL", leftLevel) // Update with appropriate levels
- }
- sendBroadcast(batteryIntent)
- }
-
- object PhoneBatteryReceiver: BroadcastReceiver() {
+ object BatteryChangedIntentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == Intent.ACTION_BATTERY_CHANGED) {
- ServiceManager.getService()?.updateBatteryWidget()
- }
- else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
+ val level = intent.getIntExtra("level", 0)
+ val scale = intent.getIntExtra("scale", 100)
+ val batteryPct = level * 100 / scale
+ val charging = intent.getIntExtra(
+ BatteryManager.EXTRA_STATUS,
+ -1
+ ) == BatteryManager.BATTERY_STATUS_CHARGING
+ if (ServiceManager.getService()?.widgetMobileBatteryEnabled == true) {
+ val appWidgetManager = AppWidgetManager.getInstance(context)
+ val componentName = ComponentName(context!!, BatteryWidget::class.java)
+ val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
+ val remoteViews = RemoteViews(context.packageName, R.layout.battery_widget)
+ remoteViews.setTextViewText(R.id.phone_battery_widget, "$batteryPct%")
+ remoteViews.setProgressBar(R.id.phone_battery_progress, 100, batteryPct, false)
+
+ appWidgetManager.updateAppWidget(widgetIds, remoteViews)
+ }
+ } else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
try {
context?.unregisterReceiver(this)
} catch (e: Exception) {
@@ -315,35 +246,12 @@ class AirPodsService: Service() {
}
}
}
- val phoneBatteryIntentFilter = IntentFilter().apply {
- addAction(Intent.ACTION_BATTERY_CHANGED)
- addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
- }
+
fun setPhoneBatteryInWidget(enabled: Boolean) {
widgetMobileBatteryEnabled = enabled
- if (enabled) {
- try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- registerReceiver(
- PhoneBatteryReceiver,
- phoneBatteryIntentFilter,
- RECEIVER_EXPORTED
- )
- } else {
- registerReceiver(PhoneBatteryReceiver, phoneBatteryIntentFilter)
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
- } else {
- try {
- unregisterReceiver(PhoneBatteryReceiver)
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }
updateBatteryWidget()
}
+
@SuppressLint("MissingPermission")
fun scanForAirPods(bluetoothAdapter: BluetoothAdapter): Flow> = callbackFlow {
val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
@@ -392,7 +300,10 @@ class AirPodsService: Service() {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
- this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ this,
+ 0,
+ notificationIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(this, "background_service_status")
@@ -434,15 +345,22 @@ class AirPodsService: Service() {
)
}
+ @OptIn(ExperimentalMaterial3Api::class)
fun updateBatteryWidget() {
val appWidgetManager = AppWidgetManager.getInstance(this)
val componentName = ComponentName(this, BatteryWidget::class.java)
val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
val remoteViews = RemoteViews(packageName, R.layout.battery_widget).also {
- val leftBattery = batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT }
- val rightBattery = batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT }
- val caseBattery = batteryNotification.getBattery().find { it.component == BatteryComponent.CASE }
+ val openActivityIntent = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ it.setOnClickPendingIntent(R.id.battery_widget, openActivityIntent)
+
+ val leftBattery =
+ batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT }
+ val rightBattery =
+ batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT }
+ val caseBattery =
+ batteryNotification.getBattery().find { it.component == BatteryComponent.CASE }
it.setTextViewText(
R.id.left_battery_widget,
@@ -501,8 +419,10 @@ class AirPodsService: Service() {
)
if (widgetMobileBatteryEnabled) {
val batteryManager = getSystemService(BatteryManager::class.java)
- val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
- val charging = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS) == BatteryManager.BATTERY_STATUS_CHARGING
+ val batteryLevel =
+ batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
+ val charging =
+ batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS) == BatteryManager.BATTERY_STATUS_CHARGING
it.setTextViewText(
R.id.phone_battery_widget,
"$batteryLevel%"
@@ -522,40 +442,105 @@ class AirPodsService: Service() {
appWidgetManager.updateAppWidget(widgetIds, remoteViews)
}
+ fun updateNoiseControlWidget() {
+ val appWidgetManager = AppWidgetManager.getInstance(this)
+ val componentName = ComponentName(this, NoiseControlWidget::class.java)
+ val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
+ val remoteViews = RemoteViews(packageName, R.layout.noise_control_widget).also {
+ val ancStatus = ancNotification.status
+ it.setInt(
+ R.id.widget_off_button,
+ "setBackgroundResource",
+ if (ancStatus == 1) R.drawable.widget_button_checked_shape_start else R.drawable.widget_button_shape_start
+ )
+ it.setInt(
+ R.id.widget_transparency_button,
+ "setBackgroundResource",
+ if (ancStatus == 3) (if (sharedPreferences.getBoolean("off_listening_mode", true)) R.drawable.widget_button_checked_shape_middle else R.drawable.widget_button_checked_shape_start) else (if (sharedPreferences.getBoolean("off_listening_mode", true)) R.drawable.widget_button_shape_middle else R.drawable.widget_button_shape_start)
+ )
+ it.setInt(
+ R.id.widget_adaptive_button,
+ "setBackgroundResource",
+ if (ancStatus == 4) R.drawable.widget_button_checked_shape_middle else R.drawable.widget_button_shape_middle
+ )
+ it.setInt(
+ R.id.widget_anc_button,
+ "setBackgroundResource",
+ if (ancStatus == 2) R.drawable.widget_button_checked_shape_end else R.drawable.widget_button_shape_end
+ )
+ it.setViewVisibility(
+ R.id.widget_off_button,
+ if (sharedPreferences.getBoolean("off_listening_mode", true)) View.VISIBLE else View.GONE
+ )
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ it.setViewLayoutMargin(
+ R.id.widget_transparency_button,
+ RemoteViews.MARGIN_START,
+ if (sharedPreferences.getBoolean("off_listening_mode", true)) 2f else 12f,
+ TypedValue.COMPLEX_UNIT_DIP
+ )
+ } else {
+ it.setViewPadding(
+ R.id.widget_transparency_button,
+ if (sharedPreferences.getBoolean("off_listening_mode", true)) 2.dpToPx() else 12.dpToPx(),
+ 12.dpToPx(),
+ 2.dpToPx(),
+ 12.dpToPx()
+ )
+ }
+ }
+
+ appWidgetManager.updateAppWidget(widgetIds, remoteViews)
+ }
+
@OptIn(ExperimentalMaterial3Api::class)
- fun updateNotificationContent(connected: Boolean, airpodsName: String? = null, batteryList: List? = null) {
+ fun updateNotificationContent(
+ connected: Boolean,
+ airpodsName: String? = null,
+ batteryList: List? = null
+ ) {
val notificationManager = getSystemService(NotificationManager::class.java)
var updatedNotification: Notification? = null
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
- this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ this,
+ 0,
+ notificationIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
if (connected) {
updatedNotification = NotificationCompat.Builder(this, "background_service_status")
.setSmallIcon(R.drawable.airpods)
.setContentTitle(airpodsName)
- .setContentText("""${batteryList?.find { it.component == BatteryComponent.LEFT }?.let {
- if (it.status != BatteryStatus.DISCONNECTED) {
- "L: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
- } else {
- ""
- }
- } ?: ""} ${batteryList?.find { it.component == BatteryComponent.RIGHT }?.let {
- if (it.status != BatteryStatus.DISCONNECTED) {
- "R: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
- } else {
- ""
- }
- } ?: ""} ${batteryList?.find { it.component == BatteryComponent.CASE }?.let {
- if (it.status != BatteryStatus.DISCONNECTED) {
- "Case: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
- } else {
- ""
- }
- } ?: ""}""")
- .setContentIntent(pendingIntent)
+ .setContentText(
+ """${
+ batteryList?.find { it.component == BatteryComponent.LEFT }?.let {
+ if (it.status != BatteryStatus.DISCONNECTED) {
+ "L: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
+ } else {
+ ""
+ }
+ } ?: ""
+ } ${
+ batteryList?.find { it.component == BatteryComponent.RIGHT }?.let {
+ if (it.status != BatteryStatus.DISCONNECTED) {
+ "R: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
+ } else {
+ ""
+ }
+ } ?: ""
+ } ${
+ batteryList?.find { it.component == BatteryComponent.CASE }?.let {
+ if (it.status != BatteryStatus.DISCONNECTED) {
+ "Case: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
+ } else {
+ ""
+ }
+ } ?: ""
+ }""")
+ .setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
@@ -583,11 +568,13 @@ class AirPodsService: Service() {
Log.d("AirPodsService", "Service started")
ServiceManager.setService(this)
startForegroundNotification()
-
+
Log.d("AirPodsService", "Initializing CrossDevice")
CrossDevice.init(this)
Log.d("AirPodsService", "CrossDevice initialized")
+ sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
+
val serviceIntentFilter = IntentFilter().apply {
addAction("android.bluetooth.device.action.ACL_CONNECTED")
addAction("android.bluetooth.device.action.ACL_DISCONNECTED")
@@ -601,7 +588,7 @@ class AirPodsService: Service() {
addAction("android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED")
}
- connectionReceiver = object: BroadcastReceiver() {
+ connectionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED) {
device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -611,9 +598,12 @@ class AirPodsService: Service() {
}
val name = this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE)
.getString("name", device?.name)
- if (this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE).getString("name", null) == null) {
+ if (this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE)
+ .getString("name", null) == null
+ ) {
this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE).edit {
- putString("name", name)}
+ putString("name", name)
+ }
}
Log.d("AirPodsQuickSwitchServices", CrossDevice.isAvailable.toString())
if (!CrossDevice.checkAirPodsConnectionStatus()) {
@@ -622,7 +612,11 @@ class AirPodsService: Service() {
connectToSocket(device!!)
isConnectedLocally = true
macAddress = device!!.address
- updateNotificationContent(true, name.toString(), batteryNotification.getBattery())
+ updateNotificationContent(
+ true,
+ name.toString(),
+ batteryNotification.getBattery()
+ )
}
} else if (intent?.action == AirPodsNotifications.Companion.AIRPODS_DISCONNECTED) {
device = null
@@ -647,14 +641,11 @@ class AirPodsService: Service() {
registerReceiver(bluetoothReceiver, serviceIntentFilter)
}
- widgetMobileBatteryEnabled = getSharedPreferences("settings", MODE_PRIVATE).getBoolean("show_phone_battery_in_widget", true)
- if (widgetMobileBatteryEnabled) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- registerReceiver(PhoneBatteryReceiver, phoneBatteryIntentFilter, RECEIVER_EXPORTED)
- } else {
- registerReceiver(PhoneBatteryReceiver, phoneBatteryIntentFilter)
- }
- }
+ widgetMobileBatteryEnabled = getSharedPreferences("settings", MODE_PRIVATE).getBoolean(
+ "show_phone_battery_in_widget",
+ true
+ )
+
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
if (bluetoothAdapter.isEnabled) {
CoroutineScope(Dispatchers.IO).launch {
@@ -663,10 +654,12 @@ class AirPodsService: Service() {
scanResults.forEach { scanResult ->
val device = scanResult.device
device.fetchUuidsWithSdp()
- val manufacturerData = scanResult.scanRecord?.manufacturerSpecificData?.get(0x004C)
+ val manufacturerData =
+ scanResult.scanRecord?.manufacturerSpecificData?.get(0x004C)
if (manufacturerData != null && manufacturerData != lastData) {
lastData = manufacturerData
- val formattedHex = manufacturerData.joinToString(" ") { "%02X".format(it) }
+ val formattedHex =
+ manufacturerData.joinToString(" ") { "%02X".format(it) }
val rssi = scanResult.rssi
Log.d(
"AirPodsBLEService",
@@ -680,8 +673,7 @@ class AirPodsService: Service() {
bluetoothAdapter.bondedDevices.forEach { device ->
device.fetchUuidsWithSdp()
- if (device.uuids != null)
- {
+ if (device.uuids != null) {
if (device.uuids.contains(ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
bluetoothAdapter.getProfileProxy(
this,
@@ -720,7 +712,10 @@ class AirPodsService: Service() {
fun manuallyCheckForAudioSource() {
if (earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) {
- Log.d("AirPodsService", "For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again!")
+ Log.d(
+ "AirPodsService",
+ "For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again!"
+ )
disconnectAudio(this, device)
}
}
@@ -806,7 +801,13 @@ class AirPodsService: Service() {
socket.let {
val audioManager =
this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
- MediaController.initialize(audioManager, this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE))
+ MediaController.initialize(
+ audioManager,
+ this@AirPodsService.getSharedPreferences(
+ "settings",
+ MODE_PRIVATE
+ )
+ )
val buffer = ByteArray(1024)
val bytesRead = it.inputStream.read(buffer)
var data: ByteArray = byteArrayOf()
@@ -904,7 +905,10 @@ class AirPodsService: Service() {
true
)
) {
- Log.d("AirPods Parser", "User put in both AirPods from just one.")
+ Log.d(
+ "AirPods Parser",
+ "User put in both AirPods from just one."
+ )
MediaController.userPlayedTheMedia = false
}
if (newInEarData.contains(false) && inEarData == listOf(
@@ -912,7 +916,10 @@ class AirPodsService: Service() {
true
)
) {
- Log.d("AirPods Parser", "User took one of two out.")
+ Log.d(
+ "AirPods Parser",
+ "User took one of two out."
+ )
MediaController.userPlayedTheMedia = false
}
@@ -924,7 +931,10 @@ class AirPodsService: Service() {
Log.d("AirPods Parser", "hi")
return
}
- Log.d("AirPods Parser", "this shouldn't be run if the last log was 'hi'.")
+ Log.d(
+ "AirPods Parser",
+ "this shouldn't be run if the last log was 'hi'."
+ )
inEarData = newInEarData
@@ -935,7 +945,7 @@ class AirPodsService: Service() {
MediaController.iPausedTheMedia = false
}
} else {
- MediaController.sendPause()
+ MediaController.sendPause()
}
}
}
@@ -958,9 +968,8 @@ class AirPodsService: Service() {
CrossDevice.sendRemotePacket(data)
CrossDevice.ancBytes = data
ancNotification.setStatus(data)
- sendBroadcast(Intent(AirPodsNotifications.Companion.ANC_DATA).apply {
- putExtra("data", ancNotification.status)
- })
+ sendANCBroadcast()
+ updateNoiseControlWidget()
Log.d("AirPods Parser", "ANC: ${ancNotification.status}")
} else if (batteryNotification.isBatteryData(data)) {
CrossDevice.sendRemotePacket(data)
@@ -992,15 +1001,6 @@ class AirPodsService: Service() {
} else {
connectAudio(this@AirPodsService, device)
}
-// setBatteryLevels(
-// batteryNotification.getBattery()[0].status == 1,
-// batteryNotification.getBattery()[0].level,
-// batteryNotification.getBattery()[1].status == 1,
-// batteryNotification.getBattery()[1].level,
-// batteryNotification.getBattery()[2].status == 1,
-// batteryNotification.getBattery()[2].level,
-// device
-// )
} else if (conversationAwarenessNotification.isConversationalAwarenessData(
data
)
@@ -1090,12 +1090,15 @@ class AirPodsService: Service() {
1 -> {
sendPacket(Enums.NOISE_CANCELLATION_OFF.value)
}
+
2 -> {
sendPacket(Enums.NOISE_CANCELLATION_ON.value)
}
+
3 -> {
sendPacket(Enums.NOISE_CANCELLATION_TRANSPARENCY.value)
}
+
4 -> {
sendPacket(Enums.NOISE_CANCELLATION_ADAPTIVE.value)
}
@@ -1107,52 +1110,118 @@ class AirPodsService: Service() {
}
fun setOffListeningMode(enabled: Boolean) {
- sendPacket(byteArrayOf(0x04, 0x00 ,0x04, 0x00, 0x09, 0x00, 0x34, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00))
+ sendPacket(
+ byteArrayOf(
+ 0x04,
+ 0x00,
+ 0x04,
+ 0x00,
+ 0x09,
+ 0x00,
+ 0x34,
+ if (enabled) 0x01 else 0x02,
+ 0x00,
+ 0x00,
+ 0x00
+ )
+ )
+ updateNoiseControlWidget()
}
fun setAdaptiveStrength(strength: Int) {
- val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00)
+ val bytes =
+ byteArrayOf(
+ 0x04,
+ 0x00,
+ 0x04,
+ 0x00,
+ 0x09,
+ 0x00,
+ 0x2E,
+ strength.toByte(),
+ 0x00,
+ 0x00,
+ 0x00
+ )
sendPacket(bytes)
}
fun setPressSpeed(speed: Int) {
// 0x00 = default, 0x01 = slower, 0x02 = slowest
- val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x17, speed.toByte(), 0x00, 0x00, 0x00)
+ val bytes =
+ byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x17, speed.toByte(), 0x00, 0x00, 0x00)
sendPacket(bytes)
}
fun setPressAndHoldDuration(speed: Int) {
// 0 - default, 1 - slower, 2 - slowest
- val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x18, speed.toByte(), 0x00, 0x00, 0x00)
+ val bytes =
+ byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x18, speed.toByte(), 0x00, 0x00, 0x00)
sendPacket(bytes)
}
fun setVolumeSwipeSpeed(speed: Int) {
// 0 - default, 1 - longer, 2 - longest
- val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x23, speed.toByte(), 0x00, 0x00, 0x00)
- Log.d("AirPodsService", "Setting volume swipe speed to $speed by packet ${bytes.joinToString(" ") { "%02X".format(it) }}")
+ val bytes =
+ byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x23, speed.toByte(), 0x00, 0x00, 0x00)
+ Log.d(
+ "AirPodsService",
+ "Setting volume swipe speed to $speed by packet ${
+ bytes.joinToString(" ") {
+ "%02X".format(
+ it
+ )
+ }
+ }"
+ )
sendPacket(bytes)
}
fun setNoiseCancellationWithOnePod(enabled: Boolean) {
- val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1B, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00)
+ val bytes = byteArrayOf(
+ 0x04,
+ 0x00,
+ 0x04,
+ 0x00,
+ 0x09,
+ 0x00,
+ 0x1B,
+ if (enabled) 0x01 else 0x02,
+ 0x00,
+ 0x00,
+ 0x00
+ )
sendPacket(bytes)
}
fun setVolumeControl(enabled: Boolean) {
- val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x25, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00)
+ val bytes = byteArrayOf(
+ 0x04,
+ 0x00,
+ 0x04,
+ 0x00,
+ 0x09,
+ 0x00,
+ 0x25,
+ if (enabled) 0x01 else 0x02,
+ 0x00,
+ 0x00,
+ 0x00
+ )
sendPacket(bytes)
}
fun setToneVolume(volume: Int) {
- val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1F, volume.toByte(), 0x50, 0x00, 0x00)
+ val bytes =
+ byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1F, volume.toByte(), 0x50, 0x00, 0x00)
sendPacket(bytes)
}
val earDetectionNotification = AirPodsNotifications.EarDetection()
val ancNotification = AirPodsNotifications.ANC()
val batteryNotification = AirPodsNotifications.BatteryNotification()
- val conversationAwarenessNotification = AirPodsNotifications.ConversationalAwarenessNotification()
+ val conversationAwarenessNotification =
+ AirPodsNotifications.ConversationalAwarenessNotification()
var earDetectionEnabled = true
@@ -1180,7 +1249,8 @@ class AirPodsService: Service() {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) {
try {
- val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
+ val method =
+ proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
method.invoke(proxy, device)
} catch (e: Exception) {
e.printStackTrace()
@@ -1190,14 +1260,15 @@ class AirPodsService: Service() {
}
}
- override fun onServiceDisconnected(profile: Int) { }
+ override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.A2DP)
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
try {
- val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
+ val method =
+ proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
method.invoke(proxy, device)
} catch (e: Exception) {
e.printStackTrace()
@@ -1207,7 +1278,7 @@ class AirPodsService: Service() {
}
}
- override fun onServiceDisconnected(profile: Int) { }
+ override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.HEADSET)
}
@@ -1218,7 +1289,8 @@ class AirPodsService: Service() {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) {
try {
- val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
+ val method =
+ proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
method.invoke(proxy, device)
} catch (e: Exception) {
e.printStackTrace()
@@ -1228,14 +1300,15 @@ class AirPodsService: Service() {
}
}
- override fun onServiceDisconnected(profile: Int) { }
+ override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.A2DP)
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
try {
- val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
+ val method =
+ proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
method.invoke(proxy, device)
} catch (e: Exception) {
e.printStackTrace()
@@ -1245,14 +1318,16 @@ class AirPodsService: Service() {
}
}
- override fun onServiceDisconnected(profile: Int) { }
+ override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.HEADSET)
}
fun setName(name: String) {
val nameBytes = name.toByteArray()
- val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
- nameBytes.size.toByte(), 0x00) + nameBytes
+ val bytes = byteArrayOf(
+ 0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
+ nameBytes.size.toByte(), 0x00
+ ) + nameBytes
sendPacket(bytes)
val hex = bytes.joinToString(" ") { "%02X".format(it) }
updateNotificationContent(true, name, batteryNotification.getBattery())
@@ -1263,7 +1338,8 @@ class AirPodsService: Service() {
var hex = "04 00 04 00 09 00 26 ${if (enabled) "01" else "02"} 00 00 00"
var bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
sendPacket(bytes)
- hex = "04 00 04 00 17 00 00 00 10 00 12 00 08 E${if (enabled) "6" else "5"} 05 10 02 42 0B 08 50 10 02 1A 05 02 ${if (enabled) "32" else "00"} 00 00 00"
+ hex =
+ "04 00 04 00 17 00 00 00 10 00 12 00 08 E${if (enabled) "6" else "5"} 05 10 02 42 0B 08 50 10 02 1A 05 02 ${if (enabled) "32" else "00"} 00 00 00"
bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
sendPacket(bytes)
}
@@ -1273,6 +1349,7 @@ class AirPodsService: Service() {
val bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
sendPacket(bytes)
}
+
fun findChangedIndex(oldArray: BooleanArray, newArray: BooleanArray): Int {
for (i in oldArray.indices) {
if (oldArray[i] != newArray[i]) {
@@ -1281,7 +1358,12 @@ class AirPodsService: Service() {
}
throw IllegalArgumentException("No element has changed")
}
- fun updateLongPress(oldLongPressArray: BooleanArray, newLongPressArray: BooleanArray, offListeningMode: Boolean) {
+
+ fun updateLongPress(
+ oldLongPressArray: BooleanArray,
+ newLongPressArray: BooleanArray,
+ offListeningMode: Boolean
+ ) {
if (oldLongPressArray.contentEquals(newLongPressArray)) {
return
}
@@ -1391,6 +1473,7 @@ class AirPodsService: Service() {
LongPressPackets.DISABLE_ANC_OFF_DISABLED.value
}
}
+
2 -> {
packet = if (newLongPressArray[2]) {
LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value
@@ -1398,6 +1481,7 @@ class AirPodsService: Service() {
LongPressPackets.DISABLE_TRANSPARENCY_OFF_DISABLED.value
}
}
+
3 -> {
packet = if (newLongPressArray[3]) {
LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value
@@ -1439,4 +1523,9 @@ class AirPodsService: Service() {
}
super.onDestroy()
}
-}
\ No newline at end of file
+}
+
+private fun Int.dpToPx(): Int {
+ val density = Resources.getSystem().displayMetrics.density
+ return (this * density).toInt()
+}
diff --git a/android/app/src/main/java/me/kavishdevar/aln/widgets/BatteryWidget.kt b/android/app/src/main/java/me/kavishdevar/aln/widgets/BatteryWidget.kt
index 72fa986..fabd594 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/widgets/BatteryWidget.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/widgets/BatteryWidget.kt
@@ -1,17 +1,17 @@
/*
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
- *
+ *
* Copyright (C) 2024 Kavish Devar
- *
+ *
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
- *
+ *
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
@@ -24,11 +24,8 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
-import android.graphics.Canvas
-import android.util.Log
import android.widget.RemoteViews
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.core.graphics.createBitmap
import me.kavishdevar.aln.MainActivity
import me.kavishdevar.aln.R
import me.kavishdevar.aln.services.ServiceManager
@@ -39,29 +36,6 @@ class BatteryWidget : AppWidgetProvider() {
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
- for (appWidgetId in appWidgetIds) {
- updateAppWidget(context, appWidgetManager, appWidgetId)
- }
- }
-
- override fun onEnabled(context: Context) {
- updateAppWidget(context, AppWidgetManager.getInstance(context), 0)
+ ServiceManager.getService()?.updateBatteryWidget()
}
}
-
-@OptIn(ExperimentalMaterial3Api::class)
-internal fun updateAppWidget(
- context: Context,
- appWidgetManager: AppWidgetManager,
- appWidgetId: Int
-) {
- val service = ServiceManager.getService()
- val views = RemoteViews(context.packageName, R.layout.battery_widget)
-
- service?.updateBatteryWidget()
-
- val openActivityIntent = PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- views.setOnClickPendingIntent(R.id.battery_widget, openActivityIntent)
-
- appWidgetManager.updateAppWidget(appWidgetId, views)
-}
\ No newline at end of file
diff --git a/android/app/src/main/java/me/kavishdevar/aln/widgets/NoiseControlWidget.kt b/android/app/src/main/java/me/kavishdevar/aln/widgets/NoiseControlWidget.kt
new file mode 100644
index 0000000..918844f
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/aln/widgets/NoiseControlWidget.kt
@@ -0,0 +1,83 @@
+/*
+ * AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
+ *
+ * Copyright (C) 2024 Kavish Devar
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+
+package me.kavishdevar.aln.widgets
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.Context
+import android.content.Intent
+import android.widget.RemoteViews
+import me.kavishdevar.aln.R
+import me.kavishdevar.aln.services.ServiceManager
+
+class NoiseControlWidget : AppWidgetProvider() {
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ val views = RemoteViews(context.packageName, R.layout.noise_control_widget)
+
+ val offIntent = Intent(context, NoiseControlWidget::class.java).apply {
+ action = "ACTION_SET_ANC_MODE"
+ putExtra("ANC_MODE", 1)
+ }
+ val transparencyIntent = Intent(context, NoiseControlWidget::class.java).apply {
+ action = "ACTION_SET_ANC_MODE"
+ putExtra("ANC_MODE", 3)
+ }
+ val adaptiveIntent = Intent(context, NoiseControlWidget::class.java).apply {
+ action = "ACTION_SET_ANC_MODE"
+ putExtra("ANC_MODE", 4)
+ }
+ val ancIntent = Intent(context, NoiseControlWidget::class.java).apply {
+ action = "ACTION_SET_ANC_MODE"
+ putExtra("ANC_MODE", 2)
+ }
+
+ views.setOnClickPendingIntent(
+ R.id.widget_off_button,
+ PendingIntent.getBroadcast(context, 0, offIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ )
+ views.setOnClickPendingIntent(
+ R.id.widget_transparency_button,
+ PendingIntent.getBroadcast(context, 1, transparencyIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ )
+ views.setOnClickPendingIntent(
+ R.id.widget_adaptive_button,
+ PendingIntent.getBroadcast(context, 2, adaptiveIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ )
+ views.setOnClickPendingIntent(
+ R.id.widget_anc_button,
+ PendingIntent.getBroadcast(context, 3, ancIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ )
+ ServiceManager.getService()?.updateNoiseControlWidget()
+ appWidgetManager.updateAppWidget(appWidgetIds, views)
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+ if (intent.action == "ACTION_SET_ANC_MODE") {
+ val mode = intent.getIntExtra("ANC_MODE", 1)
+ ServiceManager.getService()?.setANCMode(mode)
+ }
+ }
+}
diff --git a/android/app/src/main/res/drawable/button_background.xml b/android/app/src/main/res/drawable/button_background.xml
deleted file mode 100644
index 5af4447..0000000
--- a/android/app/src/main/res/drawable/button_background.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/button_shape.xml b/android/app/src/main/res/drawable/popup_button_shape.xml
similarity index 100%
rename from android/app/src/main/res/drawable/button_shape.xml
rename to android/app/src/main/res/drawable/popup_button_shape.xml
diff --git a/android/app/src/main/res/drawable/ring_background.xml b/android/app/src/main/res/drawable/ring_background.xml
deleted file mode 100644
index 26ff600..0000000
--- a/android/app/src/main/res/drawable/ring_background.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
- -
-
-
-
-
-
-
diff --git a/android/app/src/main/res/drawable/widget_button_checked_shape_end.xml b/android/app/src/main/res/drawable/widget_button_checked_shape_end.xml
new file mode 100644
index 0000000..89e4b9b
--- /dev/null
+++ b/android/app/src/main/res/drawable/widget_button_checked_shape_end.xml
@@ -0,0 +1,9 @@
+
+ -
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/widget_button_checked_shape_middle.xml b/android/app/src/main/res/drawable/widget_button_checked_shape_middle.xml
new file mode 100644
index 0000000..9e108c3
--- /dev/null
+++ b/android/app/src/main/res/drawable/widget_button_checked_shape_middle.xml
@@ -0,0 +1,9 @@
+
+ -
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/widget_button_checked_shape_start.xml b/android/app/src/main/res/drawable/widget_button_checked_shape_start.xml
new file mode 100644
index 0000000..efc3f2d
--- /dev/null
+++ b/android/app/src/main/res/drawable/widget_button_checked_shape_start.xml
@@ -0,0 +1,9 @@
+
+ -
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/widget_button_shape_end.xml b/android/app/src/main/res/drawable/widget_button_shape_end.xml
new file mode 100644
index 0000000..8381844
--- /dev/null
+++ b/android/app/src/main/res/drawable/widget_button_shape_end.xml
@@ -0,0 +1,16 @@
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/widget_button_shape_middle.xml b/android/app/src/main/res/drawable/widget_button_shape_middle.xml
new file mode 100644
index 0000000..f81adc4
--- /dev/null
+++ b/android/app/src/main/res/drawable/widget_button_shape_middle.xml
@@ -0,0 +1,16 @@
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/widget_button_shape_start.xml b/android/app/src/main/res/drawable/widget_button_shape_start.xml
new file mode 100644
index 0000000..f3958ba
--- /dev/null
+++ b/android/app/src/main/res/drawable/widget_button_shape_start.xml
@@ -0,0 +1,16 @@
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/noise_control_widget.xml b/android/app/src/main/res/layout/noise_control_widget.xml
new file mode 100644
index 0000000..41767db
--- /dev/null
+++ b/android/app/src/main/res/layout/noise_control_widget.xml
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/popup_window.xml b/android/app/src/main/res/layout/popup_window.xml
index c470317..f8d2d76 100644
--- a/android/app/src/main/res/layout/popup_window.xml
+++ b/android/app/src/main/res/layout/popup_window.xml
@@ -37,7 +37,7 @@
android:layout_height="28dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
- android:background="@drawable/button_shape"
+ android:background="@drawable/popup_button_shape"
android:contentDescription="Close Button"
android:src="@drawable/close"
app:layout_constraintEnd_toEndOf="parent"
@@ -106,4 +106,4 @@
android:textColor="@color/popup_text"
android:textSize="20sp" />
-
\ No newline at end of file
+
diff --git a/android/app/src/main/res/values-night-v31/themes.xml b/android/app/src/main/res/values-night-v31/themes.xml
index 4eb4c07..827ca82 100644
--- a/android/app/src/main/res/values-night-v31/themes.xml
+++ b/android/app/src/main/res/values-night-v31/themes.xml
@@ -1,10 +1,10 @@
-
-
-
\ No newline at end of file
+
+
diff --git a/android/app/src/main/res/values-v21/styles.xml b/android/app/src/main/res/values-v21/styles.xml
index c47dddb..d1800ed 100644
--- a/android/app/src/main/res/values-v21/styles.xml
+++ b/android/app/src/main/res/values-v21/styles.xml
@@ -1,14 +1,14 @@
-
+
-
-
\ No newline at end of file
+
+
diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml
index 78e249e..ade4de2 100644
--- a/android/app/src/main/res/values-v31/styles.xml
+++ b/android/app/src/main/res/values-v31/styles.xml
@@ -1,16 +1,16 @@
-
+
-
-
\ No newline at end of file
+
+
diff --git a/android/app/src/main/res/values-v31/themes.xml b/android/app/src/main/res/values-v31/themes.xml
index 30e6629..e970a1b 100644
--- a/android/app/src/main/res/values-v31/themes.xml
+++ b/android/app/src/main/res/values-v31/themes.xml
@@ -1,11 +1,11 @@
-
-
-
\ No newline at end of file
+
+
diff --git a/android/app/src/main/res/values/attrs.xml b/android/app/src/main/res/values/attrs.xml
index 7781ac8..01f7969 100644
--- a/android/app/src/main/res/values/attrs.xml
+++ b/android/app/src/main/res/values/attrs.xml
@@ -1,7 +1,7 @@
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index 2779d25..6e2a52b 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -1,9 +1,13 @@
- #FF000000
- #FFFFFFFF
- #FFFFFF
- @color/black
- #87FFFFFF
- @color/black
-
\ No newline at end of file
+ #FF000000
+ #FFFFFFFF
+ #FFFFFF
+ @color/black
+ #87FFFFFF
+ @color/black
+ #FFE1F5FE
+ #FF81D4FA
+ #FF039BE5
+ #FF01579B
+
diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml
index 4db8c59..3334b87 100644
--- a/android/app/src/main/res/values/dimens.xml
+++ b/android/app/src/main/res/values/dimens.xml
@@ -1,10 +1,10 @@
-
- 0dp
+ 0dp
-
\ No newline at end of file
+
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 8128cdf..e524187 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,42 +1,45 @@
- ALN
- GATT Testing
- See your AirPods battery status right from your home screen!
- Accessibility
- Tone Volume
- Audio
- Adaptive Audio
- Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.
- Buds
- Case
- Test
- Name
- Noise Control
- Off
- Transparency
- Adaptive
- Noise Cancellation
- Press and Hold AirPods
- Left
- Right
- Adjusts the volume of media in response to your environment
- Conversational Awareness
- Lowers media volume and reduces background noise when you start speaking to other people.
- Personalized Volume
- Adjusts the volume of media in response to your environment.
- Less Noise
- More Noise
- Noise Cancellation with Single AirPod
- Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.
- Volume Control
- Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.
- AirPods not connected
- Please connect your AirPods to access settings. If you\'re stuck here, then try reopening the app again after closing it from the recents.\n(DO NOT KILL THE APP!)
- Back
- App Settings
- Conversational Awareness
- Relative volume
- Reduces to a percentage of the current volume instead of the maximum volume.
- Pause Music
- When you start speaking, music will be paused.
+ ALN
+ GATT Testing
+ See your AirPods battery status right from your home screen!
+ Accessibility
+ Tone Volume
+ Audio
+ Adaptive Audio
+ Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.
+ Buds
+ Case
+ Test
+ Name
+ Noise Control
+ Off
+ Transparency
+ Adaptive
+ Noise Cancellation
+ Press and Hold AirPods
+ Left
+ Right
+ Adjusts the volume of media in response to your environment
+ Conversational Awareness
+ Lowers media volume and reduces background noise when you start speaking to other people.
+ Personalized Volume
+ Adjusts the volume of media in response to your environment.
+ Less Noise
+ More Noise
+ Noise Cancellation with Single AirPod
+ Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.
+ Volume Control
+ Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.
+ AirPods not connected
+ Please connect your AirPods to access settings. If you\'re stuck here, then try reopening the app again after closing it from the recents.\n(DO NOT KILL THE APP!)
+ Back
+ App Settings
+ Conversational Awareness
+ Relative volume
+ Reduces to a percentage of the current volume instead of the maximum volume.
+ Pause Music
+ When you start speaking, music will be paused.
+ EXAMPLE
+ Add widget
+ Control Noise Control Mode directly from your Home Screen.
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index 823075f..6cdb21d 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -1,12 +1,12 @@
-
+
-
-
\ No newline at end of file
+
+
diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml
index 8f937c1..548a11f 100644
--- a/android/app/src/main/res/values/themes.xml
+++ b/android/app/src/main/res/values/themes.xml
@@ -1,14 +1,14 @@
-
+
-
+
-
-
\ No newline at end of file
+
+
diff --git a/android/app/src/main/res/xml/battery_widget_info.xml b/android/app/src/main/res/xml/battery_widget_info.xml
index 221ebf9..e743f04 100644
--- a/android/app/src/main/res/xml/battery_widget_info.xml
+++ b/android/app/src/main/res/xml/battery_widget_info.xml
@@ -11,6 +11,6 @@
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="1"
- android:updatePeriodMillis="300000"
+ android:updatePeriodMillis="30000"
android:widgetCategory="home_screen|keyguard"
- tools:ignore="UnusedAttribute" />
\ No newline at end of file
+ tools:ignore="UnusedAttribute" />
diff --git a/android/app/src/main/res/xml/noise_control_widget_info.xml b/android/app/src/main/res/xml/noise_control_widget_info.xml
new file mode 100644
index 0000000..e9ee6ac
--- /dev/null
+++ b/android/app/src/main/res/xml/noise_control_widget_info.xml
@@ -0,0 +1,16 @@
+
+
diff --git a/android/imgs/debug.png b/android/imgs/debug.png
index 1b03492..a98db16 100644
Binary files a/android/imgs/debug.png and b/android/imgs/debug.png differ
diff --git a/android/imgs/transitions.mp4 b/android/imgs/transitions.mp4
new file mode 100644
index 0000000..bd7221a
Binary files /dev/null and b/android/imgs/transitions.mp4 differ
diff --git a/android/imgs/widget.png b/android/imgs/widget.png
index a7f3f4a..2a16a01 100644
Binary files a/android/imgs/widget.png and b/android/imgs/widget.png differ