Compare commits
No commits in common. "3ddbe78fe802ac661ee43d20f3efbd95900d447d" and "784879393f6f7bcab886d36fa32d4bb78caff444" have entirely different histories.
3ddbe78fe8
...
784879393f
24 changed files with 331 additions and 329 deletions
|
@ -44,16 +44,17 @@ 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()
|
dashboardViewModel.refresh(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboardViewModel.checkTermux(this)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
dashboardViewModel.refreshError.collect {
|
dashboardViewModel.refreshError.collect {
|
||||||
Toast.makeText(baseContext, it, Toast.LENGTH_SHORT).show()
|
it.forEach { errorMsg ->
|
||||||
} // TODO any better way?
|
Toast.makeText(baseContext, errorMsg, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,9 +113,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
|
||||||
// re-selecting the same item
|
// reselecting the same item
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
// Restore state when re-selecting a previously selected item
|
// Restore state when reselecting a previously selected item
|
||||||
restoreState = true
|
restoreState = true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
package eu.m724.vastapp.activity.dashboard
|
package eu.m724.vastapp.activity.dashboard
|
||||||
|
|
||||||
data class DashboardUiState(
|
data class DashboardUiState(
|
||||||
val refreshing: Boolean = false
|
val refreshing: Int = 0
|
||||||
)
|
)
|
|
@ -6,17 +6,18 @@ 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(
|
||||||
|
@ -24,12 +25,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(false))
|
MutableStateFlow(DashboardUiState(0))
|
||||||
val uiState: StateFlow<DashboardUiState> =
|
val uiState: StateFlow<DashboardUiState> =
|
||||||
_uiState.asStateFlow()
|
_uiState.asStateFlow()
|
||||||
|
|
||||||
private val _refreshError: MutableStateFlow<String?> = MutableStateFlow(null)
|
private val _refreshError: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
|
||||||
val refreshError: StateFlow<String?> = _refreshError.asStateFlow()
|
val refreshError: StateFlow<List<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()
|
||||||
|
@ -38,29 +39,43 @@ 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() {
|
fun refresh(activity: ComponentActivity) {
|
||||||
_uiState.update { it.copy(refreshing = true) }
|
_uiState.value = _uiState.value.copy(refreshing = 2)
|
||||||
_refreshError.value = null
|
_refreshError.value = emptyList()
|
||||||
|
|
||||||
val userDeferred = vastApi.getUser()
|
val userRequest = vastApi.buildRequest(
|
||||||
val rentedInstancesDeferred = vastApi.getRentedInstances()
|
ApiRoute.SHOW_USER,
|
||||||
|
UserUrlRequestCallback({ newUser ->
|
||||||
viewModelScope.launch {
|
account.updateUser(newUser)
|
||||||
try {
|
_uiState.update {
|
||||||
account.updateUser(userDeferred.await())
|
it.copy(refreshing = it.refreshing - 1) // TODO I don't like how this looks
|
||||||
account.updateRentedInstances(rentedInstancesDeferred.await())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_refreshError.update {
|
|
||||||
"Refresh failed: " + e.message.toString()
|
|
||||||
}
|
}
|
||||||
}
|
}, { apiFailure ->
|
||||||
_uiState.update {
|
_refreshError.update { it + apiFailure.errorMessage!! }
|
||||||
it.copy(refreshing = false)
|
_uiState.update {
|
||||||
}
|
it.copy(refreshing = it.refreshing - 1)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
val instancesRequest = vastApi.buildRequest(
|
||||||
|
ApiRoute.GET_INSTANCES,
|
||||||
|
InstancesUrlRequestCallback({ instances ->
|
||||||
|
account.updateRentedInstances(instances)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(refreshing = it.refreshing - 1)
|
||||||
|
}
|
||||||
|
}, { apiFailure ->
|
||||||
|
_refreshError.update { it + apiFailure.errorMessage!! }
|
||||||
|
_uiState.update {
|
||||||
|
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 =
|
||||||
|
@ -73,6 +88,8 @@ 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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -2,10 +2,11 @@ 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 kotlinx.coroutines.launch
|
import eu.m724.vastapp.vastai.ApiRoute
|
||||||
|
import eu.m724.vastapp.vastai.api.InstancesUrlRequestCallback
|
||||||
|
import eu.m724.vastapp.vastai.api.UserUrlRequestCallback
|
||||||
|
|
||||||
class LoadingViewModel(
|
class LoadingViewModel(
|
||||||
application: Application,
|
application: Application,
|
||||||
|
@ -14,30 +15,40 @@ class LoadingViewModel(
|
||||||
private val application = application as VastApplication
|
private val application = application as VastApplication
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
val apiKey = application.loadKey()
|
|
||||||
if (apiKey == null) {
|
|
||||||
onEnded(LoadingResult(false, null))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val vastApi = application.vastApi
|
val vastApi = application.vastApi
|
||||||
|
val apiKey = application.loadKey()
|
||||||
|
|
||||||
vastApi.setApiKey(apiKey)
|
if (apiKey != null) {
|
||||||
val userDeferred = vastApi.getUser()
|
vastApi.apiKey = apiKey
|
||||||
|
|
||||||
viewModelScope.launch {
|
val request = vastApi.buildRequest(
|
||||||
try {
|
ApiRoute.SHOW_USER,
|
||||||
val user = userDeferred.await()
|
UserUrlRequestCallback({ user ->
|
||||||
application.account = Account(user)
|
application.account = Account(user)
|
||||||
|
loadInstances()
|
||||||
|
}, { apiFailure ->
|
||||||
|
onEnded(LoadingResult(false, apiFailure.errorMessage))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// TODO should we do this were, or is it better to say you're logged in and handle the error from dashboard
|
request.start()
|
||||||
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()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
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
|
||||||
|
@ -23,7 +24,9 @@ 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
|
||||||
|
@ -44,13 +47,14 @@ 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 = uiState.refreshing,
|
isRefreshing = isRefreshing,
|
||||||
state = rememberPullToRefreshState(),
|
state = rememberPullToRefreshState(),
|
||||||
onRefresh = { dashboardViewModel.refresh() }
|
onRefresh = { dashboardViewModel.refresh(context as ComponentActivity) }
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
|
|
@ -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,23 +30,29 @@ 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 userDeferred = application.vastApi.getUser()
|
val vastApi = application.vastApi
|
||||||
|
vastApi.apiKey = apiKey.value
|
||||||
|
|
||||||
viewModelScope.launch {
|
val request = vastApi.buildRequest(
|
||||||
try {
|
ApiRoute.SHOW_USER,
|
||||||
val user = userDeferred.await()
|
UserUrlRequestCallback({ user ->
|
||||||
application.submitKey(apiKey.value) // TODO toggle for this
|
application.submitKey(apiKey.value) // TODO toggle for this
|
||||||
_uiState.update { LoginUiState.Success(user) }
|
_uiState.value = LoginUiState.Success(user)
|
||||||
} catch (e: Exception) {
|
}, { apiFailure ->
|
||||||
_uiState.update { LoginUiState.Idle }
|
_uiState.value = LoginUiState.Idle
|
||||||
_error.postValue(e.toString())
|
_fullscreenLoading.value = false
|
||||||
}
|
_error.postValue(apiFailure.errorMessage)
|
||||||
}
|
})
|
||||||
|
)
|
||||||
|
|
||||||
_uiState.update { LoginUiState.Loading }
|
_uiState.value = LoginUiState.Loading
|
||||||
|
request.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onApiKeyChange(apiKey: String) {
|
fun onApiKeyChange(apiKey: String) {
|
||||||
|
|
6
app/src/main/java/eu/m724/vastapp/vastai/ApiFailure.kt
Normal file
6
app/src/main/java/eu/m724/vastapp/vastai/ApiFailure.kt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package eu.m724.vastapp.vastai
|
||||||
|
|
||||||
|
data class ApiFailure(
|
||||||
|
/** user friendly error message */
|
||||||
|
val errorMessage: String?,
|
||||||
|
)
|
8
app/src/main/java/eu/m724/vastapp/vastai/ApiRoute.kt
Normal file
8
app/src/main/java/eu/m724/vastapp/vastai/ApiRoute.kt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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")
|
||||||
|
}
|
|
@ -1,55 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +1,57 @@
|
||||||
package eu.m724.vastapp.vastai
|
package eu.m724.vastapp.vastai
|
||||||
|
|
||||||
import eu.m724.vastapp.vastai.cronet.InstancesUrlRequestCallback
|
import eu.m724.vastapp.BuildConfig
|
||||||
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(
|
||||||
apiKey: String, // TODO make private?
|
var apiKey: String, // TODO make private?
|
||||||
cronetEngine: CronetEngine,
|
private val cronetEngine: CronetEngine,
|
||||||
executor: Executor
|
private val 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")
|
||||||
|
|
||||||
fun setApiKey(apiKey: String) {
|
requestBuilder = requestBuilder.setHttpMethod(method)
|
||||||
requestMaker.setApiKey(apiKey)
|
|
||||||
|
if (uploadDataProvider != null) {
|
||||||
|
requestBuilder = requestBuilder.setUploadDataProvider(uploadDataProvider, executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUser(): CompletableDeferred<User> {
|
/**
|
||||||
val deferred = CompletableDeferred<User>()
|
* build an api request
|
||||||
|
* don't forget to call .start() on the returned [UrlRequest]
|
||||||
val request = requestMaker.buildRequest(
|
*
|
||||||
"/users/current",
|
* @param apiRoute the api route
|
||||||
UserUrlRequestCallback(
|
* @param callback any callback for example [UserUrlRequestCallback]
|
||||||
onSuccess = { deferred.complete(it) },
|
* @param uploadDataProvider [UploadDataProvider] if request sends data
|
||||||
onFailure = { deferred.completeExceptionally(it) }
|
* @return an [UrlRequest] you must .start() yourself
|
||||||
)
|
*/
|
||||||
)
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
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"}"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
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)
|
|
@ -1,9 +1,6 @@
|
||||||
package eu.m724.vastapp.vastai.cronet
|
package eu.m724.vastapp.vastai.api
|
||||||
|
|
||||||
import eu.m724.vastapp.vastai.exceptions.ApiException
|
import eu.m724.vastapp.vastai.ApiFailure
|
||||||
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
|
||||||
|
@ -12,7 +9,7 @@ import java.nio.charset.CodingErrorAction
|
||||||
|
|
||||||
open class StringUrlRequestCallback(
|
open class StringUrlRequestCallback(
|
||||||
val onSuccess: (String) -> Unit,
|
val onSuccess: (String) -> Unit,
|
||||||
val onFailure: (ApiException) -> Unit
|
val onFailure: (ApiFailure) -> Unit
|
||||||
) : UrlRequest.Callback() {
|
) : UrlRequest.Callback() {
|
||||||
protected val stringResponse = StringBuilder()
|
protected val stringResponse = StringBuilder()
|
||||||
|
|
||||||
|
@ -40,27 +37,15 @@ open class StringUrlRequestCallback(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
|
override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
|
||||||
if (info != null) {
|
if (info?.httpStatusCode == 200) {
|
||||||
val body = stringResponse.toString()
|
onSuccess(stringResponse.toString())
|
||||||
val statusCode = info.httpStatusCode
|
} else {
|
||||||
|
onFailure(ApiFailure("${info?.httpStatusCode} ${info?.httpStatusText}"))
|
||||||
if (statusCode == 200) {
|
println("API error: ${stringResponse.toString()}")
|
||||||
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(ApiException(error?.message, error))
|
onFailure(ApiFailure("Network error: ${error?.message ?: "Unknown"}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
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"}"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.vastapp.vastai.cronet.upload
|
package eu.m724.vastapp.vastai.api.upload
|
||||||
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.vastapp.vastai.cronet.upload
|
package eu.m724.vastapp.vastai.api.upload
|
||||||
|
|
||||||
import org.chromium.net.UploadDataProvider
|
import org.chromium.net.UploadDataProvider
|
||||||
import org.chromium.net.UploadDataSink
|
import org.chromium.net.UploadDataSink
|
|
@ -1,18 +0,0 @@
|
||||||
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)
|
|
|
@ -1,12 +0,0 @@
|
||||||
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)
|
|
|
@ -1,21 +0,0 @@
|
||||||
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)
|
|
|
@ -1,6 +0,0 @@
|
||||||
package eu.m724.vastapp.vastai.exceptions
|
|
||||||
|
|
||||||
open class ApiException(
|
|
||||||
override val message: String? = null,
|
|
||||||
override val cause: Throwable? = null
|
|
||||||
): Exception()
|
|
|
@ -1,6 +0,0 @@
|
||||||
package eu.m724.vastapp.vastai.exceptions
|
|
||||||
|
|
||||||
open class ClientException(
|
|
||||||
message: String? = null,
|
|
||||||
cause: Throwable? = null
|
|
||||||
) : ApiException(message, cause)
|
|
|
@ -1,6 +0,0 @@
|
||||||
package eu.m724.vastapp.vastai.exceptions
|
|
||||||
|
|
||||||
open class ServerError(
|
|
||||||
val statusCode: Int,
|
|
||||||
message: String?,
|
|
||||||
): ApiException(message, null)
|
|
|
@ -1,5 +0,0 @@
|
||||||
package eu.m724.vastapp.vastai.exceptions
|
|
||||||
|
|
||||||
class UnauthorizedException(
|
|
||||||
message: String? = null
|
|
||||||
) : ClientException(message, null)
|
|
Loading…
Reference in a new issue