mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-12 22:36:04 +00:00
add persistent notification for battery; bug fixes
This commit is contained in:
@@ -14,7 +14,7 @@ android {
|
|||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "0.0.2-beta"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.RemoteViews
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -36,12 +37,19 @@ class AirPodsService: Service() {
|
|||||||
return LocalBinder()
|
return LocalBinder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var popupShown = false
|
||||||
|
|
||||||
fun showPopup(service: Service, name: String) {
|
fun showPopup(service: Service, name: String) {
|
||||||
|
if (popupShown) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val window = Window(service.applicationContext)
|
val window = Window(service.applicationContext)
|
||||||
window.open(name, batteryNotification)
|
window.open(name, batteryNotification)
|
||||||
|
popupShown = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private object Receiver: BroadcastReceiver() {
|
@Suppress("ClassName")
|
||||||
|
private object bluetoothReceiver: BroadcastReceiver() {
|
||||||
@SuppressLint("NewApi", "MissingPermission")
|
@SuppressLint("NewApi", "MissingPermission")
|
||||||
override fun onReceive(context: Context?, intent: Intent) {
|
override fun onReceive(context: Context?, intent: Intent) {
|
||||||
val bluetoothDevice =
|
val bluetoothDevice =
|
||||||
@@ -50,11 +58,10 @@ class AirPodsService: Service() {
|
|||||||
val context = context?.applicationContext
|
val context = context?.applicationContext
|
||||||
val name = context?.getSharedPreferences("settings", MODE_PRIVATE)?.getString("name", bluetoothDevice?.name)
|
val name = context?.getSharedPreferences("settings", MODE_PRIVATE)?.getString("name", bluetoothDevice?.name)
|
||||||
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
|
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
|
||||||
Log.d("BluetoothReceiver", "Received broadcast")
|
Log.d("AirPodsService", "Received bluetooth connection broadcast")
|
||||||
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
|
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
|
||||||
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
if (bluetoothDevice.uuids.contains(uuid)) {
|
if (bluetoothDevice.uuids.contains(uuid)) {
|
||||||
Log.d("AirPodsService", "Service started")
|
|
||||||
val intent = Intent(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
|
val intent = Intent(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
|
||||||
intent.putExtra("name", name)
|
intent.putExtra("name", name)
|
||||||
intent.putExtra("device", bluetoothDevice)
|
intent.putExtra("device", bluetoothDevice)
|
||||||
@@ -66,7 +73,6 @@ class AirPodsService: Service() {
|
|||||||
if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action
|
if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action
|
||||||
|| BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action
|
|| BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action
|
||||||
) {
|
) {
|
||||||
Log.d("AirPodsService", "Closed Socket")
|
|
||||||
context?.sendBroadcast(
|
context?.sendBroadcast(
|
||||||
Intent(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
Intent(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
||||||
)
|
)
|
||||||
@@ -78,50 +84,135 @@ class AirPodsService: Service() {
|
|||||||
var isConnected = false
|
var isConnected = false
|
||||||
var device: BluetoothDevice? = null
|
var device: BluetoothDevice? = null
|
||||||
|
|
||||||
|
private lateinit var earReceiver: BroadcastReceiver
|
||||||
|
|
||||||
fun startForegroundNotification() {
|
fun startForegroundNotification() {
|
||||||
val notificationChannel = NotificationChannel(
|
val notificationChannel = NotificationChannel(
|
||||||
"airpods",
|
"background_service_status",
|
||||||
"AirPods",
|
"Background Service Status",
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_LOW
|
||||||
)
|
)
|
||||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||||
notificationManager.createNotificationChannel(notificationChannel)
|
notificationManager.createNotificationChannel(notificationChannel)
|
||||||
|
val notification = NotificationCompat.Builder(this, "background_service_status")
|
||||||
val notification = NotificationCompat.Builder(this, "airpods")
|
.setSmallIcon(R.drawable.airpods)
|
||||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
.setContentTitle("AirPods Service")
|
||||||
.setContentTitle("AirPods Service Running")
|
.setContentText("Service is running in the background")
|
||||||
.setContentText("AirPods service is running in the background.")
|
|
||||||
.setCategory(Notification.CATEGORY_SERVICE)
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
startForeground(2, notification)
|
try {
|
||||||
|
startForeground(1, notification)
|
||||||
|
}
|
||||||
|
catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateNotificationContent(connected: Boolean, airpodsName: String? = null, batteryList: List<Battery>? = null) {
|
||||||
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||||
|
val textColor = this.getSharedPreferences("settings", MODE_PRIVATE).getLong("textColor", 0)
|
||||||
|
var updatedNotification: Notification? = null
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
val collapsedRemoteViews = RemoteViews(packageName, R.layout.notification)
|
||||||
|
val expandedRemoteViews = RemoteViews(packageName, R.layout.notification_expanded)
|
||||||
|
collapsedRemoteViews.setTextColor(R.id.notification_title, textColor.toInt())
|
||||||
|
|
||||||
|
collapsedRemoteViews.setTextViewText(R.id.notification_title, "Connected to $airpodsName")
|
||||||
|
expandedRemoteViews.setTextViewText(
|
||||||
|
R.id.notification_title,
|
||||||
|
"Connected to $airpodsName"
|
||||||
|
)
|
||||||
|
expandedRemoteViews.setTextViewText(
|
||||||
|
R.id.left_battery_notification,
|
||||||
|
batteryList?.find { it.component == BatteryComponent.LEFT }?.let {
|
||||||
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
|
"Left ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
} ?: "")
|
||||||
|
expandedRemoteViews.setTextViewText(
|
||||||
|
R.id.right_battery_notification,
|
||||||
|
batteryList?.find { it.component == BatteryComponent.RIGHT }?.let {
|
||||||
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
|
"Right ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
} ?: "")
|
||||||
|
expandedRemoteViews.setTextViewText(
|
||||||
|
R.id.case_battery_notification,
|
||||||
|
batteryList?.find { it.component == BatteryComponent.CASE }?.let {
|
||||||
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
|
"Case ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
} ?: "")
|
||||||
|
expandedRemoteViews.setTextColor(R.id.notification_title, textColor.toInt())
|
||||||
|
expandedRemoteViews.setTextColor(R.id.left_battery_notification, textColor.toInt())
|
||||||
|
expandedRemoteViews.setTextColor(R.id.right_battery_notification, textColor.toInt())
|
||||||
|
expandedRemoteViews.setTextColor(R.id.case_battery_notification, textColor.toInt())
|
||||||
|
updatedNotification = NotificationCompat.Builder(this, "background_service_status")
|
||||||
|
.setSmallIcon(R.drawable.airpods)
|
||||||
|
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
||||||
|
.setCustomContentView(collapsedRemoteViews)
|
||||||
|
.setCustomBigContentView(expandedRemoteViews)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
updatedNotification = NotificationCompat.Builder(this, "background_service_status")
|
||||||
|
.setSmallIcon(R.drawable.airpods)
|
||||||
|
.setContentTitle("AirPods Service")
|
||||||
|
.setContentText("Service is running in the background!")
|
||||||
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the NotificationManager with the same ID
|
||||||
|
notificationManager.notify(1, updatedNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var connectionReceiver: BroadcastReceiver
|
||||||
|
private lateinit var disconnectionReceiver: BroadcastReceiver
|
||||||
|
|
||||||
@SuppressLint("InlinedApi", "MissingPermission")
|
@SuppressLint("InlinedApi", "MissingPermission")
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.d("AirPodsService", "Service started")
|
Log.d("AirPodsService", "Service started")
|
||||||
startForegroundNotification()
|
startForegroundNotification()
|
||||||
registerReceiver(Receiver, BluetoothReceiver.buildFilter(), RECEIVER_EXPORTED)
|
registerReceiver(bluetoothReceiver, BluetoothReceiver.buildFilter(), RECEIVER_EXPORTED)
|
||||||
|
|
||||||
registerReceiver(object: BroadcastReceiver() {
|
connectionReceiver = object: BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
val name = this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE).getString("name", device?.name)
|
if (intent?.action == AirPodsNotifications.AIRPODS_CONNECTION_DETECTED) {
|
||||||
device = intent?.getParcelableExtra("device", BluetoothDevice::class.java)!!
|
val name = this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
showPopup(this@AirPodsService, name.toString())
|
.getString("name", device?.name)
|
||||||
connectToSocket(device!!)
|
Log.d("AirPodsService", "$name connected")
|
||||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_CONNECTED).apply {
|
device = intent.getParcelableExtra("device", BluetoothDevice::class.java)!!
|
||||||
putExtra("device", device)
|
showPopup(this@AirPodsService, name.toString())
|
||||||
})
|
connectToSocket(device!!)
|
||||||
|
updateNotificationContent(true, name.toString(), batteryNotification.getBattery())
|
||||||
|
}
|
||||||
|
else if (intent?.action == AirPodsNotifications.AIRPODS_DISCONNECTED) {
|
||||||
|
device = null
|
||||||
|
isConnected = false
|
||||||
|
popupShown = false
|
||||||
|
updateNotificationContent(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, IntentFilter(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED), RECEIVER_EXPORTED)
|
}
|
||||||
|
|
||||||
registerReceiver(object: BroadcastReceiver() {
|
val intentFilter = IntentFilter().apply {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
addAction(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
|
||||||
device = null
|
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
||||||
isConnected = false
|
}
|
||||||
}
|
registerReceiver(connectionReceiver, intentFilter, RECEIVER_EXPORTED)
|
||||||
}, IntentFilter(AirPodsNotifications.AIRPODS_DISCONNECTED), RECEIVER_EXPORTED)
|
|
||||||
|
|
||||||
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
|
||||||
bluetoothAdapter.bondedDevices.forEach { device ->
|
bluetoothAdapter.bondedDevices.forEach { device ->
|
||||||
@@ -200,9 +291,8 @@ class AirPodsService: Service() {
|
|||||||
delay(500)
|
delay(500)
|
||||||
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
|
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
|
||||||
it.outputStream.flush()
|
it.outputStream.flush()
|
||||||
Log.d("AirPodsService","This should run first")
|
|
||||||
}
|
}
|
||||||
Log.d("AirPodsService","This should run later")
|
|
||||||
sendBroadcast(
|
sendBroadcast(
|
||||||
Intent(AirPodsNotifications.AIRPODS_CONNECTED)
|
Intent(AirPodsNotifications.AIRPODS_CONNECTED)
|
||||||
.putExtra("device", device)
|
.putExtra("device", device)
|
||||||
@@ -243,7 +333,7 @@ class AirPodsService: Service() {
|
|||||||
})
|
})
|
||||||
Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}")
|
Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}")
|
||||||
var justEnabledA2dp = false
|
var justEnabledA2dp = false
|
||||||
val earReceiver = object : BroadcastReceiver() {
|
earReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val data = intent.getByteArrayExtra("data")
|
val data = intent.getByteArrayExtra("data")
|
||||||
if (data != null && earDetectionEnabled) {
|
if (data != null && earDetectionEnabled) {
|
||||||
@@ -325,17 +415,16 @@ class AirPodsService: Service() {
|
|||||||
sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
||||||
putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery()))
|
putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery()))
|
||||||
})
|
})
|
||||||
|
updateNotificationContent(true, this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE).getString("name", device.name), batteryNotification.getBattery())
|
||||||
for (battery in batteryNotification.getBattery()) {
|
for (battery in batteryNotification.getBattery()) {
|
||||||
Log.d("AirPods Parser", "${battery.getComponentName()}: ${battery.getStatusName()} at ${battery.level}% ")
|
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) {
|
if (batteryNotification.getBattery()[0].status == 1 && batteryNotification.getBattery()[1].status == 1) {
|
||||||
disconnectAudio(this@AirPodsService, device)
|
disconnectAudio(this@AirPodsService, device)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
connectAudio(this@AirPodsService, device)
|
connectAudio(this@AirPodsService, device)
|
||||||
}
|
}
|
||||||
// updatePodsStatus(device!!, batteryNotification.getBattery())
|
|
||||||
}
|
}
|
||||||
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
|
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
|
||||||
conversationAwarenessNotification.setData(data)
|
conversationAwarenessNotification.setData(data)
|
||||||
@@ -357,7 +446,6 @@ class AirPodsService: Service() {
|
|||||||
}
|
}
|
||||||
Log.d("AirPods Service", "Socket closed")
|
Log.d("AirPods Service", "Socket closed")
|
||||||
isConnected = false
|
isConnected = false
|
||||||
this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
|
|
||||||
socket.close()
|
socket.close()
|
||||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
||||||
}
|
}
|
||||||
@@ -471,22 +559,6 @@ class AirPodsService: Service() {
|
|||||||
return ancNotification.status
|
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?) {
|
fun disconnectAudio(context: Context, device: BluetoothDevice?) {
|
||||||
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
@@ -590,4 +662,29 @@ class AirPodsService: Service() {
|
|||||||
socket.outputStream?.write(bytes)
|
socket.outputStream?.write(bytes)
|
||||||
socket.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Log.d("AirPodsService", "Service stopped is being destroyed for some reason!")
|
||||||
|
try {
|
||||||
|
unregisterReceiver(bluetoothReceiver)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unregisterReceiver(connectionReceiver)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unregisterReceiver(disconnectionReceiver)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unregisterReceiver(earReceiver)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,6 @@ 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.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
|
||||||
@@ -61,9 +60,11 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
@@ -83,11 +84,17 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.primex.core.ExperimentalToolkitApi
|
import com.primex.core.ExperimentalToolkitApi
|
||||||
import com.primex.core.blur.newBackgroundBlur
|
import com.primex.core.blur.newBackgroundBlur
|
||||||
import me.kavishdevar.aln.AirPodsService
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|
||||||
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryView() {
|
fun BatteryViewPreview() {
|
||||||
|
BatteryView(AirPodsService(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
||||||
val batteryStatus = remember { mutableStateOf<List<Battery>>(listOf()) }
|
val batteryStatus = remember { mutableStateOf<List<Battery>>(listOf()) }
|
||||||
@Suppress("DEPRECATION") val batteryReceiver = remember {
|
@Suppress("DEPRECATION") val batteryReceiver = remember {
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
@@ -114,6 +121,16 @@ fun BatteryView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
batteryStatus.value = service.getBattery()
|
||||||
|
|
||||||
|
if (preview) {
|
||||||
|
batteryStatus.value = listOf<Battery>(
|
||||||
|
Battery(BatteryComponent.LEFT, 100, BatteryStatus.CHARGING),
|
||||||
|
Battery(BatteryComponent.RIGHT, 50, BatteryStatus.NOT_CHARGING),
|
||||||
|
Battery(BatteryComponent.CASE, 5, BatteryStatus.CHARGING)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
Column (
|
Column (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -125,7 +142,7 @@ fun BatteryView() {
|
|||||||
contentDescription = "Buds",
|
contentDescription = "Buds",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.scale(0.50f)
|
.scale(0.80f)
|
||||||
)
|
)
|
||||||
val left = batteryStatus.value.find { it.component == BatteryComponent.LEFT }
|
val left = batteryStatus.value.find { it.component == BatteryComponent.LEFT }
|
||||||
val right = batteryStatus.value.find { it.component == BatteryComponent.RIGHT }
|
val right = batteryStatus.value.find { it.component == BatteryComponent.RIGHT }
|
||||||
@@ -134,15 +151,44 @@ fun BatteryView() {
|
|||||||
BatteryIndicator(right.level.let { left.level.coerceAtMost(it) }, left.status == BatteryStatus.CHARGING)
|
BatteryIndicator(right.level.let { left.level.coerceAtMost(it) }, left.status == BatteryStatus.CHARGING)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Row {
|
Row (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
if (left?.status != BatteryStatus.DISCONNECTED) {
|
if (left?.status != BatteryStatus.DISCONNECTED) {
|
||||||
Text(text = "\uDBC6\uDCE5", fontFamily = FontFamily(Font(R.font.sf_pro)))
|
Row (
|
||||||
BatteryIndicator(left?.level ?: 0, left?.status == BatteryStatus.CHARGING)
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "\uDBC6\uDCE5",
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
)
|
||||||
|
BatteryIndicator(
|
||||||
|
left?.level ?: 0,
|
||||||
|
left?.status == BatteryStatus.CHARGING
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (right?.status != BatteryStatus.DISCONNECTED) {
|
if (right?.status != BatteryStatus.DISCONNECTED) {
|
||||||
Text(text = "\uDBC6\uDCE8", fontFamily = FontFamily(Font(R.font.sf_pro)))
|
Row (
|
||||||
BatteryIndicator(right?.level ?: 0, right?.status == BatteryStatus.CHARGING)
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "\uDBC6\uDCE8",
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.5f)
|
||||||
|
)
|
||||||
|
BatteryIndicator(
|
||||||
|
right?.level ?: 0,
|
||||||
|
right?.status == BatteryStatus.CHARGING
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,6 +206,7 @@ fun BatteryView() {
|
|||||||
contentDescription = "Case",
|
contentDescription = "Case",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.scale(1.25f)
|
||||||
)
|
)
|
||||||
BatteryIndicator(case?.level ?: 0)
|
BatteryIndicator(case?.level ?: 0)
|
||||||
}
|
}
|
||||||
@@ -210,12 +257,11 @@ fun ToneVolumeSlider(service: AirPodsService, sharedPreferences: SharedPreferenc
|
|||||||
},
|
},
|
||||||
valueRange = 0f..100f,
|
valueRange = 0f..100f,
|
||||||
onValueChangeFinished = {
|
onValueChangeFinished = {
|
||||||
// Round the value when the user stops sliding
|
|
||||||
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.height(36.dp), // Adjust height to ensure thumb fits well
|
.height(36.dp),
|
||||||
colors = SliderDefaults.colors(
|
colors = SliderDefaults.colors(
|
||||||
thumbColor = thumbColor,
|
thumbColor = thumbColor,
|
||||||
activeTrackColor = activeTrackColor,
|
activeTrackColor = activeTrackColor,
|
||||||
@@ -224,9 +270,9 @@ fun ToneVolumeSlider(service: AirPodsService, sharedPreferences: SharedPreferenc
|
|||||||
thumb = {
|
thumb = {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp) // Circular thumb size
|
.size(24.dp)
|
||||||
.shadow(4.dp, CircleShape) // Apply shadow to the thumb
|
.shadow(4.dp, CircleShape)
|
||||||
.background(thumbColor, CircleShape) // Circular thumb
|
.background(thumbColor, CircleShape)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
track = {
|
track = {
|
||||||
@@ -245,7 +291,7 @@ fun ToneVolumeSlider(service: AirPodsService, sharedPreferences: SharedPreferenc
|
|||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(sliderValue.value / 100)
|
.fillMaxWidth(sliderValue.floatValue / 100)
|
||||||
.height(4.dp)
|
.height(4.dp)
|
||||||
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
||||||
)
|
)
|
||||||
@@ -273,7 +319,6 @@ fun SinglePodANCSwitch(service: AirPodsService, sharedPreferences: SharedPrefere
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the service when the toggle is changed
|
|
||||||
fun updateSingleEnabled(enabled: Boolean) {
|
fun updateSingleEnabled(enabled: Boolean) {
|
||||||
singleANCEnabled = enabled
|
singleANCEnabled = enabled
|
||||||
sharedPreferences.edit().putBoolean("single_anc", enabled).apply()
|
sharedPreferences.edit().putBoolean("single_anc", enabled).apply()
|
||||||
@@ -293,20 +338,19 @@ fun SinglePodANCSwitch(service: AirPodsService, sharedPreferences: SharedPrefere
|
|||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
)
|
)
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
.pointerInput(Unit) { // Detect press state for iOS-like effect
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = {
|
onPress = {
|
||||||
isPressed.value = true
|
isPressed.value = true
|
||||||
tryAwaitRelease() // Wait until release
|
tryAwaitRelease()
|
||||||
isPressed.value = false
|
isPressed.value = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null, // Disable ripple effect
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() } // Required for clickable
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
// Toggle the conversational awareness value
|
|
||||||
updateSingleEnabled(!singleANCEnabled)
|
updateSingleEnabled(!singleANCEnabled)
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -321,7 +365,7 @@ fun SinglePodANCSwitch(service: AirPodsService, sharedPreferences: SharedPrefere
|
|||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.",
|
text = "Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
@@ -346,7 +390,6 @@ fun VolumeControlSwitch(service: AirPodsService, sharedPreferences: SharedPrefer
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the service when the toggle is changed
|
|
||||||
fun updateVolumeControlEnabled(enabled: Boolean) {
|
fun updateVolumeControlEnabled(enabled: Boolean) {
|
||||||
volumeControlEnabled = enabled
|
volumeControlEnabled = enabled
|
||||||
sharedPreferences.edit().putBoolean("volume_control", enabled).apply()
|
sharedPreferences.edit().putBoolean("volume_control", enabled).apply()
|
||||||
@@ -366,20 +409,19 @@ fun VolumeControlSwitch(service: AirPodsService, sharedPreferences: SharedPrefer
|
|||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
)
|
)
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
.pointerInput(Unit) { // Detect press state for iOS-like effect
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = {
|
onPress = {
|
||||||
isPressed.value = true
|
isPressed.value = true
|
||||||
tryAwaitRelease() // Wait until release
|
tryAwaitRelease()
|
||||||
isPressed.value = false
|
isPressed.value = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null, // Disable ripple effect
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() } // Required for clickable
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
// Toggle the conversational awareness value
|
|
||||||
updateVolumeControlEnabled(!volumeControlEnabled)
|
updateVolumeControlEnabled(!volumeControlEnabled)
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -394,7 +436,7 @@ fun VolumeControlSwitch(service: AirPodsService, sharedPreferences: SharedPrefer
|
|||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.",
|
text = "Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
@@ -434,9 +476,7 @@ 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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -476,11 +516,10 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalToolkitApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalToolkitApi::class)
|
||||||
@SuppressLint("MissingPermission", "NewApi")
|
@SuppressLint("MissingPermission", "NewApi")
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreen(device: BluetoothDevice?, service: AirPodsService?,
|
fun AirPodsSettingsScreen(device: BluetoothDevice?, service: AirPodsService,
|
||||||
navController: NavController) {
|
navController: NavController, isConnected: Boolean) {
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", 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
|
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -490,6 +529,7 @@ fun AirPodsSettingsScreen(device: BluetoothDevice?, service: AirPodsService?,
|
|||||||
0xFFF2F2F7
|
0xFFF2F2F7
|
||||||
),
|
),
|
||||||
topBar = {
|
topBar = {
|
||||||
|
val darkMode = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
@@ -499,43 +539,56 @@ fun AirPodsSettingsScreen(device: BluetoothDevice?, service: AirPodsService?,
|
|||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.newBackgroundBlur(
|
.newBackgroundBlur(
|
||||||
radius = 24.dp, // the radius of the blur effect, in pixels)
|
radius = 24.dp,
|
||||||
),
|
downsample = 0.5f,
|
||||||
|
)
|
||||||
|
.drawBehind {
|
||||||
|
val strokeWidth = 0.7.dp.value * density
|
||||||
|
val y = size.height - strokeWidth / 2
|
||||||
|
if (verticalScrollState.value > 55.dp.value * density) {
|
||||||
|
drawLine(
|
||||||
|
if (darkMode) Color.DarkGray else Color.LightGray,
|
||||||
|
Offset(0f, y),
|
||||||
|
Offset(size.width, y),
|
||||||
|
strokeWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.Black.copy(0.2f) else Color(0xFFF2F2F7).copy(0.2f),
|
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.Black.copy(0.3f) else Color(0xFFF2F2F7).copy(0.2f),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
HorizontalDivider(thickness = 3.dp, color = Color.DarkGray)
|
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
if (isConnected == true) {
|
||||||
modifier = Modifier
|
Column(
|
||||||
.fillMaxSize()
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp)
|
.fillMaxSize()
|
||||||
// .padding(top = 55.dp, bottom = 32.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.verticalScroll(
|
.verticalScroll(
|
||||||
state = verticalScrollState,
|
state = verticalScrollState,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.height(75.dp))
|
Spacer(Modifier.height(75.dp))
|
||||||
LaunchedEffect(service) {
|
LaunchedEffect(service) {
|
||||||
service?.let {
|
service.let {
|
||||||
it.sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
it.sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
||||||
putParcelableArrayListExtra("data", ArrayList(it.getBattery()))
|
putParcelableArrayListExtra("data", ArrayList(it.getBattery()))
|
||||||
})
|
})
|
||||||
it.sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
it.sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
||||||
putExtra("data", it.getANC())
|
putExtra("data", it.getANC())
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
val sharedPreferences =
|
|
||||||
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
if (service != null) {
|
Spacer(modifier = Modifier.height(64.dp))
|
||||||
BatteryView()
|
|
||||||
|
BatteryView(service = service)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
StyledTextField(
|
StyledTextField(
|
||||||
name = "Name",
|
name = "Name",
|
||||||
value = deviceName.text,
|
value = deviceName.text,
|
||||||
@@ -627,8 +680,43 @@ fun AirPodsSettingsScreen(device: BluetoothDevice?, service: AirPodsService?,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.verticalScroll(
|
||||||
|
state = verticalScrollState,
|
||||||
|
enabled = true,
|
||||||
|
),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "AirPods not connected",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = "Please connect your AirPods to access settings. If you're stuck here, then try reopening the app again after closing it from the recents.\n(DO NOT KILL THE APP!)",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(24.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -658,7 +746,6 @@ fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPrefere
|
|||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// Slider
|
|
||||||
Slider(
|
Slider(
|
||||||
value = sliderValue.floatValue,
|
value = sliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
@@ -667,12 +754,11 @@ fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPrefere
|
|||||||
},
|
},
|
||||||
valueRange = 0f..100f,
|
valueRange = 0f..100f,
|
||||||
onValueChangeFinished = {
|
onValueChangeFinished = {
|
||||||
// Round the value when the user stops sliding
|
|
||||||
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(36.dp), // Adjust height to ensure thumb fits well
|
.height(36.dp),
|
||||||
colors = SliderDefaults.colors(
|
colors = SliderDefaults.colors(
|
||||||
thumbColor = thumbColor,
|
thumbColor = thumbColor,
|
||||||
inactiveTrackColor = trackColor
|
inactiveTrackColor = trackColor
|
||||||
@@ -680,9 +766,9 @@ fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPrefere
|
|||||||
thumb = {
|
thumb = {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp) // Circular thumb size
|
.size(24.dp)
|
||||||
.shadow(4.dp, CircleShape) // Apply shadow to the thumb
|
.shadow(4.dp, CircleShape)
|
||||||
.background(thumbColor, CircleShape) // Circular thumb
|
.background(thumbColor, CircleShape)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
track = {
|
track = {
|
||||||
@@ -703,8 +789,7 @@ fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPrefere
|
|||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Labels
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
@@ -742,13 +827,9 @@ fun IndependentToggle(name: String, service: AirPodsService, functionName: Strin
|
|||||||
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
|
||||||
|
|
||||||
// Standardize the key
|
|
||||||
val snakeCasedName = name.replace(Regex("[\\W\\s]+"), "_").lowercase()
|
val snakeCasedName = name.replace(Regex("[\\W\\s]+"), "_").lowercase()
|
||||||
|
|
||||||
// State for the toggle
|
|
||||||
var checked by remember { mutableStateOf(default) }
|
var checked by remember { mutableStateOf(default) }
|
||||||
|
|
||||||
// Load initial state from SharedPreferences
|
|
||||||
LaunchedEffect(sharedPreferences) {
|
LaunchedEffect(sharedPreferences) {
|
||||||
checked = sharedPreferences.getBoolean(snakeCasedName, true)
|
checked = sharedPreferences.getBoolean(snakeCasedName, true)
|
||||||
}
|
}
|
||||||
@@ -767,14 +848,12 @@ fun IndependentToggle(name: String, service: AirPodsService, functionName: Strin
|
|||||||
.height(55.dp)
|
.height(55.dp)
|
||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = 12.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
// Toggle checked state and save to SharedPreferences
|
|
||||||
checked = !checked
|
checked = !checked
|
||||||
sharedPreferences
|
sharedPreferences
|
||||||
.edit()
|
.edit()
|
||||||
.putBoolean(snakeCasedName, checked)
|
.putBoolean(snakeCasedName, checked)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
// Call the corresponding method in the service
|
|
||||||
val method = service::class.java.getMethod(functionName, Boolean::class.java)
|
val method = service::class.java.getMethod(functionName, Boolean::class.java)
|
||||||
method.invoke(service, checked)
|
method.invoke(service, checked)
|
||||||
},
|
},
|
||||||
@@ -786,8 +865,6 @@ fun IndependentToggle(name: String, service: AirPodsService, functionName: Strin
|
|||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
checked = it
|
checked = it
|
||||||
sharedPreferences.edit().putBoolean(snakeCasedName, it).apply()
|
sharedPreferences.edit().putBoolean(snakeCasedName, it).apply()
|
||||||
|
|
||||||
// Call the corresponding method in the service
|
|
||||||
val method = service::class.java.getMethod(functionName, Boolean::class.java)
|
val method = service::class.java.getMethod(functionName, Boolean::class.java)
|
||||||
method.invoke(service, it)
|
method.invoke(service, it)
|
||||||
},
|
},
|
||||||
@@ -804,7 +881,6 @@ fun ConversationalAwarenessSwitch(service: AirPodsService, sharedPreferences: Sh
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the service when the toggle is changed
|
|
||||||
fun updateConversationalAwareness(enabled: Boolean) {
|
fun updateConversationalAwareness(enabled: Boolean) {
|
||||||
conversationalAwarenessEnabled = enabled
|
conversationalAwarenessEnabled = enabled
|
||||||
sharedPreferences.edit().putBoolean("conversational_awareness", enabled).apply()
|
sharedPreferences.edit().putBoolean("conversational_awareness", enabled).apply()
|
||||||
@@ -824,20 +900,19 @@ fun ConversationalAwarenessSwitch(service: AirPodsService, sharedPreferences: Sh
|
|||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
)
|
)
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
.pointerInput(Unit) { // Detect press state for iOS-like effect
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = {
|
onPress = {
|
||||||
isPressed.value = true
|
isPressed.value = true
|
||||||
tryAwaitRelease() // Wait until release
|
tryAwaitRelease()
|
||||||
isPressed.value = false
|
isPressed.value = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null, // Disable ripple effect
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() } // Required for clickable
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
// Toggle the conversational awareness value
|
|
||||||
updateConversationalAwareness(!conversationalAwarenessEnabled)
|
updateConversationalAwareness(!conversationalAwarenessEnabled)
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -852,7 +927,7 @@ fun ConversationalAwarenessSwitch(service: AirPodsService, sharedPreferences: Sh
|
|||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Lowers media volume and reduces background noise when you start speaking to other people.",
|
text = "Lowers media volume and reduces background noise when you start speaking to other people.",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
@@ -877,7 +952,6 @@ fun PersonalizedVolumeSwitch(service: AirPodsService, sharedPreferences: SharedP
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the service when the toggle is changed
|
|
||||||
fun updatePersonalizedVolume(enabled: Boolean) {
|
fun updatePersonalizedVolume(enabled: Boolean) {
|
||||||
personalizedVolumeEnabled = enabled
|
personalizedVolumeEnabled = enabled
|
||||||
sharedPreferences.edit().putBoolean("personalized_volume", enabled).apply()
|
sharedPreferences.edit().putBoolean("personalized_volume", enabled).apply()
|
||||||
@@ -897,20 +971,19 @@ fun PersonalizedVolumeSwitch(service: AirPodsService, sharedPreferences: SharedP
|
|||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
)
|
)
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
.pointerInput(Unit) { // Detect press state for iOS-like effect
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = {
|
onPress = {
|
||||||
isPressed.value = true
|
isPressed.value = true
|
||||||
tryAwaitRelease() // Wait until release
|
tryAwaitRelease()
|
||||||
isPressed.value = false
|
isPressed.value = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null, // Disable ripple effect
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() } // Required for clickable
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
// Toggle the conversational awareness value
|
|
||||||
updatePersonalizedVolume(!personalizedVolumeEnabled)
|
updatePersonalizedVolume(!personalizedVolumeEnabled)
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -925,7 +998,7 @@ fun PersonalizedVolumeSwitch(service: AirPodsService, sharedPreferences: SharedP
|
|||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Adjusts the volume of media in response to your environment.",
|
text = "Adjusts the volume of media in response to your environment.",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
@@ -951,7 +1024,6 @@ fun LoudSoundReductionSwitch(service: AirPodsService, sharedPreferences: SharedP
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the service when the toggle is changed
|
|
||||||
fun updateLoudSoundReduction(enabled: Boolean) {
|
fun updateLoudSoundReduction(enabled: Boolean) {
|
||||||
loudSoundReductionEnabled = enabled
|
loudSoundReductionEnabled = enabled
|
||||||
sharedPreferences.edit().putBoolean("loud_sound_reduction", enabled).apply()
|
sharedPreferences.edit().putBoolean("loud_sound_reduction", enabled).apply()
|
||||||
@@ -971,20 +1043,19 @@ fun LoudSoundReductionSwitch(service: AirPodsService, sharedPreferences: SharedP
|
|||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
)
|
)
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
.pointerInput(Unit) { // Detect press state for iOS-like effect
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = {
|
onPress = {
|
||||||
isPressed.value = true
|
isPressed.value = true
|
||||||
tryAwaitRelease() // Wait until release
|
tryAwaitRelease()
|
||||||
isPressed.value = false
|
isPressed.value = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null, // Disable ripple effect
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() } // Required for clickable
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
// Toggle the conversational awareness value
|
|
||||||
updateLoudSoundReduction(!loudSoundReductionEnabled)
|
updateLoudSoundReduction(!loudSoundReductionEnabled)
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -999,7 +1070,7 @@ fun LoudSoundReductionSwitch(service: AirPodsService, sharedPreferences: SharedP
|
|||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Reduces loud sounds you are exposed to.",
|
text = "Reduces loud sounds you are exposed to.",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
@@ -1393,14 +1464,15 @@ fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) {
|
|||||||
val batteryTextColor = MaterialTheme.colorScheme.onSurface
|
val batteryTextColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
// Battery indicator dimensions
|
// Battery indicator dimensions
|
||||||
val batteryWidth = 30.dp
|
val batteryWidth = 40.dp
|
||||||
val batteryHeight = 15.dp
|
val batteryHeight = 15.dp
|
||||||
val batteryCornerRadius = 4.dp
|
val batteryCornerRadius = 4.dp
|
||||||
val tipWidth = 5.dp
|
val tipWidth = 4.dp
|
||||||
val tipHeight = batteryHeight * 0.3f
|
val tipHeight = batteryHeight * 0.3f
|
||||||
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(
|
||||||
// Row for battery icon and tip
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
@@ -1423,21 +1495,23 @@ fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) {
|
|||||||
if (charging) {
|
if (charging) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(), // Take up the entire size of the outer Box
|
.padding(0.dp)
|
||||||
contentAlignment = Alignment.Center // Center the charging bolt within the Box
|
.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "\uDBC0\uDEE6",
|
text = "\uDBC0\uDEE6",
|
||||||
fontSize = 12.sp,
|
fontSize = 15.sp,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(0.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Battery Tip (Protrusion)
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(tipWidth)
|
.width(tipWidth)
|
||||||
@@ -1455,7 +1529,6 @@ fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Battery Percentage Text
|
|
||||||
Text(
|
Text(
|
||||||
text = "$batteryPercentage%",
|
text = "$batteryPercentage%",
|
||||||
color = batteryTextColor,
|
color = batteryTextColor,
|
||||||
|
|||||||
@@ -20,23 +20,25 @@ import androidx.compose.foundation.layout.Column
|
|||||||
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.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
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
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
|
||||||
import com.google.accompanist.permissions.shouldShowRationale
|
|
||||||
import com.primex.core.ExperimentalToolkitApi
|
import com.primex.core.ExperimentalToolkitApi
|
||||||
import me.kavishdevar.aln.ui.theme.ALNTheme
|
import me.kavishdevar.aln.ui.theme.ALNTheme
|
||||||
|
|
||||||
|
lateinit var serviceConnection: ServiceConnection
|
||||||
|
lateinit var connectionStatusReceiver: BroadcastReceiver
|
||||||
|
|
||||||
@ExperimentalMaterial3Api
|
@ExperimentalMaterial3Api
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -46,72 +48,94 @@ class MainActivity : ComponentActivity() {
|
|||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
ALNTheme {
|
ALNTheme {
|
||||||
|
getSharedPreferences("settings", MODE_PRIVATE).edit().putLong("textColor",
|
||||||
|
MaterialTheme.colorScheme.onSurface.toArgb().toLong()).apply()
|
||||||
Main()
|
Main()
|
||||||
startService(Intent(this, AirPodsService::class.java))
|
startService(Intent(this, AirPodsService::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun onDestroy() {
|
||||||
|
try {
|
||||||
|
unbindService(serviceConnection)
|
||||||
|
Log.d("MainActivity", "Unbound service")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("MainActivity", "Error while unbinding service: $e")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unregisterReceiver(connectionStatusReceiver)
|
||||||
|
Log.d("MainActivity", "Unregistered receiver")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("MainActivity", "Error while unregistering receiver: $e")
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission", "InlinedApi")
|
@SuppressLint("MissingPermission", "InlinedApi")
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Main() {
|
fun Main() {
|
||||||
val bluetoothConnectPermissionState = rememberPermissionState(
|
val isConnected = remember { mutableStateOf(false) }
|
||||||
permission = "android.permission.BLUETOOTH_CONNECT"
|
val permissionState = rememberMultiplePermissionsState(
|
||||||
|
permissions = listOf(
|
||||||
|
"android.permission.BLUETOOTH_CONNECT",
|
||||||
|
"android.permission.POST_NOTIFICATIONS"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (bluetoothConnectPermissionState.status.isGranted) {
|
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
||||||
|
if (permissionState.allPermissionsGranted) {
|
||||||
|
Log.d("MainActivity", "HIIIIIIIIIIIIIII")
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
connectionStatusReceiver = object : BroadcastReceiver() {
|
||||||
val disconnectReceiver = object : BroadcastReceiver() {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
if (intent.action == AirPodsNotifications.AIRPODS_CONNECTED) {
|
||||||
Log.d("MainActivity", "Received DISCONNECTED broadcast")
|
Log.d("MainActivity", "AirPods Connected intent received")
|
||||||
navController.navigate("notConnected")
|
isConnected.value = true
|
||||||
|
}
|
||||||
|
else if (intent.action == AirPodsNotifications.AIRPODS_DISCONNECTED) {
|
||||||
|
Log.d("MainActivity", "AirPods Disconnected intent received")
|
||||||
|
isConnected.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context.registerReceiver(disconnectReceiver, IntentFilter(AirPodsNotifications.AIRPODS_DISCONNECTED),
|
val filter = IntentFilter().apply {
|
||||||
Context.RECEIVER_NOT_EXPORTED)
|
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
|
||||||
|
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
||||||
|
addAction(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
|
||||||
|
}
|
||||||
|
Log.d("MainActivity", "Registering Receiver")
|
||||||
|
context.registerReceiver(connectionStatusReceiver, filter, Context.RECEIVER_EXPORTED)
|
||||||
|
Log.d("MainActivity", "Registered Receiver")
|
||||||
|
|
||||||
// UI logic
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = "notConnected",
|
startDestination = "settings",
|
||||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) },
|
enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) },
|
||||||
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) },
|
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) },
|
||||||
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) },
|
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) },
|
||||||
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(300)) }
|
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(300)) }
|
||||||
) {
|
) {
|
||||||
composable("notConnected") {
|
|
||||||
Text("Not Connected...")
|
|
||||||
}
|
|
||||||
composable("settings") {
|
composable("settings") {
|
||||||
AirPodsSettingsScreen(
|
if (airPodsService.value != null) {
|
||||||
device = airPodsService.value?.device,
|
AirPodsSettingsScreen(
|
||||||
service = airPodsService.value,
|
device = airPodsService.value?.device,
|
||||||
navController = navController
|
service = airPodsService.value!!,
|
||||||
)
|
navController = navController,
|
||||||
|
isConnected = isConnected.value
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
composable("debug") {
|
composable("debug") {
|
||||||
DebugScreen(navController = navController)
|
DebugScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val receiver = object: BroadcastReceiver() {
|
serviceConnection = remember {
|
||||||
override fun onReceive(p0: Context?, p1: Intent?) {
|
|
||||||
navController.navigate("settings")
|
|
||||||
navController.popBackStack("notConnected", inclusive = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.registerReceiver(receiver, IntentFilter(AirPodsNotifications.AIRPODS_CONNECTED),
|
|
||||||
Context.RECEIVER_EXPORTED)
|
|
||||||
|
|
||||||
val serviceConnection = remember {
|
|
||||||
object : ServiceConnection {
|
object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
val binder = service as AirPodsService.LocalBinder
|
val binder = service as AirPodsService.LocalBinder
|
||||||
@@ -126,28 +150,23 @@ fun Main() {
|
|||||||
|
|
||||||
context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
|
context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
|
||||||
val alreadyConnected = remember { mutableStateOf(false) }
|
if (airPodsService.value?.isConnected == true) {
|
||||||
if (airPodsService.value?.isConnected == true && !alreadyConnected.value) {
|
isConnected.value = true
|
||||||
Log.d("ALN", "Connected")
|
|
||||||
navController.navigate("settings")
|
|
||||||
} else {
|
|
||||||
Log.d("ALN", "Not connected")
|
|
||||||
navController.navigate("notConnected")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Permission is not granted, request it
|
// Permission is not granted, request it
|
||||||
Column (
|
Column (
|
||||||
modifier = Modifier.padding(24.dp),
|
modifier = Modifier.padding(24.dp),
|
||||||
){
|
){
|
||||||
val textToShow = if (bluetoothConnectPermissionState.status.shouldShowRationale) {
|
val textToShow = if (permissionState.shouldShowRationale) {
|
||||||
// If the user has denied the permission but not permanently, explain why it's needed.
|
// If the user has denied the permission but not permanently, explain why it's needed.
|
||||||
"The BLUETOOTH_CONNECT permission is important for this app. Please grant it to proceed."
|
"Please enable Bluetooth and Notification permissions to use the app. The Nearby Devices is required to connect to your AirPods, and the notification is required to show the AirPods battery status."
|
||||||
} else {
|
} else {
|
||||||
// If the user has permanently denied the permission, inform them to enable it in settings.
|
// If the user has permanently denied the permission, inform them to enable it in settings.
|
||||||
"BLUETOOTH_CONNECT permission required for this feature. Please enable it in settings."
|
"Please enable Bluetooth and Notification permissions in the app settings to use the app."
|
||||||
}
|
}
|
||||||
Text(textToShow)
|
Text(textToShow)
|
||||||
Button(onClick = { bluetoothConnectPermissionState.launchPermissionRequest() }) {
|
Button(onClick = { permissionState.launchMultiplePermissionRequest() }) {
|
||||||
Text("Request permission")
|
Text("Request permission")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
package me.kavishdevar.aln
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
@@ -139,10 +141,18 @@ class AirPodsNotifications {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setBattery(data: ByteArray) {
|
fun setBattery(data: ByteArray) {
|
||||||
first = Battery(data[7].toInt(), data[9].toInt(), data[10].toInt())
|
first = if (data[10].toInt() == BatteryStatus.DISCONNECTED) {
|
||||||
second = Battery(data[12].toInt(), data[14].toInt(), data[15].toInt())
|
Battery(first.component, first.level, data[10].toInt())
|
||||||
|
} else {
|
||||||
|
Battery(data[7].toInt(), data[9].toInt(), data[10].toInt())
|
||||||
|
}
|
||||||
|
second = if (data[15].toInt() == BatteryStatus.DISCONNECTED) {
|
||||||
|
Battery(second.component, second.level, data[15].toInt())
|
||||||
|
} else {
|
||||||
|
Battery(data[12].toInt(), data[14].toInt(), data[15].toInt())
|
||||||
|
}
|
||||||
case = if (data[20].toInt() == BatteryStatus.DISCONNECTED && case.status != BatteryStatus.DISCONNECTED) {
|
case = if (data[20].toInt() == BatteryStatus.DISCONNECTED && case.status != BatteryStatus.DISCONNECTED) {
|
||||||
Battery(data[17].toInt(), case.level, data[20].toInt())
|
Battery(case.component, case.level, data[20].toInt())
|
||||||
} else {
|
} else {
|
||||||
Battery(data[17].toInt(), data[19].toInt(), data[20].toInt())
|
Battery(data[17].toInt(), data[19].toInt(), data[20].toInt())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,16 +91,6 @@ class Window (context: Context) {
|
|||||||
batteryCaseText.text = batteryStatus.find { it.component == BatteryComponent.CASE }?.let {
|
batteryCaseText.text = batteryStatus.find { it.component == BatteryComponent.CASE }?.let {
|
||||||
"\uDBC3\uDE6C ${it.level}%"
|
"\uDBC3\uDE6C ${it.level}%"
|
||||||
} ?: ""
|
} ?: ""
|
||||||
// bText.text =
|
|
||||||
// batteryStatus.joinToString(separator = "") {
|
|
||||||
// when (it.component) {
|
|
||||||
// BatteryComponent.LEFT -> "\uDBC6\uDCE5 ${it.level}%"
|
|
||||||
// BatteryComponent.RIGHT -> "\uDBC6\uDCE8 ${it.level}%"
|
|
||||||
// BatteryComponent.CASE -> "\uDBC6\uDCE6 ${it.level}%"
|
|
||||||
// else -> ""
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// composeView.setContent {
|
// composeView.setContent {
|
||||||
// Row (
|
// Row (
|
||||||
// modifier = Modifier
|
// modifier = Modifier
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="141.5dp"
|
||||||
|
android:height="109.06dp"
|
||||||
|
android:viewportWidth="141.5"
|
||||||
|
android:viewportHeight="109.06">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M0,0h141.5v109.06h-141.5z"
|
||||||
|
android:strokeAlpha="0"
|
||||||
|
android:fillAlpha="0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M37.88,109L103.63,109C129.19,109 141.5,96.75 141.5,71.31L141.5,41.63L102.81,41.63C101.75,44.44 99.13,46.25 95.81,46.25L45.75,46.25C42.5,46.25 39.75,44.44 38.75,41.63L0,41.63L0,71.31C0,96.75 12.38,109 37.88,109ZM70.75,70.06C67.63,70.13 65.06,67.5 65.06,64.44C65.06,61.31 67.63,58.69 70.75,58.69C73.88,58.69 76.5,61.31 76.5,64.44C76.5,67.38 73.88,70 70.75,70.06ZM0,35.94L38.63,35.94C39.63,33.06 42.38,31.31 45.63,31.31L95.75,31.31C99,31.31 101.69,33.06 102.69,35.94L141.31,35.94L141.31,33.94C141.31,11.13 127.81,0 103.44,0L37.88,0C13.56,0 0,11.13 0,33.94Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.85"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="94.19dp"
|
||||||
|
android:height="123.31dp"
|
||||||
|
android:viewportWidth="94.19"
|
||||||
|
android:viewportHeight="123.31">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M0,0h94.19v123.31h-94.19z"
|
||||||
|
android:strokeAlpha="0"
|
||||||
|
android:fillAlpha="0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M46.81,73.29L46.81,111.06C46.81,114.94 44.25,116.94 40.38,116.94L35.5,116.94C31.63,116.94 29.06,114.94 29.06,111.06L29.06,69.82C33.68,71.55 39.25,72.79 46.81,73.29Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.85"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M62,22.63C62.94,23.8 64.28,25.84 65.55,28.29C51.19,30.59 40.75,43.46 40.75,56.13C40.75,60.05 41.47,63.51 42.75,66.49C34.56,65.53 29.44,63.52 24.75,60.88C14.44,54.94 6.31,45.31 6.31,31.5C6.31,15.56 17.56,6.5 31.38,6.38C42.06,6.25 54,11.5 62,22.63ZM19.44,21.25C18.31,22.5 18.38,24.38 19.69,25.44L29,33.19C30.19,34.25 32.13,34.13 33.19,32.81C34.25,31.5 34.13,29.63 32.75,28.56L23.63,20.81C22.31,19.75 20.44,19.94 19.44,21.25Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.85"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M65.13,74.25C76.31,74.25 87.81,65.38 87.81,51.81C87.81,42.75 81.44,33.88 70.19,33.88C57,33.88 46.81,45.31 46.81,56.13C46.81,68.25 55.38,74.25 65.13,74.25ZM68.19,64.75C66.44,63.25 67.44,60.25 71,55.69C74.69,51.19 77.63,49.69 79.5,51.31C81.19,52.81 80.25,55.88 76.69,60.31C72.94,64.75 70,66.31 68.19,64.75Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.85"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="94.19dp"
|
||||||
|
android:height="123.31dp"
|
||||||
|
android:viewportWidth="94.19"
|
||||||
|
android:viewportHeight="123.31">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M0,0h94.19v123.31h-94.19z"
|
||||||
|
android:strokeAlpha="0"
|
||||||
|
android:fillAlpha="0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M65.06,111.06C65.06,114.94 62.56,116.94 58.69,116.94L53.81,116.94C49.88,116.94 47.31,114.94 47.31,111.06L47.31,73.29C54.87,72.79 60.44,71.56 65.06,69.84Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.85"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M87.81,31.5C87.81,45.31 79.69,54.94 69.44,60.88C64.74,63.52 59.6,65.54 51.37,66.5C52.65,63.51 53.38,60.05 53.38,56.13C53.38,43.47 43,30.6 28.61,28.29C29.89,25.84 31.25,23.8 32.19,22.63C40.19,11.5 52.06,6.25 62.81,6.38C76.56,6.5 87.81,15.56 87.81,31.5ZM70.56,20.81L61.38,28.56C60.06,29.63 59.94,31.5 61,32.81C62.06,34.13 63.94,34.25 65.19,33.19L74.44,25.44C75.75,24.38 75.81,22.5 74.75,21.25C73.75,19.94 71.81,19.75 70.56,20.81Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.85"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M29.06,74.25C38.81,74.25 47.38,68.25 47.38,56.13C47.38,45.31 37.19,33.88 23.94,33.88C12.75,33.88 6.31,42.75 6.31,51.81C6.31,65.38 17.88,74.25 29.06,74.25ZM26,64.75C24.19,66.31 21.25,64.75 17.5,60.31C13.88,55.88 12.94,52.81 14.69,51.31C16.56,49.69 19.44,51.19 23.19,55.69C26.75,60.25 27.75,63.25 26,64.75Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillAlpha="0.85"/>
|
||||||
|
</vector>
|
||||||
13
android/app/src/main/res/layout/notification.xml
Normal file
13
android/app/src/main/res/layout/notification.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
||||||
49
android/app/src/main/res/layout/notification_expanded.xml
Normal file
49
android/app/src/main/res/layout/notification_expanded.xml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:fontFamily="@font/sf_pro" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:gravity="center">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/left_battery_notification"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/sf_pro"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/right_battery_notification"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/sf_pro"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/case_battery_notification"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/sf_pro"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
|
||||||
<color name="purple_500">#FF6200EE</color>
|
|
||||||
<color name="purple_700">#FF3700B3</color>
|
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.ALN" parent="android:Theme.Material.Light.NoActionBar" />
|
<style name="Theme.ALN" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user