diff --git a/mobile/src/foss/java/org/openhab/habdroid/core/CloudMessagingHelper.kt b/mobile/src/foss/java/org/openhab/habdroid/core/CloudMessagingHelper.kt index 8655ba1965..361447e56b 100644 --- a/mobile/src/foss/java/org/openhab/habdroid/core/CloudMessagingHelper.kt +++ b/mobile/src/foss/java/org/openhab/habdroid/core/CloudMessagingHelper.kt @@ -17,12 +17,12 @@ import android.content.Context import android.content.Intent import android.util.Log import androidx.core.content.edit +import kotlinx.coroutines.flow.first import org.json.JSONArray import org.json.JSONException import org.openhab.habdroid.R import org.openhab.habdroid.core.connection.CloudConnection import org.openhab.habdroid.core.connection.Connection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.core.connection.NotACloudServerException import org.openhab.habdroid.model.CloudMessage import org.openhab.habdroid.model.toCloudMessage @@ -30,6 +30,7 @@ import org.openhab.habdroid.ui.CloudNotificationListFragment import org.openhab.habdroid.ui.preference.PushNotificationStatus import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.PrefKeys +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getHumanReadableErrorMessage import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.getPrimaryServerId @@ -51,8 +52,7 @@ object CloudMessagingHelper { context.getPrefs().getBoolean(PrefKeys.FOSS_NOTIFICATIONS_ENABLED, false) suspend fun pollForNotifications(context: Context) { - ConnectionFactory.waitForInitialization() - val connection = ConnectionFactory.primaryCloudConnection?.connection + val connection = context.getConnectionFactory().primaryFlow.first().conn?.connection if (connection == null) { Log.d(TAG, "No connection for loading notifications") return @@ -96,8 +96,7 @@ object CloudMessagingHelper { } suspend fun getPushNotificationStatus(context: Context): PushNotificationStatus { - ConnectionFactory.waitForInitialization() - val cloudFailure = ConnectionFactory.primaryCloudConnection?.failureReason + val cloudResult = context.getConnectionFactory().primaryFlow.first().cloud val prefs = context.getPrefs() return when { !prefs.getBoolean(PrefKeys.FOSS_NOTIFICATIONS_ENABLED, false) -> PushNotificationStatus( @@ -112,19 +111,19 @@ object CloudMessagingHelper { false ) - ConnectionFactory.primaryCloudConnection?.connection != null -> PushNotificationStatus( + cloudResult?.connection != null -> PushNotificationStatus( context.getString(R.string.push_notification_status_impaired), R.drawable.ic_bell_ring_outline_grey_24dp, false ) - cloudFailure != null && cloudFailure !is NotACloudServerException -> { + cloudResult?.failureReason != null && cloudResult.failureReason !is NotACloudServerException -> { val message = context.getString( R.string.push_notification_status_http_error, context.getHumanReadableErrorMessage( - if (cloudFailure is HttpClient.HttpException) cloudFailure.originalUrl else "", - if (cloudFailure is HttpClient.HttpException) cloudFailure.statusCode else 0, - cloudFailure, + (cloudResult.failureReason as? HttpClient.HttpException)?.originalUrl ?: "", + (cloudResult.failureReason as? HttpClient.HttpException)?.statusCode ?: 0, + cloudResult.failureReason, true ) ) diff --git a/mobile/src/full/java/org/openhab/habdroid/core/CloudMessagingHelper.kt b/mobile/src/full/java/org/openhab/habdroid/core/CloudMessagingHelper.kt index 8f05c4121f..6d4f3b1c23 100644 --- a/mobile/src/full/java/org/openhab/habdroid/core/CloudMessagingHelper.kt +++ b/mobile/src/full/java/org/openhab/habdroid/core/CloudMessagingHelper.kt @@ -17,12 +17,13 @@ import android.content.Context import android.content.Intent import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability +import kotlinx.coroutines.flow.first import org.openhab.habdroid.R import org.openhab.habdroid.core.connection.CloudConnection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.core.connection.NotACloudServerException import org.openhab.habdroid.ui.preference.PushNotificationStatus import org.openhab.habdroid.util.HttpClient +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getHumanReadableErrorMessage import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.getPrimaryServerId @@ -53,10 +54,9 @@ object CloudMessagingHelper { } suspend fun getPushNotificationStatus(context: Context): PushNotificationStatus { - ConnectionFactory.waitForInitialization() + val result = context.getConnectionFactory().primaryFlow.first() val prefs = context.getPrefs() - val cloudConnectionResult = ConnectionFactory.primaryCloudConnection - val cloudFailure = cloudConnectionResult?.failureReason + val cloudFailure = result.cloud?.failureReason return when { // No remote server is configured prefs.getRemoteUrl(prefs.getPrimaryServerId()).isEmpty() -> PushNotificationStatus( @@ -80,7 +80,7 @@ object CloudMessagingHelper { } // Remote server is configured, but it's not a cloud instance - cloudConnectionResult?.connection == null && ConnectionFactory.hasPrimaryRemoteConnection -> + result.cloud?.connection == null && result.hasRemote -> PushNotificationStatus( context.getString(R.string.push_notification_status_remote_no_cloud), R.drawable.ic_bell_off_outline_grey_24dp, diff --git a/mobile/src/full/java/org/openhab/habdroid/core/FcmRegistrationWorker.kt b/mobile/src/full/java/org/openhab/habdroid/core/FcmRegistrationWorker.kt index 2d926e3a9f..54b17ae5ab 100644 --- a/mobile/src/full/java/org/openhab/habdroid/core/FcmRegistrationWorker.kt +++ b/mobile/src/full/java/org/openhab/habdroid/core/FcmRegistrationWorker.kt @@ -39,13 +39,14 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import org.openhab.habdroid.R import org.openhab.habdroid.core.connection.CloudConnection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.PendingIntent_Immutable import org.openhab.habdroid.util.Util +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.parcelable class FcmRegistrationWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @@ -75,10 +76,7 @@ class FcmRegistrationWorker(private val context: Context, params: WorkerParamete val action = inputData.getString(KEY_ACTION) Log.d(TAG, "Run with action $action") - ConnectionFactory.waitForInitialization() - - val connection = ConnectionFactory.primaryCloudConnection?.connection - + val connection = context.getConnectionFactory().primaryFlow.first().cloud?.connection if (connection == null) { Log.d(TAG, "Got no connection") return retryOrFail() diff --git a/mobile/src/main/java/org/openhab/habdroid/background/ItemUpdateWorker.kt b/mobile/src/main/java/org/openhab/habdroid/background/ItemUpdateWorker.kt index e7c0545374..097cd82c0a 100644 --- a/mobile/src/main/java/org/openhab/habdroid/background/ItemUpdateWorker.kt +++ b/mobile/src/main/java/org/openhab/habdroid/background/ItemUpdateWorker.kt @@ -31,16 +31,17 @@ import java.net.SocketTimeoutException import java.text.SimpleDateFormat import java.util.Locale import java.util.TimeZone +import kotlinx.coroutines.flow.first import kotlinx.parcelize.Parcelize import org.openhab.habdroid.R import org.openhab.habdroid.background.NotificationUpdateObserver.Companion.NOTIFICATION_ID_BACKGROUND_WORK_RUNNING import org.openhab.habdroid.core.connection.Connection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.model.Item import org.openhab.habdroid.ui.TaskerItemPickerActivity import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.ItemClient import org.openhab.habdroid.util.TaskerPlugin +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getHumanReadableErrorMessage import org.openhab.habdroid.util.getPrefixForVoice import org.openhab.habdroid.util.getPrefs @@ -54,15 +55,15 @@ class ItemUpdateWorker(context: Context, params: WorkerParameters) : CoroutineWo if (isImportant) { setForegroundAsync(getForegroundInfo()) } - ConnectionFactory.waitForInitialization() Log.d(TAG, "Trying to get connection") - val connection = if (inputData.getBoolean(INPUT_DATA_PRIMARY_SERVER, false)) { - ConnectionFactory.primaryUsableConnection?.connection + val connectionFlow = if (inputData.getBoolean(INPUT_DATA_PRIMARY_SERVER, false)) { + applicationContext.getConnectionFactory().primaryFlow } else { - ConnectionFactory.activeUsableConnection?.connection + applicationContext.getConnectionFactory().activeFlow } + val connection = connectionFlow.first().conn?.connection val showToast = inputData.getBoolean(INPUT_DATA_SHOW_TOAST, false) val taskerIntent = inputData.getString(INPUT_DATA_TASKER_INTENT) diff --git a/mobile/src/main/java/org/openhab/habdroid/background/ItemsControlsProviderService.kt b/mobile/src/main/java/org/openhab/habdroid/background/ItemsControlsProviderService.kt index 5231803ec6..f67d200c20 100644 --- a/mobile/src/main/java/org/openhab/habdroid/background/ItemsControlsProviderService.kt +++ b/mobile/src/main/java/org/openhab/habdroid/background/ItemsControlsProviderService.kt @@ -37,11 +37,11 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.jdk9.flowPublish import kotlinx.coroutines.launch import org.openhab.habdroid.R import org.openhab.habdroid.core.connection.Connection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.model.Item import org.openhab.habdroid.model.ServerConfiguration import org.openhab.habdroid.model.toParsedState @@ -53,6 +53,7 @@ import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.ItemClient import org.openhab.habdroid.util.PendingIntent_Immutable import org.openhab.habdroid.util.PrefKeys +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getDeviceControlSubtitle import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.getPrimaryServerId @@ -62,8 +63,7 @@ import org.openhab.habdroid.util.orDefaultIfEmpty @RequiresApi(Build.VERSION_CODES.R) class ItemsControlsProviderService : ControlsProviderService() { override fun createPublisherForAllAvailable(): Flow.Publisher = flowPublish { - ConnectionFactory.waitForInitialization() - val connection = ConnectionFactory.primaryUsableConnection?.connection ?: return@flowPublish + val connection = getConnectionFactory().primaryFlow.first().conn?.connection ?: return@flowPublish val allItems = loadItems(connection) ?: return@flowPublish val factory = ItemControlFactory(this@ItemsControlsProviderService, allItems, false) allItems @@ -72,8 +72,7 @@ class ItemsControlsProviderService : ControlsProviderService() { } override fun createPublisherFor(itemNames: List): Flow.Publisher = flowPublish { - ConnectionFactory.waitForInitialization() - val connection = ConnectionFactory.primaryUsableConnection?.connection ?: return@flowPublish + val connection = getConnectionFactory().primaryFlow.first().conn?.connection ?: return@flowPublish val allItems = loadItems(connection) ?: return@flowPublish val factory = ItemControlFactory(this@ItemsControlsProviderService, allItems, true) allItems.filterKeys { itemName -> itemName in itemNames } @@ -93,8 +92,7 @@ class ItemsControlsProviderService : ControlsProviderService() { override fun performControlAction(controlId: String, action: ControlAction, consumer: Consumer) { GlobalScope.launch { - ConnectionFactory.waitForInitialization() - val connection = ConnectionFactory.primaryUsableConnection?.connection + val connection = getConnectionFactory().primaryFlow.first().conn?.connection consumer.accept(performItemControl(connection, controlId, action)) } } diff --git a/mobile/src/main/java/org/openhab/habdroid/core/NotificationHelper.kt b/mobile/src/main/java/org/openhab/habdroid/core/NotificationHelper.kt index e8e11a3175..0e0ad5366a 100644 --- a/mobile/src/main/java/org/openhab/habdroid/core/NotificationHelper.kt +++ b/mobile/src/main/java/org/openhab/habdroid/core/NotificationHelper.kt @@ -20,16 +20,16 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.graphics.Bitmap import android.os.Build import android.service.notification.StatusBarNotification import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import kotlinx.coroutines.flow.first import org.openhab.habdroid.R import org.openhab.habdroid.background.NotificationUpdateObserver -import org.openhab.habdroid.core.connection.ConnectionFactory +import org.openhab.habdroid.core.connection.Connection import org.openhab.habdroid.model.CloudMessage import org.openhab.habdroid.model.CloudNotificationId import org.openhab.habdroid.model.IconResource @@ -39,6 +39,7 @@ import org.openhab.habdroid.util.IconBackground import org.openhab.habdroid.util.ImageConversionPolicy import org.openhab.habdroid.util.PendingIntent_Immutable import org.openhab.habdroid.util.determineDataUsagePolicy +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getIconFallbackColor import org.openhab.habdroid.util.getNotificationTone import org.openhab.habdroid.util.getNotificationVibrationPattern @@ -145,7 +146,8 @@ class NotificationHelper(private val context: Context) { active.count { n -> n.id != 0 && (n.groupKey?.endsWith("gcm") == true) } private suspend fun makeNotification(message: CloudMessage.CloudNotification): Notification { - val iconBitmap = getNotificationIcon(message.icon) + val connection = context.getConnectionFactory().primaryFlow.first().conn?.connection + val iconBitmap = getNotificationIcon(connection, message.icon) val contentIntent = if (message.onClickAction == null) { makeNotificationClickIntent(message.id, message.id.notificationId) @@ -177,8 +179,7 @@ class NotificationHelper(private val context: Context) { .setPublicVersion(publicVersion) val messageImage = if (message.mediaAttachmentUrl != null) { - ConnectionFactory.waitForInitialization() - ConnectionFactory.primaryUsableConnection?.connection?.let { + connection?.let { message.loadImage(it, context, context.resources.displayMetrics.widthPixels) } } else { @@ -199,45 +200,41 @@ class NotificationHelper(private val context: Context) { return builder.build() } - private suspend fun getNotificationIcon(icon: IconResource?): Bitmap? { - val connection = ConnectionFactory.primaryCloudConnection?.connection + private suspend fun getNotificationIcon(connection: Connection?, icon: IconResource?) = when { + icon == null -> null - return when { - icon == null -> null + connection == null -> { + Log.d(TAG, "Got no connection to load icon") + null + } - connection == null -> { - Log.d(TAG, "Got no connection to load icon") - null - } + !context.determineDataUsagePolicy(connection).canDoLargeTransfers -> { + Log.d(TAG, "Don't load icon: Data usage policy doesn't allow large transfers") + null + } - !context.determineDataUsagePolicy(connection).canDoLargeTransfers -> { - Log.d(TAG, "Don't load icon: Data usage policy doesn't allow large transfers") + else -> { + Log.d(TAG, "Load icon from server") + try { + val targetSize = context.resources.getDimensionPixelSize(R.dimen.notificationlist_icon_size) + val iconUrlPath = icon.toUrl(context, true) + val bitmap = connection.httpClient + .get( + iconUrlPath, + timeoutMillis = 1000, + caching = HttpClient.CachingMode.FORCE_CACHE_IF_POSSIBLE + ) + .asBitmap( + targetSize, + context.getIconFallbackColor(IconBackground.OS_THEME), + ImageConversionPolicy.PreferTargetSize + ) + .response + bitmap + } catch (e: HttpClient.HttpException) { + Log.e(TAG, "Error getting icon", e) null } - - else -> { - Log.d(TAG, "Load icon from server") - try { - val targetSize = context.resources.getDimensionPixelSize(R.dimen.notificationlist_icon_size) - val iconUrlPath = icon.toUrl(context, true) - val bitmap = connection.httpClient - .get( - iconUrlPath, - timeoutMillis = 1000, - caching = HttpClient.CachingMode.FORCE_CACHE_IF_POSSIBLE - ) - .asBitmap( - targetSize, - context.getIconFallbackColor(IconBackground.OS_THEME), - ImageConversionPolicy.PreferTargetSize - ) - .response - bitmap - } catch (e: HttpClient.HttpException) { - Log.e(TAG, "Error getting icon", e) - null - } - } } } diff --git a/mobile/src/main/java/org/openhab/habdroid/core/OpenHabApplication.kt b/mobile/src/main/java/org/openhab/habdroid/core/OpenHabApplication.kt index 752572faae..8a89eb0143 100644 --- a/mobile/src/main/java/org/openhab/habdroid/core/OpenHabApplication.kt +++ b/mobile/src/main/java/org/openhab/habdroid/core/OpenHabApplication.kt @@ -37,6 +37,7 @@ import org.openhab.habdroid.BuildConfig import org.openhab.habdroid.R import org.openhab.habdroid.background.BackgroundTasksManager import org.openhab.habdroid.core.connection.ConnectionFactory +import org.openhab.habdroid.core.connection.ConnectionManagerHelper import org.openhab.habdroid.util.CrashReportingHelper import org.openhab.habdroid.util.getDayNightMode import org.openhab.habdroid.util.getPrefs @@ -61,6 +62,10 @@ class OpenHabApplication : MultiDexApplication() { } } + val connectionFactory: ConnectionFactory by lazy { + ConnectionFactory(this, getPrefs(), secretPrefs, ConnectionManagerHelper.create(this)) + } + var systemDataSaverStatus: Int = ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED private set var batterySaverActive: Boolean = false @@ -93,8 +98,8 @@ class OpenHabApplication : MultiDexApplication() { CrashReportingHelper.initialize(this) AppCompatDelegate.setDefaultNightMode(getPrefs().getDayNightMode(this)) - ConnectionFactory.initialize(this) BackgroundTasksManager.initialize(this) + connectionFactory.start() dataSaverChangeListener.let { listener -> registerExportedReceiver( @@ -165,7 +170,7 @@ class OpenHabApplication : MultiDexApplication() { override fun onTerminate() { super.onTerminate() - ConnectionFactory.shutdown() + connectionFactory.shutdown() } fun registerSystemDataSaverStateChangedListener(l: OnDataUsagePolicyChangedListener) { diff --git a/mobile/src/main/java/org/openhab/habdroid/core/connection/ConnectionFactory.kt b/mobile/src/main/java/org/openhab/habdroid/core/connection/ConnectionFactory.kt index 197cec6b0a..cc2df6381a 100644 --- a/mobile/src/main/java/org/openhab/habdroid/core/connection/ConnectionFactory.kt +++ b/mobile/src/main/java/org/openhab/habdroid/core/connection/ConnectionFactory.kt @@ -11,11 +11,8 @@ * SPDX-License-Identifier: EPL-2.0 */ -@file:Suppress("DEPRECATION") - package org.openhab.habdroid.core.connection -import android.app.Activity import android.app.Application import android.content.Context import android.content.SharedPreferences @@ -38,22 +35,19 @@ import javax.net.ssl.X509KeyManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.ClosedSendChannelException -import kotlinx.coroutines.channels.ConflatedBroadcastChannel -import kotlinx.coroutines.channels.onClosed +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.internal.tls.OkHostnameVerifier import okhttp3.logging.HttpLoggingInterceptor -import org.openhab.habdroid.core.CloudMessagingHelper import org.openhab.habdroid.model.ServerConfiguration import org.openhab.habdroid.util.CacheManager import org.openhab.habdroid.util.PrefKeys import org.openhab.habdroid.util.getActiveServerId -import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.getPrimaryServerId -import org.openhab.habdroid.util.getSecretPrefs import org.openhab.habdroid.util.getStringOrNull import org.openhab.habdroid.util.isDebugModeEnabled import org.openhab.habdroid.util.isDemoModeEnabled @@ -71,7 +65,8 @@ class ConnectionFactory internal constructor( private val connectionHelper: ConnectionManagerHelper ) : CoroutineScope by CoroutineScope(Dispatchers.Main), SharedPreferences.OnSharedPreferenceChangeListener { - private val trustManager: MemorizingTrustManager + + val trustManager: MemorizingTrustManager private val httpLogger: HttpLoggingInterceptor private var httpClient: OkHttpClient private var lastClientCertAlias: String? = null @@ -79,39 +74,83 @@ class ConnectionFactory internal constructor( private var primaryConn: ServerConnections? = null private var activeConn: ServerConnections? = null - private val listeners = HashSet() - private var needsUpdate: Boolean = false - - private var activeCheck: Job? = null - private var primaryCheck: Job? = null - private var activeCloudCheck: Job? = null - private var primaryCloudCheck: Job? = null + private var needsUpdate = false + private var pendingChecks = mutableListOf() + private var subscriptionCount = 0 private data class ServerConnections(val local: Connection?, val remote: AbstractConnection?) data class ConnectionResult(val connection: Connection?, val failureReason: ConnectionException?) + private data class ConnectionResultWithSource(val result: ConnectionResult, val connections: ServerConnections?) + data class CloudConnectionResult(val connection: CloudConnection?, val failureReason: Exception?) + data class ConnectionInfo( + val conn: ConnectionResult?, + val cloud: CloudConnectionResult?, + val hasLocal: Boolean, + val hasRemote: Boolean + ) + private data class StateHolder( - val primary: ConnectionResult?, - val active: ConnectionResult?, + val intermediate: Boolean, + val primary: ConnectionResultWithSource?, + val active: ConnectionResultWithSource?, val primaryCloud: CloudConnectionResult?, val activeCloud: CloudConnectionResult? - ) - - private val stateChannel = ConflatedBroadcastChannel(StateHolder(null, null, null, null)) - - interface UpdateListener { - fun onActiveConnectionChanged() - - fun onPrimaryConnectionChanged() + ) { + fun isReady(): Boolean { + if (intermediate) { + return false + } + if (active == null && primary == null && primaryCloud == null && activeCloud == null) { + return true + } + return active != null && primary != null && primaryCloud != null && activeCloud != null + } - fun onActiveCloudConnectionChanged(connection: CloudConnection?) + fun toActiveConnectionInfo() = toInfo(active, activeCloud) + fun toPrimaryConnectionInfo() = toInfo(primary, primaryCloud) - fun onPrimaryCloudConnectionChanged(connection: CloudConnection?) + private fun toInfo(conn: ConnectionResultWithSource?, cloud: CloudConnectionResult?) = ConnectionInfo( + conn?.result, + cloud, + conn?.connections?.local != null, + conn?.connections?.remote != null + ) } + private val stateFlow = MutableStateFlow(StateHolder(true, null, null, null, null)) + + /** + * Returns a {@link Flow} that emits information about the current connection to the active server + */ + val activeFlow get() = stateFlow + .filter { it.isReady() } + .map { it.toActiveConnectionInfo() } + + /** + * Like {@link activeFlow}, but for the primary instead of the active server + */ + val primaryFlow get() = stateFlow + .filter { it.isReady() } + .map { it.toPrimaryConnectionInfo() } + + /** + * Returns the current information about the connection to the active server + */ + val currentActive get() = stateFlow.value + .takeIf { it.isReady() } + ?.toActiveConnectionInfo() + + /** + * Like {@link currentActive}, but for the primary instead of the active server + */ + val currentPrimary get() = stateFlow.value + .takeIf { it.isReady() } + ?.toPrimaryConnectionInfo() + init { prefs.registerOnSharedPreferenceChangeListener(this) secretPrefs.registerOnSharedPreferenceChangeListener(this) @@ -141,36 +180,50 @@ class ConnectionFactory internal constructor( httpClient.dispatcher.maxRequestsPerHost = httpClient.dispatcher.maxRequests connectionHelper.changeCallback = { - if (listeners.isEmpty()) { + if (subscriptionCount == 0) { // We're running in background. Clear current state and postpone update for next // listener registration. - updateState(false, active = null, primary = null) + updateState(true, active = null, primary = null) needsUpdate = true } else { triggerConnectionUpdateIfNeeded() } } - } - private fun addListenerInternal(l: UpdateListener) { - if (listeners.add(l)) { - if (l is Activity) { - trustManager.bindDisplayActivity(l) - } - if (!triggerConnectionUpdateIfNeededAndPending() && activeConn?.local != null && listeners.size == 1) { - // When coming back from background, re-do connectivity check for - // local connections, as the reachability of the local server might have - // changed since we went to background - val (_, active, _, _) = stateChannel.value - val local = active?.connection === activeConn?.local || - (active?.failureReason as? NoUrlInformationException)?.wouldHaveUsedLocalConnection() == true - if (local) { - triggerConnectionUpdateIfNeeded() + launch { + stateFlow.subscriptionCount.collect { + subscriptionCount = it + if (!triggerConnectionUpdateIfNeededAndPending() && it == 1) { + // When coming back from background, re-do connectivity check for + // local connections, as the reachability of the local server might have + // changed since we went to background + val (_, _, active, _, _) = stateFlow.value + val result = active?.result + val local = result?.connection === active?.connections?.local || + (result?.failureReason as? NoUrlInformationException)?.wouldHaveUsedLocalConnection() == true + if (local) { + triggerConnectionUpdateIfNeeded() + } } } } } + fun start() { + launch { + connectionHelper.start() + updateConnections() + } + } + + fun shutdown() { + connectionHelper.shutdown() + } + + fun restartNetworkCheck() { + triggerConnectionUpdateIfNeeded() + } + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { if (key == PrefKeys.DEBUG_MESSAGES) { updateHttpLoggerSettings() @@ -193,7 +246,7 @@ class ConnectionFactory internal constructor( } @VisibleForTesting - fun updateConnections(callListenersImmediately: Boolean = false) { + fun updateConnections(updateStateImmediately: Boolean = false) { if (prefs.isDemoModeEnabled()) { if (activeConn?.local is DemoConnection) { // demo mode already was enabled @@ -202,8 +255,8 @@ class ConnectionFactory internal constructor( val conn = DemoConnection(httpClient) activeConn = ServerConnections(conn, conn) primaryConn = activeConn - val connResult = ConnectionResult(conn, null) - updateState(true, connResult, connResult, CloudConnectionResult(null, null)) + val connResult = ConnectionResultWithSource(ConnectionResult(conn, null), activeConn) + updateState(false, connResult, connResult, CloudConnectionResult(null, null)) } else { val activeServer = prefs.getActiveServerId() activeConn = loadServerConnections(activeServer) @@ -215,9 +268,9 @@ class ConnectionFactory internal constructor( loadServerConnections(primaryServer) } - updateState(callListenersImmediately, null, null, null) - triggerConnectionUpdateIfNeeded() + updateState(!updateStateImmediately, null, null, null) } + triggerConnectionUpdateIfNeeded() } private fun loadServerConnections(serverId: Int): ServerConnections? { @@ -277,38 +330,14 @@ class ConnectionFactory internal constructor( } private fun updateState( - callListenersOnChange: Boolean, - primary: ConnectionResult? = stateChannel.value.primary, - active: ConnectionResult? = stateChannel.value.active, - primaryCloud: CloudConnectionResult? = stateChannel.value.primaryCloud, - activeCloud: CloudConnectionResult? = stateChannel.value.activeCloud + isIntermediate: Boolean, + primary: ConnectionResultWithSource? = stateFlow.value.primary, + active: ConnectionResultWithSource? = stateFlow.value.active, + primaryCloud: CloudConnectionResult? = stateFlow.value.primaryCloud, + activeCloud: CloudConnectionResult? = stateFlow.value.activeCloud ) { - val prevState = stateChannel.value - val newState = StateHolder(primary, active, primaryCloud, activeCloud) - stateChannel.trySend(newState) - .onClosed { throw it ?: ClosedSendChannelException("Channel was closed normally") } - if (!callListenersOnChange) { - return - } - launch { - if (newState.active?.failureReason != null || - prevState.active?.connection !== newState.active?.connection - ) { - listeners.forEach { l -> l.onActiveConnectionChanged() } - } - if (newState.primary?.failureReason != null || - prevState.primary?.connection !== newState.primary?.connection - ) { - listeners.forEach { l -> l.onPrimaryConnectionChanged() } - } - if (prevState.activeCloud !== newState.activeCloud) { - listeners.forEach { l -> l.onActiveCloudConnectionChanged(newState.activeCloud?.connection) } - } - if (prevState.primaryCloud !== newState.primaryCloud) { - CloudMessagingHelper.onConnectionUpdated(context, newState.primaryCloud?.connection) - listeners.forEach { l -> l.onPrimaryCloudConnectionChanged(newState.primaryCloud?.connection) } - } - } + val newState = StateHolder(isIntermediate, primary, active, primaryCloud, activeCloud) + stateFlow.tryEmit(newState) } private fun triggerConnectionUpdateIfNeededAndPending(): Boolean { @@ -321,10 +350,8 @@ class ConnectionFactory internal constructor( } private fun triggerConnectionUpdateIfNeeded() { - activeCheck?.cancel() - primaryCheck?.cancel() - activeCloudCheck?.cancel() - primaryCloudCheck?.cancel() + pendingChecks.forEach { it.cancel() } + pendingChecks.clear() if (activeConn?.local is DemoConnection) { return @@ -333,52 +360,52 @@ class ConnectionFactory internal constructor( val active = activeConn val primary = primaryConn - val updateActive = { result: ConnectionResult -> + val updateActive = { result: ConnectionResultWithSource -> if (active === primary) { - updateState(true, active = result, primary = result) + updateState(false, active = result, primary = result) } else { - updateState(true, active = result) + updateState(false, active = result) } } val updateActiveCloud = { result: CloudConnectionResult -> if (active === primary) { - updateState(true, activeCloud = result, primaryCloud = result) + updateState(false, activeCloud = result, primaryCloud = result) } else { - updateState(true, activeCloud = result) + updateState(false, activeCloud = result) } } - activeCheck = launch { + pendingChecks += launch { try { val usable = withContext(Dispatchers.IO) { checkAvailableConnection(active?.local, active?.remote) } - updateActive(ConnectionResult(usable, null)) + updateActive(ConnectionResultWithSource(ConnectionResult(usable, null), active)) } catch (e: ConnectionException) { - updateActive(ConnectionResult(null, e)) + updateActive(ConnectionResultWithSource(ConnectionResult(null, e), active)) } } if (active !== primary) { - primaryCheck = launch { + pendingChecks += launch { try { val usable = withContext(Dispatchers.IO) { checkAvailableConnection(primary?.local, primary?.remote) } - updateState(true, primary = ConnectionResult(usable, null)) + updateState(false, primary = ConnectionResultWithSource(ConnectionResult(usable, null), primary)) } catch (e: ConnectionException) { - updateState(true, primary = ConnectionResult(null, e)) + updateState(false, primary = ConnectionResultWithSource(ConnectionResult(null, e), primary)) } } } - activeCloudCheck = launch { + pendingChecks += launch { try { val result = withContext(Dispatchers.IO) { active?.remote?.toCloudConnection() } updateActiveCloud(CloudConnectionResult(result, null)) - } catch (e: CancellationException) { + } catch (_: CancellationException) { // ignored } catch (e: Exception) { updateActiveCloud(CloudConnectionResult(null, e)) @@ -386,16 +413,16 @@ class ConnectionFactory internal constructor( } if (active !== primary) { - primaryCloudCheck = launch { + pendingChecks += launch { try { val result = withContext(Dispatchers.IO) { primary?.remote?.toCloudConnection() } - updateState(true, primaryCloud = CloudConnectionResult(result, null)) - } catch (e: CancellationException) { + updateState(false, primaryCloud = CloudConnectionResult(result, null)) + } catch (_: CancellationException) { // ignored } catch (e: Exception) { - updateState(true, primaryCloud = CloudConnectionResult(null, e)) + updateState(false, primaryCloud = CloudConnectionResult(null, e)) } } } @@ -562,102 +589,5 @@ class ConnectionFactory internal constructor( PrefKeys.RESTRICT_TO_SSID_PREFIX ) private val CLIENT_CERT_UPDATE_TRIGGERING_PREFIXES = listOf(PrefKeys.SSL_CLIENT_CERT_PREFIX) - - @VisibleForTesting - lateinit var instance: ConnectionFactory - - fun initialize(ctx: Application) { - instance = ConnectionFactory(ctx, ctx.getPrefs(), ctx.getSecretPrefs(), ConnectionManagerHelper.create(ctx)) - instance.launch { - instance.connectionHelper.start() - instance.updateConnections() - } - } - - @VisibleForTesting - fun initialize(ctx: Application, prefs: SharedPreferences, connectionHelper: ConnectionManagerHelper) { - instance = ConnectionFactory(ctx, prefs, prefs, connectionHelper) - } - - fun shutdown() { - instance.connectionHelper.shutdown() - } - - /** - * Wait for initialization of the factory. - * - * This method blocks until all asynchronous work (that is, determination of - * available and cloud connection) is ready, so that {@link connection} - * and {@link usableConnection} can safely be used. - */ - suspend fun waitForInitialization() { - instance.triggerConnectionUpdateIfNeededAndPending() - val sub = instance.stateChannel.openSubscription() - do { - val (primary, active, primaryCloud, activeCloud) = sub.receive() - } while (primary == null || active == null || primaryCloud == null || activeCloud == null) - } - - fun addListener(l: UpdateListener) { - instance.addListenerInternal(l) - } - - fun removeListener(l: UpdateListener) { - if (instance.listeners.remove(l) && l is Activity) { - instance.trustManager.unbindDisplayActivity(l as Activity) - } - } - - fun restartNetworkCheck() { - instance.triggerConnectionUpdateIfNeeded() - } - - /** - * Returns any openHAB connection that is most likely to work for the active server on the current network. - * The returned object will contain either a working connection, or the initialization failure cause. - * If initialization did not finish yet, null is returned. - */ - val activeUsableConnection get() = instance.stateChannel.value.active - - /** - * Returns whether the active server has a configured local connection - */ - val hasActiveLocalConnection get() = instance.activeConn?.local != null - - /** - * Returns whether the active server has a configured remote connection - */ - val hasActiveRemoteConnection get() = instance.activeConn?.remote != null - - /** - * Like {@link activeUsableConnection}, but for the primary instead of active server. - */ - val primaryUsableConnection get() = instance.stateChannel.value.primary - - /** - * Like {@link hasActiveLocalConnection}, but for the primary instead of active server. - */ - val hasPrimaryLocalConnection get() = instance.primaryConn?.local != null - - /** - * Like {@link hasActiveRemoteConnection}, but for the primary instead of active server. - */ - val hasPrimaryRemoteConnection get() = instance.primaryConn?.remote != null - - /** - * Returns the resolved cloud connection for the active server. - * The returned object will contain either - * - a working connection - * - the initialization failure cause or - * - null for both values - * (in case no remote server is configured or the remote server is not an openHAB cloud instance) - * If initialization did not finish yet, null is returned. - */ - val activeCloudConnection get() = instance.stateChannel.value.activeCloud - - /** - * Like {@link activeCloudConnection}, but for the primary instead of active server. - */ - val primaryCloudConnection get() = instance.stateChannel.value.primaryCloud } } diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/AbstractBaseActivity.kt b/mobile/src/main/java/org/openhab/habdroid/ui/AbstractBaseActivity.kt index aae715947d..53384426c9 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/AbstractBaseActivity.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/AbstractBaseActivity.kt @@ -57,6 +57,7 @@ import org.openhab.habdroid.ui.preference.PreferencesActivity import org.openhab.habdroid.util.PrefKeys import org.openhab.habdroid.util.ScreenLockMode import org.openhab.habdroid.util.applyUserSelectedTheme +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.getScreenLockMode import org.openhab.habdroid.util.hasPermissions @@ -123,9 +124,16 @@ abstract class AbstractBaseActivity : @CallSuper override fun onStart() { super.onStart() + getConnectionFactory().trustManager.bindDisplayActivity(this) promptForDevicePasswordIfRequired() } + @CallSuper + override fun onStop() { + super.onStop() + getConnectionFactory().trustManager.unbindDisplayActivity(this) + } + @CallSuper override fun onDestroy() { super.onDestroy() diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/AbstractItemPickerActivity.kt b/mobile/src/main/java/org/openhab/habdroid/ui/AbstractItemPickerActivity.kt index 08c61fd65f..e7871c4b1b 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/AbstractItemPickerActivity.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/AbstractItemPickerActivity.kt @@ -36,10 +36,10 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.openhab.habdroid.R -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.core.connection.DemoConnection import org.openhab.habdroid.databinding.ActivityItemPickerBinding import org.openhab.habdroid.databinding.BottomSheetItemPickerCommandBinding @@ -49,6 +49,7 @@ import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.ItemClient import org.openhab.habdroid.util.PrefKeys import org.openhab.habdroid.util.SuggestedCommandsFactory +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.parcelable import org.openhab.habdroid.util.parcelableArrayList @@ -201,9 +202,7 @@ abstract class AbstractItemPickerActivity : itemPickerAdapter.clear() requestJob = launch { - ConnectionFactory.waitForInitialization() - - val connection = ConnectionFactory.primaryUsableConnection?.connection + val connection = getConnectionFactory().primaryFlow.first().conn?.connection if (connection == null) { updateViewVisibility(loading = false, loadError = true, showHint = false) return@launch diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/ChartImageActivity.kt b/mobile/src/main/java/org/openhab/habdroid/ui/ChartImageActivity.kt index 0703e65eb6..f1552bbf1e 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/ChartImageActivity.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/ChartImageActivity.kt @@ -21,13 +21,13 @@ import android.view.MenuItem import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import org.openhab.habdroid.R import org.openhab.habdroid.core.connection.Connection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.ActivityChartimageBinding import org.openhab.habdroid.model.Item import org.openhab.habdroid.model.Widget import org.openhab.habdroid.util.ScreenLockMode import org.openhab.habdroid.util.determineDataUsagePolicy import org.openhab.habdroid.util.getChartTheme +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.orDefaultIfEmpty import org.openhab.habdroid.util.parcelable @@ -75,7 +75,7 @@ class ChartImageActivity : override fun onResume() { super.onResume() - connection = ConnectionFactory.activeUsableConnection?.connection + connection = getConnectionFactory().currentActive?.conn?.connection if (connection == null) { finish() return diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/ChartWidgetActivity.kt b/mobile/src/main/java/org/openhab/habdroid/ui/ChartWidgetActivity.kt index c494b6008c..7e47999111 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/ChartWidgetActivity.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/ChartWidgetActivity.kt @@ -72,7 +72,6 @@ import kotlinx.parcelize.Parcelize import org.json.JSONObject import org.openhab.habdroid.R import org.openhab.habdroid.core.connection.Connection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.ActivityChartBinding import org.openhab.habdroid.model.Item import org.openhab.habdroid.model.ParsedState @@ -82,6 +81,7 @@ import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.ItemClient import org.openhab.habdroid.util.appendQueryParameter import org.openhab.habdroid.util.determineDataUsagePolicy +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.map import org.openhab.habdroid.util.orDefaultIfEmpty import org.openhab.habdroid.util.parcelable @@ -191,7 +191,7 @@ class ChartWidgetActivity : AbstractBaseActivity() { super.onStart() val data = dataCacheFragment?.loadedData val loadExistingData = data?.let { - val dataUsagePolicy = determineDataUsagePolicy(ConnectionFactory.activeUsableConnection?.connection) + val dataUsagePolicy = determineDataUsagePolicy(getConnectionFactory().currentActive?.conn?.connection) val now = Instant.now().atZone(data.timestamp.zone) val dataIsOutdated = widget.refresh > 0 && Duration.between(data.timestamp, now).toMillis() > widget.refresh @@ -209,7 +209,7 @@ class ChartWidgetActivity : AbstractBaseActivity() { } private fun onRefresh() = lifecycleScope.launch { - val connection = ConnectionFactory.activeUsableConnection?.connection + val connection = getConnectionFactory().currentActive?.conn?.connection val item = widget.item if (connection == null || item == null) { finish() diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/CloudNotificationAdapter.kt b/mobile/src/main/java/org/openhab/habdroid/ui/CloudNotificationAdapter.kt index 05a9b8a5e7..58c45fa516 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/CloudNotificationAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/CloudNotificationAdapter.kt @@ -25,11 +25,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.NotificationlistItemBinding import org.openhab.habdroid.databinding.NotificationlistLoadingItemBinding import org.openhab.habdroid.model.CloudMessage import org.openhab.habdroid.util.determineDataUsagePolicy +import org.openhab.habdroid.util.getConnectionFactory class CloudNotificationAdapter(context: Context, private val loadMoreListener: () -> Unit) : RecyclerView.Adapter() { @@ -126,7 +126,7 @@ class CloudNotificationAdapter(context: Context, private val loadMoreListener: ( isVisible = notification.message.isNotEmpty() } - val conn = ConnectionFactory.activeCloudConnection?.connection + val conn = itemView.context.getConnectionFactory().currentActive?.cloud?.connection if (conn == null) { binding.notificationIcon.applyFallbackDrawable() binding.notificationImage.isVisible = false diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/CloudNotificationListFragment.kt b/mobile/src/main/java/org/openhab/habdroid/ui/CloudNotificationListFragment.kt index 23ac5e0441..2742f3fba8 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/CloudNotificationListFragment.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/CloudNotificationListFragment.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.launch import org.json.JSONArray import org.json.JSONException import org.openhab.habdroid.R -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.FragmentNotificationlistBinding import org.openhab.habdroid.model.CloudMessage import org.openhab.habdroid.model.ServerConfiguration @@ -36,6 +35,7 @@ import org.openhab.habdroid.model.toCloudMessage import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.getActiveServerId import org.openhab.habdroid.util.getConfiguredServerIds +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.getPrimaryServerId import org.openhab.habdroid.util.getSecretPrefs @@ -106,11 +106,12 @@ class CloudNotificationListFragment : Fragment() { private fun loadNotifications(clearExisting: Boolean) { val activity = activity as AbstractBaseActivity? ?: return - val conn = if (usePrimaryServer()) { - ConnectionFactory.primaryCloudConnection?.connection + val connInfo = if (usePrimaryServer()) { + activity.getConnectionFactory().currentPrimary } else { - ConnectionFactory.activeCloudConnection?.connection + activity.getConnectionFactory().currentActive } + val conn = connInfo?.conn?.connection if (conn == null) { updateViewVisibility(loading = false, loadError = true) return diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/ColorItemActivity.kt b/mobile/src/main/java/org/openhab/habdroid/ui/ColorItemActivity.kt index 677a520015..e76e80524b 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/ColorItemActivity.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/ColorItemActivity.kt @@ -16,10 +16,10 @@ package org.openhab.habdroid.ui import android.os.Bundle import android.view.MenuItem import org.openhab.habdroid.R -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.ActivityColorPickerBinding import org.openhab.habdroid.model.Item import org.openhab.habdroid.util.ColorPickerHelper +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.orDefaultIfEmpty import org.openhab.habdroid.util.parcelable @@ -34,7 +34,7 @@ class ColorItemActivity : AbstractBaseActivity() { supportActionBar?.title = boundItem?.label.orDefaultIfEmpty(getString(R.string.widget_type_color)) val pickerHelper = ColorPickerHelper(binding.picker, binding.brightnessSlider) - pickerHelper.attach(boundItem, this, ConnectionFactory.primaryUsableConnection?.connection) + pickerHelper.attach(boundItem, this, getConnectionFactory().currentPrimary?.conn?.connection) } override fun inflateBinding(): CommonBinding { diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/DayDream.kt b/mobile/src/main/java/org/openhab/habdroid/ui/DayDream.kt index 7d2cf65d1e..ad007f3ef8 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/DayDream.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/DayDream.kt @@ -33,14 +33,15 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.openhab.habdroid.BuildConfig import org.openhab.habdroid.R -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.DaydreamBinding import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.ItemClient import org.openhab.habdroid.util.PrefKeys +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.getStringOrNull @@ -84,8 +85,7 @@ class DayDream : } private suspend fun listenForTextItem(item: String) { - ConnectionFactory.waitForInitialization() - val connection = ConnectionFactory.primaryUsableConnection?.connection ?: return + val connection = getConnectionFactory().primaryFlow.first().conn?.connection ?: return moveText() val initialText = try { diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/ImageWidgetActivity.kt b/mobile/src/main/java/org/openhab/habdroid/ui/ImageWidgetActivity.kt index 9e6d0fa08d..d6a28d8d1e 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/ImageWidgetActivity.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/ImageWidgetActivity.kt @@ -29,13 +29,13 @@ import kotlinx.coroutines.withContext import org.json.JSONObject import org.openhab.habdroid.R import org.openhab.habdroid.core.connection.Connection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.ActivityImageBinding import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.IconBackground import org.openhab.habdroid.util.ImageConversionPolicy import org.openhab.habdroid.util.ScreenLockMode import org.openhab.habdroid.util.determineDataUsagePolicy +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getIconFallbackColor import org.openhab.habdroid.util.orDefaultIfEmpty @@ -62,7 +62,7 @@ class ImageWidgetActivity : AbstractBaseActivity() { override fun onResume() { super.onResume() - connection = ConnectionFactory.activeUsableConnection?.connection + connection = getConnectionFactory().currentActive?.conn?.connection if (connection == null) { finish() return diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/ItemPickerAdapter.kt b/mobile/src/main/java/org/openhab/habdroid/ui/ItemPickerAdapter.kt index 31196ef20e..5bc57dbb60 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/ItemPickerAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/ItemPickerAdapter.kt @@ -19,11 +19,11 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import java.util.Locale -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.ItempickerlistItemBinding import org.openhab.habdroid.model.Item import org.openhab.habdroid.model.toOH2IconResource import org.openhab.habdroid.util.determineDataUsagePolicy +import org.openhab.habdroid.util.getConnectionFactory class ItemPickerAdapter(context: Context, private val itemClickListener: ItemClickListener?) : RecyclerView.Adapter(), @@ -106,7 +106,7 @@ class ItemPickerAdapter(context: Context, private val itemClickListener: ItemCli binding.itemType.text = item.type.toString() val context = itemView.context - val connection = ConnectionFactory.primaryUsableConnection?.connection + val connection = context.getConnectionFactory().currentPrimary?.conn?.connection val icon = item.category.toOH2IconResource() if (icon != null && connection != null) { binding.itemIcon.setImageUrl( diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.kt b/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.kt index e829d2600a..3aa79f5686 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/MainActivity.kt @@ -62,6 +62,9 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar @@ -76,6 +79,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @@ -126,6 +130,7 @@ import org.openhab.habdroid.util.areSitemapsShownInDrawer import org.openhab.habdroid.util.determineDataUsagePolicy import org.openhab.habdroid.util.getActiveServerId import org.openhab.habdroid.util.getConfiguredServerIds +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getCurrentWifiSsid import org.openhab.habdroid.util.getDefaultSitemap import org.openhab.habdroid.util.getGroupItems @@ -148,9 +153,7 @@ import org.openhab.habdroid.util.registerExportedReceiver import org.openhab.habdroid.util.resolveThemedColor import org.openhab.habdroid.util.updateDefaultSitemap -class MainActivity : - AbstractBaseActivity(), - ConnectionFactory.UpdateListener { +class MainActivity : AbstractBaseActivity() { private lateinit var prefs: SharedPreferences private val onBackPressedCallback = MainOnBackPressedCallback() private var serviceResolveJob: Job? = null @@ -164,6 +167,9 @@ class MainActivity : private var sitemapSelectionDialog: AlertDialog? = null var connection: Connection? = null private set + private var lastActiveConnectionResult: ConnectionFactory.ConnectionResult? = null + private var lastPrimaryConnectionResult: ConnectionFactory.ConnectionResult? = null + private var lastPrimaryCloudConnectionResult: ConnectionFactory.CloudConnectionResult? = null private var pendingAction: PendingAction? = null private lateinit var controller: ContentController @@ -248,7 +254,7 @@ class MainActivity : serverProperties = savedInstanceState.parcelable(STATE_KEY_SERVER_PROPERTIES) val lastConnectionHash = savedInstanceState.getInt(STATE_KEY_CONNECTION_HASH) if (lastConnectionHash != -1) { - val c = ConnectionFactory.activeUsableConnection?.connection + val c = getConnectionFactory().currentActive?.conn?.connection if (c != null && c.hashCode() == lastConnectionHash) { connection = c } @@ -267,6 +273,8 @@ class MainActivity : } updateSitemapDrawerEntries() + } else { + controller.updateConnection(null, null, 0) } processIntent(intent) @@ -305,6 +313,17 @@ class MainActivity : } WindowInsetsCompat.CONSUMED } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + getConnectionFactory().activeFlow.collectLatest { info -> + if (info.conn != lastActiveConnectionResult) { + lastActiveConnectionResult = info.conn + info.conn?.let { onActiveConnectionChanged(it, info.hasLocal) } + } + } + } + } } override fun inflateBinding(): CommonBinding { @@ -324,19 +343,16 @@ class MainActivity : super.onStart() isStarted = true - ConnectionFactory.addListener(this) - window.setFlags( if (prefs.isScreenTimerDisabled()) WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON else 0, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON ) updateDrawerServerEntries() - onActiveConnectionChanged() // Make sure the connection to be used is up-to-date. There can be scenarios where the current connection // is e.g. a remote one just because the local server lookup timed out for whatever reason when we were last // started, and the user might have done changes to fix those timeouts since that time. - ConnectionFactory.restartNetworkCheck() + getConnectionFactory().restartNetworkCheck() if (connection != null && serverProperties == null) { controller.clearServerCommunicationFailure() @@ -368,7 +384,6 @@ class MainActivity : CrashReportingHelper.d(TAG, "onStop()") isStarted = false super.onStop() - ConnectionFactory.removeListener(this) serviceResolveJob?.cancel() serviceResolveJob = null if (sitemapSelectionDialog?.isShowing == true) { @@ -538,13 +553,12 @@ class MainActivity : } } - override fun onActiveConnectionChanged() { - CrashReportingHelper.d(TAG, "onActiveConnectionChanged()") - val result = ConnectionFactory.activeUsableConnection - val newConnection = result?.connection - val failureReason = result?.failureReason + private fun onActiveConnectionChanged(result: ConnectionFactory.ConnectionResult, hasLocal: Boolean) { + CrashReportingHelper.d(TAG, "onActiveConnectionChanged($result)") + val newConnection = result.connection + val failureReason = result.failureReason - if (ConnectionFactory.activeCloudConnection?.connection != null) { + if (newConnection != null) { manageNotificationShortcut(true) } @@ -578,7 +592,7 @@ class MainActivity : failureReason is NoUrlInformationException -> { // Attempt resolving only if we're connected locally and // no local connection is configured yet - if (failureReason.wouldHaveUsedLocalConnection() && !ConnectionFactory.hasActiveLocalConnection) { + if (failureReason.wouldHaveUsedLocalConnection() && hasLocal) { if (serviceResolveJob == null) { val resolver = AsyncServiceResolver( this, @@ -613,7 +627,7 @@ class MainActivity : else -> { controller.indicateNoNetwork(getString(R.string.error_network_not_available), false) scheduleRetry { - ConnectionFactory.restartNetworkCheck() + getConnectionFactory().restartNetworkCheck() recreate() } } @@ -640,17 +654,13 @@ class MainActivity : } } - override fun onPrimaryConnectionChanged() { - // no-op - } - - override fun onActiveCloudConnectionChanged(connection: CloudConnection?) { + private fun onActiveCloudConnectionChanged(connection: CloudConnection?) { CrashReportingHelper.d(TAG, "onActiveCloudConnectionChanged()") updateDrawerItemVisibility() handlePendingAction() } - override fun onPrimaryCloudConnectionChanged(connection: CloudConnection?) { + private fun onPrimaryCloudConnectionChanged(connection: CloudConnection?) { CrashReportingHelper.d(TAG, "onPrimaryCloudConnectionChanged()") handlePendingAction() launch { @@ -770,8 +780,8 @@ class MainActivity : } } } else { - val hasLocalAndRemote = - ConnectionFactory.hasActiveLocalConnection && ConnectionFactory.hasActiveRemoteConnection + val activeInfo = getConnectionFactory().currentActive + val hasLocalAndRemote = activeInfo?.hasLocal == true && activeInfo.hasRemote val type = connection?.connectionType if (hasLocalAndRemote && type == Connection.TYPE_LOCAL) { showSnackbar( @@ -1185,7 +1195,7 @@ class MainActivity : drawerMenu.setGroupVisible(R.id.options, true) val notificationsItem = drawerMenu.findItem(R.id.notifications) - notificationsItem.isVisible = ConnectionFactory.activeCloudConnection?.connection != null + notificationsItem.isVisible = getConnectionFactory().currentActive?.cloud?.connection != null val habPanelItem = drawerMenu.findItem(R.id.habpanel) habPanelItem.isVisible = serverProperties?.hasWebViewUiInstalled(WebViewUi.HABPANEL) == true && @@ -1292,9 +1302,9 @@ class MainActivity : action is PendingAction.OpenNotification && isStarted -> { val conn = if (action.primary) { - ConnectionFactory.primaryCloudConnection + getConnectionFactory().currentPrimary?.cloud } else { - ConnectionFactory.activeCloudConnection + getConnectionFactory().currentActive?.cloud } if (conn?.connection != null) { openNotifications(action.notificationId, action.primary) diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/SelectionItemActivity.kt b/mobile/src/main/java/org/openhab/habdroid/ui/SelectionItemActivity.kt index b29e8a4411..c8e5c21f89 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/SelectionItemActivity.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/SelectionItemActivity.kt @@ -22,11 +22,11 @@ import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import org.openhab.habdroid.R -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.ActivitySelectionItemBinding import org.openhab.habdroid.databinding.SelectionItemBinding import org.openhab.habdroid.model.Item import org.openhab.habdroid.model.LabeledValue +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.orDefaultIfEmpty import org.openhab.habdroid.util.parcelable @@ -96,7 +96,8 @@ class SelectionAdapter(context: Context, val item: Item) : RecyclerView.Adapter< isChecked = option.value == adapter?.itemState text = option.label setOnClickListener { - val connection = ConnectionFactory.primaryUsableConnection?.connection ?: return@setOnClickListener + val connection = context.getConnectionFactory().currentPrimary?.conn?.connection + ?: return@setOnClickListener adapter?.itemState = option.value adapter?.notifyDataSetChanged() connection.httpClient.sendItemCommand(item, option.value) diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetListFragment.kt b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetListFragment.kt index 10b78751fa..8b0c979f55 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetListFragment.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetListFragment.kt @@ -56,7 +56,6 @@ import kotlinx.coroutines.withContext import org.openhab.habdroid.R import org.openhab.habdroid.core.OpenHabApplication import org.openhab.habdroid.core.connection.Connection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.FragmentWidgetlistBinding import org.openhab.habdroid.model.LinkedPage import org.openhab.habdroid.model.Widget @@ -267,7 +266,7 @@ class WidgetListFragment : } private fun populateContextMenu(widget: Widget, menu: ContextMenu) { - val activity = (activity as AbstractBaseActivity?) ?: return + val activity = (activity as MainActivity?) ?: return val suggestedCommands = suggestedCommandsFactory.fill(widget) val nfcSupported = NfcAdapter.getDefaultAdapter(activity) != null || Util.isEmulator() val hasCommandOptions = suggestedCommands.entries.isNotEmpty() || suggestedCommands.shouldShowCustom @@ -285,12 +284,11 @@ class WidgetListFragment : Menu.NONE, R.string.analyse ).setOnMenuItemClickListener { - val mainActivity = activity as MainActivity - val intent = mainActivity.getChartDetailsActivityIntent( + val intent = activity.getChartDetailsActivityIntent( widget, - mainActivity.serverProperties + activity.serverProperties ) - mainActivity.startActivity(intent) + activity.startActivity(intent) return@setOnMenuItemClickListener true } @@ -585,9 +583,9 @@ class WidgetListFragment : } } - private fun createShortcut(activity: AbstractBaseActivity, linkedPage: LinkedPage, whiteBackground: Boolean) = + private fun createShortcut(activity: MainActivity, linkedPage: LinkedPage, whiteBackground: Boolean) = activity.launch { - val connection = ConnectionFactory.activeUsableConnection?.connection ?: return@launch + val connection = activity.connection ?: return@launch /** * Icon size is defined in {@link AdaptiveIconDrawable}. Foreground size of diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/activity/AbstractWebViewFragment.kt b/mobile/src/main/java/org/openhab/habdroid/ui/activity/AbstractWebViewFragment.kt index 7d24377f8a..610f2210ac 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/activity/AbstractWebViewFragment.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/activity/AbstractWebViewFragment.kt @@ -44,18 +44,19 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.snackbar.Snackbar import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.openhab.habdroid.R -import org.openhab.habdroid.core.connection.CloudConnection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.core.connection.DemoConnection import org.openhab.habdroid.databinding.BottomSheetShortcutLabelBinding import org.openhab.habdroid.databinding.FragmentWebviewBinding @@ -66,6 +67,7 @@ import org.openhab.habdroid.ui.MainActivity import org.openhab.habdroid.ui.setUpForConnection import org.openhab.habdroid.util.getActiveServerId import org.openhab.habdroid.util.getConfiguredServerIds +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.getSecretPrefs import org.openhab.habdroid.util.hasPermissions @@ -75,7 +77,6 @@ import org.openhab.habdroid.util.toRelativeUrl abstract class AbstractWebViewFragment : Fragment(), - ConnectionFactory.UpdateListener, CoroutineScope, MenuProvider { private val job = Job() @@ -138,13 +139,26 @@ abstract class AbstractWebViewFragment : title = context.getString(titleRes) if ( prefs.getConfiguredServerIds().size > 1 && - ConnectionFactory.activeUsableConnection?.connection !is DemoConnection + context.getConnectionFactory().currentActive?.conn?.connection !is DemoConnection ) { val activeServerName = ServerConfiguration.load(prefs, context.getSecretPrefs(), activeServerId)?.name title = getString(R.string.ui_on_server, title, activeServerName) } } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + requireContext().getConnectionFactory().activeFlow.collectLatest { info -> + if (info.conn?.connection != null) { + loadWebsite() + } + } + } + } + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) val binding = FragmentWebviewBinding.inflate(inflater, container, false) @@ -297,26 +311,6 @@ abstract class AbstractWebViewFragment : mainActivity?.showSnackbar(MainActivity.SNACKBAR_TAG_SHORTCUT_INFO, textResId, duration) } - override fun onActiveConnectionChanged() { - Log.d(TAG, "onActiveConnectionChanged()") - loadWebsite() - } - - override fun onPrimaryConnectionChanged() { - Log.d(TAG, "onPrimaryConnectionChanged()") - // no-op - } - - override fun onActiveCloudConnectionChanged(connection: CloudConnection?) { - Log.d(TAG, "onActiveCloudConnectionChanged()") - // no-op - } - - override fun onPrimaryCloudConnectionChanged(connection: CloudConnection?) { - Log.d(TAG, "onPrimaryCloudConnectionChanged()") - // no-op - } - fun goBack(): Boolean { if (webView?.canGoBack() == true) { val oldUrl = webView?.url @@ -332,7 +326,7 @@ abstract class AbstractWebViewFragment : fun canGoBack(): Boolean = webView?.canGoBack() == true private fun loadWebsite(urlToLoad: String = this.urlToLoad) { - val conn = ConnectionFactory.activeUsableConnection?.connection + val conn = requireContext().getConnectionFactory().currentActive?.conn?.connection if (conn == null) { updateViewVisibility(true, null) return diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.kt b/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.kt index 3bd12919dc..4eed79b6e5 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.kt @@ -43,7 +43,6 @@ import java.util.Stack import org.openhab.habdroid.R import org.openhab.habdroid.core.OpenHabApplication import org.openhab.habdroid.core.connection.Connection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.databinding.FragmentStatusBinding import org.openhab.habdroid.model.LinkedPage import org.openhab.habdroid.model.Sitemap @@ -58,6 +57,7 @@ import org.openhab.habdroid.ui.preference.PreferencesActivity import org.openhab.habdroid.util.CrashReportingHelper import org.openhab.habdroid.util.HttpClient import org.openhab.habdroid.util.PrefKeys +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getHumanReadableErrorMessage import org.openhab.habdroid.util.getPrefs import org.openhab.habdroid.util.getWifiManager @@ -637,7 +637,7 @@ abstract class ContentController protected constructor(private val activity: Mai internal class NoNetworkFragment : StatusFragment() { override fun onClick(view: View) { - ConnectionFactory.restartNetworkCheck() + activity?.getConnectionFactory()?.restartNetworkCheck() activity?.recreate() } @@ -720,7 +720,7 @@ abstract class ContentController protected constructor(private val activity: Mai arguments?.getBoolean(KEY_WIFI_ENABLED) == true -> { // If Wifi is enabled, primary button suggests retrying - ConnectionFactory.restartNetworkCheck() + activity?.getConnectionFactory()?.restartNetworkCheck() activity?.recreate() } diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/homescreenwidget/ItemUpdateWidget.kt b/mobile/src/main/java/org/openhab/habdroid/ui/homescreenwidget/ItemUpdateWidget.kt index 2e895f3028..f5891c3c7e 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/homescreenwidget/ItemUpdateWidget.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/homescreenwidget/ItemUpdateWidget.kt @@ -36,11 +36,11 @@ import java.io.IOException import java.io.InputStream import kotlin.math.min import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.openhab.habdroid.R import org.openhab.habdroid.background.BackgroundTasksManager -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.model.IconFormat import org.openhab.habdroid.model.IconResource import org.openhab.habdroid.model.Item @@ -55,6 +55,7 @@ import org.openhab.habdroid.util.ImageConversionPolicy import org.openhab.habdroid.util.ItemClient import org.openhab.habdroid.util.PendingIntent_Immutable import org.openhab.habdroid.util.dpToPixel +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getIconFallbackColor import org.openhab.habdroid.util.getStringOrEmpty import org.openhab.habdroid.util.getStringOrNull @@ -169,9 +170,8 @@ open class ItemUpdateWidget : AppWidgetProvider() { GlobalScope.launch { val itemState = if (data.showState) { - ConnectionFactory.waitForInitialization() try { - ConnectionFactory.primaryUsableConnection?.connection?.let { connection -> + context.getConnectionFactory().primaryFlow.first().conn?.connection?.let { connection -> val item = ItemClient.loadItem(connection, data.item) when { item?.isOfTypeOrGroupType(Item.Type.Number) == true -> item.state?.asNumber?.toString() @@ -269,8 +269,7 @@ open class ItemUpdateWidget : AppWidgetProvider() { cachedIcon.use { setIcon(it, cachedIconType == IconFormat.Svg) } } else { Log.d(TAG, "Download icon") - ConnectionFactory.waitForInitialization() - val connection = ConnectionFactory.primaryUsableConnection?.connection + val connection = context.getConnectionFactory().primaryFlow.first().conn?.connection if (connection == null) { Log.d(TAG, "Got no connection") return diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/preference/fragments/MainSettingsFragment.kt b/mobile/src/main/java/org/openhab/habdroid/ui/preference/fragments/MainSettingsFragment.kt index 68363193c3..6b96e5052b 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/preference/fragments/MainSettingsFragment.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/preference/fragments/MainSettingsFragment.kt @@ -31,19 +31,21 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.ContextCompat import androidx.core.content.edit +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.SwitchPreferenceCompat import com.google.android.material.color.DynamicColors import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.openhab.habdroid.R import org.openhab.habdroid.background.tiles.AbstractTileService import org.openhab.habdroid.background.tiles.getTileData import org.openhab.habdroid.core.CloudMessagingHelper -import org.openhab.habdroid.core.connection.CloudConnection -import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.model.ServerConfiguration import org.openhab.habdroid.model.ServerProperties import org.openhab.habdroid.ui.AbstractBaseActivity @@ -55,6 +57,7 @@ import org.openhab.habdroid.util.CacheManager import org.openhab.habdroid.util.CrashReportingHelper import org.openhab.habdroid.util.PrefKeys import org.openhab.habdroid.util.getConfiguredServerIds +import org.openhab.habdroid.util.getConnectionFactory import org.openhab.habdroid.util.getDayNightMode import org.openhab.habdroid.util.getNextAvailableServerId import org.openhab.habdroid.util.getNotificationTone @@ -67,9 +70,7 @@ import org.openhab.habdroid.util.isInstalled import org.openhab.habdroid.util.isTaskerPluginEnabled import org.openhab.habdroid.util.parcelable -class MainSettingsFragment : - AbstractSettingsFragment(), - ConnectionFactory.UpdateListener { +class MainSettingsFragment : AbstractSettingsFragment() { override val titleResId: Int @StringRes get() = R.string.action_settings private var notificationPollingPref: NotificationPollingPreference? = null @@ -87,23 +88,30 @@ class MainSettingsFragment : } } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + requireContext().getConnectionFactory().primaryFlow.collectLatest { + updateNotificationStatusSummaries() + } + } + } + } + override fun onStart() { super.onStart() updateScreenLockStateAndSummary( prefs.getStringOrFallbackIfEmpty(PrefKeys.SCREEN_LOCK, getString(R.string.settings_screen_lock_off_value)) ) populateServerPrefs() - ConnectionFactory.addListener(this) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { updateTileSummary() } } - override fun onStop() { - super.onStop() - ConnectionFactory.removeListener(this) - } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences) // Populate server prefs here, so they don't get animated. @@ -434,22 +442,6 @@ class MainSettingsFragment : } } - override fun onActiveConnectionChanged() { - // no-op - } - - override fun onPrimaryConnectionChanged() { - updateNotificationStatusSummaries() - } - - override fun onActiveCloudConnectionChanged(connection: CloudConnection?) { - // no-op - } - - override fun onPrimaryCloudConnectionChanged(connection: CloudConnection?) { - updateNotificationStatusSummaries() - } - companion object { private val TAG = MainSettingsFragment::class.java.simpleName } diff --git a/mobile/src/main/java/org/openhab/habdroid/util/ExtensionFuncs.kt b/mobile/src/main/java/org/openhab/habdroid/util/ExtensionFuncs.kt index dc9ba65b40..cd26a45163 100644 --- a/mobile/src/main/java/org/openhab/habdroid/util/ExtensionFuncs.kt +++ b/mobile/src/main/java/org/openhab/habdroid/util/ExtensionFuncs.kt @@ -91,6 +91,7 @@ import org.json.JSONObject import org.openhab.habdroid.R import org.openhab.habdroid.core.OpenHabApplication import org.openhab.habdroid.core.connection.Connection +import org.openhab.habdroid.core.connection.ConnectionFactory import org.openhab.habdroid.core.connection.DefaultConnection import org.openhab.habdroid.model.ServerConfiguration import org.openhab.habdroid.model.ServerPath @@ -351,6 +352,8 @@ fun Context.getPrefs(): SharedPreferences = PreferenceManager.getDefaultSharedPr fun Context.getSecretPrefs(): SharedPreferences = (applicationContext as OpenHabApplication).secretPrefs +fun Context.getConnectionFactory(): ConnectionFactory = (applicationContext as OpenHabApplication).connectionFactory + /** * Shows an Toast and can be called from the background. */ diff --git a/mobile/src/test/java/org/openhab/habdroid/core/connection/ConnectionFactoryTest.kt b/mobile/src/test/java/org/openhab/habdroid/core/connection/ConnectionFactoryTest.kt index e5a5e29d37..fdee72fbc2 100644 --- a/mobile/src/test/java/org/openhab/habdroid/core/connection/ConnectionFactoryTest.kt +++ b/mobile/src/test/java/org/openhab/habdroid/core/connection/ConnectionFactoryTest.kt @@ -30,6 +30,7 @@ import java.io.File import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.runBlocking @@ -85,6 +86,7 @@ class ConnectionFactoryTest { private lateinit var mockNetwork: Network private lateinit var mockNetworkCaps: NetworkCapabilities private val mockConnectionHelper = MockConnectionHelper() + private lateinit var connectionFactory: ConnectionFactory @Before @Throws(IOException::class) @@ -115,7 +117,7 @@ class ConnectionFactoryTest { mockNetwork = mock {} mockNetworkCaps = mock {} - ConnectionFactory.initialize(mockContext, mockPrefs, mockConnectionHelper) + connectionFactory = ConnectionFactory(mockContext, mockPrefs, mockPrefs, mockConnectionHelper) } @Test @@ -130,7 +132,7 @@ class ConnectionFactoryTest { updateAndWaitForConnections() assertTrue( "Should return a connection if remote url is set.", - ConnectionFactory.hasActiveRemoteConnection + connectionFactory.currentActive?.hasRemote == true ) } @@ -141,7 +143,7 @@ class ConnectionFactoryTest { updateAndWaitForConnections() assertFalse( "Should not return a remote connection if remote url isn't set.", - ConnectionFactory.hasActiveRemoteConnection + connectionFactory.currentActive?.hasRemote == true ) } @@ -152,7 +154,7 @@ class ConnectionFactoryTest { updateAndWaitForConnections() assertTrue( "Should return a local connection if local url is set.", - ConnectionFactory.hasActiveLocalConnection + connectionFactory.currentActive?.hasLocal == true ) } @@ -163,7 +165,7 @@ class ConnectionFactoryTest { updateAndWaitForConnections() assertFalse( "Should not return a local connection when local url isn't set.", - ConnectionFactory.hasActiveLocalConnection + connectionFactory.currentActive?.hasLocal == true ) } @@ -177,7 +179,7 @@ class ConnectionFactoryTest { fillInServers(remote = server.url("/").toString()) updateAndWaitForConnections() - val conn = ConnectionFactory.activeCloudConnection?.connection + val conn = connectionFactory.currentActive?.cloud?.connection assertNotNull("Should return a cloud connection if remote url is set.", conn) assertEquals(CloudConnection::class.java, conn!!.javaClass) @@ -201,7 +203,7 @@ class ConnectionFactoryTest { mockConnectionHelper.update(null) updateAndWaitForConnections() assertEquals( - ConnectionFactory.activeUsableConnection?.failureReason?.javaClass, + connectionFactory.currentActive?.conn?.failureReason?.javaClass, NetworkNotAvailableException::class.java ) } @@ -214,7 +216,7 @@ class ConnectionFactoryTest { updateAndWaitForConnections() assertEquals( "Unknown transport types should be used for remote connections", - ConnectionFactory.activeUsableConnection?.connection?.connectionType, + connectionFactory.currentActive?.conn?.connection?.connectionType, Connection.TYPE_REMOTE ) } @@ -230,7 +232,7 @@ class ConnectionFactoryTest { mockConnectionHelper.update(ConnectionManagerHelper.ConnectionType.Wifi(mockNetwork, mockNetworkCaps)) updateAndWaitForConnections() - val conn = ConnectionFactory.activeUsableConnection?.connection + val conn = connectionFactory.currentActive?.conn?.connection assertNotNull("Should return a connection in WIFI when only remote url is set.", conn) assertEquals( @@ -253,7 +255,7 @@ class ConnectionFactoryTest { mockConnectionHelper.update(ConnectionManagerHelper.ConnectionType.Wifi(mockNetwork, mockNetworkCaps)) updateAndWaitForConnections() - val conn = ConnectionFactory.activeUsableConnection?.connection + val conn = connectionFactory.currentActive?.conn?.connection assertNotNull("Should return a connection in WIFI when a local url is set.", conn) assertEquals( @@ -272,7 +274,7 @@ class ConnectionFactoryTest { mockConnectionHelper.update(ConnectionManagerHelper.ConnectionType.Wifi(mockNetwork, mockNetworkCaps)) updateAndWaitForConnections() assertEquals( - ConnectionFactory.activeUsableConnection?.failureReason?.javaClass, + connectionFactory.currentActive?.conn?.failureReason?.javaClass, NoUrlInformationException::class.java ) } @@ -306,9 +308,9 @@ class ConnectionFactoryTest { private fun updateAndWaitForConnections() { runBlocking { launch(Dispatchers.Main) { - ConnectionFactory.instance.updateConnections() + connectionFactory.updateConnections() + connectionFactory.activeFlow.first() } - ConnectionFactory.waitForInitialization() } } }