This commit is contained in:
Thomas Andres Gomez 2021-05-08 08:48:16 +02:00
parent 7898a51252
commit 8fbe3c0b7b
21 changed files with 359 additions and 35 deletions

View file

@ -3,21 +3,24 @@
package="com.pixelized.biblib">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".BibLibApplication"
android:allowBackup="true"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

View file

@ -0,0 +1,22 @@
package com.pixelized.biblib
import android.app.Application
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.pixelized.biblib.injection.Bob
import com.pixelized.biblib.network.client.BibLibClient
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.utils.BitmapCache
class BibLibApplication : Application() {
override fun onCreate() {
super.onCreate()
Bob[BitmapCache::class] = BitmapCache(this)
Bob[Gson::class] = GsonBuilder().create()
Bob[IBibLibClient::class] = BibLibClient()
}
}

View file

@ -0,0 +1,10 @@
package com.pixelized.biblib.data.network.query
import com.google.gson.annotations.SerializedName
data class AuthLoginQuery(
@SerializedName("username")
val username: String?,
@SerializedName("password")
val password: String?,
)

View file

@ -0,0 +1,8 @@
package com.pixelized.biblib.data.network.response
import com.google.gson.annotations.SerializedName
data class AuthLoginResponse(
@SerializedName("id_token")
val token: String? = null,
) : ErrorResponse()

View file

@ -0,0 +1,25 @@
package com.pixelized.biblib.data.network.response
import com.google.gson.annotations.SerializedName
open class ErrorResponse(
@SerializedName("error")
val error: Error? = null,
@SerializedName("message")
val message: String? = null,
) {
val isError: Boolean get() = error != null
class Error(
@SerializedName("expose")
val expose: Boolean? = null,
@SerializedName("statusCode")
val statusCode: Int? = null,
@SerializedName("status")
val status: Int? = null,
@SerializedName("body")
val body: String? = null,
@SerializedName("type")
val type: String? = null,
)
}

View file

@ -1,6 +1,6 @@
package com.pixelized.biblib.data.ui
import com.pixelized.biblib.utils.Constant.THUMBNAIL_URL
import com.pixelized.biblib.network.client.IBibLibClient.Companion.THUMBNAIL_URL
import java.net.URL
data class BookThumbnailUio(

View file

@ -1,6 +1,6 @@
package com.pixelized.biblib.data.ui
import com.pixelized.biblib.utils.Constant
import com.pixelized.biblib.network.client.IBibLibClient.Companion.COVER_URL
import java.net.URL
data class BookUio(
@ -14,5 +14,5 @@ data class BookUio(
val series: String?,
val description: String,
) {
val cover: URL = URL("${Constant.COVER_URL}/$id.jpg")
val cover: URL = URL("${COVER_URL}/$id.jpg")
}

View file

@ -0,0 +1,21 @@
package com.pixelized.biblib.injection
import com.pixelized.biblib.utils.exception.InjectionException
import kotlin.reflect.KClass
@Suppress("UNCHECKED_CAST")
object Bob {
private val components = hashMapOf<KClass<*>, Any>()
operator fun <I : Any, O : I> set(clazz: KClass<I>, component: O) {
components[clazz] = component
}
operator fun <T> get(clazz: KClass<*>): T {
return components[clazz] as? T ?: throw InjectionException(clazz)
}
}
inline fun <reified T> get(): T = Bob[T::class]
inline fun <reified T> inject(): Lazy<T> = lazy { Bob[T::class] }

View file

@ -0,0 +1,23 @@
package com.pixelized.biblib.network.client
import okhttp3.Interceptor
import okhttp3.Response
class BearerInterceptor : Interceptor {
private val bearer get() = "$BEARER $token"
var token: String? = null
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (request.header(NO_AUTHORIZATION) == null && token.isNullOrEmpty().not()) {
request = request.newBuilder().addHeader(AUTHORIZATION, bearer).build()
}
return chain.proceed(request)
}
companion object {
private const val NO_AUTHORIZATION = "No-Authentication"
private const val AUTHORIZATION = "Authorization"
private const val BEARER = "Bearer"
}
}

View file

@ -0,0 +1,30 @@
package com.pixelized.biblib.network.client
import com.google.gson.Gson
import com.pixelized.biblib.injection.inject
import com.pixelized.biblib.network.client.IBibLibClient.Companion.BASE_URL
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class BibLibClient : IBibLibClient {
private val gson by inject<Gson>()
private val interceptor = BearerInterceptor()
private val retrofit: Retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.client(OkHttpClient.Builder().addInterceptor(interceptor).build())
.baseUrl(BASE_URL)
.build()
override val service: IBibLibWebServiceAPI = retrofit.create(IBibLibWebServiceAPI::class.java)
// endregion
///////////////////////////////////
// region BibLib webservice Auth
override fun updateBearerToken(token: String?) {
interceptor.token = token
}
// endregion
}

View file

@ -0,0 +1,15 @@
package com.pixelized.biblib.network.client
interface IBibLibClient {
val service: IBibLibWebServiceAPI
fun updateBearerToken(token: String?)
companion object {
const val BASE_URL = "https://bib.bibulle.fr"
const val THUMBNAIL_URL = "$BASE_URL/api/book/thumbnail"
const val COVER_URL = "$BASE_URL/api/book/cover"
const val REGISTER_URL = "$BASE_URL/signup"
}
}

View file

@ -0,0 +1,43 @@
package com.pixelized.biblib.network.client
import com.pixelized.biblib.data.network.query.AuthLoginQuery
import com.pixelized.biblib.data.network.response.AuthLoginResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
interface IBibLibWebServiceAPI {
@POST("/authent/login")
suspend fun login(@Body query: AuthLoginQuery): AuthLoginResponse
@GET("/authent/google-id-token")
suspend fun loginWithGoogle(@Query("id_token") token: String): AuthLoginResponse
// @GET("/authent/user")
// suspend fun user(): UserResponse
//
// @GET("/api/book/new")
// suspend fun new(): BookListResponse
//
// @GET("/api/book")
// suspend fun list(): BookListResponse
//
// @GET("/api/book/{id}")
// suspend fun detail(@Path("id") bookId: Int): BookDetailResponse
//
// @GET("/api/book/{id}/send/kindle")
// suspend fun send(@Path("id") bookId: Int, @Query("mail") mail: String): LinkedTreeMap<String, Any>
//
// @GET("/api/book/{id}/epub/url")
// suspend fun epub(@Path("id") bookId: Int): TokenResponse
//
// @GET("/api/book/{id}/mobi/url")
// suspend fun mobi(@Path("id") bookId: Int): TokenResponse
//
// @GET("/api/book/{id}/epub")
// fun downloadEpub(@Path("id") bookId: Int, @Query("token") token: String): Call<ResponseBody>
//
// @GET("/api/book/{id}/mobi")
// fun downloadMobi(@Path("id") bookId: Int, @Query("token") token: String): Call<ResponseBody>
}

View file

@ -5,28 +5,37 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.screen.LoginScreenComposable
import com.pixelized.biblib.ui.composable.screen.MainScreenComposable
import com.pixelized.biblib.ui.composable.screen.SplashScreenComposable
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.AuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel.Screen
import com.pixelized.biblib.utils.BitmapCache
class MainActivity : ComponentActivity() {
private val navigationViewModel: NavigationViewModel by viewModels()
private val googleSignInOption by lazy {
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.biblib_server_id))
.requestEmail()
.build()
}
val googleSignIn by lazy {
GoogleSignIn.getClient(this, googleSignInOption)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
BitmapCache.init(this)
setContent {
BibLibTheme {
ContentComposable()

View file

@ -1,5 +1,10 @@
package com.pixelized.biblib.ui.composable.screen
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -19,6 +24,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -28,7 +34,11 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.common.api.ApiException
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.MainActivity
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.AuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.NavigationViewModel
@ -43,10 +53,35 @@ fun LoginScreenComposablePreview() {
@Composable
fun LoginScreenComposable(
navigationViewModel: NavigationViewModel = viewModel(),
authenticationViewModel: AuthenticationViewModel = viewModel(),
) {
val navigationViewModel = viewModel<NavigationViewModel>()
val authenticationViewModel = viewModel<AuthenticationViewModel>()
// TODO : c'est de la merde ça
val activity = LocalContext.current as MainActivity
val result = remember { mutableStateOf<String?>(null) }
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
try {
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
val account: GoogleSignInAccount? = task.getResult(ApiException::class.java)
val idToken = account?.idToken
// if (idToken != null) {
// viewModel.loginWithGoogle(idToken).observeLogin()
// } else {
// Toast.makeText(requireActivity(), "GoogleSignIn missing Token", Toast.LENGTH_SHORT).show()
// }
Log.e("AuthLoginFragment", "idToken: $idToken")
} catch (exception: Exception) {
// Toast.makeText(requireActivity(), "GoogleSignIn exception: ${exception.message}", Toast.LENGTH_SHORT).show()
Log.e("AuthLoginFragment", exception.message, exception)
}
// // Here we just update the state, but you could imagine
// // pre-processing the result, or updating a MutableSharedFlow that
// // your composable collects
// result.value = it
}
val typography = MaterialTheme.typography
Column(
@ -102,14 +137,12 @@ fun LoginScreenComposable(
modifier = Modifier.padding(end = 8.dp),
colors = outlinedButtonColors(),
onClick = {
// TODO:
navigationViewModel.navigateTo(NavigationViewModel.Screen.MainScreen)
authenticationViewModel.register()
}) {
Text(text = stringResource(id = R.string.action_register))
}
Button(onClick = {
// TODO:
navigationViewModel.navigateTo(NavigationViewModel.Screen.MainScreen)
authenticationViewModel.login()
}) {
Text(text = stringResource(id = R.string.action_login))
}
@ -121,8 +154,7 @@ fun LoginScreenComposable(
modifier = Modifier.fillMaxWidth(),
colors = outlinedButtonColors(),
onClick = {
// TODO:
navigationViewModel.navigateTo(NavigationViewModel.Screen.MainScreen)
launcher.launch(activity.googleSignIn.signInIntent)
}) {
Image(
modifier = Modifier.padding(end = 8.dp),
@ -189,7 +221,7 @@ private fun PasswordField(
private fun CredentialRemember(viewModel: AuthenticationViewModel, modifier: Modifier = Modifier) {
val credential = viewModel.rememberCredential.observeAsState()
Row(modifier = modifier.clickable {
viewModel.updateRememberCredential(credential = credential.value?.not() ?: false)
viewModel.updateRememberCredential(rememberCredential = credential.value?.not() ?: false)
}) {
Checkbox(
modifier = Modifier.align(Alignment.CenterVertically),

View file

@ -1,10 +1,24 @@
package com.pixelized.biblib.ui.viewmodel
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.data.network.query.AuthLoginQuery
import com.pixelized.biblib.injection.inject
import com.pixelized.biblib.network.client.IBibLibClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AuthenticationViewModel: ViewModel() {
class AuthenticationViewModel(application: Application) : AndroidViewModel(application) {
private val preferences = application.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE)
private val client: IBibLibClient by inject()
private val _login = MutableLiveData<String>()
val login: LiveData<String> get() = _login
@ -12,9 +26,17 @@ class AuthenticationViewModel: ViewModel() {
private val _password = MutableLiveData<String>()
val password: LiveData<String> get() = _password
private val _rememberCredential = MutableLiveData<Boolean>(false)
private val _rememberCredential = MutableLiveData<Boolean>()
val rememberCredential: LiveData<Boolean> get() = _rememberCredential
init {
viewModelScope.launch(Dispatchers.Main) {
_login.value = preferences.login
_password.value = preferences.password
_rememberCredential.value = preferences.rememberCredential
}
}
fun updateLogin(login: String) {
_login.postValue(login)
}
@ -23,7 +45,54 @@ class AuthenticationViewModel: ViewModel() {
_password.postValue(password)
}
fun updateRememberCredential(credential: Boolean) {
_rememberCredential.postValue(credential)
fun updateRememberCredential(rememberCredential: Boolean) {
_rememberCredential.postValue(rememberCredential)
viewModelScope.launch {
preferences.rememberCredential = rememberCredential
if (rememberCredential.not()) {
preferences.login = null
preferences.password = null
}
}
}
fun register() {
}
fun login() {
viewModelScope.launch(Dispatchers.IO) {
if (rememberCredential.value == true) {
preferences.login = login.value
preferences.password = password.value
}
// TODO : validation !
val query = AuthLoginQuery(
username = login.value,
password = password.value
)
// TODO : Repository (token management & co)
val response = client.service.login(query)
Log.e("pouet", response.toString())
}
}
private var SharedPreferences.login: String?
get() = getString(REMEMBER_USER, null)
set(value) = edit { putString(REMEMBER_USER, value) }
private var SharedPreferences.password: String?
get() = getString(REMEMBER_PASSWORD, null)
set(value) = edit { putString(REMEMBER_PASSWORD, value) }
private var SharedPreferences.rememberCredential: Boolean
get() = getBoolean(REMEMBER_CREDENTIAL, false)
set(value) = edit { putBoolean(REMEMBER_CREDENTIAL, value) }
companion object {
const val SHARED_PREF = "BIB_LIB_SHARED_PREF"
const val REMEMBER_CREDENTIAL = "REMEMBER_CREDENTIAL"
const val REMEMBER_USER = "REMEMBER_USER"
const val REMEMBER_PASSWORD = "REMEMBER_PASSWORD"
}
}

View file

@ -1,6 +1,6 @@
package com.pixelized.biblib.utils
import android.content.Context
import android.app.Application
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
@ -12,12 +12,11 @@ import java.io.File
import java.io.IOException
import java.net.URL
object BitmapCache {
private val scope = CoroutineScope(Dispatchers.IO)
class BitmapCache(application: Application) {
private var cache: File? = null
fun init(context: Context) {
cache = context.cacheDir
init {
cache = application.cacheDir
}
fun writeToDisk(url: URL, bitmap: Bitmap) {

View file

@ -0,0 +1,5 @@
package com.pixelized.biblib.utils.exception
import kotlin.reflect.KClass
class InjectionException(clazz: KClass<*>) : RuntimeException("InjectionException occur for class:${clazz}")

View file

@ -0,0 +1,3 @@
package com.pixelized.biblib.utils.exception
class NoBearerException : RuntimeException("Bearer token is null")

View file

@ -5,18 +5,19 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import com.pixelized.biblib.injection.get
import com.pixelized.biblib.utils.BitmapCache
import java.net.URL
fun URL.toImage(placeHolder: Painter): State<Painter> {
val cached = BitmapCache.readFromDisk(this)?.let { BitmapPainter(it.asImageBitmap()) }
val state = mutableStateOf(cached ?: placeHolder)
val cache: BitmapCache = get()
val resource = cache.readFromDisk(this)?.let { BitmapPainter(it.asImageBitmap()) }
val state = mutableStateOf(resource ?: placeHolder)
if (cached == null) {
BitmapCache.download(url = this) { downloaded ->
if (resource == null) {
cache.download(url = this) { downloaded ->
if (downloaded != null) {
BitmapCache.writeToDisk(this, downloaded)
cache.writeToDisk(this, downloaded)
state.value = BitmapPainter(downloaded.asImageBitmap())
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="biblib_server_id" translatable="false">725701605591-rr2dqeabon4kjpfevoruru65eo3rukmv.apps.googleusercontent.com</string>
</resources>