Skip to content

Commit db03d64

Browse files
authored
Merge pull request #111 from sameerasw/webDAV
Web dav file access impl
2 parents 4da51ab + 4fd6376 commit db03d64

11 files changed

Lines changed: 351 additions & 7 deletions

File tree

app/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ dependencies {
154154

155155
implementation(libs.wire.runtime)
156156
implementation(libs.bouncycastle)
157+
158+
// Ktor Server for WebDAV
159+
implementation(libs.ktor.server.core)
160+
implementation(libs.ktor.server.cio)
161+
implementation(libs.ktor.server.host.common)
162+
implementation(libs.ktor.server.status.pages)
163+
implementation(libs.ktor.server.content.negotiation)
164+
implementation(libs.ktor.serialization.gson)
157165
}
158166

159167
wire {

app/src/main/java/com/sameerasw/airsync/data/local/DataStoreManager.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class DataStoreManager(private val context: Context) {
9292
private val PITCH_BLACK_THEME = booleanPreferencesKey("pitch_black_theme")
9393
private val SENTRY_REPORTING_ENABLED = booleanPreferencesKey("sentry_reporting_enabled")
9494
private val QUICK_SHARE_ENABLED = booleanPreferencesKey("quick_share_enabled")
95+
private val FILE_ACCESS_ENABLED = booleanPreferencesKey("file_access_enabled")
9596

9697
// Widget preferences
9798
private val WIDGET_TRANSPARENCY = androidx.datastore.preferences.core.floatPreferencesKey("widget_transparency")
@@ -346,6 +347,18 @@ class DataStoreManager(private val context: Context) {
346347
}
347348
}
348349

350+
suspend fun setFileAccessEnabled(enabled: Boolean) {
351+
context.dataStore.edit { preferences ->
352+
preferences[FILE_ACCESS_ENABLED] = enabled
353+
}
354+
}
355+
356+
fun isFileAccessEnabled(): Flow<Boolean> {
357+
return context.dataStore.data.map { preferences ->
358+
preferences[FILE_ACCESS_ENABLED] != false // Default to enabled
359+
}
360+
}
361+
349362
suspend fun setDefaultTab(tab: String) {
350363
context.dataStore.edit { prefs ->
351364
prefs[DEFAULT_TAB] = tab

app/src/main/java/com/sameerasw/airsync/data/repository/AirSyncRepositoryImpl.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,12 @@ class AirSyncRepositoryImpl(
295295
override fun isQuickShareEnabled(): Flow<Boolean> {
296296
return dataStoreManager.isQuickShareEnabled()
297297
}
298+
299+
override suspend fun setFileAccessEnabled(enabled: Boolean) {
300+
dataStoreManager.setFileAccessEnabled(enabled)
301+
}
302+
303+
override fun isFileAccessEnabled(): Flow<Boolean> {
304+
return dataStoreManager.isFileAccessEnabled()
305+
}
298306
}

app/src/main/java/com/sameerasw/airsync/domain/model/UiState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@ data class UiState(
5050
val isOnboardingCompleted: Boolean = true,
5151
val widgetTransparency: Float = 1f,
5252
val isQuickShareEnabled: Boolean = false,
53+
val isFileAccessEnabled: Boolean = true,
5354
val bleConnectionState: com.sameerasw.airsync.data.ble.BleGattServer.BleConnectionState = com.sameerasw.airsync.data.ble.BleGattServer.BleConnectionState.DISCONNECTED
5455
)

app/src/main/java/com/sameerasw/airsync/domain/repository/AirSyncRepository.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,8 @@ interface AirSyncRepository {
132132
// Quick Share (receiving)
133133
suspend fun setQuickShareEnabled(enabled: Boolean)
134134
fun isQuickShareEnabled(): Flow<Boolean>
135+
136+
// File Access (WebDAV Server)
137+
suspend fun setFileAccessEnabled(enabled: Boolean)
138+
fun isFileAccessEnabled(): Flow<Boolean>
135139
}

app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import androidx.compose.runtime.Composable
3838
import androidx.compose.ui.Alignment
3939
import androidx.compose.ui.Modifier
4040
import androidx.compose.ui.platform.LocalHapticFeedback
41+
import androidx.compose.ui.res.stringResource
4142
import androidx.compose.ui.unit.dp
4243
import com.sameerasw.airsync.R
4344
import com.sameerasw.airsync.domain.model.DeviceInfo
@@ -242,6 +243,16 @@ fun SettingsView(
242243
viewModel.setQuickShareEnabled(context, enabled)
243244
}
244245
)
246+
247+
IconToggleItem(
248+
title = stringResource(R.string.label_file_access),
249+
description = stringResource(R.string.subtitle_file_access),
250+
iconRes = R.drawable.rounded_folder_managed_24,
251+
isChecked = uiState.isFileAccessEnabled,
252+
onCheckedChange = { enabled: Boolean ->
253+
viewModel.setFileAccessEnabled(context, enabled)
254+
}
255+
)
245256
}
246257
}
247258

app/src/main/java/com/sameerasw/airsync/presentation/viewmodel/AirSyncViewModel.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ class AirSyncViewModel(
184184
}
185185
}
186186

187+
// Observe File Access preference
188+
viewModelScope.launch {
189+
repository.isFileAccessEnabled().collect { enabled ->
190+
_uiState.value = _uiState.value.copy(isFileAccessEnabled = enabled)
191+
}
192+
}
193+
187194
// Observe BLE connection status
188195
viewModelScope.launch {
189196
com.sameerasw.airsync.AirSyncApp.getBleConnectionManager()?.connectionState?.collect { state ->
@@ -691,6 +698,14 @@ class AirSyncViewModel(
691698
}
692699
}
693700

701+
fun setFileAccessEnabled(context: Context, enabled: Boolean) {
702+
_uiState.value = _uiState.value.copy(isFileAccessEnabled = enabled)
703+
viewModelScope.launch {
704+
repository.setFileAccessEnabled(enabled)
705+
ServiceManager.updateServiceState(context)
706+
}
707+
}
708+
694709
fun manualSyncAppIcons(context: Context) {
695710
_uiState.value = _uiState.value.copy(isIconSyncLoading = true, iconSyncMessage = "")
696711

app/src/main/java/com/sameerasw/airsync/service/AirSyncService.kt

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ import android.util.Log
1717
import androidx.core.app.NotificationCompat
1818
import com.sameerasw.airsync.MainActivity
1919
import com.sameerasw.airsync.R
20+
import com.sameerasw.airsync.data.local.DataStoreManager
2021
import com.sameerasw.airsync.utils.DiscoveryMode
22+
import com.sameerasw.airsync.utils.MacDeviceStatusManager
23+
import com.sameerasw.airsync.utils.ShortcutUtil
2124
import com.sameerasw.airsync.utils.UDPDiscoveryManager
25+
import com.sameerasw.airsync.utils.WebDavServer
2226
import com.sameerasw.airsync.utils.WebSocketUtil
2327
import kotlinx.coroutines.CoroutineScope
2428
import kotlinx.coroutines.Dispatchers
2529
import kotlinx.coroutines.Job
2630
import kotlinx.coroutines.cancel
31+
import kotlinx.coroutines.flow.combine
2732
import kotlinx.coroutines.flow.first
33+
import kotlinx.coroutines.launch
2834
import kotlinx.coroutines.runBlocking
2935

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

47+
private var webDavServer: WebDavServer? = null
48+
private var webDavJob: Job? = null
49+
4150
// Network state tracking
4251
private var networkCallback: ConnectivityManager.NetworkCallback? = null
4352

4453
override fun onCreate() {
4554
super.onCreate()
4655
Log.d(TAG, "AirSyncService created")
4756
createNotificationChannel()
48-
com.sameerasw.airsync.utils.MacDeviceStatusManager.startMonitoring(this)
57+
MacDeviceStatusManager.startMonitoring(this)
4958
registerNetworkCallback()
5059
}
5160

@@ -58,7 +67,7 @@ class AirSyncService : Service() {
5867
ACTION_START_SYNC -> {
5968
connectedDeviceName = intent.getStringExtra(EXTRA_DEVICE_NAME) ?: "Mac"
6069
startSync()
61-
com.sameerasw.airsync.utils.ShortcutUtil.refreshShortcuts(this, true)
70+
ShortcutUtil.refreshShortcuts(this, true)
6271
}
6372

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

8594
val dataStoreManager =
86-
com.sameerasw.airsync.data.local.DataStoreManager.getInstance(applicationContext)
95+
DataStoreManager.getInstance(applicationContext)
8796
val isDiscoveryEnabled = runBlocking {
8897
dataStoreManager.getDeviceDiscoveryEnabled().first()
8998
}
@@ -101,6 +110,39 @@ class AirSyncService : Service() {
101110
WebSocketUtil.requestAutoReconnect(this)
102111
}
103112

113+
private fun startWebDavServer() {
114+
if (webDavServer == null) {
115+
webDavServer = WebDavServer(this)
116+
}
117+
webDavServer?.start()
118+
}
119+
120+
private fun stopWebDavServer() {
121+
webDavServer?.stop()
122+
webDavServer = null
123+
}
124+
125+
private fun monitorWebDavRequirements() {
126+
webDavJob?.cancel()
127+
webDavJob = scope.launch {
128+
val dataStoreManager = DataStoreManager.getInstance(applicationContext)
129+
combine(
130+
dataStoreManager.isFileAccessEnabled(),
131+
dataStoreManager.getLastConnectedDevice()
132+
) { isEnabled, device ->
133+
Log.d(TAG, "WebDAV flow evaluation: isEnabled=$isEnabled, isPlus=${device?.isPlus}")
134+
isEnabled && device?.isPlus == true
135+
}.collect { shouldStart ->
136+
Log.d(TAG, "WebDAV requirement state updated: shouldStart = $shouldStart")
137+
if (shouldStart) {
138+
startWebDavServer()
139+
} else {
140+
stopWebDavServer()
141+
}
142+
}
143+
}
144+
}
145+
104146
private fun handleAppForeground() {
105147
if (isScanning) {
106148
Log.d(TAG, "App in foreground, switching to ACTIVE discovery")
@@ -118,12 +160,16 @@ class AirSyncService : Service() {
118160
}
119161

120162
private fun startSync() {
163+
if (!isScanning && connectedDeviceName != null) {
164+
Log.d(TAG, "AirSync foreground service already in sync state, ignoring")
165+
return
166+
}
121167
Log.d(TAG, "Starting AirSync foreground service (connected)")
122168
isScanning = false
123169
startForeground(NOTIFICATION_ID, buildNotification())
124170

125171
val dataStoreManager =
126-
com.sameerasw.airsync.data.local.DataStoreManager.getInstance(applicationContext)
172+
DataStoreManager.getInstance(applicationContext)
127173
val isDiscoveryEnabled = runBlocking {
128174
dataStoreManager.getDeviceDiscoveryEnabled().first()
129175
}
@@ -134,11 +180,15 @@ class AirSyncService : Service() {
134180
UDPDiscoveryManager.setDiscoveryMode(this, DiscoveryMode.PASSIVE)
135181

136182
WakeupService.startService(this)
183+
monitorWebDavRequirements()
137184
}
138185

139186
private fun stopSync() {
140187
Log.d(TAG, "Stopping AirSync foreground service")
141-
com.sameerasw.airsync.utils.ShortcutUtil.refreshShortcuts(this, false)
188+
webDavJob?.cancel()
189+
webDavJob = null
190+
stopWebDavServer()
191+
ShortcutUtil.refreshShortcuts(this, false)
142192
UDPDiscoveryManager.stop(this)
143193
WakeupService.stopService(this)
144194
stopForeground(STOP_FOREGROUND_REMOVE)
@@ -234,8 +284,9 @@ class AirSyncService : Service() {
234284
}
235285
}
236286

237-
com.sameerasw.airsync.utils.MacDeviceStatusManager.stopMonitoring()
238-
com.sameerasw.airsync.utils.MacDeviceStatusManager.cleanup(this)
287+
stopWebDavServer()
288+
MacDeviceStatusManager.stopMonitoring()
289+
MacDeviceStatusManager.cleanup(this)
239290
scope.coroutineContext.cancel()
240291
super.onDestroy()
241292
}

0 commit comments

Comments
 (0)