diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 23d0390c..115e6d42 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,5 +1,3 @@
-import org.gradle.api.JavaVersion.VERSION_11
-import org.gradle.api.JavaVersion.VERSION_17
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@@ -49,11 +47,11 @@ android {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
-kotlin {
- compilerOptions {
- jvmTarget.set(JvmTarget.JVM_21)
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_21)
+ }
}
-}
buildFeatures {
compose = true
buildConfig = true
@@ -86,7 +84,7 @@ dependencies {
// Android 12+ SplashScreen API with backward compatibility attributes
implementation("androidx.core:core-splashscreen:1.0.1")
- implementation ("androidx.compose.material3:material3:1.5.0-alpha10")
+ implementation("androidx.compose.material3:material3:1.5.0-alpha10")
implementation("androidx.compose.material:material-icons-core:1.7.8")
implementation("androidx.compose.material:material-icons-extended:1.7.8")
diff --git a/app/src/androidTest/java/com/sameerasw/airsync/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/sameerasw/airsync/ExampleInstrumentedTest.kt
index 659a5c25..4557bbf5 100644
--- a/app/src/androidTest/java/com/sameerasw/airsync/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/sameerasw/airsync/ExampleInstrumentedTest.kt
@@ -1,13 +1,11 @@
package com.sameerasw.airsync
-import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
-
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.Assert.*
-
/**
* Instrumented test, which will execute on an Android device.
*
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a9f19d1a..5cd44fcb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,14 +19,16 @@
-
+
+
-
@@ -57,21 +59,23 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AirSync"
- android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="36">
-
+
+ android:theme="@style/Theme.AirSync.Splash">
@@ -87,29 +91,30 @@
+
+
-
+
-
+ android:taskAffinity=""
+ android:theme="@style/Theme.AirSync.Transparent">
@@ -131,8 +136,8 @@
android:name=".presentation.ui.activities.QRScannerActivity"
android:exported="true"
android:label="@string/app_name"
- android:theme="@style/Theme.AirSync"
- android:screenOrientation="portrait">
+ android:screenOrientation="portrait"
+ android:theme="@style/Theme.AirSync">
@@ -151,8 +156,7 @@
-
+ android:foregroundServiceType="connectedDevice">
-
+ android:exported="true"
+ android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS">
@@ -270,8 +273,9 @@
-
-
+
+
+
diff --git a/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt b/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt
index a01c427b..45f8a4fe 100644
--- a/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt
+++ b/app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt
@@ -15,14 +15,15 @@ class AirSyncApp : Application() {
companion object {
private var instance: AirSyncApp? = null
fun isAppForeground(): Boolean = instance?.isForeground() ?: false
- fun getBleConnectionManager(): com.sameerasw.airsync.data.ble.BleConnectionManager? = instance?.bleConnectionManager
+ fun getBleConnectionManager(): com.sameerasw.airsync.data.ble.BleConnectionManager? =
+ instance?.bleConnectionManager
}
override fun onCreate() {
super.onCreate()
instance = this
initSentry()
-
+
bleConnectionManager = com.sameerasw.airsync.data.ble.BleConnectionManager(this)
bleConnectionManager.start()
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
@@ -30,11 +31,13 @@ class AirSyncApp : Application() {
override fun onActivityStarted(activity: Activity) {
activityCount++
}
+
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {
activityCount--
}
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
})
@@ -49,7 +52,8 @@ class AirSyncApp : Application() {
if (!isEnabled) return
SentryAndroid.init(this) { options ->
- options.dsn = "https://cb9b0ead9e88e0818269e773cb662141@o4510996760887296.ingest.de.sentry.io/4511002261389392"
+ options.dsn =
+ "https://cb9b0ead9e88e0818269e773cb662141@o4510996760887296.ingest.de.sentry.io/4511002261389392"
options.isEnabled = true
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/MainActivity.kt b/app/src/main/java/com/sameerasw/airsync/MainActivity.kt
index c9848bc0..b71c49da 100644
--- a/app/src/main/java/com/sameerasw/airsync/MainActivity.kt
+++ b/app/src/main/java/com/sameerasw/airsync/MainActivity.kt
@@ -12,37 +12,15 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.statusBars
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.material.icons.rounded.HelpOutline
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.material3.Surface
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.unit.dp
import androidx.core.animation.doOnEnd
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.compose.NavHost
@@ -52,7 +30,6 @@ import com.sameerasw.airsync.data.local.DataStoreManager
import com.sameerasw.airsync.presentation.ui.activities.QRScannerActivity
import com.sameerasw.airsync.presentation.ui.screens.AirSyncMainScreen
import com.sameerasw.airsync.ui.theme.AirSyncTheme
-import com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel
import com.sameerasw.airsync.utils.AdbMdnsDiscovery
import com.sameerasw.airsync.utils.ContentCaptureManager
import com.sameerasw.airsync.utils.DevicePreviewResolver
@@ -246,7 +223,8 @@ class MainActivity : ComponentActivity() {
this@MainActivity,
R.color.material_primary
)
- splashIcon.imageTintList = android.content.res.ColorStateList.valueOf(colorPrimary)
+ splashIcon.imageTintList =
+ android.content.res.ColorStateList.valueOf(colorPrimary)
Log.d("MainActivity", "Switched to device icon with primary tint")
// Fade in the new device icon
@@ -299,28 +277,28 @@ class MainActivity : ComponentActivity() {
fadeOutIcon.start()
} else {
// No device icon found, or splashIcon is null/not ImageView (OEM device compatibility)
- // Proceed directly to outro after a brief hold
- try {
- splashScreenView.postDelayed({
- startOutroAnimation(
- splashScreenView,
- splashIcon,
- splashScreenViewProvider
- )
- }, 500)
- } catch (e: Exception) {
- Log.e(
- "MainActivity",
- "Error scheduling outro with no icon: ${e.message}",
- e
- )
- // Fallback: start outro immediately
+ // Proceed directly to outro after a brief hold
+ try {
+ splashScreenView.postDelayed({
startOutroAnimation(
splashScreenView,
splashIcon,
splashScreenViewProvider
)
- }
+ }, 500)
+ } catch (e: Exception) {
+ Log.e(
+ "MainActivity",
+ "Error scheduling outro with no icon: ${e.message}",
+ e
+ )
+ // Fallback: start outro immediately
+ startOutroAnimation(
+ splashScreenView,
+ splashIcon,
+ splashScreenViewProvider
+ )
+ }
}
} catch (e: Exception) {
// Fallback for any unexpected exceptions during animation
@@ -344,7 +322,10 @@ class MainActivity : ComponentActivity() {
AdbDiscoveryHolder.initialize(this)
Log.d("MainActivity", "Started persistent ADB discovery at app startup")
} else {
- Log.d("MainActivity", "Skipping persistent ADB discovery at startup: ACCESS_LOCAL_NETWORK permission not granted")
+ Log.d(
+ "MainActivity",
+ "Skipping persistent ADB discovery at startup: ACCESS_LOCAL_NETWORK permission not granted"
+ )
}
// Check if this is a QS tile long-press intent and device is not connected
@@ -412,7 +393,8 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.padding(innerPadding)
) {
composable("main") {
- val initialPage = if (intent?.action == ShortcutUtil.DASH_ACTION_REMOTE) 1 else 0
+ val initialPage =
+ if (intent?.action == ShortcutUtil.DASH_ACTION_REMOTE) 1 else 0
AirSyncMainScreen(
initialIp = ip,
initialPort = port,
diff --git a/app/src/main/java/com/sameerasw/airsync/data/ble/BleChunkUtil.kt b/app/src/main/java/com/sameerasw/airsync/data/ble/BleChunkUtil.kt
index 7504492c..88f781ed 100644
--- a/app/src/main/java/com/sameerasw/airsync/data/ble/BleChunkUtil.kt
+++ b/app/src/main/java/com/sameerasw/airsync/data/ble/BleChunkUtil.kt
@@ -12,7 +12,7 @@ object BleChunkUtil {
fun splitIntoChunks(payload: String, mtu: Int): List {
val data = payload.toByteArray(Charsets.UTF_8)
val maxPayloadSize = mtu - BleConstants.CHUNK_HEADER_SIZE
-
+
if (maxPayloadSize <= 0) {
Log.e(TAG, "MTU too small: $mtu")
return emptyList()
@@ -20,21 +20,21 @@ object BleChunkUtil {
val totalChunks = (data.size + maxPayloadSize - 1) / maxPayloadSize
val chunks = mutableListOf()
-
+
for (i in 0 until totalChunks) {
val start = i * maxPayloadSize
val end = minOf(start + maxPayloadSize, data.size)
val chunkData = data.sliceArray(start until end)
-
+
val chunk = ByteArray(BleConstants.CHUNK_HEADER_SIZE + chunkData.size)
val buffer = java.nio.ByteBuffer.wrap(chunk)
buffer.putShort(i.toShort())
buffer.putShort(totalChunks.toShort())
-
+
chunkData.copyInto(chunk, BleConstants.CHUNK_HEADER_SIZE)
chunks.add(chunk)
}
-
+
return chunks
}
@@ -45,20 +45,20 @@ object BleChunkUtil {
fun reassemble(chunks: Map): String {
val sortedIndices = chunks.keys.sorted()
if (sortedIndices.isEmpty()) return ""
-
+
val totalSize = chunks.values.sumOf { it.size }
val result = ByteArray(totalSize)
-
+
var offset = 0
for (index in sortedIndices) {
val chunk = chunks[index] ?: continue
chunk.copyInto(result, offset)
offset += chunk.size
}
-
+
return String(result, Charsets.UTF_8)
}
-
+
/**
* Extracts header information from a raw BLE packet.
*/
@@ -89,15 +89,15 @@ object BleChunkUtil {
val header = parseHeader(packet) ?: return null
val current = header.first
val total = header.second
-
+
if (totalChunks != -1 && totalChunks != total) {
// New transmission started or mismatch, reset
chunks.clear()
}
totalChunks = total
-
+
chunks[current] = getPayload(packet)
-
+
if (chunks.size == totalChunks) {
val result = reassemble(chunks)
chunks.clear()
diff --git a/app/src/main/java/com/sameerasw/airsync/data/ble/BleConnectionManager.kt b/app/src/main/java/com/sameerasw/airsync/data/ble/BleConnectionManager.kt
index a74882b0..cc161b3b 100644
--- a/app/src/main/java/com/sameerasw/airsync/data/ble/BleConnectionManager.kt
+++ b/app/src/main/java/com/sameerasw/airsync/data/ble/BleConnectionManager.kt
@@ -4,28 +4,34 @@ import android.content.Context
import android.util.Log
import com.sameerasw.airsync.data.local.DataStoreManager
import com.sameerasw.airsync.utils.WebSocketUtil
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.launch
class BleConnectionManager(private val context: Context) {
companion object {
private const val TAG = "BleConnectionManager"
}
-
+
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val dataStoreManager = DataStoreManager(context)
private var bleServer: BleGattServer? = null
-
+
private var isBleEnabled = false
@OptIn(ExperimentalCoroutinesApi::class)
private val _serverFlow = kotlinx.coroutines.flow.MutableStateFlow(null)
-
+
@OptIn(ExperimentalCoroutinesApi::class)
val connectionState = _serverFlow.flatMapLatest { server ->
- server?.connectionState ?: kotlinx.coroutines.flow.MutableStateFlow(BleGattServer.BleConnectionState.DISCONNECTED)
+ server?.connectionState
+ ?: kotlinx.coroutines.flow.MutableStateFlow(BleGattServer.BleConnectionState.DISCONNECTED)
}
fun start() {
diff --git a/app/src/main/java/com/sameerasw/airsync/data/ble/BleConstants.kt b/app/src/main/java/com/sameerasw/airsync/data/ble/BleConstants.kt
index 78d5c7c9..113e5c7d 100644
--- a/app/src/main/java/com/sameerasw/airsync/data/ble/BleConstants.kt
+++ b/app/src/main/java/com/sameerasw/airsync/data/ble/BleConstants.kt
@@ -44,7 +44,7 @@ object BleConstants {
// Chunking
const val MAX_MTU = 512
const val CHUNK_HEADER_SIZE = 4 // [index: UInt16][total: UInt16]
-
+
// Delimiter for compact strings
const val DELIMITER = "\u001F"
}
diff --git a/app/src/main/java/com/sameerasw/airsync/data/ble/BleGattServer.kt b/app/src/main/java/com/sameerasw/airsync/data/ble/BleGattServer.kt
index 671ec295..5d99ed16 100644
--- a/app/src/main/java/com/sameerasw/airsync/data/ble/BleGattServer.kt
+++ b/app/src/main/java/com/sameerasw/airsync/data/ble/BleGattServer.kt
@@ -1,7 +1,15 @@
package com.sameerasw.airsync.data.ble
import android.annotation.SuppressLint
-import android.bluetooth.*
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGattCharacteristic
+import android.bluetooth.BluetoothGattDescriptor
+import android.bluetooth.BluetoothGattServer
+import android.bluetooth.BluetoothGattServerCallback
+import android.bluetooth.BluetoothGattService
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothProfile
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
@@ -9,14 +17,21 @@ import android.content.Context
import android.os.ParcelUuid
import android.util.Log
import com.sameerasw.airsync.data.local.DataStoreManager
+import com.sameerasw.airsync.utils.ClipboardSyncManager
import com.sameerasw.airsync.utils.MacDeviceStatusManager
import com.sameerasw.airsync.utils.NotificationDismissalUtil
-import com.sameerasw.airsync.utils.ClipboardSyncManager
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
-import java.util.*
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue
@SuppressLint("MissingPermission")
@@ -31,7 +46,8 @@ class BleGattServer(private val context: Context) {
instance = this
}
- private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+ private val bluetoothManager =
+ context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val adapter = bluetoothManager.adapter
private var gattServer: BluetoothGattServer? = null
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -91,14 +107,17 @@ class BleGattServer(private val context: Context) {
} catch (e: Exception) {
""
}
- val rawName = if (customName.isNotBlank()) customName else com.sameerasw.airsync.utils.DeviceInfoUtil.getDeviceName(context)
+ val rawName =
+ if (customName.isNotBlank()) customName else com.sameerasw.airsync.utils.DeviceInfoUtil.getDeviceName(
+ context
+ )
val baseName = rawName
.replace("AirSync-AirSync-", "")
.replace("AirSync-", "")
.replace("airsync-", "")
.replace("airsync", "")
.trim()
-
+
val bleName = "AirSync-$baseName"
try {
if (adapter.name != bleName) {
@@ -130,11 +149,14 @@ class BleGattServer(private val context: Context) {
private fun setupGattServer() {
gattServer = bluetoothManager.openGattServer(context, gattServerCallback)
-
+
pendingServices.clear()
// System Service
- val systemService = BluetoothGattService(BleConstants.SERVICE_SYSTEM, BluetoothGattService.SERVICE_TYPE_PRIMARY)
+ val systemService = BluetoothGattService(
+ BleConstants.SERVICE_SYSTEM,
+ BluetoothGattService.SERVICE_TYPE_PRIMARY
+ )
systemService.addCharacteristic(createReadCharacteristic(BleConstants.CHAR_PROTOCOL_VERSION))
systemService.addCharacteristic(createWriteCharacteristic(BleConstants.CHAR_AUTH_TOKEN))
systemService.addCharacteristic(createNotifyCharacteristic(BleConstants.CHAR_AUTH_RESULT))
@@ -146,7 +168,10 @@ class BleGattServer(private val context: Context) {
pendingServices.add(systemService)
// Notifications Service
- val notifService = BluetoothGattService(BleConstants.SERVICE_NOTIFICATIONS, BluetoothGattService.SERVICE_TYPE_PRIMARY)
+ val notifService = BluetoothGattService(
+ BleConstants.SERVICE_NOTIFICATIONS,
+ BluetoothGattService.SERVICE_TYPE_PRIMARY
+ )
notifService.addCharacteristic(createNotifyCharacteristic(BleConstants.CHAR_NOTIFICATION_DATA))
notifService.addCharacteristic(createWriteCharacteristic(BleConstants.CHAR_NOTIFICATION_ACTION))
notifService.addCharacteristic(createWriteCharacteristic(BleConstants.CHAR_NOTIFICATION_DISMISS))
@@ -154,14 +179,20 @@ class BleGattServer(private val context: Context) {
pendingServices.add(notifService)
// Media Service
- val mediaService = BluetoothGattService(BleConstants.SERVICE_MEDIA, BluetoothGattService.SERVICE_TYPE_PRIMARY)
+ val mediaService = BluetoothGattService(
+ BleConstants.SERVICE_MEDIA,
+ BluetoothGattService.SERVICE_TYPE_PRIMARY
+ )
mediaService.addCharacteristic(createNotifyCharacteristic(BleConstants.CHAR_MEDIA_STATE))
mediaService.addCharacteristic(createWriteCharacteristic(BleConstants.CHAR_MEDIA_CONTROL))
mediaService.addCharacteristic(createWriteCharacteristic(BleConstants.CHAR_MAC_MEDIA_STATE))
pendingServices.add(mediaService)
// Clipboard Service
- val clipService = BluetoothGattService(BleConstants.SERVICE_CLIPBOARD, BluetoothGattService.SERVICE_TYPE_PRIMARY)
+ val clipService = BluetoothGattService(
+ BleConstants.SERVICE_CLIPBOARD,
+ BluetoothGattService.SERVICE_TYPE_PRIMARY
+ )
clipService.addCharacteristic(createNotifyCharacteristic(BleConstants.CHAR_CLIPBOARD_DATA_NOTIFY))
clipService.addCharacteristic(createWriteCharacteristic(BleConstants.CHAR_CLIPBOARD_DATA_WRITE))
pendingServices.add(clipService)
@@ -182,7 +213,7 @@ class BleGattServer(private val context: Context) {
if (currentAdvertiseCallback != null) {
stopAdvertising()
}
-
+
val advertiser = adapter.bluetoothLeAdvertiser ?: return
val settings = AdvertiseSettings.Builder()
@@ -268,7 +299,10 @@ class BleGattServer(private val context: Context) {
}
override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
- Log.d(TAG, "onConnectionStateChange: device=${device.address}, status=$status, newState=$newState, bond=${device.bondState}")
+ Log.d(
+ TAG,
+ "onConnectionStateChange: device=${device.address}, status=$status, newState=$newState, bond=${device.bondState}"
+ )
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d(TAG, "Device connected: ${device.address}")
@@ -279,7 +313,8 @@ class BleGattServer(private val context: Context) {
connectedDevices.remove(device)
if (connectedDevices.isEmpty()) {
stopHeartbeat()
- _connectionState.value = if (gattServer != null) BleConnectionState.ADVERTISING else BleConnectionState.DISCONNECTED
+ _connectionState.value =
+ if (gattServer != null) BleConnectionState.ADVERTISING else BleConnectionState.DISCONNECTED
isAuthenticated = false
if (gattServer != null) {
val isEnabled = try {
@@ -305,48 +340,109 @@ class BleGattServer(private val context: Context) {
negotiatedMtu = mtu
}
- override fun onCharacteristicReadRequest(device: BluetoothDevice, requestId: Int, offset: Int, characteristic: BluetoothGattCharacteristic) {
+ override fun onCharacteristicReadRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ offset: Int,
+ characteristic: BluetoothGattCharacteristic
+ ) {
if (characteristic.uuid == BleConstants.CHAR_PROTOCOL_VERSION) {
- gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, byteArrayOf(BleConstants.PROTOCOL_VERSION.toByte()))
+ gattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ 0,
+ byteArrayOf(BleConstants.PROTOCOL_VERSION.toByte())
+ )
} else {
- gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_READ_NOT_PERMITTED, 0, null)
+ gattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_READ_NOT_PERMITTED,
+ 0,
+ null
+ )
}
}
private val chunkBuffers = mutableMapOf>()
- override fun onCharacteristicWriteRequest(device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) {
+ override fun onCharacteristicWriteRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ characteristic: BluetoothGattCharacteristic,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray
+ ) {
Log.d(TAG, "Write request for ${characteristic.uuid}, length: ${value.size}")
-
+
if (characteristic.uuid != BleConstants.CHAR_AUTH_TOKEN && !isAuthenticated) {
- Log.w(TAG, "Blocked unauthorized write request to ${characteristic.uuid} from ${device.address}")
+ Log.w(
+ TAG,
+ "Blocked unauthorized write request to ${characteristic.uuid} from ${device.address}"
+ )
if (responseNeeded) {
- gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_WRITE_NOT_PERMITTED, offset, null)
+ gattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_WRITE_NOT_PERMITTED,
+ offset,
+ null
+ )
}
return
}
-
+
when (characteristic.uuid) {
BleConstants.CHAR_AUTH_TOKEN -> handleAuthRequest(device, value)
BleConstants.CHAR_MAC_BATTERY -> handleMacBattery(value)
- BleConstants.CHAR_NOTIFICATION_ACTION -> handleChunkedWrite(characteristic.uuid, value) { handleNotificationAction(it.toByteArray(Charsets.UTF_8)) }
- BleConstants.CHAR_MEDIA_CONTROL -> handleChunkedWrite(characteristic.uuid, value) { handleMediaControl(it.toByteArray(Charsets.UTF_8)) }
- BleConstants.CHAR_MAC_MEDIA_STATE -> handleChunkedWrite(characteristic.uuid, value) { handleMacMediaState(it) }
- BleConstants.CHAR_CLIPBOARD_DATA_WRITE -> handleChunkedWrite(characteristic.uuid, value) {
+ BleConstants.CHAR_NOTIFICATION_ACTION -> handleChunkedWrite(
+ characteristic.uuid,
+ value
+ ) { handleNotificationAction(it.toByteArray(Charsets.UTF_8)) }
+
+ BleConstants.CHAR_MEDIA_CONTROL -> handleChunkedWrite(
+ characteristic.uuid,
+ value
+ ) { handleMediaControl(it.toByteArray(Charsets.UTF_8)) }
+
+ BleConstants.CHAR_MAC_MEDIA_STATE -> handleChunkedWrite(
+ characteristic.uuid,
+ value
+ ) { handleMacMediaState(it) }
+
+ BleConstants.CHAR_CLIPBOARD_DATA_WRITE -> handleChunkedWrite(
+ characteristic.uuid,
+ value
+ ) {
Log.d(TAG, "Received clipboard from Mac via BLE: ${it.take(50)}")
ClipboardSyncManager.handleClipboardUpdate(context, it)
}
- BleConstants.CHAR_DEVICE_NAME -> handleChunkedWrite(characteristic.uuid, value) {
+
+ BleConstants.CHAR_DEVICE_NAME -> handleChunkedWrite(characteristic.uuid, value) {
Log.d(TAG, "Received Mac Device Name: $it")
// Update Mac name in status manager
MacDeviceStatusManager.updateMacStatus(context, name = it)
}
- BleConstants.CHAR_NOTIFICATION_DISMISS -> handleChunkedWrite(characteristic.uuid, value) { handleNotificationDismiss(it) }
+
+ BleConstants.CHAR_NOTIFICATION_DISMISS -> handleChunkedWrite(
+ characteristic.uuid,
+ value
+ ) { handleNotificationDismiss(it) }
+
else -> Log.w(TAG, "Unknown characteristic write: ${characteristic.uuid}")
}
if (responseNeeded) {
- gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value)
+ gattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ offset,
+ value
+ )
}
}
@@ -359,10 +455,10 @@ class BleGattServer(private val context: Context) {
}
val (current, total) = header
val payload = BleChunkUtil.getPayload(value)
-
+
val buffer = chunkBuffers.getOrPut(uuid) { mutableMapOf() }
buffer[current] = payload
-
+
if (buffer.size == total) {
val completePayload = BleChunkUtil.reassemble(buffer)
chunkBuffers.remove(uuid)
@@ -370,15 +466,37 @@ class BleGattServer(private val context: Context) {
}
}
- override fun onDescriptorReadRequest(device: BluetoothDevice, requestId: Int, offset: Int, descriptor: BluetoothGattDescriptor) {
+ override fun onDescriptorReadRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ offset: Int,
+ descriptor: BluetoothGattDescriptor
+ ) {
Log.d(TAG, "Descriptor read request: ${descriptor.uuid}")
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null)
}
- override fun onDescriptorWriteRequest(device: BluetoothDevice, requestId: Int, descriptor: BluetoothGattDescriptor, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) {
- Log.d(TAG, "Descriptor write request: ${descriptor.uuid}, value: ${value.contentToString()}")
+ override fun onDescriptorWriteRequest(
+ device: BluetoothDevice,
+ requestId: Int,
+ descriptor: BluetoothGattDescriptor,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray
+ ) {
+ Log.d(
+ TAG,
+ "Descriptor write request: ${descriptor.uuid}, value: ${value.contentToString()}"
+ )
if (responseNeeded) {
- gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value)
+ gattServer?.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ offset,
+ value
+ )
}
}
@@ -392,12 +510,15 @@ class BleGattServer(private val context: Context) {
scope.launch {
val deviceData = dataStoreManager.getLastConnectedDevice().first()
val storedKey = deviceData?.symmetricKey
- Log.d(TAG, "Handling auth request from ${device.address}. Device in DB: ${deviceData?.name}, hasKey: ${storedKey != null}")
-
+ Log.d(
+ TAG,
+ "Handling auth request from ${device.address}. Device in DB: ${deviceData?.name}, hasKey: ${storedKey != null}"
+ )
+
if (storedKey != null) {
val expectedToken = BleTransportBridge.deriveAuthToken(storedKey)
val receivedTokenStr = String(token, Charsets.UTF_8)
-
+
Log.d(TAG, "Expected token: $expectedToken")
Log.d(TAG, "Received token: $receivedTokenStr")
@@ -405,16 +526,25 @@ class BleGattServer(private val context: Context) {
Log.i(TAG, "BLE Auth Success!")
isAuthenticated = true
_connectionState.value = BleConnectionState.AUTHENTICATED
- sendNotification(BleConstants.CHAR_AUTH_RESULT, byteArrayOf(BleConstants.AUTH_SUCCESS))
+ sendNotification(
+ BleConstants.CHAR_AUTH_RESULT,
+ byteArrayOf(BleConstants.AUTH_SUCCESS)
+ )
BleTransportBridge.sendDeviceName()
startHeartbeat()
} else {
Log.w(TAG, "BLE Auth Failed! Token mismatch.")
- sendNotification(BleConstants.CHAR_AUTH_RESULT, byteArrayOf(BleConstants.AUTH_FAILED))
+ sendNotification(
+ BleConstants.CHAR_AUTH_RESULT,
+ byteArrayOf(BleConstants.AUTH_FAILED)
+ )
}
} else {
Log.w(TAG, "BLE Auth Failed! No symmetric key found for last connected device.")
- sendNotification(BleConstants.CHAR_AUTH_RESULT, byteArrayOf(BleConstants.AUTH_FAILED))
+ sendNotification(
+ BleConstants.CHAR_AUTH_RESULT,
+ byteArrayOf(BleConstants.AUTH_FAILED)
+ )
}
}
}
@@ -425,7 +555,8 @@ class BleGattServer(private val context: Context) {
while (isActive && isAuthenticated) {
delay(5000)
if (connectedDevices.isNotEmpty()) {
- val level = com.sameerasw.airsync.utils.DeviceInfoUtil.getBatteryInfo(context).level
+ val level =
+ com.sameerasw.airsync.utils.DeviceInfoUtil.getBatteryInfo(context).level
sendNotification(BleConstants.CHAR_BATTERY_LEVEL, byteArrayOf(level.toByte()))
}
}
@@ -469,7 +600,7 @@ class BleGattServer(private val context: Context) {
val isMuted = parts[4] == "1"
val likeStatus = parts[5]
val albumArt = if (parts.size >= 7) parts[6] else null
-
+
Log.d(TAG, "Received Mac media state via BLE: $title by $artist (Playing: $isPlaying)")
MacDeviceStatusManager.updateMusicStatus(
context, isPlaying, title, artist, volume, isMuted, likeStatus, albumArt
@@ -494,11 +625,11 @@ class BleGattServer(private val context: Context) {
*/
fun sendNotification(characteristicUuid: UUID, data: ByteArray) {
if (connectedDevices.isEmpty()) return
-
+
// Characteristic level queue to ensure order
val queue = characteristicQueues.getOrPut(characteristicUuid) { ConcurrentLinkedQueue() }
queue.add(data)
-
+
if (isSending[characteristicUuid] != true) {
processNextInQueue(characteristicUuid)
}
@@ -506,18 +637,18 @@ class BleGattServer(private val context: Context) {
fun sendChunkedNotification(characteristicUuid: UUID, payload: String) {
if (connectedDevices.isEmpty()) return
-
+
// Truncate notification text to conserve BLE bandwidth
val truncatedPayload = if (characteristicUuid == BleConstants.CHAR_NOTIFICATION_DATA) {
- payload.take(500)
+ payload.take(500)
} else payload
val mtu = negotiatedMtu
val chunks = BleChunkUtil.splitIntoChunks(truncatedPayload, mtu)
-
+
val queue = characteristicQueues.getOrPut(characteristicUuid) { ConcurrentLinkedQueue() }
chunks.forEach { queue.add(it) }
-
+
if (isSending[characteristicUuid] != true) {
processNextInQueue(characteristicUuid)
}
@@ -535,12 +666,12 @@ class BleGattServer(private val context: Context) {
private fun processNextInQueue(uuid: UUID) {
val queue = characteristicQueues[uuid] ?: return
val data = queue.poll() ?: return
-
+
isSending[uuid] = true
-
+
val characteristic = findCharacteristic(uuid) ?: return
characteristic.value = data
-
+
connectedDevices.forEach { device ->
gattServer?.notifyCharacteristicChanged(device, characteristic, false)
}
@@ -554,19 +685,32 @@ class BleGattServer(private val context: Context) {
}
private fun createReadCharacteristic(uuid: UUID): BluetoothGattCharacteristic {
- return BluetoothGattCharacteristic(uuid, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ)
+ return BluetoothGattCharacteristic(
+ uuid,
+ BluetoothGattCharacteristic.PROPERTY_READ,
+ BluetoothGattCharacteristic.PERMISSION_READ
+ )
}
private fun createWriteCharacteristic(uuid: UUID): BluetoothGattCharacteristic {
- return BluetoothGattCharacteristic(uuid,
- BluetoothGattCharacteristic.PROPERTY_WRITE or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
- BluetoothGattCharacteristic.PERMISSION_WRITE)
+ return BluetoothGattCharacteristic(
+ uuid,
+ BluetoothGattCharacteristic.PROPERTY_WRITE or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE
+ )
}
private fun createNotifyCharacteristic(uuid: UUID): BluetoothGattCharacteristic {
- val char = BluetoothGattCharacteristic(uuid, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ)
+ val char = BluetoothGattCharacteristic(
+ uuid,
+ BluetoothGattCharacteristic.PROPERTY_NOTIFY,
+ BluetoothGattCharacteristic.PERMISSION_READ
+ )
// Add CCCD for notification support
- val configDescriptor = BluetoothGattDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE)
+ val configDescriptor = BluetoothGattDescriptor(
+ UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
+ BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
+ )
char.addDescriptor(configDescriptor)
return char
}
diff --git a/app/src/main/java/com/sameerasw/airsync/data/ble/BleTransportBridge.kt b/app/src/main/java/com/sameerasw/airsync/data/ble/BleTransportBridge.kt
index 07fe68f3..b11a7d9c 100644
--- a/app/src/main/java/com/sameerasw/airsync/data/ble/BleTransportBridge.kt
+++ b/app/src/main/java/com/sameerasw/airsync/data/ble/BleTransportBridge.kt
@@ -1,17 +1,17 @@
package com.sameerasw.airsync.data.ble
import android.util.Log
-import com.sameerasw.airsync.domain.model.BatteryInfo
import com.sameerasw.airsync.domain.model.AudioInfo
-import java.security.MessageDigest
-import java.util.*
+import com.sameerasw.airsync.domain.model.BatteryInfo
import com.sameerasw.airsync.utils.CallControlUtil
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
+import java.security.MessageDigest
+import java.util.Base64
object BleTransportBridge {
private const val TAG = "BleTransportBridge"
-
+
private var gattServer: BleGattServer? = null
fun initialize(server: BleGattServer) {
@@ -51,7 +51,7 @@ object BleTransportBridge {
audio.likeStatus,
"" // Avoid sending heavy base64 art over BLE to conserve bandwidth
).joinToString(BleConstants.DELIMITER)
-
+
gattServer?.sendChunkedNotification(BleConstants.CHAR_MEDIA_STATE, payload)
}
@@ -60,7 +60,7 @@ object BleTransportBridge {
if (isDnd) "1" else "0",
if (isPowerSave) "1" else "0"
).joinToString(BleConstants.DELIMITER)
-
+
gattServer?.sendNotification(BleConstants.CHAR_SYSTEM_STATE, payload.toByteArray())
}
@@ -84,14 +84,26 @@ object BleTransportBridge {
when {
action == "playPause" -> com.sameerasw.airsync.utils.MediaControlUtil.playPause(context)
action == "next" -> com.sameerasw.airsync.utils.MediaControlUtil.skipNext(context)
- action == "previous" -> com.sameerasw.airsync.utils.MediaControlUtil.skipPrevious(context)
+ action == "previous" -> com.sameerasw.airsync.utils.MediaControlUtil.skipPrevious(
+ context
+ )
+
action == "callAccept" -> CallControlUtil.acceptCall(context)
action == "callDecline" || action == "callEnd" -> CallControlUtil.endCall(context)
-
+
// Volume Controls over BLE
- action == "volumeUp" -> com.sameerasw.airsync.utils.VolumeControlUtil.increaseVolume(context)
- action == "volumeDown" -> com.sameerasw.airsync.utils.VolumeControlUtil.decreaseVolume(context)
- action == "muteToggle" -> com.sameerasw.airsync.utils.VolumeControlUtil.toggleMute(context)
+ action == "volumeUp" -> com.sameerasw.airsync.utils.VolumeControlUtil.increaseVolume(
+ context
+ )
+
+ action == "volumeDown" -> com.sameerasw.airsync.utils.VolumeControlUtil.decreaseVolume(
+ context
+ )
+
+ action == "muteToggle" -> com.sameerasw.airsync.utils.VolumeControlUtil.toggleMute(
+ context
+ )
+
action.startsWith("setVolume|") -> {
val volStr = action.substringAfter("setVolume|")
val vol = volStr.toIntOrNull()
@@ -99,6 +111,7 @@ object BleTransportBridge {
com.sameerasw.airsync.utils.VolumeControlUtil.setVolume(context, vol)
}
}
+
action.startsWith("toggleNotif|") -> {
val parts = action.split("|")
if (parts.size >= 3) {
@@ -107,16 +120,27 @@ object BleTransportBridge {
Log.d(TAG, "Received toggleAppNotif via BLE: pkg=$pkg, state=$state")
kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch {
try {
- val dataStoreManager = com.sameerasw.airsync.data.local.DataStoreManager.getInstance(context)
- val currentApps = dataStoreManager.getNotificationApps().first().toMutableList()
+ val dataStoreManager =
+ com.sameerasw.airsync.data.local.DataStoreManager.getInstance(
+ context
+ )
+ val currentApps =
+ dataStoreManager.getNotificationApps().first().toMutableList()
val idx = currentApps.indexOfFirst { it.packageName == pkg }
if (idx != -1) {
- currentApps[idx] = currentApps[idx].copy(isEnabled = state, lastUpdated = System.currentTimeMillis())
+ currentApps[idx] = currentApps[idx].copy(
+ isEnabled = state,
+ lastUpdated = System.currentTimeMillis()
+ )
dataStoreManager.saveNotificationApps(currentApps)
- Log.d(TAG, "Successfully toggled app notification preference via BLE for $pkg to $state")
+ Log.d(
+ TAG,
+ "Successfully toggled app notification preference via BLE for $pkg to $state"
+ )
} else {
val isSystemApp = try {
- val applicationInfo = context.packageManager.getApplicationInfo(pkg, 0)
+ val applicationInfo =
+ context.packageManager.getApplicationInfo(pkg, 0)
(applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_SYSTEM) != 0
} catch (_: Exception) {
false
@@ -130,11 +154,20 @@ object BleTransportBridge {
)
currentApps.add(newApp)
dataStoreManager.saveNotificationApps(currentApps)
- Log.d(TAG, "Saved new app notification preference via BLE for $pkg to $state")
+ Log.d(
+ TAG,
+ "Saved new app notification preference via BLE for $pkg to $state"
+ )
}
- com.sameerasw.airsync.utils.SyncManager.checkAndSyncDeviceStatus(context, forceSync = true)
+ com.sameerasw.airsync.utils.SyncManager.checkAndSyncDeviceStatus(
+ context,
+ forceSync = true
+ )
} catch (e: Exception) {
- Log.e(TAG, "Error toggling app notification preference via BLE: ${e.message}")
+ Log.e(
+ TAG,
+ "Error toggling app notification preference via BLE: ${e.message}"
+ )
}
}
}
@@ -148,7 +181,10 @@ object BleTransportBridge {
if (parts.size >= 2) {
val id = parts[0]
val actionName = parts[1]
- com.sameerasw.airsync.utils.NotificationDismissalUtil.performNotificationAction(id, actionName)
+ com.sameerasw.airsync.utils.NotificationDismissalUtil.performNotificationAction(
+ id,
+ actionName
+ )
}
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/data/local/DataStoreManager.kt b/app/src/main/java/com/sameerasw/airsync/data/local/DataStoreManager.kt
index a7aad644..1ffdc453 100644
--- a/app/src/main/java/com/sameerasw/airsync/data/local/DataStoreManager.kt
+++ b/app/src/main/java/com/sameerasw/airsync/data/local/DataStoreManager.kt
@@ -95,7 +95,8 @@ class DataStoreManager(private val context: Context) {
private val FILE_ACCESS_ENABLED = booleanPreferencesKey("file_access_enabled")
// Widget preferences
- private val WIDGET_TRANSPARENCY = androidx.datastore.preferences.core.floatPreferencesKey("widget_transparency")
+ private val WIDGET_TRANSPARENCY =
+ androidx.datastore.preferences.core.floatPreferencesKey("widget_transparency")
private val REMOTE_FLIPPED = booleanPreferencesKey("remote_flipped")
@@ -1031,11 +1032,13 @@ class DataStoreManager(private val context: Context) {
context.dataStore.edit { it[BLE_SYNC_ENABLED] = enabled }
}
- fun getBleSyncEnabled(): Flow = context.dataStore.data.map { it[BLE_SYNC_ENABLED] ?: false }
+ fun getBleSyncEnabled(): Flow =
+ context.dataStore.data.map { it[BLE_SYNC_ENABLED] ?: false }
suspend fun setBleAutoConnectEnabled(enabled: Boolean) {
context.dataStore.edit { it[BLE_AUTO_CONNECT_ENABLED] = enabled }
}
- fun getBleAutoConnectEnabled(): Flow = context.dataStore.data.map { it[BLE_AUTO_CONNECT_ENABLED] ?: true }
+ fun getBleAutoConnectEnabled(): Flow =
+ context.dataStore.data.map { it[BLE_AUTO_CONNECT_ENABLED] ?: true }
}
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/ClipboardActionActivity.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/ClipboardActionActivity.kt
index ca00177b..b7f2f7c3 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/ClipboardActionActivity.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/ClipboardActionActivity.kt
@@ -13,25 +13,16 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.rounded.CheckCircle
-import androidx.compose.material.icons.rounded.ContentPaste
import androidx.compose.material.icons.rounded.Error
-import androidx.compose.material.icons.rounded.ReceiptLong
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LoadingIndicator
@@ -47,7 +38,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -62,7 +52,6 @@ import com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel
import com.sameerasw.airsync.ui.theme.AirSyncTheme
import com.sameerasw.airsync.utils.ClipboardSyncManager
import com.sameerasw.airsync.utils.ClipboardUtil
-import com.sameerasw.airsync.utils.DevicePreviewResolver
import com.sameerasw.airsync.utils.ShortcutUtil
import com.sameerasw.airsync.utils.WebSocketUtil
import kotlinx.coroutines.delay
@@ -273,7 +262,7 @@ private fun ClipboardActionScreenContent(
)
// Divider or Space if needed, but spacing is enough
-
+
// Status Icon / Loading Indicator
AnimatedContent(
targetState = uiState,
@@ -309,7 +298,7 @@ private fun ClipboardActionScreenContent(
modifier = Modifier.size(28.dp)
)
}
-
+
else -> {
// Default/Idle icon
val iconPainter = when (shortcutAction) {
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/PermissionsActivity.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/PermissionsActivity.kt
index 3e0ad33e..5251e047 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/PermissionsActivity.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/PermissionsActivity.kt
@@ -29,7 +29,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import com.sameerasw.airsync.presentation.ui.screens.PermissionsScreen
import com.sameerasw.airsync.ui.theme.AirSyncTheme
-import com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel
import com.sameerasw.airsync.utils.PermissionUtil
class PermissionsActivity : ComponentActivity() {
@@ -177,7 +176,7 @@ class PermissionsActivity : ComponentActivity() {
phonePermissionLauncher.launch(Manifest.permission.READ_PHONE_STATE)
}
}
-
+
private fun requestBluetoothPermission() {
if (!PermissionUtil.isBluetoothPermissionsGranted(this)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/QRScannerActivity.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/QRScannerActivity.kt
index 4327ab66..fb2b157a 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/QRScannerActivity.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/activities/QRScannerActivity.kt
@@ -51,7 +51,6 @@ import androidx.core.content.ContextCompat
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
-import com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel
import com.sameerasw.airsync.ui.theme.AirSyncTheme
import com.sameerasw.airsync.utils.HapticUtil
import java.util.concurrent.Executors
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AboutSection.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AboutSection.kt
index 47d9385b..1f7d8202 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AboutSection.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AboutSection.kt
@@ -1,6 +1,10 @@
package com.sameerasw.airsync.presentation.ui.components
import android.content.Intent
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.togetherWith
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -15,10 +19,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -38,13 +39,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
-import androidx.compose.foundation.text.ClickableText
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.sameerasw.airsync.R
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AirSyncFloatingToolbar.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AirSyncFloatingToolbar.kt
index 5ebc9dc6..5e92435a 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AirSyncFloatingToolbar.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/AirSyncFloatingToolbar.kt
@@ -8,11 +8,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingToolbarDefaults
@@ -51,11 +49,11 @@ fun AirSyncFloatingToolbar(
val configuration = LocalConfiguration.current
val fontScale = LocalDensity.current.fontScale
val screenWidth = configuration.screenWidthDp
-
+
// Hide label if font scale is large or screen width is too small
val isLargeFont = fontScale > 1.25f
- val isCompactScreen = screenWidth < 400
-
+ val isCompactScreen = screenWidth < 400
+
val shouldHideLabel = isLargeFont || (isCompactScreen && tabs.size > 3)
HorizontalFloatingToolbar(
@@ -104,7 +102,7 @@ fun AirSyncFloatingToolbar(
),
label = "spacer_width_$index"
)
-
+
// Always render the button, but animate its visibility
if (itemWidth > 0.dp || isSelected) {
IconButton(
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt
index 91ccfc2d..ec2e1b95 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/FloatingMediaPlayer.kt
@@ -1,21 +1,24 @@
package com.sameerasw.airsync.presentation.ui.components
import android.graphics.Bitmap
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.animateContentSize
-import androidx.compose.animation.expandVertically
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.core.exponentialDecay
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.gestures.AnchoredDraggableState
+import androidx.compose.foundation.gestures.DraggableAnchors
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.anchoredDraggable
+import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -34,54 +37,37 @@ import androidx.compose.material3.ButtonGroup
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-import androidx.compose.animation.core.exponentialDecay
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.basicMarquee
-import androidx.compose.foundation.gestures.AnchoredDraggableState
-import androidx.compose.foundation.gestures.DraggableAnchors
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.anchoredDraggable
-import androidx.compose.foundation.gestures.animateTo
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.offset
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
import com.sameerasw.airsync.domain.model.MacMusicInfo
import com.sameerasw.airsync.utils.HapticUtil
import kotlinx.coroutines.launch
-import kotlin.math.roundToInt
enum class DragValue { Collapsed, Expanded }
@@ -120,7 +106,7 @@ fun FloatingMediaPlayer(
val collapsedHeight = 72.dp
val expandedHeight = 280.dp
-
+
val collapsedPx = with(density) { collapsedHeight.toPx() }
val expandedPx = with(density) { expandedHeight.toPx() }
@@ -194,7 +180,7 @@ fun FloatingMediaPlayer(
) {
// Expand Button
IconButton(
- onClick = {
+ onClick = {
scope.launch { anchoredDraggableState.animateTo(DragValue.Expanded) }
},
modifier = Modifier.size(40.dp)
@@ -212,7 +198,8 @@ fun FloatingMediaPlayer(
.weight(1f)
) {
Text(
- text = musicInfo?.title?.takeIf { it.isNotEmpty() } ?: "Nothing Playing",
+ text = musicInfo?.title?.takeIf { it.isNotEmpty() }
+ ?: "Nothing Playing",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
maxLines = 1,
@@ -255,7 +242,7 @@ fun FloatingMediaPlayer(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
- IconButton(onClick = {
+ IconButton(onClick = {
scope.launch { anchoredDraggableState.animateTo(DragValue.Collapsed) }
}) {
Icon(
@@ -264,14 +251,15 @@ fun FloatingMediaPlayer(
tint = MaterialTheme.colorScheme.onSurface
)
}
-
+
// Metadata (Centered)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.weight(1f)
) {
Text(
- text = musicInfo?.title?.takeIf { it.isNotEmpty() } ?: "Nothing Playing",
+ text = musicInfo?.title?.takeIf { it.isNotEmpty() }
+ ?: "Nothing Playing",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
maxLines = 1,
@@ -279,14 +267,15 @@ fun FloatingMediaPlayer(
color = MaterialTheme.colorScheme.onSurface
)
Text(
- text = musicInfo?.artist?.takeIf { it.isNotEmpty() } ?: "from your Mac",
+ text = musicInfo?.artist?.takeIf { it.isNotEmpty() }
+ ?: "from your Mac",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
-
+
Spacer(modifier = Modifier.size(48.dp)) // To balance the chevron
}
@@ -296,8 +285,13 @@ fun FloatingMediaPlayer(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
val durationSeconds = musicInfo.duration / 1000L
- val elapsedSeconds = (currentElapsedTimeMs / 1000L).coerceIn(0L, durationSeconds)
- val elapsedFraction = (currentElapsedTimeMs.toFloat() / musicInfo.duration.toFloat()).coerceIn(0f, 1f)
+ val elapsedSeconds =
+ (currentElapsedTimeMs / 1000L).coerceIn(0L, durationSeconds)
+ val elapsedFraction =
+ (currentElapsedTimeMs.toFloat() / musicInfo.duration.toFloat()).coerceIn(
+ 0f,
+ 1f
+ )
LinearWavyProgressIndicator(
progress = { elapsedFraction },
@@ -342,14 +336,22 @@ fun FloatingMediaPlayer(
content = {
FilledTonalIconButton(
onClick = { onMediaAction("media_prev") },
- modifier = Modifier.weight(0.7f).fillMaxHeight()
+ modifier = Modifier
+ .weight(0.7f)
+ .fillMaxHeight()
) {
- Icon(Icons.Rounded.SkipPrevious, contentDescription = "Previous", modifier = Modifier.size(36.dp))
+ Icon(
+ Icons.Rounded.SkipPrevious,
+ contentDescription = "Previous",
+ modifier = Modifier.size(36.dp)
+ )
}
FilledIconButton(
onClick = { onMediaAction("media_play_pause") },
- modifier = Modifier.weight(1.5f).fillMaxHeight()
+ modifier = Modifier
+ .weight(1.5f)
+ .fillMaxHeight()
) {
Icon(
imageVector = if (musicInfo?.isPlaying == true) Icons.Rounded.Pause else Icons.Rounded.PlayArrow,
@@ -360,9 +362,15 @@ fun FloatingMediaPlayer(
FilledTonalIconButton(
onClick = { onMediaAction("media_next") },
- modifier = Modifier.weight(0.7f).fillMaxHeight()
+ modifier = Modifier
+ .weight(0.7f)
+ .fillMaxHeight()
) {
- Icon(Icons.Rounded.SkipNext, contentDescription = "Next", modifier = Modifier.size(36.dp))
+ Icon(
+ Icons.Rounded.SkipNext,
+ contentDescription = "Next",
+ modifier = Modifier.size(36.dp)
+ )
}
}
)
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/HelpAndGuides.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/HelpAndGuides.kt
index 94084cd3..dea4080e 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/HelpAndGuides.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/HelpAndGuides.kt
@@ -1,14 +1,30 @@
package com.sameerasw.airsync.presentation.ui.components
+import android.content.Intent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
@@ -19,8 +35,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.sameerasw.airsync.R
import com.sameerasw.airsync.presentation.ui.components.sheets.HelpSection
-import android.content.Intent
-import android.net.Uri
@Composable
fun HelpAndGuidesContent() {
@@ -134,7 +148,7 @@ private fun ExpandableHelpSection(section: HelpSection) {
section.links.forEach { (label, url) ->
TextButton(
- onClick = {
+ onClick = {
try {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
context.startActivity(intent)
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/RotatingAppIcon.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/RotatingAppIcon.kt
index 6e9fc475..97c051f6 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/RotatingAppIcon.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/RotatingAppIcon.kt
@@ -10,14 +10,18 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -39,7 +43,8 @@ fun RotatingAppIcon(
val scope = rememberCoroutineScope()
val rotationAnimatable = remember { Animatable(0f) }
- val sensorManager = remember { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager }
+ val sensorManager =
+ remember { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager }
val gravitySensor = remember { sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) }
var accumulatedRotation by remember { mutableFloatStateOf(0f) }
var lastAngle by remember { mutableFloatStateOf(0f) }
@@ -68,7 +73,10 @@ fun RotatingAppIcon(
val tiltMagnitudeSqr = smoothedAx * smoothedAx + smoothedAy * smoothedAy
if (tiltMagnitudeSqr < 2.0f) return
- val targetAngle = (atan2(smoothedAx.toDouble(), smoothedAy.toDouble()) * 180 / PI).toFloat()
+ val targetAngle = (atan2(
+ smoothedAx.toDouble(),
+ smoothedAy.toDouble()
+ ) * 180 / PI).toFloat()
var delta = targetAngle - lastAngle
if (delta > 180) delta -= 360
@@ -107,11 +115,17 @@ fun RotatingAppIcon(
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
- sensorManager.registerListener(listener, gravitySensor, SensorManager.SENSOR_DELAY_UI)
+ sensorManager.registerListener(
+ listener,
+ gravitySensor,
+ SensorManager.SENSOR_DELAY_UI
+ )
}
+
Lifecycle.Event.ON_PAUSE -> {
sensorManager.unregisterListener(listener)
}
+
else -> {}
}
}
@@ -119,7 +133,11 @@ fun RotatingAppIcon(
lifecycleOwner.lifecycle.addObserver(observer)
// Initial register if already resumed
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
- sensorManager.registerListener(listener, gravitySensor, SensorManager.SENSOR_DELAY_UI)
+ sensorManager.registerListener(
+ listener,
+ gravitySensor,
+ SensorManager.SENSOR_DELAY_UI
+ )
}
onDispose {
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/RoundedCardContainer.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/RoundedCardContainer.kt
index d9855a03..36e119c9 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/RoundedCardContainer.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/RoundedCardContainer.kt
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt
index 76a7db2d..87782df6 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt
@@ -7,10 +7,8 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
@@ -18,30 +16,20 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import com.sameerasw.airsync.presentation.ui.components.sheets.AppSelectionSheet
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
@@ -54,12 +42,13 @@ import com.sameerasw.airsync.presentation.ui.components.cards.DefaultTabCard
import com.sameerasw.airsync.presentation.ui.components.cards.DeveloperModeCard
import com.sameerasw.airsync.presentation.ui.components.cards.DeviceInfoCard
import com.sameerasw.airsync.presentation.ui.components.cards.ExpandNetworkingCard
+import com.sameerasw.airsync.presentation.ui.components.cards.IconToggleItem
import com.sameerasw.airsync.presentation.ui.components.cards.MediaSyncCard
import com.sameerasw.airsync.presentation.ui.components.cards.NotificationSyncCard
import com.sameerasw.airsync.presentation.ui.components.cards.PermissionsCard
import com.sameerasw.airsync.presentation.ui.components.cards.QuickSettingsTilesCard
-import com.sameerasw.airsync.presentation.ui.components.cards.IconToggleItem
import com.sameerasw.airsync.presentation.ui.components.cards.SmartspacerCard
+import com.sameerasw.airsync.presentation.ui.components.sheets.AppSelectionSheet
import com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel
import com.sameerasw.airsync.utils.HapticUtil
import kotlinx.coroutines.CoroutineScope
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/BleSyncCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/BleSyncCard.kt
index 045fb0f1..2843e859 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/BleSyncCard.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/BleSyncCard.kt
@@ -1,33 +1,28 @@
package com.sameerasw.airsync.presentation.ui.components.cards
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Bluetooth
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
-import com.sameerasw.airsync.data.local.DataStoreManager
+import com.sameerasw.airsync.R
import com.sameerasw.airsync.data.ble.BleGattServer
+import com.sameerasw.airsync.data.local.DataStoreManager
import kotlinx.coroutines.launch
-import com.sameerasw.airsync.presentation.ui.components.RoundedCardContainer
-import com.sameerasw.airsync.presentation.ui.components.cards.IconToggleItem
-import com.sameerasw.airsync.R
-
@Composable
fun BleSyncCard(viewModel: com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel) {
val context = LocalContext.current
val dataStoreManager = remember { DataStoreManager.getInstance(context) }
val scope = rememberCoroutineScope()
-
+
val bleEnabled by dataStoreManager.getBleSyncEnabled().collectAsState(initial = false)
val autoConnect by dataStoreManager.getBleAutoConnectEnabled().collectAsState(initial = true)
-
+
val uiState by viewModel.uiState.collectAsState()
val bleState = uiState.bleConnectionState
-
+
val statusText = when (bleState) {
BleGattServer.BleConnectionState.DISCONNECTED -> "For nearby connection"
BleGattServer.BleConnectionState.ADVERTISING -> "Scanning"
@@ -35,24 +30,24 @@ fun BleSyncCard(viewModel: com.sameerasw.airsync.presentation.viewmodel.AirSyncV
BleGattServer.BleConnectionState.AUTHENTICATED -> "Connected"
}
- IconToggleItem(
- iconRes = R.drawable.rounded_bluetooth_24,
- title = "Bluetooth LE Sync",
- description = statusText,
- isChecked = bleEnabled,
- onCheckedChange = {
- scope.launch { dataStoreManager.setBleSyncEnabled(it) }
- }
- )
-
- IconToggleItem(
- iconRes = R.drawable.rounded_bluetooth_searching_24,
- title = "BLE Auto-connect",
- description = "Automatically switch to Bluetooth when Wi-Fi drops",
- isChecked = autoConnect,
- onCheckedChange = {
- scope.launch { dataStoreManager.setBleAutoConnectEnabled(it) }
- },
- enabled = bleEnabled
- )
+ IconToggleItem(
+ iconRes = R.drawable.rounded_bluetooth_24,
+ title = "Bluetooth LE Sync",
+ description = statusText,
+ isChecked = bleEnabled,
+ onCheckedChange = {
+ scope.launch { dataStoreManager.setBleSyncEnabled(it) }
+ }
+ )
+
+ IconToggleItem(
+ iconRes = R.drawable.rounded_bluetooth_searching_24,
+ title = "BLE Auto-connect",
+ description = "Automatically switch to Bluetooth when Wi-Fi drops",
+ isChecked = autoConnect,
+ onCheckedChange = {
+ scope.launch { dataStoreManager.setBleAutoConnectEnabled(it) }
+ },
+ enabled = bleEnabled
+ )
}
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt
index eab5a685..261d438e 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ConnectionStatusCard.kt
@@ -64,140 +64,141 @@ fun ConnectionStatusCard(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
- // 1) Device image at the top (only when connected)
- if (isConnected) {
- val previewRes = DevicePreviewResolver.getPreviewRes(connectedDevice)
- Image(
- painter = painterResource(id = previewRes),
- contentDescription = "Connected Mac preview",
- modifier = Modifier.fillMaxWidth(0.75f),
- contentScale = ContentScale.Fit,
- colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(MaterialTheme.colorScheme.primary)
- )
- }
-
- // 2) Device info block (when connected)
- if (isConnected && connectedDevice != null) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- "${connectedDevice.name}",
- style = MaterialTheme.typography.headlineSmall,
- color = MaterialTheme.colorScheme.onSurface,
- modifier = Modifier.weight(1f)
+ // 1) Device image at the top (only when connected)
+ if (isConnected) {
+ val previewRes = DevicePreviewResolver.getPreviewRes(connectedDevice)
+ Image(
+ painter = painterResource(id = previewRes),
+ contentDescription = "Connected Mac preview",
+ modifier = Modifier.fillMaxWidth(0.75f),
+ contentScale = ContentScale.Fit,
+ colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(MaterialTheme.colorScheme.primary)
)
+ }
- Surface(
- shape = RoundedCornerShape(8.dp),
- color = if (connectedDevice.isPlus)
- MaterialTheme.colorScheme.primaryContainer
- else
- MaterialTheme.colorScheme.surfaceVariant,
- modifier = Modifier.padding(start = 16.dp)
+ // 2) Device info block (when connected)
+ if (isConnected && connectedDevice != null) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = if (connectedDevice.isPlus) "PLUS" else "FREE",
- modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
- style = MaterialTheme.typography.labelSmall,
- color = if (connectedDevice.isPlus)
- MaterialTheme.colorScheme.onPrimaryContainer
- else
- MaterialTheme.colorScheme.onSurfaceVariant
+ "${connectedDevice.name}",
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.weight(1f)
)
- }
- }
- FlowRow(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- val ips = uiState.ipAddress.split(",").map { it.trim() }.filter { it.isNotEmpty() }
- ips.forEach { ip ->
- val isActive = ip == uiState.activeIp
Surface(
- shape = RoundedCornerShape(12.dp),
- color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
- modifier = Modifier.animateContentSize()
+ shape = RoundedCornerShape(8.dp),
+ color = if (connectedDevice.isPlus)
+ MaterialTheme.colorScheme.primaryContainer
+ else
+ MaterialTheme.colorScheme.surfaceVariant,
+ modifier = Modifier.padding(start = 16.dp)
) {
Text(
- text = "$ip:${connectedDevice.port}",
+ text = if (connectedDevice.isPlus) "PLUS" else "FREE",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
- style = MaterialTheme.typography.labelMedium,
- color = if (isActive) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant
+ style = MaterialTheme.typography.labelSmall,
+ color = if (connectedDevice.isPlus)
+ MaterialTheme.colorScheme.onPrimaryContainer
+ else
+ MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
- }
- }
- // 3) Connection status row last
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = if (isConnected) 0.dp else 8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- val statusText = when {
- isConnecting -> "Connecting..."
- isConnected -> "Syncing"
- else -> "Disconnected"
- }
-
- if (isConnecting) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) { LoadingIndicator() }
+ FlowRow(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ val ips =
+ uiState.ipAddress.split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ ips.forEach { ip ->
+ val isActive = ip == uiState.activeIp
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
+ modifier = Modifier.animateContentSize()
+ ) {
+ Text(
+ text = "$ip:${connectedDevice.port}",
+ modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
+ style = MaterialTheme.typography.labelMedium,
+ color = if (isActive) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
}
- if (isConnected) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- SlowlyRotatingAppIcon(
- modifier = Modifier.size(54.dp)
- )
+ // 3) Connection status row last
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = if (isConnected) 0.dp else 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val statusText = when {
+ isConnecting -> "Connecting..."
+ isConnected -> "Syncing"
+ else -> "Disconnected"
}
- } else if (!isConnecting) {
- Icon(
- painter = painterResource(id = com.sameerasw.airsync.R.drawable.rounded_devices_off_24),
- contentDescription = "Disconnected",
- modifier = Modifier.padding(end = 8.dp),
- tint = MaterialTheme.colorScheme.error
- )
- }
- Text(
- text = statusText,
- style = MaterialTheme.typography.bodyLarge,
- modifier = Modifier.weight(1f)
- )
+ if (isConnecting) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) { LoadingIndicator() }
+ }
- if (isConnected) {
- Button(
- onClick = {
- HapticUtil.performClick(haptics)
- onDisconnect()
- },
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.surfaceBright,
- contentColor = MaterialTheme.colorScheme.onSecondaryContainer
- ),
- modifier = Modifier.height(48.dp)
- ) {
+ if (isConnected) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ SlowlyRotatingAppIcon(
+ modifier = Modifier.size(54.dp)
+ )
+ }
+ } else if (!isConnecting) {
Icon(
painter = painterResource(id = com.sameerasw.airsync.R.drawable.rounded_devices_off_24),
- contentDescription = null,
- modifier = Modifier.size(18.dp)
- )
- Spacer(modifier = Modifier.size(6.dp))
- Text(
- text = "Disconnect",
- style = MaterialTheme.typography.labelLarge,
- maxLines = 1
+ contentDescription = "Disconnected",
+ modifier = Modifier.padding(end = 8.dp),
+ tint = MaterialTheme.colorScheme.error
)
}
+
+ Text(
+ text = statusText,
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.weight(1f)
+ )
+
+ if (isConnected) {
+ Button(
+ onClick = {
+ HapticUtil.performClick(haptics)
+ onDisconnect()
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
+ contentColor = MaterialTheme.colorScheme.onSecondaryContainer
+ ),
+ modifier = Modifier.height(48.dp)
+ ) {
+ Icon(
+ painter = painterResource(id = com.sameerasw.airsync.R.drawable.rounded_devices_off_24),
+ contentDescription = null,
+ modifier = Modifier.size(18.dp)
+ )
+ Spacer(modifier = Modifier.size(6.dp))
+ Text(
+ text = "Disconnect",
+ style = MaterialTheme.typography.labelLarge,
+ maxLines = 1
+ )
+ }
+ }
}
}
}
-}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DeveloperModeCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DeveloperModeCard.kt
index a7494d83..1a845e02 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DeveloperModeCard.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DeveloperModeCard.kt
@@ -57,176 +57,176 @@ fun DeveloperModeCard(
) {
Column(modifier = Modifier.fillMaxWidth()) {
IconToggleItem(
- iconRes = R.drawable.rounded_troubleshoot_24,
- title = "Developer Mode",
- isChecked = isDeveloperMode,
- onCheckedChange = onToggleDeveloperMode
- )
-
- if (isDeveloperMode) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- Text(
- "Test Functions",
- style = MaterialTheme.typography.titleSmall,
- color = MaterialTheme.colorScheme.primary
- )
-
- Button(
- onClick = {
- HapticUtil.performClick(haptics)
- onSendDeviceInfo()
- },
- modifier = Modifier.fillMaxWidth(),
- enabled = !isLoading
- ) {
- Text("Send Device Info")
- }
+ iconRes = R.drawable.rounded_troubleshoot_24,
+ title = "Developer Mode",
+ isChecked = isDeveloperMode,
+ onCheckedChange = onToggleDeveloperMode
+ )
- Button(
- onClick = {
- HapticUtil.performClick(haptics)
- onSendNotification()
- },
- modifier = Modifier.fillMaxWidth(),
- enabled = !isLoading
+ if (isDeveloperMode) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- Text("Send Test Notification")
- }
+ Text(
+ "Test Functions",
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary
+ )
- Button(
- onClick = {
- HapticUtil.performClick(haptics)
- onSendDeviceStatus()
- },
- modifier = Modifier.fillMaxWidth(),
- enabled = !isLoading
- ) {
- Text("Send Device Status")
- }
+ Button(
+ onClick = {
+ HapticUtil.performClick(haptics)
+ onSendDeviceInfo()
+ },
+ modifier = Modifier.fillMaxWidth(),
+ enabled = !isLoading
+ ) {
+ Text("Send Device Info")
+ }
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = {
HapticUtil.performClick(haptics)
- onExportData()
+ onSendNotification()
},
- modifier = Modifier.weight(1f),
+ modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
- Text("Export Data")
+ Text("Send Test Notification")
}
Button(
onClick = {
HapticUtil.performClick(haptics)
- onImportData()
+ onSendDeviceStatus()
},
- modifier = Modifier.weight(1f),
+ modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
- Text("Import Data")
+ Text("Send Device Status")
}
- }
- Button(
- onClick = {
- HapticUtil.performClick(haptics)
- onResetOnboarding()
- },
- modifier = Modifier.fillMaxWidth(),
- enabled = !isLoading
- ) {
- Text("Reset Onboarding")
- }
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Button(
+ onClick = {
+ HapticUtil.performClick(haptics)
+ onExportData()
+ },
+ modifier = Modifier.weight(1f),
+ enabled = !isLoading
+ ) {
+ Text("Export Data")
+ }
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- "Icons",
- style = MaterialTheme.typography.titleSmall,
- color = MaterialTheme.colorScheme.primary
- )
+ Button(
+ onClick = {
+ HapticUtil.performClick(haptics)
+ onImportData()
+ },
+ modifier = Modifier.weight(1f),
+ enabled = !isLoading
+ ) {
+ Text("Import Data")
+ }
+ }
- Button(
- onClick = {
- HapticUtil.performClick(haptics)
- onManualSyncIcons()
- },
- modifier = Modifier.fillMaxWidth(),
- enabled = isConnected && !isIconSyncLoading
- ) {
- if (isIconSyncLoading) {
- CircularProgressIndicator(
- modifier = Modifier.width(16.dp),
- strokeWidth = 2.dp,
- color = MaterialTheme.colorScheme.onPrimary
- )
- Spacer(modifier = Modifier.width(8.dp))
+ Button(
+ onClick = {
+ HapticUtil.performClick(haptics)
+ onResetOnboarding()
+ },
+ modifier = Modifier.fillMaxWidth(),
+ enabled = !isLoading
+ ) {
+ Text("Reset Onboarding")
}
- Text(if (isIconSyncLoading) "Syncing Icons..." else "Sync App Icons")
- }
- AnimatedVisibility(
- visible = iconSyncMessage.isNotEmpty(),
- enter = expandVertically() + fadeIn(),
- exit = shrinkVertically() + fadeOut()
- ) {
- Card(
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ "Icons",
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary
+ )
+
+ Button(
+ onClick = {
+ HapticUtil.performClick(haptics)
+ onManualSyncIcons()
+ },
modifier = Modifier.fillMaxWidth(),
- shape = MaterialTheme.shapes.extraSmall,
- colors = CardDefaults.cardColors(
- containerColor = if (iconSyncMessage.contains("Successfully"))
- MaterialTheme.colorScheme.primaryContainer
- else MaterialTheme.colorScheme.errorContainer
- )
+ enabled = isConnected && !isIconSyncLoading
) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(12.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = iconSyncMessage,
- modifier = Modifier.weight(1f),
- style = MaterialTheme.typography.bodySmall,
- color = if (iconSyncMessage.contains("Successfully"))
- MaterialTheme.colorScheme.onPrimaryContainer
- else MaterialTheme.colorScheme.onErrorContainer
+ if (isIconSyncLoading) {
+ CircularProgressIndicator(
+ modifier = Modifier.width(16.dp),
+ strokeWidth = 2.dp,
+ color = MaterialTheme.colorScheme.onPrimary
)
- TextButton(onClick = {
- HapticUtil.performClick(haptics)
- onClearIconSyncMessage()
- }) {
- Text("Dismiss", style = MaterialTheme.typography.labelMedium)
+ Spacer(modifier = Modifier.width(8.dp))
+ }
+ Text(if (isIconSyncLoading) "Syncing Icons..." else "Sync App Icons")
+ }
+
+ AnimatedVisibility(
+ visible = iconSyncMessage.isNotEmpty(),
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = MaterialTheme.shapes.extraSmall,
+ colors = CardDefaults.cardColors(
+ containerColor = if (iconSyncMessage.contains("Successfully"))
+ MaterialTheme.colorScheme.primaryContainer
+ else MaterialTheme.colorScheme.errorContainer
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = iconSyncMessage,
+ modifier = Modifier.weight(1f),
+ style = MaterialTheme.typography.bodySmall,
+ color = if (iconSyncMessage.contains("Successfully"))
+ MaterialTheme.colorScheme.onPrimaryContainer
+ else MaterialTheme.colorScheme.onErrorContainer
+ )
+ TextButton(onClick = {
+ HapticUtil.performClick(haptics)
+ onClearIconSyncMessage()
+ }) {
+ Text("Dismiss", style = MaterialTheme.typography.labelMedium)
+ }
}
}
}
- }
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- "Crash Reporting",
- style = MaterialTheme.typography.titleSmall,
- color = MaterialTheme.colorScheme.primary
- )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ "Crash Reporting",
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary
+ )
- Button(
- onClick = {
- HapticUtil.performClick(haptics)
- throw RuntimeException("Test Crash from Developer Options")
- },
- modifier = Modifier.fillMaxWidth()
- ) {
- Text("Simulate Crash")
+ Button(
+ onClick = {
+ HapticUtil.performClick(haptics)
+ throw RuntimeException("Test Crash from Developer Options")
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("Simulate Crash")
+ }
}
}
}
}
-}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DeviceInfoCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DeviceInfoCard.kt
index a4523d51..deef47a4 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DeviceInfoCard.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/DeviceInfoCard.kt
@@ -33,19 +33,19 @@ fun DeviceInfoCard(
.fillMaxWidth()
.padding(16.dp)
) {
- Text("My Android", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- Text("Local IP: $localIp", style = MaterialTheme.typography.bodyMedium)
+ Text("My Android", style = MaterialTheme.typography.titleMedium)
+ Spacer(modifier = Modifier.height(8.dp))
+ Text("Local IP: $localIp", style = MaterialTheme.typography.bodyMedium)
- Spacer(modifier = Modifier.height(8.dp))
- OutlinedTextField(
- value = deviceName,
- onValueChange = onDeviceNameChange,
- label = { Text("Device Name") },
- modifier = Modifier.fillMaxWidth(),
- shape = MaterialTheme.shapes.medium,
- )
+ Spacer(modifier = Modifier.height(8.dp))
+ OutlinedTextField(
+ value = deviceName,
+ onValueChange = onDeviceNameChange,
+ label = { Text("Device Name") },
+ modifier = Modifier.fillMaxWidth(),
+ shape = MaterialTheme.shapes.medium,
+ )
+ }
}
}
-}
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/IconToggleItem.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/IconToggleItem.kt
index 86de817a..904fbce6 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/IconToggleItem.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/IconToggleItem.kt
@@ -73,7 +73,9 @@ fun IconToggleItem(
painter = painterResource(id = iconRes),
contentDescription = title,
modifier = Modifier.size(24.dp),
- tint = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
+ tint = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(
+ alpha = 0.38f
+ )
)
}
@@ -84,13 +86,17 @@ fun IconToggleItem(
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
- color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
+ color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy(
+ alpha = 0.38f
+ )
)
if (description != null) {
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
- color = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),
+ color = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(
+ alpha = 0.38f
+ ),
modifier = Modifier.padding(top = 2.dp)
)
}
@@ -109,10 +115,14 @@ fun IconToggleItem(
)
} else if (onClick != null && !showToggle) {
Icon(
- painter = painterResource(id = trailingIcon ?: R.drawable.rounded_keyboard_arrow_right_24),
+ painter = painterResource(
+ id = trailingIcon ?: R.drawable.rounded_keyboard_arrow_right_24
+ ),
contentDescription = null,
modifier = Modifier.size(24.dp),
- tint = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
+ tint = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(
+ alpha = 0.38f
+ )
)
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt
index 0eebfbe5..2c512408 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/LastConnectedDeviceCard.kt
@@ -60,94 +60,94 @@ fun LastConnectedDeviceCard(
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
- Text(
- "Last Connected Device",
- style = MaterialTheme.typography.titleLarge,
- color = MaterialTheme.colorScheme.primary
- )
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceAround,
- verticalAlignment = Alignment.CenterVertically
- ) {
- val previewRes = DevicePreviewResolver.getPreviewRes(device)
- Image(
- painter = painterResource(id = previewRes),
- contentDescription = "Connected Mac preview",
- modifier = Modifier.fillMaxWidth(0.45f),
- contentScale = ContentScale.Fit,
- colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(MaterialTheme.colorScheme.primary)
+ Text(
+ "Last Connected Device",
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.primary
)
- }
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column(modifier = Modifier.weight(1f)) {
- Text(
- "${device.name}",
- style = MaterialTheme.typography.headlineSmall,
- color = MaterialTheme.colorScheme.onSurface
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceAround,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val previewRes = DevicePreviewResolver.getPreviewRes(device)
+ Image(
+ painter = painterResource(id = previewRes),
+ contentDescription = "Connected Mac preview",
+ modifier = Modifier.fillMaxWidth(0.45f),
+ contentScale = ContentScale.Fit,
+ colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(MaterialTheme.colorScheme.primary)
)
+ }
- val lastConnectedTime = remember(device.lastConnected) {
- val currentTime = System.currentTimeMillis()
- val diffMinutes = (currentTime - device.lastConnected) / (1000 * 60)
- when {
- diffMinutes < 1 -> "Just now"
- diffMinutes < 60 -> "${diffMinutes}m ago"
- diffMinutes < 1440 -> "${diffMinutes / 60}h ago"
- else -> "${diffMinutes / 1440}d ago"
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ "${device.name}",
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ val lastConnectedTime = remember(device.lastConnected) {
+ val currentTime = System.currentTimeMillis()
+ val diffMinutes = (currentTime - device.lastConnected) / (1000 * 60)
+ when {
+ diffMinutes < 1 -> "Just now"
+ diffMinutes < 60 -> "${diffMinutes}m ago"
+ diffMinutes < 1440 -> "${diffMinutes / 60}h ago"
+ else -> "${diffMinutes / 1440}d ago"
+ }
}
+ Text(
+ "Last seen $lastConnectedTime",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
}
- Text(
- "Last seen $lastConnectedTime",
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- Surface(
- shape = RoundedCornerShape(8.dp),
- color = if (device.isPlus)
- MaterialTheme.colorScheme.primaryContainer
- else
- MaterialTheme.colorScheme.surfaceVariant,
- modifier = Modifier.padding(start = 8.dp)
- ) {
- Text(
- text = if (device.isPlus) "PLUS" else "FREE",
- modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
- style = MaterialTheme.typography.labelSmall,
+ Surface(
+ shape = RoundedCornerShape(8.dp),
color = if (device.isPlus)
- MaterialTheme.colorScheme.onPrimaryContainer
+ MaterialTheme.colorScheme.primaryContainer
else
- MaterialTheme.colorScheme.onSurfaceVariant
- )
+ MaterialTheme.colorScheme.surfaceVariant,
+ modifier = Modifier.padding(start = 8.dp)
+ ) {
+ Text(
+ text = if (device.isPlus) "PLUS" else "FREE",
+ modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
+ style = MaterialTheme.typography.labelSmall,
+ color = if (device.isPlus)
+ MaterialTheme.colorScheme.onPrimaryContainer
+ else
+ MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
}
- }
- Button(
- onClick = {
- HapticUtil.performClick(haptics)
- onQuickConnect()
- },
- modifier = Modifier
- .fillMaxWidth()
- .requiredHeight(48.dp)
- ) {
- Icon(
- painter = painterResource(id = com.sameerasw.airsync.R.drawable.rounded_sync_desktop_24),
- contentDescription = "Quick connect",
- modifier = Modifier.size(18.dp)
- )
- Spacer(modifier = Modifier.width(8.dp))
- Text("Quick Connect")
+ Button(
+ onClick = {
+ HapticUtil.performClick(haptics)
+ onQuickConnect()
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .requiredHeight(48.dp)
+ ) {
+ Icon(
+ painter = painterResource(id = com.sameerasw.airsync.R.drawable.rounded_sync_desktop_24),
+ contentDescription = "Quick connect",
+ modifier = Modifier.size(18.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text("Quick Connect")
+ }
}
- }
IconToggleItem(
@@ -168,5 +168,5 @@ fun LastConnectedDeviceCard(
onDismissRequest = { showBottomSheet = false }
)
}
-}
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ManualConnectionCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ManualConnectionCard.kt
index 2ec23551..9e1185c2 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ManualConnectionCard.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/ManualConnectionCard.kt
@@ -57,96 +57,96 @@ fun ManualConnectionCard(
)
) {
Column(modifier = Modifier.fillMaxWidth()) {
- IconToggleItem(
- iconRes = R.drawable.rounded_devices_24,
- title = "Manual Connection",
- description = if (expanded) "Hide connection details" else "Enter connection details manually",
- showToggle = false,
- onClick = {
- HapticUtil.performLightTick(haptics)
- expanded = !expanded
- },
- trailingIcon = if (expanded) R.drawable.outline_expand_circle_up_24 else R.drawable.outline_expand_circle_down_24
- )
+ IconToggleItem(
+ iconRes = R.drawable.rounded_devices_24,
+ title = "Manual Connection",
+ description = if (expanded) "Hide connection details" else "Enter connection details manually",
+ showToggle = false,
+ onClick = {
+ HapticUtil.performLightTick(haptics)
+ expanded = !expanded
+ },
+ trailingIcon = if (expanded) R.drawable.outline_expand_circle_up_24 else R.drawable.outline_expand_circle_down_24
+ )
- AnimatedVisibility(visible = expanded) {
- Column(
- verticalArrangement = Arrangement.spacedBy(8.dp),
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
- ) {
- if (onQrScanClick != null) {
+ AnimatedVisibility(visible = expanded) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
+ ) {
+ if (onQrScanClick != null) {
+ Button(
+ onClick = {
+ HapticUtil.performClick(haptics)
+ onQrScanClick()
+ },
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.rounded_qr_code_scanner_24),
+ contentDescription = "Scan QR Code",
+ modifier = Modifier
+ .size(20.dp)
+ .padding(end = 8.dp)
+ )
+ Text("Scan QR Code")
+ }
+ }
+ OutlinedTextField(
+ value = uiState.ipAddress,
+ onValueChange = onIpChange,
+ label = { Text("IP Address") },
+ modifier = Modifier.fillMaxWidth(),
+ shape = MaterialTheme.shapes.medium,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
+ )
+ OutlinedTextField(
+ value = uiState.port,
+ onValueChange = onPortChange,
+ label = { Text("Port") },
+ modifier = Modifier.fillMaxWidth(),
+ shape = MaterialTheme.shapes.medium,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
+ )
+ OutlinedTextField(
+ value = uiState.manualPcName,
+ onValueChange = onPcNameChange,
+ label = { Text("PC Name (Optional)") },
+ modifier = Modifier.fillMaxWidth(),
+ shape = MaterialTheme.shapes.medium,
+ )
+ OutlinedTextField(
+ value = uiState.symmetricKey ?: "",
+ onValueChange = onSymmetricKeyChange,
+ label = { Text("Encryption Key") },
+ modifier = Modifier.fillMaxWidth(),
+ shape = MaterialTheme.shapes.medium,
+ )
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text("AirSync+", color = MaterialTheme.colorScheme.onSurface)
+ Spacer(Modifier.weight(1f))
+ Switch(
+ checked = uiState.manualIsPlus,
+ onCheckedChange = { enabled ->
+ if (enabled) HapticUtil.performToggleOn(haptics) else HapticUtil.performToggleOff(
+ haptics
+ )
+ onIsPlusChange(enabled)
+ }
+ )
+ }
Button(
onClick = {
HapticUtil.performClick(haptics)
- onQrScanClick()
+ onConnect()
},
modifier = Modifier.fillMaxWidth(),
) {
- Icon(
- painter = painterResource(id = R.drawable.rounded_qr_code_scanner_24),
- contentDescription = "Scan QR Code",
- modifier = Modifier
- .size(20.dp)
- .padding(end = 8.dp)
- )
- Text("Scan QR Code")
+ Text("Connect")
}
}
- OutlinedTextField(
- value = uiState.ipAddress,
- onValueChange = onIpChange,
- label = { Text("IP Address") },
- modifier = Modifier.fillMaxWidth(),
- shape = MaterialTheme.shapes.medium,
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
- )
- OutlinedTextField(
- value = uiState.port,
- onValueChange = onPortChange,
- label = { Text("Port") },
- modifier = Modifier.fillMaxWidth(),
- shape = MaterialTheme.shapes.medium,
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
- )
- OutlinedTextField(
- value = uiState.manualPcName,
- onValueChange = onPcNameChange,
- label = { Text("PC Name (Optional)") },
- modifier = Modifier.fillMaxWidth(),
- shape = MaterialTheme.shapes.medium,
- )
- OutlinedTextField(
- value = uiState.symmetricKey ?: "",
- onValueChange = onSymmetricKeyChange,
- label = { Text("Encryption Key") },
- modifier = Modifier.fillMaxWidth(),
- shape = MaterialTheme.shapes.medium,
- )
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text("AirSync+", color = MaterialTheme.colorScheme.onSurface)
- Spacer(Modifier.weight(1f))
- Switch(
- checked = uiState.manualIsPlus,
- onCheckedChange = { enabled ->
- if (enabled) HapticUtil.performToggleOn(haptics) else HapticUtil.performToggleOff(
- haptics
- )
- onIsPlusChange(enabled)
- }
- )
- }
- Button(
- onClick = {
- HapticUtil.performClick(haptics)
- onConnect()
- },
- modifier = Modifier.fillMaxWidth(),
- ) {
- Text("Connect")
- }
}
}
}
}
-}
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/QuickSettingsTilesCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/QuickSettingsTilesCard.kt
index 26d0c5d6..8df166ea 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/QuickSettingsTilesCard.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/QuickSettingsTilesCard.kt
@@ -18,7 +18,6 @@ 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.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
@@ -110,7 +109,9 @@ private fun TileItem(
Text(
text = if (isAdded) stringResource(R.string.status_added) else stringResource(R.string.status_add),
style = MaterialTheme.typography.bodySmall,
- color = if (isAdded) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
+ color = if (isAdded) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onPrimary.copy(
+ alpha = 0.8f
+ )
)
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/RemoteFunctionsCard.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/RemoteFunctionsCard.kt
index bb85d7bd..316b2ccb 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/RemoteFunctionsCard.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/cards/RemoteFunctionsCard.kt
@@ -1,6 +1,5 @@
package com.sameerasw.airsync.presentation.ui.components.cards
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -9,14 +8,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -44,7 +40,7 @@ fun RemoteFunctionsCard(
modifier = modifier
.fillMaxWidth(),
shape = MaterialTheme.shapes.extraSmall,
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHighest)
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceBright)
) {
Row(
modifier = Modifier
@@ -60,7 +56,7 @@ fun RemoteFunctionsCard(
onRemoteAction("lock_screen")
},
colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.surfaceBright,
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
),
contentPadding = PaddingValues(horizontal = 8.dp),
@@ -88,7 +84,7 @@ fun RemoteFunctionsCard(
onRemoteAction("screensaver")
},
colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.surfaceBright,
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
),
contentPadding = PaddingValues(horizontal = 8.dp),
@@ -116,7 +112,7 @@ fun RemoteFunctionsCard(
onRemoteAction("brightness_down")
},
colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.surfaceBright,
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
),
contentPadding = PaddingValues(0.dp),
@@ -138,7 +134,7 @@ fun RemoteFunctionsCard(
onRemoteAction("brightness_up")
},
colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.surfaceBright,
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
),
contentPadding = PaddingValues(0.dp),
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/dialogs/PermissionDialog.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/dialogs/PermissionDialog.kt
index d3ad30b4..fd242a95 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/dialogs/PermissionDialog.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/dialogs/PermissionDialog.kt
@@ -1,5 +1,6 @@
package com.sameerasw.airsync.presentation.ui.components.dialogs
+import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -21,13 +22,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
-import androidx.compose.ui.platform.LocalContext
-import android.content.Context
import com.sameerasw.airsync.R
enum class PermissionType {
@@ -76,7 +76,8 @@ fun PermissionExplanationDialog(
) {
Column(
modifier = Modifier
- .fillMaxWidth().background(MaterialTheme.colorScheme.surfaceContainerHigh)
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surfaceContainerHigh)
.padding(18.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
@@ -217,7 +218,7 @@ private fun getPermissionInfo(context: Context, permissionType: PermissionType):
whyNeeded = "This permission allows AirSync to detect when your phone is ringing, when you answer, or when a call ends, so it can display a live call status on your Mac. \n\nAirSync NEVER accesses your call audio or records conversations. This is used solely to facilitate the remote call notification feature as a device companion.",
buttonText = "Grant Phone Access"
)
-
+
PermissionType.BLUETOOTH -> PermissionInfo(
title = "Bluetooth Access",
icon = R.drawable.rounded_sync_desktop_24,
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/pickers/CrashReportingPicker.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/pickers/CrashReportingPicker.kt
index 53771a32..446ed7ef 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/pickers/CrashReportingPicker.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/pickers/CrashReportingPicker.kt
@@ -1,9 +1,19 @@
package com.sameerasw.airsync.presentation.ui.components.pickers
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ButtonGroupDefaults
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.ToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/sheets/AppSelectionSheets.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/sheets/AppSelectionSheets.kt
index 48772339..f3d73db3 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/sheets/AppSelectionSheets.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/sheets/AppSelectionSheets.kt
@@ -1,22 +1,42 @@
package com.sameerasw.airsync.presentation.ui.components.sheets
-import android.content.Context
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.sameerasw.airsync.R
@@ -38,11 +58,12 @@ fun AppSelectionSheet(
var showSystemApps by remember { mutableStateOf(false) }
val filteredApps = apps.filter {
- val matchesSearch = searchQuery.isEmpty() || it.appName.contains(searchQuery, ignoreCase = true)
+ val matchesSearch =
+ searchQuery.isEmpty() || it.appName.contains(searchQuery, ignoreCase = true)
val isVisible = !it.isSystemApp || showSystemApps || it.isEnabled
matchesSearch && isVisible
}.distinctBy { it.packageName }
- .sortedWith(compareByDescending { it.isEnabled }.thenBy { it.appName.lowercase() })
+ .sortedWith(compareByDescending { it.isEnabled }.thenBy { it.appName.lowercase() })
ModalBottomSheet(
onDismissRequest = onDismissRequest,
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/sliders/ConfigSliderItem.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/sliders/ConfigSliderItem.kt
index 5a8ca661..59c9dfc0 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/sliders/ConfigSliderItem.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/sliders/ConfigSliderItem.kt
@@ -3,7 +3,9 @@ package com.sameerasw.airsync.presentation.ui.components.sliders
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
@@ -12,19 +14,16 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.unit.dp
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
import com.sameerasw.airsync.R
import com.sameerasw.airsync.utils.HapticUtil
import java.math.BigDecimal
@@ -59,11 +58,13 @@ fun ConfigSliderItem(
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
- color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
+ color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(
+ alpha = 0.38f
+ )
)
-
+
Spacer(modifier = Modifier.height(8.dp))
-
+
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/composables/WelcomeScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/composables/WelcomeScreen.kt
index 447cc645..d03651ed 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/composables/WelcomeScreen.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/composables/WelcomeScreen.kt
@@ -3,25 +3,51 @@ package com.sameerasw.airsync.presentation.ui.composables
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
-import androidx.compose.animation.*
-import androidx.compose.animation.core.*
-import androidx.compose.foundation.*
-import androidx.compose.foundation.gestures.detectDragGestures
-import androidx.compose.foundation.layout.*
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
@@ -39,17 +65,13 @@ import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest
import com.sameerasw.airsync.R
import com.sameerasw.airsync.presentation.ui.components.HelpAndGuidesContent
-import com.sameerasw.airsync.presentation.ui.components.cards.IconToggleItem
-import com.sameerasw.airsync.presentation.ui.components.pickers.CrashReportingPicker
import com.sameerasw.airsync.presentation.ui.components.RotatingAppIcon
import com.sameerasw.airsync.presentation.ui.components.RoundedCardContainer
+import com.sameerasw.airsync.presentation.ui.components.cards.IconToggleItem
import com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel
import com.sameerasw.airsync.ui.theme.GoogleSansFlex
-import com.sameerasw.airsync.utils.HapticUtil
import com.sameerasw.airsync.utils.DeviceInfoUtil
-import kotlinx.coroutines.launch
-import kotlin.math.PI
-import kotlin.math.atan2
+import com.sameerasw.airsync.utils.HapticUtil
enum class OnboardingStep {
WELCOME,
@@ -304,7 +326,7 @@ fun AcknowledgementStepContent(
.padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState())
) {
-
+
Spacer(modifier = Modifier.height(16.dp))
Text(
@@ -439,7 +461,9 @@ fun FeatureIntroStepContent(
Text(
text = "Quick Settings Tiles",
style = MaterialTheme.typography.titleMedium,
- modifier = Modifier.padding(start = 16.dp, bottom = 8.dp).fillMaxWidth(),
+ modifier = Modifier
+ .padding(start = 16.dp, bottom = 8.dp)
+ .fillMaxWidth(),
textAlign = TextAlign.Start
)
@@ -630,7 +654,9 @@ fun PreferencesStepContent(
Text(
text = stringResource(R.string.label_app_settings),
style = MaterialTheme.typography.titleMedium,
- modifier = Modifier.padding(start = 12.dp, bottom = 8.dp).fillMaxWidth(),
+ modifier = Modifier
+ .padding(start = 12.dp, bottom = 8.dp)
+ .fillMaxWidth(),
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Start
)
@@ -678,7 +704,9 @@ fun PreferencesStepContent(
Text(
text = "Connection",
style = MaterialTheme.typography.titleMedium,
- modifier = Modifier.padding(start = 12.dp, bottom = 8.dp).fillMaxWidth(),
+ modifier = Modifier
+ .padding(start = 12.dp, bottom = 8.dp)
+ .fillMaxWidth(),
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Start
)
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/modifiers/ProgressiveBlurModifier.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/modifiers/ProgressiveBlurModifier.kt
index 860c2a2b..594d24dc 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/modifiers/ProgressiveBlurModifier.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/modifiers/ProgressiveBlurModifier.kt
@@ -81,19 +81,20 @@ fun Modifier.progressiveBlur(
showGradientOverlay: Boolean = true
): Modifier = composed {
val overlayColor = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.65f)
-
- val blurModifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && blurRadius > 0f) {
- Modifier.graphicsLayer {
- val shader = RuntimeShader(PROGRESSIVE_BLUR_SKSL)
- shader.setFloatUniform("blurRadius", blurRadius)
- shader.setFloatUniform("height", height)
- shader.setFloatUniform("contentHeight", size.height)
- shader.setIntUniform("isTop", if (direction == BlurDirection.TOP) 1 else 0)
-
- renderEffect = RenderEffect.createRuntimeShaderEffect(shader, "content")
- .asComposeRenderEffect()
- }
- } else Modifier
+
+ val blurModifier =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && blurRadius > 0f) {
+ Modifier.graphicsLayer {
+ val shader = RuntimeShader(PROGRESSIVE_BLUR_SKSL)
+ shader.setFloatUniform("blurRadius", blurRadius)
+ shader.setFloatUniform("height", height)
+ shader.setFloatUniform("contentHeight", size.height)
+ shader.setIntUniform("isTop", if (direction == BlurDirection.TOP) 1 else 0)
+
+ renderEffect = RenderEffect.createRuntimeShaderEffect(shader, "content")
+ .asComposeRenderEffect()
+ }
+ } else Modifier
val gradientModifier = if (showGradientOverlay) {
Modifier.drawWithContent {
@@ -105,6 +106,7 @@ fun Modifier.progressiveBlur(
endY = height
) to height
}
+
BlurDirection.BOTTOM -> {
Brush.verticalGradient(
colors = listOf(Color.Transparent, overlayColor),
@@ -116,5 +118,7 @@ fun Modifier.progressiveBlur(
}
} else Modifier
- this.then(blurModifier).then(gradientModifier)
+ this
+ .then(blurModifier)
+ .then(gradientModifier)
}
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt
index 768ad625..85d03b90 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt
@@ -27,26 +27,18 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
-import androidx.compose.material.icons.filled.ContentPaste
-import androidx.compose.material.icons.filled.Gamepad
import androidx.compose.material.icons.filled.LinkOff
-import androidx.compose.material.icons.filled.Phonelink
import androidx.compose.material.icons.filled.QrCodeScanner
-import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material.icons.outlined.Phonelink
import androidx.compose.material.icons.rounded.ContentPaste
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Gamepad
@@ -60,14 +52,10 @@ import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingToolbarDefaults
-import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
-import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Bottom
-import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
@@ -89,7 +77,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
@@ -102,20 +89,20 @@ import androidx.navigation.compose.rememberNavController
import com.sameerasw.airsync.R
import com.sameerasw.airsync.presentation.ui.activities.QRScannerActivity
import com.sameerasw.airsync.presentation.ui.components.AirSyncFloatingToolbar
+import com.sameerasw.airsync.presentation.ui.components.FloatingMediaPlayer
import com.sameerasw.airsync.presentation.ui.components.RoundedCardContainer
import com.sameerasw.airsync.presentation.ui.components.SettingsView
-import com.sameerasw.airsync.presentation.ui.modifiers.BlurDirection
-import com.sameerasw.airsync.presentation.ui.modifiers.progressiveBlur
-import com.sameerasw.airsync.presentation.ui.components.FloatingMediaPlayer
import com.sameerasw.airsync.presentation.ui.components.cards.ConnectionStatusCard
import com.sameerasw.airsync.presentation.ui.components.cards.LastConnectedDeviceCard
import com.sameerasw.airsync.presentation.ui.components.cards.ManualConnectionCard
-import com.sameerasw.airsync.presentation.ui.components.cards.RemoteFunctionsCard
import com.sameerasw.airsync.presentation.ui.components.cards.RateAppCard
+import com.sameerasw.airsync.presentation.ui.components.cards.RemoteFunctionsCard
import com.sameerasw.airsync.presentation.ui.components.dialogs.ConnectionDialog
import com.sameerasw.airsync.presentation.ui.components.sheets.HelpSupportBottomSheet
import com.sameerasw.airsync.presentation.ui.composables.WelcomeScreen
import com.sameerasw.airsync.presentation.ui.models.AirSyncTab
+import com.sameerasw.airsync.presentation.ui.modifiers.BlurDirection
+import com.sameerasw.airsync.presentation.ui.modifiers.progressiveBlur
import com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel
import com.sameerasw.airsync.utils.ClipboardSyncManager
import com.sameerasw.airsync.utils.HapticUtil
@@ -220,8 +207,11 @@ fun AirSyncMainScreen(
}
}
}
+
val pagerState =
- rememberPagerState(initialPage = initialPage, pageCount = { if (uiState.isConnected) 4 else 2 })
+ rememberPagerState(
+ initialPage = initialPage,
+ pageCount = { if (uiState.isConnected) 4 else 2 })
val navCallbackState = rememberUpdatedState(onNavigateToApps)
LaunchedEffect(navCallbackState.value) {
}
@@ -701,305 +691,316 @@ fun AirSyncMainScreen(
modifier = Modifier.fillMaxSize(),
containerColor = MaterialTheme.colorScheme.surfaceContainer,
) { innerPadding ->
- val density = androidx.compose.ui.platform.LocalDensity.current
- val configuration = androidx.compose.ui.platform.LocalConfiguration.current
- val isLandscape = configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
-
- val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
- val statusBarHeightPx = with(density) { statusBarHeight.toPx() }
- val topSpacing = (statusBarHeight - 24.dp).coerceAtLeast(0.dp)
-
- // Track page changes for haptic feedback on swipe
- LaunchedEffect(pagerState.currentPage) {
- snapshotFlow { pagerState.currentPage }.collect { _ ->
- HapticUtil.performLightTick(haptics)
+ val density = androidx.compose.ui.platform.LocalDensity.current
+ val configuration = androidx.compose.ui.platform.LocalConfiguration.current
+ val isLandscape =
+ configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
+
+ val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
+ val statusBarHeightPx = with(density) { statusBarHeight.toPx() }
+ val topSpacing = (statusBarHeight - 24.dp).coerceAtLeast(0.dp)
+
+ // Track page changes for haptic feedback on swipe
+ LaunchedEffect(pagerState.currentPage) {
+ snapshotFlow { pagerState.currentPage }.collect { _ ->
+ HapticUtil.performLightTick(haptics)
+ }
}
- }
- // Blur heights
- val bottomBlurHeightPx = with(density) {
- if (isLandscape) 100.dp.toPx() else 180.dp.toPx()
- }
+ // Blur heights
+ val bottomBlurHeightPx = with(density) {
+ if (isLandscape) 100.dp.toPx() else 180.dp.toPx()
+ }
- Box(
- modifier = Modifier
- .fillMaxSize()
- ) {
- HorizontalPager(
- modifier = modifier
+ Box(
+ modifier = Modifier
.fillMaxSize()
- .progressiveBlur(
- blurRadius = if (uiState.isBlurEnabled) 40f else 0f,
- height = statusBarHeightPx * 1.15f,
- direction = BlurDirection.TOP
- )
- .progressiveBlur(
- blurRadius = if (uiState.isBlurEnabled) 40f else 0f,
- height = bottomBlurHeightPx,
- direction = BlurDirection.BOTTOM
- ),
- state = pagerState
- ) { page ->
- when (page) {
- 0 -> {
- // Connect tab content
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(vertical = 0.dp)
- .verticalScroll(connectScrollState)
- .padding(horizontal = 16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(24.dp)
- ) {
-
- Spacer(
+ ) {
+ HorizontalPager(
+ modifier = modifier
+ .fillMaxSize()
+ .progressiveBlur(
+ blurRadius = if (uiState.isBlurEnabled) 40f else 0f,
+ height = statusBarHeightPx * 1.15f,
+ direction = BlurDirection.TOP
+ )
+ .progressiveBlur(
+ blurRadius = if (uiState.isBlurEnabled) 40f else 0f,
+ height = bottomBlurHeightPx,
+ direction = BlurDirection.BOTTOM
+ ),
+ state = pagerState
+ ) { page ->
+ when (page) {
+ 0 -> {
+ // Connect tab content
+ Column(
modifier = Modifier
- .height(topSpacing)
- .fillMaxWidth()
- )
+ .fillMaxSize()
+ .padding(vertical = 0.dp)
+ .verticalScroll(connectScrollState)
+ .padding(horizontal = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+
+ Spacer(
+ modifier = Modifier
+ .height(topSpacing)
+ .fillMaxWidth()
+ )
- RoundedCardContainer {
-
- // Rating Prompt Card
- AnimatedVisibility(
- visible = uiState.shouldShowRatingPrompt,
- enter = expandVertically() + fadeIn(),
- exit = shrinkVertically() + fadeOut()
- ) {
- RateAppCard(
- onDismiss = { viewModel.setRatingCardDismissed() },
- onRate = { viewModel.setAppRated() }
- )
- }
+ RoundedCardContainer {
+ // Rating Prompt Card
+ AnimatedVisibility(
+ visible = uiState.shouldShowRatingPrompt,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ RateAppCard(
+ onDismiss = { viewModel.setRatingCardDismissed() },
+ onRate = { viewModel.setAppRated() }
+ )
+ }
- // Connection Status Card
- ConnectionStatusCard(
- isConnected = uiState.isConnected,
- isConnecting = uiState.isConnecting,
- onDisconnect = { disconnect() },
- connectedDevice = uiState.lastConnectedDevice,
- lastConnected = uiState.lastConnectedDevice != null,
- uiState = uiState,
- )
- // Remote Functions Card (Lock Screen, etc.)
- AnimatedVisibility(
- visible = uiState.isConnected,
- enter = expandVertically() + fadeIn(),
- exit = shrinkVertically() + fadeOut()
- ) {
- RemoteFunctionsCard(
- onRemoteAction = { sendRemoteAction(it) }
+ // Connection Status Card
+ ConnectionStatusCard(
+ isConnected = uiState.isConnected,
+ isConnecting = uiState.isConnecting,
+ onDisconnect = { disconnect() },
+ connectedDevice = uiState.lastConnectedDevice,
+ lastConnected = uiState.lastConnectedDevice != null,
+ uiState = uiState,
)
- }
- }
- RoundedCardContainer {
- // Nearby Devices (UDP Discovery)
- val discoveredDevices by viewModel.discoveredDevices.collectAsState()
-
- // Last Connected Device Section
- AnimatedVisibility(
- visible = !uiState.isConnected && uiState.lastConnectedDevice != null,
- enter = expandVertically() + fadeIn(),
- exit = shrinkVertically() + fadeOut()
- ) {
- uiState.lastConnectedDevice?.let { device ->
- LastConnectedDeviceCard(
- device = device,
- isAutoReconnectEnabled = uiState.isAutoReconnectEnabled,
- onToggleAutoReconnect = { enabled ->
- viewModel.setAutoReconnectEnabled(
- enabled
- )
- },
- onQuickConnect = {
- // Check if we can use network-aware connection first
- val networkAwareDevice =
- viewModel.getNetworkAwareLastConnectedDevice()
- if (networkAwareDevice != null) {
- // Use network-aware device IP for current network
- viewModel.updateIpAddress(networkAwareDevice.ipAddress)
- viewModel.updatePort(networkAwareDevice.port)
- connect()
- } else {
- // Fallback to legacy stored device
- viewModel.updateIpAddress(device.ipAddress)
- viewModel.updatePort(device.port)
- viewModel.updateSymmetricKey(device.symmetricKey)
- connect()
- }
- }
+ // Remote Functions Card (Lock Screen, etc.)
+ AnimatedVisibility(
+ visible = uiState.isConnected,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ RemoteFunctionsCard(
+ onRemoteAction = { sendRemoteAction(it) }
)
}
}
- AnimatedVisibility(
- visible = !uiState.isConnected,
- enter = expandVertically() + fadeIn(),
- exit = shrinkVertically() + fadeOut()
- ) {
- Card(
- modifier = Modifier.fillMaxWidth(),
- shape = MaterialTheme.shapes.extraSmall,
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceBright
- )
+ RoundedCardContainer {
+ // Nearby Devices (UDP Discovery)
+ val discoveredDevices by viewModel.discoveredDevices.collectAsState()
+
+ // Last Connected Device Section
+ AnimatedVisibility(
+ visible = !uiState.isConnected && uiState.lastConnectedDevice != null,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ uiState.lastConnectedDevice?.let { device ->
+ LastConnectedDeviceCard(
+ device = device,
+ isAutoReconnectEnabled = uiState.isAutoReconnectEnabled,
+ onToggleAutoReconnect = { enabled ->
+ viewModel.setAutoReconnectEnabled(
+ enabled
+ )
+ },
+ onQuickConnect = {
+ // Check if we can use network-aware connection first
+ val networkAwareDevice =
+ viewModel.getNetworkAwareLastConnectedDevice()
+ if (networkAwareDevice != null) {
+ // Use network-aware device IP for current network
+ viewModel.updateIpAddress(networkAwareDevice.ipAddress)
+ viewModel.updatePort(networkAwareDevice.port)
+ connect()
+ } else {
+ // Fallback to legacy stored device
+ viewModel.updateIpAddress(device.ipAddress)
+ viewModel.updatePort(device.port)
+ viewModel.updateSymmetricKey(device.symmetricKey)
+ connect()
+ }
+ }
+ )
+ }
+ }
+
+ AnimatedVisibility(
+ visible = !uiState.isConnected,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = MaterialTheme.shapes.extraSmall,
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceBright
+ )
) {
- Row(
+ Column(
modifier = Modifier
.fillMaxWidth()
- .padding(bottom = 12.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
+ .padding(16.dp)
) {
- Text(
- text = "Available Devices",
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.primary
- )
-
- Switch(
- checked = uiState.isDeviceDiscoveryEnabled,
- onCheckedChange = { enabled ->
- HapticUtil.performClick(haptics)
- viewModel.setDeviceDiscoveryEnabled(
- context,
- enabled
- )
- },
- thumbContent = if (uiState.isDeviceDiscoveryEnabled) {
- {
- Icon(
- painter = painterResource(R.drawable.rounded_android_wifi_3_bar_24),
- contentDescription = null,
- modifier = Modifier.size(
- SwitchDefaults.IconSize
- ),
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "Available Devices",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.primary
+ )
+
+ Switch(
+ checked = uiState.isDeviceDiscoveryEnabled,
+ onCheckedChange = { enabled ->
+ HapticUtil.performClick(haptics)
+ viewModel.setDeviceDiscoveryEnabled(
+ context,
+ enabled
)
- }
- } else null
- )
- }
+ },
+ thumbContent = if (uiState.isDeviceDiscoveryEnabled) {
+ {
+ Icon(
+ painter = painterResource(R.drawable.rounded_android_wifi_3_bar_24),
+ contentDescription = null,
+ modifier = Modifier.size(
+ SwitchDefaults.IconSize
+ ),
+ )
+ }
+ } else null
+ )
+ }
- AnimatedVisibility(
- visible = uiState.isDeviceDiscoveryEnabled,
- enter = expandVertically() + fadeIn(),
- exit = shrinkVertically() + fadeOut()
- ) {
- Column {
- if (discoveredDevices.isEmpty()) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(
- 4.dp
- )
- ) {
- LoadingIndicator()
-
- Text(
- text = "Scanning...",
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(vertical = 8.dp)
- )
- }
- }
+ AnimatedVisibility(
+ visible = uiState.isDeviceDiscoveryEnabled,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column {
+ if (discoveredDevices.isEmpty()) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(
+ 4.dp
+ )
+ ) {
+ LoadingIndicator()
- discoveredDevices.forEachIndexed { index, device ->
- if (index > 0) {
- Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = "Scanning...",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(
+ vertical = 8.dp
+ )
+ )
+ }
}
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clip(MaterialTheme.shapes.medium)
- .background(MaterialTheme.colorScheme.surfaceContainerHigh)
- .clickable {
- HapticUtil.performClick(haptics)
- viewModel.updateIpAddress(device.getBestIp())
- viewModel.updatePort(device.port.toString())
- viewModel.updateManualPcName(
- device.name
- )
- connect(device.id)
- }
- .padding(horizontal = 16.dp, vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- painter = painterResource(R.drawable.apple),
- contentDescription = null,
- tint = MaterialTheme.colorScheme.primary
- )
- Spacer(modifier = Modifier.width(12.dp))
- Column {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = device.name,
- style = MaterialTheme.typography.bodyLarge
- )
- Spacer(
- modifier = Modifier.width(
- 8.dp
+ discoveredDevices.forEachIndexed { index, device ->
+ if (index > 0) {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium)
+ .background(MaterialTheme.colorScheme.surfaceContainerHigh)
+ .clickable {
+ HapticUtil.performClick(
+ haptics
)
- )
- if (device.hasLocalIp()) {
- Icon(
- painter = painterResource(
- R.drawable.rounded_android_wifi_3_bar_24
- ),
- contentDescription = "Wi-Fi",
- modifier = Modifier.size(
- 14.dp
- ),
- tint = MaterialTheme.colorScheme.primary
+ viewModel.updateIpAddress(
+ device.getBestIp()
+ )
+ viewModel.updatePort(device.port.toString())
+ viewModel.updateManualPcName(
+ device.name
)
+ connect(device.id)
}
- if (device.hasTailscaleIp()) {
- if (device.hasLocalIp()) Spacer(
+ .padding(
+ horizontal = 16.dp,
+ vertical = 12.dp
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.apple),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Column {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = device.name,
+ style = MaterialTheme.typography.bodyLarge
+ )
+ Spacer(
modifier = Modifier.width(
- 4.dp
+ 8.dp
)
)
- Icon(
- painter = painterResource(
- R.drawable.rounded_network_node_24
- ),
- contentDescription = "Tailscale",
- modifier = Modifier.size(
- 14.dp
- ),
- tint = MaterialTheme.colorScheme.secondary
- )
+ if (device.hasLocalIp()) {
+ Icon(
+ painter = painterResource(
+ R.drawable.rounded_android_wifi_3_bar_24
+ ),
+ contentDescription = "Wi-Fi",
+ modifier = Modifier.size(
+ 14.dp
+ ),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ if (device.hasTailscaleIp()) {
+ if (device.hasLocalIp()) Spacer(
+ modifier = Modifier.width(
+ 4.dp
+ )
+ )
+ Icon(
+ painter = painterResource(
+ R.drawable.rounded_network_node_24
+ ),
+ contentDescription = "Tailscale",
+ modifier = Modifier.size(
+ 14.dp
+ ),
+ tint = MaterialTheme.colorScheme.secondary
+ )
+ }
}
+ Text(
+ text = "${device.getBestIp()}:${device.port}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ if (uiState.isConnecting && uiState.connectingDeviceId == device.id) {
+ CircularWavyProgressIndicator(
+ modifier = Modifier.size(20.dp)
+ )
+ } else {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowForward,
+ contentDescription = "Connect",
+ modifier = Modifier.size(20.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
}
- Text(
- text = "${device.getBestIp()}:${device.port}",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- Spacer(modifier = Modifier.weight(1f))
- if (uiState.isConnecting && uiState.connectingDeviceId == device.id) {
- CircularWavyProgressIndicator(
- modifier = Modifier.size(20.dp)
- )
- } else {
- Icon(
- Icons.AutoMirrored.Filled.ArrowForward,
- contentDescription = "Connect",
- modifier = Modifier.size(20.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant
- )
}
}
}
@@ -1007,46 +1008,103 @@ fun AirSyncMainScreen(
}
}
}
- }
- AnimatedVisibility(
- visible = !uiState.isConnected,
- enter = expandVertically() + fadeIn(),
- exit = shrinkVertically() + fadeOut()
- ) {
- Column {
- ManualConnectionCard(
- isConnected = uiState.isConnected,
- lastConnected = uiState.lastConnectedDevice != null,
- uiState = uiState,
- onIpChange = { viewModel.updateIpAddress(it) },
- onPortChange = { viewModel.updatePort(it) },
- onPcNameChange = { viewModel.updateManualPcName(it) },
- onIsPlusChange = { viewModel.updateManualIsPlus(it) },
- onSymmetricKeyChange = { viewModel.updateSymmetricKey(it) },
- onConnect = { viewModel.prepareForManualConnection() },
- onQrScanClick = { launchScanner(context) }
- )
+ AnimatedVisibility(
+ visible = !uiState.isConnected,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column {
+ ManualConnectionCard(
+ isConnected = uiState.isConnected,
+ lastConnected = uiState.lastConnectedDevice != null,
+ uiState = uiState,
+ onIpChange = { viewModel.updateIpAddress(it) },
+ onPortChange = { viewModel.updatePort(it) },
+ onPcNameChange = { viewModel.updateManualPcName(it) },
+ onIsPlusChange = { viewModel.updateManualIsPlus(it) },
+ onSymmetricKeyChange = {
+ viewModel.updateSymmetricKey(
+ it
+ )
+ },
+ onConnect = { viewModel.prepareForManualConnection() },
+ onQrScanClick = { launchScanner(context) }
+ )
+ }
}
}
+
+ Spacer(modifier = Modifier.height(100.dp))
}
+ }
- Spacer(modifier = Modifier.height(100.dp))
+ 1 -> {
+ if (uiState.isConnected) {
+ // When connected: page 1 = Remote
+ RemoteControlScreen(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(
+ top = statusBarHeight,
+ bottom = if (isLandscape) 100.dp else 180.dp
+ ),
+ showKeyboard = showKeyboard,
+ onDismissKeyboard = { showKeyboard = false }
+ )
+ } else {
+ // When disconnected: page 1 = Settings
+ SettingsView(
+ modifier = Modifier.fillMaxSize(),
+ context = context,
+ innerPaddingBottom = 0.dp,
+ uiState = uiState,
+ deviceInfo = deviceInfo,
+ versionName = versionName,
+ viewModel = viewModel,
+ scrollState = settingsScrollState,
+ scope = scope,
+ onSendMessage = { message -> sendMessage(message) },
+ onExport = { json ->
+ pendingExportJson = json
+ createDocLauncher.launch("airsync_settings_${System.currentTimeMillis()}.json")
+ },
+ onImport = { openDocLauncher.launch(arrayOf("application/json")) },
+ onResetOnboarding = { viewModel.resetOnboarding() },
+ onShowHelp = { showHelpSheet = true },
+ onToggleDeveloperMode = { viewModel.toggleDeveloperModeVisibility() }
+ )
+ }
}
- }
- 1 -> {
- if (uiState.isConnected) {
- // When connected: page 1 = Remote
- RemoteControlScreen(
- modifier = Modifier
- .fillMaxSize()
- .padding(top = statusBarHeight, bottom = if (isLandscape) 100.dp else 180.dp),
- showKeyboard = showKeyboard,
- onDismissKeyboard = { showKeyboard = false }
- )
- } else {
- // When disconnected: page 1 = Settings
+ 2 -> {
+ if (uiState.isConnected) {
+ // When connected: page 2 = Clipboard
+ ClipboardScreen(
+ clipboardHistory = uiState.clipboardHistory,
+ isConnected = true,
+ onSendText = { text ->
+ viewModel.addClipboardEntry(text, isFromPc = false)
+ val clipboardJson = JsonUtil.createClipboardUpdateJson(text)
+ WebSocketUtil.sendMessage(clipboardJson)
+ },
+ onClearHistory = { viewModel.clearClipboardHistory() },
+ isHistoryEnabled = uiState.isClipboardHistoryEnabled,
+ onHistoryToggle = { viewModel.setClipboardHistoryEnabled(it) },
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(
+ top = topSpacing,
+ bottom = if (isLandscape) 100.dp else 180.dp
+ ),
+ )
+ } else {
+ Box(Modifier.fillMaxSize())
+ }
+ }
+
+ 3 -> {
+ // Page 3 only exists when connected = Settings tab
SettingsView(
modifier = Modifier.fillMaxSize(),
context = context,
@@ -1069,214 +1127,174 @@ fun AirSyncMainScreen(
)
}
}
-
- 2 -> {
- if (uiState.isConnected) {
- // When connected: page 2 = Clipboard
- ClipboardScreen(
- clipboardHistory = uiState.clipboardHistory,
- isConnected = true,
- onSendText = { text ->
- viewModel.addClipboardEntry(text, isFromPc = false)
- val clipboardJson = JsonUtil.createClipboardUpdateJson(text)
- WebSocketUtil.sendMessage(clipboardJson)
- },
- onClearHistory = { viewModel.clearClipboardHistory() },
- isHistoryEnabled = uiState.isClipboardHistoryEnabled,
- onHistoryToggle = { viewModel.setClipboardHistoryEnabled(it) },
- modifier = Modifier
- .fillMaxSize()
- .padding(top = topSpacing, bottom = if (isLandscape) 100.dp else 180.dp),
- )
- } else {
- Box(Modifier.fillMaxSize())
- }
- }
-
- 3 -> {
- // Page 3 only exists when connected = Settings tab
- SettingsView(
- modifier = Modifier.fillMaxSize(),
- context = context,
- innerPaddingBottom = 0.dp,
- uiState = uiState,
- deviceInfo = deviceInfo,
- versionName = versionName,
- viewModel = viewModel,
- scrollState = settingsScrollState,
- scope = scope,
- onSendMessage = { message -> sendMessage(message) },
- onExport = { json ->
- pendingExportJson = json
- createDocLauncher.launch("airsync_settings_${System.currentTimeMillis()}.json")
- },
- onImport = { openDocLauncher.launch(arrayOf("application/json")) },
- onResetOnboarding = { viewModel.resetOnboarding() },
- onShowHelp = { showHelpSheet = true },
- onToggleDeveloperMode = { viewModel.toggleDeveloperModeVisibility() }
- )
- }
}
- }
- // Adaptive Bottom Bars Container
- Box(
- modifier = Modifier
- .align(Alignment.BottomCenter)
- .fillMaxWidth()
- .padding(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding())
+ // Adaptive Bottom Bars Container
+ Box(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .fillMaxWidth()
+ .padding(
+ bottom = WindowInsets.navigationBars.asPaddingValues()
+ .calculateBottomPadding()
+ )
// .padding(bottom = 16.dp)
- .zIndex(2f)
- ) {
- if (isLandscape) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
- verticalAlignment = Alignment.Bottom
- ) {
- AnimatedVisibility(
- visible = uiState.isConnected,
- enter = fadeIn() + expandHorizontally(),
- exit = fadeOut() + shrinkHorizontally(),
- modifier = Modifier.weight(1f)
+ .zIndex(2f)
+ ) {
+ if (isLandscape) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(
+ 16.dp,
+ Alignment.CenterHorizontally
+ ),
+ verticalAlignment = Alignment.Bottom
) {
- FloatingMediaPlayer(
- musicInfo = macStatus?.music,
- albumArtBitmap = albumArtBitmap,
- volume = volume,
- isMuted = isMuted,
- onVolumeChange = {
- volume = it
- sendRemoteAction("vol_set", it.toInt())
- },
- onToggleMute = {
- sendRemoteAction("vol_mute")
- isMuted = !isMuted
- },
- onMediaAction = { sendRemoteAction(it) }
- )
- }
+ AnimatedVisibility(
+ visible = uiState.isConnected,
+ enter = fadeIn() + expandHorizontally(),
+ exit = fadeOut() + shrinkHorizontally(),
+ modifier = Modifier.weight(1f)
+ ) {
+ FloatingMediaPlayer(
+ musicInfo = macStatus?.music,
+ albumArtBitmap = albumArtBitmap,
+ volume = volume,
+ isMuted = isMuted,
+ onVolumeChange = {
+ volume = it
+ sendRemoteAction("vol_set", it.toInt())
+ },
+ onToggleMute = {
+ sendRemoteAction("vol_mute")
+ isMuted = !isMuted
+ },
+ onMediaAction = { sendRemoteAction(it) }
+ )
+ }
- AirSyncFloatingToolbar(
- modifier = Modifier.zIndex(1f),
- currentPage = pagerState.currentPage,
- tabs = tabs,
- onTabSelected = { index ->
- scope.launch {
- val distance = kotlin.math.abs(index - pagerState.currentPage)
- if (distance == 1) {
- pagerState.animateScrollToPage(index)
- } else {
- pagerState.scrollToPage(index)
- }
- }
- },
- floatingActionButton = {
- MainFAB(
- currentTab = tabs.getOrNull(pagerState.currentPage),
- isConnected = uiState.isConnected,
- onAction = { action ->
- when (action) {
- "keyboard" -> showKeyboard = !showKeyboard
- "clear_history" -> viewModel.clearClipboardHistory()
- "disconnect" -> disconnect()
- "scan" -> launchScanner(context)
+ AirSyncFloatingToolbar(
+ modifier = Modifier.zIndex(1f),
+ currentPage = pagerState.currentPage,
+ tabs = tabs,
+ onTabSelected = { index ->
+ scope.launch {
+ val distance =
+ kotlin.math.abs(index - pagerState.currentPage)
+ if (distance == 1) {
+ pagerState.animateScrollToPage(index)
+ } else {
+ pagerState.scrollToPage(index)
}
}
- )
- }
- )
- }
- } else {
- // Portrait: Stacked
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(6.dp)
- ) {
- AnimatedVisibility(
- visible = uiState.isConnected,
- enter = fadeIn() + expandVertically(expandFrom = Alignment.Bottom),
- exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom),
- ) {
- FloatingMediaPlayer(
- musicInfo = macStatus?.music,
- albumArtBitmap = albumArtBitmap,
- volume = volume,
- isMuted = isMuted,
- onVolumeChange = {
- volume = it
- sendRemoteAction("vol_set", it.toInt())
- },
- onToggleMute = {
- sendRemoteAction("vol_mute")
- isMuted = !isMuted
},
- onMediaAction = { sendRemoteAction(it) }
+ floatingActionButton = {
+ MainFAB(
+ currentTab = tabs.getOrNull(pagerState.currentPage),
+ isConnected = uiState.isConnected,
+ onAction = { action ->
+ when (action) {
+ "keyboard" -> showKeyboard = !showKeyboard
+ "clear_history" -> viewModel.clearClipboardHistory()
+ "disconnect" -> disconnect()
+ "scan" -> launchScanner(context)
+ }
+ }
+ )
+ }
)
}
+ } else {
+ // Portrait: Stacked
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ AnimatedVisibility(
+ visible = uiState.isConnected,
+ enter = fadeIn() + expandVertically(expandFrom = Alignment.Bottom),
+ exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom),
+ ) {
+ FloatingMediaPlayer(
+ musicInfo = macStatus?.music,
+ albumArtBitmap = albumArtBitmap,
+ volume = volume,
+ isMuted = isMuted,
+ onVolumeChange = {
+ volume = it
+ sendRemoteAction("vol_set", it.toInt())
+ },
+ onToggleMute = {
+ sendRemoteAction("vol_mute")
+ isMuted = !isMuted
+ },
+ onMediaAction = { sendRemoteAction(it) }
+ )
+ }
- AirSyncFloatingToolbar(
- modifier = Modifier.zIndex(1f),
- currentPage = pagerState.currentPage,
- tabs = tabs,
- onTabSelected = { index ->
- scope.launch {
- val distance = kotlin.math.abs(index - pagerState.currentPage)
- if (distance == 1) {
- pagerState.animateScrollToPage(index)
- } else {
- pagerState.scrollToPage(index)
- }
- }
- },
- floatingActionButton = {
- MainFAB(
- currentTab = tabs.getOrNull(pagerState.currentPage),
- isConnected = uiState.isConnected,
- onAction = { action ->
- when (action) {
- "keyboard" -> showKeyboard = !showKeyboard
- "clear_history" -> viewModel.clearClipboardHistory()
- "disconnect" -> disconnect()
- "scan" -> launchScanner(context)
+ AirSyncFloatingToolbar(
+ modifier = Modifier.zIndex(1f),
+ currentPage = pagerState.currentPage,
+ tabs = tabs,
+ onTabSelected = { index ->
+ scope.launch {
+ val distance =
+ kotlin.math.abs(index - pagerState.currentPage)
+ if (distance == 1) {
+ pagerState.animateScrollToPage(index)
+ } else {
+ pagerState.scrollToPage(index)
}
}
- )
- }
- )
+ },
+ floatingActionButton = {
+ MainFAB(
+ currentTab = tabs.getOrNull(pagerState.currentPage),
+ isConnected = uiState.isConnected,
+ onAction = { action ->
+ when (action) {
+ "keyboard" -> showKeyboard = !showKeyboard
+ "clear_history" -> viewModel.clearClipboardHistory()
+ "disconnect" -> disconnect()
+ "scan" -> launchScanner(context)
+ }
+ }
+ )
+ }
+ )
+ }
}
}
}
}
- }
- // Dialogs
- if (uiState.isDialogVisible) {
- ConnectionDialog(
- deviceName = deviceInfo.name,
- localIp = deviceInfo.localIp,
- desktopIp = uiState.ipAddress,
- port = uiState.port,
- pcName = pcName ?: uiState.lastConnectedDevice?.name,
- isPlus = uiState.lastConnectedDevice?.isPlus ?: isPlus,
- onDismiss = { viewModel.setDialogVisible(false) },
- onConnect = {
- viewModel.setDialogVisible(false)
- connect()
- }
- )
- }
+ // Dialogs
+ if (uiState.isDialogVisible) {
+ ConnectionDialog(
+ deviceName = deviceInfo.name,
+ localIp = deviceInfo.localIp,
+ desktopIp = uiState.ipAddress,
+ port = uiState.port,
+ pcName = pcName ?: uiState.lastConnectedDevice?.name,
+ isPlus = uiState.lastConnectedDevice?.isPlus ?: isPlus,
+ onDismiss = { viewModel.setDialogVisible(false) },
+ onConnect = {
+ viewModel.setDialogVisible(false)
+ connect()
+ }
+ )
+ }
- // Help & Support Bottom Sheet
- if (showHelpSheet) {
- HelpSupportBottomSheet(
- onDismissRequest = onDismissHelp
- )
- }
+ // Help & Support Bottom Sheet
+ if (showHelpSheet) {
+ HelpSupportBottomSheet(
+ onDismissRequest = onDismissHelp
+ )
+ }
// Welcome Screen Overlay
AnimatedVisibility(
@@ -1304,7 +1322,7 @@ private fun MainFAB(
onAction: (String) -> Unit
) {
val haptics = LocalHapticFeedback.current
-
+
FloatingToolbarDefaults.StandardFloatingActionButton(
onClick = {
HapticUtil.performClick(haptics)
@@ -1321,9 +1339,11 @@ private fun MainFAB(
R.string.tab_remote -> {
Icon(Icons.Rounded.Keyboard, contentDescription = "Keyboard")
}
+
R.string.tab_clipboard -> {
Icon(Icons.Rounded.Delete, contentDescription = "Clear History")
}
+
else -> {
if (isConnected) {
Icon(imageVector = Icons.Filled.LinkOff, contentDescription = "Disconnect")
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/PermissionsScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/PermissionsScreen.kt
index f051f478..11eb4c2e 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/PermissionsScreen.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/PermissionsScreen.kt
@@ -222,7 +222,7 @@ fun PermissionsScreen(
isCritical = false
)
}
-
+
"Bluetooth Access" -> {
PermissionButton(
permissionName = permission,
@@ -236,7 +236,9 @@ fun PermissionsScreen(
PermissionButton(
permissionName = permission,
description = "Discover nearby Mac devices on Wi-Fi",
- onExplainClick = { showDialog = PermissionType.LOCAL_NETWORK },
+ onExplainClick = {
+ showDialog = PermissionType.LOCAL_NETWORK
+ },
isCritical = false
)
}
@@ -245,7 +247,9 @@ fun PermissionsScreen(
PermissionButton(
permissionName = permission,
description = "Accept and end calls from Mac",
- onExplainClick = { showDialog = PermissionType.ANSWER_CALLS },
+ onExplainClick = {
+ showDialog = PermissionType.ANSWER_CALLS
+ },
isCritical = false
)
}
@@ -293,7 +297,7 @@ fun PermissionsScreen(
PermissionType.PHONE -> {
onRequestPhonePermission?.invoke()
}
-
+
PermissionType.BLUETOOTH -> {
onRequestBluetoothPermission?.invoke()
}
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/RemoteControlScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/RemoteControlScreen.kt
index 1c7e4819..9a82618a 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/RemoteControlScreen.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/RemoteControlScreen.kt
@@ -10,9 +10,9 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -23,26 +23,20 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.Circle
import androidx.compose.material.icons.filled.SpaceBar
-import androidx.compose.material.icons.rounded.KeyboardArrowDown
-import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
@@ -59,21 +53,17 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.sameerasw.airsync.R
import com.sameerasw.airsync.presentation.ui.components.KeyboardInputSheet
import com.sameerasw.airsync.presentation.ui.components.KeyboardModifiers
import com.sameerasw.airsync.presentation.ui.components.ModifierStatus
-import com.sameerasw.airsync.presentation.ui.components.RoundedCardContainer
import com.sameerasw.airsync.utils.HapticUtil
import com.sameerasw.airsync.utils.WebSocketUtil
import kotlinx.coroutines.channels.Channel
@@ -92,7 +82,8 @@ fun RemoteControlScreen(
val haptics = LocalHapticFeedback.current
val scope = rememberCoroutineScope()
val context = LocalContext.current
- val dataStoreManager = remember { com.sameerasw.airsync.data.local.DataStoreManager.getInstance(context) }
+ val dataStoreManager =
+ remember { com.sameerasw.airsync.data.local.DataStoreManager.getInstance(context) }
val isFlipped by dataStoreManager.isRemoteFlipped().collectAsState(initial = null)
if (isFlipped == null) return
@@ -258,13 +249,16 @@ fun RemoteControlScreen(
}
val config = LocalConfiguration.current
- val isWide = config.orientation == Configuration.ORIENTATION_LANDSCAPE || config.screenWidthDp > 600
+ val isWide =
+ config.orientation == Configuration.ORIENTATION_LANDSCAPE || config.screenWidthDp > 600
@Composable
fun ExtraKeys() {
// Extra Keys
FlowRow(
- modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 8.dp),
horizontalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.spacedBy(12.dp),
maxItemsInEachRow = 3
@@ -284,7 +278,7 @@ fun RemoteControlScreen(
}
OutlinedButton(
- onClick = {
+ onClick = {
HapticUtil.performLightTick(haptics)
scope.launch {
dataStoreManager.setRemoteFlipped(isFlipped != true)
diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/viewmodel/AirSyncViewModel.kt b/app/src/main/java/com/sameerasw/airsync/presentation/viewmodel/AirSyncViewModel.kt
index 2e99cd73..330f0ee7 100644
--- a/app/src/main/java/com/sameerasw/airsync/presentation/viewmodel/AirSyncViewModel.kt
+++ b/app/src/main/java/com/sameerasw/airsync/presentation/viewmodel/AirSyncViewModel.kt
@@ -97,7 +97,8 @@ class AirSyncViewModel(
// Connection status listener for WebSocket updates
private val connectionStatusListener: (Boolean) -> Unit = { isWsConnected ->
viewModelScope.launch {
- val isBleConnected = _uiState.value.bleConnectionState == com.sameerasw.airsync.data.ble.BleGattServer.BleConnectionState.AUTHENTICATED
+ val isBleConnected =
+ _uiState.value.bleConnectionState == com.sameerasw.airsync.data.ble.BleGattServer.BleConnectionState.AUTHENTICATED
val isGlobalConnected = isWsConnected || isBleConnected
_uiState.value = _uiState.value.copy(
@@ -196,14 +197,15 @@ class AirSyncViewModel(
viewModelScope.launch {
com.sameerasw.airsync.AirSyncApp.getBleConnectionManager()?.connectionState?.collect { state ->
Log.d("AirSyncViewModel", "BLE connection state changed: $state")
- val isBleAuthenticated = state == com.sameerasw.airsync.data.ble.BleGattServer.BleConnectionState.AUTHENTICATED
+ val isBleAuthenticated =
+ state == com.sameerasw.airsync.data.ble.BleGattServer.BleConnectionState.AUTHENTICATED
val isWsConnected = WebSocketUtil.isConnected()
-
+
_uiState.value = _uiState.value.copy(
bleConnectionState = state,
isConnected = isWsConnected || isBleAuthenticated
)
-
+
if (isBleAuthenticated && !isWsConnected) {
// Refresh shortcuts and other side effects if this is the only connection
appContext?.let { ctx ->
@@ -318,7 +320,7 @@ class AirSyncViewModel(
val isPowerSaveMode = DeviceInfoUtil.isPowerSaveMode(context)
val isBlurProblematic = DeviceInfoUtil.isBlurProblematicDevice()
val isQuickShareEnabled = repository.isQuickShareEnabled().first()
-
+
// Replicate Essentials logic for initial state
val isBlurEnabled = isBlurEnabledSetting && !isPowerSaveMode && !isBlurProblematic
@@ -411,7 +413,7 @@ class AirSyncViewModel(
// Start AirSync Service conditionally
ServiceManager.updateServiceState(context)
-
+
// Initial shortcut state
ShortcutUtil.refreshShortcuts(context, WebSocketUtil.isConnected())
isNetworkMonitoringActive = true
@@ -686,7 +688,8 @@ class AirSyncViewModel(
_uiState.value = _uiState.value.copy(isQuickShareEnabled = enabled)
viewModelScope.launch {
repository.setQuickShareEnabled(enabled)
- val intent = Intent(context, com.sameerasw.airsync.quickshare.QuickShareService::class.java)
+ val intent =
+ Intent(context, com.sameerasw.airsync.quickshare.QuickShareService::class.java)
if (enabled) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
context.startForegroundService(intent)
@@ -1133,15 +1136,18 @@ class AirSyncViewModel(
}
}
- private val _notificationApps = MutableStateFlow>(emptyList())
- val notificationApps: StateFlow> = _notificationApps.asStateFlow()
+ private val _notificationApps =
+ MutableStateFlow>(emptyList())
+ val notificationApps: StateFlow> =
+ _notificationApps.asStateFlow()
fun loadNotificationApps(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
val installed = com.sameerasw.airsync.utils.AppUtil.getInstalledApps(context)
val saved = repository.getNotificationApps().first()
- val merged = com.sameerasw.airsync.utils.AppUtil.mergeWithSavedApps(installed, saved)
+ val merged =
+ com.sameerasw.airsync.utils.AppUtil.mergeWithSavedApps(installed, saved)
_notificationApps.value = merged
} catch (e: Exception) {
Log.e("AirSyncViewModel", "Failed to load notification apps: ${e.message}")
@@ -1163,7 +1169,10 @@ class AirSyncViewModel(
}
}
- fun saveAllNotificationApps(context: Context, apps: List) {
+ fun saveAllNotificationApps(
+ context: Context,
+ apps: List
+ ) {
viewModelScope.launch(Dispatchers.IO) {
try {
_notificationApps.value = apps
diff --git a/app/src/main/java/com/sameerasw/airsync/quickshare/InboundQuickShareConnection.kt b/app/src/main/java/com/sameerasw/airsync/quickshare/InboundQuickShareConnection.kt
index accc2a81..68e0c8ef 100644
--- a/app/src/main/java/com/sameerasw/airsync/quickshare/InboundQuickShareConnection.kt
+++ b/app/src/main/java/com/sameerasw/airsync/quickshare/InboundQuickShareConnection.kt
@@ -7,28 +7,26 @@ import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
-import com.google.location.nearby.connections.proto.OfflineFrame
-import com.google.location.nearby.connections.proto.PayloadTransferFrame
-import com.google.location.nearby.connections.proto.V1Frame
-import com.google.location.nearby.connections.proto.ConnectionResponseFrame
-import com.google.location.nearby.connections.proto.OsInfo
-import com.google.security.cryptauth.lib.securegcm.Ukey2ClientFinished
-import com.google.security.cryptauth.lib.securegcm.Ukey2ClientInit
-import com.google.security.cryptauth.lib.securegcm.Ukey2ServerInit
-import okio.ByteString.Companion.toByteString
-import com.google.android.gms.nearby.sharing.ConnectionResponseFrame as SharingResponse
import com.google.android.gms.nearby.sharing.Frame
import com.google.android.gms.nearby.sharing.IntroductionFrame
import com.google.android.gms.nearby.sharing.PairedKeyEncryptionFrame
import com.google.android.gms.nearby.sharing.PairedKeyResultFrame
-import com.google.security.cryptauth.lib.securegcm.Ukey2Message
-import com.google.android.gms.nearby.sharing.V1Frame as SharingV1
+import com.google.location.nearby.connections.proto.ConnectionResponseFrame
+import com.google.location.nearby.connections.proto.OfflineFrame
+import com.google.location.nearby.connections.proto.OsInfo
+import com.google.location.nearby.connections.proto.PayloadTransferFrame
import com.google.location.nearby.connections.proto.PayloadTransferFrame.PayloadHeader
+import com.google.location.nearby.connections.proto.V1Frame
+import com.google.security.cryptauth.lib.securegcm.Ukey2ClientInit
+import com.google.security.cryptauth.lib.securegcm.Ukey2Message
+import okio.ByteString.Companion.toByteString
import java.io.File
import java.io.FileOutputStream
import java.net.Socket
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
+import com.google.android.gms.nearby.sharing.ConnectionResponseFrame as SharingResponse
+import com.google.android.gms.nearby.sharing.V1Frame as SharingV1
/**
* Handles an incoming Quick Share connection.
@@ -42,8 +40,10 @@ class InboundQuickShareConnection(
var onConnectionReady: ((InboundQuickShareConnection) -> Unit)? = null
var onIntroductionReceived: ((IntroductionFrame) -> Unit)? = null
var onFinished: ((InboundQuickShareConnection) -> Unit)? = null
- var onFileProgress: ((fileName: String, percent: Int, bytesTransferred: Long, totalSize: Long, transferId: String) -> Unit)? = null
- var onFileComplete: ((fileName: String, transferId: String, success: Boolean, uri: android.net.Uri?) -> Unit)? = null
+ var onFileProgress: ((fileName: String, percent: Int, bytesTransferred: Long, totalSize: Long, transferId: String) -> Unit)? =
+ null
+ var onFileComplete: ((fileName: String, transferId: String, success: Boolean, uri: android.net.Uri?) -> Unit)? =
+ null
companion object {
private const val TAG = "InboundQSConnection"
@@ -109,9 +109,10 @@ class InboundQuickShareConnection(
throw IllegalStateException("Expected CLIENT_INIT, got ${clientInitEnvelope.message_type}")
}
val clientInit = Ukey2ClientInit.ADAPTER.decode(clientInitEnvelope.message_data!!)
-
+
// Send ServerInit (Wrapped in Ukey2Message)
- val serverInit = ukey2.handleClientInit(clientInit) ?: throw IllegalStateException("Failed to handle ClientInit")
+ val serverInit = ukey2.handleClientInit(clientInit)
+ ?: throw IllegalStateException("Failed to handle ClientInit")
val serverInitEnvelope = Ukey2Message(
message_type = Ukey2Message.Type.SERVER_INIT,
message_data = serverInit.encode().toByteString()
@@ -133,7 +134,7 @@ class InboundQuickShareConnection(
serverInitEnvelopeBytes = serverInitEnvelopeBytes,
clientInit = clientInit
)
-
+
Log.d(TAG, "UKEY2 Handshake complete. PIN: ${ukey2.authString}")
onConnectionReady?.invoke(this)
@@ -144,9 +145,9 @@ class InboundQuickShareConnection(
Log.d(TAG, "Read ConnectionResponse: ${responseFrameData.size} bytes")
val responseFrame = OfflineFrame.ADAPTER.decode(responseFrameData)
if (responseFrame.v1!!.type != V1Frame.FrameType.CONNECTION_RESPONSE) {
- throw IllegalStateException("Expected CONNECTION_RESPONSE, got ${responseFrame.v1!!.type}")
+ throw IllegalStateException("Expected CONNECTION_RESPONSE, got ${responseFrame.v1!!.type}")
}
-
+
// Send OUR ConnectionResponse (Accept)
val ourResponse = OfflineFrame(
version = OfflineFrame.Version.V1,
@@ -280,13 +281,13 @@ class InboundQuickShareConnection(
try {
val d2dPayload = readEncryptedMessage()
val offlineFrame = OfflineFrame.ADAPTER.decode(d2dPayload)
-
+
when (offlineFrame.v1?.type) {
V1Frame.FrameType.PAYLOAD_TRANSFER -> {
val transfer = offlineFrame.v1!!.payload_transfer!!
val header = transfer.payload_header
val chunk = transfer.payload_chunk
-
+
when (header?.type) {
PayloadHeader.PayloadType.BYTES -> {
if (chunk?.body != null && chunk.body.size > 0) {
@@ -301,16 +302,20 @@ class InboundQuickShareConnection(
pendingBytesPayload = null
}
}
+
PayloadHeader.PayloadType.FILE -> {
handlePayloadTransfer(transfer)
}
+
else -> Log.d(TAG, "Unknown payload type: ${header?.type}")
}
}
+
V1Frame.FrameType.DISCONNECTION -> {
Log.d(TAG, "Received disconnection frame")
isRunning = false
}
+
else -> Log.d(TAG, "Unknown offline frame type: ${offlineFrame.v1?.type}")
}
} catch (e: Exception) {
@@ -333,13 +338,16 @@ class InboundQuickShareConnection(
prepareFiles(v1Frame.introduction!!)
onIntroductionReceived?.invoke(v1Frame.introduction!!)
}
+
SharingV1.FrameType.CANCEL -> {
Log.d(TAG, "Transfer cancelled by sender")
isRunning = false
}
+
SharingV1.FrameType.PAIRED_KEY_RESULT -> {
Log.d(TAG, "Received PairedKeyResult")
}
+
else -> Log.d(TAG, "Received unhandled sharing frame type: ${v1Frame.type}")
}
}
@@ -349,7 +357,7 @@ class InboundQuickShareConnection(
*/
fun sendSharingResponse(status: SharingResponse.Status) {
val responseFrame = SharingResponse(status = status)
-
+
val frame = Frame(
version = Frame.Version.V1,
v1 = SharingV1(
@@ -357,9 +365,9 @@ class InboundQuickShareConnection(
connection_response = responseFrame
)
)
-
+
writeSharingFrame(frame)
-
+
if (status == SharingResponse.Status.ACCEPT) {
openFiles()
}
@@ -382,7 +390,10 @@ class InboundQuickShareConnection(
put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
put(MediaStore.Downloads.IS_PENDING, 1)
}
- val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
+ val uri = context.contentResolver.insert(
+ MediaStore.Downloads.EXTERNAL_CONTENT_URI,
+ values
+ )
if (uri != null) {
info.outputStream = context.contentResolver.openOutputStream(uri)
info.uri = uri
@@ -390,7 +401,8 @@ class InboundQuickShareConnection(
}
} else {
@Suppress("DEPRECATION")
- val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ val downloadsDir =
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
downloadsDir.mkdirs()
var targetFile = File(downloadsDir, info.name)
var counter = 1
@@ -420,7 +432,13 @@ class InboundQuickShareConnection(
// Update progress (throttle if needed, but for now simple)
if (info.size > 0) {
val percent = ((info.bytesTransferred * 100) / info.size).toInt()
- onFileProgress?.invoke(info.name, percent, info.bytesTransferred, info.size, id.toString())
+ onFileProgress?.invoke(
+ info.name,
+ percent,
+ info.bytesTransferred,
+ info.size,
+ id.toString()
+ )
}
}
@@ -429,7 +447,7 @@ class InboundQuickShareConnection(
Log.d(TAG, "File ${info.name} transfer complete (${info.bytesTransferred} bytes)")
info.outputStream?.close()
info.outputStream = null
-
+
// Clear IS_PENDING so file becomes visible in Downloads
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && info.uri != null) {
val values = ContentValues().apply {
@@ -437,9 +455,9 @@ class InboundQuickShareConnection(
}
context.contentResolver.update(info.uri!!, values, null, null)
}
-
+
onFileComplete?.invoke(info.name, id.toString(), true, info.uri)
-
+
// Check if all files are finished
if (transferredFiles.values.all { it.outputStream == null }) {
Log.d(TAG, "All files transferred")
diff --git a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareAdvertiser.kt b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareAdvertiser.kt
index d8073e5d..be3e866a 100644
--- a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareAdvertiser.kt
+++ b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareAdvertiser.kt
@@ -3,7 +3,6 @@ package com.sameerasw.airsync.quickshare
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
-import android.os.Build
import android.util.Base64
import android.util.Log
import java.nio.charset.StandardCharsets
@@ -16,7 +15,8 @@ class QuickShareAdvertiser(private val context: Context) {
companion object {
private const val TAG = "QuickShareAdvertiser"
private const val SERVICE_TYPE = "_FC9F5ED42C8A._tcp."
- private const val SERVICE_ID_HASH = "fM5e" // Base64 of 0xFC, 0x9F, 0x5E (after PCP 0x23 and 4-byte ID)
+ private const val SERVICE_ID_HASH =
+ "fM5e" // Base64 of 0xFC, 0x9F, 0x5E (after PCP 0x23 and 4-byte ID)
// Actually, let's calculate it properly.
}
@@ -35,10 +35,13 @@ class QuickShareAdvertiser(private val context: Context) {
serviceType = SERVICE_TYPE
serviceName = generateServiceName()
setPort(port)
-
+
// TXT record 'n' contains endpoint info
val endpointInfo = serializeEndpointInfo(deviceName)
- val endpointInfoBase64 = Base64.encodeToString(endpointInfo, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
+ val endpointInfoBase64 = Base64.encodeToString(
+ endpointInfo,
+ Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
+ )
setAttribute("n", endpointInfoBase64)
}
@@ -61,7 +64,11 @@ class QuickShareAdvertiser(private val context: Context) {
}
try {
- nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
+ nsdManager.registerService(
+ serviceInfo,
+ NsdManager.PROTOCOL_DNS_SD,
+ registrationListener
+ )
} catch (e: Exception) {
Log.e(TAG, "Failed to register service", e)
}
@@ -88,19 +95,19 @@ class QuickShareAdvertiser(private val context: Context) {
bytes[7] = 0x5E.toByte()
bytes[8] = 0
bytes[9] = 0
-
+
return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}
private fun serializeEndpointInfo(deviceName: String): ByteArray {
val nameBytes = deviceName.toByteArray(StandardCharsets.UTF_8)
val nameLen = Math.min(nameBytes.size, 255)
-
+
// 1 byte: (deviceType << 1) | Visibility(0) | Version(0)
// Device types: phone=1, tablet=2, computer=3. We'll use phone=1.
val deviceType = 1
val firstByte = (deviceType shl 1).toByte()
-
+
val bytes = ByteArray(1 + 16 + 1 + nameLen)
bytes[0] = firstByte
// 16 random bytes
@@ -108,10 +115,10 @@ class QuickShareAdvertiser(private val context: Context) {
val randomBytes = ByteArray(16)
random.nextBytes(randomBytes)
System.arraycopy(randomBytes, 0, bytes, 1, 16)
-
+
bytes[17] = nameLen.toByte()
System.arraycopy(nameBytes, 0, bytes, 18, nameLen)
-
+
return bytes
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareConnection.kt b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareConnection.kt
index 92d60446..551047fd 100644
--- a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareConnection.kt
+++ b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareConnection.kt
@@ -64,9 +64,9 @@ open class QuickShareConnection(
fun readEncryptedMessage(): ByteArray {
val context = ukey2Context ?: throw IllegalStateException("UKEY2 context not set")
val frameData = readFrame()
-
+
val smsg = SecureMessage.ADAPTER.decode(frameData)
-
+
// 1. Verify HMAC
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(context.receiveHmacKey, "HmacSHA256"))
@@ -80,7 +80,11 @@ open class QuickShareConnection(
val hb = HeaderAndBody.ADAPTER.decode(smsg.header_and_body!!)
val iv = hb.header_!!.iv!!.toByteArray()
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
- cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(context.decryptKey, "AES"), IvParameterSpec(iv))
+ cipher.init(
+ Cipher.DECRYPT_MODE,
+ SecretKeySpec(context.decryptKey, "AES"),
+ IvParameterSpec(iv)
+ )
val decryptedData = cipher.doFinal(hb.body!!.toByteArray())
// 3. Parse DeviceToDeviceMessage
@@ -111,7 +115,11 @@ open class QuickShareConnection(
// 2. Encrypt with AES-CBC
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val iv = ByteArray(16).also { java.util.Random().nextBytes(it) }
- cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(context.encryptKey, "AES"), IvParameterSpec(iv))
+ cipher.init(
+ Cipher.ENCRYPT_MODE,
+ SecretKeySpec(context.encryptKey, "AES"),
+ IvParameterSpec(iv)
+ )
val encryptedData = cipher.doFinal(serializedD2D)
// 3. Create HeaderAndBody
diff --git a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareServer.kt b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareServer.kt
index 2c5630cd..ffeca54b 100644
--- a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareServer.kt
+++ b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareServer.kt
@@ -3,7 +3,6 @@ package com.sameerasw.airsync.quickshare
import android.content.Context
import android.util.Log
import java.net.ServerSocket
-import java.net.Socket
import java.util.concurrent.Executors
/**
@@ -27,18 +26,18 @@ class QuickShareServer(
fun start() {
if (isRunning) return
isRunning = true
-
+
try {
serverSocket = ServerSocket(0) // Bind to any available port synchronously
val currentPort = port
Log.d(TAG, "Server bound to port $currentPort")
-
+
executor.execute {
try {
while (isRunning) {
val socket = serverSocket?.accept() ?: break
Log.d(TAG, "New connection from ${socket.remoteSocketAddress}")
-
+
val connection = InboundQuickShareConnection(
context = context,
socket = socket
diff --git a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareService.kt b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareService.kt
index 4d216972..c5eb9cd4 100644
--- a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareService.kt
+++ b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareService.kt
@@ -16,8 +16,6 @@ import com.sameerasw.airsync.R
import com.sameerasw.airsync.data.local.DataStoreManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -30,7 +28,7 @@ class QuickShareService : Service() {
private const val TAG = "QuickShareService"
private const val NOTIFICATION_ID = 2001
private const val CHANNEL_ID = "quick_share_channel"
-
+
const val ACTION_ACCEPT = "com.sameerasw.airsync.quickshare.ACCEPT"
const val ACTION_REJECT = "com.sameerasw.airsync.quickshare.REJECT"
const val ACTION_START_DISCOVERY = "com.sameerasw.airsync.quickshare.START_DISCOVERY"
@@ -46,13 +44,14 @@ class QuickShareService : Service() {
private val binder = LocalBinder()
private val serviceScope = CoroutineScope(Dispatchers.IO)
private var discoveryJob: kotlinx.coroutines.Job? = null
-
+
private data class SpeedState(
var lastBytes: Long = 0,
var lastTime: Long = System.currentTimeMillis(),
var smoothedSpeed: Double? = null,
var lastEtaString: String? = null
)
+
private val speedStates = mutableMapOf()
inner class LocalBinder : Binder() {
@@ -62,68 +61,75 @@ class QuickShareService : Service() {
override fun onCreate() {
super.onCreate()
createNotificationChannel()
-
+
startForeground(NOTIFICATION_ID, createNotification("Quick Share is active"))
-
+
server = QuickShareServer(this) { connection ->
val id = java.util.UUID.randomUUID().toString()
activeConnections[id] = connection
-
+
connection.onConnectionReady = { conn ->
discoveryJob?.cancel() // Transfer started, abort timeout
val pin = conn.ukey2Context?.authString ?: ""
Log.d(TAG, "Connection ready, PIN: $pin")
updateForegroundNotification("PIN: $pin - Waiting for files...")
-
+
var lastUpdate = 0L
- conn.onFileProgress = { fileName, percent, bytesTransferred, totalSize, transferId ->
- val now = System.currentTimeMillis()
- if (now - lastUpdate > 800) { // Throttle updates
- val state = speedStates.getOrPut(transferId) { SpeedState(bytesTransferred, now) }
- val timeDiff = (now - state.lastTime) / 1000.0
-
- var etaString: String? = null
- if (timeDiff >= 1.0) {
- val bytesDiff = bytesTransferred - state.lastBytes
- val intervalSpeed = bytesDiff / timeDiff
-
- val alpha = 0.4
- val newSpeed = if (state.smoothedSpeed != null) {
- (alpha * intervalSpeed) + ((1.0 - alpha) * state.smoothedSpeed!!)
- } else {
- intervalSpeed
+ conn.onFileProgress =
+ { fileName, percent, bytesTransferred, totalSize, transferId ->
+ val now = System.currentTimeMillis()
+ if (now - lastUpdate > 800) { // Throttle updates
+ val state = speedStates.getOrPut(transferId) {
+ SpeedState(
+ bytesTransferred,
+ now
+ )
}
- state.smoothedSpeed = newSpeed
- state.lastBytes = bytesTransferred
- state.lastTime = now
-
- if (newSpeed > 0) {
- val remainingBytes = (totalSize - bytesTransferred).coerceAtLeast(0)
- val secondsRemaining = (remainingBytes / newSpeed).toLong()
- etaString = if (secondsRemaining < 60) {
- "$secondsRemaining sec remaining"
+ val timeDiff = (now - state.lastTime) / 1000.0
+
+ var etaString: String? = null
+ if (timeDiff >= 1.0) {
+ val bytesDiff = bytesTransferred - state.lastBytes
+ val intervalSpeed = bytesDiff / timeDiff
+
+ val alpha = 0.4
+ val newSpeed = if (state.smoothedSpeed != null) {
+ (alpha * intervalSpeed) + ((1.0 - alpha) * state.smoothedSpeed!!)
} else {
- "${secondsRemaining / 60} min remaining"
+ intervalSpeed
+ }
+ state.smoothedSpeed = newSpeed
+ state.lastBytes = bytesTransferred
+ state.lastTime = now
+
+ if (newSpeed > 0) {
+ val remainingBytes =
+ (totalSize - bytesTransferred).coerceAtLeast(0)
+ val secondsRemaining = (remainingBytes / newSpeed).toLong()
+ etaString = if (secondsRemaining < 60) {
+ "$secondsRemaining sec remaining"
+ } else {
+ "${secondsRemaining / 60} min remaining"
+ }
}
}
- }
- lastUpdate = now
- com.sameerasw.airsync.utils.NotificationUtil.showFileProgress(
- this@QuickShareService,
- transferId.hashCode(),
- fileName,
- percent,
- transferId,
- isSending = false,
- etaString = etaString ?: state.lastEtaString ?: "Calculating..."
- )
- if (etaString != null) {
- state.lastEtaString = etaString
+ lastUpdate = now
+ com.sameerasw.airsync.utils.NotificationUtil.showFileProgress(
+ this@QuickShareService,
+ transferId.hashCode(),
+ fileName,
+ percent,
+ transferId,
+ isSending = false,
+ etaString = etaString ?: state.lastEtaString ?: "Calculating..."
+ )
+ if (etaString != null) {
+ state.lastEtaString = etaString
+ }
}
}
- }
-
+
conn.onFileComplete = { fileName, transferId, success, uri ->
speedStates.remove(transferId)
com.sameerasw.airsync.utils.NotificationUtil.showFileComplete(
@@ -136,17 +142,18 @@ class QuickShareService : Service() {
)
}
}
-
+
connection.onIntroductionReceived = { intro ->
val deviceName = connection.endpointName ?: "Unknown Device"
val firstFileName = intro.file_metadata.firstOrNull()?.name ?: "Unknown File"
val fileCount = intro.file_metadata.size
- val displayText = if (fileCount > 1) "$firstFileName and ${fileCount - 1} more" else firstFileName
-
+ val displayText =
+ if (fileCount > 1) "$firstFileName and ${fileCount - 1} more" else firstFileName
+
serviceScope.launch {
val pairedDevice = dataStoreManager.getLastConnectedDevice().first()
val pairedName = pairedDevice?.name
-
+
if (!pairedName.isNullOrBlank() && deviceName == pairedName) {
Log.d(TAG, "Auto-accepting transfer from paired Mac: $deviceName")
connection.sendSharingResponse(ConnectionResponseFrame.Status.ACCEPT)
@@ -155,12 +162,12 @@ class QuickShareService : Service() {
}
}
}
-
+
connection.onFinished = {
activeConnections.remove(id)
val manager = getSystemService(NotificationManager::class.java)
manager.cancel(NOTIFICATION_ID + id.hashCode())
-
+
if (activeConnections.isEmpty()) {
Log.d(TAG, "All transfers finished, stopping discovery")
stopDiscovery()
@@ -177,11 +184,13 @@ class QuickShareService : Service() {
val id = intent.getStringExtra(EXTRA_CONNECTION_ID)
activeConnections[id]?.sendSharingResponse(ConnectionResponseFrame.Status.ACCEPT)
}
+
ACTION_REJECT -> {
val id = intent.getStringExtra(EXTRA_CONNECTION_ID)
activeConnections[id]?.sendSharingResponse(ConnectionResponseFrame.Status.REJECT)
activeConnections.remove(id)
}
+
ACTION_START_DISCOVERY -> {
serviceScope.launch {
val enabled = dataStoreManager.isQuickShareEnabled().first()
@@ -192,6 +201,7 @@ class QuickShareService : Service() {
}
}
}
+
ACTION_CANCEL_TRANSFER -> {
val transferIdStr = intent.getStringExtra(EXTRA_TRANSFER_ID)
val transferId = transferIdStr?.toLongOrNull()
@@ -207,6 +217,7 @@ class QuickShareService : Service() {
val manager = getSystemService(NotificationManager::class.java)
manager.cancel(transferIdStr?.hashCode() ?: 0)
}
+
else -> {
// Remove the startForeground/createNotification call from here
server.start()
@@ -217,10 +228,10 @@ class QuickShareService : Service() {
private fun startDiscoveryWithTimeout() {
discoveryJob?.cancel()
-
+
// Ensure service is in foreground with a notification while active
startForeground(NOTIFICATION_ID, createNotification("Searching for files..."))
-
+
server.start()
val port = server.port
if (port == -1) {
@@ -236,7 +247,7 @@ class QuickShareService : Service() {
updateForegroundNotification("Quick Share is visible for 60s...")
kotlinx.coroutines.delay(60_000) // 1 minute timeout
-
+
if (activeConnections.isEmpty()) {
Log.d(TAG, "Discovery timed out, stopping")
stopDiscovery()
@@ -250,7 +261,7 @@ class QuickShareService : Service() {
discoveryJob?.cancel()
discoveryJob = null
advertiser.stopAdvertising()
-
+
if (activeConnections.isEmpty()) {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
@@ -291,13 +302,23 @@ class QuickShareService : Service() {
action = ACTION_ACCEPT
putExtra(EXTRA_CONNECTION_ID, connectionId)
}
- val acceptPendingIntent = PendingIntent.getService(this, 0, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ val acceptPendingIntent = PendingIntent.getService(
+ this,
+ 0,
+ acceptIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
val rejectIntent = Intent(this, QuickShareService::class.java).apply {
action = ACTION_REJECT
putExtra(EXTRA_CONNECTION_ID, connectionId)
}
- val rejectPendingIntent = PendingIntent.getService(this, 1, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ val rejectPendingIntent = PendingIntent.getService(
+ this,
+ 1,
+ rejectIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Quick Share from $deviceName")
diff --git a/app/src/main/java/com/sameerasw/airsync/quickshare/Ukey2Context.kt b/app/src/main/java/com/sameerasw/airsync/quickshare/Ukey2Context.kt
index da53c173..a0e884ba 100644
--- a/app/src/main/java/com/sameerasw/airsync/quickshare/Ukey2Context.kt
+++ b/app/src/main/java/com/sameerasw/airsync/quickshare/Ukey2Context.kt
@@ -8,13 +8,11 @@ import com.google.security.cryptauth.lib.securegcm.Ukey2ServerInit
import com.google.security.cryptauth.lib.securemessage.EcP256PublicKey
import com.google.security.cryptauth.lib.securemessage.GenericPublicKey
import com.google.security.cryptauth.lib.securemessage.PublicKeyType
-import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.digests.SHA512Digest
import org.bouncycastle.crypto.generators.HKDFBytesGenerator
import org.bouncycastle.crypto.params.HKDFParameters
-import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.KeyPair
import java.security.KeyPairGenerator
@@ -35,10 +33,38 @@ class Ukey2Context {
}
private val D2D_SALT = byteArrayOf(
- 0x82.toByte(), 0xAA.toByte(), 0x55.toByte(), 0xA0.toByte(), 0xD3.toByte(), 0x97.toByte(), 0xF8.toByte(), 0x83.toByte(),
- 0x46.toByte(), 0xCA.toByte(), 0x1C.toByte(), 0xEE.toByte(), 0x8D.toByte(), 0x39.toByte(), 0x09.toByte(), 0xB9.toByte(),
- 0x5F.toByte(), 0x13.toByte(), 0xFA.toByte(), 0x7D.toByte(), 0xEB.toByte(), 0x1D.toByte(), 0x4A.toByte(), 0xB3.toByte(),
- 0x83.toByte(), 0x76.toByte(), 0xB8.toByte(), 0x25.toByte(), 0x6D.toByte(), 0xA8.toByte(), 0x55.toByte(), 0x10.toByte()
+ 0x82.toByte(),
+ 0xAA.toByte(),
+ 0x55.toByte(),
+ 0xA0.toByte(),
+ 0xD3.toByte(),
+ 0x97.toByte(),
+ 0xF8.toByte(),
+ 0x83.toByte(),
+ 0x46.toByte(),
+ 0xCA.toByte(),
+ 0x1C.toByte(),
+ 0xEE.toByte(),
+ 0x8D.toByte(),
+ 0x39.toByte(),
+ 0x09.toByte(),
+ 0xB9.toByte(),
+ 0x5F.toByte(),
+ 0x13.toByte(),
+ 0xFA.toByte(),
+ 0x7D.toByte(),
+ 0xEB.toByte(),
+ 0x1D.toByte(),
+ 0x4A.toByte(),
+ 0xB3.toByte(),
+ 0x83.toByte(),
+ 0x76.toByte(),
+ 0xB8.toByte(),
+ 0x25.toByte(),
+ 0x6D.toByte(),
+ 0xA8.toByte(),
+ 0x55.toByte(),
+ 0x10.toByte()
)
}
@@ -104,8 +130,9 @@ class Ukey2Context {
digest.update(clientFinishEnvelopeBytes, 0, clientFinishEnvelopeBytes.size)
val calculatedCommitment = ByteArray(digest.digestSize)
digest.doFinal(calculatedCommitment, 0)
-
- val p256Commitment = clientInit.cipher_commitments.find { it.handshake_cipher == Ukey2HandshakeCipher.P256_SHA512 }?.commitment?.toByteArray()
+
+ val p256Commitment =
+ clientInit.cipher_commitments.find { it.handshake_cipher == Ukey2HandshakeCipher.P256_SHA512 }?.commitment?.toByteArray()
if (p256Commitment == null || !p256Commitment.contentEquals(calculatedCommitment)) {
Log.w("Ukey2Context", "Commitment mismatch (bypassed for reliability)")
} else {
@@ -113,13 +140,15 @@ class Ukey2Context {
}
val clientFinished = Ukey2ClientFinished.ADAPTER.decode(
- com.google.security.cryptauth.lib.securegcm.Ukey2Message.ADAPTER.decode(clientFinishEnvelopeBytes).message_data!!
+ com.google.security.cryptauth.lib.securegcm.Ukey2Message.ADAPTER.decode(
+ clientFinishEnvelopeBytes
+ ).message_data!!
)
-
+
// 2. ECDH Shared Secret
val clientPubKeyProto = GenericPublicKey.ADAPTER.decode(clientFinished.public_key!!)
val clientPubKey = decodePublicKey(clientPubKeyProto.ec_p256_public_key!!)
-
+
val ka = KeyAgreement.getInstance("ECDH")
ka.init(keyPair.private)
ka.doPhase(clientPubKey, true)
@@ -131,7 +160,7 @@ class Ukey2Context {
// 4. HKDF Derivation — use the raw Ukey2Message envelope bytes, matching the Mac
val ukeyInfo = clientInitEnvelopeBytes + serverInitEnvelopeBytes
-
+
val authKey = hkdf(derivedSecretKey, "UKEY2 v1 auth".toByteArray(), ukeyInfo)
val nextSecret = hkdf(derivedSecretKey, "UKEY2 v1 next".toByteArray(), ukeyInfo)
@@ -149,7 +178,12 @@ class Ukey2Context {
sendHmacKey = hkdf(d2dServerKey, smsgSalt, "SIG:1".toByteArray())
}
- private fun hkdf(key: ByteArray, salt: ByteArray, info: ByteArray, length: Int = 32): ByteArray {
+ private fun hkdf(
+ key: ByteArray,
+ salt: ByteArray,
+ info: ByteArray,
+ length: Int = 32
+ ): ByteArray {
val generator = HKDFBytesGenerator(SHA256Digest())
generator.init(HKDFParameters(key, salt, info))
val result = ByteArray(length)
@@ -179,14 +213,15 @@ class Ukey2Context {
val x = java.math.BigInteger(1, ecPubKey.x.toByteArray())
val y = java.math.BigInteger(1, ecPubKey.y.toByteArray())
val ecPoint = java.security.spec.ECPoint(x, y)
-
+
val kf = java.security.KeyFactory.getInstance("EC")
-
+
// Get P-256 parameter spec
val algorithmParameters = java.security.AlgorithmParameters.getInstance("EC")
algorithmParameters.init(java.security.spec.ECGenParameterSpec("secp256r1"))
- val ecParameterSpec = algorithmParameters.getParameterSpec(java.security.spec.ECParameterSpec::class.java)
-
+ val ecParameterSpec =
+ algorithmParameters.getParameterSpec(java.security.spec.ECParameterSpec::class.java)
+
val keySpec = java.security.spec.ECPublicKeySpec(ecPoint, ecParameterSpec)
return kf.generatePublic(keySpec)
}
diff --git a/app/src/main/java/com/sameerasw/airsync/service/AirSyncService.kt b/app/src/main/java/com/sameerasw/airsync/service/AirSyncService.kt
index 400b434c..d7850963 100644
--- a/app/src/main/java/com/sameerasw/airsync/service/AirSyncService.kt
+++ b/app/src/main/java/com/sameerasw/airsync/service/AirSyncService.kt
@@ -50,12 +50,26 @@ class AirSyncService : Service() {
// Network state tracking
private var networkCallback: ConnectivityManager.NetworkCallback? = null
+ private val connectionStatusListener: (Boolean) -> Unit = { _ ->
+ scope.launch {
+ updateNotification()
+ }
+ }
+
override fun onCreate() {
super.onCreate()
Log.d(TAG, "AirSyncService created")
createNotificationChannel()
MacDeviceStatusManager.startMonitoring(this)
registerNetworkCallback()
+ WebSocketUtil.registerConnectionStatusListener(connectionStatusListener)
+
+ // Monitor connection status, auto-reconnect, and battery status to update notification live
+ scope.launch {
+ MacDeviceStatusManager.macDeviceStatus.collect {
+ updateNotification()
+ }
+ }
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -205,7 +219,10 @@ class AirSyncService : Service() {
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
- Log.d(TAG, "Network available, triggering burst broadcast and refreshing socket")
+ Log.d(
+ TAG,
+ "Network available, triggering burst broadcast and refreshing socket"
+ )
// Refresh UDP socket to bind to new network interface
UDPDiscoveryManager.refreshSocket()
// When network becomes available, do a burst to announce ourselves
@@ -237,6 +254,15 @@ class AirSyncService : Service() {
}
}
+ private fun updateNotification() {
+ try {
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.notify(NOTIFICATION_ID, buildNotification())
+ } catch (e: Exception) {
+ Log.e(TAG, "Error updating foreground notification", e)
+ }
+ }
+
private fun buildNotification(): Notification {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
@@ -255,18 +281,49 @@ class AirSyncService : Service() {
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- if (isScanning && connectedDeviceName == null) {
+ val isConnected = WebSocketUtil.isConnected()
+ val isAuto = WebSocketUtil.isAutoReconnecting()
+ val isConnecting = WebSocketUtil.isConnecting()
+
+ val dataStoreManager = DataStoreManager.getInstance(applicationContext)
+ val lastDevice = runBlocking { dataStoreManager.getLastConnectedDevice().first() }
+ val macStatus = MacDeviceStatusManager.macDeviceStatus.value
+
+ if (isConnected && lastDevice != null) {
+ val name = lastDevice.name
builder.setContentTitle(getString(R.string.app_name))
- builder.setContentText(getString(R.string.no_device_connected))
- } else {
- val name = connectedDeviceName ?: "Mac"
+
+ val batteryText = macStatus?.let { status ->
+ val level = status.battery.level
+ if (level >= 0) {
+ val pct = level.coerceIn(0, 100)
+ if (status.battery.isCharging) " ($pct% Charging)" else " ($pct%)"
+ } else ""
+ } ?: ""
+
+ builder.setContentText(getString(R.string.connected_to_device, name) + batteryText)
+ builder.addAction(
+ R.drawable.rounded_link_off_24,
+ getString(R.string.disconnect),
+ disconnectPendingIntent
+ )
+ } else if (com.sameerasw.airsync.data.ble.BleGattServer.isAnyAuthenticated() && lastDevice != null) {
builder.setContentTitle(getString(R.string.app_name))
- builder.setContentText(getString(R.string.connected_to_device, name))
+ builder.setContentText("Connected to ${lastDevice.name} via Bluetooth")
builder.addAction(
R.drawable.rounded_link_off_24,
getString(R.string.disconnect),
disconnectPendingIntent
)
+ } else if (isAuto) {
+ builder.setContentTitle("Reconnecting...")
+ builder.setContentText(if (isConnecting) "Trying to connect to Mac..." else "Waiting to retry connection...")
+ } else if (isConnecting) {
+ builder.setContentTitle("Connecting...")
+ builder.setContentText("Connecting to last device...")
+ } else {
+ builder.setContentTitle(getString(R.string.app_name))
+ builder.setContentText(getString(R.string.no_device_connected))
}
return builder.build()
@@ -276,6 +333,7 @@ class AirSyncService : Service() {
override fun onDestroy() {
Log.d(TAG, "AirSyncService destroyed")
+ WebSocketUtil.unregisterConnectionStatusListener(connectionStatusListener)
networkCallback?.let {
try {
diff --git a/app/src/main/java/com/sameerasw/airsync/service/MacMediaPlayerService.kt b/app/src/main/java/com/sameerasw/airsync/service/MacMediaPlayerService.kt
index 6f9ef219..fa43708d 100644
--- a/app/src/main/java/com/sameerasw/airsync/service/MacMediaPlayerService.kt
+++ b/app/src/main/java/com/sameerasw/airsync/service/MacMediaPlayerService.kt
@@ -165,8 +165,16 @@ class MacMediaPlayerService : Service() {
val duration = intent.getLongExtra(EXTRA_DURATION, 0L)
val timestamp = intent.getStringExtra(EXTRA_TIMESTAMP)
val playbackRate = intent.getDoubleExtra(EXTRA_PLAYBACK_RATE, 1.0)
-
- startMacMediaSession(title, artist, isPlaying, elapsedTime, duration, timestamp, playbackRate)
+
+ startMacMediaSession(
+ title,
+ artist,
+ isPlaying,
+ elapsedTime,
+ duration,
+ timestamp,
+ playbackRate
+ )
}
ACTION_UPDATE_MAC_MEDIA -> {
@@ -177,8 +185,16 @@ class MacMediaPlayerService : Service() {
val duration = intent.getLongExtra(EXTRA_DURATION, 0L)
val timestamp = intent.getStringExtra(EXTRA_TIMESTAMP)
val playbackRate = intent.getDoubleExtra(EXTRA_PLAYBACK_RATE, 1.0)
-
- updateMacMediaSession(title, artist, isPlaying, elapsedTime, duration, timestamp, playbackRate)
+
+ updateMacMediaSession(
+ title,
+ artist,
+ isPlaying,
+ elapsedTime,
+ duration,
+ timestamp,
+ playbackRate
+ )
}
// Handle media control actions from notification buttons
"MAC_MEDIA_play" -> {
@@ -298,7 +314,9 @@ class MacMediaPlayerService : Service() {
PlaybackStateCompat.ACTION_STOP
// Parse ISO8601 timestamp to calculate elapsed time since reporting
- var position = if (elapsedTime >= 0) elapsedTime else (mediaSession?.controller?.playbackState?.position ?: 0L)
+ var position =
+ if (elapsedTime >= 0) elapsedTime else (mediaSession?.controller?.playbackState?.position
+ ?: 0L)
if (isPlaying && !timestamp.isNullOrEmpty()) {
try {
val reportedAt = java.time.Instant.parse(timestamp).toEpochMilli()
@@ -430,7 +448,7 @@ class MacMediaPlayerService : Service() {
val duration = metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)
val position = it.controller.playbackState?.position ?: 0L
val speed = it.controller.playbackState?.playbackSpeed?.toDouble() ?: 1.0
-
+
updateMacMediaSession(title, artist, isPlaying, position, duration, null, speed)
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/service/MediaNotificationListener.kt b/app/src/main/java/com/sameerasw/airsync/service/MediaNotificationListener.kt
index c0f22a71..32d88ea9 100644
--- a/app/src/main/java/com/sameerasw/airsync/service/MediaNotificationListener.kt
+++ b/app/src/main/java/com/sameerasw/airsync/service/MediaNotificationListener.kt
@@ -119,12 +119,14 @@ class MediaNotificationListener : NotificationListenerService() {
val title = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) ?: ""
val artist = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: ""
val isPlaying = playbackState?.state == PlaybackState.STATE_PLAYING
- val durationMs = metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L
+ val durationMs =
+ metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L
val positionMs = playbackState?.position ?: 0L
val positionTimestampMs = System.currentTimeMillis()
val isBuffering = when (playbackState?.state) {
PlaybackState.STATE_BUFFERING,
PlaybackState.STATE_CONNECTING -> true
+
else -> false
}
@@ -540,7 +542,7 @@ class MediaNotificationListener : NotificationListenerService() {
serviceScope.launch {
try {
val id = NotificationDismissalUtil.getIdForSbn(sbn) ?: sbn.key
-
+
// If this dismissal was initiated by our own dismiss request, skip echo
val wasSuppressed = NotificationDismissalUtil.consumeSuppressed(id)
if (wasSuppressed) {
@@ -558,7 +560,7 @@ class MediaNotificationListener : NotificationListenerService() {
)
)
WebSocketUtil.sendMessage(updateJson)
-
+
Log.d(TAG, "Sent notification removal sync for $id")
// Remove from caches since it's gone now
@@ -641,12 +643,13 @@ class MediaNotificationListener : NotificationListenerService() {
return@launch
}
- // Generate unique notification ID
- val notificationId = NotificationDismissalUtil.generateNotificationId(
- sbn.packageName,
- title,
- sbn.postTime
- )
+ // Retrieve existing notification ID or generate a new one
+ val notificationId = NotificationDismissalUtil.getIdBySystemKey(sbn.key)
+ ?: NotificationDismissalUtil.generateNotificationId(
+ sbn.packageName,
+ title,
+ sbn.postTime
+ )
// Store notification for potential dismissal or actions
NotificationDismissalUtil.storeNotification(notificationId, sbn)
@@ -668,10 +671,37 @@ class MediaNotificationListener : NotificationListenerService() {
Log.w(TAG, "Failed to extract actions: ${e.message}")
}
+ // Check for progress bar extras
+ var progress: Int? = null
+ var progressMax: Int? = null
+ var progressIndeterminate: Boolean? = null
+ val ongoing = (notification.flags and Notification.FLAG_ONGOING_EVENT) != 0
+
+ try {
+ val hasProgress = extras.containsKey(Notification.EXTRA_PROGRESS) ||
+ extras.containsKey(Notification.EXTRA_PROGRESS_MAX)
+ if (hasProgress) {
+ val maxVal = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0)
+ val progressVal = extras.getInt(Notification.EXTRA_PROGRESS, 0)
+ val indeterminateVal = extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE, false)
+
+ if (maxVal > 0 || indeterminateVal) {
+ progress = progressVal
+ progressMax = maxVal
+ progressIndeterminate = indeterminateVal
+ }
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to parse notification progress: ${e.message}")
+ }
+
// Get notification priority (alerting vs silent)
- val priority = getNotificationPriority(sbn)
+ var priority = getNotificationPriority(sbn)
+ if (progressMax != null || progressIndeterminate == true) {
+ priority = "silent"
+ }
- // Create notification JSON with actions
+ // Create notification JSON with actions and progress details
val notificationJson = JsonUtil.toSingleLine(
JsonUtil.createNotificationJson(
id = notificationId,
@@ -680,7 +710,11 @@ class MediaNotificationListener : NotificationListenerService() {
app = appName,
packageName = sbn.packageName,
priority = priority,
- actions = actions
+ actions = actions,
+ progress = progress,
+ progressMax = progressMax,
+ progressIndeterminate = progressIndeterminate,
+ ongoing = ongoing
)
)
diff --git a/app/src/main/java/com/sameerasw/airsync/service/NotificationActionReceiver.kt b/app/src/main/java/com/sameerasw/airsync/service/NotificationActionReceiver.kt
index 50297b8e..4d7a376a 100644
--- a/app/src/main/java/com/sameerasw/airsync/service/NotificationActionReceiver.kt
+++ b/app/src/main/java/com/sameerasw/airsync/service/NotificationActionReceiver.kt
@@ -50,9 +50,16 @@ class NotificationActionReceiver : BroadcastReceiver() {
if (!transferId.isNullOrEmpty()) {
Log.d(TAG, "Cancelling transfer $transferId from notification")
// Also try cancelling Quick Share transfer
- val qsIntent = Intent(context, com.sameerasw.airsync.quickshare.QuickShareService::class.java).apply {
- action = com.sameerasw.airsync.quickshare.QuickShareService.ACTION_CANCEL_TRANSFER
- putExtra(com.sameerasw.airsync.quickshare.QuickShareService.EXTRA_TRANSFER_ID, transferId)
+ val qsIntent = Intent(
+ context,
+ com.sameerasw.airsync.quickshare.QuickShareService::class.java
+ ).apply {
+ action =
+ com.sameerasw.airsync.quickshare.QuickShareService.ACTION_CANCEL_TRANSFER
+ putExtra(
+ com.sameerasw.airsync.quickshare.QuickShareService.EXTRA_TRANSFER_ID,
+ transferId
+ )
}
context.startService(qsIntent)
}
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/AdbMdnsDiscovery.kt b/app/src/main/java/com/sameerasw/airsync/utils/AdbMdnsDiscovery.kt
index f24d088e..e428bba7 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/AdbMdnsDiscovery.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/AdbMdnsDiscovery.kt
@@ -30,10 +30,13 @@ class AdbMdnsDiscovery(context: Context) {
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
Log.d(TAG, "Service found: ${serviceInfo.serviceName}")
-
+
synchronized(discoveredServices) {
if (discoveredServices.any { it.serviceName == serviceInfo.serviceName }) {
- Log.d(TAG, "Service ${serviceInfo.serviceName} already discovered, skipping resolve")
+ Log.d(
+ TAG,
+ "Service ${serviceInfo.serviceName} already discovered, skipping resolve"
+ )
return
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/CallControlUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/CallControlUtil.kt
index c76da93e..23603636 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/CallControlUtil.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/CallControlUtil.kt
@@ -6,9 +6,9 @@ import android.content.pm.PackageManager
import android.media.AudioManager
import android.os.Build
import android.telecom.TelecomManager
+import android.util.Log
import android.view.KeyEvent
import androidx.core.content.ContextCompat
-import android.util.Log
object CallControlUtil {
private const val TAG = "CallControlUtil"
@@ -26,7 +26,8 @@ object CallControlUtil {
if (hasPermission) {
try {
- val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager
+ val telecomManager =
+ context.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager
if (telecomManager != null) {
Log.d(TAG, "Accepting ringing call via TelecomManager")
telecomManager.acceptRingingCall()
@@ -36,7 +37,10 @@ object CallControlUtil {
Log.e(TAG, "Failed to accept ringing call via TelecomManager, falling back", e)
}
} else {
- Log.w(TAG, "ANSWER_PHONE_CALLS permission not granted, falling back to media key hook")
+ Log.w(
+ TAG,
+ "ANSWER_PHONE_CALLS permission not granted, falling back to media key hook"
+ )
}
}
@@ -57,7 +61,8 @@ object CallControlUtil {
if (hasPermission) {
try {
- val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager
+ val telecomManager =
+ context.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager
if (telecomManager != null) {
Log.d(TAG, "Ending/declining call via TelecomManager")
val success = telecomManager.endCall()
@@ -70,7 +75,10 @@ object CallControlUtil {
Log.e(TAG, "Failed to end call via TelecomManager, falling back", e)
}
} else {
- Log.w(TAG, "ANSWER_PHONE_CALLS permission not granted, falling back to media key hook")
+ Log.w(
+ TAG,
+ "ANSWER_PHONE_CALLS permission not granted, falling back to media key hook"
+ )
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/ClipboardSyncManager.kt b/app/src/main/java/com/sameerasw/airsync/utils/ClipboardSyncManager.kt
index 28acc8ae..1962c4b1 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/ClipboardSyncManager.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/ClipboardSyncManager.kt
@@ -172,15 +172,7 @@ object ClipboardSyncManager {
} catch (_: Exception) {
true
}
- // Only for Plus and while connected
- val isConnected = WebSocketUtil.isConnected()
- val last = try {
- dataStoreManager.getLastConnectedDevice().first()
- } catch (_: Exception) {
- null
- }
- val isPlus = last?.isPlus == true
- if (continueEnabled && isConnected && isPlus && isLinkOnly(text)) {
+ if (continueEnabled && WebSocketUtil.isConnected() && isLinkOnly(text)) {
NotificationUtil.showContinueBrowsingLink(context, text.trim(), keepPrevious)
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/DeviceInfoUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/DeviceInfoUtil.kt
index 49d006cd..403a1c42 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/DeviceInfoUtil.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/DeviceInfoUtil.kt
@@ -87,6 +87,7 @@ object DeviceInfoUtil {
@Suppress("DEPRECATION")
val wifiInfo = wifiManager.connectionInfo
+
@Suppress("DEPRECATION")
val ipAddress = wifiInfo.ipAddress
if (ipAddress != 0) {
@@ -210,7 +211,7 @@ object DeviceInfoUtil {
fun isBlurProblematicDevice(): Boolean {
// Samsung devices on One UI 7 (Android 15) or below have a broken blur implementation
// that causes a gray screen overlay. Disable it for them. (╯°□°)╯︵ ┻━┻
- return Build.MANUFACTURER.equals("samsung", ignoreCase = true) &&
+ return Build.MANUFACTURER.equals("samsung", ignoreCase = true) &&
Build.VERSION.SDK_INT <= 35 // Android 15
}
@@ -224,7 +225,8 @@ object DeviceInfoUtil {
}
fun isPowerSaveMode(context: Context): Boolean {
- val powerManager = context.getSystemService(Context.POWER_SERVICE) as? android.os.PowerManager
+ val powerManager =
+ context.getSystemService(Context.POWER_SERVICE) as? android.os.PowerManager
return powerManager?.isPowerSaveMode == true
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/DevicePreviewResolver.kt b/app/src/main/java/com/sameerasw/airsync/utils/DevicePreviewResolver.kt
index 81fb4aa8..fd65cf73 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/DevicePreviewResolver.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/DevicePreviewResolver.kt
@@ -1,7 +1,6 @@
package com.sameerasw.airsync.utils
import androidx.annotation.DrawableRes
-import com.sameerasw.airsync.R
import com.sameerasw.airsync.domain.model.ConnectedDevice
/**
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt
index 0ee5ca05..0709674e 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt
@@ -104,7 +104,11 @@ object JsonUtil {
app: String,
packageName: String,
priority: String = "alerting",
- actions: List>
+ actions: List>,
+ progress: Int? = null,
+ progressMax: Int? = null,
+ progressIndeterminate: Boolean? = null,
+ ongoing: Boolean? = null
): String {
val actionsJson = if (actions.isNotEmpty()) {
val items = actions.joinToString(",") { (name, type) ->
@@ -114,11 +118,15 @@ object JsonUtil {
} else {
""
}
+ val progressJson = if (progress != null) ",\"progress\":$progress" else ""
+ val progressMaxJson = if (progressMax != null) ",\"progressMax\":$progressMax" else ""
+ val progressIndeterminateJson = if (progressIndeterminate != null) ",\"progressIndeterminate\":$progressIndeterminate" else ""
+ val ongoingJson = if (ongoing != null) ",\"ongoing\":$ongoing" else ""
return """{"type":"notification","data":{"id":"$id","title":"${escape(title)}","body":"${
escape(
body
)
- }","app":"${escape(app)}","package":"${escape(packageName)}","priority":"$priority"$actionsJson}}"""
+ }","app":"${escape(app)}","package":"${escape(packageName)}","priority":"$priority"$actionsJson$progressJson$progressMaxJson$progressIndeterminateJson$ongoingJson}}"""
}
/**
@@ -164,7 +172,8 @@ object JsonUtil {
likeStatus: String
): String {
val albumArtJson = if (albumArt != null) ",\"albumArt\":\"$albumArt\"" else ""
- val albumArtLiteJson = if (albumArtLite != null) ",\"albumArtLite\":\"$albumArtLite\"" else ""
+ val albumArtLiteJson =
+ if (albumArtLite != null) ",\"albumArtLite\":\"$albumArtLite\"" else ""
return """{"type":"status","data":{"battery":{"level":$batteryLevel,"isCharging":$isCharging},"isPaired":$isPaired,"music":{"isPlaying":$isPlaying,"title":"$title","artist":"$artist","volume":$volume,"isMuted":$isMuted$albumArtJson$albumArtLiteJson,"duration":$duration,"position":$position,"positionTimestamp":$positionTimestamp,"isBuffering":$isBuffering,"likeStatus":"$likeStatus"}}}"""
}
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/MacDeviceStatusManager.kt b/app/src/main/java/com/sameerasw/airsync/utils/MacDeviceStatusManager.kt
index 65cacb01..e873edf9 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/MacDeviceStatusManager.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/MacDeviceStatusManager.kt
@@ -5,8 +5,8 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import android.util.Log
-import com.sameerasw.airsync.data.local.DataStoreManager
import com.sameerasw.airsync.data.ble.BleGattServer
+import com.sameerasw.airsync.data.local.DataStoreManager
import com.sameerasw.airsync.domain.model.MacBattery
import com.sameerasw.airsync.domain.model.MacDeviceStatus
import com.sameerasw.airsync.domain.model.MacMusicInfo
@@ -159,7 +159,8 @@ object MacDeviceStatusManager {
CoroutineScope(Dispatchers.IO).launch {
val ds = DataStoreManager(context)
val isMediaControlsEnabled = ds.getMacMediaControlsEnabled().first()
- val isConnected = WebSocketUtil.isConnected() || WebSocketUtil.isConnecting() || BleGattServer.isAnyAuthenticated()
+ val isConnected =
+ WebSocketUtil.isConnected() || WebSocketUtil.isConnecting() || BleGattServer.isAnyAuthenticated()
val isEssentialsEnabled = ds.getEssentialsConnectionEnabled().first()
if (isConnected && isMediaControlsEnabled && (title.isNotEmpty() || artist.isNotEmpty() || isPlaying)) {
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt b/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt
index 499f4828..69760501 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/MacModelMapper.kt
@@ -45,7 +45,7 @@ object MacModelMapper {
val nameStr = name.replace(" ", "").lowercase()
val typeStr = deviceType?.replace(" ", "")?.lowercase() ?: ""
val hay = "$nameStr$modelStr$typeStr".lowercase()
-
+
return when {
hay.contains("macbookair") -> R.drawable.rounded_laptop_mac_24
hay.contains("macbookpro") -> R.drawable.rounded_laptop_mac_24
@@ -63,25 +63,89 @@ object MacModelMapper {
// 1) Explicit Model Matching based on user mapping
return when {
// MacBook Air Gen 3
- isMatch(model, listOf("Mac17,4", "Mac17,3", "Mac16,13", "Mac16,12", "Mac15,13", "Mac15,12", "Mac14,15", "Mac14,2")) -> R.drawable.macbook_air_gen3
+ isMatch(
+ model,
+ listOf(
+ "Mac17,4",
+ "Mac17,3",
+ "Mac16,13",
+ "Mac16,12",
+ "Mac15,13",
+ "Mac15,12",
+ "Mac14,15",
+ "Mac14,2"
+ )
+ ) -> R.drawable.macbook_air_gen3
// MacBook Air Gen 2
- isMatch(model, listOf("MacBookAir10,1", "MacBookAir9,1", "MacBookAir8,2", "MacBookAir8,1")) -> R.drawable.macbook_air_gen2
+ isMatch(
+ model,
+ listOf("MacBookAir10,1", "MacBookAir9,1", "MacBookAir8,2", "MacBookAir8,1")
+ ) -> R.drawable.macbook_air_gen2
// MacBook Pro Gen 3
- isMatch(model, listOf("Mac17,7", "Mac17,9", "Mac17,6", "Mac17,8", "Mac17,2", "Mac16,1", "Mac16,6", "Mac16,8", "Mac16,7", "Mac16,5", "Mac15,3", "Mac15,6", "Mac15,8", "Mac15,10", "Mac15,7", "Mac15,9", "Mac15,11", "Mac14,5", "Mac14,9", "Mac14,6", "Mac14,10", "MacBookPro18,3", "MacBookPro18,4", "MacBookPro18,1", "MacBookPro18,2")) -> R.drawable.macbook_pro_gen3
+ isMatch(
+ model,
+ listOf(
+ "Mac17,7",
+ "Mac17,9",
+ "Mac17,6",
+ "Mac17,8",
+ "Mac17,2",
+ "Mac16,1",
+ "Mac16,6",
+ "Mac16,8",
+ "Mac16,7",
+ "Mac16,5",
+ "Mac15,3",
+ "Mac15,6",
+ "Mac15,8",
+ "Mac15,10",
+ "Mac15,7",
+ "Mac15,9",
+ "Mac15,11",
+ "Mac14,5",
+ "Mac14,9",
+ "Mac14,6",
+ "Mac14,10",
+ "MacBookPro18,3",
+ "MacBookPro18,4",
+ "MacBookPro18,1",
+ "MacBookPro18,2"
+ )
+ ) -> R.drawable.macbook_pro_gen3
// MacBook Pro Gen 2
- isMatch(model, listOf("Mac14,7", "MacBookPro17,1", "MacBookPro16,3", "MacBookPro16,2", "MacBookPro16,1", "MacBookPro16,4", "MacBookPro15,4", "MacBookPro15,1", "MacBookPro15,3", "MacBookPro15,2")) -> R.drawable.macbook_pro_gen2
+ isMatch(
+ model,
+ listOf(
+ "Mac14,7",
+ "MacBookPro17,1",
+ "MacBookPro16,3",
+ "MacBookPro16,2",
+ "MacBookPro16,1",
+ "MacBookPro16,4",
+ "MacBookPro15,4",
+ "MacBookPro15,1",
+ "MacBookPro15,3",
+ "MacBookPro15,2"
+ )
+ ) -> R.drawable.macbook_pro_gen2
// Mac mini Gen 3
isMatch(model, listOf("Mac16,11", "Mac16,10")) -> R.drawable.macmini_gen3
// iMac Gen 3
- isMatch(model, listOf("Mac16,3", "Mac16,2", "Mac15,5", "Mac15,4", "iMac21,1", "iMac21,2")) -> R.drawable.imac_gen3
+ isMatch(
+ model,
+ listOf("Mac16,3", "Mac16,2", "Mac15,5", "Mac15,4", "iMac21,1", "iMac21,2")
+ ) -> R.drawable.imac_gen3
// iMac Gen 2
- isMatch(model, listOf("iMac20,1", "iMac20,2", "iMac19,1", "iMac19,2", "iMacPro1,1")) -> R.drawable.imac_gen2
+ isMatch(
+ model,
+ listOf("iMac20,1", "iMac20,2", "iMac19,1", "iMac19,2", "iMacPro1,1")
+ ) -> R.drawable.imac_gen2
// MacBook Neo
model.contains("Mac17,5", ignoreCase = true) -> R.drawable.macbook_neo
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/PermissionUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/PermissionUtil.kt
index 49da76d9..e7d18211 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/PermissionUtil.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/PermissionUtil.kt
@@ -209,7 +209,10 @@ object PermissionUtil {
missing.add("Local Network Access")
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isAnswerCallsPermissionGranted(context)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isAnswerCallsPermissionGranted(
+ context
+ )
+ ) {
missing.add("Answer Calls")
}
@@ -274,7 +277,10 @@ object PermissionUtil {
optional.add("Local Network Access")
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isAnswerCallsPermissionGranted(context)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isAnswerCallsPermissionGranted(
+ context
+ )
+ ) {
optional.add("Answer Calls")
}
@@ -310,14 +316,20 @@ object PermissionUtil {
Manifest.permission.READ_PHONE_STATE
) == PackageManager.PERMISSION_GRANTED
}
-
+
/**
* Check if Bluetooth permissions are granted (Connect and Advertise/Scan on Android 12+)
*/
fun isBluetoothPermissionsGranted(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED &&
- ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADVERTISE) == PackageManager.PERMISSION_GRANTED
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED &&
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.BLUETOOTH_ADVERTISE
+ ) == PackageManager.PERMISSION_GRANTED
} else {
// On older versions, manifest permissions are enough
true
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/ServiceManager.kt b/app/src/main/java/com/sameerasw/airsync/utils/ServiceManager.kt
index 4a15335b..f8aa4b6a 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/ServiceManager.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/ServiceManager.kt
@@ -13,7 +13,7 @@ import kotlinx.coroutines.launch
* and user preferences for background features.
*/
object ServiceManager {
-
+
/**
* Determines if any background service should be running based on settings.
*/
@@ -22,7 +22,7 @@ object ServiceManager {
val isConnected = WebSocketUtil.isConnected()
val isAutoReconnectEnabled = dataStore.getAutoReconnectEnabled().first()
val isDiscoveryEnabled = dataStore.getDeviceDiscoveryEnabled().first()
-
+
// Service needs to run if:
// 1. We are currently connected
// 2. We need to auto-reconnect in the background
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/ShortcutUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/ShortcutUtil.kt
index c1235d7f..6fcc2400 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/ShortcutUtil.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/ShortcutUtil.kt
@@ -31,10 +31,19 @@ object ShortcutUtil {
ShortcutInfoCompat.Builder(context, SHORTCUT_ID_SCAN)
.setShortLabel("Scan")
.setLongLabel("Scan QR Code")
- .setIcon(IconCompat.createWithResource(context, R.drawable.rounded_qr_code_scanner_24))
- .setIntent(Intent(context, com.sameerasw.airsync.presentation.ui.activities.QRScannerActivity::class.java).apply {
- action = "com.sameerasw.airsync.SCAN_QR"
- })
+ .setIcon(
+ IconCompat.createWithResource(
+ context,
+ R.drawable.rounded_qr_code_scanner_24
+ )
+ )
+ .setIntent(
+ Intent(
+ context,
+ com.sameerasw.airsync.presentation.ui.activities.QRScannerActivity::class.java
+ ).apply {
+ action = "com.sameerasw.airsync.SCAN_QR"
+ })
.build()
)
@@ -56,11 +65,20 @@ object ShortcutUtil {
ShortcutInfoCompat.Builder(context, SHORTCUT_ID_REMOTE)
.setShortLabel("Remote")
.setLongLabel("Remote Control")
- .setIcon(IconCompat.createWithResource(context, R.drawable.rounded_compare_arrows_24))
- .setIntent(Intent(context, com.sameerasw.airsync.MainActivity::class.java).apply {
- action = DASH_ACTION_REMOTE
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
- })
+ .setIcon(
+ IconCompat.createWithResource(
+ context,
+ R.drawable.rounded_compare_arrows_24
+ )
+ )
+ .setIntent(
+ Intent(
+ context,
+ com.sameerasw.airsync.MainActivity::class.java
+ ).apply {
+ action = DASH_ACTION_REMOTE
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ })
.build()
)
@@ -93,7 +111,12 @@ object ShortcutUtil {
ShortcutInfoCompat.Builder(context, SHORTCUT_ID_DISCONNECT)
.setShortLabel("Disconnect")
.setLongLabel("Disconnect")
- .setIcon(IconCompat.createWithResource(context, R.drawable.rounded_mimo_disconnect_24))
+ .setIcon(
+ IconCompat.createWithResource(
+ context,
+ R.drawable.rounded_mimo_disconnect_24
+ )
+ )
.setIntent(Intent(context, ClipboardActionActivity::class.java).apply {
action = DASH_ACTION_DISCONNECT
})
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt b/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt
index cb414316..5c02e201 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt
@@ -45,7 +45,9 @@ object SyncManager {
// Heartbeat: Sync battery over BLE if authenticated and WS not connected to keep Mac connection alive
if (com.sameerasw.airsync.data.ble.BleGattServer.isAnyAuthenticated() && !WebSocketUtil.isConnected()) {
val currentBattery = DeviceInfoUtil.getBatteryInfo(context)
- com.sameerasw.airsync.data.ble.BleTransportBridge.sendBatteryStatus(currentBattery)
+ com.sameerasw.airsync.data.ble.BleTransportBridge.sendBatteryStatus(
+ currentBattery
+ )
}
// Check if sync is needed (either via WebSocket or BLE)
@@ -201,8 +203,10 @@ object SyncManager {
Log.d(TAG, "Discovered ADB ports: $adbPorts")
val deviceId = DeviceInfoUtil.getDeviceId(context)
- val symmetricKey = dataStoreManager.getLastConnectedDevice().first()?.symmetricKey ?: ""
- val bleAuthToken = com.sameerasw.airsync.data.ble.BleTransportBridge.deriveAuthToken(symmetricKey)
+ val symmetricKey =
+ dataStoreManager.getLastConnectedDevice().first()?.symmetricKey ?: ""
+ val bleAuthToken =
+ com.sameerasw.airsync.data.ble.BleTransportBridge.deriveAuthToken(symmetricKey)
val liteDeviceInfoJson = JsonUtil.createDeviceInfoJson(
id = deviceId,
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/UDPDiscoveryManager.kt b/app/src/main/java/com/sameerasw/airsync/utils/UDPDiscoveryManager.kt
index d520447f..8465d1ae 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/UDPDiscoveryManager.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/UDPDiscoveryManager.kt
@@ -57,6 +57,7 @@ object UDPDiscoveryManager {
@Volatile
private var isRunning = false
+
@Volatile
private var currentMode = DiscoveryMode.ACTIVE
@@ -69,7 +70,8 @@ object UDPDiscoveryManager {
private fun acquireMulticastLock(context: Context) {
try {
if (multicastLock == null) {
- val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as android.net.wifi.WifiManager
+ val wm =
+ context.applicationContext.getSystemService(Context.WIFI_SERVICE) as android.net.wifi.WifiManager
multicastLock = wm.createMulticastLock("AirSync:DiscoveryLock")
}
if (multicastLock?.isHeld == false) {
@@ -131,7 +133,7 @@ object UDPDiscoveryManager {
Log.d(TAG, "Discovery disabled, skipping burst broadcast")
return
}
-
+
Log.d(TAG, "Starting burst broadcast for ${durationMs}ms")
burstJob?.cancel()
burstJob = CoroutineScope(Dispatchers.IO).launch {
@@ -240,7 +242,8 @@ object UDPDiscoveryManager {
Log.e(TAG, "Error receiving packet: ${e.message}, recreating socket...")
try {
socket?.close()
- } catch (_: Exception) {}
+ } catch (_: Exception) {
+ }
socket = null
delay(2000)
}
@@ -376,7 +379,7 @@ object UDPDiscoveryManager {
private fun broadcastPresence(context: Context) {
if (!isDiscoveryEnabled) return
-
+
val allIps = getAllIpAddresses()
if (allIps.isEmpty()) {
return
@@ -459,7 +462,7 @@ object UDPDiscoveryManager {
private fun broadcastGoodbye(context: Context) {
if (!isDiscoveryEnabled) return
-
+
val allIps = getAllIpAddresses()
if (allIps.isEmpty()) return
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/WebDavServer.kt b/app/src/main/java/com/sameerasw/airsync/utils/WebDavServer.kt
index 22c94ed3..5367f5ee 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/WebDavServer.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/WebDavServer.kt
@@ -3,19 +3,35 @@ package com.sameerasw.airsync.utils
import android.content.Context
import android.os.Environment
import android.util.Log
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.cio.*
-import io.ktor.server.engine.*
-import io.ktor.server.plugins.statuspages.*
+import io.ktor.http.ContentType
+import io.ktor.http.HttpHeaders
+import io.ktor.http.HttpMethod
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.encodeURLPathPart
+import io.ktor.server.application.ApplicationCall
+import io.ktor.server.application.call
+import io.ktor.server.application.install
+import io.ktor.server.cio.CIO
+import io.ktor.server.engine.ApplicationEngine
+import io.ktor.server.engine.embeddedServer
+import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.request.path
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
+import io.ktor.server.response.header
+import io.ktor.server.response.respond
+import io.ktor.server.response.respondFile
+import io.ktor.server.response.respondText
+import io.ktor.server.routing.get
+import io.ktor.server.routing.head
+import io.ktor.server.routing.method
+import io.ktor.server.routing.route
+import io.ktor.server.routing.routing
import java.io.File
import java.net.ServerSocket
import java.net.URLDecoder
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
class WebDavServer(private val context: Context) {
private var engine: ApplicationEngine? = null
@@ -116,7 +132,11 @@ class WebDavServer(private val context: Context) {
val depth = call.request.headers["Depth"] ?: "1"
val xml = buildPropfindXml(file, relativePath, depth)
- call.respondText(xml, ContentType.Text.Xml.withParameter("charset", "utf-8"), HttpStatusCode.MultiStatus)
+ call.respondText(
+ xml,
+ ContentType.Text.Xml.withParameter("charset", "utf-8"),
+ HttpStatusCode.MultiStatus
+ )
}
private fun buildPropfindXml(file: File, relativePath: String, depth: String): String {
@@ -131,7 +151,8 @@ class WebDavServer(private val context: Context) {
?.filter { !it.name.startsWith(".") }
?.sortedWith(compareBy({ !it.isDirectory }, { it.name.lowercase() }))
?.forEach { child ->
- val childRelPath = if (relativePath.isEmpty()) child.name else "$relativePath/${child.name}"
+ val childRelPath =
+ if (relativePath.isEmpty()) child.name else "$relativePath/${child.name}"
appendFileEntry(sb, child, childRelPath)
}
}
@@ -170,7 +191,8 @@ class WebDavServer(private val context: Context) {
} else {
sb.append(" \n")
sb.append(" ${file.length()}\n")
- val contentType = java.net.URLConnection.guessContentTypeFromName(file.name) ?: "application/octet-stream"
+ val contentType = java.net.URLConnection.guessContentTypeFromName(file.name)
+ ?: "application/octet-stream"
sb.append(" $contentType\n")
}
sb.append(" $lastModified\n")
@@ -210,13 +232,17 @@ class WebDavServer(private val context: Context) {
if (!file.isDirectory) {
call.response.header(HttpHeaders.ContentLength, file.length().toString())
- val contentType = java.net.URLConnection.guessContentTypeFromName(file.name) ?: "application/octet-stream"
+ val contentType = java.net.URLConnection.guessContentTypeFromName(file.name)
+ ?: "application/octet-stream"
call.response.header(HttpHeaders.ContentType, contentType)
}
val rfc1123Format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US).apply {
timeZone = TimeZone.getTimeZone("GMT")
}
- call.response.header(HttpHeaders.LastModified, rfc1123Format.format(Date(file.lastModified())))
+ call.response.header(
+ HttpHeaders.LastModified,
+ rfc1123Format.format(Date(file.lastModified()))
+ )
call.respond(HttpStatusCode.OK)
}
}
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt
index 1c0cd75f..07b644a8 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt
@@ -297,15 +297,15 @@ object WebSocketMessageHandler {
// Send updated media state after successful control
if (success) {
- // For track skip actions (next/previous), add a delay to allow media player to update
- CoroutineScope(Dispatchers.IO).launch {
- val delayMs = when (action) {
- "seekTo" -> 650L
- "next", "previous" -> 1200L
- else -> 400L // smaller delay for like/others
- }
- delay(delayMs)
- SyncManager.onMediaStateChanged(context)
+ // For track skip actions (next/previous), add a delay to allow media player to update
+ CoroutineScope(Dispatchers.IO).launch {
+ val delayMs = when (action) {
+ "seekTo" -> 650L
+ "next", "previous" -> 1200L
+ else -> 400L // smaller delay for like/others
+ }
+ delay(delayMs)
+ SyncManager.onMediaStateChanged(context)
}
}
} catch (e: Exception) {
@@ -425,6 +425,10 @@ object WebSocketMessageHandler {
private fun handlePing(context: Context) {
try {
+ // Reply immediately with lightweight pong message to keep session active
+ val pongJson = "{\"type\":\"pong\",\"data\":{}}"
+ WebSocketUtil.sendMessage(pongJson)
+
// Respond to ping with current device status to keep connection alive
// We must force sync here because the server expects a response to every ping
SyncManager.checkAndSyncDeviceStatus(context, forceSync = true)
@@ -499,7 +503,10 @@ object WebSocketMessageHandler {
// Update the Mac device status manager with all media info
MacDeviceStatusManager.updateStatus(
context = context,
- name = data.optString("name", MacDeviceStatusManager.macDeviceStatus.value?.name ?: "Unknown"),
+ name = data.optString(
+ "name",
+ MacDeviceStatusManager.macDeviceStatus.value?.name ?: "Unknown"
+ ),
batteryLevel = batteryLevel,
isCharging = isCharging,
isPaired = isPaired,
@@ -895,7 +902,7 @@ object WebSocketMessageHandler {
private fun handleRefreshAdbPorts(context: Context) {
Log.d(TAG, "Request to refresh ADB ports received. Restarting discovery...")
com.sameerasw.airsync.AdbDiscoveryHolder.restartDiscovery(context)
-
+
CoroutineScope(Dispatchers.IO).launch {
delay(2500)
Log.d(TAG, "Sending refreshed device info with ADB ports after delay")
@@ -932,8 +939,12 @@ object WebSocketMessageHandler {
}
Log.d(TAG, "Triggering Quick Share receiving mode via WebSocket")
- val intent = Intent(context, com.sameerasw.airsync.quickshare.QuickShareService::class.java).apply {
- action = com.sameerasw.airsync.quickshare.QuickShareService.ACTION_START_DISCOVERY
+ val intent = Intent(
+ context,
+ com.sameerasw.airsync.quickshare.QuickShareService::class.java
+ ).apply {
+ action =
+ com.sameerasw.airsync.quickshare.QuickShareService.ACTION_START_DISCOVERY
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
context.startForegroundService(intent)
diff --git a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketUtil.kt
index c2b8a861..0881916c 100644
--- a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketUtil.kt
+++ b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketUtil.kt
@@ -10,9 +10,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -41,6 +41,7 @@ object WebSocketUtil {
private fun updateConnectedStatus(status: Boolean) {
isConnected.set(status)
_connectionStateFlow.value = status
+ notifyConnectionStatusListeners(status)
}
// Transport state: true after OkHttp onOpen, false after closing/failure/disconnect
@@ -76,10 +77,6 @@ object WebSocketUtil {
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS) // Keep connection alive
- .pingInterval(
- 20,
- TimeUnit.SECONDS
- ) // Send ping every 20 seconds
.build()
}
@@ -151,6 +148,7 @@ object WebSocketUtil {
isConnecting.set(true)
handshakeCompleted.set(false)
+ notifyConnectionStatusListeners(false)
// Reset manual disconnect flag on manual attempt
if (manualAttempt) {
@@ -290,7 +288,10 @@ object WebSocketUtil {
Log.d(TAG, "RAW WebSocket message received: ${text}...")
val decryptedMessage = currentSymmetricKey?.let { key ->
val decrypted = CryptoUtil.decryptMessage(text, key)
- if (decrypted == null) Log.e(TAG, "FAILED TO DECRYPT WebSocket message!")
+ if (decrypted == null) Log.e(
+ TAG,
+ "FAILED TO DECRYPT WebSocket message!"
+ )
decrypted
} ?: text
@@ -373,8 +374,13 @@ object WebSocketUtil {
if (code != 1000) {
if (com.sameerasw.airsync.AirSyncApp.isAppForeground()) {
CoroutineScope(Dispatchers.Main).launch {
- val msg = reason.ifEmpty { "Unknown Server Disconnect" }
- android.widget.Toast.makeText(context, "Disconnected: $msg", android.widget.Toast.LENGTH_SHORT).show()
+ val msg =
+ reason.ifEmpty { "Unknown Server Disconnect" }
+ android.widget.Toast.makeText(
+ context,
+ "Disconnected: $msg",
+ android.widget.Toast.LENGTH_SHORT
+ ).show()
}
}
}
@@ -389,18 +395,22 @@ object WebSocketUtil {
} catch (_: Exception) {
}
try {
- com.sameerasw.airsync.service.MacMediaPlayerService.stopMacMedia(context)
- com.sameerasw.airsync.utils.MacDeviceStatusManager.cleanup(context)
+ com.sameerasw.airsync.service.MacMediaPlayerService.stopMacMedia(
+ context
+ )
+ com.sameerasw.airsync.utils.MacDeviceStatusManager.cleanup(
+ context
+ )
} catch (_: Exception) {
}
onConnectionStatusChanged?.invoke(false)
notifyConnectionStatusListeners(false)
-
+
// Only auto-reconnect if it wasn't a manual close (1000)
if (code != 1000) {
tryStartAutoReconnect(context)
}
-
+
try {
AirSyncWidgetProvider.updateAllWidgets(context)
} catch (_: Exception) {
@@ -416,7 +426,8 @@ object WebSocketUtil {
val totalToTry = ipList.size
val failedCount = failedAttempts.incrementAndGet()
val wasActive = webSocket == WebSocketUtil.webSocket
- val isFinalManualAttempt = manualAttempt && !connectionStarted.get() && failedCount >= totalToTry
+ val isFinalManualAttempt =
+ manualAttempt && !connectionStarted.get() && failedCount >= totalToTry
if (wasActive || isFinalManualAttempt) {
if (manualAttempt || isSocketOpen.get()) {
@@ -429,7 +440,11 @@ object WebSocketUtil {
is java.io.EOFException, is java.net.SocketException -> "Lost connection to your mac"
else -> t.message ?: "Unknown connection error"
}
- android.widget.Toast.makeText(context, "AirSync: $msg", android.widget.Toast.LENGTH_LONG).show()
+ android.widget.Toast.makeText(
+ context,
+ "AirSync: $msg",
+ android.widget.Toast.LENGTH_LONG
+ ).show()
}
}
}
@@ -445,17 +460,24 @@ object WebSocketUtil {
} catch (_: Exception) {
}
try {
- com.sameerasw.airsync.service.MacMediaPlayerService.stopMacMedia(context)
- com.sameerasw.airsync.utils.MacDeviceStatusManager.cleanup(context)
+ com.sameerasw.airsync.service.MacMediaPlayerService.stopMacMedia(
+ context
+ )
+ com.sameerasw.airsync.utils.MacDeviceStatusManager.cleanup(
+ context
+ )
} catch (_: Exception) {
}
onConnectionStatusChanged?.invoke(false)
notifyConnectionStatusListeners(false)
-
+
// Check manual disconnect flag before auto-reconnecting on failure
CoroutineScope(Dispatchers.IO).launch {
try {
- val ds = com.sameerasw.airsync.data.local.DataStoreManager.getInstance(context)
+ val ds =
+ com.sameerasw.airsync.data.local.DataStoreManager.getInstance(
+ context
+ )
val manual = ds.getUserManuallyDisconnected().first()
if (!manual) {
tryStartAutoReconnect(context)
@@ -539,7 +561,7 @@ object WebSocketUtil {
Log.d(TAG, "WebSocket not connected, falling back to BLE: $message")
return sendOverBLE(message)
}
-
+
Log.w(TAG, "Neither WebSocket nor BLE connected, cannot send message")
return false
}
@@ -560,6 +582,7 @@ object WebSocketUtil {
ble.sendChunkedNotification(BleConstants.CHAR_NOTIFICATION_ACTION, payload)
return true
}
+
"mediaControl" -> {
val action = data.optString("action")
// Protocol: type|action
@@ -567,6 +590,7 @@ object WebSocketUtil {
ble.sendChunkedNotification(BleConstants.CHAR_MAC_CONTROL, payload)
return true
}
+
"volumeControl" -> {
val action = data.optString("action")
// Protocol: type|action
@@ -574,16 +598,19 @@ object WebSocketUtil {
ble.sendChunkedNotification(BleConstants.CHAR_MAC_CONTROL, payload)
return true
}
+
"clipboard", "clipboardUpdate" -> {
val content = data.optString("text", data.optString("content"))
ble.sendChunkedNotification(BleConstants.CHAR_CLIPBOARD_DATA_NOTIFY, content)
return true
}
+
"dismissNotification" -> {
val id = data.optString("id")
ble.sendChunkedNotification(BleConstants.CHAR_NOTIFICATION_DISMISS_NOTIFY, id)
return true
}
+
"remoteControl" -> {
val action = data.optString("action")
// Filter out high-frequency cursor controls over BLE
@@ -596,6 +623,7 @@ object WebSocketUtil {
ble.sendChunkedNotification(BleConstants.CHAR_MAC_CONTROL, payload)
return true
}
+
"notification" -> {
val pkg = data.optString("package")
val appName = data.optString("app")
@@ -604,11 +632,15 @@ object WebSocketUtil {
BleTransportBridge.sendNotification(pkg, appName, title, body)
return true
}
+
"status" -> {
val battery = data.optJSONObject("battery")
if (battery != null) {
val level = battery.optInt("level")
- ble.sendNotification(BleConstants.CHAR_BATTERY_LEVEL, byteArrayOf(level.toByte()))
+ ble.sendNotification(
+ BleConstants.CHAR_BATTERY_LEVEL,
+ byteArrayOf(level.toByte())
+ )
}
val music = data.optJSONObject("music")
if (music != null) {
@@ -655,14 +687,17 @@ object WebSocketUtil {
} catch (_: Exception) {
}
}
-
+
// Send manual disconnect signal over BLE before disconnecting BLE client
try {
val ble = com.sameerasw.airsync.AirSyncApp.getBleConnectionManager()
if (ble != null && ble.isAuthenticated) {
Log.d(TAG, "Sending manual disconnect signal over BLE before disconnecting")
- ble.sendChunkedNotification(BleConstants.CHAR_MAC_CONTROL, "remote|manual_disconnect")
-
+ ble.sendChunkedNotification(
+ BleConstants.CHAR_MAC_CONTROL,
+ "remote|manual_disconnect"
+ )
+
CoroutineScope(Dispatchers.IO).launch {
delay(300)
ble.disconnectAllConnectedDevices()
@@ -787,6 +822,7 @@ object WebSocketUtil {
autoReconnectJob = null
autoReconnectAttempts = 0
autoReconnectStartTime = 0L
+ notifyConnectionStatusListeners(false)
}
fun isAutoReconnecting(): Boolean = autoReconnectActive.get()
@@ -800,8 +836,12 @@ object WebSocketUtil {
private fun acquireWifiLock(context: Context) {
try {
if (wifiLock == null) {
- val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as android.net.wifi.WifiManager
- wifiLock = wm.createWifiLock(android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF, "AirSync:ReconnectLock")
+ val wm =
+ context.applicationContext.getSystemService(Context.WIFI_SERVICE) as android.net.wifi.WifiManager
+ wifiLock = wm.createWifiLock(
+ android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF,
+ "AirSync:ReconnectLock"
+ )
}
if (wifiLock?.isHeld == false) {
wifiLock?.acquire()
@@ -831,6 +871,7 @@ object WebSocketUtil {
if (autoReconnectActive.get()) return // already running
autoReconnectActive.set(true)
autoReconnectStartTime = System.currentTimeMillis()
+ notifyConnectionStatusListeners(false)
Log.d(TAG, "Starting Smart Auto-Reconnect strategy")
autoReconnectJob?.cancel()
@@ -845,9 +886,12 @@ object WebSocketUtil {
while (autoReconnectActive.get() && !isConnected.get()) {
val manual = ds.getUserManuallyDisconnected().first()
val autoEnabled = ds.getAutoReconnectEnabled().first()
-
+
if (manual || !autoEnabled) {
- Log.d(TAG, "Auto-reconnect cancelled: manual=$manual, enabled=$autoEnabled")
+ Log.d(
+ TAG,
+ "Auto-reconnect cancelled: manual=$manual, enabled=$autoEnabled"
+ )
cancelAutoReconnect()
break
}
@@ -856,13 +900,18 @@ object WebSocketUtil {
val last = ds.getLastConnectedDevice().first()
if (last != null) {
val all = ds.getAllNetworkDeviceConnections().first()
- val targetConnection = all.firstOrNull { it.deviceName == last.name }
-
+ val targetConnection =
+ all.firstOrNull { it.deviceName == last.name }
+
if (targetConnection != null) {
- val ips = targetConnection.networkConnections.values.joinToString(",")
+ val ips =
+ targetConnection.networkConnections.values.joinToString(",")
val port = targetConnection.port.toIntOrNull() ?: 6996
-
- Log.d(TAG, "Proactive retry to $ips:$port (backoff: ${backoffMs}ms)")
+
+ Log.d(
+ TAG,
+ "Proactive retry to $ips:$port (backoff: ${backoffMs}ms)"
+ )
connect(
context = context,
ipAddress = ips,
@@ -879,7 +928,7 @@ object WebSocketUtil {
}
}
}
-
+
delay(backoffMs)
// Exponential backoff capped at 1 minute
backoffMs = (backoffMs * 2).coerceAtMost(60_000L)
@@ -891,7 +940,7 @@ object WebSocketUtil {
if (!autoReconnectActive.get() || isConnected.get() || isConnecting.get()) return@collect
val last = ds.getLastConnectedDevice().first() ?: return@collect
-
+
// Match by name within the discovery list
val discoveryMatch = discoveredList.find { it.name == last.name }
if (discoveryMatch != null) {
diff --git a/app/src/main/java/com/sameerasw/airsync/widget/AirSyncWidgetProvider.kt b/app/src/main/java/com/sameerasw/airsync/widget/AirSyncWidgetProvider.kt
index 29042e56..7f0c76b1 100644
--- a/app/src/main/java/com/sameerasw/airsync/widget/AirSyncWidgetProvider.kt
+++ b/app/src/main/java/com/sameerasw/airsync/widget/AirSyncWidgetProvider.kt
@@ -106,16 +106,21 @@ class AirSyncWidgetProvider : AppWidgetProvider() {
val widgetAlpha = runBlocking { ds.getWidgetTransparency().first() }
// Apply background transparency
- val baseBg = androidx.core.content.ContextCompat.getColor(context, R.color.widget_background)
- val bgWithAlpha = androidx.core.graphics.ColorUtils.setAlphaComponent(baseBg, (widgetAlpha * 255).toInt().coerceIn(0, 255))
+ val baseBg =
+ androidx.core.content.ContextCompat.getColor(context, R.color.widget_background)
+ val bgWithAlpha = androidx.core.graphics.ColorUtils.setAlphaComponent(
+ baseBg,
+ (widgetAlpha * 255).toInt().coerceIn(0, 255)
+ )
views.setInt(R.id.widget_container, "setBackgroundColor", bgWithAlpha)
// Device image (large preview) and name
val previewRes = DevicePreviewResolver.getPreviewRes(lastDevice)
views.setImageViewResource(R.id.widget_device_image, previewRes)
-
+
// Apply primary accent tint
- val accentColor = androidx.core.content.ContextCompat.getColor(context, R.color.material_primary)
+ val accentColor =
+ androidx.core.content.ContextCompat.getColor(context, R.color.material_primary)
views.setInt(R.id.widget_device_image, "setColorFilter", accentColor)
// Dim the device image when not connected (including while connecting)
diff --git a/app/src/main/res/drawable/brand_github.xml b/app/src/main/res/drawable/brand_github.xml
index e26e25c0..492db5d6 100644
--- a/app/src/main/res/drawable/brand_github.xml
+++ b/app/src/main/res/drawable/brand_github.xml
@@ -28,9 +28,9 @@ SOFTWARE.
android:viewportWidth="24"
android:viewportHeight="24">
+ android:strokeLineJoin="round" />
\ No newline at end of file
diff --git a/app/src/main/res/drawable/brand_telegram.xml b/app/src/main/res/drawable/brand_telegram.xml
index 6c3318ef..4cba053c 100644
--- a/app/src/main/res/drawable/brand_telegram.xml
+++ b/app/src/main/res/drawable/brand_telegram.xml
@@ -28,9 +28,9 @@ SOFTWARE.
android:viewportWidth="24"
android:viewportHeight="24">
+ android:strokeLineJoin="round" />
\ No newline at end of file
diff --git a/app/src/main/res/drawable/essentials_icon.xml b/app/src/main/res/drawable/essentials_icon.xml
index cfe80112..fb484f05 100644
--- a/app/src/main/res/drawable/essentials_icon.xml
+++ b/app/src/main/res/drawable/essentials_icon.xml
@@ -3,47 +3,42 @@
android:height="24dp"
android:viewportWidth="108"
android:viewportHeight="108">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:pathData="M55.83,27.05C58.26,27.05 60.34,28.72 60.79,31.03L61,32.05C61.8,36.09 66.1,38.5 70.1,37.15L71.12,36.81C73.41,36.04 75.95,36.95 77.16,38.99L78.99,42.06C80.2,44.1 79.75,46.68 77.92,48.22L77.1,48.9C73.9,51.59 73.9,56.41 77.1,59.1L77.92,59.78C79.75,61.32 80.2,63.9 78.99,65.94L77.16,69.01C75.95,71.05 73.41,71.96 71.12,71.19L70.1,70.85C66.1,69.5 61.8,71.91 61,75.94L60.79,76.97C60.34,79.28 58.26,80.95 55.83,80.95H52.17C49.74,80.95 47.66,79.28 47.2,76.97L47,75.94C46.2,71.91 41.9,69.5 37.9,70.85L36.88,71.19C34.59,71.96 32.05,71.05 30.84,69.01L29.01,65.94C27.8,63.9 28.25,61.32 30.08,59.78L30.9,59.1C34.1,56.41 34.1,51.59 30.9,48.9L30.08,48.22C28.25,46.68 27.8,44.1 29.01,42.06L30.84,38.99C32.05,36.95 34.59,36.04 36.88,36.81L37.9,37.15C41.9,38.5 46.2,36.09 47,32.06L47.2,31.03C47.66,28.72 49.74,27.05 52.17,27.05H55.83ZM57.7,39.19C55.41,38.62 53,38.58 50.67,39.1C47.25,39.87 44.19,41.79 42.02,44.55C39.84,47.3 38.69,50.72 38.74,54.23C38.8,57.74 40.06,61.12 42.32,63.81C44.57,66.5 47.69,68.33 51.13,68.98C54.58,69.64 58.15,69.09 61.24,67.43C64.33,65.76 66.75,63.08 68.09,59.84C69.01,57.64 69.39,55.26 69.22,52.9C69.09,51.07 67.19,50.06 65.43,50.59C63.67,51.11 62.74,53 62.56,54.83C62.47,55.67 62.27,56.5 61.94,57.29C61.18,59.12 59.82,60.63 58.08,61.56C56.34,62.5 54.33,62.81 52.39,62.44C51.7,62.31 51.04,62.1 50.42,61.81C49.61,61.44 49.78,60.36 50.62,60.08L52.45,59.46C52.55,59.44 52.64,59.42 52.74,59.38L54.03,58.96L55.3,58.52C56.07,58.26 56.64,57.61 56.81,56.81L57.09,55.49L57.34,54.17C57.5,53.37 57.22,52.55 56.62,52.01L55.61,51.11L54.59,50.22C53.98,49.69 53.13,49.52 52.36,49.77L51.23,50.14C51.03,50.18 50.83,50.23 50.64,50.3L47.56,51.34C46.73,51.62 45.94,50.87 46.34,50.09C46.59,49.59 46.9,49.12 47.25,48.67C48.47,47.12 50.19,46.04 52.12,45.61C52.96,45.42 53.81,45.36 54.65,45.42C56.49,45.56 58.51,44.97 59.33,43.32C60.15,41.68 59.49,39.64 57.7,39.19Z" />
diff --git a/app/src/main/res/drawable/ic_clipboard_24.xml b/app/src/main/res/drawable/ic_clipboard_24.xml
index 5234eeea..504748cd 100644
--- a/app/src/main/res/drawable/ic_clipboard_24.xml
+++ b/app/src/main/res/drawable/ic_clipboard_24.xml
@@ -1,30 +1,30 @@
-
+ android:viewportHeight="24">
+ android:strokeWidth="2"
+ android:strokeColor="#000000" />
+ android:strokeWidth="2"
+ android:strokeColor="#000000" />
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_desktop_24.xml b/app/src/main/res/drawable/ic_desktop_24.xml
index 39e4ad73..03f4df1e 100644
--- a/app/src/main/res/drawable/ic_desktop_24.xml
+++ b/app/src/main/res/drawable/ic_desktop_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_laptop_24.xml b/app/src/main/res/drawable/ic_laptop_24.xml
index a81cbd52..6e653bc6 100644
--- a/app/src/main/res/drawable/ic_laptop_24.xml
+++ b/app/src/main/res/drawable/ic_laptop_24.xml
@@ -1,11 +1,11 @@
-
+ android:viewportHeight="24">
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index 25421b9d..bd5f69ed 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -5,5 +5,5 @@
android:viewportHeight="108">
+ android:pathData="M0,0h108v108h-108z" />
diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml
index b8ce5e2c..300a0aa4 100644
--- a/app/src/main/res/drawable/ic_launcher_monochrome.xml
+++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml
@@ -7,7 +7,7 @@
android:fillColor="#000000"
android:pathData="M48.629,28.8234C51.79,26.4364 56.1555,26.4365 59.3165,28.8234C59.4506,28.9247 59.5977,29.0453 59.8917,29.2853C60.0226,29.3923 60.0886,29.4453 60.1534,29.4962C61.641,30.6654 63.4649,31.3281 65.3575,31.3869C65.44,31.3894 65.5249,31.3906 65.6944,31.3927C66.0739,31.3974 66.2637,31.3999 66.4317,31.4084C70.3906,31.6077 73.7351,34.4083 74.6192,38.2648C74.6567,38.4284 74.6922,38.6152 74.7628,38.9875C74.7942,39.1533 74.8094,39.2369 74.8263,39.3175C75.2129,41.1673 76.184,42.8446 77.5958,44.1037C77.6574,44.1586 77.7213,44.2141 77.8497,44.3244C78.1375,44.5716 78.2811,44.6955 78.4044,44.8097C81.3086,47.5021 82.067,51.7922 80.2608,55.3136C80.1841,55.4631 80.0911,55.6287 79.9054,55.9591C79.8226,56.1065 79.7813,56.1805 79.7423,56.2531C78.847,57.9182 78.5097,59.8261 78.7804,61.6964C78.7922,61.7779 78.8058,61.8616 78.8331,62.0285C78.8943,62.4021 78.9245,62.5894 78.9454,62.756C79.4361,66.6817 77.2534,70.4556 73.6016,71.9943C73.4468,72.0595 73.269,72.126 72.9141,72.2599C72.7557,72.3197 72.6764,72.3504 72.5997,72.381C72.3035,72.4992 72.0152,72.6332 71.7354,72.7814C70.8652,70.386 69.2651,67.8806 69.2579,67.8693C69.2579,67.8693 64.674,60.8538 62.3302,57.2648C62.3493,57.2287 62.3699,57.1930 62.3917,57.1584C62.4615,57.0475 62.5505,56.9471 62.7266,56.7453L63.7208,55.6056C64.1565,55.1062 64.3743,54.8557 64.4806,54.5841C64.6342,54.1912 64.6343,53.7544 64.4806,53.3615C64.3742,53.0900 64.1563,52.8401 63.7208,52.3410L62.7266,51.2013C62.5504,50.9993 62.4615,50.8982 62.3917,50.7873C62.2911,50.6273 62.2188,50.4511 62.1768,50.2668C62.1478,50.1390 62.1383,50.0049 62.1202,49.7375L62.0177,48.2287C61.9728,47.5676 61.9498,47.2371 61.8331,46.9699C61.6641,46.5831 61.3554,46.2738 60.9688,46.1046C60.7016,45.9879 60.3709,45.9650 59.7100,45.9201L58.2013,45.8175C57.9343,45.7993 57.8005,45.7909 57.6729,45.7619C57.4886,45.7198 57.3125,45.6468 57.1524,45.5460C57.0416,45.4763 56.9402,45.3882 56.7384,45.2121L55.5987,44.2169C55.0999,43.7815 54.8505,43.5635 54.5792,43.4572C54.1864,43.3033 53.7495,43.3033 53.3565,43.4572C53.0851,43.5635 52.8352,43.7812 52.3360,44.2169L51.1964,45.2121C50.9948,45.3881 50.8940,45.4764 50.7833,45.5460C50.6230,45.6469 50.4464,45.7198 50.2618,45.7619C50.1342,45.7908 50.0005,45.7993 49.7335,45.8175L48.2247,45.9201C47.5637,45.9650 47.2330,45.9878 46.9659,46.1046C46.5793,46.2738 46.2707,46.5832 46.1016,46.9699C45.9850,47.2371 45.9619,47.5677 45.9171,48.2287L45.8145,49.7375C45.7964,50.0049 45.7880,50.1389 45.7589,50.2668C45.7170,50.4512 45.6437,50.6272 45.5431,50.7873C45.4734,50.8982 45.3852,50.9994 45.2091,51.2013L44.2149,52.3410C43.7795,52.8400 43.5615,53.0901 43.4552,53.3615C43.3015,53.7545 43.3015,54.1911 43.4552,54.5841C43.5615,54.8557 43.7793,55.1062 44.2149,55.6056L45.2091,56.7453C45.3850,56.9468 45.4734,57.0476 45.5431,57.1584C45.5860,57.2266 45.6228,57.2988 45.6554,57.3722C43.2851,60.9785 40.9131,64.5853 38.5431,68.1945C37.6014,69.6266 36.6415,71.1092 36.1163,72.7335C35.8658,72.6047 35.6092,72.4860 35.3458,72.3810C35.2691,72.3504 35.1898,72.3197 35.0313,72.2599C34.6765,72.1260 34.4987,72.0595 34.3438,71.9943C30.6920,70.4556 28.5094,66.6817 29.0001,62.7560C29.0209,62.5894 29.0511,62.4022 29.1124,62.0285C29.1397,61.8616 29.1543,61.7780 29.1661,61.6964C29.4367,59.8261 29.0995,57.9182 28.2042,56.2531C28.1652,56.1804 28.1230,56.1067 28.0401,55.9591C27.8545,55.6288 27.7623,55.4633 27.6856,55.3136C25.8793,51.7922 26.6370,47.5022 29.5411,44.8097C29.6643,44.6955 29.8081,44.5715 30.0958,44.3244C30.2243,44.2140 30.2891,44.1586 30.3507,44.1037C31.7624,42.8446 32.7325,41.1672 33.1192,39.3175C33.1360,39.2368 33.1522,39.1534 33.1837,38.9875C33.2543,38.6151 33.2898,38.4284 33.3272,38.2648C34.2114,34.4084 37.5551,31.6078 41.5138,31.4084C41.6817,31.3999 41.8724,31.3974 42.2520,31.3927C42.4209,31.3906 42.5057,31.3894 42.5880,31.3869C44.4805,31.3281 46.3045,30.6654 47.7921,29.4962C47.8569,29.4453 47.9228,29.3922 48.0538,29.2853C48.3476,29.0454 48.4948,28.9247 48.6290,28.8234Z" />
diff --git a/app/src/main/res/drawable/ic_mac_mini_24.xml b/app/src/main/res/drawable/ic_mac_mini_24.xml
index c2184b70..7220b77c 100644
--- a/app/src/main/res/drawable/ic_mac_mini_24.xml
+++ b/app/src/main/res/drawable/ic_mac_mini_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_mac_pro_24.xml b/app/src/main/res/drawable/ic_mac_pro_24.xml
index beccb419..57e7e1df 100644
--- a/app/src/main/res/drawable/ic_mac_pro_24.xml
+++ b/app/src/main/res/drawable/ic_mac_pro_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_mac_studio_24.xml b/app/src/main/res/drawable/ic_mac_studio_24.xml
index c2184b70..7220b77c 100644
--- a/app/src/main/res/drawable/ic_mac_studio_24.xml
+++ b/app/src/main/res/drawable/ic_mac_studio_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_stat_name.xml b/app/src/main/res/drawable/ic_stat_name.xml
index 1e6ceb3a..67ac0336 100644
--- a/app/src/main/res/drawable/ic_stat_name.xml
+++ b/app/src/main/res/drawable/ic_stat_name.xml
@@ -2,11 +2,11 @@
-
+ android:viewportHeight="24">
+
diff --git a/app/src/main/res/drawable/imac_gen2.xml b/app/src/main/res/drawable/imac_gen2.xml
index f9835cb6..55f03d8f 100644
--- a/app/src/main/res/drawable/imac_gen2.xml
+++ b/app/src/main/res/drawable/imac_gen2.xml
@@ -3,23 +3,22 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
-
-
-
-
+ android:fillColor="#D9D9D9"
+ android:pathData="M300,385C300.67,395.33 302.4,417.3 304,422.5C306,429 306,428.5 312.5,430C317.7,431.2 321.67,432.17 323,432.5C323.33,433.33 323.6,435.2 322,436C320.4,436.8 307.33,437 301,437H210.5C204.17,437 191.1,436.8 189.5,436C187.9,435.2 188.17,433.33 188.5,432.5C189.83,432.17 193.8,431.2 199,430C205.5,428.5 205.5,429 207.5,422.5C209.1,417.3 210.83,395.33 211.5,385H300Z" />
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/imac_gen3.xml b/app/src/main/res/drawable/imac_gen3.xml
index 9dd61019..3ed05293 100644
--- a/app/src/main/res/drawable/imac_gen3.xml
+++ b/app/src/main/res/drawable/imac_gen3.xml
@@ -3,26 +3,25 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
+
+
+
+
-
-
-
-
-
+ android:fillColor="#D9D9D9"
+ android:pathData="M32,366C32,370.42 35.58,374 40,374H471C475.42,374 479,370.42 479,366V329H32V366Z" />
+
+
+
diff --git a/app/src/main/res/drawable/key_command.xml b/app/src/main/res/drawable/key_command.xml
index 3a2bb0ed..af0ad8c7 100644
--- a/app/src/main/res/drawable/key_command.xml
+++ b/app/src/main/res/drawable/key_command.xml
@@ -3,11 +3,11 @@
android:height="800dp"
android:viewportWidth="24"
android:viewportHeight="24">
-
+
diff --git a/app/src/main/res/drawable/key_control.xml b/app/src/main/res/drawable/key_control.xml
index a308dd46..14df83c7 100644
--- a/app/src/main/res/drawable/key_control.xml
+++ b/app/src/main/res/drawable/key_control.xml
@@ -3,7 +3,7 @@
android:height="800dp"
android:viewportWidth="56"
android:viewportHeight="56">
-
+
diff --git a/app/src/main/res/drawable/key_option.xml b/app/src/main/res/drawable/key_option.xml
index 79824f13..a0459224 100644
--- a/app/src/main/res/drawable/key_option.xml
+++ b/app/src/main/res/drawable/key_option.xml
@@ -3,11 +3,11 @@
android:height="800dp"
android:viewportWidth="24"
android:viewportHeight="24">
-
+
diff --git a/app/src/main/res/drawable/key_shift.xml b/app/src/main/res/drawable/key_shift.xml
index 0294fd9e..8ec55b48 100644
--- a/app/src/main/res/drawable/key_shift.xml
+++ b/app/src/main/res/drawable/key_shift.xml
@@ -3,7 +3,7 @@
android:height="800dp"
android:viewportWidth="56"
android:viewportHeight="56">
-
+
diff --git a/app/src/main/res/drawable/macbook_air_gen2.xml b/app/src/main/res/drawable/macbook_air_gen2.xml
index 33642db0..bccb64b8 100644
--- a/app/src/main/res/drawable/macbook_air_gen2.xml
+++ b/app/src/main/res/drawable/macbook_air_gen2.xml
@@ -3,32 +3,31 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
-
-
-
-
-
-
-
+ android:fillColor="#ffffff"
+ android:pathData="M64,366h384v13h-384z" />
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/macbook_air_gen3.xml b/app/src/main/res/drawable/macbook_air_gen3.xml
index ea0c2d00..dfead008 100644
--- a/app/src/main/res/drawable/macbook_air_gen3.xml
+++ b/app/src/main/res/drawable/macbook_air_gen3.xml
@@ -3,35 +3,34 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
-
-
-
-
-
-
-
-
+ android:fillColor="#ffffff"
+ android:pathData="M78,394.5C78,395.33 77.33,396 76.5,396H50.5C49.67,396 49,395.33 49,394.5C49,393.67 48.33,393 47.5,393H42.5C41.67,393 41,392.33 41,391.5C41,390.67 41.67,390 42.5,390H84.5C85.33,390 86,390.67 86,391.5C86,392.33 85.33,393 84.5,393H79.5C78.67,393 78,393.67 78,394.5Z" />
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/macbook_neo.xml b/app/src/main/res/drawable/macbook_neo.xml
index c781de25..1525c0cc 100644
--- a/app/src/main/res/drawable/macbook_neo.xml
+++ b/app/src/main/res/drawable/macbook_neo.xml
@@ -3,32 +3,31 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
-
-
-
-
-
-
-
+ android:fillColor="#ffffff"
+ android:pathData="M67,400.5C67,401.33 66.33,402 65.5,402H39.5C38.67,402 38,401.33 38,400.5C38,399.67 37.33,399 36.5,399H31.5C30.67,399 30,398.33 30,397.5C30,396.67 30.67,396 31.5,396H73.5C74.33,396 75,396.67 75,397.5C75,398.33 74.33,399 73.5,399H68.5C67.67,399 67,399.67 67,400.5Z" />
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/macbook_pro_gen2.xml b/app/src/main/res/drawable/macbook_pro_gen2.xml
index f295f391..758ef46e 100644
--- a/app/src/main/res/drawable/macbook_pro_gen2.xml
+++ b/app/src/main/res/drawable/macbook_pro_gen2.xml
@@ -3,28 +3,28 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/macbook_pro_gen3.xml b/app/src/main/res/drawable/macbook_pro_gen3.xml
index fde25723..6f845a93 100644
--- a/app/src/main/res/drawable/macbook_pro_gen3.xml
+++ b/app/src/main/res/drawable/macbook_pro_gen3.xml
@@ -3,31 +3,31 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/macmini_gen1.xml b/app/src/main/res/drawable/macmini_gen1.xml
index 96709735..db0ad8cb 100644
--- a/app/src/main/res/drawable/macmini_gen1.xml
+++ b/app/src/main/res/drawable/macmini_gen1.xml
@@ -3,19 +3,19 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
-
-
-
+
+
+
+
+
diff --git a/app/src/main/res/drawable/macmini_gen3.xml b/app/src/main/res/drawable/macmini_gen3.xml
index 82ef14a0..acf466d8 100644
--- a/app/src/main/res/drawable/macmini_gen3.xml
+++ b/app/src/main/res/drawable/macmini_gen3.xml
@@ -3,16 +3,16 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/macpro_gen3.xml b/app/src/main/res/drawable/macpro_gen3.xml
index da7b81c4..6370d36d 100644
--- a/app/src/main/res/drawable/macpro_gen3.xml
+++ b/app/src/main/res/drawable/macpro_gen3.xml
@@ -3,35 +3,34 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/macstudio_gen1.xml b/app/src/main/res/drawable/macstudio_gen1.xml
index 067c2678..e67eb97a 100644
--- a/app/src/main/res/drawable/macstudio_gen1.xml
+++ b/app/src/main/res/drawable/macstudio_gen1.xml
@@ -3,13 +3,13 @@
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
-
-
-
+
+
+
diff --git a/app/src/main/res/drawable/outline_battery_full_24.xml b/app/src/main/res/drawable/outline_battery_full_24.xml
index b4172971..4d799c6e 100644
--- a/app/src/main/res/drawable/outline_battery_full_24.xml
+++ b/app/src/main/res/drawable/outline_battery_full_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/outline_downloading_24.xml b/app/src/main/res/drawable/outline_downloading_24.xml
index e7cfd195..cb1ff65a 100644
--- a/app/src/main/res/drawable/outline_downloading_24.xml
+++ b/app/src/main/res/drawable/outline_downloading_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/outline_expand_circle_down_24.xml b/app/src/main/res/drawable/outline_expand_circle_down_24.xml
index e9ec09d9..b0884ecb 100644
--- a/app/src/main/res/drawable/outline_expand_circle_down_24.xml
+++ b/app/src/main/res/drawable/outline_expand_circle_down_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/outline_expand_circle_up_24.xml b/app/src/main/res/drawable/outline_expand_circle_up_24.xml
index 76400fa2..08c04072 100644
--- a/app/src/main/res/drawable/outline_expand_circle_up_24.xml
+++ b/app/src/main/res/drawable/outline_expand_circle_up_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/outline_feedback_24.xml b/app/src/main/res/drawable/outline_feedback_24.xml
index cc97afab..8d2c5147 100644
--- a/app/src/main/res/drawable/outline_feedback_24.xml
+++ b/app/src/main/res/drawable/outline_feedback_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/outline_info_24.xml b/app/src/main/res/drawable/outline_info_24.xml
index 270273c7..eb7fe2d5 100644
--- a/app/src/main/res/drawable/outline_info_24.xml
+++ b/app/src/main/res/drawable/outline_info_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/outline_open_in_browser_24.xml b/app/src/main/res/drawable/outline_open_in_browser_24.xml
index b2d652fa..5a2c3835 100644
--- a/app/src/main/res/drawable/outline_open_in_browser_24.xml
+++ b/app/src/main/res/drawable/outline_open_in_browser_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/outline_pause_24.xml b/app/src/main/res/drawable/outline_pause_24.xml
index f159da91..86d83067 100644
--- a/app/src/main/res/drawable/outline_pause_24.xml
+++ b/app/src/main/res/drawable/outline_pause_24.xml
@@ -3,8 +3,8 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
-
+
diff --git a/app/src/main/res/drawable/outline_play_arrow_24.xml b/app/src/main/res/drawable/outline_play_arrow_24.xml
index 350cbcad..159270cc 100644
--- a/app/src/main/res/drawable/outline_play_arrow_24.xml
+++ b/app/src/main/res/drawable/outline_play_arrow_24.xml
@@ -3,8 +3,8 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
-
+
diff --git a/app/src/main/res/drawable/outline_skip_next_24.xml b/app/src/main/res/drawable/outline_skip_next_24.xml
index 73ad19bb..179df7b1 100644
--- a/app/src/main/res/drawable/outline_skip_next_24.xml
+++ b/app/src/main/res/drawable/outline_skip_next_24.xml
@@ -3,8 +3,8 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
-
+
diff --git a/app/src/main/res/drawable/outline_skip_previous_24.xml b/app/src/main/res/drawable/outline_skip_previous_24.xml
index 52e7af47..31ab087f 100644
--- a/app/src/main/res/drawable/outline_skip_previous_24.xml
+++ b/app/src/main/res/drawable/outline_skip_previous_24.xml
@@ -3,8 +3,8 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
-
+
diff --git a/app/src/main/res/drawable/quick_share.xml b/app/src/main/res/drawable/quick_share.xml
index 7eef4496..2c5d4a57 100644
--- a/app/src/main/res/drawable/quick_share.xml
+++ b/app/src/main/res/drawable/quick_share.xml
@@ -18,10 +18,10 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
-
-
+
+
diff --git a/app/src/main/res/drawable/rounded_add_24.xml b/app/src/main/res/drawable/rounded_add_24.xml
index 6ec9fed9..5b3f0f8a 100644
--- a/app/src/main/res/drawable/rounded_add_24.xml
+++ b/app/src/main/res/drawable/rounded_add_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_android_24.xml b/app/src/main/res/drawable/rounded_android_24.xml
index c412317c..1099c879 100644
--- a/app/src/main/res/drawable/rounded_android_24.xml
+++ b/app/src/main/res/drawable/rounded_android_24.xml
@@ -13,8 +13,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_android_wifi_3_bar_24.xml b/app/src/main/res/drawable/rounded_android_wifi_3_bar_24.xml
index 127e5660..babaa94c 100644
--- a/app/src/main/res/drawable/rounded_android_wifi_3_bar_24.xml
+++ b/app/src/main/res/drawable/rounded_android_wifi_3_bar_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_arrow_back_24.xml b/app/src/main/res/drawable/rounded_arrow_back_24.xml
index f6012a30..ce6c5a8c 100644
--- a/app/src/main/res/drawable/rounded_arrow_back_24.xml
+++ b/app/src/main/res/drawable/rounded_arrow_back_24.xml
@@ -1,5 +1,13 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_arrow_forward_24.xml b/app/src/main/res/drawable/rounded_arrow_forward_24.xml
index c72c75d8..2217c472 100644
--- a/app/src/main/res/drawable/rounded_arrow_forward_24.xml
+++ b/app/src/main/res/drawable/rounded_arrow_forward_24.xml
@@ -1,5 +1,13 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_asterisk_24.xml b/app/src/main/res/drawable/rounded_asterisk_24.xml
index b7483053..cc62f92f 100644
--- a/app/src/main/res/drawable/rounded_asterisk_24.xml
+++ b/app/src/main/res/drawable/rounded_asterisk_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_bluetooth_24.xml b/app/src/main/res/drawable/rounded_bluetooth_24.xml
index 9600872a..c7593e7e 100644
--- a/app/src/main/res/drawable/rounded_bluetooth_24.xml
+++ b/app/src/main/res/drawable/rounded_bluetooth_24.xml
@@ -13,8 +13,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_bluetooth_searching_24.xml b/app/src/main/res/drawable/rounded_bluetooth_searching_24.xml
index 341352de..79cbb521 100644
--- a/app/src/main/res/drawable/rounded_bluetooth_searching_24.xml
+++ b/app/src/main/res/drawable/rounded_bluetooth_searching_24.xml
@@ -13,8 +13,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_blur_on_24.xml b/app/src/main/res/drawable/rounded_blur_on_24.xml
index 760ffba9..e6b47eed 100644
--- a/app/src/main/res/drawable/rounded_blur_on_24.xml
+++ b/app/src/main/res/drawable/rounded_blur_on_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_brightness_5_24.xml b/app/src/main/res/drawable/rounded_brightness_5_24.xml
index eb982e63..6388e857 100644
--- a/app/src/main/res/drawable/rounded_brightness_5_24.xml
+++ b/app/src/main/res/drawable/rounded_brightness_5_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_brightness_7_24.xml b/app/src/main/res/drawable/rounded_brightness_7_24.xml
index 1343d0da..5c8e77d6 100644
--- a/app/src/main/res/drawable/rounded_brightness_7_24.xml
+++ b/app/src/main/res/drawable/rounded_brightness_7_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_bug_report_24.xml b/app/src/main/res/drawable/rounded_bug_report_24.xml
index 2899c219..03871abd 100644
--- a/app/src/main/res/drawable/rounded_bug_report_24.xml
+++ b/app/src/main/res/drawable/rounded_bug_report_24.xml
@@ -13,8 +13,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_call_log_24.xml b/app/src/main/res/drawable/rounded_call_log_24.xml
index eeeca9f2..1caf606d 100644
--- a/app/src/main/res/drawable/rounded_call_log_24.xml
+++ b/app/src/main/res/drawable/rounded_call_log_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_check_24.xml b/app/src/main/res/drawable/rounded_check_24.xml
index eb2564df..e8b1c651 100644
--- a/app/src/main/res/drawable/rounded_check_24.xml
+++ b/app/src/main/res/drawable/rounded_check_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_compare_arrows_24.xml b/app/src/main/res/drawable/rounded_compare_arrows_24.xml
index 39bd25ab..1b8fa55c 100644
--- a/app/src/main/res/drawable/rounded_compare_arrows_24.xml
+++ b/app/src/main/res/drawable/rounded_compare_arrows_24.xml
@@ -1,5 +1,13 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_contacts_24.xml b/app/src/main/res/drawable/rounded_contacts_24.xml
index c98aa0d9..16b4881a 100644
--- a/app/src/main/res/drawable/rounded_contacts_24.xml
+++ b/app/src/main/res/drawable/rounded_contacts_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_dark_mode_24.xml b/app/src/main/res/drawable/rounded_dark_mode_24.xml
index 0788c72e..e981f426 100644
--- a/app/src/main/res/drawable/rounded_dark_mode_24.xml
+++ b/app/src/main/res/drawable/rounded_dark_mode_24.xml
@@ -13,8 +13,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_devices_24.xml b/app/src/main/res/drawable/rounded_devices_24.xml
index 6c6c4b6c..6014e542 100644
--- a/app/src/main/res/drawable/rounded_devices_24.xml
+++ b/app/src/main/res/drawable/rounded_devices_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_devices_off_24.xml b/app/src/main/res/drawable/rounded_devices_off_24.xml
index 11d4eec9..99ec61ff 100644
--- a/app/src/main/res/drawable/rounded_devices_off_24.xml
+++ b/app/src/main/res/drawable/rounded_devices_off_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_drag_click_24.xml b/app/src/main/res/drawable/rounded_drag_click_24.xml
index 0719f269..6a1ce507 100644
--- a/app/src/main/res/drawable/rounded_drag_click_24.xml
+++ b/app/src/main/res/drawable/rounded_drag_click_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_draw_24.xml b/app/src/main/res/drawable/rounded_draw_24.xml
index ac3976f2..eae58adc 100644
--- a/app/src/main/res/drawable/rounded_draw_24.xml
+++ b/app/src/main/res/drawable/rounded_draw_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_extension_24.xml b/app/src/main/res/drawable/rounded_extension_24.xml
index 73d85e26..7aa0c93a 100644
--- a/app/src/main/res/drawable/rounded_extension_24.xml
+++ b/app/src/main/res/drawable/rounded_extension_24.xml
@@ -13,8 +13,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_folder_managed_24.xml b/app/src/main/res/drawable/rounded_folder_managed_24.xml
index 39d3faf3..81002a36 100644
--- a/app/src/main/res/drawable/rounded_folder_managed_24.xml
+++ b/app/src/main/res/drawable/rounded_folder_managed_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_gamepad_circle_up_24.xml b/app/src/main/res/drawable/rounded_gamepad_circle_up_24.xml
index 324cda20..9d374d42 100644
--- a/app/src/main/res/drawable/rounded_gamepad_circle_up_24.xml
+++ b/app/src/main/res/drawable/rounded_gamepad_circle_up_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_heart_smile_24.xml b/app/src/main/res/drawable/rounded_heart_smile_24.xml
index 436ff14c..f17a0c5f 100644
--- a/app/src/main/res/drawable/rounded_heart_smile_24.xml
+++ b/app/src/main/res/drawable/rounded_heart_smile_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_history_24.xml b/app/src/main/res/drawable/rounded_history_24.xml
index a273bff9..99e8f6c6 100644
--- a/app/src/main/res/drawable/rounded_history_24.xml
+++ b/app/src/main/res/drawable/rounded_history_24.xml
@@ -13,8 +13,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_info_24.xml b/app/src/main/res/drawable/rounded_info_24.xml
index ccb4a26b..f5d2b8c9 100644
--- a/app/src/main/res/drawable/rounded_info_24.xml
+++ b/app/src/main/res/drawable/rounded_info_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_invert_colors_24.xml b/app/src/main/res/drawable/rounded_invert_colors_24.xml
index 4a137ecc..d5656bc3 100644
--- a/app/src/main/res/drawable/rounded_invert_colors_24.xml
+++ b/app/src/main/res/drawable/rounded_invert_colors_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_keyboard_arrow_right_24.xml b/app/src/main/res/drawable/rounded_keyboard_arrow_right_24.xml
index f00e768d..ae1ffa87 100644
--- a/app/src/main/res/drawable/rounded_keyboard_arrow_right_24.xml
+++ b/app/src/main/res/drawable/rounded_keyboard_arrow_right_24.xml
@@ -1,5 +1,13 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_laptop_mac_24.xml b/app/src/main/res/drawable/rounded_laptop_mac_24.xml
index b8c1fd91..404807f3 100644
--- a/app/src/main/res/drawable/rounded_laptop_mac_24.xml
+++ b/app/src/main/res/drawable/rounded_laptop_mac_24.xml
@@ -1,10 +1,10 @@
-
+ android:viewportHeight="24">
+
diff --git a/app/src/main/res/drawable/rounded_link_off_24.xml b/app/src/main/res/drawable/rounded_link_off_24.xml
index 97f9a7a9..26a43212 100644
--- a/app/src/main/res/drawable/rounded_link_off_24.xml
+++ b/app/src/main/res/drawable/rounded_link_off_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_local_laundry_service_24.xml b/app/src/main/res/drawable/rounded_local_laundry_service_24.xml
index fc833691..5b2df14f 100644
--- a/app/src/main/res/drawable/rounded_local_laundry_service_24.xml
+++ b/app/src/main/res/drawable/rounded_local_laundry_service_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_lock_24.xml b/app/src/main/res/drawable/rounded_lock_24.xml
index 8eeaf62b..e87bf793 100644
--- a/app/src/main/res/drawable/rounded_lock_24.xml
+++ b/app/src/main/res/drawable/rounded_lock_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_mimo_disconnect_24.xml b/app/src/main/res/drawable/rounded_mimo_disconnect_24.xml
index c007dca1..029cbf79 100644
--- a/app/src/main/res/drawable/rounded_mimo_disconnect_24.xml
+++ b/app/src/main/res/drawable/rounded_mimo_disconnect_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_mobile_check_24.xml b/app/src/main/res/drawable/rounded_mobile_check_24.xml
index a1b8d88b..39d1b48d 100644
--- a/app/src/main/res/drawable/rounded_mobile_check_24.xml
+++ b/app/src/main/res/drawable/rounded_mobile_check_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_mobile_vibrate_24.xml b/app/src/main/res/drawable/rounded_mobile_vibrate_24.xml
index dcbcaf79..e9922793 100644
--- a/app/src/main/res/drawable/rounded_mobile_vibrate_24.xml
+++ b/app/src/main/res/drawable/rounded_mobile_vibrate_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_mobiledata_arrows_24.xml b/app/src/main/res/drawable/rounded_mobiledata_arrows_24.xml
index b4bcaf1f..5e8fb308 100644
--- a/app/src/main/res/drawable/rounded_mobiledata_arrows_24.xml
+++ b/app/src/main/res/drawable/rounded_mobiledata_arrows_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_music_cast_24.xml b/app/src/main/res/drawable/rounded_music_cast_24.xml
index 4fd8dcc2..4b0b0c76 100644
--- a/app/src/main/res/drawable/rounded_music_cast_24.xml
+++ b/app/src/main/res/drawable/rounded_music_cast_24.xml
@@ -13,8 +13,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_network_node_24.xml b/app/src/main/res/drawable/rounded_network_node_24.xml
index c52cd7a9..60ee39b3 100644
--- a/app/src/main/res/drawable/rounded_network_node_24.xml
+++ b/app/src/main/res/drawable/rounded_network_node_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_notification_settings_24.xml b/app/src/main/res/drawable/rounded_notification_settings_24.xml
index 39176180..3e7aa19f 100644
--- a/app/src/main/res/drawable/rounded_notification_settings_24.xml
+++ b/app/src/main/res/drawable/rounded_notification_settings_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_notifications_active_24.xml b/app/src/main/res/drawable/rounded_notifications_active_24.xml
index 90a1acf3..7f622658 100644
--- a/app/src/main/res/drawable/rounded_notifications_active_24.xml
+++ b/app/src/main/res/drawable/rounded_notifications_active_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_qr_code_scanner_24.xml b/app/src/main/res/drawable/rounded_qr_code_scanner_24.xml
index 0a452aa7..f72dc730 100644
--- a/app/src/main/res/drawable/rounded_qr_code_scanner_24.xml
+++ b/app/src/main/res/drawable/rounded_qr_code_scanner_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_remove_24.xml b/app/src/main/res/drawable/rounded_remove_24.xml
index d0b84241..8d9550cc 100644
--- a/app/src/main/res/drawable/rounded_remove_24.xml
+++ b/app/src/main/res/drawable/rounded_remove_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_screenshot_monitor_24.xml b/app/src/main/res/drawable/rounded_screenshot_monitor_24.xml
index b26baa75..dbd6abd4 100644
--- a/app/src/main/res/drawable/rounded_screenshot_monitor_24.xml
+++ b/app/src/main/res/drawable/rounded_screenshot_monitor_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_security_24.xml b/app/src/main/res/drawable/rounded_security_24.xml
index 4c5c7e69..26faeb3d 100644
--- a/app/src/main/res/drawable/rounded_security_24.xml
+++ b/app/src/main/res/drawable/rounded_security_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_settings_phone_24.xml b/app/src/main/res/drawable/rounded_settings_phone_24.xml
index 33b114da..b8ddfc12 100644
--- a/app/src/main/res/drawable/rounded_settings_phone_24.xml
+++ b/app/src/main/res/drawable/rounded_settings_phone_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_shield_toggle_24.xml b/app/src/main/res/drawable/rounded_shield_toggle_24.xml
index 74e500a3..084742d7 100644
--- a/app/src/main/res/drawable/rounded_shield_toggle_24.xml
+++ b/app/src/main/res/drawable/rounded_shield_toggle_24.xml
@@ -13,8 +13,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_smart_display_24.xml b/app/src/main/res/drawable/rounded_smart_display_24.xml
index e653df58..f83d7a84 100644
--- a/app/src/main/res/drawable/rounded_smart_display_24.xml
+++ b/app/src/main/res/drawable/rounded_smart_display_24.xml
@@ -13,8 +13,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_star_shine_24.xml b/app/src/main/res/drawable/rounded_star_shine_24.xml
index 785acaaf..8fba5bf7 100644
--- a/app/src/main/res/drawable/rounded_star_shine_24.xml
+++ b/app/src/main/res/drawable/rounded_star_shine_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_sync_desktop_24.xml b/app/src/main/res/drawable/rounded_sync_desktop_24.xml
index d7ac461d..0cdd949e 100644
--- a/app/src/main/res/drawable/rounded_sync_desktop_24.xml
+++ b/app/src/main/res/drawable/rounded_sync_desktop_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_task_alt_24.xml b/app/src/main/res/drawable/rounded_task_alt_24.xml
index fa0ffe7d..cd9cfaf1 100644
--- a/app/src/main/res/drawable/rounded_task_alt_24.xml
+++ b/app/src/main/res/drawable/rounded_task_alt_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_troubleshoot_24.xml b/app/src/main/res/drawable/rounded_troubleshoot_24.xml
index da84f242..aba9b5b1 100644
--- a/app/src/main/res/drawable/rounded_troubleshoot_24.xml
+++ b/app/src/main/res/drawable/rounded_troubleshoot_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_web_24.xml b/app/src/main/res/drawable/rounded_web_24.xml
index 22faee8b..fa59d368 100644
--- a/app/src/main/res/drawable/rounded_web_24.xml
+++ b/app/src/main/res/drawable/rounded_web_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_web_traffic_24.xml b/app/src/main/res/drawable/rounded_web_traffic_24.xml
index bd74b6ac..b399bb6c 100644
--- a/app/src/main/res/drawable/rounded_web_traffic_24.xml
+++ b/app/src/main/res/drawable/rounded_web_traffic_24.xml
@@ -1,5 +1,12 @@
-
-
-
-
+
+
+
+
diff --git a/app/src/main/res/drawable/widget_preview.xml b/app/src/main/res/drawable/widget_preview.xml
index bcccf8ec..60f3bd5f 100644
--- a/app/src/main/res/drawable/widget_preview.xml
+++ b/app/src/main/res/drawable/widget_preview.xml
@@ -11,10 +11,10 @@
+ android:pathData="M8,0L172,0A8,8 0 0,1 180,8L180,102A8,8 0 0,1 172,110L8,110A8,8 0 0,1 0,102L0,8A8,8 0 0,1 8,0z"
+ android:strokeWidth="1"
+ android:strokeColor="#E0E0E0" />
+ android:strokeColor="#79747E" />
diff --git a/app/src/main/res/drawable/widget_tap_hint_bg.xml b/app/src/main/res/drawable/widget_tap_hint_bg.xml
index a43cec2c..9495e894 100644
--- a/app/src/main/res/drawable/widget_tap_hint_bg.xml
+++ b/app/src/main/res/drawable/widget_tap_hint_bg.xml
@@ -7,8 +7,8 @@
android:width="1dp"
android:color="@color/widget_border" />
+ android:top="4dp" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_airsync.xml b/app/src/main/res/layout/widget_airsync.xml
index 62365419..28e9dc82 100644
--- a/app/src/main/res/layout/widget_airsync.xml
+++ b/app/src/main/res/layout/widget_airsync.xml
@@ -3,11 +3,11 @@
android:id="@+id/widget_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
- android:padding="12dp"
android:background="@drawable/widget_background"
android:clickable="true"
- android:gravity="center_horizontal">
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:padding="12dp">
+ android:src="@drawable/ic_laptop_24" />
@@ -43,13 +43,13 @@
android:id="@+id/widget_battery_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="horizontal"
android:layout_gravity="center"
- android:gravity="center_vertical"
android:background="@drawable/widget_tap_hint_bg"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
android:paddingStart="8dp"
- android:paddingEnd="8dp"
android:paddingTop="4dp"
+ android:paddingEnd="8dp"
android:paddingBottom="4dp"
android:visibility="gone">
@@ -59,8 +59,8 @@
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerInside"
- android:tint="@color/widget_primary_text"
- android:src="@drawable/outline_battery_full_24" />
+ android:src="@drawable/outline_battery_full_24"
+ android:tint="@color/widget_primary_text" />
+ android:textStyle="bold" />
@@ -94,19 +94,19 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AirSync"
- android:textStyle="bold"
+ android:textColor="@color/widget_primary_text"
android:textSize="16sp"
- android:textColor="@color/widget_primary_text" />
+ android:textStyle="bold" />
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 04091bd1..b070c763 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,6 +1,6 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 04091bd1..b070c763 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,6 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/splash.xml b/app/src/main/res/values-night/splash.xml
index 5846b743..d9a662bd 100644
--- a/app/src/main/res/values-night/splash.xml
+++ b/app/src/main/res/values-night/splash.xml
@@ -1,5 +1,5 @@
-
+
-
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
index 5551dbb1..95c3329d 100644
--- a/app/src/main/res/xml/network_security_config.xml
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -13,7 +13,7 @@
-
+
diff --git a/app/src/main/res/xml/widget_airsync_info.xml b/app/src/main/res/xml/widget_airsync_info.xml
index 81ca3c85..b420db68 100644
--- a/app/src/main/res/xml/widget_airsync_info.xml
+++ b/app/src/main/res/xml/widget_airsync_info.xml
@@ -5,9 +5,9 @@
android:initialLayout="@layout/widget_airsync"
android:minWidth="180dp"
android:minHeight="110dp"
- android:resizeMode="horizontal|vertical"
android:previewImage="@drawable/widget_preview"
android:previewLayout="@layout/widget_airsync"
+ android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="2"
android:updatePeriodMillis="1800000"
diff --git a/app/src/test/java/com/sameerasw/airsync/ExampleUnitTest.kt b/app/src/test/java/com/sameerasw/airsync/ExampleUnitTest.kt
index 50caa84a..5c4d8236 100644
--- a/app/src/test/java/com/sameerasw/airsync/ExampleUnitTest.kt
+++ b/app/src/test/java/com/sameerasw/airsync/ExampleUnitTest.kt
@@ -1,9 +1,8 @@
package com.sameerasw.airsync
+import org.junit.Assert.assertEquals
import org.junit.Test
-import org.junit.Assert.*
-
/**
* Example local unit test, which will execute on the development machine (host).
*