Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ dependencies {

implementation(libs.wire.runtime)
implementation(libs.bouncycastle)

// Ktor Server for WebDAV
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.cio)
implementation(libs.ktor.server.host.common)
implementation(libs.ktor.server.status.pages)
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.serialization.gson)
}

wire {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class DataStoreManager(private val context: Context) {
private val PITCH_BLACK_THEME = booleanPreferencesKey("pitch_black_theme")
private val SENTRY_REPORTING_ENABLED = booleanPreferencesKey("sentry_reporting_enabled")
private val QUICK_SHARE_ENABLED = booleanPreferencesKey("quick_share_enabled")
private val FILE_ACCESS_ENABLED = booleanPreferencesKey("file_access_enabled")

// Widget preferences
private val WIDGET_TRANSPARENCY = androidx.datastore.preferences.core.floatPreferencesKey("widget_transparency")
Expand Down Expand Up @@ -346,6 +347,18 @@ class DataStoreManager(private val context: Context) {
}
}

suspend fun setFileAccessEnabled(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[FILE_ACCESS_ENABLED] = enabled
}
}

fun isFileAccessEnabled(): Flow<Boolean> {
return context.dataStore.data.map { preferences ->
preferences[FILE_ACCESS_ENABLED] != false // Default to enabled
}
}

suspend fun setDefaultTab(tab: String) {
context.dataStore.edit { prefs ->
prefs[DEFAULT_TAB] = tab
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,12 @@ class AirSyncRepositoryImpl(
override fun isQuickShareEnabled(): Flow<Boolean> {
return dataStoreManager.isQuickShareEnabled()
}

override suspend fun setFileAccessEnabled(enabled: Boolean) {
dataStoreManager.setFileAccessEnabled(enabled)
}

override fun isFileAccessEnabled(): Flow<Boolean> {
return dataStoreManager.isFileAccessEnabled()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ data class UiState(
val isOnboardingCompleted: Boolean = true,
val widgetTransparency: Float = 1f,
val isQuickShareEnabled: Boolean = false,
val isFileAccessEnabled: Boolean = true,
val bleConnectionState: com.sameerasw.airsync.data.ble.BleGattServer.BleConnectionState = com.sameerasw.airsync.data.ble.BleGattServer.BleConnectionState.DISCONNECTED
)
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,8 @@ interface AirSyncRepository {
// Quick Share (receiving)
suspend fun setQuickShareEnabled(enabled: Boolean)
fun isQuickShareEnabled(): Flow<Boolean>

// File Access (WebDAV Server)
suspend fun setFileAccessEnabled(enabled: Boolean)
fun isFileAccessEnabled(): Flow<Boolean>
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.sameerasw.airsync.R
import com.sameerasw.airsync.domain.model.DeviceInfo
Expand Down Expand Up @@ -242,6 +243,16 @@ fun SettingsView(
viewModel.setQuickShareEnabled(context, enabled)
}
)

IconToggleItem(
title = stringResource(R.string.label_file_access),
description = stringResource(R.string.subtitle_file_access),
iconRes = R.drawable.rounded_folder_managed_24,
isChecked = uiState.isFileAccessEnabled,
onCheckedChange = { enabled: Boolean ->
viewModel.setFileAccessEnabled(context, enabled)
}
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ class AirSyncViewModel(
}
}

// Observe File Access preference
viewModelScope.launch {
repository.isFileAccessEnabled().collect { enabled ->
_uiState.value = _uiState.value.copy(isFileAccessEnabled = enabled)
}
}

// Observe BLE connection status
viewModelScope.launch {
com.sameerasw.airsync.AirSyncApp.getBleConnectionManager()?.connectionState?.collect { state ->
Expand Down Expand Up @@ -691,6 +698,14 @@ class AirSyncViewModel(
}
}

fun setFileAccessEnabled(context: Context, enabled: Boolean) {
_uiState.value = _uiState.value.copy(isFileAccessEnabled = enabled)
viewModelScope.launch {
repository.setFileAccessEnabled(enabled)
ServiceManager.updateServiceState(context)
}
}

fun manualSyncAppIcons(context: Context) {
_uiState.value = _uiState.value.copy(isIconSyncLoading = true, iconSyncMessage = "")

Expand Down
65 changes: 58 additions & 7 deletions app/src/main/java/com/sameerasw/airsync/service/AirSyncService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import com.sameerasw.airsync.MainActivity
import com.sameerasw.airsync.R
import com.sameerasw.airsync.data.local.DataStoreManager
import com.sameerasw.airsync.utils.DiscoveryMode
import com.sameerasw.airsync.utils.MacDeviceStatusManager
import com.sameerasw.airsync.utils.ShortcutUtil
import com.sameerasw.airsync.utils.UDPDiscoveryManager
import com.sameerasw.airsync.utils.WebDavServer
import com.sameerasw.airsync.utils.WebSocketUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

/**
Expand All @@ -38,14 +44,17 @@ class AirSyncService : Service() {
private var connectedDeviceName: String? = null
private var isScanning = false

private var webDavServer: WebDavServer? = null
private var webDavJob: Job? = null

// Network state tracking
private var networkCallback: ConnectivityManager.NetworkCallback? = null

override fun onCreate() {
super.onCreate()
Log.d(TAG, "AirSyncService created")
createNotificationChannel()
com.sameerasw.airsync.utils.MacDeviceStatusManager.startMonitoring(this)
MacDeviceStatusManager.startMonitoring(this)
registerNetworkCallback()
}

Expand All @@ -58,7 +67,7 @@ class AirSyncService : Service() {
ACTION_START_SYNC -> {
connectedDeviceName = intent.getStringExtra(EXTRA_DEVICE_NAME) ?: "Mac"
startSync()
com.sameerasw.airsync.utils.ShortcutUtil.refreshShortcuts(this, true)
ShortcutUtil.refreshShortcuts(this, true)
}

ACTION_STOP_SYNC -> stopSync()
Expand All @@ -83,7 +92,7 @@ class AirSyncService : Service() {
startForeground(NOTIFICATION_ID, buildNotification())

val dataStoreManager =
com.sameerasw.airsync.data.local.DataStoreManager.getInstance(applicationContext)
DataStoreManager.getInstance(applicationContext)
val isDiscoveryEnabled = runBlocking {
dataStoreManager.getDeviceDiscoveryEnabled().first()
}
Expand All @@ -101,6 +110,39 @@ class AirSyncService : Service() {
WebSocketUtil.requestAutoReconnect(this)
}

private fun startWebDavServer() {
if (webDavServer == null) {
webDavServer = WebDavServer(this)
}
webDavServer?.start()
}

private fun stopWebDavServer() {
webDavServer?.stop()
webDavServer = null
}

private fun monitorWebDavRequirements() {
webDavJob?.cancel()
webDavJob = scope.launch {
val dataStoreManager = DataStoreManager.getInstance(applicationContext)
combine(
dataStoreManager.isFileAccessEnabled(),
dataStoreManager.getLastConnectedDevice()
) { isEnabled, device ->
Log.d(TAG, "WebDAV flow evaluation: isEnabled=$isEnabled, isPlus=${device?.isPlus}")
isEnabled && device?.isPlus == true
}.collect { shouldStart ->
Log.d(TAG, "WebDAV requirement state updated: shouldStart = $shouldStart")
if (shouldStart) {
startWebDavServer()
} else {
stopWebDavServer()
}
}
}
}

private fun handleAppForeground() {
if (isScanning) {
Log.d(TAG, "App in foreground, switching to ACTIVE discovery")
Expand All @@ -118,12 +160,16 @@ class AirSyncService : Service() {
}

private fun startSync() {
if (!isScanning && connectedDeviceName != null) {
Log.d(TAG, "AirSync foreground service already in sync state, ignoring")
return
}
Log.d(TAG, "Starting AirSync foreground service (connected)")
isScanning = false
startForeground(NOTIFICATION_ID, buildNotification())

val dataStoreManager =
com.sameerasw.airsync.data.local.DataStoreManager.getInstance(applicationContext)
DataStoreManager.getInstance(applicationContext)
val isDiscoveryEnabled = runBlocking {
dataStoreManager.getDeviceDiscoveryEnabled().first()
}
Expand All @@ -134,11 +180,15 @@ class AirSyncService : Service() {
UDPDiscoveryManager.setDiscoveryMode(this, DiscoveryMode.PASSIVE)

WakeupService.startService(this)
monitorWebDavRequirements()
}

private fun stopSync() {
Log.d(TAG, "Stopping AirSync foreground service")
com.sameerasw.airsync.utils.ShortcutUtil.refreshShortcuts(this, false)
webDavJob?.cancel()
webDavJob = null
stopWebDavServer()
ShortcutUtil.refreshShortcuts(this, false)
UDPDiscoveryManager.stop(this)
WakeupService.stopService(this)
stopForeground(STOP_FOREGROUND_REMOVE)
Expand Down Expand Up @@ -234,8 +284,9 @@ class AirSyncService : Service() {
}
}

com.sameerasw.airsync.utils.MacDeviceStatusManager.stopMonitoring()
com.sameerasw.airsync.utils.MacDeviceStatusManager.cleanup(this)
stopWebDavServer()
MacDeviceStatusManager.stopMonitoring()
MacDeviceStatusManager.cleanup(this)
scope.coroutineContext.cancel()
super.onDestroy()
}
Expand Down
Loading