diff --git a/.circleci/config.yml b/.circleci/config.yml index cbf6396..293eaf5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -154,6 +154,13 @@ aliases: - /^release-sync-ios-\d+\.\d+\.\d+-beta\d+$/ branches: ignore: /.*/ + - snapshot-android-tag-filter: &snapshot-android-tag-filter + filters: + tags: + only: + - /^snapshot-sync-android-\d+\.\d+\.\d+$/ + branches: + ignore: /.*/ #============================================================================ # Executors @@ -172,15 +179,10 @@ executors: working_directory: *workspace macos-kotlin-ios-executor-m1: macos: - xcode: "15.1.0" - resource_class: macos.m1.medium.gen1 + xcode: "16.4.0" + resource_class: m4pro.medium working_directory: *workspace - environment: - # when bumping xcode version - update the IOS_SIMULATOR_DEVICE_ID - # See: https://circleci.com/docs/using-macos/ - # (on m1 executor deviceId is different than specified in the doc above (the doc is for x86_64). - # Run `xcrun simctl list` on a circleci node to get full device ids list) - IOS_SIMULATOR_DEVICE_ID: A2CD73C7-D03C-47CA-858B-94C0D2A9475A + # IOS_SIMULATOR_DEVICE_ID is auto-detected by run-tests-kotlin-ios.rb #============================================================================ # Commands: All @@ -374,8 +376,8 @@ commands: - run: name: "Generate project settings: << parameters.gradle-project-dir >>" command: | - echo "sonatype.username=$SONATYPE_USERNAME" >> gradle.properties - echo "sonatype.password=$SONATYPE_PASSWORD" >> gradle.properties + echo "sonatype.username=$MAVEN_CENTRAL_TOKEN_USERNAME" >> gradle.properties + echo "sonatype.password=$MAVEN_CENTRAL_TOKEN_PASSWORD" >> gradle.properties echo "signing.keyId=$SONATYPE_SIGNING_KEY_ID" >> gradle.properties echo "signing.password=$SONATYPE_SIGNING_PASSWORD" >> gradle.properties @@ -1026,6 +1028,18 @@ jobs: gradle-project-dir: *android_sync_root_project_dir input-file: gradle-output.log + publish-snapshot-sdk-android: + executor: linux-android-executor + description: "Publish Snapshot" + steps: + - checkout + - attach_workspace: + at: *workspace + - restore_gradle_cache + - run_gradle_target: + gradle-project-dir: *android_sync_root_project_dir + gradle-target: publishToSonatype -Pdisable-metadata-subtasks + promote-to-release-sdk-android: executor: linux-android-executor description: "Promote to release" @@ -1251,3 +1265,50 @@ workflows: - rtd-ci-build-publish-snapshot - rtd-sdk-slack-secrets + #========================================================================= + # Workflows: Publish Snapshot + #========================================================================= + + publish-snapshot-android: + jobs: + - prepare-dependencies: + <<: *snapshot-android-tag-filter + context: + - rtd-ci-tokens + - rtd-ci-test-instance-config + - rtd-ci-build-publish-snapshot + - rtd-android-chat-demo-app-release + - build-twilsock-android: + <<: *snapshot-android-tag-filter + context: rtd-ci-build-publish-snapshot + requires: + - prepare-dependencies + - build-shared-internal-android: + <<: *snapshot-android-tag-filter + context: rtd-ci-build-publish-snapshot + requires: + - prepare-dependencies + - build-shared-public-android: + <<: *snapshot-android-tag-filter + context: rtd-ci-build-publish-snapshot + requires: + - prepare-dependencies + - build-sync-sdk-android: + <<: *snapshot-android-tag-filter + context: rtd-ci-build-publish-snapshot + requires: + - prepare-dependencies + - build-sync-docs-android: + <<: *snapshot-android-tag-filter + context: rtd-ci-build-publish-snapshot + requires: + - prepare-dependencies + - publish-snapshot-sdk-android: + <<: *snapshot-android-tag-filter + context: rtd-ci-build-publish-snapshot + requires: + - build-twilsock-android + - build-shared-internal-android + - build-shared-public-android + - build-sync-sdk-android + - build-sync-docs-android \ No newline at end of file diff --git a/BuildScripts/run-sdk-android-tests-in-gcloud.sh b/BuildScripts/run-sdk-android-tests-in-gcloud.sh index 02f0430..35d535c 100755 --- a/BuildScripts/run-sdk-android-tests-in-gcloud.sh +++ b/BuildScripts/run-sdk-android-tests-in-gcloud.sh @@ -22,11 +22,7 @@ $BUILDSCRIPTS_DIR/run-android-tests-in-gcloud.sh \ -"$RENAME_SUFFIX" \ "" \ $SHARDS_COUNT \ - model=Nexus5X,version=24 \ - model=Nexus5X,version=25 \ - model=Nexus5X,version=26 \ - model=HWMHA,version=24 \ - model=cactus,version=27 \ + model=blueline,version=28 \ model=redfin,version=30 \ model=oriole,version=31 \ model=oriole,version=32 \ diff --git a/BuildScripts/run-tests-kotlin-ios.rb b/BuildScripts/run-tests-kotlin-ios.rb index 5a850d3..db69745 100755 --- a/BuildScripts/run-tests-kotlin-ios.rb +++ b/BuildScripts/run-tests-kotlin-ios.rb @@ -28,11 +28,41 @@ def exec_command(message, command, fail_on_error: true) TEST_BINARY = ARGV[0] MAX_RETRIES = ARGV[1] ? ARGV[1].to_i : 2 -IOS_SIMULATOR_DEVICE_ID = ENV['IOS_SIMULATOR_DEVICE_ID'] || (raise "IOS_SIMULATOR_DEVICE_ID env var is not set") + +# Auto-detect an iPhone simulator if IOS_SIMULATOR_DEVICE_ID is not set +def detect_simulator_device_id + output = `xcrun simctl list devices available -j` + require 'json' + devices = JSON.parse(output)["devices"] + + # Find an available iPhone simulator (prefer iPhone 15 Pro or any iPhone) + devices.each do |runtime, device_list| + next unless runtime.include?("iOS") + device_list.each do |device| + if device["name"].include?("iPhone 15 Pro") + return device["udid"] + end + end + end + + # Fallback to any iPhone + devices.each do |runtime, device_list| + next unless runtime.include?("iOS") + device_list.each do |device| + if device["name"].include?("iPhone") + return device["udid"] + end + end + end + + raise "No iPhone simulator found" +end + +IOS_SIMULATOR_DEVICE_ID = ENV['IOS_SIMULATOR_DEVICE_ID'] || detect_simulator_device_id puts "TEST_BINARY: #{TEST_BINARY}" puts "MAX_RETRIES: #{MAX_RETRIES}" -puts "IOS_SIMULATOR_DEVICE_ID: #{ENV['IOS_SIMULATOR_DEVICE_ID']}" +puts "IOS_SIMULATOR_DEVICE_ID: #{IOS_SIMULATOR_DEVICE_ID}" exec_command "Booting simulator:", "xcrun simctl boot #{IOS_SIMULATOR_DEVICE_ID}", fail_on_error: false diff --git a/build.gradle b/build.gradle index ba8bd5f..784a202 100644 --- a/build.gradle +++ b/build.gradle @@ -34,13 +34,23 @@ if (project.hasProperty('disable-metadata-subtasks')) { apply from: "./disable-metadata-subtasks.gradle" } +// Determine if this is a snapshot build for nexusPublishing configuration +def gitTagForNexus = findProperty("gitTag") ?: "" +def isSnapshotForNexus = (gitTagForNexus =~ /^snapshot-sync-android-\d+\.\d+\.\d+$/).find() nexusPublishing { repositories { sonatype { + nexusUrl = uri("https://ossrh-staging-api.central.sonatype.com/service/local/") + snapshotRepositoryUrl = uri("https://central.sonatype.com/repository/maven-snapshots/") + username = project.getProperty('sonatype.username') password = project.getProperty('sonatype.password') packageGroup = "com.twilio" + + // useStaging = false for snapshots (publish directly to snapshot repo) + // useStaging = true for releases (publish to staging repo first) + useStaging = !isSnapshotForNexus } } } @@ -110,6 +120,18 @@ task packageDocs { dependsOn 'packageDokka' } +def gitHash = findProperty("gitHash") ?: "HEAD" +def gitTag = findProperty("gitTag") ?: "" + +// For snapshot builds, only append -SNAPSHOT to twilsock version +// shared-internal and shared-public use their released versions +def isSnapshotBuild = (gitTag =~ /^snapshot-sync-android-\d+\.\d+\.\d+$/).find() +if (isSnapshotBuild) { + twilsockVersion = twilsockVersion + "-SNAPSHOT" + // Don't snapshot shared-internal/shared-public - use released versions + println "Snapshot build detected - twilsock version suffixed with -SNAPSHOT" +} + def needPublishTwilsock = needPublishTwilsock(twilsockVersion) println "needPublishTwilsock: $needPublishTwilsock" @@ -119,8 +141,6 @@ println "need to publish shared-internal: $needPublishSharedInternal" def needPublishSharedPublic = needPublishSharedPublic(sharedPublicVersion) println "need to publish shared-public: $needPublishSharedPublic" -def gitHash = findProperty("gitHash") ?: "HEAD" -def gitTag = findProperty("gitTag") ?: "" def (isSyncReleaseCandidate, syncVersion) = generatePublishVersionNames(gitHash, gitTag) println "publishing isSyncReleaseCandidate?: $isSyncReleaseCandidate" @@ -235,7 +255,7 @@ if (needPublishSharedPublic) { tasks['generateMetadataFileForSharedPublicReleasePublication'].mustRunAfter(':shared-public:reZipReleaseAar') } -if (isSyncReleaseCandidate) { +if (isSyncReleaseCandidate || isSnapshotBuild) { // this line is parsed on circleci. don't touch it! // see: $MONOREPO/BuildScripts/push-sonatype-git-tag.sh println "Publishing sync v$syncVersion" @@ -318,14 +338,20 @@ if (isSyncReleaseCandidate) { } tasks['publishToSonatype'].with { - dependsOn(validateIfSingleRcTagOnCommit) - mustRunAfter(validateIfSingleRcTagOnCommit) + dependsOn(validatePublishTag) + mustRunAfter(validatePublishTag) } tasks['releaseSonatypeStagingRepository'].dependsOn(checkReleaseVersionMatchesRC) } // gradle.projectsEvaluated +task validatePublishTag { + doLast { + findPublishTag() // throws exception if no valid tag found + } +} + task validateIfSingleRcTagOnCommit { doLast { findRCTag() // throws exception if zero or more than one RC tag found @@ -342,6 +368,29 @@ task checkReleaseVersionMatchesRC { } } +def findPublishTag() { + def isRcTag = { tag -> + (tag =~ /^release-sync-android-\d+\.\d+\.\d+-rc\d+$/).find() + } + def isSnapshotTag = { tag -> + (tag =~ /^snapshot-sync-android-\d+\.\d+\.\d+$/).find() + } + + def tags = getGitTagList(gitHash) + .findAll { isRcTag(it) || isSnapshotTag(it) } + + if (tags.size > 1) { + throw new GradleException("Attempt to build with multiple publish tags on same commit") + } + + if (tags.size == 0) { + throw new GradleException("Cannot find RC or snapshot tag") + } + + println "Found publish tag: ${tags[0]}" + return tags[0] +} + def findRCTag() { def isRcTag = { tag -> (tag =~ /^release-sync-android-\d+\.\d+\.\d+-rc\d+$/).find() diff --git a/sdk/shared/shared-test/src/commonMain/kotlin/com/twilio/test/util/commonTestUtils.kt b/sdk/shared/shared-test/src/commonMain/kotlin/com/twilio/test/util/commonTestUtils.kt index d2bde37..8df1605 100644 --- a/sdk/shared/shared-test/src/commonMain/kotlin/com/twilio/test/util/commonTestUtils.kt +++ b/sdk/shared/shared-test/src/commonMain/kotlin/com/twilio/test/util/commonTestUtils.kt @@ -86,7 +86,9 @@ class TestContinuationTokenStorage : ContinuationTokenStorage { class TestConnectivityMonitor : ConnectivityMonitor { override val isNetworkAvailable = true + override val defaultNetworkId: String? = null override var onChanged: () -> Unit = {} + override var onDefaultNetworkChanged: (networkId: String?) -> Unit = {} override fun start() = Unit override fun stop() = Unit } diff --git a/sdk/twilsock/api/twilsock.api b/sdk/twilsock/api/twilsock.api index 2b19aee..8d30043 100644 --- a/sdk/twilsock/api/twilsock.api +++ b/sdk/twilsock/api/twilsock.api @@ -91,6 +91,8 @@ public abstract interface class com/twilio/twilsock/client/Twilsock { public abstract fun getAccountDescriptor ()Lcom/twilio/util/AccountDescriptor; public abstract fun handleMessageReceived ([B)V public abstract fun isConnected ()Z + public abstract fun onAppBackgrounded ()V + public abstract fun onAppForegrounded ()V public abstract fun populateInitRegistrations (Ljava/util/Set;)V public abstract fun sendRequest (Lcom/twilio/twilsock/util/HttpRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun sendRequest-dWUq8MI (Ljava/lang/String;J[BLkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -189,9 +191,12 @@ public final class com/twilio/twilsock/commands/CommandsScheduler { } public abstract interface class com/twilio/twilsock/util/ConnectivityMonitor { + public abstract fun getDefaultNetworkId ()Ljava/lang/String; public abstract fun getOnChanged ()Lkotlin/jvm/functions/Function0; + public abstract fun getOnDefaultNetworkChanged ()Lkotlin/jvm/functions/Function1; public abstract fun isNetworkAvailable ()Z public abstract fun setOnChanged (Lkotlin/jvm/functions/Function0;)V + public abstract fun setOnDefaultNetworkChanged (Lkotlin/jvm/functions/Function1;)V public abstract fun start ()V public abstract fun stop ()V } diff --git a/sdk/twilsock/src/androidMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitorAndroid.kt b/sdk/twilsock/src/androidMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitorAndroid.kt index 77ee4a7..1cbfa0d 100644 --- a/sdk/twilsock/src/androidMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitorAndroid.kt +++ b/sdk/twilsock/src/androidMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitorAndroid.kt @@ -35,13 +35,30 @@ internal actual class ConnectivityMonitorImpl actual constructor(private val cor } } + override val defaultNetworkId: String? + get() = try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + connectivityManager?.activeNetwork?.toString() + } else { + null + } + } catch (e: Exception) { + logger.w("Cannot get defaultNetworkId", e) + null + } + override var onChanged: () -> Unit = {} + override var onDefaultNetworkChanged: (networkId: String?) -> Unit = {} private val connectionStatusCallback by lazy { ConnectionStatusCallback() } + private val defaultNetworkCallback by lazy { DefaultNetworkCallback() } override fun start() { try { connectivityManager?.registerNetworkCallback(NetworkRequest.Builder().build(), connectionStatusCallback) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + connectivityManager?.registerDefaultNetworkCallback(defaultNetworkCallback) + } } catch (e: Exception) { logger.w("Cannot registerNetworkCallback (probably app doesn't have ACCESS_NETWORK_STATE " + "permission? Considering network as always available)", e) @@ -52,6 +69,9 @@ internal actual class ConnectivityMonitorImpl actual constructor(private val cor override fun stop() { try { connectivityManager?.unregisterNetworkCallback(connectionStatusCallback) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + connectivityManager?.unregisterNetworkCallback(defaultNetworkCallback) + } } catch (e: Exception) { logger.w("Cannot unregisterNetworkCallback (probably app doesn't have ACCESS_NETWORK_STATE " + "permission?", e) @@ -97,4 +117,16 @@ internal actual class ConnectivityMonitorImpl actual constructor(private val cor isNetworkAvailable = activeNetworks.isNotEmpty() } } + + private inner class DefaultNetworkCallback : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + logger.d { "Default network changed to: $network" } + coroutineScope.launch { onDefaultNetworkChanged(network.toString()) } + } + + override fun onLost(network: Network) { + logger.d { "Default network lost: $network" } + coroutineScope.launch { onDefaultNetworkChanged(null) } + } + } } diff --git a/sdk/twilsock/src/androidUnitTest/kotlin/com/twilio/twilsock/client/test/unit/states/BaseTwilsockTest.kt b/sdk/twilsock/src/androidUnitTest/kotlin/com/twilio/twilsock/client/test/unit/states/BaseTwilsockTest.kt index 687453a..4789b42 100644 --- a/sdk/twilsock/src/androidUnitTest/kotlin/com/twilio/twilsock/client/test/unit/states/BaseTwilsockTest.kt +++ b/sdk/twilsock/src/androidUnitTest/kotlin/com/twilio/twilsock/client/test/unit/states/BaseTwilsockTest.kt @@ -42,13 +42,16 @@ open class BaseTwilsockTest { lateinit var twilsockObserver: TwilsockObserver var onConnectivityChanged = {} + var onDefaultNetworkChanged: (String?) -> Unit = {} @BeforeTest open fun setUp() { setupTestLogging() MockKAnnotations.init(this@BaseTwilsockTest, relaxUnitFun = true) every { connectivityMonitor.isNetworkAvailable } returns true + every { connectivityMonitor.defaultNetworkId } returns "defaultNetwork" every { connectivityMonitor.onChanged = any() } propertyType onConnectivityChanged::class answers { onConnectivityChanged = value } + every { connectivityMonitor.onDefaultNetworkChanged = any() } propertyType onDefaultNetworkChanged::class answers { onDefaultNetworkChanged = value } every { twilsockTransportFactory(any(), any(), any(), any()) } returns twilsockTransport clearTwilsockObserverMock() diff --git a/sdk/twilsock/src/androidUnitTest/kotlin/com/twilio/twilsock/client/test/unit/states/WaitAndReconnectTwilsockTest.kt b/sdk/twilsock/src/androidUnitTest/kotlin/com/twilio/twilsock/client/test/unit/states/WaitAndReconnectTwilsockTest.kt index 7cf99f3..8cd8cd2 100644 --- a/sdk/twilsock/src/androidUnitTest/kotlin/com/twilio/twilsock/client/test/unit/states/WaitAndReconnectTwilsockTest.kt +++ b/sdk/twilsock/src/androidUnitTest/kotlin/com/twilio/twilsock/client/test/unit/states/WaitAndReconnectTwilsockTest.kt @@ -51,8 +51,12 @@ class WaitAndReconnectTwilsockTest : BaseTwilsockTest() { @Test fun connect() { + // Before fix: failedReconnectionAttempts was NOT reset on explicit connect() + // After fix: it should be reset to 0 for immediate reconnection + assertTrue(twilsock.failedReconnectionAttempts > 0) // Verify we're in a backoff state twilsock.connect() assertIs(twilsock.state) + assertEquals(0, twilsock.failedReconnectionAttempts) } @Test @@ -114,4 +118,42 @@ class WaitAndReconnectTwilsockTest : BaseTwilsockTest() { assertEquals(Timeout, result.twilioException.errorInfo.reason) assertEquals(0, twilsock.pendingRequests.size) } + + @Test + fun appForegrounded() = runTest { + // Verify we're in backoff state + assertTrue(twilsock.failedReconnectionAttempts > 0) + + twilsock.onAppForegrounded() + wait { twilsock.state is Connecting } + + assertIs(twilsock.state) + assertEquals(0, twilsock.failedReconnectionAttempts) + } + + @Test + fun appBackgrounded() = runTest { + // Verify we're in WaitAndReconnect state + assertIs(twilsock.state) + + twilsock.onAppBackgrounded() + + // Should stay in WaitAndReconnect but timer is cancelled (pause reconnection) + assertIs(twilsock.state) + } + + @Test + fun defaultNetworkChanged() = runTest { + // Verify we're in backoff state + assertTrue(twilsock.failedReconnectionAttempts > 0) + + // Simulate network change callback + every { connectivityMonitor.defaultNetworkId } returns "newNetwork123" + onDefaultNetworkChanged("newNetwork123") + + wait { twilsock.state is Connecting } + + assertIs(twilsock.state) + assertEquals(0, twilsock.failedReconnectionAttempts) + } } diff --git a/sdk/twilsock/src/commonMain/kotlin/com/twilio/twilsock/client/Twilsock.kt b/sdk/twilsock/src/commonMain/kotlin/com/twilio/twilsock/client/Twilsock.kt index 223818c..b1aec9d 100644 --- a/sdk/twilsock/src/commonMain/kotlin/com/twilio/twilsock/client/Twilsock.kt +++ b/sdk/twilsock/src/commonMain/kotlin/com/twilio/twilsock/client/Twilsock.kt @@ -15,6 +15,9 @@ import com.twilio.twilsock.client.TwilsockEvent.OnDisconnect import com.twilio.twilsock.client.TwilsockEvent.OnFatalError import com.twilio.twilsock.client.TwilsockEvent.OnInitMessageReceived import com.twilio.twilsock.client.TwilsockEvent.OnMessageReceived +import com.twilio.twilsock.client.TwilsockEvent.OnDefaultNetworkChanged +import com.twilio.twilsock.client.TwilsockEvent.OnAppForegrounded +import com.twilio.twilsock.client.TwilsockEvent.OnAppBackgrounded import com.twilio.twilsock.client.TwilsockEvent.OnNetworkBecameReachable import com.twilio.twilsock.client.TwilsockEvent.OnNetworkBecameUnreachable import com.twilio.twilsock.client.TwilsockEvent.OnNonFatalError @@ -95,6 +98,10 @@ interface Twilsock { fun handleMessageReceived(data: ByteArray) fun addObserver(block: TwilsockObserver.() -> Unit): Unsubscriber + + fun onAppForegrounded() + + fun onAppBackgrounded() } data class AuthData( @@ -135,9 +142,12 @@ private sealed class TwilsockEvent { data class OnSendRequest(val request: TwilsockRequest) : TwilsockEvent() object OnTransportConnected : TwilsockEvent() object OnInitMessageReceived : TwilsockEvent() - data class OnTooManyRequests(val waitTime: Duration) : TwilsockEvent() + data class OnTooManyRequests(val waitTime: Duration, val errorInfo: ErrorInfo) : TwilsockEvent() object OnNetworkBecameReachable : TwilsockEvent() object OnNetworkBecameUnreachable : TwilsockEvent() + data class OnDefaultNetworkChanged(val networkId: String?) : TwilsockEvent() + object OnAppForegrounded : TwilsockEvent() + object OnAppBackgrounded : TwilsockEvent() object OnTimeout : TwilsockEvent() data class OnNonFatalError(val errorInfo: ErrorInfo) : TwilsockEvent() data class OnFatalError(val errorInfo: ErrorInfo) : TwilsockEvent() @@ -199,6 +209,9 @@ internal class TwilsockImpl( private var websocket: TwilsockTransport? = null + private var connectedNetworkId: String? = null + private val defaultNetworkId: String? get() = connectivityMonitor.defaultNetworkId + private val isNetworkAvailable get() = connectivityMonitor.isNetworkAvailable private val watchdogTimer = Timer(coroutineScope) @@ -285,8 +298,7 @@ internal class TwilsockImpl( } on { transitionTo(Connected) } on { event -> - val errorInfo = ErrorInfo(TooManyRequests) - transitionTo(WaitAndReconnect(event.waitTime), NotifyObservers { onNonFatalError(errorInfo) }) + transitionTo(WaitAndReconnect(event.waitTime), NotifyObservers { onNonFatalError(event.errorInfo) }) } defaultOnNetworkBecameUnreachable() defaultOnNonFatalError() @@ -296,6 +308,8 @@ internal class TwilsockImpl( state { onEnter { failedReconnectionAttempts = 0 + connectedNetworkId = defaultNetworkId + logger.i { "Connected on network $connectedNetworkId" } startWatchdogTimer() sendAllPendingRequests() notifyObservers { onConnected() } @@ -315,8 +329,17 @@ internal class TwilsockImpl( dontTransition() } on { event -> - val errorInfo = ErrorInfo(TooManyRequests) - transitionTo(Throttling(event.waitTime), NotifyObservers { onNonFatalError(errorInfo) }) + transitionTo(Throttling(event.waitTime), NotifyObservers { onNonFatalError(event.errorInfo) }) + } + on { event -> + val newId = event.networkId + if (newId != null && newId != connectedNetworkId && isNetworkAvailable) { + logger.w { "Default network changed: $connectedNetworkId -> $newId, forcing reconnect" } + val errorInfo = ErrorInfo(NetworkBecameUnreachable, message = "Default network changed") + transitionTo(WaitAndReconnect(waitTime = 0.seconds), NotifyObservers { onNonFatalError(errorInfo) }) + } else { + dontTransition() + } } defaultOnNetworkBecameUnreachable() defaultOnNonFatalError() @@ -345,7 +368,10 @@ internal class TwilsockImpl( timer.cancel() } on { transitionTo(Connecting) } - on { transitionTo(Connecting) } + on { + failedReconnectionAttempts = 0 + transitionTo(Connecting) + } defaultOnDisconnect() on { event -> token = event.token @@ -364,6 +390,18 @@ internal class TwilsockImpl( timer.cancel() dontTransition() } + on { + failedReconnectionAttempts = 0 + transitionTo(Connecting) + } + on { + failedReconnectionAttempts = 0 + transitionTo(Connecting) + } + on { + timer.cancel() + dontTransition() + } // Fatal/NonFatalError are ignored: // 1. Websocket is disconnected in this state. So no errors can happen. // 2. shutdownWebSocket() can lead to onTransportDisconnected() callback which @@ -432,6 +470,26 @@ internal class TwilsockImpl( init { connectivityMonitor.onChanged = this::onConnectivityChanged + connectivityMonitor.onDefaultNetworkChanged = this::onDefaultNetworkChanged + } + + override fun onAppForegrounded() { + logger.d { "onAppForegrounded" } + coroutineScope.launch { + stateMachine.transition(OnAppForegrounded) + } + } + + override fun onAppBackgrounded() { + logger.d { "onAppBackgrounded" } + coroutineScope.launch { + stateMachine.transition(OnAppBackgrounded) + } + } + + private fun onDefaultNetworkChanged(networkId: String?) { + logger.d { "onDefaultNetworkChanged: $networkId" } + stateMachine.transition(OnDefaultNetworkChanged(networkId)) } private fun failAllPendingRequests(errorInfo: ErrorInfo) { @@ -844,7 +902,7 @@ internal class TwilsockImpl( backoffPolicy.reconnectMaxMilliseconds, ) - stateMachine.transition(OnTooManyRequests(waitTime.milliseconds)) + stateMachine.transition(OnTooManyRequests(waitTime.milliseconds, errorInfo)) } else -> stateMachine.transition(OnNonFatalError(errorInfo)) diff --git a/sdk/twilsock/src/commonMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitor.kt b/sdk/twilsock/src/commonMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitor.kt index 2b2c017..581fee9 100644 --- a/sdk/twilsock/src/commonMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitor.kt +++ b/sdk/twilsock/src/commonMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitor.kt @@ -10,7 +10,9 @@ import kotlinx.coroutines.CoroutineScope interface ConnectivityMonitor { val isNetworkAvailable: Boolean + val defaultNetworkId: String? var onChanged: () -> Unit + var onDefaultNetworkChanged: (networkId: String?) -> Unit fun start() fun stop() diff --git a/sdk/twilsock/src/iosMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitorIOS.kt b/sdk/twilsock/src/iosMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitorIOS.kt index 7d07410..ba65341 100644 --- a/sdk/twilsock/src/iosMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitorIOS.kt +++ b/sdk/twilsock/src/iosMain/kotlin/com/twilio/twilsock/util/ConnectivityMonitorIOS.kt @@ -11,7 +11,9 @@ import kotlinx.coroutines.CoroutineScope internal actual class ConnectivityMonitorImpl actual constructor(coroutineScope: CoroutineScope): ConnectivityMonitor { override val isNetworkAvailable = true + override val defaultNetworkId: String? = null override var onChanged: () -> Unit = {} + override var onDefaultNetworkChanged: (networkId: String?) -> Unit = {} override fun start() = Unit override fun stop() = Unit } diff --git a/sdk/utils/prepare-version-names.gradle b/sdk/utils/prepare-version-names.gradle index 18b16a6..91a7603 100644 --- a/sdk/utils/prepare-version-names.gradle +++ b/sdk/utils/prepare-version-names.gradle @@ -18,6 +18,13 @@ ext.generateVersionNames = { gitHash, gitTag -> def isSyncReleaseCandidate = false def syncVersion = "SNAPSHOT" + // Check for snapshot tag first: snapshot-sync-android-X.Y.Z + def syncSnapshotTag = (gitTag =~ /^snapshot-sync-android-(\d+\.\d+\.\d+)$/) + if (syncSnapshotTag.find()) { + syncVersion = syncSnapshotTag.group(1) + "-SNAPSHOT" + } + + // Check for release candidate tag: release-sync-android-X.Y.Z-rcN def syncReleaseTag = (gitTag =~ /^release-sync-android-((\d+\.\d+\.\d+)-rc(\d+))$/) if (syncReleaseTag.find()) { isSyncReleaseCandidate = true diff --git a/sdk/utils/shared-internal-version.gradle b/sdk/utils/shared-internal-version.gradle index 4277e0c..7075178 100644 --- a/sdk/utils/shared-internal-version.gradle +++ b/sdk/utils/shared-internal-version.gradle @@ -1,3 +1,3 @@ ext { - sharedInternalVersion = "3.0.0" + sharedInternalVersion = "2.1.1" } diff --git a/sdk/utils/shared-public-version.gradle b/sdk/utils/shared-public-version.gradle index e415e47..08b26a2 100644 --- a/sdk/utils/shared-public-version.gradle +++ b/sdk/utils/shared-public-version.gradle @@ -1,3 +1,3 @@ ext { - sharedPublicVersion = "1.2.2" + sharedPublicVersion = "1.2.1" } diff --git a/sdk/utils/twilsock-version.gradle b/sdk/utils/twilsock-version.gradle index 2165239..f164e19 100644 --- a/sdk/utils/twilsock-version.gradle +++ b/sdk/utils/twilsock-version.gradle @@ -1,3 +1,3 @@ ext { - twilsockVersion = "3.1.2" + twilsockVersion = "3.2.0" }