Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ name: Unit tests

on:
push:
branches:
- 'main'
branches: [ "main" ]
paths-ignore:
- '**.md'
- '**.yml'
pull_request:
types: [ opened, reopened, ready_for_review ]
branches: [ "main" ]
workflow_dispatch:

jobs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@ import com.looker.droidify.network.ProgressListener
import com.looker.droidify.network.header.HeadersBuilder
import com.looker.droidify.network.validation.FileValidator
import com.looker.droidify.sync.common.assets
import java.io.File
import java.io.InputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream
import java.net.Proxy

val FakeDownloader = object : Downloader {
override fun setProxy(proxy: Proxy) {
TODO("Not yet implemented")
}

override suspend fun headCall(
url: String,
Expand Down
42 changes: 9 additions & 33 deletions app/src/main/kotlin/com/looker/droidify/Droidify.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.intercept.Interceptor
import coil3.memory.MemoryCache
import coil3.network.ktor3.KtorNetworkFetcherFactory
import coil3.request.ImageResult
import coil3.request.SuccessResult
import coil3.request.crossfade
Expand All @@ -26,8 +27,6 @@ import com.looker.droidify.database.Database
import com.looker.droidify.datastore.SettingsRepository
import com.looker.droidify.datastore.get
import com.looker.droidify.datastore.model.AutoSync
import com.looker.droidify.datastore.model.ProxyPreference
import com.looker.droidify.datastore.model.ProxyType
import com.looker.droidify.index.RepositoryUpdater
import com.looker.droidify.installer.InstallManager
import com.looker.droidify.network.Downloader
Expand All @@ -41,12 +40,10 @@ import com.looker.droidify.utility.common.cache.Cache
import com.looker.droidify.utility.common.extension.getDrawableCompat
import com.looker.droidify.utility.common.extension.getInstalledPackagesCompat
import com.looker.droidify.utility.common.extension.jobScheduler
import com.looker.droidify.utility.common.log
import com.looker.droidify.utility.extension.toInstalledItem
import com.looker.droidify.work.CleanUpWorker
import dagger.hilt.android.HiltAndroidApp
import java.net.InetSocketAddress
import java.net.Proxy
import io.ktor.client.HttpClient
import javax.inject.Inject
import kotlin.time.Duration.Companion.INFINITE
import kotlin.time.Duration.Companion.hours
Expand All @@ -73,6 +70,9 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
@Inject
lateinit var downloader: Downloader

@Inject
lateinit var httpClient: HttpClient

@Inject
lateinit var workerFactory: HiltWorkerFactory

Expand Down Expand Up @@ -147,36 +147,9 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
}
}
}
launch {
settingsRepository.get { proxy }.collect(::updateProxy)
}
}
}

private fun updateProxy(proxyPreference: ProxyPreference) {
val type = proxyPreference.type
val host = proxyPreference.host
val port = proxyPreference.port
val socketAddress = when (type) {
ProxyType.DIRECT -> null
ProxyType.HTTP, ProxyType.SOCKS -> {
try {
InetSocketAddress.createUnresolved(host, port)
} catch (e: IllegalArgumentException) {
log(e)
null
}
}
}
val androidProxyType = when (type) {
ProxyType.DIRECT -> Proxy.Type.DIRECT
ProxyType.HTTP -> Proxy.Type.HTTP
ProxyType.SOCKS -> Proxy.Type.SOCKS
}
val determinedProxy = socketAddress?.let { Proxy(androidProxyType, it) } ?: Proxy.NO_PROXY
downloader.setProxy(determinedProxy)
}

private fun updateSyncJob(force: Boolean, autoSync: AutoSync) {
if (autoSync == AutoSync.NEVER) {
jobScheduler?.cancel(Constants.JOB_ID_SYNC)
Expand Down Expand Up @@ -243,7 +216,10 @@ class Droidify : Application(), SingletonImageLoader.Factory, Configuration.Prov
.diskCache(diskCache)
.error(getDrawableCompat(R.drawable.ic_cannot_load).asImage())
.crossfade(350)
.components { add(FallbackIconInterceptor()) }
.components {
add(KtorNetworkFetcherFactory(httpClient = { httpClient }))
add(FallbackIconInterceptor())
}
.build()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -69,18 +68,10 @@ fun SettingsScreen(
val customButtons by viewModel.customButtons.collectAsStateWithLifecycle()
val isBackgroundAllowed by viewModel.isBackgroundAllowed.collectAsStateWithLifecycle()

val snackbarHostState = remember { SnackbarHostState() }

LaunchedEffect(Unit) {
viewModel.updateBackgroundAccessState(context.isIgnoreBatteryEnabled())
}

LaunchedEffect(Unit) {
viewModel.snackbarMessage.collect { messageRes ->
snackbarHostState.showSnackbar(context.getString(messageRes))
}
}

val exportSettingsLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(BACKUP_MIME_TYPE),
) { uri -> uri?.let { viewModel.exportSettings(it) } }
Expand Down Expand Up @@ -132,7 +123,7 @@ fun SettingsScreen(
navigationIcon = { BackButton(onBackClick) },
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
snackbarHost = { SnackbarHost(viewModel.snackbarHostState) },
) { contentPadding ->
LazyColumn(
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import android.content.Context
import android.net.Uri
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.material3.SnackbarHostState
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.looker.droidify.BuildConfig
import com.looker.droidify.R
import com.looker.droidify.data.StringHandler
import com.looker.droidify.database.Database
import com.looker.droidify.database.RepositoryExporter
import com.looker.droidify.datastore.CustomButtonRepository
Expand All @@ -35,20 +37,21 @@ import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

@HiltViewModel
class SettingsViewModel @Inject constructor(
private val settingsRepository: SettingsRepository,
private val repositoryExporter: RepositoryExporter,
private val customButtonRepository: CustomButtonRepository,
private val handler: StringHandler,
) : ViewModel() {

val snackbarHostState = SnackbarHostState()

val settings = settingsRepository.data.asStateFlow(Settings())

val customButtons: StateFlow<List<CustomButton>> = customButtonRepository.buttons
Expand All @@ -57,16 +60,13 @@ class SettingsViewModel @Inject constructor(
private val _isBackgroundAllowed = MutableStateFlow(true)
val isBackgroundAllowed = _isBackgroundAllowed.asStateFlow()

private val _snackbarMessage = Channel<Int>(Channel.BUFFERED)
val snackbarMessage = _snackbarMessage.receiveAsFlow()

fun updateBackgroundAccessState(allowed: Boolean) {
_isBackgroundAllowed.value = allowed
}

fun showSnackbar(@StringRes messageRes: Int) {
viewModelScope.launch {
_snackbarMessage.send(messageRes)
snackbarHostState.showSnackbar(handler.getString(messageRes))
}
}

Expand Down Expand Up @@ -191,21 +191,25 @@ class SettingsViewModel @Inject constructor(
fun setProxyType(proxyType: ProxyType) {
viewModelScope.launch {
settingsRepository.setProxyType(proxyType)
showSnackbar(R.string.proxy_restart_required)
}
}

fun setProxyHost(host: String) {
viewModelScope.launch {
settingsRepository.setProxyHost(host)
showSnackbar(R.string.proxy_restart_required)
}
}

fun setProxyPort(port: String) {
viewModelScope.launch {
try {
settingsRepository.setProxyPort(port.toInt())
} catch (_: NumberFormatException) {
val portInt = port.toIntOrNull()
if (portInt == null) {
showSnackbar(R.string.proxy_port_error_not_int)
} else {
settingsRepository.setProxyPort(portInt)
showSnackbar(R.string.proxy_restart_required)
}
}
}
Expand Down
51 changes: 50 additions & 1 deletion app/src/main/kotlin/com/looker/droidify/di/NetworkModule.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,75 @@
package com.looker.droidify.di

import com.looker.droidify.BuildConfig.BUILD_TYPE
import com.looker.droidify.BuildConfig.VERSION_NAME
import com.looker.droidify.datastore.SettingsRepository
import com.looker.droidify.datastore.model.ProxyPreference
import com.looker.droidify.datastore.model.ProxyType
import com.looker.droidify.network.Downloader
import com.looker.droidify.network.KtorDownloader
import com.looker.droidify.utility.common.log
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.UserAgent
import java.net.InetSocketAddress
import java.net.Proxy
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.runBlocking

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Singleton
@Provides
fun provideHttpClient(settingsRepository: SettingsRepository): HttpClient {
val proxyPreference = runBlocking { settingsRepository.getInitial().proxy }
val engine = OkHttp.create { proxy = proxyPreference.toProxy() }
return HttpClient(engine) {
install(UserAgent) {
agent = "Droid-ify/${VERSION_NAME}-${BUILD_TYPE}"
}
install(HttpTimeout) {
connectTimeoutMillis = 30_000L
socketTimeoutMillis = 15_000L
}
}
}

@Singleton
@Provides
fun provideDownloader(
httpClient: HttpClient,
@IoDispatcher
dispatcher: CoroutineDispatcher,
): Downloader = KtorDownloader(
httpClientEngine = OkHttp.create(),
client = httpClient,
dispatcher = dispatcher,
)
}

private fun ProxyPreference.toProxy(): Proxy {
val socketAddress = when (type) {
ProxyType.DIRECT -> null
ProxyType.HTTP, ProxyType.SOCKS -> {
try {
InetSocketAddress.createUnresolved(host, port)
} catch (e: IllegalArgumentException) {
log(e)
null
}
}
}
val androidProxyType = when (type) {
ProxyType.DIRECT -> Proxy.Type.DIRECT
ProxyType.HTTP -> Proxy.Type.HTTP
ProxyType.SOCKS -> Proxy.Type.SOCKS
}
return socketAddress?.let { Proxy(androidProxyType, it) } ?: Proxy.NO_PROXY
}
7 changes: 2 additions & 5 deletions app/src/main/kotlin/com/looker/droidify/network/Downloader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,20 @@ package com.looker.droidify.network
import com.looker.droidify.network.header.HeadersBuilder
import com.looker.droidify.network.validation.FileValidator
import java.io.File
import java.net.Proxy

interface Downloader {

fun setProxy(proxy: Proxy)

suspend fun headCall(
url: String,
headers: HeadersBuilder.() -> Unit = {}
headers: HeadersBuilder.() -> Unit = {},
): NetworkResponse

suspend fun downloadToFile(
url: String,
target: File,
validator: FileValidator? = null,
headers: HeadersBuilder.() -> Unit = {},
block: ProgressListener? = null
block: ProgressListener? = null,
): NetworkResponse
}

Expand Down
Loading