Compare commits

...

3 commits

Author SHA1 Message Date
3ddbe78fe8
nice and clean now
I don't know if this works
2024-08-07 13:37:42 +02:00
5cf1466c54
refactor the api 2024-08-07 12:55:17 +02:00
ebddf95465
merge callbacks 2024-08-07 11:34:42 +02:00
24 changed files with 328 additions and 330 deletions

View file

@ -44,17 +44,16 @@ class DashboardActivity : ComponentActivity() {
val dashboardViewModel = DashboardViewModel(application) val dashboardViewModel = DashboardViewModel(application)
if (intent.getBooleanExtra("direct", false).not()) { if (intent.getBooleanExtra("direct", false).not()) {
dashboardViewModel.refresh(this) dashboardViewModel.refresh()
} }
dashboardViewModel.checkTermux(this)
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
dashboardViewModel.refreshError.collect { dashboardViewModel.refreshError.collect {
it.forEach { errorMsg -> Toast.makeText(baseContext, it, Toast.LENGTH_SHORT).show()
Toast.makeText(baseContext, errorMsg, Toast.LENGTH_SHORT).show() } // TODO any better way?
}
}
} }
} }
@ -113,9 +112,9 @@ fun MyNavigationBar(items: List<Screen>, navController: NavHostController) {
saveState = true saveState = true
} }
// Avoid multiple copies of the same destination when // Avoid multiple copies of the same destination when
// reselecting the same item // re-selecting the same item
launchSingleTop = true launchSingleTop = true
// Restore state when reselecting a previously selected item // Restore state when re-selecting a previously selected item
restoreState = true restoreState = true
} }

View file

@ -1,5 +1,5 @@
package eu.m724.vastapp.activity.dashboard package eu.m724.vastapp.activity.dashboard
data class DashboardUiState( data class DashboardUiState(
val refreshing: Int = 0 val refreshing: Boolean = false
) )

View file

@ -6,18 +6,17 @@ import android.content.Context
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.viewModelScope
import eu.m724.vastapp.R import eu.m724.vastapp.R
import eu.m724.vastapp.VastApplication import eu.m724.vastapp.VastApplication
import eu.m724.vastapp.activity.Opener import eu.m724.vastapp.activity.Opener
import eu.m724.vastapp.activity.PermissionChecker import eu.m724.vastapp.activity.PermissionChecker
import eu.m724.vastapp.vastai.ApiRoute
import eu.m724.vastapp.vastai.api.InstancesUrlRequestCallback
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
import eu.m724.vastapp.vastai.data.RentedInstance import eu.m724.vastapp.vastai.data.RentedInstance
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class DashboardViewModel( class DashboardViewModel(
@ -25,12 +24,12 @@ class DashboardViewModel(
) : AndroidViewModel(application) { // TODO do something with the user ) : AndroidViewModel(application) { // TODO do something with the user
private val _uiState: MutableStateFlow<DashboardUiState> = private val _uiState: MutableStateFlow<DashboardUiState> =
MutableStateFlow(DashboardUiState(0)) MutableStateFlow(DashboardUiState(false))
val uiState: StateFlow<DashboardUiState> = val uiState: StateFlow<DashboardUiState> =
_uiState.asStateFlow() _uiState.asStateFlow()
private val _refreshError: MutableStateFlow<List<String>> = MutableStateFlow(emptyList()) private val _refreshError: MutableStateFlow<String?> = MutableStateFlow(null)
val refreshError: StateFlow<List<String>> = _refreshError.asStateFlow() val refreshError: StateFlow<String?> = _refreshError.asStateFlow()
private val _termuxAvailable: MutableStateFlow<Int> = MutableStateFlow(0) private val _termuxAvailable: MutableStateFlow<Int> = MutableStateFlow(0)
val termuxAvailable: StateFlow<Int> = _termuxAvailable.asStateFlow() val termuxAvailable: StateFlow<Int> = _termuxAvailable.asStateFlow()
@ -39,43 +38,29 @@ class DashboardViewModel(
private val vastApi = this.application.vastApi private val vastApi = this.application.vastApi
val account = this.application.account!! val account = this.application.account!!
fun refresh(activity: ComponentActivity) { fun refresh() {
_uiState.value = _uiState.value.copy(refreshing = 2) _uiState.update { it.copy(refreshing = true) }
_refreshError.value = emptyList() _refreshError.value = null
val userRequest = vastApi.buildRequest( val userDeferred = vastApi.getUser()
ApiRoute.SHOW_USER, val rentedInstancesDeferred = vastApi.getRentedInstances()
UserUrlRequestCallback({ newUser ->
account.updateUser(newUser)
_uiState.update {
it.copy(refreshing = it.refreshing - 1) // TODO I don't like how this looks
}
}, { apiFailure ->
_refreshError.update { it + apiFailure.errorMessage!! }
_uiState.update {
it.copy(refreshing = it.refreshing - 1)
}
})
)
val instancesRequest = vastApi.buildRequest( viewModelScope.launch {
ApiRoute.GET_INSTANCES, try {
InstancesUrlRequestCallback({ instances -> account.updateUser(userDeferred.await())
account.updateRentedInstances(instances) account.updateRentedInstances(rentedInstancesDeferred.await())
_uiState.update { } catch (e: Exception) {
it.copy(refreshing = it.refreshing - 1) _refreshError.update {
"Refresh failed: " + e.message.toString()
} }
}, { apiFailure -> }
_refreshError.update { it + apiFailure.errorMessage!! } _uiState.update {
_uiState.update { it.copy(refreshing = false)
it.copy(refreshing = it.refreshing - 1) }
} }
}) }
) // TODO move all that refreshing to some shared place
userRequest.start()
instancesRequest.start()
fun checkTermux(activity: ComponentActivity) {
val context = activity.applicationContext val context = activity.applicationContext
_termuxAvailable.value = _termuxAvailable.value =
@ -88,8 +73,6 @@ class DashboardViewModel(
} else -1 // not available because permission denied } else -1 // not available because permission denied
} }
} else -1 // not available } else -1 // not available
// TODO I don't like this function especially the last line. I think it should be moved to application
} }
@SuppressLint("SdCardPath") @SuppressLint("SdCardPath")

View file

@ -32,7 +32,7 @@ class LoadingActivity : ComponentActivity() {
.putExtra("direct", true) .putExtra("direct", true)
} else { } else {
Intent(this, LoginActivity::class.java) Intent(this, LoginActivity::class.java)
.putExtra("error", result.error!!) .putExtra("error", result.error)
} }
this.startActivity(intent) this.startActivity(intent)

View file

@ -2,11 +2,10 @@ package eu.m724.vastapp.activity.dashboard.loading
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import eu.m724.vastapp.VastApplication import eu.m724.vastapp.VastApplication
import eu.m724.vastapp.vastai.Account import eu.m724.vastapp.vastai.Account
import eu.m724.vastapp.vastai.ApiRoute import kotlinx.coroutines.launch
import eu.m724.vastapp.vastai.api.InstancesUrlRequestCallback
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
class LoadingViewModel( class LoadingViewModel(
application: Application, application: Application,
@ -15,40 +14,30 @@ class LoadingViewModel(
private val application = application as VastApplication private val application = application as VastApplication
fun init() { fun init() {
val vastApi = application.vastApi
val apiKey = application.loadKey() val apiKey = application.loadKey()
if (apiKey == null) {
onEnded(LoadingResult(false, null))
return
}
if (apiKey != null) { val vastApi = application.vastApi
vastApi.apiKey = apiKey
val request = vastApi.buildRequest( vastApi.setApiKey(apiKey)
ApiRoute.SHOW_USER, val userDeferred = vastApi.getUser()
UserUrlRequestCallback({ user ->
application.account = Account(user)
loadInstances()
}, { apiFailure ->
onEnded(LoadingResult(false, apiFailure.errorMessage))
})
)
request.start() viewModelScope.launch {
try {
val user = userDeferred.await()
application.account = Account(user)
// TODO should we do this were, or is it better to say you're logged in and handle the error from dashboard
val rentedInstances = vastApi.getRentedInstances().await()
application.account!!.updateRentedInstances(rentedInstances)
onEnded(LoadingResult(true, null))
} catch (e: Exception) {
onEnded(LoadingResult(false, e.message.toString()))
}
} }
} }
private fun loadInstances() {
val vastApi = application.vastApi
val instancesRequest = vastApi.buildRequest(
ApiRoute.GET_INSTANCES,
InstancesUrlRequestCallback({ instances ->
application.account!!.updateRentedInstances(instances)
onEnded(LoadingResult(true, null))
}, { apiFailure ->
// TODO I don't know what to do yet
onEnded(LoadingResult(true, null))
})
)
instancesRequest.start()
}
} }

View file

@ -1,6 +1,5 @@
package eu.m724.vastapp.activity.dashboard.screen package eu.m724.vastapp.activity.dashboard.screen
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
@ -24,9 +23,7 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -47,14 +44,13 @@ fun DashboardScreen(dashboardViewModel: DashboardViewModel) {
val user by dashboardViewModel.account.user.collectAsState() val user by dashboardViewModel.account.user.collectAsState()
val rentedInstances by dashboardViewModel.account.rentedInstances.collectAsState() val rentedInstances by dashboardViewModel.account.rentedInstances.collectAsState()
val remainingTime by dashboardViewModel.account.remainingTime.collectAsState() val remainingTime by dashboardViewModel.account.remainingTime.collectAsState()
val isRefreshing by remember(uiState) { derivedStateOf { uiState.refreshing > 0 } }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
PullToRefreshBox( PullToRefreshBox(
isRefreshing = isRefreshing, isRefreshing = uiState.refreshing,
state = rememberPullToRefreshState(), state = rememberPullToRefreshState(),
onRefresh = { dashboardViewModel.refresh(context as ComponentActivity) } onRefresh = { dashboardViewModel.refresh() }
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,

View file

@ -4,13 +4,13 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import eu.m724.vastapp.VastApplication import eu.m724.vastapp.VastApplication
import eu.m724.vastapp.vastai.ApiRoute
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.chromium.net.CronetEngine import org.chromium.net.CronetEngine
import java.util.concurrent.Executor import java.util.concurrent.Executor
@ -30,29 +30,23 @@ class LoginViewModel(
private val _apiKey = MutableStateFlow<String>("") private val _apiKey = MutableStateFlow<String>("")
var apiKey: StateFlow<String> = _apiKey.asStateFlow() var apiKey: StateFlow<String> = _apiKey.asStateFlow()
private val _fullscreenLoading = MutableStateFlow<Boolean>(false)
var fullscreenLoading: StateFlow<Boolean> = _fullscreenLoading.asStateFlow()
private val application = getApplication<VastApplication>() private val application = getApplication<VastApplication>()
fun tryLogin() { fun tryLogin() {
val vastApi = application.vastApi val userDeferred = application.vastApi.getUser()
vastApi.apiKey = apiKey.value
val request = vastApi.buildRequest( viewModelScope.launch {
ApiRoute.SHOW_USER, try {
UserUrlRequestCallback({ user -> val user = userDeferred.await()
application.submitKey(apiKey.value) // TODO toggle for this application.submitKey(apiKey.value) // TODO toggle for this
_uiState.value = LoginUiState.Success(user) _uiState.update { LoginUiState.Success(user) }
}, { apiFailure -> } catch (e: Exception) {
_uiState.value = LoginUiState.Idle _uiState.update { LoginUiState.Idle }
_fullscreenLoading.value = false _error.postValue(e.toString())
_error.postValue(apiFailure.errorMessage) }
}) }
)
_uiState.value = LoginUiState.Loading _uiState.update { LoginUiState.Loading }
request.start()
} }
fun onApiKeyChange(apiKey: String) { fun onApiKeyChange(apiKey: String) {

View file

@ -1,6 +0,0 @@
package eu.m724.vastapp.vastai
data class ApiFailure(
/** user friendly error message */
val errorMessage: String?,
)

View file

@ -1,8 +0,0 @@
package eu.m724.vastapp.vastai
enum class ApiRoute(val path: String, val method: String) {
SHOW_USER("/users/current", "GET"),
GET_INSTANCES("/instances", "GET"),
INSTANCES_COUNT("/instances/count", "GET"),
MACHINES_MAINTENANCES("/machines/maintenances", "GET")
}

View file

@ -0,0 +1,55 @@
package eu.m724.vastapp.vastai
import eu.m724.vastapp.BuildConfig
import org.chromium.net.CronetEngine
import org.chromium.net.UploadDataProvider
import org.chromium.net.UrlRequest
import java.util.concurrent.Executor
class RequestMaker(
private var apiKey: String,
private val cronetEngine: CronetEngine,
private val executor: Executor
) {
fun setApiKey(apiKey: String) {
this.apiKey = apiKey
}
/**
* build an api request
* don't forget to call .start() on the returned [UrlRequest]
*
* @param endpoint the endpoint path starting with a slash like /users/current
* @param callback any callback for example [UserUrlRequestCallback]
* @param method request method, default GET
* @param headers additional request headers
* @param uploadDataProvider [UploadDataProvider] if request sends data
* @return an [UrlRequest] you must .start() yourself
*/
fun buildRequest(
endpoint: String,
callback: UrlRequest.Callback,
method: String = "GET",
headers: Map<String, String>? = null,
uploadDataProvider: UploadDataProvider? = null
): UrlRequest {
var requestBuilder = cronetEngine.newUrlRequestBuilder(
BuildConfig.VASIAI_API_ENDPOINT + endpoint,
callback,
executor
).addHeader("Authorization", "Bearer $apiKey")
requestBuilder = requestBuilder.setHttpMethod(method)
headers?.forEach { e ->
requestBuilder = requestBuilder.addHeader(e.key, e.value)
}
if (uploadDataProvider != null) {
requestBuilder = requestBuilder.setUploadDataProvider(uploadDataProvider, executor)
}
return requestBuilder.build()
}
}

View file

@ -1,57 +1,115 @@
package eu.m724.vastapp.vastai package eu.m724.vastapp.vastai
import eu.m724.vastapp.BuildConfig import eu.m724.vastapp.vastai.cronet.InstancesUrlRequestCallback
import eu.m724.vastapp.vastai.cronet.JsonUrlRequestCallback
import eu.m724.vastapp.vastai.cronet.UserUrlRequestCallback
import eu.m724.vastapp.vastai.cronet.upload.StringUploadDataProvider
import eu.m724.vastapp.vastai.data.RentedInstance
import eu.m724.vastapp.vastai.data.User
import eu.m724.vastapp.vastai.exceptions.ApiException
import kotlinx.coroutines.CompletableDeferred
import org.chromium.net.CronetEngine import org.chromium.net.CronetEngine
import org.chromium.net.UploadDataProvider
import org.chromium.net.UrlRequest
import java.util.concurrent.Executor import java.util.concurrent.Executor
class VastApi( class VastApi(
var apiKey: String, // TODO make private? apiKey: String, // TODO make private?
private val cronetEngine: CronetEngine, cronetEngine: CronetEngine,
private val executor: Executor executor: Executor
) { ) {
/** private val requestMaker = RequestMaker(apiKey, cronetEngine, executor)
* build an api request
* don't forget to call .start() on the returned [UrlRequest]
*
* @param endpoint the endpoint path starting with a slash like /users/current
* @param callback any callback for example [UserUrlRequestCallback]
* @param method request method, default GET
* @param uploadDataProvider [UploadDataProvider] if request sends data
* @return an [UrlRequest] you must .start() yourself
*/
fun buildRequest(
endpoint: String,
callback: UrlRequest.Callback,
method: String = "GET",
uploadDataProvider: UploadDataProvider?
): UrlRequest {
var requestBuilder = cronetEngine.newUrlRequestBuilder(
BuildConfig.VASIAI_API_ENDPOINT + endpoint,
callback,
executor
).addHeader("Authorization", "Bearer $apiKey")
requestBuilder = requestBuilder.setHttpMethod(method) fun setApiKey(apiKey: String) {
requestMaker.setApiKey(apiKey)
if (uploadDataProvider != null) {
requestBuilder = requestBuilder.setUploadDataProvider(uploadDataProvider, executor)
}
return requestBuilder.build()
} }
/** fun getUser(): CompletableDeferred<User> {
* build an api request val deferred = CompletableDeferred<User>()
* don't forget to call .start() on the returned [UrlRequest]
* val request = requestMaker.buildRequest(
* @param apiRoute the api route "/users/current",
* @param callback any callback for example [UserUrlRequestCallback] UserUrlRequestCallback(
* @param uploadDataProvider [UploadDataProvider] if request sends data onSuccess = { deferred.complete(it) },
* @return an [UrlRequest] you must .start() yourself onFailure = { deferred.completeExceptionally(it) }
*/ )
fun buildRequest(apiRoute: ApiRoute, callback: UrlRequest.Callback, uploadDataProvider: UploadDataProvider? = null): UrlRequest { )
return buildRequest(apiRoute.path, callback, apiRoute.method, uploadDataProvider)
request.start()
return deferred
}
fun getRentedInstances(): CompletableDeferred<List<RentedInstance>> {
val deferred = CompletableDeferred<List<RentedInstance>>()
val request = requestMaker.buildRequest(
"/instances",
InstancesUrlRequestCallback(
onSuccess = { deferred.complete(it) },
onFailure = { deferred.completeExceptionally(it) }
)
)
request.start()
return deferred
} // TODO maybe we could make a function that handles all that build stuff and just takes a type and path
fun deleteInstance(rentalId: Int): CompletableDeferred<Unit> {
val deferred = CompletableDeferred<Unit>()
val request = requestMaker.buildRequest(
"/instances/$rentalId",
JsonUrlRequestCallback(
onSuccess = {
if (it.getBoolean("success"))
deferred.complete(Unit)
else
deferred.completeExceptionally(ApiException("Failed to delete: $it"))
},
onFailure = { deferred.completeExceptionally(it) }
),
"DELETE"
)
request.start()
return deferred
}
fun startInstance(rentalId: Int): CompletableDeferred<Unit> {
val deferred = CompletableDeferred<Unit>()
val request = requestMaker.buildRequest(
"/instances/$rentalId",
JsonUrlRequestCallback(
onSuccess = {
deferred.complete(Unit)
},
onFailure = { deferred.completeExceptionally(it) }
),
"PUT",
mapOf(Pair("Content-Type", "application/json")),
StringUploadDataProvider("{\"state\": \"running\"}")
)
request.start()
return deferred
}
fun stopInstance(rentalId: Int): CompletableDeferred<Unit> { // TODO this too make one function that does all things
val deferred = CompletableDeferred<Unit>()
val request = requestMaker.buildRequest(
"/instances/$rentalId",
JsonUrlRequestCallback(
onSuccess = {
deferred.complete(Unit)
},
onFailure = { deferred.completeExceptionally(it) }
),
"PUT",
mapOf(Pair("Content-Type", "application/json")),
StringUploadDataProvider("{\"state\": \"stopped\"}")
)
request.start()
return deferred
} }
} }

View file

@ -1,63 +0,0 @@
package eu.m724.vastapp.vastai.api
import eu.m724.vastapp.vastai.ApiFailure
import eu.m724.vastapp.vastai.data.RentedInstance
import org.chromium.net.CronetException
import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo
import org.json.JSONObject
import java.nio.ByteBuffer
import java.nio.charset.CodingErrorAction
class InstancesUrlRequestCallback(
val onSuccess: (List<RentedInstance>) -> Unit,
val onFailure: (ApiFailure) -> Unit
) : UrlRequest.Callback() {
private val stringResponse = StringBuilder()
override fun onRedirectReceived(
request: UrlRequest?,
info: UrlResponseInfo?,
newLocationUrl: String?
) {
request?.followRedirect()
}
override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) {
request?.read(ByteBuffer.allocateDirect(102400))
}
override fun onReadCompleted(
request: UrlRequest?,
info: UrlResponseInfo?,
byteBuffer: ByteBuffer?
) {
byteBuffer?.clear()
request?.read(byteBuffer)
stringResponse.append(Charsets.UTF_8.newDecoder().onUnmappableCharacter(CodingErrorAction.IGNORE).decode(byteBuffer))
}
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
println(stringResponse) // TODO don't do that
if (info?.httpStatusCode == 200) {
val jsonResponse = JSONObject(stringResponse.toString())
val instances = ArrayList<RentedInstance>()
val instancesJson = jsonResponse.getJSONArray("instances")
for (i in 0..<instancesJson.length()) {
instances.add(RentedInstance.fromJson(instancesJson.getJSONObject(i)))
}
onSuccess(instances) // TODO handle json errors
} else {
onFailure(ApiFailure("${info?.httpStatusCode} ${info?.httpStatusText}"))
println("API error: $stringResponse")
}
}
override fun onFailed(request: UrlRequest?, info: UrlResponseInfo?, error: CronetException?) {
onFailure(ApiFailure("Network error: ${error?.message ?: "Unknown"}"))
}
}

View file

@ -1,16 +0,0 @@
package eu.m724.vastapp.vastai.api
import eu.m724.vastapp.vastai.ApiFailure
import org.json.JSONObject
class JsonUrlRequestCallback(
onSuccess: (JSONObject) -> Unit,
onFailure: (ApiFailure) -> Unit
) : StringUrlRequestCallback({ stringResponse ->
try {
val jsonResponse = JSONObject(stringResponse)
onSuccess(jsonResponse)
} catch (e: Exception) {
onFailure(ApiFailure(e.message))
}
}, onFailure)

View file

@ -1,72 +0,0 @@
package eu.m724.vastapp.vastai.api
import eu.m724.vastapp.vastai.ApiFailure
import eu.m724.vastapp.vastai.data.User
import org.chromium.net.CronetException
import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo
import org.json.JSONObject
import java.nio.ByteBuffer
import java.nio.charset.CodingErrorAction
class UserUrlRequestCallback(
val onSuccess: (User) -> Unit,
val onFailure: (ApiFailure) -> Unit
) : UrlRequest.Callback() {
private val stringResponse = StringBuilder()
override fun onRedirectReceived(
request: UrlRequest?,
info: UrlResponseInfo?,
newLocationUrl: String?
) {
request?.followRedirect()
}
override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) {
request?.read(ByteBuffer.allocateDirect(102400))
}
override fun onReadCompleted(
request: UrlRequest?,
info: UrlResponseInfo?,
byteBuffer: ByteBuffer?
) {
byteBuffer?.clear()
request?.read(byteBuffer)
stringResponse.append(Charsets.UTF_8.newDecoder().onUnmappableCharacter(CodingErrorAction.IGNORE).decode(byteBuffer))
}
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
println(stringResponse) // TODO don't do that
if (info?.httpStatusCode == 200) {
try {
val jsonResponse = JSONObject(stringResponse.toString())
onSuccess(
User(
id = jsonResponse.getString("id"),
username = jsonResponse.getString("username"),
email = jsonResponse.getString("email"),
apiKey = jsonResponse.getString("api_key"),
credit = jsonResponse.getDouble("credit"),
balanceThreshold = jsonResponse.getDouble("balance_threshold"),
balanceThresholdEnabled = jsonResponse.getBoolean("balance_threshold_enabled"),
)
)
} catch (e: Exception) {
onFailure(ApiFailure(e.message))
println("API response error: $stringResponse")
}
} else {
onFailure(ApiFailure("${info?.httpStatusCode} ${info?.httpStatusText}"))
println("API error: $stringResponse")
}
}
override fun onFailed(request: UrlRequest?, info: UrlResponseInfo?, error: CronetException?) {
onFailure(ApiFailure("Network error: ${error?.message ?: "Unknown"}"))
}
}

View file

@ -0,0 +1,18 @@
package eu.m724.vastapp.vastai.cronet
import eu.m724.vastapp.vastai.exceptions.ApiException
import eu.m724.vastapp.vastai.data.RentedInstance
class InstancesUrlRequestCallback(
onSuccess: (List<RentedInstance>) -> Unit,
onFailure: (ApiException) -> Unit
) : JsonUrlRequestCallback({ json ->
val instances = ArrayList<RentedInstance>()
val instancesJson = json.getJSONArray("instances")
for (i in 0..<instancesJson.length()) {
instances.add(RentedInstance.fromJson(instancesJson.getJSONObject(i)))
}
onSuccess(instances)
}, onFailure)

View file

@ -0,0 +1,12 @@
package eu.m724.vastapp.vastai.cronet
import eu.m724.vastapp.vastai.exceptions.ApiException
import org.json.JSONObject
open class JsonUrlRequestCallback(
onSuccess: (JSONObject) -> Unit,
onFailure: (ApiException) -> Unit
) : StringUrlRequestCallback({ stringResponse ->
val jsonResponse = JSONObject(stringResponse) // errors are handled in the super class
onSuccess(jsonResponse)
}, onFailure)

View file

@ -1,6 +1,9 @@
package eu.m724.vastapp.vastai.api package eu.m724.vastapp.vastai.cronet
import eu.m724.vastapp.vastai.ApiFailure import eu.m724.vastapp.vastai.exceptions.ApiException
import eu.m724.vastapp.vastai.exceptions.ClientException
import eu.m724.vastapp.vastai.exceptions.ServerError
import eu.m724.vastapp.vastai.exceptions.UnauthorizedException
import org.chromium.net.CronetException import org.chromium.net.CronetException
import org.chromium.net.UrlRequest import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo import org.chromium.net.UrlResponseInfo
@ -9,7 +12,7 @@ import java.nio.charset.CodingErrorAction
open class StringUrlRequestCallback( open class StringUrlRequestCallback(
val onSuccess: (String) -> Unit, val onSuccess: (String) -> Unit,
val onFailure: (ApiFailure) -> Unit val onFailure: (ApiException) -> Unit
) : UrlRequest.Callback() { ) : UrlRequest.Callback() {
protected val stringResponse = StringBuilder() protected val stringResponse = StringBuilder()
@ -37,15 +40,27 @@ open class StringUrlRequestCallback(
} }
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) { override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
if (info?.httpStatusCode == 200) { if (info != null) {
onSuccess(stringResponse.toString()) val body = stringResponse.toString()
} else { val statusCode = info.httpStatusCode
onFailure(ApiFailure("${info?.httpStatusCode} ${info?.httpStatusText}"))
println("API error: ${stringResponse.toString()}") if (statusCode == 200) {
try {
onSuccess(body)
} catch (e: Exception) { // TODO maybe do it differently
onFailure(ClientException(body))
}
} else if (statusCode >= 500) {
onFailure(ServerError(statusCode, body))
} else if (statusCode == 403) {
onFailure(UnauthorizedException(body))
} else {
onFailure(ClientException(body))
}
} }
} }
override fun onFailed(request: UrlRequest?, info: UrlResponseInfo?, error: CronetException?) { override fun onFailed(request: UrlRequest?, info: UrlResponseInfo?, error: CronetException?) {
onFailure(ApiFailure("Network error: ${error?.message ?: "Unknown"}")) onFailure(ApiException(error?.message, error))
} }
} }

View file

@ -0,0 +1,21 @@
package eu.m724.vastapp.vastai.cronet
import eu.m724.vastapp.vastai.exceptions.ApiException
import eu.m724.vastapp.vastai.data.User
class UserUrlRequestCallback(
onSuccess: (User) -> Unit,
onFailure: (ApiException) -> Unit
): JsonUrlRequestCallback({ json ->
onSuccess(
User( // TODO move that to a static function in User
id = json.getString("id"),
username = json.getString("username"),
email = json.getString("email"),
apiKey = json.getString("api_key"),
credit = json.getDouble("credit"),
balanceThreshold = json.getDouble("balance_threshold"),
balanceThresholdEnabled = json.getBoolean("balance_threshold_enabled"),
)
)
}, onFailure)

View file

@ -1,4 +1,4 @@
package eu.m724.vastapp.vastai.api.upload package eu.m724.vastapp.vastai.cronet.upload
import org.json.JSONObject import org.json.JSONObject

View file

@ -1,4 +1,4 @@
package eu.m724.vastapp.vastai.api.upload package eu.m724.vastapp.vastai.cronet.upload
import org.chromium.net.UploadDataProvider import org.chromium.net.UploadDataProvider
import org.chromium.net.UploadDataSink import org.chromium.net.UploadDataSink

View file

@ -0,0 +1,6 @@
package eu.m724.vastapp.vastai.exceptions
open class ApiException(
override val message: String? = null,
override val cause: Throwable? = null
): Exception()

View file

@ -0,0 +1,6 @@
package eu.m724.vastapp.vastai.exceptions
open class ClientException(
message: String? = null,
cause: Throwable? = null
) : ApiException(message, cause)

View file

@ -0,0 +1,6 @@
package eu.m724.vastapp.vastai.exceptions
open class ServerError(
val statusCode: Int,
message: String?,
): ApiException(message, null)

View file

@ -0,0 +1,5 @@
package eu.m724.vastapp.vastai.exceptions
class UnauthorizedException(
message: String? = null
) : ClientException(message, null)