Skip to content

Commit b6966f8

Browse files
committed
some progress on cross-device, and new dynamic island thingy!
1 parent 8b57a97 commit b6966f8

19 files changed

+733
-475
lines changed

android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
android:usesPermissionFlags="neverForLocation"
2424
tools:ignore="UnusedAttribute" />
2525
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
26+
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
2627

2728
<application
2829
android:allowBackup="true"

android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
/*
22
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
3-
*
3+
*
44
* Copyright (C) 2024 Kavish Devar
5-
*
5+
*
66
* This program is free software: you can redistribute it and/or modify
77
* it under the terms of the GNU Affero General Public License as published
88
* by the Free Software Foundation, either version 3 of the License.
9-
*
9+
*
1010
* This program is distributed in the hope that it will be useful,
1111
* but WITHOUT ANY WARRANTY; without even the implied warranty of
1212
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1313
* GNU Affero General Public License for more details.
14-
*
14+
*
1515
* You should have received a copy of the GNU Affero General Public License
1616
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
@@ -135,7 +135,8 @@ fun Main() {
135135
permissions = listOf(
136136
"android.permission.BLUETOOTH_CONNECT",
137137
"android.permission.BLUETOOTH_SCAN",
138-
"android.permission.POST_NOTIFICATIONS"
138+
"android.permission.POST_NOTIFICATIONS",
139+
"android.permission.READ_PHONE_STATE"
139140
)
140141
)
141142
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
@@ -308,7 +309,6 @@ fun Main() {
308309
isConnected.value = true
309310
}
310311
} else {
311-
// Permission is not granted, request it
312312
Column (
313313
modifier = Modifier.padding(24.dp),
314314
){
@@ -325,4 +325,4 @@ fun Main() {
325325
}
326326
}
327327
}
328-
}
328+
}

android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt

Lines changed: 131 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import android.os.Handler
5050
import android.os.IBinder
5151
import android.os.Looper
5252
import android.os.ParcelUuid
53+
import android.telephony.PhoneStateListener
54+
import android.telephony.TelephonyManager
5355
import android.util.Log
5456
import android.util.TypedValue
5557
import android.view.View
@@ -76,9 +78,10 @@ import me.kavishdevar.aln.utils.BatteryStatus
7678
import me.kavishdevar.aln.utils.CrossDevice
7779
import me.kavishdevar.aln.utils.CrossDevicePackets
7880
import me.kavishdevar.aln.utils.Enums
81+
import me.kavishdevar.aln.utils.IslandWindow
7982
import me.kavishdevar.aln.utils.LongPressPackets
8083
import me.kavishdevar.aln.utils.MediaController
81-
import me.kavishdevar.aln.utils.Window
84+
import me.kavishdevar.aln.utils.PopupWindow
8285
import me.kavishdevar.aln.widgets.BatteryWidget
8386
import me.kavishdevar.aln.widgets.NoiseControlWidget
8487
import org.lsposed.hiddenapibypass.HiddenApiBypass
@@ -114,7 +117,7 @@ object ServiceManager {
114117

115118
// @Suppress("unused")
116119
class AirPodsService : Service() {
117-
private var macAddress = ""
120+
var macAddress = ""
118121

119122
inner class LocalBinder : Binder() {
120123
fun getService(): AirPodsService = this@AirPodsService
@@ -126,6 +129,9 @@ class AirPodsService : Service() {
126129
private val _packetLogsFlow = MutableStateFlow<Set<String>>(emptySet())
127130
val packetLogsFlow: StateFlow<Set<String>> get() = _packetLogsFlow
128131

132+
private lateinit var telephonyManager: TelephonyManager
133+
private lateinit var phoneStateListener: PhoneStateListener
134+
129135
override fun onCreate() {
130136
super.onCreate()
131137
sharedPreferencesLogs = getSharedPreferences("packet_logs", MODE_PRIVATE)
@@ -166,10 +172,25 @@ class AirPodsService : Service() {
166172
if (popupShown) {
167173
return
168174
}
169-
val window = Window(service.applicationContext)
170-
window.open(name, batteryNotification)
175+
val popupWindow = PopupWindow(service.applicationContext)
176+
popupWindow.open(name, batteryNotification)
171177
popupShown = true
172178
}
179+
var islandOpen = false
180+
var islandWindow: IslandWindow? = null
181+
@SuppressLint("MissingPermission")
182+
fun showIsland(service: Service, batteryPercentage: Int, takingOver: Boolean = false) {
183+
Log.d("AirPodsService", "Showing island window")
184+
islandWindow = IslandWindow(service.applicationContext)
185+
islandWindow!!.show(sharedPreferences.getString("name", "AirPods Pro").toString(), batteryPercentage, this, takingOver)
186+
}
187+
188+
@OptIn(ExperimentalMaterial3Api::class)
189+
fun startMainActivity() {
190+
val intent = Intent(this, MainActivity::class.java)
191+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
192+
startActivity(intent)
193+
}
173194

174195
@Suppress("ClassName")
175196
private object bluetoothReceiver : BroadcastReceiver() {
@@ -220,23 +241,7 @@ class AirPodsService : Service() {
220241
object BatteryChangedIntentReceiver : BroadcastReceiver() {
221242
override fun onReceive(context: Context?, intent: Intent) {
222243
if (intent.action == Intent.ACTION_BATTERY_CHANGED) {
223-
val level = intent.getIntExtra("level", 0)
224-
val scale = intent.getIntExtra("scale", 100)
225-
val batteryPct = level * 100 / scale
226-
val charging = intent.getIntExtra(
227-
BatteryManager.EXTRA_STATUS,
228-
-1
229-
) == BatteryManager.BATTERY_STATUS_CHARGING
230-
if (ServiceManager.getService()?.widgetMobileBatteryEnabled == true) {
231-
val appWidgetManager = AppWidgetManager.getInstance(context)
232-
val componentName = ComponentName(context!!, BatteryWidget::class.java)
233-
val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
234-
val remoteViews = RemoteViews(context.packageName, R.layout.battery_widget)
235-
remoteViews.setTextViewText(R.id.phone_battery_widget, "$batteryPct%")
236-
remoteViews.setProgressBar(R.id.phone_battery_progress, 100, batteryPct, false)
237-
238-
appWidgetManager.updateAppWidget(widgetIds, remoteViews)
239-
}
244+
ServiceManager.getService()?.updateBatteryWidget()
240245
} else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
241246
try {
242247
context?.unregisterReceiver(this)
@@ -568,13 +573,55 @@ class AirPodsService : Service() {
568573
Log.d("AirPodsService", "Service started")
569574
ServiceManager.setService(this)
570575
startForegroundNotification()
571-
576+
val audioManager =
577+
this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
578+
MediaController.initialize(
579+
audioManager,
580+
this@AirPodsService.getSharedPreferences(
581+
"settings",
582+
MODE_PRIVATE
583+
)
584+
)
572585
Log.d("AirPodsService", "Initializing CrossDevice")
573-
CrossDevice.init(this)
574-
Log.d("AirPodsService", "CrossDevice initialized")
586+
CoroutineScope(Dispatchers.IO).launch {
587+
CrossDevice.init(this@AirPodsService)
588+
Log.d("AirPodsService", "CrossDevice initialized")
589+
}
575590

576591
sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
577-
592+
macAddress = sharedPreferences.getString("mac_address", "") ?: ""
593+
594+
telephonyManager = getSystemService(TELEPHONY_SERVICE) as TelephonyManager
595+
phoneStateListener = object : PhoneStateListener() {
596+
@SuppressLint("SwitchIntDef")
597+
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
598+
super.onCallStateChanged(state, phoneNumber)
599+
when (state) {
600+
TelephonyManager.CALL_STATE_RINGING -> {
601+
if (CrossDevice.isAvailable && !isConnectedLocally && earDetectionNotification.status.contains(0x00)) takeOver()
602+
}
603+
TelephonyManager.CALL_STATE_OFFHOOK -> {
604+
if (CrossDevice.isAvailable && !isConnectedLocally && earDetectionNotification.status.contains(0x00)) takeOver()
605+
}
606+
}
607+
}
608+
}
609+
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
610+
611+
if (sharedPreferences.getBoolean("show_phone_battery_in_widget", true)) {
612+
widgetMobileBatteryEnabled = true
613+
val batteryChangedIntentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
614+
batteryChangedIntentFilter.addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
615+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
616+
registerReceiver(
617+
BatteryChangedIntentReceiver,
618+
batteryChangedIntentFilter,
619+
RECEIVER_EXPORTED
620+
)
621+
} else {
622+
registerReceiver(BatteryChangedIntentReceiver, batteryChangedIntentFilter)
623+
}
624+
}
578625
val serviceIntentFilter = IntentFilter().apply {
579626
addAction("android.bluetooth.device.action.ACL_CONNECTED")
580627
addAction("android.bluetooth.device.action.ACL_DISCONNECTED")
@@ -605,13 +652,16 @@ class AirPodsService : Service() {
605652
putString("name", name)
606653
}
607654
}
608-
Log.d("AirPodsQuickSwitchServices", CrossDevice.isAvailable.toString())
609-
if (!CrossDevice.checkAirPodsConnectionStatus()) {
655+
Log.d("AirPodsCrossDevice", CrossDevice.isAvailable.toString())
656+
if (!CrossDevice.isAvailable) {
610657
Log.d("AirPodsService", "$name connected")
611658
showPopup(this@AirPodsService, name.toString())
612659
connectToSocket(device!!)
613660
isConnectedLocally = true
614661
macAddress = device!!.address
662+
sharedPreferences.edit {
663+
putString("mac_address", macAddress)
664+
}
615665
updateNotificationContent(
616666
true,
617667
name.toString(),
@@ -626,7 +676,30 @@ class AirPodsService : Service() {
626676
}
627677
}
628678
}
679+
val showIslandReceiver = object: BroadcastReceiver() {
680+
override fun onReceive(context: Context?, intent: Intent?) {
681+
if (intent?.action == "me.kavishdevar.aln.cross_device_island") {
682+
showIsland(this@AirPodsService, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!))
683+
} else if (intent?.action == AirPodsNotifications.Companion.DISCONNECT_RECEIVERS) {
684+
try {
685+
context?.unregisterReceiver(this)
686+
} catch (e: Exception) {
687+
e.printStackTrace()
688+
}
689+
}
690+
}
691+
}
629692

693+
val showIslandIntentFilter = IntentFilter().apply {
694+
addAction("me.kavishdevar.aln.cross_device_island")
695+
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
696+
}
697+
698+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
699+
registerReceiver(showIslandReceiver, showIslandIntentFilter, RECEIVER_EXPORTED)
700+
} else {
701+
registerReceiver(showIslandReceiver, showIslandIntentFilter)
702+
}
630703

631704
val deviceIntentFilter = IntentFilter().apply {
632705
addAction(AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED)
@@ -641,11 +714,6 @@ class AirPodsService : Service() {
641714
registerReceiver(bluetoothReceiver, serviceIntentFilter)
642715
}
643716

644-
widgetMobileBatteryEnabled = getSharedPreferences("settings", MODE_PRIVATE).getBoolean(
645-
"show_phone_battery_in_widget",
646-
true
647-
)
648-
649717
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
650718
if (bluetoothAdapter.isEnabled) {
651719
CoroutineScope(Dispatchers.IO).launch {
@@ -682,8 +750,12 @@ class AirPodsService : Service() {
682750
if (profile == BluetoothProfile.A2DP) {
683751
val connectedDevices = proxy.connectedDevices
684752
if (connectedDevices.isNotEmpty()) {
685-
if (!CrossDevice.checkAirPodsConnectionStatus()) {
753+
if (!CrossDevice.isAvailable) {
686754
connectToSocket(device)
755+
macAddress = device.address
756+
sharedPreferences.edit {
757+
putString("mac_address", macAddress)
758+
}
687759
}
688760
this@AirPodsService.sendBroadcast(
689761
Intent(AirPodsNotifications.Companion.AIRPODS_CONNECTED)
@@ -720,12 +792,27 @@ class AirPodsService : Service() {
720792
}
721793
}
722794

795+
@SuppressLint("MissingPermission")
796+
fun takeOver() {
797+
Log.d("AirPodsService", "Taking over audio")
798+
CrossDevice.sendRemotePacket(CrossDevicePackets.REQUEST_DISCONNECT.packet)
799+
Log.d("AirPodsService", macAddress)
800+
device = getSystemService<BluetoothManager>(BluetoothManager::class.java).adapter.bondedDevices.find {
801+
it.address == macAddress
802+
}
803+
if (device != null) {
804+
connectToSocket(device!!)
805+
connectAudio(this, device)
806+
}
807+
showIsland(this, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!), true)
808+
}
809+
723810
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
724811
fun connectToSocket(device: BluetoothDevice) {
725812
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
726813
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
727814

728-
if (isConnectedLocally != true) {
815+
if (isConnectedLocally != true && !CrossDevice.isAvailable) {
729816
try {
730817
socket = HiddenApiBypass.newInstance(
731818
BluetoothSocket::class.java,
@@ -799,15 +886,6 @@ class AirPodsService : Service() {
799886
)
800887
while (socket.isConnected == true) {
801888
socket.let {
802-
val audioManager =
803-
this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
804-
MediaController.initialize(
805-
audioManager,
806-
this@AirPodsService.getSharedPreferences(
807-
"settings",
808-
MODE_PRIVATE
809-
)
810-
)
811889
val buffer = ByteArray(1024)
812890
val bytesRead = it.inputStream.read(buffer)
813891
var data: ByteArray = byteArrayOf()
@@ -852,11 +930,16 @@ class AirPodsService : Service() {
852930
} else {
853931
data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
854932
}
855-
856933
val newInEarData = listOf(
857934
data[0] == 0x00.toByte(),
858935
data[1] == 0x00.toByte()
859936
)
937+
if (inEarData.sorted() == listOf(false, false) && newInEarData.sorted() != listOf(false, false) && islandWindow?.isVisible != true) {
938+
showIsland(this@AirPodsService, batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level!!.coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level!!))
939+
}
940+
if (newInEarData == listOf(false, false) && islandWindow?.isVisible == true) {
941+
islandWindow?.close()
942+
}
860943
if (newInEarData.contains(true) && inEarData == listOf(
861944
false,
862945
false
@@ -1296,6 +1379,9 @@ class AirPodsService : Service() {
12961379
e.printStackTrace()
12971380
} finally {
12981381
bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
1382+
if (MediaController.pausedForCrossDevice) {
1383+
MediaController.sendPlay()
1384+
}
12991385
}
13001386
}
13011387
}
@@ -1521,6 +1607,7 @@ class AirPodsService : Service() {
15211607
} catch (e: Exception) {
15221608
e.printStackTrace()
15231609
}
1610+
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
15241611
super.onDestroy()
15251612
}
15261613
}

0 commit comments

Comments
 (0)