Skip to content

Commit

Permalink
Initial WebSocket Implementation (#1884)
Browse files Browse the repository at this point in the history
* Initial setup of WebSockets!

* Got some good sets of table tennis going.

* Move to a more kotlin friendly way to lock.

* Functional get config call.

* Remove testing function.

* Linting.

* Migrate get config calls to websockets!

* Working retries.

* Get services now as websocket request.

* Remove unused service call via api.

* Fix issue with widget not prompting the correct items.

* Migrate to websocket get states.

* ktlint.

* Review Comments.
  • Loading branch information
JBassett authored Nov 10, 2021
1 parent 86c1279 commit ba8345a
Show file tree
Hide file tree
Showing 26 changed files with 434 additions and 229 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.homeassistant.companion.android.common.data.integration.IntegrationRep
import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitResponse
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.common.data.url.UrlRepository
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository
import io.homeassistant.companion.android.settings.language.LanguagesManager
import io.homeassistant.companion.android.themes.ThemesManager
import kotlinx.coroutines.CoroutineScope
Expand All @@ -26,7 +27,8 @@ class SettingsPresenterImpl @Inject constructor(
private val authenticationUseCase: AuthenticationRepository,
private val prefsRepository: PrefsRepository,
private val themesManager: ThemesManager,
private val langsManager: LanguagesManager
private val langsManager: LanguagesManager,
private val webSocketRepository: WebSocketRepository
) : SettingsPresenter, PreferenceDataStore() {

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,41 +175,27 @@ class WebViewPresenterImpl @Inject constructor(
}

override suspend fun getStatusBarAndNavigationBarColor(webViewColor: String): Int = withContext(Dispatchers.IO) {
var statusbarNavBarColor = 0
var statusBarNavBarColor = 0

Log.d(TAG, "Try getting status bar/navigation bar color from webviews color \"$webViewColor\"")
if (!webViewColor.isNullOrEmpty() && webViewColor != "null" && webViewColor.length >= 2) {
val trimmedColorString = webViewColor.substring(1, webViewColor.length - 1).trim()
Log.d(TAG, "Color from webview is \"$trimmedColorString\"")
try {
statusbarNavBarColor = parseColorWithRgb(trimmedColorString)
Log.i(TAG, "Found color $statusbarNavBarColor for status bar/navigation bar")
statusBarNavBarColor = parseColorWithRgb(trimmedColorString)
Log.i(TAG, "Found color $statusBarNavBarColor for status bar/navigation bar")
} catch (e: Exception) {
Log.w(TAG, "Could not get status bar/navigation bar color from webview. Try getting status bar/navigation bar color from HA", e)
}
} else {
Log.w(TAG, "Could not get status bar/navigation bar color from webview. Color \"$webViewColor\" is not a valid color. Try getting status bar/navigation bar color from HA")
}

if (statusbarNavBarColor == 0) {
Log.d(TAG, "Try getting status bar/navigation bar color from HA")
runBlocking {
try {
val colorString = integrationUseCase.getThemeColor()
Log.d(TAG, "Color from HA is \"$colorString\"")
if (!colorString.isNullOrEmpty()) {
statusbarNavBarColor = parseColorWithRgb(colorString)
Log.i(TAG, "Found color $statusbarNavBarColor for status bar/navigation bar")
} else {
Log.e(TAG, "Could not get status bar/navigation bar color from HA. No theme color defined in theme variable \"app-header-background-color\"")
}
} catch (e: Exception) {
Log.e(TAG, "Could not get status bar/navigation bar color from HA.", e)
}
}
if (statusBarNavBarColor == 0) {
Log.w(TAG, "Couldn't get color for status bar.")
}

return@withContext statusbarNavBarColor
return@withContext statusBarNavBarColor
}

private fun parseColorWithRgb(colorString: String): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ class CameraWidgetConfigureActivity : BaseActivity() {
try {
// Fetch entities
val fetchedEntities = integrationUseCase.getEntities()
fetchedEntities.sortBy { e -> e.entityId }
fetchedEntities.forEach {
val entityId = it.entityId
val domain = entityId.split(".")[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ class EntityWidgetConfigureActivity : BaseActivity() {
}
val entityAdapter = SingleItemArrayAdapter<Entity<Any>>(this) { it?.entityId ?: "" }

binding.widgetTextConfigAttribute.setAdapter(entityAdapter)
binding.widgetTextConfigAttribute.onFocusChangeListener = dropDownOnFocus
binding.widgetTextConfigAttribute.onItemClickListener = entityDropDownOnItemClick
binding.widgetTextConfigEntityId.setAdapter(entityAdapter)
binding.widgetTextConfigEntityId.onFocusChangeListener = dropDownOnFocus
binding.widgetTextConfigEntityId.onItemClickListener = entityDropDownOnItemClick
binding.widgetTextConfigAttribute.onFocusChangeListener = dropDownOnFocus
binding.widgetTextConfigAttribute.onItemClickListener = attributeDropDownOnItemClick
binding.widgetTextConfigAttribute.setOnClickListener {
Expand All @@ -140,7 +140,6 @@ class EntityWidgetConfigureActivity : BaseActivity() {
try {
// Fetch entities
val fetchedEntities = integrationUseCase.getEntities()
fetchedEntities.sortBy { e -> e.entityId }
fetchedEntities.forEach {
entities[it.entityId] = it
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseActivity() {
try {
// Fetch entities
val fetchedEntities = integrationUseCase.getEntities()
fetchedEntities.sortBy { e -> e.entityId }
fetchedEntities.forEach {
val entityId = it.entityId
val domain = entityId.split(".")[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.homeassistant.companion.android.common.data.authentication.Authenticat
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.common.data.url.UrlRepository
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository

@Component(modules = [DataModule::class])
interface AppComponent {
Expand All @@ -16,4 +17,6 @@ interface AppComponent {
fun integrationUseCase(): IntegrationRepository

fun prefsUseCase(): PrefsRepository

fun webSocketRepository(): WebSocketRepository
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.homeassistant.companion.android.common.data.authentication.Authenticat
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.common.data.url.UrlRepository
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository

@Component(modules = [DataModule::class])
interface DataComponent {
Expand All @@ -16,4 +17,6 @@ interface DataComponent {
fun integrationRepository(): IntegrationRepository

fun prefsRepository(): PrefsRepository

fun webSocketRepository(): WebSocketRepository
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import android.os.Build
import dagger.Binds
import dagger.Module
import dagger.Provides
import io.homeassistant.companion.android.common.data.HomeAssistantRetrofit
import io.homeassistant.companion.android.common.data.HomeAssistantApis
import io.homeassistant.companion.android.common.data.LocalStorage
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
import io.homeassistant.companion.android.common.data.authentication.impl.AuthenticationRepositoryImpl
Expand All @@ -16,7 +16,10 @@ import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.common.data.prefs.PrefsRepositoryImpl
import io.homeassistant.companion.android.common.data.url.UrlRepository
import io.homeassistant.companion.android.common.data.url.UrlRepositoryImpl
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository
import io.homeassistant.companion.android.common.data.websocket.impl.WebSocketRepositoryImpl
import io.homeassistant.companion.android.common.data.wifi.WifiHelper
import okhttp3.OkHttpClient
import javax.inject.Named

@Module(includes = [DataModule.Declaration::class])
Expand All @@ -30,12 +33,16 @@ class DataModule(
) {

@Provides
fun provideAuthenticationService(homeAssistantRetrofit: HomeAssistantRetrofit): AuthenticationService =
homeAssistantRetrofit.retrofit.create(AuthenticationService::class.java)
fun provideAuthenticationService(homeAssistantApis: HomeAssistantApis): AuthenticationService =
homeAssistantApis.retrofit.create(AuthenticationService::class.java)

@Provides
fun providesIntegrationService(homeAssistantRetrofit: HomeAssistantRetrofit): IntegrationService =
homeAssistantRetrofit.retrofit.create(IntegrationService::class.java)
fun providesIntegrationService(homeAssistantApis: HomeAssistantApis): IntegrationService =
homeAssistantApis.retrofit.create(IntegrationService::class.java)

@Provides
fun providesOkHttpClient(homeAssistantApis: HomeAssistantApis): OkHttpClient =
homeAssistantApis.okHttpClient

@Provides
fun providesWifiHelper() = wifiHelper
Expand Down Expand Up @@ -85,5 +92,8 @@ class DataModule(

@Binds
fun bindPrefsRepositoryImpl(repository: PrefsRepositoryImpl): PrefsRepository

@Binds
fun bindWebSocketRepositoryImpl(repository: WebSocketRepositoryImpl): WebSocketRepository
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.homeassistant.companion.android.common.data

import android.os.Build
import android.webkit.CookieManager
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import io.homeassistant.companion.android.common.BuildConfig
import io.homeassistant.companion.android.common.data.url.UrlRepository
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Inject

class HomeAssistantApis @Inject constructor(private val urlRepository: UrlRepository) {
companion object {
private const val LOCAL_HOST = "http://localhost/"
private const val USER_AGENT = "User-Agent"
private const val USER_AGENT_STRING = "HomeAssistant/Android"

private val CALL_TIMEOUT = 30L
private val READ_TIMEOUT = 30L
}

private fun configureOkHttpClient(builder: OkHttpClient.Builder): OkHttpClient.Builder {
if (BuildConfig.DEBUG) {
builder.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
}
builder.addInterceptor {
return@addInterceptor if (it.request().url.toString().contains(LOCAL_HOST)) {
val newRequest = runBlocking {
it.request().newBuilder()
.url(
it.request().url.toString()
.replace(LOCAL_HOST, urlRepository.getUrl().toString())
)
.header(
USER_AGENT,
"$USER_AGENT_STRING ${Build.MODEL} ${BuildConfig.VERSION_NAME}"
)
.build()
}
it.proceed(newRequest)
} else {
it.proceed(it.request())
}
}
// Only deal with cookies when on non wear device and for now I don't have a better
// way to determine if we are really on wear os....
// TODO: Please fix me.
var cookieManager: CookieManager? = null
try {
cookieManager = CookieManager.getInstance()
} catch (e: Exception) {
// Noop
}
if (cookieManager != null) {
builder.cookieJar(CookieJarCookieManagerShim())
}
builder.callTimeout(CALL_TIMEOUT, TimeUnit.SECONDS)
builder.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)

return builder
}

val retrofit: Retrofit = Retrofit
.Builder()
.addConverterFactory(
JacksonConverterFactory.create(
ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.registerKotlinModule()
)
)
.client(configureOkHttpClient(OkHttpClient.Builder()).build())
.baseUrl(LOCAL_HOST)
.build()

val okHttpClient = configureOkHttpClient(OkHttpClient.Builder()).build()
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface AuthenticationRepository {

suspend fun retrieveExternalAuthentication(forceRefresh: Boolean): String

suspend fun retrieveAccessToken(): String

suspend fun revokeSession()

suspend fun getSessionState(): SessionState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ class AuthenticationRepositoryImpl @Inject constructor(
return convertSession(ensureValidSession(forceRefresh))
}

override suspend fun retrieveAccessToken(): String {
return ensureValidSession(false).accessToken
}

override suspend fun revokeSession() {
val session = retrieveSession() ?: throw AuthorizationException()
authenticationService.revokeToken(session.refreshToken, AuthenticationService.REVOKE_ACTION)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,11 @@ interface IntegrationRepository {
suspend fun setWearHomeFavorites(favorites: Set<String>)
suspend fun getWearHomeFavorites(): Set<String>

suspend fun getThemeColor(): String

suspend fun getHomeAssistantVersion(): String

suspend fun getServices(): Array<Service>
suspend fun getServices(): List<Service>

suspend fun getEntities(): Array<Entity<Any>>
suspend fun getEntities(): List<Entity<Any>>
suspend fun getEntity(entityId: String): Entity<Map<String, Any>>

suspend fun callService(domain: String, service: String, serviceData: HashMap<String, Any>)
Expand Down
Loading

0 comments on commit ba8345a

Please sign in to comment.