Skip to content

Commit

Permalink
fix: Reflect WiFi hotspot availability on Android 13 (#573)
Browse files Browse the repository at this point in the history
# What kind of change does this PR introduce?
Fixes the hotspot state detection on Android 13.

# What is the current behavior?
This is a temporary fix for [#569 ](#569) but a long-term solution is still needed.

# What is the new behavior (if this is a feature change)?
On Android 13, we are looking to the gateway addresses request results to infer if hotspot is available. 
In previous versions of Android, there is no behavior change.
  • Loading branch information
LilianaFaustinoDev authored Oct 4, 2022
1 parent 4c08e68 commit 88a791d
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 71 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ repositories {
}

android {
compileSdkVersion 32
compileSdkVersion 33
ndkVersion '21.3.6528147'

defaultConfig {
applicationId "tech.relaycorp.courier"
minSdkVersion 21
targetSdkVersion 32
targetSdkVersion 33
versionCode 1
versionName project.findProperty("versionName") ?: "0.1"

Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/tech/relaycorp/courier/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package tech.relaycorp.courier
import android.app.Application
import android.os.Build
import android.os.StrictMode
import tech.relaycorp.courier.background.WifiHotspotStateReceiver
import tech.relaycorp.courier.background.ForegroundAppMonitor
import tech.relaycorp.courier.background.WifiHotspotStateWatcher
import tech.relaycorp.courier.common.Logging
import tech.relaycorp.courier.common.di.AppComponent
import tech.relaycorp.courier.common.di.DaggerAppComponent
Expand All @@ -14,7 +15,10 @@ import javax.inject.Inject
open class App : Application() {

@Inject
lateinit var wifiHotspotStateReceiver: WifiHotspotStateReceiver
lateinit var wifiHotspotStateWatcher: WifiHotspotStateWatcher

@Inject
lateinit var foregroundAppMonitor: ForegroundAppMonitor

open val component: AppComponent by lazy {
DaggerAppComponent.builder()
Expand All @@ -36,12 +40,13 @@ open class App : Application() {
component.inject(this)
setupLogger()
setupStrictMode()
wifiHotspotStateReceiver.register()
registerActivityLifecycleCallbacks(foregroundAppMonitor)
wifiHotspotStateWatcher.start()
}

override fun onTerminate() {
super.onTerminate()
wifiHotspotStateReceiver.unregister()
wifiHotspotStateWatcher.stop()
}

private fun setupLogger() {
Expand Down
24 changes: 24 additions & 0 deletions app/src/main/java/tech/relaycorp/courier/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import android.content.res.Resources
import android.net.ConnectivityManager
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.Dispatchers
import tech.relaycorp.cogrpc.server.Networking
import javax.inject.Named
import kotlin.coroutines.CoroutineContext

@Module
class AppModule(
Expand All @@ -26,4 +30,24 @@ class AppModule(
@Provides
fun connectivityManager() =
app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

@Provides
fun wifiApState(): WifiApStateAvailability =
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) {
WifiApStateAvailability.Available
} else {
WifiApStateAvailability.Unavailable
}

@Provides
@Named("GetGatewayIpAddress")
fun getGatewayIpAddress(): () -> String = Networking::getGatewayIpAddress

@Provides
@Named("BackgroundCoroutineContext")
fun backgroundCoroutineContext(): CoroutineContext = Dispatchers.IO

enum class WifiApStateAvailability {
Available, Unavailable
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package tech.relaycorp.courier.background

import android.app.Activity
import android.app.Application
import android.os.Bundle
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ForegroundAppMonitor
@Inject constructor() : Application.ActivityLifecycleCallbacks {
private val activityCountFlow = MutableStateFlow(0)

fun observe() = activityCountFlow.map { if (it == 0) State.Background else State.Foreground }

override fun onActivityStarted(activity: Activity) {
activityCountFlow.value++
}

override fun onActivityStopped(activity: Activity) {
activityCountFlow.value--
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
override fun onActivityResumed(activity: Activity) = Unit
override fun onActivityPaused(activity: Activity) = Unit
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
override fun onActivityDestroyed(activity: Activity) = Unit

enum class State {
Foreground, Background
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package tech.relaycorp.courier.background

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.wifi.WifiManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import tech.relaycorp.cogrpc.server.GatewayIPAddressException
import tech.relaycorp.courier.AppModule.WifiApStateAvailability
import tech.relaycorp.courier.common.Logging.logger
import tech.relaycorp.courier.common.tickerFlow
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.seconds

@Singleton
class WifiHotspotStateWatcher
@Inject constructor(
private val context: Context,
private val wifiApState: WifiApStateAvailability,
private val foregroundAppMonitor: ForegroundAppMonitor,
@Named("GetGatewayIpAddress") private val getGatewayIpAddress: () -> String,
@Named("BackgroundCoroutineContext") private val backgroundCoroutineContext: CoroutineContext
) {

private val state = MutableStateFlow(WifiHotspotState.Disabled)
fun state() = state.asStateFlow()

private var pollingGatewayAddressesJob: Job? = null

fun start() {
when (wifiApState) {
WifiApStateAvailability.Available -> {
context.registerReceiver(
wifiApStateChangeReceiver,
IntentFilter(WIFI_AP_STATE_CHANGED_ACTION)
)
}
WifiApStateAvailability.Unavailable -> {
startPollingGatewayAddresses()
}
}
}

fun stop() {
when (wifiApState) {
WifiApStateAvailability.Available -> {
context.unregisterReceiver(wifiApStateChangeReceiver)
}
WifiApStateAvailability.Unavailable -> {
stopPollingGatewayAddresses()
}
}
}

private fun startPollingGatewayAddresses() {
pollingGatewayAddressesJob = foregroundAppMonitor.observe()
.flatMapLatest {
if (it == ForegroundAppMonitor.State.Foreground) {
tickerFlow(POLLING_GATEWAY_ADDRESS_INTERVAL)
} else {
emptyFlow()
}
}.map {
try {
getGatewayIpAddress()
WifiHotspotState.Enabled
} catch (exception: GatewayIPAddressException) {
WifiHotspotState.Disabled
}
}
.distinctUntilChanged()
.onEach {
logger.info("Hotspot State $it")
state.value = it
}
.launchIn(CoroutineScope(backgroundCoroutineContext))
}

private fun stopPollingGatewayAddresses() {
pollingGatewayAddressesJob?.cancel()
pollingGatewayAddressesJob = null
}

private val wifiApStateChangeReceiver by lazy {
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != WIFI_AP_STATE_CHANGED_ACTION) return

val stateFlag = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0)
logger.info("Hotspot State $stateFlag")
state.value =
if (stateFlag == WIFI_AP_STATE_ENABLED) {
WifiHotspotState.Enabled
} else {
WifiHotspotState.Disabled
}
}
}
}

companion object {
// From WifiManager documentation
private const val WIFI_AP_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_AP_STATE_CHANGED"
private const val WIFI_AP_STATE_ENABLED = 13

private val POLLING_GATEWAY_ADDRESS_INTERVAL = 2.seconds
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/tech/relaycorp/courier/common/TickerFlow.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tech.relaycorp.courier.common

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlin.time.Duration

fun tickerFlow(duration: Duration) = flow {
while (true) {
emit(Unit)
delay(duration)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import kotlinx.coroutines.launch
import tech.relaycorp.courier.background.InternetConnection
import tech.relaycorp.courier.background.InternetConnectionObserver
import tech.relaycorp.courier.background.WifiHotspotState
import tech.relaycorp.courier.background.WifiHotspotStateReceiver
import tech.relaycorp.courier.background.WifiHotspotStateWatcher
import tech.relaycorp.courier.common.BehaviorChannel
import tech.relaycorp.courier.data.model.StorageSize
import tech.relaycorp.courier.data.model.StorageUsage
Expand All @@ -22,7 +22,7 @@ import javax.inject.Inject
class MainViewModel
@Inject constructor(
internetConnectionObserver: InternetConnectionObserver,
hotspotStateReceiver: WifiHotspotStateReceiver,
hotspotStateReceiver: WifiHotspotStateWatcher,
getStorageUsage: GetStorageUsage,
observeCCACount: ObserveCCACount,
deleteExpiredMessages: DeleteExpiredMessages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ package tech.relaycorp.courier.ui.sync.people

import kotlinx.coroutines.flow.map
import tech.relaycorp.courier.background.WifiHotspotState
import tech.relaycorp.courier.background.WifiHotspotStateReceiver
import tech.relaycorp.courier.background.WifiHotspotStateWatcher
import tech.relaycorp.courier.ui.BaseViewModel
import javax.inject.Inject

class HotspotInstructionsViewModel
@Inject constructor(
private val wifiHotspotStateReceiver: WifiHotspotStateReceiver
private val wifiHotspotStateWatcher: WifiHotspotStateWatcher
) : BaseViewModel() {

fun state() =
wifiHotspotStateReceiver
wifiHotspotStateWatcher
.state()
.map { it.toState() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import tech.relaycorp.courier.background.WifiHotspotState
import tech.relaycorp.courier.background.WifiHotspotStateReceiver
import tech.relaycorp.courier.background.WifiHotspotStateWatcher
import tech.relaycorp.courier.common.BehaviorChannel
import tech.relaycorp.courier.common.PublishChannel
import tech.relaycorp.courier.domain.PrivateSync
Expand All @@ -21,7 +21,7 @@ import javax.inject.Inject
class PeopleSyncViewModel
@Inject constructor(
private val privateSync: PrivateSync,
wifiHotspotStateReceiver: WifiHotspotStateReceiver
wifiHotspotStateWatcher: WifiHotspotStateWatcher
) : BaseViewModel() {

// Inputs
Expand Down Expand Up @@ -49,7 +49,7 @@ class PeopleSyncViewModel
private var hadFirstClient = false

init {
wifiHotspotStateReceiver
wifiHotspotStateWatcher
.state()
.take(1)
.onEach {
Expand All @@ -63,7 +63,7 @@ class PeopleSyncViewModel
}
.launchIn(scope)

wifiHotspotStateReceiver
wifiHotspotStateWatcher
.state()
.drop(1)
.filter { it == WifiHotspotState.Disabled }
Expand Down
Loading

0 comments on commit 88a791d

Please sign in to comment.