diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 6975964..e480f8d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -14,7 +14,7 @@ android { minSdk = 28 targetSdk = 35 versionCode = 1 - versionName = "1.0" + versionName = "0.0.2-beta" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt index 47dcf73..1c6cafe 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt @@ -19,6 +19,7 @@ import android.os.Build import android.os.IBinder import android.os.ParcelUuid import android.util.Log +import android.widget.RemoteViews import androidx.core.app.NotificationCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -36,12 +37,19 @@ class AirPodsService: Service() { return LocalBinder() } + var popupShown = false + fun showPopup(service: Service, name: String) { + if (popupShown) { + return + } val window = Window(service.applicationContext) window.open(name, batteryNotification) + popupShown = true } - private object Receiver: BroadcastReceiver() { + @Suppress("ClassName") + private object bluetoothReceiver: BroadcastReceiver() { @SuppressLint("NewApi", "MissingPermission") override fun onReceive(context: Context?, intent: Intent) { val bluetoothDevice = @@ -50,11 +58,10 @@ class AirPodsService: Service() { val context = context?.applicationContext val name = context?.getSharedPreferences("settings", MODE_PRIVATE)?.getString("name", bluetoothDevice?.name) 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) { 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", name) intent.putExtra("device", bluetoothDevice) @@ -66,7 +73,6 @@ class AirPodsService: Service() { if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action || BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action ) { - Log.d("AirPodsService", "Closed Socket") context?.sendBroadcast( Intent(AirPodsNotifications.AIRPODS_DISCONNECTED) ) @@ -78,50 +84,135 @@ class AirPodsService: Service() { var isConnected = false var device: BluetoothDevice? = null + private lateinit var earReceiver: BroadcastReceiver + fun startForegroundNotification() { val notificationChannel = NotificationChannel( - "airpods", - "AirPods", - NotificationManager.IMPORTANCE_DEFAULT + "background_service_status", + "Background Service Status", + NotificationManager.IMPORTANCE_LOW ) 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.") + val notification = 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() - startForeground(2, notification) + try { + startForeground(1, notification) + } + catch (e: Exception) { + e.printStackTrace() + } } + fun updateNotificationContent(connected: Boolean, airpodsName: String? = null, batteryList: List? = 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") override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d("AirPodsService", "Service started") 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?) { - val name = this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE).getString("name", device?.name) - device = intent?.getParcelableExtra("device", BluetoothDevice::class.java)!! - showPopup(this@AirPodsService, name.toString()) - connectToSocket(device!!) - sendBroadcast(Intent(AirPodsNotifications.AIRPODS_CONNECTED).apply { - putExtra("device", device) - }) + if (intent?.action == AirPodsNotifications.AIRPODS_CONNECTION_DETECTED) { + val name = this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE) + .getString("name", device?.name) + Log.d("AirPodsService", "$name connected") + device = intent.getParcelableExtra("device", BluetoothDevice::class.java)!! + 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() { - override fun onReceive(context: Context?, intent: Intent?) { - device = null - isConnected = false - } - }, IntentFilter(AirPodsNotifications.AIRPODS_DISCONNECTED), RECEIVER_EXPORTED) + val intentFilter = IntentFilter().apply { + addAction(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED) + addAction(AirPodsNotifications.AIRPODS_DISCONNECTED) + } + registerReceiver(connectionReceiver, intentFilter, RECEIVER_EXPORTED) val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter bluetoothAdapter.bondedDevices.forEach { device -> @@ -200,9 +291,8 @@ class AirPodsService: Service() { delay(500) it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value) it.outputStream.flush() - Log.d("AirPodsService","This should run first") } - Log.d("AirPodsService","This should run later") + sendBroadcast( Intent(AirPodsNotifications.AIRPODS_CONNECTED) .putExtra("device", device) @@ -243,7 +333,7 @@ class AirPodsService: Service() { }) Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}") var justEnabledA2dp = false - val earReceiver = object : BroadcastReceiver() { + earReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val data = intent.getByteArrayExtra("data") if (data != null && earDetectionEnabled) { @@ -325,17 +415,16 @@ class AirPodsService: Service() { sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply { putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery())) }) + updateNotificationContent(true, this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE).getString("name", device.name), 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) @@ -357,7 +446,6 @@ class AirPodsService: Service() { } Log.d("AirPods Service", "Socket closed") isConnected = false - this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE) socket.close() sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED)) } @@ -471,22 +559,6 @@ class AirPodsService: Service() { 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 @@ -590,4 +662,29 @@ class AirPodsService: Service() { socket.outputStream?.write(bytes) 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() + } } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt index 3c5a18d..f3a901a 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults @@ -61,9 +60,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.shadow import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.SolidColor @@ -83,11 +84,17 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.primex.core.ExperimentalToolkitApi import com.primex.core.blur.newBackgroundBlur -import me.kavishdevar.aln.AirPodsService import kotlin.math.roundToInt + +@Preview @Composable -fun BatteryView() { +fun BatteryViewPreview() { + BatteryView(AirPodsService(), true) +} + +@Composable +fun BatteryView(service: AirPodsService, preview: Boolean = false) { val batteryStatus = remember { mutableStateOf>(listOf()) } @Suppress("DEPRECATION") val batteryReceiver = remember { object : BroadcastReceiver() { @@ -114,6 +121,16 @@ fun BatteryView() { } } + batteryStatus.value = service.getBattery() + + if (preview) { + batteryStatus.value = listOf( + Battery(BatteryComponent.LEFT, 100, BatteryStatus.CHARGING), + Battery(BatteryComponent.RIGHT, 50, BatteryStatus.NOT_CHARGING), + Battery(BatteryComponent.CASE, 5, BatteryStatus.CHARGING) + ) + } + Row { Column ( modifier = Modifier @@ -125,7 +142,7 @@ fun BatteryView() { contentDescription = "Buds", modifier = Modifier .fillMaxWidth() - .scale(0.50f) + .scale(0.80f) ) val left = batteryStatus.value.find { it.component == BatteryComponent.LEFT } 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) } else { - Row { + Row ( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { if (left?.status != BatteryStatus.DISCONNECTED) { - Text(text = "\uDBC6\uDCE5", fontFamily = FontFamily(Font(R.font.sf_pro))) - BatteryIndicator(left?.level ?: 0, left?.status == BatteryStatus.CHARGING) - Spacer(modifier = Modifier.width(16.dp)) + Row ( + horizontalArrangement = Arrangement.SpaceEvenly, + 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) { - Text(text = "\uDBC6\uDCE8", fontFamily = FontFamily(Font(R.font.sf_pro))) - BatteryIndicator(right?.level ?: 0, right?.status == BatteryStatus.CHARGING) + Row ( + 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", modifier = Modifier .fillMaxWidth() + .scale(1.25f) ) BatteryIndicator(case?.level ?: 0) } @@ -210,12 +257,11 @@ fun ToneVolumeSlider(service: AirPodsService, sharedPreferences: SharedPreferenc }, 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 + .height(36.dp), colors = SliderDefaults.colors( thumbColor = thumbColor, activeTrackColor = activeTrackColor, @@ -224,9 +270,9 @@ fun ToneVolumeSlider(service: AirPodsService, sharedPreferences: SharedPreferenc thumb = { Box( modifier = Modifier - .size(24.dp) // Circular thumb size - .shadow(4.dp, CircleShape) // Apply shadow to the thumb - .background(thumbColor, CircleShape) // Circular thumb + .size(24.dp) + .shadow(4.dp, CircleShape) + .background(thumbColor, CircleShape) ) }, track = { @@ -245,7 +291,7 @@ fun ToneVolumeSlider(service: AirPodsService, sharedPreferences: SharedPreferenc ) Box( modifier = Modifier - .fillMaxWidth(sliderValue.value / 100) + .fillMaxWidth(sliderValue.floatValue / 100) .height(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) { singleANCEnabled = enabled 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 ) .padding(horizontal = 12.dp, vertical = 12.dp) - .pointerInput(Unit) { // Detect press state for iOS-like effect + .pointerInput(Unit) { detectTapGestures( onPress = { isPressed.value = true - tryAwaitRelease() // Wait until release + tryAwaitRelease() isPressed.value = false } ) } .clickable( - indication = null, // Disable ripple effect - interactionSource = remember { MutableInteractionSource() } // Required for clickable + indication = null, + interactionSource = remember { MutableInteractionSource() } ) { - // Toggle the conversational awareness value updateSingleEnabled(!singleANCEnabled) }, verticalAlignment = Alignment.CenterVertically @@ -321,7 +365,7 @@ fun SinglePodANCSwitch(service: AirPodsService, sharedPreferences: SharedPrefere fontSize = 16.sp, color = textColor ) - Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description + Spacer(modifier = Modifier.height(4.dp)) Text( text = "Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.", 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) { volumeControlEnabled = enabled 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 ) .padding(horizontal = 12.dp, vertical = 12.dp) - .pointerInput(Unit) { // Detect press state for iOS-like effect + .pointerInput(Unit) { detectTapGestures( onPress = { isPressed.value = true - tryAwaitRelease() // Wait until release + tryAwaitRelease() isPressed.value = false } ) } .clickable( - indication = null, // Disable ripple effect - interactionSource = remember { MutableInteractionSource() } // Required for clickable + indication = null, + interactionSource = remember { MutableInteractionSource() } ) { - // Toggle the conversational awareness value updateVolumeControlEnabled(!volumeControlEnabled) }, verticalAlignment = Alignment.CenterVertically @@ -394,7 +436,7 @@ fun VolumeControlSwitch(service: AirPodsService, sharedPreferences: SharedPrefer fontSize = 16.sp, color = textColor ) - Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description + Spacer(modifier = Modifier.height(4.dp)) Text( text = "Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.", fontSize = 12.sp, @@ -434,9 +476,7 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref .background(backgroundColor, RoundedCornerShape(14.dp)) .padding(top = 2.dp) ) { - -// Tone Volume Slider - + Column( modifier = Modifier .fillMaxWidth() @@ -476,11 +516,10 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref @OptIn(ExperimentalMaterial3Api::class, ExperimentalToolkitApi::class) @SuppressLint("MissingPermission", "NewApi") @Composable -fun AirPodsSettingsScreen(device: BluetoothDevice?, service: AirPodsService?, - navController: NavController) { - val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) +fun AirPodsSettingsScreen(device: BluetoothDevice?, service: AirPodsService, + navController: NavController, isConnected: Boolean) { + val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE) 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() @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") Scaffold( @@ -490,6 +529,7 @@ fun AirPodsSettingsScreen(device: BluetoothDevice?, service: AirPodsService?, 0xFFF2F2F7 ), topBar = { + val darkMode = MaterialTheme.colorScheme.surface.luminance() < 0.5 CenterAlignedTopAppBar( title = { Text( @@ -499,43 +539,56 @@ fun AirPodsSettingsScreen(device: BluetoothDevice?, service: AirPodsService?, }, modifier = Modifier .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( - 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 -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 8.dp) -// .padding(top = 55.dp, bottom = 32.dp) - .verticalScroll( - state = verticalScrollState, - enabled = true, - ) - ) { - Spacer(Modifier.height(75.dp)) - 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()) - }) + if (isConnected == true) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .verticalScroll( + state = verticalScrollState, + enabled = true, + ) + ) { + Spacer(Modifier.height(75.dp)) + 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) + val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE) - if (service != null) { - BatteryView() + Spacer(modifier = Modifier.height(64.dp)) + + BatteryView(service = service) Spacer(modifier = Modifier.height(32.dp)) + StyledTextField( name = "Name", 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), horizontalAlignment = Alignment.CenterHorizontally ) { - // Slider Slider( value = sliderValue.floatValue, onValueChange = { @@ -667,12 +754,11 @@ fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPrefere }, valueRange = 0f..100f, onValueChangeFinished = { - // Round the value when the user stops sliding sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat() }, modifier = Modifier .fillMaxWidth() - .height(36.dp), // Adjust height to ensure thumb fits well + .height(36.dp), colors = SliderDefaults.colors( thumbColor = thumbColor, inactiveTrackColor = trackColor @@ -680,9 +766,9 @@ fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPrefere thumb = { Box( modifier = Modifier - .size(24.dp) // Circular thumb size - .shadow(4.dp, CircleShape) // Apply shadow to the thumb - .background(thumbColor, CircleShape) // Circular thumb + .size(24.dp) + .shadow(4.dp, CircleShape) + .background(thumbColor, CircleShape) ) }, track = { @@ -703,8 +789,7 @@ fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPrefere } ) - - // Labels + Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween @@ -742,13 +827,9 @@ fun IndependentToggle(name: String, service: AirPodsService, functionName: Strin val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5 val textColor = if (isDarkTheme) Color.White else Color.Black - // Standardize the key val snakeCasedName = name.replace(Regex("[\\W\\s]+"), "_").lowercase() - - // State for the toggle var checked by remember { mutableStateOf(default) } - // Load initial state from SharedPreferences LaunchedEffect(sharedPreferences) { checked = sharedPreferences.getBoolean(snakeCasedName, true) } @@ -767,14 +848,12 @@ fun IndependentToggle(name: String, service: AirPodsService, functionName: Strin .height(55.dp) .padding(horizontal = 12.dp) .clickable { - // Toggle checked state and save to SharedPreferences checked = !checked sharedPreferences .edit() .putBoolean(snakeCasedName, checked) .apply() - // Call the corresponding method in the service val method = service::class.java.getMethod(functionName, Boolean::class.java) method.invoke(service, checked) }, @@ -786,8 +865,6 @@ fun IndependentToggle(name: String, service: AirPodsService, functionName: Strin onCheckedChange = { checked = it sharedPreferences.edit().putBoolean(snakeCasedName, it).apply() - - // Call the corresponding method in the service val method = service::class.java.getMethod(functionName, Boolean::class.java) 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) { conversationalAwarenessEnabled = enabled 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 ) .padding(horizontal = 12.dp, vertical = 12.dp) - .pointerInput(Unit) { // Detect press state for iOS-like effect + .pointerInput(Unit) { detectTapGestures( onPress = { isPressed.value = true - tryAwaitRelease() // Wait until release + tryAwaitRelease() isPressed.value = false } ) } .clickable( - indication = null, // Disable ripple effect - interactionSource = remember { MutableInteractionSource() } // Required for clickable + indication = null, + interactionSource = remember { MutableInteractionSource() } ) { - // Toggle the conversational awareness value updateConversationalAwareness(!conversationalAwarenessEnabled) }, verticalAlignment = Alignment.CenterVertically @@ -852,7 +927,7 @@ fun ConversationalAwarenessSwitch(service: AirPodsService, sharedPreferences: Sh fontSize = 16.sp, color = textColor ) - Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description + Spacer(modifier = Modifier.height(4.dp)) Text( text = "Lowers media volume and reduces background noise when you start speaking to other people.", 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) { personalizedVolumeEnabled = enabled 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 ) .padding(horizontal = 12.dp, vertical = 12.dp) - .pointerInput(Unit) { // Detect press state for iOS-like effect + .pointerInput(Unit) { detectTapGestures( onPress = { isPressed.value = true - tryAwaitRelease() // Wait until release + tryAwaitRelease() isPressed.value = false } ) } .clickable( - indication = null, // Disable ripple effect - interactionSource = remember { MutableInteractionSource() } // Required for clickable + indication = null, + interactionSource = remember { MutableInteractionSource() } ) { - // Toggle the conversational awareness value updatePersonalizedVolume(!personalizedVolumeEnabled) }, verticalAlignment = Alignment.CenterVertically @@ -925,7 +998,7 @@ fun PersonalizedVolumeSwitch(service: AirPodsService, sharedPreferences: SharedP fontSize = 16.sp, color = textColor ) - Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description + Spacer(modifier = Modifier.height(4.dp)) Text( text = "Adjusts the volume of media in response to your environment.", 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) { loudSoundReductionEnabled = enabled 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 ) .padding(horizontal = 12.dp, vertical = 12.dp) - .pointerInput(Unit) { // Detect press state for iOS-like effect + .pointerInput(Unit) { detectTapGestures( onPress = { isPressed.value = true - tryAwaitRelease() // Wait until release + tryAwaitRelease() isPressed.value = false } ) } .clickable( - indication = null, // Disable ripple effect - interactionSource = remember { MutableInteractionSource() } // Required for clickable + indication = null, + interactionSource = remember { MutableInteractionSource() } ) { - // Toggle the conversational awareness value updateLoudSoundReduction(!loudSoundReductionEnabled) }, verticalAlignment = Alignment.CenterVertically @@ -999,7 +1070,7 @@ fun LoudSoundReductionSwitch(service: AirPodsService, sharedPreferences: SharedP fontSize = 16.sp, color = textColor ) - Spacer(modifier = Modifier.height(4.dp)) // Small space between main text and description + Spacer(modifier = Modifier.height(4.dp)) Text( text = "Reduces loud sounds you are exposed to.", fontSize = 12.sp, @@ -1393,14 +1464,15 @@ fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) { val batteryTextColor = MaterialTheme.colorScheme.onSurface // Battery indicator dimensions - val batteryWidth = 30.dp + val batteryWidth = 40.dp val batteryHeight = 15.dp val batteryCornerRadius = 4.dp - val tipWidth = 5.dp + val tipWidth = 4.dp val tipHeight = batteryHeight * 0.3f - Column(horizontalAlignment = Alignment.CenterHorizontally) { - // Row for battery icon and tip + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(0.dp), @@ -1423,21 +1495,23 @@ fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) { if (charging) { Box( modifier = Modifier - .fillMaxSize(), // Take up the entire size of the outer Box - contentAlignment = Alignment.Center // Center the charging bolt within the Box + .padding(0.dp) + .fillMaxSize(), + contentAlignment = Alignment.Center ) { Text( text = "\uDBC0\uDEE6", - fontSize = 12.sp, + fontSize = 15.sp, fontFamily = FontFamily(Font(R.font.sf_pro)), color = Color.White, - modifier = Modifier.align(Alignment.Center) + modifier = Modifier + .align(Alignment.Center) + .padding(0.dp) ) } } } - // Battery Tip (Protrusion) Box( modifier = Modifier .width(tipWidth) @@ -1455,7 +1529,6 @@ fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) { ) } - // Battery Percentage Text Text( text = "$batteryPercentage%", color = batteryTextColor, diff --git a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt index 3e3a316..3d024ea 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt @@ -20,23 +20,25 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState -import com.google.accompanist.permissions.shouldShowRationale +import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.primex.core.ExperimentalToolkitApi import me.kavishdevar.aln.ui.theme.ALNTheme +lateinit var serviceConnection: ServiceConnection +lateinit var connectionStatusReceiver: BroadcastReceiver @ExperimentalMaterial3Api class MainActivity : ComponentActivity() { @@ -46,72 +48,94 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { ALNTheme { + getSharedPreferences("settings", MODE_PRIVATE).edit().putLong("textColor", + MaterialTheme.colorScheme.onSurface.toArgb().toLong()).apply() Main() 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") @OptIn(ExperimentalPermissionsApi::class) @Composable fun Main() { - val bluetoothConnectPermissionState = rememberPermissionState( - permission = "android.permission.BLUETOOTH_CONNECT" + val isConnected = remember { mutableStateOf(false) } + val permissionState = rememberMultiplePermissionsState( + permissions = listOf( + "android.permission.BLUETOOTH_CONNECT", + "android.permission.POST_NOTIFICATIONS" + ) ) - if (bluetoothConnectPermissionState.status.isGranted) { + val airPodsService = remember { mutableStateOf(null) } + if (permissionState.allPermissionsGranted) { + Log.d("MainActivity", "HIIIIIIIIIIIIIII") val context = LocalContext.current - val airPodsService = remember { mutableStateOf(null) } val navController = rememberNavController() - - val disconnectReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - Log.d("MainActivity", "Received DISCONNECTED broadcast") - navController.navigate("notConnected") + connectionStatusReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == AirPodsNotifications.AIRPODS_CONNECTED) { + Log.d("MainActivity", "AirPods Connected intent received") + 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), - Context.RECEIVER_NOT_EXPORTED) + val filter = IntentFilter().apply { + 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( navController = navController, - startDestination = "notConnected", + startDestination = "settings", enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) }, exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) }, popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) }, popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(300)) } ) { - composable("notConnected") { - Text("Not Connected...") - } composable("settings") { - AirPodsSettingsScreen( - device = airPodsService.value?.device, - service = airPodsService.value, - navController = navController - ) + if (airPodsService.value != null) { + AirPodsSettingsScreen( + device = airPodsService.value?.device, + service = airPodsService.value!!, + navController = navController, + isConnected = isConnected.value + ) + } } composable("debug") { DebugScreen(navController = navController) } } - val receiver = object: BroadcastReceiver() { - 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 { + serviceConnection = remember { object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { val binder = service as AirPodsService.LocalBinder @@ -126,28 +150,23 @@ fun Main() { context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE) - val alreadyConnected = remember { mutableStateOf(false) } - if (airPodsService.value?.isConnected == true && !alreadyConnected.value) { - Log.d("ALN", "Connected") - navController.navigate("settings") - } else { - Log.d("ALN", "Not connected") - navController.navigate("notConnected") + if (airPodsService.value?.isConnected == true) { + isConnected.value = true } } else { // Permission is not granted, request it Column ( 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. - "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 { // 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) - Button(onClick = { bluetoothConnectPermissionState.launchPermissionRequest() }) { + Button(onClick = { permissionState.launchMultiplePermissionRequest() }) { Text("Request permission") } } diff --git a/android/app/src/main/java/me/kavishdevar/aln/Packets.kt b/android/app/src/main/java/me/kavishdevar/aln/Packets.kt index 20813b1..dabd42d 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/Packets.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/Packets.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package me.kavishdevar.aln import android.os.Parcelable @@ -139,10 +141,18 @@ class AirPodsNotifications { } fun setBattery(data: ByteArray) { - first = Battery(data[7].toInt(), data[9].toInt(), data[10].toInt()) - second = Battery(data[12].toInt(), data[14].toInt(), data[15].toInt()) + first = if (data[10].toInt() == BatteryStatus.DISCONNECTED) { + 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) { - Battery(data[17].toInt(), case.level, data[20].toInt()) + Battery(case.component, case.level, data[20].toInt()) } else { Battery(data[17].toInt(), data[19].toInt(), data[20].toInt()) } diff --git a/android/app/src/main/java/me/kavishdevar/aln/Window.kt b/android/app/src/main/java/me/kavishdevar/aln/Window.kt index 633c8b5..5404b84 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/Window.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/Window.kt @@ -91,16 +91,6 @@ class Window (context: Context) { batteryCaseText.text = batteryStatus.find { it.component == BatteryComponent.CASE }?.let { "\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 { // Row ( // modifier = Modifier diff --git a/android/app/src/main/res/drawable/airpods_pro_case_notification.xml b/android/app/src/main/res/drawable/airpods_pro_case_notification.xml new file mode 100644 index 0000000..33031c5 --- /dev/null +++ b/android/app/src/main/res/drawable/airpods_pro_case_notification.xml @@ -0,0 +1,15 @@ + + + + diff --git a/android/app/src/main/res/drawable/airpods_pro_left_notification.xml b/android/app/src/main/res/drawable/airpods_pro_left_notification.xml new file mode 100644 index 0000000..7d82fcf --- /dev/null +++ b/android/app/src/main/res/drawable/airpods_pro_left_notification.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/airpods_pro_right_notification.xml b/android/app/src/main/res/drawable/airpods_pro_right_notification.xml new file mode 100644 index 0000000..6cbaf96 --- /dev/null +++ b/android/app/src/main/res/drawable/airpods_pro_right_notification.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/android/app/src/main/res/layout/notification.xml b/android/app/src/main/res/layout/notification.xml new file mode 100644 index 0000000..fba864b --- /dev/null +++ b/android/app/src/main/res/layout/notification.xml @@ -0,0 +1,13 @@ + + + + diff --git a/android/app/src/main/res/layout/notification_expanded.xml b/android/app/src/main/res/layout/notification_expanded.xml new file mode 100644 index 0000000..9b6213c --- /dev/null +++ b/android/app/src/main/res/layout/notification_expanded.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index f8c6127..c8524cd 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,10 +1,5 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 #FF000000 #FFFFFFFF \ No newline at end of file diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index 10880fc..af1de19 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -1,5 +1,4 @@ -