Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ jobs:

- name: Run test
# Run testFullDebugUnitTest at the app level since :automotive shares the same sourceSet than app. Minimal and Full are the same in unit tests for now
run: ./gradlew testDebugUnitTest :app:testFullDebugUnitTest :lint:test
run: ./gradlew testDebugUnitTest :app:testFullDebugUnitTest :app:testMinimalDebugUnitTest :lint:test

- name: Upload test results
if: always()
Expand Down
229 changes: 223 additions & 6 deletions app/gradle.lockfile

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,37 +0,0 @@
package io.homeassistant.companion.android.launch

import android.content.Context
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.onboarding.getMessagingToken
import javax.inject.Inject
import kotlinx.coroutines.launch
import timber.log.Timber

@ActivityScoped
class LaunchPresenterImpl @Inject constructor(@ActivityContext context: Context, serverManager: ServerManager) :
LaunchPresenterBase(context as LaunchView, serverManager) {
override fun resyncRegistration() {
if (!serverManager.isRegistered()) return
serverManager.defaultServers.forEach {
ioScope.launch {
try {
serverManager.integrationRepository(it.id).updateRegistration(
DeviceRegistration(
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
null,
getMessagingToken(),
),
)
serverManager.integrationRepository(it.id).getConfig() // Update cached data
serverManager.webSocketRepository(it.id).getCurrentUser() // Update cached data
} catch (e: Exception) {
Timber.e(e, "Issue updating Registration")
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package io.homeassistant.companion.android.notifications
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
import io.homeassistant.companion.android.common.data.servers.ServerManager
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -14,22 +12,16 @@ import timber.log.Timber

@AndroidEntryPoint
class FirebaseCloudMessagingService : FirebaseMessagingService() {
companion object {
private const val SOURCE = "FCM"
}

@Inject
lateinit var serverManager: ServerManager

@Inject
lateinit var messagingManager: MessagingManager
lateinit var pushProvider: FirebasePushProvider

private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())

override fun onMessageReceived(remoteMessage: RemoteMessage) {
Timber.d("From: ${remoteMessage.from}")

messagingManager.handleMessage(remoteMessage.data, SOURCE)
pushProvider.onMessage(this, remoteMessage.data)
}

/**
Expand All @@ -39,23 +31,8 @@ class FirebaseCloudMessagingService : FirebaseMessagingService() {
*/
override fun onNewToken(token: String) {
mainScope.launch {
Timber.d("Refreshed token: $token")
if (!serverManager.isRegistered()) {
Timber.d("Not trying to update registration since we aren't authenticated.")
return@launch
}
serverManager.defaultServers.forEach {
launch {
try {
serverManager.integrationRepository(it.id).updateRegistration(
deviceRegistration = DeviceRegistration(pushToken = token),
allowReregistration = false,
)
} catch (e: Exception) {
Timber.e(e, "Issue updating token")
}
}
}
pushProvider.setToken(token)
pushProvider.updateRegistration(this@FirebaseCloudMessagingService)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.homeassistant.companion.android.notifications

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import io.homeassistant.companion.android.common.notifications.PushProvider
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class FirebasePushModule {
@Binds
@Singleton
@IntoMap
@ClassKey(FirebasePushProvider::class)
abstract fun bindFirebasePushProvider(firebasePushProvider: FirebasePushProvider): PushProvider
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.homeassistant.companion.android.notifications

import android.content.Context
import com.google.firebase.messaging.FirebaseMessaging
import io.homeassistant.companion.android.common.BuildConfig
import io.homeassistant.companion.android.common.notifications.PushProvider
import javax.inject.Inject
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.tasks.await
import timber.log.Timber

class FirebasePushProvider @Inject constructor(
private val messagingManager: MessagingManager
) : PushProvider {

companion object {
const val SOURCE = "FCM"
}

override fun isAvailable(context: Context): Boolean = true

override suspend fun getDistributors(): List<String> = emptyList()

override suspend fun getDistributor(): String? = null

private var token: String? = null
private val tokenMutex = Mutex()

suspend fun setToken(token: String) = tokenMutex.withLock {
this.token = token
}

override suspend fun getUrl(): String = BuildConfig.PUSH_URL

override suspend fun getToken(): String {
return tokenMutex.withLock { token } ?: try {
FirebaseMessaging.getInstance().token.await()
} catch (e: Exception) {
Timber.e(e, "Issue getting token")
""
}
}

override fun onMessage(context: Context, notificationData: Map<String, String>) {
messagingManager.handleMessage(notificationData, SOURCE)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.homeassistant.companion.android.notifications

import io.homeassistant.companion.android.common.notifications.PushProvider
import javax.inject.Inject

class PushManagerImpl @Inject constructor(
providers: Map<Class<*>, @JvmSuppressWildcards PushProvider>
) : PushManagerBase(providers) {
override val defaultProvider: PushProvider?
get() = providers[FirebasePushProvider::class.java]!!
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import io.homeassistant.companion.android.database.server.ServerSessionInfo
import io.homeassistant.companion.android.database.server.ServerType
import io.homeassistant.companion.android.database.server.ServerUserInfo
import io.homeassistant.companion.android.database.settings.WebsocketSetting
import io.homeassistant.companion.android.notifications.PushManager
import io.homeassistant.companion.android.onboarding.OnboardApp
import io.homeassistant.companion.android.onboarding.getMessagingToken
import io.homeassistant.companion.android.sensors.LocationSensorManager
import io.homeassistant.companion.android.settings.SettingViewModel
import io.homeassistant.companion.android.settings.server.ServerChooserFragment
Expand Down Expand Up @@ -67,6 +67,9 @@ class LaunchActivity :
@Inject
lateinit var sensorDao: SensorDao

@Inject
lateinit var pushManager: PushManager

private val mainScope = CoroutineScope(Dispatchers.Main + Job())

private val settingViewModel: SettingViewModel by viewModels()
Expand Down Expand Up @@ -151,7 +154,9 @@ class LaunchActivity :
mainScope.launch {
if (result != null) {
val (url, authCode, deviceName, deviceTrackingEnabled, notificationsEnabled) = result
val messagingToken = getMessagingToken()
val pushProvider = pushManager.defaultProvider
val messagingToken = pushProvider?.getToken().orEmpty()
val pushUrl = pushProvider?.getUrl().orEmpty()
if (messagingToken.isBlank() && BuildConfig.FLAVOR == "full") {
AlertDialog.Builder(this@LaunchActivity)
.setTitle(commonR.string.firebase_error_title)
Expand All @@ -163,6 +168,7 @@ class LaunchActivity :
authCode,
deviceName,
messagingToken,
pushUrl,
deviceTrackingEnabled,
notificationsEnabled,
)
Expand All @@ -175,6 +181,7 @@ class LaunchActivity :
authCode,
deviceName,
messagingToken,
pushUrl,
deviceTrackingEnabled,
notificationsEnabled,
)
Expand All @@ -190,6 +197,7 @@ class LaunchActivity :
authCode: String,
deviceName: String,
messagingToken: String,
pushUrl: String,
deviceTrackingEnabled: Boolean,
notificationsEnabled: Boolean,
) {
Expand All @@ -209,9 +217,10 @@ class LaunchActivity :
serverManager.authenticationRepository(serverId).registerAuthorizationCode(authCode)
serverManager.integrationRepository(serverId).registerDevice(
DeviceRegistration(
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
deviceName,
messagingToken,
appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
deviceName = deviceName,
pushToken = messagingToken,
pushUrl = pushUrl,
),
)
serverId = serverManager.convertTemporaryServer(serverId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
package io.homeassistant.companion.android.launch

import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.common.data.authentication.SessionState
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.notifications.PushManager
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber

abstract class LaunchPresenterBase(private val view: LaunchView, internal val serverManager: ServerManager) :
LaunchPresenter {
class LaunchPresenterImpl @Inject constructor(
private val view: LaunchView,
private val serverManager: ServerManager,
private val pushManager: PushManager
) : LaunchPresenter {

internal val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
internal val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)

override fun onViewReady(serverUrlToOnboard: String?) {
mainScope.launch {
Expand Down Expand Up @@ -52,5 +60,21 @@ abstract class LaunchPresenterBase(private val view: LaunchView, internal val se
}

// TODO: This should probably go in settings?
internal abstract fun resyncRegistration()
private fun resyncRegistration() {
if (!serverManager.isRegistered()) return
serverManager.defaultServers.forEach {
ioScope.launch {
try {
serverManager.integrationRepository(it.id).updateRegistration(
DeviceRegistration(
appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
pushToken = pushManager.getToken()
)
)
} catch (e: Exception) {
Timber.e(e, "Issue updating Registration")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.homeassistant.companion.android.notifications

import android.content.Context
import androidx.annotation.VisibleForTesting
import io.homeassistant.companion.android.common.notifications.PushProvider

/**
* This interface manages push notification providers.
*
* **Key Responsibilities:**
* - Provide list of available providers.
* - Provide a default provider to use.
* - Enable the selected provider.
* - Get the push URL and token for the selected provider.
*/
interface PushManager {
val defaultProvider: PushProvider?

/**
* Get the push provider of the given class.
*
* @return If a provider of the given class exists, this method returns the [PushProvider],
* otherwise it returns `null`
*/
fun getProvider(clazz: Class<*>): PushProvider?

/**
* Get the push provider of the given id.
*
* @return If a provider of the given id exists, this method returns the [PushProvider],
* otherwise it returns `null`
*/
fun getProvider(id: String): PushProvider?

/**
* Get the push provider that is enabled for the given server.
*
* @return If the server exists and a provider is enabled for it, this method returns
* the [PushProvider], otherwise it returns `null`
*/
fun getProviderForServer(context: Context, serverId: Int): PushProvider?

/**
* Get the push token for the default push provider.
*
* @return The default provider's token, or `null` if there is no default provider.
*/
suspend fun getToken(): String?

// Needs to be visible for testing to assert the content of the map.
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
val providers: Map<Class<*>, @JvmSuppressWildcards PushProvider>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.homeassistant.companion.android.notifications

import android.content.Context
import io.homeassistant.companion.android.common.notifications.PushProvider
import io.homeassistant.companion.android.common.notifications.id

abstract class PushManagerBase(
override val providers: Map<Class<*>, @JvmSuppressWildcards PushProvider>
) : PushManager {
override fun getProvider(clazz: Class<*>): PushProvider? = providers[clazz]

override fun getProvider(id: String): PushProvider? =
providers.values.find { it.id() == id }

override fun getProviderForServer(context: Context, serverId: Int): PushProvider? =
providers.values.find { it.isEnabled(context, serverId) }

override suspend fun getToken(): String? {
return defaultProvider?.getToken()
}
}
Loading
Loading