data: handle network exceptions elegantly
This commit is contained in:
parent
85434e6580
commit
7c2e24b4fe
|
|
@ -7,6 +7,8 @@ import android.util.Log
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -26,14 +28,14 @@ interface HuggingFaceRemoteDataSource {
|
||||||
direction: String? = "-1",
|
direction: String? = "-1",
|
||||||
limit: Int? = SEARCH_RESULT_LIMIT,
|
limit: Int? = SEARCH_RESULT_LIMIT,
|
||||||
full: Boolean = true,
|
full: Boolean = true,
|
||||||
): List<HuggingFaceModel>
|
): Result<List<HuggingFaceModel>>
|
||||||
|
|
||||||
suspend fun getModelDetails(modelId: String): HuggingFaceModelDetails
|
suspend fun getModelDetails(modelId: String): HuggingFaceModelDetails
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain selected HuggingFace model's GGUF file size from HTTP header
|
* Obtain selected HuggingFace model's GGUF file size from HTTP header
|
||||||
*/
|
*/
|
||||||
suspend fun getFileSize(modelId: String, filePath: String): Long?
|
suspend fun getFileSize(modelId: String, filePath: String): Result<Long>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download selected HuggingFace model's GGUF file via DownloadManager
|
* Download selected HuggingFace model's GGUF file via DownloadManager
|
||||||
|
|
@ -57,6 +59,7 @@ class HuggingFaceRemoteDataSourceImpl @Inject constructor(
|
||||||
limit: Int?,
|
limit: Int?,
|
||||||
full: Boolean,
|
full: Boolean,
|
||||||
) = withContext(Dispatchers.IO) {
|
) = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
apiService.getModels(
|
apiService.getModels(
|
||||||
search = query,
|
search = query,
|
||||||
filter = filter,
|
filter = filter,
|
||||||
|
|
@ -64,7 +67,15 @@ class HuggingFaceRemoteDataSourceImpl @Inject constructor(
|
||||||
direction = direction,
|
direction = direction,
|
||||||
limit = limit,
|
limit = limit,
|
||||||
full = full,
|
full = full,
|
||||||
).filter { it.gated != true && it.private != true && it.getGgufFilename() != null }
|
).filter {
|
||||||
|
it.gated != true && it.private != true && it.getGgufFilename() != null
|
||||||
|
}.let {
|
||||||
|
if (it.isEmpty()) Result.failure(FileNotFoundException()) else Result.success(it)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error searching for models on HuggingFace: ${e.message}")
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getModelDetails(
|
override suspend fun getModelDetails(
|
||||||
|
|
@ -76,18 +87,26 @@ class HuggingFaceRemoteDataSourceImpl @Inject constructor(
|
||||||
override suspend fun getFileSize(
|
override suspend fun getFileSize(
|
||||||
modelId: String,
|
modelId: String,
|
||||||
filePath: String
|
filePath: String
|
||||||
): Long? = withContext(Dispatchers.IO) {
|
): Result<Long> = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
apiService.getModelFileHeader(modelId, filePath).let {
|
apiService.getModelFileHeader(modelId, filePath).let { resp ->
|
||||||
if (it.isSuccessful) {
|
if (resp.isSuccessful) {
|
||||||
it.headers()[HTTP_HEADER_CONTENT_LENGTH]?.toLongOrNull()
|
resp.headers()[HTTP_HEADER_CONTENT_LENGTH]?.toLongOrNull()?.let {
|
||||||
|
Result.success(it)
|
||||||
|
} ?: Result.failure(IOException("Content-Length header missing"))
|
||||||
} else {
|
} else {
|
||||||
null
|
Result.failure(
|
||||||
|
when (resp.code()) {
|
||||||
|
401 -> SecurityException("Model requires authentication")
|
||||||
|
404 -> FileNotFoundException("Model file not found")
|
||||||
|
else -> IOException("Failed to get file info: HTTP ${resp.code()}")
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error getting file size for $modelId: ${e.message}")
|
Log.e(TAG, "Error getting file size for $modelId: ${e.message}")
|
||||||
null
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ interface ModelRepository {
|
||||||
/**
|
/**
|
||||||
* Search models on HuggingFace
|
* Search models on HuggingFace
|
||||||
*/
|
*/
|
||||||
suspend fun searchHuggingFaceModels(limit: Int = 20): List<HuggingFaceModel>
|
suspend fun searchHuggingFaceModels(limit: Int = 20): Result<List<HuggingFaceModel>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain the model details from HuggingFace
|
* Obtain the model details from HuggingFace
|
||||||
|
|
@ -96,7 +96,7 @@ interface ModelRepository {
|
||||||
/**
|
/**
|
||||||
* Obtain the model's size from HTTP response header
|
* Obtain the model's size from HTTP response header
|
||||||
*/
|
*/
|
||||||
suspend fun getHuggingFaceModelFileSize(downloadInfo: HuggingFaceDownloadInfo): Long?
|
suspend fun getHuggingFaceModelFileSize(downloadInfo: HuggingFaceDownloadInfo): Result<Long>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a HuggingFace model via system download manager
|
* Download a HuggingFace model via system download manager
|
||||||
|
|
@ -351,7 +351,7 @@ class ModelRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
override suspend fun searchHuggingFaceModels(
|
override suspend fun searchHuggingFaceModels(
|
||||||
limit: Int
|
limit: Int
|
||||||
): List<HuggingFaceModel> = withContext(Dispatchers.IO) {
|
) = withContext(Dispatchers.IO) {
|
||||||
huggingFaceRemoteDataSource.searchModels(limit = limit)
|
huggingFaceRemoteDataSource.searchModels(limit = limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,7 +363,7 @@ class ModelRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
override suspend fun getHuggingFaceModelFileSize(
|
override suspend fun getHuggingFaceModelFileSize(
|
||||||
downloadInfo: HuggingFaceDownloadInfo,
|
downloadInfo: HuggingFaceDownloadInfo,
|
||||||
): Long? = withContext(Dispatchers.IO) {
|
): Result<Long> = withContext(Dispatchers.IO) {
|
||||||
huggingFaceRemoteDataSource.getFileSize(downloadInfo.modelId, downloadInfo.filename)
|
huggingFaceRemoteDataSource.getFileSize(downloadInfo.modelId, downloadInfo.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.net.UnknownHostException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -246,11 +249,23 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
huggingFaceQueryJob = viewModelScope.launch {
|
huggingFaceQueryJob = viewModelScope.launch {
|
||||||
_managementState.emit(Download.Querying)
|
_managementState.emit(Download.Querying)
|
||||||
try {
|
try {
|
||||||
val models = modelRepository.searchHuggingFaceModels()
|
modelRepository.searchHuggingFaceModels().fold(
|
||||||
|
onSuccess = { models ->
|
||||||
Log.d(TAG, "Fetched ${models.size} models from HuggingFace:")
|
Log.d(TAG, "Fetched ${models.size} models from HuggingFace:")
|
||||||
_managementState.emit(Download.Ready(models))
|
_managementState.emit(Download.Ready(models))
|
||||||
|
},
|
||||||
|
onFailure = { throw it }
|
||||||
|
)
|
||||||
} catch (_: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
// no-op
|
// no-op
|
||||||
|
} catch (_: UnknownHostException) {
|
||||||
|
_managementState.value = Download.Error(message = "No internet connection")
|
||||||
|
} catch (_: SocketTimeoutException) {
|
||||||
|
_managementState.value = Download.Error(message = "Connection timed out")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
_managementState.value = Download.Error(message = "Network error: ${e.message}")
|
||||||
|
} catch (_: FileNotFoundException) {
|
||||||
|
_managementState.emit(Download.Error(message = "No eligible models"))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_managementState.emit(Download.Error(message = e.message ?: "Unknown error"))
|
_managementState.emit(Download.Error(message = e.message ?: "Unknown error"))
|
||||||
}
|
}
|
||||||
|
|
@ -267,17 +282,24 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
val downloadInfo = model.toDownloadInfo()
|
val downloadInfo = model.toDownloadInfo()
|
||||||
requireNotNull(downloadInfo) { "Download URL is missing!" }
|
requireNotNull(downloadInfo) { "Download URL is missing!" }
|
||||||
|
|
||||||
val actualSize = modelRepository.getHuggingFaceModelFileSize(downloadInfo)
|
modelRepository.getHuggingFaceModelFileSize(downloadInfo).fold(
|
||||||
requireNotNull(actualSize) { "Unknown model file size!" }
|
onSuccess = { actualSize ->
|
||||||
Log.d(TAG, "Model file size: ${formatFileByteSize(actualSize)}")
|
Log.d(TAG, "Model file size: ${formatFileByteSize(actualSize)}")
|
||||||
|
|
||||||
modelRepository.downloadHuggingFaceModel(downloadInfo, actualSize)
|
modelRepository.downloadHuggingFaceModel(downloadInfo, actualSize)
|
||||||
.onSuccess { downloadId ->
|
.onSuccess { downloadId ->
|
||||||
activeDownloads[downloadId] = model
|
activeDownloads[downloadId] = model
|
||||||
_managementState.value = Download.Dispatched(downloadInfo)
|
_managementState.value = Download.Dispatched(downloadInfo)
|
||||||
}
|
}
|
||||||
.onFailure { throw it }
|
.onFailure { throw it }
|
||||||
|
},
|
||||||
|
onFailure = { throw it }
|
||||||
|
)
|
||||||
|
} catch (_: UnknownHostException) {
|
||||||
|
_managementState.value = Download.Error(message = "No internet connection")
|
||||||
|
} catch (_: SocketTimeoutException) {
|
||||||
|
_managementState.value = Download.Error(message = "Connection timed out")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
_managementState.value = Download.Error(message = "Network error: ${e.message}")
|
||||||
} catch (e: InsufficientStorageException) {
|
} catch (e: InsufficientStorageException) {
|
||||||
_managementState.value = Download.Error(
|
_managementState.value = Download.Error(
|
||||||
message = e.message ?: "Insufficient storage space to download ${model.modelId}",
|
message = e.message ?: "Insufficient storage space to download ${model.modelId}",
|
||||||
|
|
@ -343,7 +365,6 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = ModelsManagementViewModel::class.java.simpleName
|
private val TAG = ModelsManagementViewModel::class.java.simpleName
|
||||||
|
|
||||||
private const val SUBSCRIPTION_TIMEOUT_MS = 5000L
|
|
||||||
private const val SUCCESS_RESET_TIMEOUT_MS = 1000L
|
private const val SUCCESS_RESET_TIMEOUT_MS = 1000L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue