Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Reflect WiFi hotspot availability on Android 13 #573

Merged
merged 5 commits into from
Oct 4, 2022
Merged
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
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