Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ class DataStoreManager(private val context: Context) {

private const val NETWORK_DEVICES_PREFIX = "network_device_"
private const val NETWORK_CONNECTIONS_PREFIX = "network_connections_"

private val IS_CELLULAR_SYNC_ENABLED = booleanPreferencesKey("is_cellular_sync_enabled")

private var instance: DataStoreManager? = null

Expand Down Expand Up @@ -1043,4 +1045,14 @@ class DataStoreManager(private val context: Context) {

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

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

val isCellularSyncEnabled: Flow<Boolean> = context.dataStore.data.map { preferences ->
preferences[IS_CELLULAR_SYNC_ENABLED] ?: true // Default to true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,14 @@ class AirSyncRepositoryImpl(
return dataStoreManager.isFileAccessEnabled()
}

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

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

override suspend fun setNotifyOnCrashEnabled(enabled: Boolean) {
dataStoreManager.setNotifyOnCrashEnabled(enabled)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ data class UiState(
val widgetTransparency: Float = 1f,
val isQuickShareEnabled: Boolean = false,
val isFileAccessEnabled: Boolean = true,
val isCellularSyncEnabled: Boolean = true,
val isNotifyOnCrashEnabled: 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 @@ -134,6 +134,9 @@ interface AirSyncRepository {
// File Access (WebDAV Server)
suspend fun setFileAccessEnabled(enabled: Boolean)
fun isFileAccessEnabled(): Flow<Boolean>
// Cellular Network Sync
suspend fun setCellularSyncEnabled(enabled: Boolean)
fun isCellularSyncEnabled(): Flow<Boolean>

suspend fun setNotifyOnCrashEnabled(enabled: Boolean)
fun getNotifyOnCrashEnabled(): Flow<Boolean>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,28 @@ fun SettingsView(
RoundedCardContainer {
PermissionsCard(missingPermissionsCount = uiState.missingPermissions.size)

if (com.sameerasw.airsync.utils.AutoStartHelper.isAutoStartSupported()) {
val manufacturerName = android.os.Build.MANUFACTURER.replaceFirstChar { if (it.isLowerCase()) it.titlecase(java.util.Locale.getDefault()) else it.toString() }
com.sameerasw.airsync.presentation.ui.components.cards.IconToggleItem(
title = "Background Stability",
description = "Fix connection drops by allowing Auto-Start in $manufacturerName settings.",
iconRes = com.sameerasw.airsync.R.drawable.rounded_info_24,
showToggle = false,
onClick = {
val intent = com.sameerasw.airsync.utils.AutoStartHelper.getAutoStartIntent(context)
if (intent != null) {
try {
context.startActivity(intent)
} catch (e: android.content.ActivityNotFoundException) {
android.widget.Toast.makeText(context, "Auto-Start settings not found on this device", android.widget.Toast.LENGTH_SHORT).show()
}
} else {
android.widget.Toast.makeText(context, "Auto-Start settings not found on this device", android.widget.Toast.LENGTH_SHORT).show()
}
}
)
}

// Help and guides card
com.sameerasw.airsync.presentation.ui.components.cards.IconToggleItem(
iconRes = com.sameerasw.airsync.R.drawable.rounded_info_24,
Expand Down Expand Up @@ -230,6 +252,10 @@ fun SettingsView(
isKeepPreviousLinkEnabled = uiState.isKeepPreviousLinkEnabled,
onToggleKeepPreviousLink = { enabled: Boolean ->
viewModel.setKeepPreviousLinkEnabled(enabled)
},
isCellularSyncEnabled = uiState.isCellularSyncEnabled,
onToggleCellularSync = { enabled: Boolean ->
viewModel.toggleCellularSync(enabled)
}
)

Expand Down Expand Up @@ -383,7 +409,7 @@ fun SettingsView(
deviceInfo.name,
deviceInfo.localIp,
uiState.port.toIntOrNull() ?: 6996,
versionName ?: "2.0.0",
versionName ?: "3.0.0",
adbPorts
)
onSendMessage(message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ fun ClipboardFeaturesCard(
// Keep previous link props
isKeepPreviousLinkEnabled: Boolean,
onToggleKeepPreviousLink: (Boolean) -> Unit,
// Cellular Network Sync props
isCellularSyncEnabled: Boolean,
onToggleCellularSync: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) {
Expand All @@ -30,6 +33,13 @@ fun ClipboardFeaturesCard(
isChecked = isClipboardSyncEnabled,
onCheckedChange = onToggleClipboardSync
)
IconToggleItem(
iconRes = R.drawable.rounded_network_node_24,
title = "Cellular Network Sync",
description = "Sync 5G/4G status to Mac",
isChecked = isCellularSyncEnabled,
onCheckedChange = onToggleCellularSync
)
IconToggleItem(
iconRes = R.drawable.outline_open_in_browser_24,
title = "Continue browsing",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ class AirSyncViewModel(
null
}

val storedDefaultTab = try {
kotlinx.coroutines.runBlocking { repository.getDefaultTab().first() }
} catch (_: Exception) {
"dynamic"
}

val activeIp = if (isWsConnected) WebSocketUtil.currentIpAddress else null

val initialIp = if (isWsConnected && activeIp != null) {
Expand All @@ -181,7 +187,8 @@ class AirSyncViewModel(
ipAddress = savedIp,
activeIp = activeIp,
macDeviceStatus = if (isGlobalConnected) MacDeviceStatusManager.macDeviceStatus.value else null,
lastConnectedDevice = storedDevice
lastConnectedDevice = storedDevice,
defaultTab = storedDefaultTab
)

// Register for WebSocket connection status updates
Expand Down Expand Up @@ -241,6 +248,13 @@ class AirSyncViewModel(
}
}

// Observe Cellular Sync preference
viewModelScope.launch {
repository.isCellularSyncEnabled().collect { enabled ->
_uiState.value = _uiState.value.copy(isCellularSyncEnabled = enabled)
}
}

// Observe Notify on Crash preference
viewModelScope.launch {
repository.getNotifyOnCrashEnabled().collect { enabled ->
Expand Down Expand Up @@ -365,7 +379,7 @@ class AirSyncViewModel(
val isKeepPreviousLinkEnabled = repository.getKeepPreviousLinkEnabled().first()
val isMacMediaControlsEnabled = repository.getMacMediaControlsEnabled().first()
val isClipboardHistoryEnabled = repository.getClipboardHistoryEnabled().first()
repository.getDefaultTab().first()
val defaultTab = repository.getDefaultTab().first()
val isEssentialsConnectionEnabled = repository.getEssentialsConnectionEnabled().first()
val isDeviceDiscoveryEnabled = repository.getDeviceDiscoveryEnabled().first()
val isBlurEnabledSetting = repository.getUseBlurEnabled().first()
Expand All @@ -374,6 +388,7 @@ class AirSyncViewModel(
val isPowerSaveMode = DeviceInfoUtil.isPowerSaveMode(context)
val isBlurProblematic = DeviceInfoUtil.isBlurProblematicDevice()
val isQuickShareEnabled = repository.isQuickShareEnabled().first()
val isCellularSyncEnabled = repository.isCellularSyncEnabled().first()
val isNotifyOnCrashEnabled = repository.getNotifyOnCrashEnabled().first()

// Replicate Essentials logic for initial state
Expand Down Expand Up @@ -429,6 +444,7 @@ class AirSyncViewModel(
isKeepPreviousLinkEnabled = isKeepPreviousLinkEnabled,
isMacMediaControlsEnabled = isMacMediaControlsEnabled,
isClipboardHistoryEnabled = isClipboardHistoryEnabled,
defaultTab = defaultTab,
isEssentialsConnectionEnabled = isEssentialsConnectionEnabled,
isDeviceDiscoveryEnabled = isDeviceDiscoveryEnabled,
isBlurSettingEnabled = isBlurEnabledSetting,
Expand All @@ -437,6 +453,7 @@ class AirSyncViewModel(
isBlurEnabled = isBlurEnabled,
isOnboardingCompleted = !isFirstRun,
isQuickShareEnabled = isQuickShareEnabled,
isCellularSyncEnabled = isCellularSyncEnabled,
isNotifyOnCrashEnabled = isNotifyOnCrashEnabled
)

Expand Down Expand Up @@ -759,6 +776,13 @@ class AirSyncViewModel(
}
}

fun toggleCellularSync(enabled: Boolean) {
_uiState.value = _uiState.value.copy(isCellularSyncEnabled = enabled)
viewModelScope.launch {
repository.setCellularSyncEnabled(enabled)
}
}

fun setNotifyOnCrashEnabled(enabled: Boolean) {
_uiState.value = _uiState.value.copy(isNotifyOnCrashEnabled = enabled)
viewModelScope.launch {
Expand Down
134 changes: 134 additions & 0 deletions app/src/main/java/com/sameerasw/airsync/utils/CellularMonitor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.sameerasw.airsync.utils

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.telephony.TelephonyCallback
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyManager
import androidx.core.content.ContextCompat

object CellularMonitor {
var currentNetworkState: String? = null
private var lastKnownBaseNetworkType: Int = TelephonyManager.NETWORK_TYPE_UNKNOWN
private var lastKnownOverrideType: Int = TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
private var currentServiceState: Int = android.telephony.ServiceState.STATE_IN_SERVICE
private var telephonyCallback: TelephonyCallback? = null
private var activeDataSubId: Int = android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
private var subscriptionListener: android.telephony.SubscriptionManager.OnSubscriptionsChangedListener? = null

fun start(context: Context) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
return
}

val subManager = context.getSystemService(android.telephony.SubscriptionManager::class.java)
subscriptionListener = object : android.telephony.SubscriptionManager.OnSubscriptionsChangedListener() {
override fun onSubscriptionsChanged() {
val newSubId = android.telephony.SubscriptionManager.getDefaultDataSubscriptionId()
if (newSubId != activeDataSubId && newSubId != android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
bindToSubscription(context, newSubId)
}
}
}
subManager.addOnSubscriptionsChangedListener(context.mainExecutor, subscriptionListener!!)

val initialSubId = android.telephony.SubscriptionManager.getDefaultDataSubscriptionId()
bindToSubscription(context, initialSubId)
}

private fun bindToSubscription(context: Context, subId: Int) {
val defaultTm = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager ?: return

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
telephonyCallback?.let { callback ->
val oldTm = if (activeDataSubId != android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
defaultTm.createForSubscriptionId(activeDataSubId)
} else {
defaultTm
}
oldTm.unregisterTelephonyCallback(callback)
}

val newTm = if (subId != android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
defaultTm.createForSubscriptionId(subId)
} else {
defaultTm
}

telephonyCallback = object : TelephonyCallback(), TelephonyCallback.DisplayInfoListener, TelephonyCallback.ServiceStateListener {
override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo) {
lastKnownBaseNetworkType = telephonyDisplayInfo.networkType
lastKnownOverrideType = telephonyDisplayInfo.overrideNetworkType
evaluateAndPushState(context)
}

override fun onServiceStateChanged(serviceState: android.telephony.ServiceState) {
currentServiceState = serviceState.state
evaluateAndPushState(context)
}
}

newTm.registerTelephonyCallback(context.mainExecutor, telephonyCallback!!)
activeDataSubId = subId
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
lastKnownBaseNetworkType = newTm.dataNetworkType
}
evaluateAndPushState(context)
}
}

fun stop(context: Context) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val defaultTm = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
if (defaultTm != null) {
val telephonyManager = if (activeDataSubId != android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
defaultTm.createForSubscriptionId(activeDataSubId)
} else {
defaultTm
}

telephonyCallback?.let {
telephonyManager.unregisterTelephonyCallback(it)
}
}
}

subscriptionListener?.let {
val subManager = context.getSystemService(android.telephony.SubscriptionManager::class.java)
subManager?.removeOnSubscriptionsChangedListener(it)
}
} finally {
telephonyCallback = null
subscriptionListener = null
activeDataSubId = android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
currentNetworkState = null
}
}

private fun evaluateAndPushState(context: Context) {
val newState = if (currentServiceState == android.telephony.ServiceState.STATE_OUT_OF_SERVICE ||
currentServiceState == android.telephony.ServiceState.STATE_EMERGENCY_ONLY ||
currentServiceState == android.telephony.ServiceState.STATE_POWER_OFF) {
"NO_SIGNAL"
} else if (lastKnownBaseNetworkType == TelephonyManager.NETWORK_TYPE_NR) {
"5G_SA"
} else if (lastKnownOverrideType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED) {
"5G_NSA+"
} else if (lastKnownOverrideType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA) {
"5G_NSA"
} else if (lastKnownBaseNetworkType == TelephonyManager.NETWORK_TYPE_LTE) {
"LTE"
} else {
"4G/Below"
}

if (currentNetworkState != newState) {
currentNetworkState = newState
SyncManager.checkAndSyncDeviceStatus(context, forceSync = true)
}
}
}
6 changes: 3 additions & 3 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<string name="disconnect">Disconnect</string>
<string name="device_discovery">Device discovery</string>
<string name="device_discovery_description">Allow other devices to find this phone in the background</string>
<string name="rating_prompt">Psst... You\'ve been syncing with your mac for a while now, Do you like the app? Rate and give your feedback. ♥</string>
<string name="rating_prompt">Psst... You\'ve been syncing with your Mac for a while now, Do you like the app? Rate and give your feedback. ♥</string>
<string name="rate_now">Rate Now</string>
<string name="maybe_later">Maybe Later</string>
<string name="no_device_connected">No device connected</string>
Expand Down Expand Up @@ -58,10 +58,10 @@
<string name="welcome_developer_attribution">by sameerasw.com</string>
<string name="action_lets_begin">Let\'s Begin</string>
<string name="acknowledgement_title">Permissions</string>
<string name="acknowledgement_desc">This app allows you to sync notifications, clipboard, and media controls with your Mac. To provide these features, AirSync requires certain permissions.\n\nYou have full control over which features are enabled and which permissions are granted. We do not collect or store any of your personal data.\n\nAirSync is open source and this app is completly free to use. Always ensure you download it from Play Store or GitHub.</string>
<string name="acknowledgement_desc">This app allows you to sync notifications, clipboard, and media controls with your Mac. To provide these features, AirSync requires certain permissions.\n\nYou have full control over which features are enabled and which permissions are granted. We do not collect or store any of your personal data.\n\nAirSync is open source and this app is completely free to use. Always ensure you download it from Play Store or GitHub.</string>
<string name="acknowledgement_footer">If you need any help, feel free to check the guides or reach out to the developer.</string>
<string name="action_i_understand">I Understand</string>
<string name="feature_intro_desc">From the scan button in app or by long pressing the connection quick settings tile, scan the QR code displayed on the app on MacOS.</string>
<string name="feature_intro_desc">From the scan button in app or by long pressing the connection quick settings tile, scan the QR code displayed on the app on macOS.</string>
<string name="feature_intro_footer">You can always find help and guides in the app settings.</string>
<string name="action_let_me_in">Let Me In Already</string>
<string name="preferences_title">Preferences</string>
Expand Down