Skip to content
Merged
42 changes: 29 additions & 13 deletions app/src/main/java/com/sameerasw/airsync/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,19 @@ class MainActivity : ComponentActivity() {

// Check if this is a QS tile long-press intent and device is not connected
if (intent?.action == "android.service.quicksettings.action.QS_TILE_PREFERENCES") {
if (!WebSocketUtil.isConnected()) {
// Not connected, open QR scanner instead
val qrScannerIntent = Intent(this, QRScannerActivity::class.java)
qrScannerLauncher.launch(qrScannerIntent)
return
val ds = DataStoreManager.getInstance(applicationContext)
val isPaused = runBlocking { ds.isAppPaused().first() }
if (!isPaused && !WebSocketUtil.isConnected()) {
// Not connected and not paused: pause the app on QS tile long-press
runBlocking {
ds.setAppPaused(true)
ds.setUserManuallyDisconnected(true)
}
WebSocketUtil.stopAutoReconnect(this)
WebSocketUtil.disconnect(this)
com.sameerasw.airsync.utils.discovery.DiscoveryOrchestrator.stop(this)
com.sameerasw.airsync.service.AirSyncService.stop(this)
ShortcutUtil.refreshShortcuts(this, false)
}
}

Expand Down Expand Up @@ -564,23 +572,31 @@ class MainActivity : ComponentActivity() {

// Check if this is a QS tile long-press intent
if (intent?.action == "android.service.quicksettings.action.QS_TILE_PREFERENCES") {
// Check if device is connected
if (!WebSocketUtil.isConnected()) {
// Not connected, open QR scanner
val qrScannerIntent = Intent(this, QRScannerActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val ds = DataStoreManager.getInstance(applicationContext)
val isPaused = runBlocking { ds.isAppPaused().first() }
// Check if device is disconnected and not paused
if (!isPaused && !WebSocketUtil.isConnected()) {
runBlocking {
ds.setAppPaused(true)
ds.setUserManuallyDisconnected(true)
}
startActivity(qrScannerIntent)
finish()
WebSocketUtil.stopAutoReconnect(this)
WebSocketUtil.disconnect(this)
com.sameerasw.airsync.utils.discovery.DiscoveryOrchestrator.stop(this)
com.sameerasw.airsync.service.AirSyncService.stop(this)
ShortcutUtil.refreshShortcuts(this, false)
}
}
}

override fun onResume() {
super.onResume()
if (PermissionUtil.isLocalNetworkPermissionGranted(this)) {
AdbDiscoveryHolder.initialize(this)
val ds = DataStoreManager.getInstance(applicationContext)
val isPaused = runBlocking { ds.isAppPaused().first() }
if (isPaused) return

AdbDiscoveryHolder.initialize(this)
val isDiscoveryEnabled = runBlocking {
ds.getDeviceDiscoveryEnabled().first()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,29 @@ class BleConnectionManager(private val context: Context) {
combine(
dataStoreManager.getBleSyncEnabled(),
dataStoreManager.getUserManuallyDisconnected(),
dataStoreManager.isAppPaused(),
WebSocketUtil.connectionState
) { enabled, manuallyDisconnected, wsConnected ->
Triple(enabled, manuallyDisconnected, wsConnected)
}.collectLatest { (enabled, manuallyDisconnected, wsConnected) ->
isBleEnabled = enabled
updateBleState(regularConnectionActive = wsConnected, manuallyDisconnected = manuallyDisconnected)
) { enabled, manuallyDisconnected, isPaused, wsConnected ->
data class BleInputs(val enabled: Boolean, val manuallyDisconnected: Boolean, val isPaused: Boolean, val wsConnected: Boolean)
BleInputs(enabled, manuallyDisconnected, isPaused, wsConnected)
}.collectLatest { inputs ->
isBleEnabled = inputs.enabled
updateBleState(
regularConnectionActive = inputs.wsConnected,
manuallyDisconnected = inputs.manuallyDisconnected,
isPaused = inputs.isPaused
)
}
}
}

private fun updateBleState(regularConnectionActive: Boolean, manuallyDisconnected: Boolean) {
if (!isBleEnabled) {
Log.d(TAG, "BLE disabled in settings, stopping/pausing server")
private fun updateBleState(regularConnectionActive: Boolean, manuallyDisconnected: Boolean, isPaused: Boolean = false) {
if (isPaused || !isBleEnabled) {
Log.d(TAG, "BLE paused or disabled in settings, stopping/pausing server")
bleServer?.pauseAdvertising()
if (isPaused) {
bleServer?.disconnectAllConnectedDevices()
}
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ class DataStoreManager(private val context: Context) {
private val MAC_WIDGET_REFRESH_AT = longPreferencesKey("mac_widget_refresh_at")
private val NOTIFICATION_SYNC_ENABLED = booleanPreferencesKey("notification_sync_enabled")
private val DEVELOPER_MODE = booleanPreferencesKey("developer_mode")
private val DEVELOPER_MODE_VISIBLE = booleanPreferencesKey("developer_mode_visible")
private val CLIPBOARD_SYNC_ENABLED = booleanPreferencesKey("clipboard_sync_enabled")
private val CLIPBOARD_HISTORY_ENABLED = booleanPreferencesKey("clipboard_history_enabled")
private val ICON_SYNC_COUNT = stringPreferencesKey("icon_sync_count")
private val LAST_ICON_SYNC_DATE = stringPreferencesKey("last_icon_sync_date")
private val USER_MANUALLY_DISCONNECTED = booleanPreferencesKey("user_manually_disconnected")
private val APP_PAUSED = booleanPreferencesKey("app_paused")

// Auto reconnect toggle
private val AUTO_RECONNECT_ENABLED = booleanPreferencesKey("auto_reconnect_enabled")
Expand Down Expand Up @@ -92,6 +94,7 @@ class DataStoreManager(private val context: Context) {
private val LAST_CALL_SYNC_TIMESTAMP = longPreferencesKey("last_call_sync_timestamp")
private val DEVICE_ID = stringPreferencesKey("device_id")
private val USE_BLUR = booleanPreferencesKey("use_blur")
private val USE_RIPPLE = booleanPreferencesKey("use_ripple")
private val PITCH_BLACK_THEME = booleanPreferencesKey("pitch_black_theme")
private val QUICK_SHARE_ENABLED = booleanPreferencesKey("quick_share_enabled")
private val FILE_ACCESS_ENABLED = booleanPreferencesKey("file_access_enabled")
Expand Down Expand Up @@ -606,6 +609,18 @@ class DataStoreManager(private val context: Context) {
}
}

suspend fun setDeveloperModeVisible(visible: Boolean) {
context.dataStore.edit { preferences ->
preferences[DEVELOPER_MODE_VISIBLE] = visible
}
}

fun getDeveloperModeVisible(): Flow<Boolean> {
return context.dataStore.data.map { preferences ->
preferences[DEVELOPER_MODE_VISIBLE] == true // Default to hidden
}
}

suspend fun setUserManuallyDisconnected(disconnected: Boolean) {
context.dataStore.edit { preferences ->
preferences[USER_MANUALLY_DISCONNECTED] = disconnected
Expand All @@ -618,6 +633,18 @@ class DataStoreManager(private val context: Context) {
}
}

suspend fun setAppPaused(paused: Boolean) {
context.dataStore.edit { preferences ->
preferences[APP_PAUSED] = paused
}
}

fun isAppPaused(): Flow<Boolean> {
return context.dataStore.data.map { preferences ->
preferences[APP_PAUSED] == true
}
}

suspend fun setWidgetTransparency(alpha: Float) {
context.dataStore.edit { preferences ->
preferences[WIDGET_TRANSPARENCY] = alpha
Expand Down Expand Up @@ -1060,4 +1087,11 @@ class DataStoreManager(private val context: Context) {

fun getBleAutoConnectEnabled(): Flow<Boolean> =
context.dataStore.data.map { it[BLE_AUTO_CONNECT_ENABLED] ?: true }

suspend fun setUseRippleEnabled(enabled: Boolean) {
context.dataStore.edit { it[USE_RIPPLE] = enabled }
}

fun getUseRippleEnabled(): Flow<Boolean> =
context.dataStore.data.map { it[USE_RIPPLE] ?: true }
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ class AirSyncRepositoryImpl(
return dataStoreManager.getDeveloperMode()
}

override suspend fun setDeveloperModeVisible(visible: Boolean) {
dataStoreManager.setDeveloperModeVisible(visible)
}

override fun getDeveloperModeVisible(): Flow<Boolean> {
return dataStoreManager.getDeveloperModeVisible()
}

override suspend fun saveLastConnectedDevice(device: ConnectedDevice) {
dataStoreManager.saveLastConnectedDevice(device)
}
Expand Down Expand Up @@ -200,6 +208,14 @@ class AirSyncRepositoryImpl(
return dataStoreManager.getUserManuallyDisconnected()
}

override suspend fun setAppPaused(paused: Boolean) {
dataStoreManager.setAppPaused(paused)
}

override fun isAppPaused(): Flow<Boolean> {
return dataStoreManager.isAppPaused()
}

override suspend fun setMacMediaControlsEnabled(enabled: Boolean) {
dataStoreManager.setMacMediaControlsEnabled(enabled)
}
Expand All @@ -224,6 +240,14 @@ class AirSyncRepositoryImpl(
return dataStoreManager.getPitchBlackThemeEnabled()
}

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

override fun getUseRippleEnabled(): Flow<Boolean> {
return dataStoreManager.getUseRippleEnabled()
}

override suspend fun setDefaultTab(tab: String) {
dataStoreManager.setDefaultTab(tab)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ data class UiState(
val isPowerSaveMode: Boolean = false,
val isPitchBlackThemeEnabled: Boolean = false,
val isBlurEnabled: Boolean = true,
val isRippleSettingEnabled: Boolean = true,
val isOnboardingCompleted: Boolean = true,
val widgetTransparency: Float = 1f,
val isQuickShareEnabled: Boolean = false,
val isFileAccessEnabled: Boolean = true,
val isNotifyOnCrashEnabled: Boolean = true,
val isAppPaused: Boolean = false,
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 @@ -27,6 +27,9 @@ interface AirSyncRepository {
suspend fun setDeveloperMode(enabled: Boolean)
fun getDeveloperMode(): Flow<Boolean>

suspend fun setDeveloperModeVisible(visible: Boolean)
fun getDeveloperModeVisible(): Flow<Boolean>

suspend fun saveLastConnectedDevice(device: ConnectedDevice)
fun getLastConnectedDevice(): Flow<ConnectedDevice?>

Expand Down Expand Up @@ -88,6 +91,10 @@ interface AirSyncRepository {
suspend fun setUserManuallyDisconnected(disconnected: Boolean)
fun getUserManuallyDisconnected(): Flow<Boolean>

// App paused mode
suspend fun setAppPaused(paused: Boolean)
fun isAppPaused(): Flow<Boolean>

// Mac Media controls
suspend fun setMacMediaControlsEnabled(enabled: Boolean)
fun getMacMediaControlsEnabled(): Flow<Boolean>
Expand All @@ -100,6 +107,10 @@ interface AirSyncRepository {
suspend fun setPitchBlackThemeEnabled(enabled: Boolean)
fun getPitchBlackThemeEnabled(): Flow<Boolean>

// Ripple animation settings
suspend fun setUseRippleEnabled(enabled: Boolean)
fun getUseRippleEnabled(): Flow<Boolean>

// Default tab settings
suspend fun setDefaultTab(tab: String)
fun getDefaultTab(): Flow<String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import com.sameerasw.airsync.utils.ClipboardUtil
import com.sameerasw.airsync.utils.ShortcutUtil
import com.sameerasw.airsync.utils.WebSocketUtil
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking

class ClipboardActionActivity : ComponentActivity() {

Expand Down Expand Up @@ -154,6 +156,34 @@ private fun ClipboardActionScreen(
onFinished()
}

ShortcutUtil.DASH_ACTION_PAUSE -> {
val ds = DataStoreManager.getInstance(context)
ds.setAppPaused(true)
ds.setUserManuallyDisconnected(true)
WebSocketUtil.stopAutoReconnect(context)
WebSocketUtil.disconnect(context)
com.sameerasw.airsync.utils.discovery.DiscoveryOrchestrator.stop(context)
com.sameerasw.airsync.service.AirSyncService.stop(context)
ShortcutUtil.refreshShortcuts(context, false)
uiState = ClipboardUiState.Success
delay(1200)
onFinished()
}

ShortcutUtil.DASH_ACTION_RESUME -> {
val ds = DataStoreManager.getInstance(context)
ds.setAppPaused(false)
ds.setUserManuallyDisconnected(false)
val isDiscovery = runBlocking { ds.getDeviceDiscoveryEnabled().first() }
com.sameerasw.airsync.utils.discovery.DiscoveryOrchestrator.start(context, isDiscovery)
com.sameerasw.airsync.service.AirSyncService.startScanning(context)
WebSocketUtil.requestAutoReconnect(context)
ShortcutUtil.refreshShortcuts(context, false)
uiState = ClipboardUiState.Success
delay(1200)
onFinished()
}

ShortcutUtil.DASH_ACTION_DISCONNECT -> {
val ds = DataStoreManager.getInstance(context)
ds.setUserManuallyDisconnected(true)
Expand Down Expand Up @@ -253,6 +283,8 @@ private fun ClipboardActionScreenContent(
ShortcutUtil.DASH_ACTION_DISCONNECT -> "Disconnected"
ShortcutUtil.DASH_ACTION_RECONNECT -> "Reconnect"
ShortcutUtil.DASH_ACTION_REMOTE -> "Opening Remote..."
ShortcutUtil.DASH_ACTION_PAUSE -> "Paused"
ShortcutUtil.DASH_ACTION_RESUME -> "Resumed"
else -> connectedDevice?.name ?: stringResource(R.string.your_mac)
}
Text(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ 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.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
Expand All @@ -54,13 +57,15 @@ import com.sameerasw.airsync.R
fun AboutSection(
modifier: Modifier = Modifier,
onAvatarLongClick: () -> Unit = {},
onAvatarLongClickWithPosition: ((Offset) -> Unit)? = null,
appName: String = "AirSync",
developerName: String = "Sameera Wijerathna",
description: String = "AirSync enables seamless synchronization between your Android device and Mac. Share notifications, clipboard content, and device status wirelessly over your local network."
) {
val context = LocalContext.current
val haptics = LocalHapticFeedback.current
var showAppLogo by remember { mutableStateOf(false) }
var avatarCenterOffset by remember { mutableStateOf(Offset.Zero) }

val versionName = try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName
Expand Down Expand Up @@ -103,9 +108,20 @@ fun AboutSection(
haptics = haptics,
modifier = Modifier
.size(200.dp)
.onGloballyPositioned { coords ->
val pos = coords.positionInRoot()
val size = coords.size
avatarCenterOffset = Offset(
x = pos.x + (size.width / 2f),
y = pos.y + (size.height / 2f)
)
}
.combinedClickable(
onClick = { showAppLogo = false },
onLongClick = { onAvatarLongClick() }
onLongClick = {
onAvatarLongClick()
onAvatarLongClickWithPosition?.invoke(avatarCenterOffset)
}
),
isVisible = true
)
Expand All @@ -116,11 +132,22 @@ fun AboutSection(
contentScale = ContentScale.Crop,
modifier = Modifier
.size(120.dp)
.onGloballyPositioned { coords ->
val pos = coords.positionInRoot()
val size = coords.size
avatarCenterOffset = Offset(
x = pos.x + (size.width / 2f),
y = pos.y + (size.height / 2f)
)
}
.clip(RoundedCornerShape(32.dp))
.background(MaterialTheme.colorScheme.primary)
.combinedClickable(
onClick = { showAppLogo = true },
onLongClick = { onAvatarLongClick() }
onLongClick = {
onAvatarLongClick()
onAvatarLongClickWithPosition?.invoke(avatarCenterOffset)
}
)
)
}
Expand Down
Loading