Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
Expand All @@ -32,6 +33,7 @@ import androidx.media3.common.Player
import androidx.media3.datasource.DataSource
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.util.SdkVersion
import io.homeassistant.companion.android.common.util.di.SuspendProvider
import io.homeassistant.companion.android.common.util.initializePlayer
import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme
import io.homeassistant.companion.android.util.compose.media.player.HAMediaPlayer
Expand All @@ -47,19 +49,26 @@ import kotlinx.coroutines.launch
@AndroidEntryPoint
class DemoExoPlayerActivity : AppCompatActivity() {
@Inject
lateinit var dataSourceFactory: DataSource.Factory
lateinit var dataSourceFactoryProvider: SuspendProvider<DataSource.Factory>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
val dataSourceFactory by produceState<DataSource.Factory?>(initialValue = null, dataSourceFactoryProvider) {
value = dataSourceFactoryProvider()
}
HomeAssistantAppTheme {
Box(modifier = Modifier.fillMaxSize()) {
HAMediaPlayer(
"https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8",
dataSourceFactory = dataSourceFactory,
modifier = Modifier.size(width = 428.dp, height = 192.dp).align(Alignment.Center),
)
dataSourceFactory?.let {
HAMediaPlayer(
"https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8",
dataSourceFactory = it,
modifier = Modifier
.size(width = 428.dp, height = 192.dp)
.align(Alignment.Center),
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.homeassistant.companion.android.common.data.integration.impl.entities.
import io.homeassistant.companion.android.common.data.integration.impl.entities.Template
import io.homeassistant.companion.android.common.data.servers.tryOnUrls
import io.homeassistant.companion.android.common.util.FailFast
import io.homeassistant.companion.android.common.util.di.SuspendProvider
import javax.inject.Inject
import kotlinx.coroutines.CancellationException
import kotlinx.serialization.json.JsonPrimitive
Expand Down Expand Up @@ -62,8 +63,8 @@ data class WearServer(
* credentials without requiring a persisted server in the app's database.
*/
class SettingsWearRepository @Inject constructor(
private val authenticationService: AuthenticationService,
private val integrationService: IntegrationService,
private val authenticationServiceProvider: SuspendProvider<AuthenticationService>,
private val integrationServiceProvider: SuspendProvider<IntegrationService>,
) {

/**
Expand All @@ -77,7 +78,7 @@ class SettingsWearRepository @Inject constructor(
*/
suspend fun registerRefreshToken(server: WearServer, refreshToken: String): WearServer {
return tryOnUrls(server.getBaseUrls(), "refresh_token") {
val response = authenticationService.refreshToken(
val response = authenticationServiceProvider().refreshToken(
it.newBuilder().addPathSegments(SEGMENT_AUTH_TOKEN).build(),
AuthenticationService.GRANT_TYPE_REFRESH,
refreshToken,
Expand Down Expand Up @@ -108,7 +109,7 @@ class SettingsWearRepository @Inject constructor(
wearServer.getWebhookUrls(),
"render_template",
) { url ->
integrationService.getTemplate(
integrationServiceProvider().getTemplate(
url,
RenderTemplateIntegrationRequest(
mapOf("template" to Template(template, emptyMap())),
Expand Down Expand Up @@ -136,7 +137,7 @@ class SettingsWearRepository @Inject constructor(

return try {
tryOnUrls(wearServer.getBaseUrls(), "get_entities") { url ->
integrationService.getStates(
integrationServiceProvider().getStates(
url.newBuilder().addPathSegments("api/states").build(),
"Bearer ${wearServer.accessToken}",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import android.webkit.WebView
import androidx.core.content.ContextCompat
import androidx.webkit.WebViewCompat
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import dagger.hilt.android.HiltAndroidApp
Expand All @@ -27,6 +26,7 @@ import io.homeassistant.companion.android.common.sensors.LastUpdateManager
import io.homeassistant.companion.android.common.util.HAStrictMode
import io.homeassistant.companion.android.common.util.SdkVersion
import io.homeassistant.companion.android.common.util.configureComposeDiagnosticStackTrace
import io.homeassistant.companion.android.common.util.di.SuspendProvider
import io.homeassistant.companion.android.common.util.isAutomotive
import io.homeassistant.companion.android.database.sensor.SensorDao
import io.homeassistant.companion.android.database.settings.SensorUpdateFrequencySetting
Expand Down Expand Up @@ -55,9 +55,7 @@ import okhttp3.OkHttpClient
import timber.log.Timber

@HiltAndroidApp
open class HomeAssistantApplication :
Application(),
SingletonImageLoader.Factory {
open class HomeAssistantApplication : Application() {

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

Expand All @@ -69,7 +67,7 @@ open class HomeAssistantApplication :
lateinit var keyChainRepository: KeyChainRepository

@Inject
lateinit var okHttpClient: OkHttpClient
lateinit var okHttpClientProvider: SuspendProvider<OkHttpClient>

@Inject
lateinit var languagesManager: LanguagesManager
Expand Down Expand Up @@ -108,6 +106,19 @@ open class HomeAssistantApplication :
prefsRepository.isCrashReporting(),
)
initCrashSaving(applicationContext)
val okHttpClient = okHttpClientProvider()

SingletonImageLoader.setSafe {
ImageLoader.Builder(this@HomeAssistantApplication)
.components {
add(
OkHttpNetworkFetcherFactory(
callFactory = okHttpClient,
),
)
}
.build()
}

configureWebViewDebugging(enabled = BuildConfig.DEBUG || prefsRepository.isWebViewDebugEnabled())

Expand Down Expand Up @@ -370,16 +381,6 @@ open class HomeAssistantApplication :
}
}

override fun newImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(context)
.components {
add(
OkHttpNetworkFetcherFactory(
callFactory = okHttpClient,
),
)
}
.build()

/**
* Enables WebView contents debugging and logs the current WebView package.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.media3.common.VideoSize
import androidx.media3.datasource.DataSource
import androidx.media3.exoplayer.ExoPlayer
import dagger.hilt.android.qualifiers.ApplicationContext
import io.homeassistant.companion.android.common.util.di.SuspendProvider
import io.homeassistant.companion.android.common.util.initializePlayer
import io.homeassistant.companion.android.frontend.handler.FrontendHandlerEvent
import java.io.Closeable
Expand All @@ -35,9 +36,12 @@ class FrontendExoPlayerManager @VisibleForTesting constructor(
) : Closeable {

@Inject
constructor(@ApplicationContext context: Context, dataSourceFactory: DataSource.Factory) : this(
constructor(
@ApplicationContext context: Context,
dataSourceFactoryProvider: SuspendProvider<DataSource.Factory>,
) : this(
{ configure ->
initializePlayer(context, dataSourceFactory).apply(configure)
initializePlayer(context, dataSourceFactoryProvider()).apply(configure)
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import io.homeassistant.companion.android.common.notifications.prepareText
import io.homeassistant.companion.android.common.util.SdkVersion
import io.homeassistant.companion.android.common.util.cancelGroupIfNeeded
import io.homeassistant.companion.android.common.util.createSystemAppSettingsIntent
import io.homeassistant.companion.android.common.util.di.SuspendProvider
import io.homeassistant.companion.android.common.util.getActiveNotification
import io.homeassistant.companion.android.common.util.isAutomotive
import io.homeassistant.companion.android.common.util.kotlinJsonMapper
Expand Down Expand Up @@ -118,7 +119,7 @@ import timber.log.Timber

class MessagingManager @Inject constructor(
@ApplicationContext val context: Context,
private val okHttpClient: OkHttpClient,
private val okHttpClientProvider: SuspendProvider<OkHttpClient>,
private val serverManager: ServerManager,
private val prefsRepository: PrefsRepository,
private val notificationDao: NotificationDao,
Expand Down Expand Up @@ -1393,9 +1394,9 @@ class MessagingManager @Inject constructor(
}
}.build()

val response = okHttpClient.newCall(request).execute()
image = BitmapFactory.decodeStream(response.body?.byteStream())
response.close()
okHttpClientProvider().newCall(request).execute().use {
image = BitmapFactory.decodeStream(it.body.byteStream())
}
} catch (e: Exception) {
Timber.e(e, "Couldn't download image for notification")
}
Expand Down Expand Up @@ -1429,11 +1430,10 @@ class MessagingManager @Inject constructor(
}
}.build()

val response = okHttpClient.newCall(request).execute()
val bytes = response.body?.bytes() ?: return@withContext null
file.writeBytes(bytes)

response.close()
okHttpClientProvider().newCall(request).execute().use {
val bytes = it.body.bytes()
file.writeBytes(bytes)
}
} catch (e: Exception) {
Timber.e(e, "Couldn't download image for notification")
}
Expand Down Expand Up @@ -1506,15 +1506,15 @@ class MessagingManager @Inject constructor(
)
}
}.build()
val response = okHttpClient.newCall(request).execute()

if (!videoFile.exists()) {
videoFile.parentFile?.mkdirs()
videoFile.createNewFile()
}
response.body.source().use { source ->
videoFile.sink().use { sink ->
source.readAll(sink)
okHttpClientProvider().newCall(request).execute().use {
it.body.source().use { source ->
videoFile.sink().use { sink ->
source.readAll(sink)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.os.strictmode.DiskWriteViolation
import android.os.strictmode.IncorrectContextUseViolation
import android.os.strictmode.Violation
import androidx.annotation.RequiresApi
import io.homeassistant.companion.android.common.data.HomeAssistantApis
import io.homeassistant.companion.android.common.util.IgnoreViolationRule

val vmPolicyIgnoredViolationRules = listOf(
Expand All @@ -29,7 +28,6 @@ val threadPolicyIgnoredViolationRules = listOf(
IgnoreMiuiTurboSchedMonitorDiskRead,
IgnoreChromiumKeyStoreDiskWrite,
IgnoreAppCompatPersistLocalesDiskReadWrite,
IgnoreConfigureOkHttpClientDiskRead,
)

/**
Expand Down Expand Up @@ -329,21 +327,3 @@ private data object IgnoreChromiumKeyStoreDiskWrite : IgnoreViolationRule {
}
}
}

/**
* Ignores a [DiskReadViolation] raised while [HomeAssistantApis] configures its OkHttpClient:
* building the TLSHelper reads CA keystores from disk on some devices.
*
* Solved by https://github.com/home-assistant/android/pull/7042
*/
private data object IgnoreConfigureOkHttpClientDiskRead : IgnoreViolationRule {
@RequiresApi(Build.VERSION_CODES.P)
override fun shouldIgnore(violation: Violation): Boolean {
if (violation !is DiskReadViolation) return false

return violation.stackTrace.any {
it.className == HomeAssistantApis::class.java.name &&
it.methodName == "configureOkHttpClient"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import io.homeassistant.companion.android.common.util.FailFast
import io.homeassistant.companion.android.common.util.GestureAction
import io.homeassistant.companion.android.common.util.GestureDirection
import io.homeassistant.companion.android.common.util.SdkVersion
import io.homeassistant.companion.android.common.util.di.SuspendProvider
import io.homeassistant.companion.android.common.util.getBooleanOrElse
import io.homeassistant.companion.android.common.util.getBooleanOrNull
import io.homeassistant.companion.android.common.util.getDoubleOrElse
Expand Down Expand Up @@ -283,7 +284,7 @@ class WebViewActivity :
lateinit var checkLocationDisabled: CheckLocationDisabledUseCase

@Inject
lateinit var dataSourceFactory: DataSource.Factory
lateinit var dataSourceFactoryProvider: SuspendProvider<DataSource.Factory>

private lateinit var webView: WebView
private var loadedUrl: Uri? = null
Expand Down Expand Up @@ -1424,7 +1425,7 @@ class WebViewActivity :
val uri = payload?.getStringOrNull("url")?.toUri() ?: return
val isMuted = payload.getBooleanOrElse("muted", false)
lifecycleScope.launch {
exoPlayer.value = initializePlayer(this@WebViewActivity, dataSourceFactory).apply {
exoPlayer.value = initializePlayer(this@WebViewActivity, dataSourceFactoryProvider()).apply {
setMediaItem(MediaItem.fromUri(uri))
playWhenReady = true
addListener(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import timber.log.Timber

@AndroidEntryPoint
Expand All @@ -57,9 +56,6 @@ class CameraWidget : AppWidgetProvider() {
@Inject
lateinit var cameraWidgetDao: CameraWidgetDao

@Inject
lateinit var okHttpClient: OkHttpClient

override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
// There may be multiple widgets active, so update all of them
appWidgetIds.forEach { appWidgetId ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.homeassistant.companion.android.onboarding

import android.content.Context
import android.content.pm.PackageManager
import android.provider.Settings
import androidx.activity.compose.LocalActivityResultRegistryOwner
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.ActivityResultRegistryOwner
Expand All @@ -13,6 +15,7 @@ import androidx.navigation.NavController
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.compose.NavHost
import androidx.navigation.testing.TestNavHostController
import androidx.test.core.app.ApplicationProvider
import dagger.hilt.android.testing.HiltAndroidRule
import io.homeassistant.companion.android.HiltComponentActivity
import io.homeassistant.companion.android.util.FakePermissionResultRegistry
Expand All @@ -26,6 +29,10 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule

// Robolectric leaves Settings.Secure.ANDROID_ID null, but the integration graph injects it as a
// non-null @NamedDeviceId, so we seed a value to avoid a null-from-@Provides crash during DI.
private const val FAKE_ANDROID_ID = "robolectric-android-id"

/**
* Base class for onboarding navigation tests providing shared setup, mocks, and utilities.
*
Expand All @@ -46,6 +53,11 @@ internal abstract class BaseOnboardingNavigationTest {

@Before
fun baseSetup() {
Settings.Secure.putString(
ApplicationProvider.getApplicationContext<Context>().contentResolver,
Settings.Secure.ANDROID_ID,
FAKE_ANDROID_ID,
)
mockkStatic(NavController::navigateToUri)
coEvery { any<NavController>().navigateToUri(any(), any()) } just Runs
}
Expand Down
Loading
Loading