mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-10 19:52:24 +00:00
add popup window when connected; automatically open socket in background
This commit is contained in:
@@ -7,7 +7,7 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "me.kavishdevar.aln"
|
namespace = "me.kavishdevar.aln"
|
||||||
compileSdk = 34
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "me.kavishdevar.aln"
|
applicationId = "me.kavishdevar.aln"
|
||||||
@@ -53,6 +53,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.annotations)
|
implementation(libs.annotations)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
implementation(libs.androidx.constraintlayout)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
@@ -60,4 +61,5 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||||
debugImplementation(libs.androidx.ui.tooling)
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
debugImplementation(libs.androidx.ui.test.manifest)
|
debugImplementation(libs.androidx.ui.test.manifest)
|
||||||
|
implementation("com.github.prime-zs.toolkit:core-ktx:2.1.0")
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
@@ -49,6 +51,12 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".OldAirPodsService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="connectedDevice"
|
||||||
|
android:permission="android.permission.BLUETOOTH_CONNECT" />
|
||||||
<service
|
<service
|
||||||
android:name=".AirPodsService"
|
android:name=".AirPodsService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|||||||
@@ -10,17 +10,20 @@ import android.service.quicksettings.TileService
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
class AirPodsQSService: TileService() {
|
class AirPodsQSService: TileService() {
|
||||||
private val ancModes = listOf(NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name)
|
private val sharedPreferences = ServiceManager.getService()?.getSharedPreferences("me.kavishdevar.aln", Context.MODE_PRIVATE)
|
||||||
private var currentModeIndex = 2
|
private val offListeningModeEnabled = sharedPreferences?.getBoolean("off_listening_mode", false) == true
|
||||||
|
private val ancModes = if (offListeningModeEnabled) listOf(NoiseControlMode.OFF.name, NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name) else listOf(NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name)
|
||||||
|
private var currentModeIndex = if (offListeningModeEnabled) 3 else 2
|
||||||
private lateinit var ancStatusReceiver: BroadcastReceiver
|
private lateinit var ancStatusReceiver: BroadcastReceiver
|
||||||
private lateinit var availabilityReceiver: BroadcastReceiver
|
private lateinit var availabilityReceiver: BroadcastReceiver
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
|
Log.d("AirPodsQSService", "off mode: $offListeningModeEnabled")
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
currentModeIndex = (ServiceManager.getService()?.getANC()?.minus(1)) ?: 3
|
currentModeIndex = (ServiceManager.getService()?.getANC()?.minus(if (offListeningModeEnabled) 1 else 2)) ?: if (offListeningModeEnabled) 3 else 2
|
||||||
if (currentModeIndex == -1) {
|
if (currentModeIndex == -1) {
|
||||||
currentModeIndex = 3
|
currentModeIndex = if (offListeningModeEnabled) 3 else 2
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ServiceManager.getService() == null) {
|
if (ServiceManager.getService() == null) {
|
||||||
@@ -39,7 +42,7 @@ class AirPodsQSService: TileService() {
|
|||||||
ancStatusReceiver = object : BroadcastReceiver() {
|
ancStatusReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val ancStatus = intent.getIntExtra("data", 4)
|
val ancStatus = intent.getIntExtra("data", 4)
|
||||||
currentModeIndex = ancStatus - 1
|
currentModeIndex = ancStatus - if (offListeningModeEnabled) 1 else 2
|
||||||
updateTile()
|
updateTile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +70,7 @@ class AirPodsQSService: TileService() {
|
|||||||
unregisterReceiver(ancStatusReceiver)
|
unregisterReceiver(ancStatusReceiver)
|
||||||
}
|
}
|
||||||
catch (
|
catch (
|
||||||
e: IllegalArgumentException
|
_: IllegalArgumentException
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Log.e("QuickSettingTileService", "Receiver not registered")
|
Log.e("QuickSettingTileService", "Receiver not registered")
|
||||||
@@ -76,7 +79,7 @@ class AirPodsQSService: TileService() {
|
|||||||
unregisterReceiver(availabilityReceiver)
|
unregisterReceiver(availabilityReceiver)
|
||||||
}
|
}
|
||||||
catch (
|
catch (
|
||||||
e: IllegalArgumentException
|
_: IllegalArgumentException
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Log.e("QuickSettingTileService", "Receiver not registered")
|
Log.e("QuickSettingTileService", "Receiver not registered")
|
||||||
@@ -86,21 +89,23 @@ class AirPodsQSService: TileService() {
|
|||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
Log.d("QuickSettingTileService", "ANC tile clicked")
|
Log.d("QuickSettingTileService", "ANC tile clicked")
|
||||||
|
Log.d("QuickSettingTileService", "Current mode index: $currentModeIndex, ancModes size: ${ancModes.size}")
|
||||||
currentModeIndex = (currentModeIndex + 1) % ancModes.size
|
currentModeIndex = (currentModeIndex + 1) % ancModes.size
|
||||||
switchAncMode(currentModeIndex)
|
Log.d("QuickSettingTileService", "New mode index: $currentModeIndex")
|
||||||
|
switchAncMode(if (offListeningModeEnabled) currentModeIndex + 1 else currentModeIndex + 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTile() {
|
private fun updateTile() {
|
||||||
val currentMode = ancModes[currentModeIndex]
|
val currentMode = ancModes[currentModeIndex % ancModes.size]
|
||||||
qsTile.label = currentMode.replace("_", " ").lowercase().replaceFirstChar { it.uppercase() }
|
qsTile.label = currentMode.replace("_", " ").lowercase().replaceFirstChar { it.uppercase() }
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchAncMode(modeIndex: Int) {
|
private fun switchAncMode(modeIndex: Int) {
|
||||||
currentModeIndex = modeIndex
|
currentModeIndex = if (offListeningModeEnabled) modeIndex else modeIndex - 1
|
||||||
val airPodsService = ServiceManager.getService()
|
val airPodsService = ServiceManager.getService()
|
||||||
airPodsService?.setANCMode(currentModeIndex + 1)
|
airPodsService?.setANCMode(if (offListeningModeEnabled) modeIndex + 1 else modeIndex)
|
||||||
updateTile()
|
updateTile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
@file:Suppress("unused")
|
|
||||||
|
|
||||||
package me.kavishdevar.aln
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -8,7 +6,6 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothHeadset
|
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.bluetooth.BluetoothProfile
|
import android.bluetooth.BluetoothProfile
|
||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
@@ -28,30 +25,8 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
|
|
||||||
private const val VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV = "+IPHONEACCEV"
|
@Suppress("unused")
|
||||||
private const val VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL = 1
|
class AirPodsService: Service() {
|
||||||
private const val APPLE = 0x004C
|
|
||||||
const val ACTION_BATTERY_LEVEL_CHANGED = "android.bluetooth.device.action.BATTERY_LEVEL_CHANGED"
|
|
||||||
const val EXTRA_BATTERY_LEVEL = "android.bluetooth.device.extra.BATTERY_LEVEL"
|
|
||||||
private const val PACKAGE_ASI = "com.google.android.settings.intelligence"
|
|
||||||
private const val ACTION_ASI_UPDATE_BLUETOOTH_DATA = "batterywidget.impl.action.update_bluetooth_data"
|
|
||||||
//private const val COMPANION_TYPE_NONE = "COMPANION_NONE"
|
|
||||||
//const val VENDOR_RESULT_CODE_COMMAND_ANDROID = "+ANDROID"
|
|
||||||
|
|
||||||
|
|
||||||
object ServiceManager {
|
|
||||||
private var service: AirPodsService? = null
|
|
||||||
@Synchronized
|
|
||||||
fun getService(): AirPodsService? {
|
|
||||||
return service
|
|
||||||
}
|
|
||||||
@Synchronized
|
|
||||||
fun setService(service: AirPodsService?) {
|
|
||||||
this.service = service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AirPodsService : Service() {
|
|
||||||
inner class LocalBinder : Binder() {
|
inner class LocalBinder : Binder() {
|
||||||
fun getService(): AirPodsService = this@AirPodsService
|
fun getService(): AirPodsService = this@AirPodsService
|
||||||
}
|
}
|
||||||
@@ -60,46 +35,407 @@ class AirPodsService : Service() {
|
|||||||
return LocalBinder()
|
return LocalBinder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showPopup(context: Context, name: String) {
|
||||||
|
val window = Window(context)
|
||||||
|
window.open(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object Receiver: BroadcastReceiver() {
|
||||||
|
@SuppressLint("NewApi", "MissingPermission")
|
||||||
|
override fun onReceive(context: Context?, intent: Intent) {
|
||||||
|
val bluetoothDevice =
|
||||||
|
intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java)
|
||||||
|
val action = intent.action
|
||||||
|
val context = context?.applicationContext
|
||||||
|
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
|
||||||
|
Log.d("BluetoothReceiver", "Received broadcast")
|
||||||
|
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
|
||||||
|
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
|
if (bluetoothDevice.uuids.contains(uuid)) {
|
||||||
|
Log.d("AirPodsService", "Service started")
|
||||||
|
val intent = Intent(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
|
||||||
|
intent.putExtra("name", bluetoothDevice.name)
|
||||||
|
intent.putExtra("device", bluetoothDevice)
|
||||||
|
context?.sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Airpods disconnected, remove notification but leave the scanner going.
|
||||||
|
if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action
|
||||||
|
|| BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action
|
||||||
|
) {
|
||||||
|
Log.d("AirPodsService", "Closed Socket")
|
||||||
|
context?.sendBroadcast(
|
||||||
|
Intent(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isConnected = false
|
||||||
|
var device: BluetoothDevice? = null
|
||||||
|
|
||||||
|
fun startForegroundNotification() {
|
||||||
|
val notificationChannel = NotificationChannel(
|
||||||
|
"airpods",
|
||||||
|
"AirPods",
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||||
|
notificationManager.createNotificationChannel(notificationChannel)
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(this, "airpods")
|
||||||
|
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||||
|
.setContentTitle("AirPods Service Running")
|
||||||
|
.setContentText("AirPods service is running in the background.")
|
||||||
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
startForeground(2, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi", "MissingPermission")
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
Log.d("AirPodsService", "Service started")
|
||||||
|
startForegroundNotification()
|
||||||
|
registerReceiver(Receiver, BluetoothReceiver.buildFilter(), RECEIVER_EXPORTED)
|
||||||
|
|
||||||
|
registerReceiver(object: BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
showPopup(context!!, intent!!.getStringExtra("name")!!)
|
||||||
|
device = intent.getParcelableExtra("device", BluetoothDevice::class.java)!!
|
||||||
|
connectToSocket(device!!)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_CONNECTED).apply {
|
||||||
|
putExtra("device", device)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, IntentFilter(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED), RECEIVER_EXPORTED)
|
||||||
|
|
||||||
|
registerReceiver(object: BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
device = null
|
||||||
|
isConnected = false
|
||||||
|
}
|
||||||
|
}, IntentFilter(AirPodsNotifications.AIRPODS_DISCONNECTED), RECEIVER_EXPORTED)
|
||||||
|
|
||||||
|
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
bluetoothAdapter.bondedDevices.forEach { device ->
|
||||||
|
if (device.uuids.contains(ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
|
||||||
|
bluetoothAdapter.getProfileProxy(this, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||||
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
|
val connectedDevices = proxy.connectedDevices
|
||||||
|
if (connectedDevices.isNotEmpty()) {
|
||||||
|
connectToSocket(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bluetoothAdapter.closeProfileProxy(profile, proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(profile: Int) { }
|
||||||
|
}, BluetoothProfile.A2DP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var socket: BluetoothSocket
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||||
|
fun connectToSocket(device: BluetoothDevice) {
|
||||||
|
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
||||||
|
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket = HiddenApiBypass.newInstance(
|
||||||
|
BluetoothSocket::class.java,
|
||||||
|
3,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
device,
|
||||||
|
0x1001,
|
||||||
|
uuid
|
||||||
|
) as BluetoothSocket
|
||||||
|
}
|
||||||
|
catch (
|
||||||
|
e: Exception
|
||||||
|
) {
|
||||||
|
e.printStackTrace()
|
||||||
|
try {
|
||||||
|
socket = HiddenApiBypass.newInstance(
|
||||||
|
BluetoothSocket::class.java,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
device,
|
||||||
|
0x1001,
|
||||||
|
uuid
|
||||||
|
) as BluetoothSocket
|
||||||
|
}
|
||||||
|
catch (
|
||||||
|
e: Exception
|
||||||
|
) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket.connect()
|
||||||
|
this@AirPodsService.device = device
|
||||||
|
isConnected = true
|
||||||
|
socket.let { it ->
|
||||||
|
it.outputStream.write(Enums.HANDSHAKE.value)
|
||||||
|
it.outputStream.flush()
|
||||||
|
it.outputStream.write(Enums.SET_SPECIFIC_FEATURES.value)
|
||||||
|
it.outputStream.flush()
|
||||||
|
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
|
||||||
|
it.outputStream.flush()
|
||||||
|
sendBroadcast(
|
||||||
|
Intent(AirPodsNotifications.AIRPODS_CONNECTED)
|
||||||
|
.putExtra("device", device)
|
||||||
|
)
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
while (socket.isConnected == true) {
|
||||||
|
socket.let {
|
||||||
|
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
|
||||||
|
MediaController.initialize(audioManager)
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
val bytesRead = it.inputStream.read(buffer)
|
||||||
|
var data: ByteArray = byteArrayOf()
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
data = buffer.copyOfRange(0, bytesRead)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
|
||||||
|
putExtra("data", buffer.copyOfRange(0, bytesRead))
|
||||||
|
})
|
||||||
|
val bytes = buffer.copyOfRange(0, bytesRead)
|
||||||
|
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
|
||||||
|
Log.d("AirPods Data", "Data received: $formattedHex")
|
||||||
|
}
|
||||||
|
else if (bytesRead == -1) {
|
||||||
|
Log.d("AirPods Service", "Socket closed (bytesRead = -1)")
|
||||||
|
this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
socket.close()
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
var inEar = false
|
||||||
|
var inEarData = listOf<Boolean>()
|
||||||
|
if (earDetectionNotification.isEarDetectionData(data)) {
|
||||||
|
earDetectionNotification.setStatus(data)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.EAR_DETECTION_DATA).apply {
|
||||||
|
val list = earDetectionNotification.status
|
||||||
|
val bytes = ByteArray(2)
|
||||||
|
bytes[0] = list[0]
|
||||||
|
bytes[1] = list[1]
|
||||||
|
putExtra("data", bytes)
|
||||||
|
})
|
||||||
|
Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}")
|
||||||
|
var justEnabledA2dp = false
|
||||||
|
val earReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val data = intent.getByteArrayExtra("data")
|
||||||
|
if (data != null && earDetectionEnabled) {
|
||||||
|
inEar = if (data.find { it == 0x02.toByte() } != null || data.find { it == 0x03.toByte() } != null) {
|
||||||
|
data[0] == 0x00.toByte() || data[1] == 0x00.toByte()
|
||||||
|
} else {
|
||||||
|
data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
val newInEarData = listOf(data[0] == 0x00.toByte(), data[1] == 0x00.toByte())
|
||||||
|
if (newInEarData.contains(true) && inEarData == listOf(false, false)) {
|
||||||
|
connectAudio(this@AirPodsService, device)
|
||||||
|
justEnabledA2dp = true
|
||||||
|
val bluetoothAdapter = this@AirPodsService.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
bluetoothAdapter.getProfileProxy(
|
||||||
|
this@AirPodsService, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(
|
||||||
|
profile: Int,
|
||||||
|
proxy: BluetoothProfile
|
||||||
|
) {
|
||||||
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
|
val connectedDevices =
|
||||||
|
proxy.connectedDevices
|
||||||
|
if (connectedDevices.isNotEmpty()) {
|
||||||
|
MediaController.sendPlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bluetoothAdapter.closeProfileProxy(
|
||||||
|
profile,
|
||||||
|
proxy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(
|
||||||
|
profile: Int
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
,BluetoothProfile.A2DP
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (newInEarData == listOf(false, false)){
|
||||||
|
disconnectAudio(this@AirPodsService, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
inEarData = newInEarData
|
||||||
|
|
||||||
|
if (inEar == true) {
|
||||||
|
if (!justEnabledA2dp) {
|
||||||
|
justEnabledA2dp = false
|
||||||
|
MediaController.sendPlay()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MediaController.sendPause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val earIntentFilter = IntentFilter(AirPodsNotifications.EAR_DETECTION_DATA)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
this@AirPodsService.registerReceiver(earReceiver, earIntentFilter,
|
||||||
|
RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this@AirPodsService.registerReceiver(earReceiver, earIntentFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ancNotification.isANCData(data)) {
|
||||||
|
ancNotification.setStatus(data)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
||||||
|
putExtra("data", ancNotification.status)
|
||||||
|
})
|
||||||
|
Log.d("AirPods Parser", "ANC: ${ancNotification.status}")
|
||||||
|
}
|
||||||
|
else if (batteryNotification.isBatteryData(data)) {
|
||||||
|
batteryNotification.setBattery(data)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
||||||
|
putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery()))
|
||||||
|
})
|
||||||
|
for (battery in batteryNotification.getBattery()) {
|
||||||
|
Log.d("AirPods Parser", "${battery.getComponentName()}: ${battery.getStatusName()} at ${battery.level}% ")
|
||||||
|
}
|
||||||
|
// if both are charging, disconnect audio profiles
|
||||||
|
if (batteryNotification.getBattery()[0].status == 1 && batteryNotification.getBattery()[1].status == 1) {
|
||||||
|
disconnectAudio(this@AirPodsService, device)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
connectAudio(this@AirPodsService, device)
|
||||||
|
}
|
||||||
|
// updatePodsStatus(device!!, batteryNotification.getBattery())
|
||||||
|
}
|
||||||
|
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
|
||||||
|
conversationAwarenessNotification.setData(data)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.CA_DATA).apply {
|
||||||
|
putExtra("data", conversationAwarenessNotification.status)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) {
|
||||||
|
MediaController.startSpeaking()
|
||||||
|
} else if (conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
|
||||||
|
MediaController.stopSpeaking()
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}")
|
||||||
|
}
|
||||||
|
else { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d("AirPods Service", "Socket closed")
|
||||||
|
isConnected = false
|
||||||
|
this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
socket.close()
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Log.d("AirPodsService", "Failed to connect to socket")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var isConnected: Boolean = false
|
|
||||||
private var socket: BluetoothSocket? = null
|
|
||||||
|
|
||||||
fun sendPacket(packet: String) {
|
fun sendPacket(packet: String) {
|
||||||
val fromHex = packet.split(" ").map { it.toInt(16).toByte() }
|
val fromHex = packet.split(" ").map { it.toInt(16).toByte() }
|
||||||
socket?.outputStream?.write(fromHex.toByteArray())
|
socket.outputStream?.write(fromHex.toByteArray())
|
||||||
socket?.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setANCMode(mode: Int) {
|
fun setANCMode(mode: Int) {
|
||||||
when (mode) {
|
when (mode) {
|
||||||
1 -> {
|
1 -> {
|
||||||
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_OFF.value)
|
socket.outputStream?.write(Enums.NOISE_CANCELLATION_OFF.value)
|
||||||
}
|
}
|
||||||
2 -> {
|
2 -> {
|
||||||
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_ON.value)
|
socket.outputStream?.write(Enums.NOISE_CANCELLATION_ON.value)
|
||||||
}
|
}
|
||||||
3 -> {
|
3 -> {
|
||||||
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_TRANSPARENCY.value)
|
socket.outputStream?.write(Enums.NOISE_CANCELLATION_TRANSPARENCY.value)
|
||||||
}
|
}
|
||||||
4 -> {
|
4 -> {
|
||||||
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_ADAPTIVE.value)
|
socket.outputStream?.write(Enums.NOISE_CANCELLATION_ADAPTIVE.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
socket?.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCAEnabled(enabled: Boolean) {
|
fun setCAEnabled(enabled: Boolean) {
|
||||||
socket?.outputStream?.write(if (enabled) Enums.SET_CONVERSATION_AWARENESS_ON.value else Enums.SET_CONVERSATION_AWARENESS_OFF.value)
|
socket.outputStream?.write(if (enabled) Enums.SET_CONVERSATION_AWARENESS_ON.value else Enums.SET_CONVERSATION_AWARENESS_OFF.value)
|
||||||
|
socket.outputStream?.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOffListeningMode(enabled: Boolean) {
|
fun setOffListeningMode(enabled: Boolean) {
|
||||||
socket?.outputStream?.write(byteArrayOf(0x04, 0x00 ,0x04, 0x00, 0x09, 0x00, 0x34, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00))
|
socket.outputStream?.write(byteArrayOf(0x04, 0x00 ,0x04, 0x00, 0x09, 0x00, 0x34, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00))
|
||||||
|
socket.outputStream?.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAdaptiveStrength(strength: Int) {
|
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)
|
||||||
socket?.outputStream?.write(bytes)
|
socket.outputStream?.write(bytes)
|
||||||
socket?.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPressSpeed(speed: Int) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x17, speed.toByte(), 0x00, 0x00, 0x00)
|
||||||
|
socket.outputStream?.write(bytes)
|
||||||
|
socket.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPressAndHoldDuration(speed: Int) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x18, speed.toByte(), 0x00, 0x00, 0x00)
|
||||||
|
socket.outputStream?.write(bytes)
|
||||||
|
socket.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNoiseCancellationWithOnePod(enabled: Boolean) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1B, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00)
|
||||||
|
socket.outputStream?.write(bytes)
|
||||||
|
socket.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVolumeControl(enabled: Boolean) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x25, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00)
|
||||||
|
socket.outputStream?.write(bytes)
|
||||||
|
socket.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVolumeSwipeSpeed(speed: Int) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x23, speed.toByte(), 0x00, 0x00, 0x00)
|
||||||
|
socket.outputStream?.write(bytes)
|
||||||
|
socket.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setToneVolume(volume: Int) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1F, volume.toByte(), 0x50, 0x00, 0x00)
|
||||||
|
socket.outputStream?.write(bytes)
|
||||||
|
socket.outputStream?.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
val earDetectionNotification = AirPodsNotifications.EarDetection()
|
val earDetectionNotification = AirPodsNotifications.EarDetection()
|
||||||
@@ -111,8 +447,8 @@ class AirPodsService : Service() {
|
|||||||
|
|
||||||
fun setCaseChargingSounds(enabled: Boolean) {
|
fun setCaseChargingSounds(enabled: Boolean) {
|
||||||
val bytes = byteArrayOf(0x12, 0x3a, 0x00, 0x01, 0x00, 0x08, if (enabled) 0x00 else 0x01)
|
val bytes = byteArrayOf(0x12, 0x3a, 0x00, 0x01, 0x00, 0x08, if (enabled) 0x00 else 0x01)
|
||||||
socket?.outputStream?.write(bytes)
|
socket.outputStream?.write(bytes)
|
||||||
socket?.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setEarDetection(enabled: Boolean) {
|
fun setEarDetection(enabled: Boolean) {
|
||||||
@@ -126,14 +462,6 @@ class AirPodsService : Service() {
|
|||||||
fun getANC(): Int {
|
fun getANC(): Int {
|
||||||
return ancNotification.status
|
return ancNotification.status
|
||||||
}
|
}
|
||||||
//
|
|
||||||
// private fun buildBatteryText(battery: List<Battery>): String {
|
|
||||||
// val left = battery[0]
|
|
||||||
// val right = battery[1]
|
|
||||||
// val case = battery[2]
|
|
||||||
//
|
|
||||||
// return "Left: ${left.level}% ${left.getStatusName()}, Right: ${right.level}% ${right.getStatusName()}, Case: ${case.level}% ${case.getStatusName()}"
|
|
||||||
// }
|
|
||||||
|
|
||||||
private fun createNotification(): Notification {
|
private fun createNotification(): Notification {
|
||||||
val channelId = "battery"
|
val channelId = "battery"
|
||||||
@@ -227,288 +555,31 @@ class AirPodsService : Service() {
|
|||||||
}, BluetoothProfile.HEADSET)
|
}, BluetoothProfile.HEADSET)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePodsStatus(device: BluetoothDevice, batteryList: List<Battery>) {
|
|
||||||
var batteryUnified = 0
|
|
||||||
var batteryUnifiedArg = 0
|
|
||||||
|
|
||||||
// Handle each Battery object from batteryList
|
|
||||||
// batteryList.forEach { battery ->
|
|
||||||
// when (battery.getComponentName()) {
|
|
||||||
// "LEFT" -> {
|
|
||||||
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 10, battery.level.toString().toByteArray())
|
|
||||||
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 13, battery.getStatusName()?.uppercase()?.toByteArray())
|
|
||||||
// }
|
|
||||||
// "RIGHT" -> {
|
|
||||||
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 11, battery.level.toString().toByteArray())
|
|
||||||
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 14, battery.getStatusName()?.uppercase()?.toByteArray())
|
|
||||||
// }
|
|
||||||
// "CASE" -> {
|
|
||||||
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 12, battery.level.toString().toByteArray())
|
|
||||||
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 15, battery.getStatusName()?.uppercase()?.toByteArray())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// Sending broadcast for battery update
|
|
||||||
broadcastVendorSpecificEventIntent(
|
|
||||||
VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV,
|
|
||||||
APPLE,
|
|
||||||
BluetoothHeadset.AT_CMD_TYPE_SET,
|
|
||||||
batteryUnified,
|
|
||||||
batteryUnifiedArg,
|
|
||||||
device
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("SameParameterValue")
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
private fun broadcastVendorSpecificEventIntent(
|
|
||||||
command: String,
|
|
||||||
companyId: Int,
|
|
||||||
commandType: Int,
|
|
||||||
batteryUnified: Int,
|
|
||||||
batteryUnifiedArg: Int,
|
|
||||||
device: BluetoothDevice
|
|
||||||
) {
|
|
||||||
val arguments = arrayOf(
|
|
||||||
1, // Number of key(IndicatorType)/value pairs
|
|
||||||
VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL, // IndicatorType: Battery Level
|
|
||||||
batteryUnifiedArg // Battery Level
|
|
||||||
)
|
|
||||||
|
|
||||||
val intent = Intent(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT).apply {
|
|
||||||
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD, command)
|
|
||||||
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, commandType)
|
|
||||||
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS, arguments)
|
|
||||||
putExtra(BluetoothDevice.EXTRA_DEVICE, device)
|
|
||||||
putExtra(BluetoothDevice.EXTRA_NAME, device.name)
|
|
||||||
addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "." + companyId.toString())
|
|
||||||
}
|
|
||||||
sendBroadcast(intent)
|
|
||||||
|
|
||||||
val batteryIntent = Intent(ACTION_BATTERY_LEVEL_CHANGED).apply {
|
|
||||||
putExtra(BluetoothDevice.EXTRA_DEVICE, device)
|
|
||||||
putExtra(EXTRA_BATTERY_LEVEL, batteryUnified)
|
|
||||||
}
|
|
||||||
sendBroadcast(batteryIntent)
|
|
||||||
|
|
||||||
val statusIntent = Intent(ACTION_ASI_UPDATE_BLUETOOTH_DATA).setPackage(PACKAGE_ASI).apply {
|
|
||||||
putExtra(ACTION_BATTERY_LEVEL_CHANGED, intent)
|
|
||||||
}
|
|
||||||
sendBroadcast(statusIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun setName(name: String) {
|
fun setName(name: String) {
|
||||||
val nameBytes = name.toByteArray()
|
val nameBytes = name.toByteArray()
|
||||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
|
||||||
nameBytes.size.toByte(), 0x00) + nameBytes
|
nameBytes.size.toByte(), 0x00) + nameBytes
|
||||||
socket?.outputStream?.write(bytes)
|
socket.outputStream?.write(bytes)
|
||||||
socket?.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
val hex = bytes.joinToString(" ") { "%02X".format(it) }
|
val hex = bytes.joinToString(" ") { "%02X".format(it) }
|
||||||
Log.d("AirPodsService", "setName: $name, sent packet: $hex")
|
Log.d("AirPodsService", "setName: $name, sent packet: $hex")
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission", "InlinedApi")
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
|
|
||||||
val notification = createNotification()
|
|
||||||
startForeground(1, notification)
|
|
||||||
|
|
||||||
ServiceManager.setService(this)
|
|
||||||
|
|
||||||
if (isConnected) {
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
isConnected = true
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION") val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) intent?.getParcelableExtra("device", BluetoothDevice::class.java) else intent?.getParcelableExtra("device")
|
|
||||||
|
|
||||||
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
|
||||||
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
|
||||||
|
|
||||||
socket = HiddenApiBypass.newInstance(BluetoothSocket::class.java, 3, true, true, device, 0x1001, uuid) as BluetoothSocket?
|
|
||||||
try {
|
|
||||||
socket?.connect()
|
|
||||||
socket?.let { it ->
|
|
||||||
it.outputStream.write(Enums.HANDSHAKE.value)
|
|
||||||
it.outputStream.write(Enums.SET_SPECIFIC_FEATURES.value)
|
|
||||||
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
|
|
||||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_CONNECTED))
|
|
||||||
it.outputStream.flush()
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
while (socket?.isConnected == true) {
|
|
||||||
socket?.let {
|
|
||||||
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
|
|
||||||
MediaController.initialize(audioManager)
|
|
||||||
val buffer = ByteArray(1024)
|
|
||||||
val bytesRead = it.inputStream.read(buffer)
|
|
||||||
var data: ByteArray = byteArrayOf()
|
|
||||||
if (bytesRead > 0) {
|
|
||||||
data = buffer.copyOfRange(0, bytesRead)
|
|
||||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
|
|
||||||
putExtra("data", buffer.copyOfRange(0, bytesRead))
|
|
||||||
})
|
|
||||||
val bytes = buffer.copyOfRange(0, bytesRead)
|
|
||||||
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
|
|
||||||
Log.d("AirPods Data", "Data received: $formattedHex")
|
|
||||||
}
|
|
||||||
else if (bytesRead == -1) {
|
|
||||||
Log.d("AirPods Service", "Socket closed (bytesRead = -1)")
|
|
||||||
this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
|
|
||||||
socket?.close()
|
|
||||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
var inEar = false
|
|
||||||
var inEarData = listOf<Boolean>()
|
|
||||||
if (earDetectionNotification.isEarDetectionData(data)) {
|
|
||||||
earDetectionNotification.setStatus(data)
|
|
||||||
sendBroadcast(Intent(AirPodsNotifications.EAR_DETECTION_DATA).apply {
|
|
||||||
val list = earDetectionNotification.status
|
|
||||||
val bytes = ByteArray(2)
|
|
||||||
bytes[0] = list[0]
|
|
||||||
bytes[1] = list[1]
|
|
||||||
putExtra("data", bytes)
|
|
||||||
})
|
|
||||||
Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}")
|
|
||||||
var justEnabledA2dp = false
|
|
||||||
val earReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val data = intent.getByteArrayExtra("data")
|
|
||||||
if (data != null && earDetectionEnabled) {
|
|
||||||
inEar = if (data.find { it == 0x02.toByte() } != null || data.find { it == 0x03.toByte() } != null) {
|
|
||||||
data[0] == 0x00.toByte() || data[1] == 0x00.toByte()
|
|
||||||
} else {
|
|
||||||
data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
|
|
||||||
}
|
|
||||||
|
|
||||||
val newInEarData = listOf(data[0] == 0x00.toByte(), data[1] == 0x00.toByte())
|
|
||||||
if (newInEarData.contains(true) && inEarData == listOf(false, false)) {
|
|
||||||
connectAudio(this@AirPodsService, device)
|
|
||||||
justEnabledA2dp = true
|
|
||||||
val bluetoothAdapter = this@AirPodsService.getSystemService(BluetoothManager::class.java).adapter
|
|
||||||
bluetoothAdapter.getProfileProxy(
|
|
||||||
this@AirPodsService, object : BluetoothProfile.ServiceListener {
|
|
||||||
override fun onServiceConnected(
|
|
||||||
profile: Int,
|
|
||||||
proxy: BluetoothProfile
|
|
||||||
) {
|
|
||||||
if (profile == BluetoothProfile.A2DP) {
|
|
||||||
val connectedDevices =
|
|
||||||
proxy.connectedDevices
|
|
||||||
if (connectedDevices.isNotEmpty()) {
|
|
||||||
MediaController.sendPlay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bluetoothAdapter.closeProfileProxy(
|
|
||||||
profile,
|
|
||||||
proxy
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(
|
|
||||||
profile: Int
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
,BluetoothProfile.A2DP
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
else if (newInEarData == listOf(false, false)){
|
|
||||||
disconnectAudio(this@AirPodsService, device)
|
|
||||||
}
|
|
||||||
|
|
||||||
inEarData = newInEarData
|
|
||||||
|
|
||||||
if (inEar == true) {
|
|
||||||
if (!justEnabledA2dp) {
|
|
||||||
justEnabledA2dp = false
|
|
||||||
MediaController.sendPlay()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MediaController.sendPause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val earIntentFilter = IntentFilter(AirPodsNotifications.EAR_DETECTION_DATA)
|
|
||||||
this@AirPodsService.registerReceiver(earReceiver, earIntentFilter,
|
|
||||||
RECEIVER_EXPORTED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else if (ancNotification.isANCData(data)) {
|
|
||||||
ancNotification.setStatus(data)
|
|
||||||
sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
|
||||||
putExtra("data", ancNotification.status)
|
|
||||||
})
|
|
||||||
Log.d("AirPods Parser", "ANC: ${ancNotification.status}")
|
|
||||||
}
|
|
||||||
else if (batteryNotification.isBatteryData(data)) {
|
|
||||||
batteryNotification.setBattery(data)
|
|
||||||
sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
|
||||||
putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery()))
|
|
||||||
})
|
|
||||||
for (battery in batteryNotification.getBattery()) {
|
|
||||||
Log.d("AirPods Parser", "${battery.getComponentName()}: ${battery.getStatusName()} at ${battery.level}% ")
|
|
||||||
}
|
|
||||||
// updatePodsStatus(device!!, batteryNotification.getBattery())
|
|
||||||
}
|
|
||||||
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
|
|
||||||
conversationAwarenessNotification.setData(data)
|
|
||||||
sendBroadcast(Intent(AirPodsNotifications.CA_DATA).apply {
|
|
||||||
putExtra("data", conversationAwarenessNotification.status)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) {
|
|
||||||
MediaController.startSpeaking()
|
|
||||||
} else if (conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
|
|
||||||
MediaController.stopSpeaking()
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}")
|
|
||||||
}
|
|
||||||
else { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d("AirPods Service", "Socket closed")
|
|
||||||
isConnected = false
|
|
||||||
this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
|
|
||||||
socket?.close()
|
|
||||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e: Exception) {
|
|
||||||
Log.e("AirPodsSettingsScreen", "Error connecting to device: ${e.message}")
|
|
||||||
}
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
socket?.close()
|
|
||||||
isConnected = false
|
|
||||||
ServiceManager.setService(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPVEnabled(enabled: Boolean) {
|
fun setPVEnabled(enabled: Boolean) {
|
||||||
var hex = "04 00 04 00 09 00 26 ${if (enabled) "01" else "02"} 00 00 00"
|
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()
|
var bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
||||||
socket?.outputStream?.write(bytes)
|
socket.outputStream?.write(bytes)
|
||||||
|
socket.outputStream?.flush()
|
||||||
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()
|
bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
||||||
socket?.outputStream?.write(bytes)
|
socket.outputStream?.write(bytes)
|
||||||
|
socket.outputStream?.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLoudSoundReduction(enabled: Boolean) {
|
fun setLoudSoundReduction(enabled: Boolean) {
|
||||||
val hex = "52 1B 00 0${if (enabled) "1" else "0"}"
|
val hex = "52 1B 00 0${if (enabled) "1" else "0"}"
|
||||||
val bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
val bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
||||||
socket?.outputStream?.write(bytes)
|
socket.outputStream?.write(bytes)
|
||||||
|
socket.outputStream?.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,14 +37,18 @@ import androidx.compose.foundation.text.BasicTextField
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.VerticalDivider
|
import androidx.compose.material3.VerticalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -77,6 +81,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import com.primex.core.ExperimentalToolkitApi
|
||||||
|
import com.primex.core.blur.newBackgroundBlur
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -150,6 +156,251 @@ fun BatteryView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ToneVolumeSlider(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
|
val sliderValue = remember { mutableFloatStateOf(0f) }
|
||||||
|
LaunchedEffect(sliderValue) {
|
||||||
|
if (sharedPreferences.contains("tone_volume")) {
|
||||||
|
sliderValue.floatValue = sharedPreferences.getInt("tone_volume", 0).toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(sliderValue.floatValue) {
|
||||||
|
sharedPreferences.edit().putInt("tone_volume", sliderValue.floatValue.toInt()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
|
||||||
|
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
||||||
|
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||||
|
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||||
|
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "\uDBC0\uDEA1",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = labelTextColor
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(start = 4.dp)
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = sliderValue.floatValue,
|
||||||
|
onValueChange = {
|
||||||
|
sliderValue.floatValue = it
|
||||||
|
service.setToneVolume(volume = it.toInt())
|
||||||
|
},
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
onValueChangeFinished = {
|
||||||
|
// Round the value when the user stops sliding
|
||||||
|
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(36.dp), // Adjust height to ensure thumb fits well
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = thumbColor,
|
||||||
|
activeTrackColor = activeTrackColor,
|
||||||
|
inactiveTrackColor = trackColor
|
||||||
|
),
|
||||||
|
thumb = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp) // Circular thumb size
|
||||||
|
.shadow(4.dp, CircleShape) // Apply shadow to the thumb
|
||||||
|
.background(thumbColor, CircleShape) // Circular thumb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
track = {
|
||||||
|
Box (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(12.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(4.dp)
|
||||||
|
.background(trackColor, RoundedCornerShape(4.dp))
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(sliderValue.value / 100)
|
||||||
|
.height(4.dp)
|
||||||
|
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "\uDBC0\uDEA9",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = labelTextColor
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SinglePodANCSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
|
var singleANCEnabled by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
sharedPreferences.getBoolean("single_anc", true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the service when the toggle is changed
|
||||||
|
fun updateSingleEnabled(enabled: Boolean) {
|
||||||
|
singleANCEnabled = enabled
|
||||||
|
sharedPreferences.edit().putBoolean("single_anc", enabled).apply()
|
||||||
|
service.setNoiseCancellationWithOnePod(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
val isPressed = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
|
.pointerInput(Unit) { // Detect press state for iOS-like effect
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
isPressed.value = true
|
||||||
|
tryAwaitRelease() // Wait until release
|
||||||
|
isPressed.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
indication = null, // Disable ripple effect
|
||||||
|
interactionSource = remember { MutableInteractionSource() } // Required for clickable
|
||||||
|
) {
|
||||||
|
// Toggle the conversational awareness value
|
||||||
|
updateSingleEnabled(!singleANCEnabled)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Noise Cancellation with Single AirPod",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description
|
||||||
|
Text(
|
||||||
|
text = "Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
StyledSwitch(
|
||||||
|
checked = singleANCEnabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
updateSingleEnabled(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VolumeControlSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
|
var volumeControlEnabled by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
sharedPreferences.getBoolean("volume_control", true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the service when the toggle is changed
|
||||||
|
fun updateVolumeControlEnabled(enabled: Boolean) {
|
||||||
|
volumeControlEnabled = enabled
|
||||||
|
sharedPreferences.edit().putBoolean("volume_control", enabled).apply()
|
||||||
|
service.setVolumeControl(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
val isPressed = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
|
.pointerInput(Unit) { // Detect press state for iOS-like effect
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
isPressed.value = true
|
||||||
|
tryAwaitRelease() // Wait until release
|
||||||
|
isPressed.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
indication = null, // Disable ripple effect
|
||||||
|
interactionSource = remember { MutableInteractionSource() } // Required for clickable
|
||||||
|
) {
|
||||||
|
// Toggle the conversational awareness value
|
||||||
|
updateVolumeControlEnabled(!volumeControlEnabled)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Volume Control",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description
|
||||||
|
Text(
|
||||||
|
text = "Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
StyledSwitch(
|
||||||
|
checked = volumeControlEnabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
updateVolumeControlEnabled(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
@@ -173,75 +424,151 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref
|
|||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
) {
|
) {
|
||||||
//
|
|
||||||
|
// Tone Volume Slider
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Tone Volume",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ToneVolumeSlider(service = service, sharedPreferences = sharedPreferences)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropdown menu with 3 options, Default, Slower, Slowest – Press speed
|
||||||
|
// Dropdown menu with 3 options, Default, Slower, Slowest – Press and hold duration
|
||||||
|
// IndependentToggle for Noise Cancellation with one AirPod
|
||||||
|
// IndependentToggle for Enable Volume Control
|
||||||
|
// Dropdown menu with 3 options, Default, Slower, Slowest – Volume Swipe Speed
|
||||||
|
|
||||||
|
// IndependentToggle(name = "Noise Cancellation with one AirPod", service = service, functionName = "setNoiseCancellationWithOnePod", sharedPreferences = sharedPreferences, false)
|
||||||
|
|
||||||
|
SinglePodANCSwitch(service = service, sharedPreferences = sharedPreferences)
|
||||||
|
|
||||||
|
// IndependentToggle(name = "Enable Volume Control", service = service, functionName = "setVolumeControl", sharedPreferences = sharedPreferences, true)
|
||||||
|
|
||||||
|
VolumeControlSwitch(service = service, sharedPreferences = sharedPreferences)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalToolkitApi::class)
|
||||||
@SuppressLint("MissingPermission", "NewApi")
|
@SuppressLint("MissingPermission", "NewApi")
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?,
|
fun AirPodsSettingsScreen(device: BluetoothDevice?, service: AirPodsService?,
|
||||||
navController: NavController) {
|
navController: NavController) {
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
var deviceName by remember { mutableStateOf(TextFieldValue(sharedPreferences.getString("name", device?.name ?: "") ?: "")) }
|
var deviceName by remember { mutableStateOf(TextFieldValue(sharedPreferences.getString("name", device?.name ?: "") ?: "")) }
|
||||||
// 4B 61 76 69 73 68 E2 80 99 73 20 41 69 72 50 6F 64 73 20 50 72 6F
|
// 4B 61 76 69 73 68 E2 80 99 73 20 41 69 72 50 6F 64 73 20 50 72 6F
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
Column(
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
modifier = Modifier
|
Scaffold(
|
||||||
.fillMaxSize()
|
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
||||||
.padding(paddingValues)
|
0xFF000000
|
||||||
.padding(vertical = 24.dp, horizontal = 12.dp)
|
) else Color(
|
||||||
.verticalScroll(
|
0xFFF2F2F7
|
||||||
state = verticalScrollState,
|
),
|
||||||
enabled = true,
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = device!!.name,
|
||||||
|
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.newBackgroundBlur(
|
||||||
|
radius = 24.dp, // the radius of the blur effect, in pixels)
|
||||||
|
),
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
|
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.Black.copy(0.2f) else Color(0xFFF2F2F7).copy(0.2f),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
) {
|
HorizontalDivider(thickness = 3.dp, color = Color.DarkGray)
|
||||||
LaunchedEffect(service) {
|
|
||||||
service?.let {
|
|
||||||
it.sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
|
||||||
putParcelableArrayListExtra("data", ArrayList(it.getBattery()))
|
|
||||||
})
|
|
||||||
it.sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
|
||||||
putExtra("data", it.getANC())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
if (service != null) {
|
modifier = Modifier
|
||||||
BatteryView()
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
// .padding(top = 55.dp, bottom = 32.dp)
|
||||||
StyledTextField(
|
.verticalScroll(
|
||||||
name = "Name",
|
state = verticalScrollState,
|
||||||
value = deviceName.text,
|
enabled = true,
|
||||||
onValueChange = {
|
)
|
||||||
deviceName = TextFieldValue(it)
|
) {
|
||||||
sharedPreferences.edit().putString("name", it).apply()
|
Spacer(Modifier.height(75.dp))
|
||||||
service.setName(it)
|
LaunchedEffect(service) {
|
||||||
|
service?.let {
|
||||||
|
it.sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
||||||
|
putParcelableArrayListExtra("data", ArrayList(it.getBattery()))
|
||||||
|
})
|
||||||
|
it.sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
||||||
|
putExtra("data", it.getANC())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
val sharedPreferences =
|
||||||
|
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
if (service != null) {
|
||||||
NoiseControlSettings(service = service)
|
BatteryView()
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
AudioSettings(service = service, sharedPreferences = sharedPreferences)
|
StyledTextField(
|
||||||
|
name = "Name",
|
||||||
|
value = deviceName.text,
|
||||||
|
onValueChange = {
|
||||||
|
deviceName = TextFieldValue(it)
|
||||||
|
sharedPreferences.edit().putString("name", it).apply()
|
||||||
|
service.setName(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
IndependentToggle(name = "Automatic Ear Detection", service = service, functionName = "setEarDetection", sharedPreferences = sharedPreferences, true)
|
NoiseControlSettings(service = service)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
IndependentToggle(name = "Off Listening Mode", service = service, functionName = "setOffListeningMode", sharedPreferences = sharedPreferences, false)
|
AudioSettings(service = service, sharedPreferences = sharedPreferences)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
AccessibilitySettings(service = service, sharedPreferences = sharedPreferences)
|
IndependentToggle(
|
||||||
|
name = "Automatic Ear Detection",
|
||||||
|
service = service,
|
||||||
|
functionName = "setEarDetection",
|
||||||
|
sharedPreferences = sharedPreferences,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
IndependentToggle(
|
||||||
|
name = "Off Listening Mode",
|
||||||
|
service = service,
|
||||||
|
functionName = "setOffListeningMode",
|
||||||
|
sharedPreferences = sharedPreferences,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
AccessibilitySettings(service = service, sharedPreferences = sharedPreferences)
|
||||||
// Spacer(modifier = Modifier.height(16.dp))
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
// val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
// val textColor = if (isDarkTheme) Color.White else Color.Black
|
// val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
// localstorage stuff
|
// localstorage stuff
|
||||||
// TODO: localstorage and call the setButtons() with previous configuration and new configuration
|
// TODO: localstorage and call the setButtons() with previous configuration and new configuration
|
||||||
// Box (
|
// Box (
|
||||||
// modifier = Modifier
|
// modifier = Modifier
|
||||||
// .padding(vertical = 8.dp)
|
// .padding(vertical = 8.dp)
|
||||||
@@ -254,28 +581,44 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
|
|||||||
// // TODO: A Column Rows with text at start and a check mark if ticked
|
// // TODO: A Column Rows with text at start and a check mark if ticked
|
||||||
// }
|
// }
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Row (
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1C1E) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp))
|
.background(
|
||||||
.height(55.dp)
|
if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
||||||
.clickable {
|
0xFF1C1C1E
|
||||||
navController.navigate("debug")
|
) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp)
|
||||||
}
|
)
|
||||||
) {
|
.height(55.dp)
|
||||||
Text(text = "Debug", modifier = Modifier.padding(16.dp), color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black)
|
.clickable {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
navController.navigate("debug")
|
||||||
IconButton(
|
}
|
||||||
onClick = { navController.navigate("debug") },
|
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black ),
|
|
||||||
modifier = Modifier.padding(start = 16.dp).fillMaxHeight()
|
|
||||||
) {
|
) {
|
||||||
@Suppress("DEPRECATION")
|
Text(
|
||||||
Icon(imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "Debug")
|
text = "Debug",
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
IconButton(
|
||||||
|
onClick = { navController.navigate("debug") },
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.KeyboardArrowRight,
|
||||||
|
contentDescription = "Debug"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,7 +639,6 @@ fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPrefere
|
|||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
|
||||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
|
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
|
||||||
val activeTrackColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFF007AFF)
|
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
@@ -323,7 +665,6 @@ fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPrefere
|
|||||||
.height(36.dp), // Adjust height to ensure thumb fits well
|
.height(36.dp), // Adjust height to ensure thumb fits well
|
||||||
colors = SliderDefaults.colors(
|
colors = SliderDefaults.colors(
|
||||||
thumbColor = thumbColor,
|
thumbColor = thumbColor,
|
||||||
activeTrackColor = activeTrackColor,
|
|
||||||
inactiveTrackColor = trackColor
|
inactiveTrackColor = trackColor
|
||||||
),
|
),
|
||||||
thumb = {
|
thumb = {
|
||||||
@@ -338,9 +679,18 @@ fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPrefere
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(12.dp)
|
.height(12.dp),
|
||||||
.background(trackColor, RoundedCornerShape(6.dp))
|
contentAlignment = Alignment.CenterStart
|
||||||
)
|
)
|
||||||
|
{
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(4.dp)
|
||||||
|
.background(trackColor, RoundedCornerShape(4.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -695,15 +1045,15 @@ fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences)
|
|||||||
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
|
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.",
|
text = "Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(8.dp, top = 2.dp)
|
.padding(bottom = 8.dp, top = 2.dp)
|
||||||
|
.padding(end = 2.dp, start = 2.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
|
class BootReceiver {
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package me.kavishdevar.aln
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
import android.bluetooth.BluetoothManager
|
|
||||||
import android.bluetooth.BluetoothProfile
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -13,7 +10,6 @@ import android.content.ServiceConnection
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ParcelUuid
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -22,28 +18,16 @@ import androidx.compose.animation.core.tween
|
|||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.luminance
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
@@ -51,98 +35,35 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.google.accompanist.permissions.shouldShowRationale
|
import com.google.accompanist.permissions.shouldShowRationale
|
||||||
|
import com.primex.core.ExperimentalToolkitApi
|
||||||
import me.kavishdevar.aln.ui.theme.ALNTheme
|
import me.kavishdevar.aln.ui.theme.ALNTheme
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalMaterial3Api
|
@ExperimentalMaterial3Api
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
@OptIn(ExperimentalToolkitApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
val topAppBarTitle = remember { mutableStateOf("AirPods Pro") }
|
|
||||||
ALNTheme {
|
ALNTheme {
|
||||||
val navController = rememberNavController()
|
Main()
|
||||||
registerReceiver(object: BroadcastReceiver() {
|
startService(Intent(this, AirPodsService::class.java))
|
||||||
override fun onReceive(context: Context?, intent: Intent) {
|
|
||||||
val bluetoothDevice =
|
|
||||||
intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java)
|
|
||||||
val action = intent.action
|
|
||||||
|
|
||||||
// Airpods filter
|
|
||||||
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
|
|
||||||
Log.d("BluetoothReceiver", "Received broadcast")
|
|
||||||
// Airpods connected, show notification.
|
|
||||||
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
|
|
||||||
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
|
||||||
if (bluetoothDevice.uuids.contains(uuid)) {
|
|
||||||
topAppBarTitle.value = bluetoothDevice.name
|
|
||||||
}
|
|
||||||
// start service
|
|
||||||
startService(Intent(context, AirPodsService::class.java).apply {
|
|
||||||
putExtra("device", bluetoothDevice)
|
|
||||||
})
|
|
||||||
Log.d("AirPodsService", "Service started")
|
|
||||||
context?.sendBroadcast(Intent(AirPodsNotifications.AIRPODS_CONNECTED))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Airpods disconnected, remove notification but leave the scanner going.
|
|
||||||
if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action
|
|
||||||
|| BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action
|
|
||||||
) {
|
|
||||||
topAppBarTitle.value = "AirPods Pro"
|
|
||||||
// stop service
|
|
||||||
stopService(Intent(context, AirPodsService::class.java))
|
|
||||||
Log.d("AirPodsService", "Service stopped")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, BluetoothReceiver.buildFilter())
|
|
||||||
|
|
||||||
Scaffold (
|
|
||||||
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
|
||||||
0xFF000000
|
|
||||||
) else Color(
|
|
||||||
0xFFF2F2F7
|
|
||||||
),
|
|
||||||
topBar = {
|
|
||||||
CenterAlignedTopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = topAppBarTitle.value,
|
|
||||||
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
|
||||||
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
|
||||||
0xFF000000
|
|
||||||
) else Color(
|
|
||||||
0xFFF2F2F7
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { innerPadding ->
|
|
||||||
Main(innerPadding, topAppBarTitle)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission", "InlinedApi")
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
|
fun Main() {
|
||||||
val bluetoothConnectPermissionState = rememberPermissionState(
|
val bluetoothConnectPermissionState = rememberPermissionState(
|
||||||
permission = "android.permission.BLUETOOTH_CONNECT"
|
permission = "android.permission.BLUETOOTH_CONNECT"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (bluetoothConnectPermissionState.status.isGranted) {
|
if (bluetoothConnectPermissionState.status.isGranted) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
|
||||||
val bluetoothManager = getSystemService(context, BluetoothManager::class.java)
|
|
||||||
val bluetoothAdapter = bluetoothManager?.adapter
|
|
||||||
val airpodsDevice = remember { mutableStateOf<BluetoothDevice?>(null) }
|
|
||||||
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
@@ -157,58 +78,6 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
|
|||||||
Context.RECEIVER_NOT_EXPORTED)
|
Context.RECEIVER_NOT_EXPORTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service connection for AirPodsService
|
|
||||||
val serviceConnection = object : ServiceConnection {
|
|
||||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
|
||||||
val binder = service as AirPodsService.LocalBinder
|
|
||||||
airPodsService.value = binder.getService()
|
|
||||||
Log.d("AirPodsService", "Service connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName) {
|
|
||||||
airPodsService.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to check if AirPods are connected
|
|
||||||
fun checkIfAirPodsConnected() {
|
|
||||||
val devices = bluetoothAdapter?.bondedDevices
|
|
||||||
devices?.forEach { device ->
|
|
||||||
if (device.uuids.contains(uuid)) {
|
|
||||||
bluetoothAdapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
|
||||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
|
||||||
if (profile == BluetoothProfile.A2DP) {
|
|
||||||
val connectedDevices = proxy.connectedDevices
|
|
||||||
if (connectedDevices.isNotEmpty()) {
|
|
||||||
airpodsDevice.value = device
|
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
topAppBarTitle.value = sharedPreferences.getString("name", device.name) ?: device.name
|
|
||||||
// Start AirPods service if not running
|
|
||||||
if (context.getSystemService(AirPodsService::class.java)?.isConnected != true) {
|
|
||||||
context.startService(Intent(context, AirPodsService::class.java).apply {
|
|
||||||
putExtra("device", device)
|
|
||||||
})
|
|
||||||
context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
airpodsDevice.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bluetoothAdapter.closeProfileProxy(profile, proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(profile: Int) {}
|
|
||||||
}, BluetoothProfile.A2DP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the receiver in LaunchedEffect
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
// Initial check for AirPods connection
|
|
||||||
checkIfAirPodsConnected()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI logic
|
// UI logic
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
@@ -223,8 +92,7 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
|
|||||||
}
|
}
|
||||||
composable("settings") {
|
composable("settings") {
|
||||||
AirPodsSettingsScreen(
|
AirPodsSettingsScreen(
|
||||||
paddingValues,
|
device = airPodsService.value?.device,
|
||||||
airpodsDevice.value,
|
|
||||||
service = airPodsService.value,
|
service = airPodsService.value,
|
||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
@@ -234,30 +102,37 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextCompat.registerReceiver(
|
val receiver = object: BroadcastReceiver() {
|
||||||
context,
|
override fun onReceive(p0: Context?, p1: Intent?) {
|
||||||
object : BroadcastReceiver() {
|
navController.navigate("settings")
|
||||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
navController.popBackStack("notConnected", inclusive = true)
|
||||||
override fun onReceive(context: Context?, intent: Intent) {
|
}
|
||||||
Log.d("PLEASE NAVIGATE", "TO SETTINGS")
|
}
|
||||||
navController.navigate("settings") {
|
|
||||||
popUpTo("notConnected") { inclusive = true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
IntentFilter(AirPodsNotifications.AIRPODS_CONNECTED),
|
|
||||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
|
||||||
)
|
|
||||||
|
|
||||||
// Automatically navigate to settings screen if AirPods are connected
|
context.registerReceiver(receiver, IntentFilter(AirPodsNotifications.AIRPODS_CONNECTED),
|
||||||
if (airpodsDevice.value != null) {
|
Context.RECEIVER_EXPORTED)
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
navController.navigate("settings") {
|
val serviceConnection = remember {
|
||||||
popUpTo("notConnected") { inclusive = true }
|
object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
val binder = service as AirPodsService.LocalBinder
|
||||||
|
airPodsService.value = binder.getService()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
airPodsService.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
|
||||||
|
if (airPodsService.value?.isConnected == true) {
|
||||||
|
Log.d("ALN", "Connected")
|
||||||
|
navController.navigate("settings")
|
||||||
} else {
|
} else {
|
||||||
Text("No AirPods connected")
|
Log.d("ALN", "Not connected")
|
||||||
|
navController.navigate("notConnected")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Permission is not granted, request it
|
// Permission is not granted, request it
|
||||||
@@ -277,10 +152,4 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewLightDark
|
|
||||||
@Composable
|
|
||||||
fun PreviewAirPodsSettingsScreen() {
|
|
||||||
AirPodsSettingsScreen(paddingValues = PaddingValues(0.dp), device = null, service = null, navController = rememberNavController())
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothProfile
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
|
|
||||||
|
object ServiceManager {
|
||||||
|
private var service: OldAirPodsService? = null
|
||||||
|
@Synchronized
|
||||||
|
fun getService(): OldAirPodsService? {
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
@Synchronized
|
||||||
|
fun setService(service: OldAirPodsService?) {
|
||||||
|
this.service = service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OldAirPodsService : Service() {
|
||||||
|
inner class LocalBinder : Binder() {
|
||||||
|
fun getService(): OldAirPodsService = this@OldAirPodsService
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder {
|
||||||
|
return LocalBinder()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isConnected: Boolean = false
|
||||||
|
private var socket: BluetoothSocket? = null
|
||||||
|
|
||||||
|
fun sendPacket(packet: String) {
|
||||||
|
val fromHex = packet.split(" ").map { it.toInt(16).toByte() }
|
||||||
|
socket?.outputStream?.write(fromHex.toByteArray())
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setANCMode(mode: Int) {
|
||||||
|
when (mode) {
|
||||||
|
1 -> {
|
||||||
|
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_OFF.value)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_ON.value)
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_TRANSPARENCY.value)
|
||||||
|
}
|
||||||
|
4 -> {
|
||||||
|
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_ADAPTIVE.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCAEnabled(enabled: Boolean) {
|
||||||
|
socket?.outputStream?.write(if (enabled) Enums.SET_CONVERSATION_AWARENESS_ON.value else Enums.SET_CONVERSATION_AWARENESS_OFF.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOffListeningMode(enabled: Boolean) {
|
||||||
|
socket?.outputStream?.write(byteArrayOf(0x04, 0x00 ,0x04, 0x00, 0x09, 0x00, 0x34, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAdaptiveStrength(strength: Int) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00)
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPressSpeed(speed: Int) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x17, speed.toByte(), 0x00, 0x00, 0x00)
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPressAndHoldDuration(speed: Int) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x18, speed.toByte(), 0x00, 0x00, 0x00)
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNoiseCancellationWithOnePod(enabled: Boolean) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1B, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00)
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVolumeControl(enabled: Boolean) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x25, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00)
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVolumeSwipeSpeed(speed: Int) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x23, speed.toByte(), 0x00, 0x00, 0x00)
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setToneVolume(volume: Int) {
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1F, volume.toByte(), 0x50, 0x00, 0x00)
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
val earDetectionNotification = AirPodsNotifications.EarDetection()
|
||||||
|
val ancNotification = AirPodsNotifications.ANC()
|
||||||
|
val batteryNotification = AirPodsNotifications.BatteryNotification()
|
||||||
|
val conversationAwarenessNotification = AirPodsNotifications.ConversationalAwarenessNotification()
|
||||||
|
|
||||||
|
var earDetectionEnabled = true
|
||||||
|
|
||||||
|
fun setCaseChargingSounds(enabled: Boolean) {
|
||||||
|
val bytes = byteArrayOf(0x12, 0x3a, 0x00, 0x01, 0x00, 0x08, if (enabled) 0x00 else 0x01)
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEarDetection(enabled: Boolean) {
|
||||||
|
earDetectionEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBattery(): List<Battery> {
|
||||||
|
return batteryNotification.getBattery()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getANC(): Int {
|
||||||
|
return ancNotification.status
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotification(): Notification {
|
||||||
|
val channelId = "battery"
|
||||||
|
val notificationBuilder = NotificationCompat.Builder(this, channelId)
|
||||||
|
.setSmallIcon(R.drawable.pro_2_buds)
|
||||||
|
.setContentTitle("AirPods Connected")
|
||||||
|
.setOngoing(true)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
|
||||||
|
val channel =
|
||||||
|
NotificationChannel(channelId, "Battery Notification", NotificationManager.IMPORTANCE_LOW)
|
||||||
|
|
||||||
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
return notificationBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnectAudio(context: Context, device: BluetoothDevice?) {
|
||||||
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||||
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
|
try {
|
||||||
|
val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
|
||||||
|
method.invoke(proxy, device)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
method.invoke(proxy, device)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(profile: Int) { }
|
||||||
|
}, BluetoothProfile.HEADSET)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connectAudio(context: Context, device: BluetoothDevice?) {
|
||||||
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||||
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
|
try {
|
||||||
|
val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
|
||||||
|
method.invoke(proxy, device)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
method.invoke(proxy, device)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
val hex = bytes.joinToString(" ") { "%02X".format(it) }
|
||||||
|
Log.d("OldAirPodsService", "setName: $name, sent packet: $hex")
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission", "InlinedApi")
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
|
||||||
|
val notification = createNotification()
|
||||||
|
startForeground(1, notification)
|
||||||
|
|
||||||
|
ServiceManager.setService(this)
|
||||||
|
|
||||||
|
if (isConnected) {
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
isConnected = true
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION") val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) intent?.getParcelableExtra("device", BluetoothDevice::class.java) else intent?.getParcelableExtra("device")
|
||||||
|
|
||||||
|
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
||||||
|
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
|
|
||||||
|
socket = HiddenApiBypass.newInstance(BluetoothSocket::class.java, 3, true, true, device, 0x1001, uuid) as BluetoothSocket?
|
||||||
|
try {
|
||||||
|
socket?.connect()
|
||||||
|
socket?.let { it ->
|
||||||
|
it.outputStream.write(Enums.HANDSHAKE.value)
|
||||||
|
it.outputStream.write(Enums.SET_SPECIFIC_FEATURES.value)
|
||||||
|
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_CONNECTED))
|
||||||
|
it.outputStream.flush()
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
while (socket?.isConnected == true) {
|
||||||
|
socket?.let {
|
||||||
|
val audioManager = this@OldAirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
|
||||||
|
MediaController.initialize(audioManager)
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
val bytesRead = it.inputStream.read(buffer)
|
||||||
|
var data: ByteArray = byteArrayOf()
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
data = buffer.copyOfRange(0, bytesRead)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
|
||||||
|
putExtra("data", buffer.copyOfRange(0, bytesRead))
|
||||||
|
})
|
||||||
|
val bytes = buffer.copyOfRange(0, bytesRead)
|
||||||
|
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
|
||||||
|
Log.d("AirPods Data", "Data received: $formattedHex")
|
||||||
|
}
|
||||||
|
else if (bytesRead == -1) {
|
||||||
|
Log.d("AirPods Service", "Socket closed (bytesRead = -1)")
|
||||||
|
this@OldAirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
socket?.close()
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
var inEar = false
|
||||||
|
var inEarData = listOf<Boolean>()
|
||||||
|
if (earDetectionNotification.isEarDetectionData(data)) {
|
||||||
|
earDetectionNotification.setStatus(data)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.EAR_DETECTION_DATA).apply {
|
||||||
|
val list = earDetectionNotification.status
|
||||||
|
val bytes = ByteArray(2)
|
||||||
|
bytes[0] = list[0]
|
||||||
|
bytes[1] = list[1]
|
||||||
|
putExtra("data", bytes)
|
||||||
|
})
|
||||||
|
Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}")
|
||||||
|
var justEnabledA2dp = false
|
||||||
|
val earReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val data = intent.getByteArrayExtra("data")
|
||||||
|
if (data != null && earDetectionEnabled) {
|
||||||
|
inEar = if (data.find { it == 0x02.toByte() } != null || data.find { it == 0x03.toByte() } != null) {
|
||||||
|
data[0] == 0x00.toByte() || data[1] == 0x00.toByte()
|
||||||
|
} else {
|
||||||
|
data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
val newInEarData = listOf(data[0] == 0x00.toByte(), data[1] == 0x00.toByte())
|
||||||
|
if (newInEarData.contains(true) && inEarData == listOf(false, false)) {
|
||||||
|
connectAudio(this@OldAirPodsService, device)
|
||||||
|
justEnabledA2dp = true
|
||||||
|
val bluetoothAdapter = this@OldAirPodsService.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
bluetoothAdapter.getProfileProxy(
|
||||||
|
this@OldAirPodsService, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(
|
||||||
|
profile: Int,
|
||||||
|
proxy: BluetoothProfile
|
||||||
|
) {
|
||||||
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
|
val connectedDevices =
|
||||||
|
proxy.connectedDevices
|
||||||
|
if (connectedDevices.isNotEmpty()) {
|
||||||
|
MediaController.sendPlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bluetoothAdapter.closeProfileProxy(
|
||||||
|
profile,
|
||||||
|
proxy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(
|
||||||
|
profile: Int
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
,BluetoothProfile.A2DP
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (newInEarData == listOf(false, false)){
|
||||||
|
disconnectAudio(this@OldAirPodsService, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
inEarData = newInEarData
|
||||||
|
|
||||||
|
if (inEar == true) {
|
||||||
|
if (!justEnabledA2dp) {
|
||||||
|
justEnabledA2dp = false
|
||||||
|
MediaController.sendPlay()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MediaController.sendPause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val earIntentFilter = IntentFilter(AirPodsNotifications.EAR_DETECTION_DATA)
|
||||||
|
this@OldAirPodsService.registerReceiver(earReceiver, earIntentFilter,
|
||||||
|
RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else if (ancNotification.isANCData(data)) {
|
||||||
|
ancNotification.setStatus(data)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
||||||
|
putExtra("data", ancNotification.status)
|
||||||
|
})
|
||||||
|
Log.d("AirPods Parser", "ANC: ${ancNotification.status}")
|
||||||
|
}
|
||||||
|
else if (batteryNotification.isBatteryData(data)) {
|
||||||
|
batteryNotification.setBattery(data)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
||||||
|
putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery()))
|
||||||
|
})
|
||||||
|
for (battery in batteryNotification.getBattery()) {
|
||||||
|
Log.d("AirPods Parser", "${battery.getComponentName()}: ${battery.getStatusName()} at ${battery.level}% ")
|
||||||
|
}
|
||||||
|
// if both are charging, disconnect audio profiles
|
||||||
|
if (batteryNotification.getBattery()[0].status == 1 && batteryNotification.getBattery()[1].status == 1) {
|
||||||
|
disconnectAudio(this@OldAirPodsService, device)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
connectAudio(this@OldAirPodsService, device)
|
||||||
|
}
|
||||||
|
// updatePodsStatus(device!!, batteryNotification.getBattery())
|
||||||
|
}
|
||||||
|
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
|
||||||
|
conversationAwarenessNotification.setData(data)
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.CA_DATA).apply {
|
||||||
|
putExtra("data", conversationAwarenessNotification.status)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) {
|
||||||
|
MediaController.startSpeaking()
|
||||||
|
} else if (conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
|
||||||
|
MediaController.stopSpeaking()
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}")
|
||||||
|
}
|
||||||
|
else { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d("AirPods Service", "Socket closed")
|
||||||
|
isConnected = false
|
||||||
|
this@OldAirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
socket?.close()
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e: Exception) {
|
||||||
|
Log.e("AirPodsSettingsScreen", "Error connecting to device: ${e.message}")
|
||||||
|
}
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
socket?.close()
|
||||||
|
isConnected = false
|
||||||
|
ServiceManager.setService(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPVEnabled(enabled: Boolean) {
|
||||||
|
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()
|
||||||
|
socket?.outputStream?.write(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"
|
||||||
|
bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLoudSoundReduction(enabled: Boolean) {
|
||||||
|
val hex = "52 1B 00 0${if (enabled) "1" else "0"}"
|
||||||
|
val bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,7 @@ class AirPodsNotifications {
|
|||||||
const val BATTERY_DATA = "me.kavishdevar.aln.BATTERY_DATA"
|
const val BATTERY_DATA = "me.kavishdevar.aln.BATTERY_DATA"
|
||||||
const val CA_DATA = "me.kavishdevar.aln.CA_DATA"
|
const val CA_DATA = "me.kavishdevar.aln.CA_DATA"
|
||||||
const val AIRPODS_DISCONNECTED = "me.kavishdevar.aln.AIRPODS_DISCONNECTED"
|
const val AIRPODS_DISCONNECTED = "me.kavishdevar.aln.AIRPODS_DISCONNECTED"
|
||||||
|
const val AIRPODS_CONNECTION_DETECTED = "me.kavishdevar.aln.AIRPODS_CONNECTION_DETECTED"
|
||||||
}
|
}
|
||||||
|
|
||||||
class EarDetection {
|
class EarDetection {
|
||||||
|
|||||||
130
android/app/src/main/java/me/kavishdevar/aln/Window.kt
Normal file
130
android/app/src/main/java/me/kavishdevar/aln/Window.kt
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.view.animation.AccelerateInterpolator
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.VideoView
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
class Window @SuppressLint("InflateParams") constructor(
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
private val mView: View
|
||||||
|
private val mParams: WindowManager.LayoutParams = WindowManager.LayoutParams(
|
||||||
|
(context.resources.displayMetrics.widthPixels * 0.95).toInt(),
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT, // Display it on top of other application windows
|
||||||
|
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, // Don't let it grab the input focus
|
||||||
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // Make the underlying application window visible
|
||||||
|
PixelFormat.TRANSLUCENT
|
||||||
|
)
|
||||||
|
private val mWindowManager: WindowManager
|
||||||
|
init {
|
||||||
|
val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
mView = layoutInflater.inflate(R.layout.popup_window, null)
|
||||||
|
mParams.x = 0
|
||||||
|
mParams.y = 0
|
||||||
|
|
||||||
|
mParams.gravity = Gravity.BOTTOM
|
||||||
|
mView.setOnClickListener(View.OnClickListener {
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
|
||||||
|
mView.findViewById<ImageButton>(R.id.close_button)
|
||||||
|
.setOnClickListener {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
fun open(name: String = "AirPods Pro") {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mView.windowToken == null) {
|
||||||
|
if (mView.parent == null) {
|
||||||
|
// Add the view initially off-screen
|
||||||
|
mWindowManager.addView(mView, mParams)
|
||||||
|
mView.findViewById<TextView>(R.id.name).text = name
|
||||||
|
val vid = mView.findViewById<VideoView>(R.id.video)
|
||||||
|
vid.setVideoPath("android.resource://me.kavishdevar.aln/" + R.raw.connected)
|
||||||
|
vid.resolveAdjustedSize(vid.width, vid.height)
|
||||||
|
vid.start()
|
||||||
|
vid.setOnCompletionListener {
|
||||||
|
vid.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive battery broadcast and set to R.id.battery
|
||||||
|
val batteryText = mView.findViewById<TextView>(R.id.battery)
|
||||||
|
val batteryIntentFilter = IntentFilter(AirPodsNotifications.BATTERY_DATA)
|
||||||
|
mView.context.registerReceiver(object : BroadcastReceiver() {
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val batteryList = intent.getParcelableArrayListExtra("data", Battery::class.java)
|
||||||
|
batteryText.text = batteryList?.get(0)?.level.toString() + "%" + " " + batteryList?.get(0)?.status + " " + batteryList?.get(1)?.level.toString() + "%" + " " + batteryList?.get(1)?.status + " " + batteryList?.get(2)?.level.toString() + "%" + " " + batteryList?.get(2)?.status
|
||||||
|
}
|
||||||
|
}, batteryIntentFilter, Context.RECEIVER_NOT_EXPORTED)
|
||||||
|
|
||||||
|
|
||||||
|
// Slide-up animation
|
||||||
|
val displayMetrics = mView.context.resources.displayMetrics
|
||||||
|
val screenHeight = displayMetrics.heightPixels
|
||||||
|
|
||||||
|
mView.translationY = screenHeight.toFloat() // Start below the screen
|
||||||
|
ObjectAnimator.ofFloat(mView, "translationY", 0f).apply {
|
||||||
|
duration = 500 // Animation duration in milliseconds
|
||||||
|
interpolator = DecelerateInterpolator() // Smooth deceleration
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(MainScope().coroutineContext).launch {
|
||||||
|
delay(12000)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d("PopupService", e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
try {
|
||||||
|
ObjectAnimator.ofFloat(mView, "translationY", mView.height.toFloat()).apply {
|
||||||
|
duration = 500 // Animation duration in milliseconds
|
||||||
|
interpolator = AccelerateInterpolator() // Smooth acceleration
|
||||||
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
try {
|
||||||
|
mWindowManager.removeView(mView)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d("PopupService", e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d("PopupService", e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,6 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
primary = Purple40,
|
primary = Purple40,
|
||||||
secondary = PurpleGrey40,
|
secondary = PurpleGrey40,
|
||||||
tertiary = Pink40
|
tertiary = Pink40
|
||||||
|
|
||||||
/* Other default colors to override
|
/* Other default colors to override
|
||||||
background = Color(0xFFFFFBFE),
|
background = Color(0xFFFFFBFE),
|
||||||
surface = Color(0xFFFFFBFE),
|
surface = Color(0xFFFFFBFE),
|
||||||
|
|||||||
20
android/app/src/main/res/drawable/button_shape.xml
Normal file
20
android/app/src/main/res/drawable/button_shape.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid
|
||||||
|
android:color="#902E2E2E">
|
||||||
|
</solid>
|
||||||
|
|
||||||
|
<padding
|
||||||
|
android:left="1dp"
|
||||||
|
android:top="1dp"
|
||||||
|
android:right="1dp"
|
||||||
|
android:bottom="1dp" />
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:radius="5000dp">
|
||||||
|
</corners>
|
||||||
|
|
||||||
|
</shape>
|
||||||
9
android/app/src/main/res/drawable/close.xml
Normal file
9
android/app/src/main/res/drawable/close.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
|
||||||
|
</vector>
|
||||||
19
android/app/src/main/res/drawable/shape.xml
Normal file
19
android/app/src/main/res/drawable/shape.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle" >
|
||||||
|
|
||||||
|
<solid
|
||||||
|
android:color="#000000" >
|
||||||
|
</solid>
|
||||||
|
|
||||||
|
<padding
|
||||||
|
android:bottom="64dp"
|
||||||
|
android:top="16dp">
|
||||||
|
</padding>
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:radius="48dp">
|
||||||
|
</corners>
|
||||||
|
|
||||||
|
</shape>
|
||||||
63
android/app/src/main/res/layout/popup_window.xml
Normal file
63
android/app/src/main/res/layout/popup_window.xml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16.dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@drawable/shape">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="end"
|
||||||
|
android:paddingBottom="48dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:fontFamily="@font/sf_pro"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="Kavish's AirPods Pro"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
|
||||||
|
android:textSize="28sp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/close_button"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:background="@drawable/button_shape"
|
||||||
|
android:contentDescription="Close Button"
|
||||||
|
android:src="@drawable/close"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
<VideoView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/video"
|
||||||
|
android:contentDescription="AirPods"
|
||||||
|
android:src="@raw/connected"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/battery"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:fontFamily="@font/sf_pro"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text=""
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
</LinearLayout>
|
||||||
BIN
android/app/src/main/res/raw/connected.mp4
Normal file
BIN
android/app/src/main/res/raw/connected.mp4
Normal file
Binary file not shown.
BIN
android/app/src/main/res/raw/connected_uncropped.mp4
Normal file
BIN
android/app/src/main/res/raw/connected_uncropped.mp4
Normal file
Binary file not shown.
BIN
android/app/src/main/res/raw/first_uncropped.mp4
Normal file
BIN
android/app/src/main/res/raw/first_uncropped.mp4
Normal file
Binary file not shown.
BIN
android/app/src/main/res/raw/loop_uncropped.mp4
Normal file
BIN
android/app/src/main/res/raw/loop_uncropped.mp4
Normal file
Binary file not shown.
@@ -3,15 +3,16 @@ accompanistPermissions = "0.36.0"
|
|||||||
agp = "8.7.2"
|
agp = "8.7.2"
|
||||||
hiddenapibypass = "4.3"
|
hiddenapibypass = "4.3"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
coreKtx = "1.13.1"
|
coreKtx = "1.15.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
lifecycleRuntimeKtx = "2.8.6"
|
lifecycleRuntimeKtx = "2.8.7"
|
||||||
activityCompose = "1.9.2"
|
activityCompose = "1.9.3"
|
||||||
composeBom = "2024.09.03"
|
composeBom = "2024.11.00"
|
||||||
annotations = "26.0.0"
|
annotations = "26.0.0"
|
||||||
navigationCompose = "2.8.2"
|
navigationCompose = "2.8.4"
|
||||||
|
constraintlayout = "2.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||||
@@ -32,6 +33,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
|
|||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" }
|
annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" }
|
||||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||||
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven("https://jitpack.io")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user