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.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -26,14 +28,14 @@ interface HuggingFaceRemoteDataSource {
|
|||
direction: String? = "-1",
|
||||
limit: Int? = SEARCH_RESULT_LIMIT,
|
||||
full: Boolean = true,
|
||||
): List<HuggingFaceModel>
|
||||
): Result<List<HuggingFaceModel>>
|
||||
|
||||
suspend fun getModelDetails(modelId: String): HuggingFaceModelDetails
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
@ -57,14 +59,23 @@ class HuggingFaceRemoteDataSourceImpl @Inject constructor(
|
|||
limit: Int?,
|
||||
full: Boolean,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
apiService.getModels(
|
||||
search = query,
|
||||
filter = filter,
|
||||
sort = sort,
|
||||
direction = direction,
|
||||
limit = limit,
|
||||
full = full,
|
||||
).filter { it.gated != true && it.private != true && it.getGgufFilename() != null }
|
||||
try {
|
||||
apiService.getModels(
|
||||
search = query,
|
||||
filter = filter,
|
||||
sort = sort,
|
||||
direction = direction,
|
||||
limit = limit,
|
||||
full = full,
|
||||
).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(
|
||||
|
|
@ -76,18 +87,26 @@ class HuggingFaceRemoteDataSourceImpl @Inject constructor(
|
|||
override suspend fun getFileSize(
|
||||
modelId: String,
|
||||
filePath: String
|
||||
): Long? = withContext(Dispatchers.IO) {
|
||||
): Result<Long> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
apiService.getModelFileHeader(modelId, filePath).let {
|
||||
if (it.isSuccessful) {
|
||||
it.headers()[HTTP_HEADER_CONTENT_LENGTH]?.toLongOrNull()
|
||||
apiService.getModelFileHeader(modelId, filePath).let { resp ->
|
||||
if (resp.isSuccessful) {
|
||||
resp.headers()[HTTP_HEADER_CONTENT_LENGTH]?.toLongOrNull()?.let {
|
||||
Result.success(it)
|
||||
} ?: Result.failure(IOException("Content-Length header missing"))
|
||||
} 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) {
|
||||
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
|
||||
*/
|
||||
suspend fun searchHuggingFaceModels(limit: Int = 20): List<HuggingFaceModel>
|
||||
suspend fun searchHuggingFaceModels(limit: Int = 20): Result<List<HuggingFaceModel>>
|
||||
|
||||
/**
|
||||
* Obtain the model details from HuggingFace
|
||||
|
|
@ -96,7 +96,7 @@ interface ModelRepository {
|
|||
/**
|
||||
* 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
|
||||
|
|
@ -351,7 +351,7 @@ class ModelRepositoryImpl @Inject constructor(
|
|||
|
||||
override suspend fun searchHuggingFaceModels(
|
||||
limit: Int
|
||||
): List<HuggingFaceModel> = withContext(Dispatchers.IO) {
|
||||
) = withContext(Dispatchers.IO) {
|
||||
huggingFaceRemoteDataSource.searchModels(limit = limit)
|
||||
}
|
||||
|
||||
|
|
@ -363,7 +363,7 @@ class ModelRepositoryImpl @Inject constructor(
|
|||
|
||||
override suspend fun getHuggingFaceModelFileSize(
|
||||
downloadInfo: HuggingFaceDownloadInfo,
|
||||
): Long? = withContext(Dispatchers.IO) {
|
||||
): Result<Long> = withContext(Dispatchers.IO) {
|
||||
huggingFaceRemoteDataSource.getFileSize(downloadInfo.modelId, downloadInfo.filename)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -246,11 +249,23 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
huggingFaceQueryJob = viewModelScope.launch {
|
||||
_managementState.emit(Download.Querying)
|
||||
try {
|
||||
val models = modelRepository.searchHuggingFaceModels()
|
||||
Log.d(TAG, "Fetched ${models.size} models from HuggingFace:")
|
||||
_managementState.emit(Download.Ready(models))
|
||||
modelRepository.searchHuggingFaceModels().fold(
|
||||
onSuccess = { models ->
|
||||
Log.d(TAG, "Fetched ${models.size} models from HuggingFace:")
|
||||
_managementState.emit(Download.Ready(models))
|
||||
},
|
||||
onFailure = { throw it }
|
||||
)
|
||||
} catch (_: CancellationException) {
|
||||
// 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) {
|
||||
_managementState.emit(Download.Error(message = e.message ?: "Unknown error"))
|
||||
}
|
||||
|
|
@ -267,17 +282,24 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
val downloadInfo = model.toDownloadInfo()
|
||||
requireNotNull(downloadInfo) { "Download URL is missing!" }
|
||||
|
||||
val actualSize = modelRepository.getHuggingFaceModelFileSize(downloadInfo)
|
||||
requireNotNull(actualSize) { "Unknown model file size!" }
|
||||
Log.d(TAG, "Model file size: ${formatFileByteSize(actualSize)}")
|
||||
|
||||
modelRepository.downloadHuggingFaceModel(downloadInfo, actualSize)
|
||||
.onSuccess { downloadId ->
|
||||
activeDownloads[downloadId] = model
|
||||
_managementState.value = Download.Dispatched(downloadInfo)
|
||||
}
|
||||
.onFailure { throw it }
|
||||
|
||||
modelRepository.getHuggingFaceModelFileSize(downloadInfo).fold(
|
||||
onSuccess = { actualSize ->
|
||||
Log.d(TAG, "Model file size: ${formatFileByteSize(actualSize)}")
|
||||
modelRepository.downloadHuggingFaceModel(downloadInfo, actualSize)
|
||||
.onSuccess { downloadId ->
|
||||
activeDownloads[downloadId] = model
|
||||
_managementState.value = Download.Dispatched(downloadInfo)
|
||||
}
|
||||
.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) {
|
||||
_managementState.value = Download.Error(
|
||||
message = e.message ?: "Insufficient storage space to download ${model.modelId}",
|
||||
|
|
@ -343,7 +365,6 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
companion object {
|
||||
private val TAG = ModelsManagementViewModel::class.java.simpleName
|
||||
|
||||
private const val SUBSCRIPTION_TIMEOUT_MS = 5000L
|
||||
private const val SUCCESS_RESET_TIMEOUT_MS = 1000L
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue