some work on v4 authentication

This commit is contained in:
Owen LeJeune
2022-06-20 15:48:31 -04:00
parent 24a0e56fd7
commit 007db6324e
11 changed files with 216 additions and 48 deletions

View File

@@ -18,12 +18,16 @@
android:exported="true"
android:theme="@style/Theme.TVTime"
android:windowSoftInputMode="adjustResize">
<!-- android:screenOrientation="portrait">-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="app" android:host="@string/intent_route_auth_return" />
</intent-filter>
</activity>
</application>

View File

@@ -1,6 +1,7 @@
package com.owenlejeune.tvtime
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.Image
@@ -59,6 +60,13 @@ class MainActivity : MonetCompatActivity() {
SessionManager.initialize()
}
var mainNavStartRoute = BottomNavItem.Items[0].route
intent.data?.let {
when (it.host) {
getString(R.string.intent_route_auth_return) -> mainNavStartRoute = BottomNavItem.Account.route
}
}
lifecycleScope.launchWhenCreated {
monet.awaitMonetReady()
setContent {
@@ -66,7 +74,7 @@ class MainActivity : MonetCompatActivity() {
TVTimeTheme(monetCompat = monet) {
val appNavController = rememberNavController()
Box {
MainNavigationRoutes(appNavController = appNavController)
MainNavigationRoutes(appNavController = appNavController, mainNavStartRoute = mainNavStartRoute)
}
}
}
@@ -74,7 +82,11 @@ class MainActivity : MonetCompatActivity() {
}
@Composable
private fun AppScaffold(appNavController: NavHostController, preferences: AppPreferences = get(AppPreferences::class.java)) {
private fun AppScaffold(
appNavController: NavHostController,
mainNavStartRoute: String = BottomNavItem.Items[0].route,
preferences: AppPreferences = get(AppPreferences::class.java)
) {
val windowSize = rememberWindowSizeClass()
val navController = rememberNavController()
@@ -127,7 +139,8 @@ class MainActivity : MonetCompatActivity() {
navController = navController,
appBarTitle = appBarTitle,
appBarActions = appBarActions,
topBarScrollBehaviour = scrollBehavior
topBarScrollBehaviour = scrollBehavior,
mainNavStartRoute = mainNavStartRoute
)
}
}
@@ -180,7 +193,11 @@ class MainActivity : MonetCompatActivity() {
item: BottomNavItem
) {
appBarTitle.value = item.name
navController.navigate(item.route) {
navigateToRoute(navController, item.route)
}
private fun navigateToRoute(navController: NavController, route: String) {
navController.navigate(route) {
navController.graph.startDestinationRoute?.let { screenRoute ->
popUpTo(screenRoute) {
saveState = true
@@ -220,7 +237,8 @@ class MainActivity : MonetCompatActivity() {
navController: NavHostController,
topBarScrollBehaviour: TopAppBarScrollBehavior,
appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({})
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.Items[0].route
) {
if (windowSize == WindowSizeClass.Expanded) {
DualColumnMainContent(
@@ -228,14 +246,16 @@ class MainActivity : MonetCompatActivity() {
navController = navController,
appBarTitle = appBarTitle,
appBarActions = appBarActions,
topBarScrollBehaviour = topBarScrollBehaviour
topBarScrollBehaviour = topBarScrollBehaviour,
mainNavStartRoute = mainNavStartRoute
)
} else {
SingleColumnMainContent(
appNavController = appNavController,
navController = navController,
appBarTitle = appBarTitle,
appBarActions = appBarActions
appBarActions = appBarActions,
mainNavStartRoute = mainNavStartRoute
)
}
}
@@ -245,13 +265,15 @@ class MainActivity : MonetCompatActivity() {
appNavController: NavHostController,
navController: NavHostController,
appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({})
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.Items[0].route
) {
MainMediaView(
appNavController = appNavController,
navController = navController,
appBarTitle = appBarTitle,
appBarActions = appBarActions
appBarActions = appBarActions,
mainNavStartRoute = mainNavStartRoute
)
}
@@ -261,7 +283,8 @@ class MainActivity : MonetCompatActivity() {
navController: NavHostController,
topBarScrollBehaviour: TopAppBarScrollBehavior,
appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({})
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.Items[0].route
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
@@ -295,7 +318,8 @@ class MainActivity : MonetCompatActivity() {
appNavController = appNavController,
navController = navController,
appBarTitle = appBarTitle,
appBarActions = appBarActions
appBarActions = appBarActions,
mainNavStartRoute = mainNavStartRoute
)
}
}
@@ -306,7 +330,8 @@ class MainActivity : MonetCompatActivity() {
appNavController: NavHostController,
navController: NavHostController,
appBarTitle: MutableState<String>,
appBarActions: MutableState<RowScope.() -> Unit> = mutableStateOf({})
appBarActions: MutableState<RowScope.() -> Unit> = mutableStateOf({}),
mainNavStartRoute: String = BottomNavItem.Items[0].route
) {
Column {
val navBackStackEntry by navController.currentBackStackEntryAsState()
@@ -324,7 +349,8 @@ class MainActivity : MonetCompatActivity() {
appNavController = appNavController,
navController = navController,
appBarTitle = appBarTitle,
appBarActions = appBarActions
appBarActions = appBarActions,
startDestination = mainNavStartRoute
)
}
}
@@ -337,11 +363,12 @@ class MainActivity : MonetCompatActivity() {
@Composable
private fun MainNavigationRoutes(
startDestination: String = MainNavItem.MainView.route,
mainNavStartRoute: String = BottomNavItem.Items[0].route,
appNavController: NavHostController,
) {
NavHost(navController = appNavController, startDestination = startDestination) {
composable(MainNavItem.MainView.route) {
AppScaffold(appNavController = appNavController)
AppScaffold(appNavController = appNavController, mainNavStartRoute = mainNavStartRoute)
}
composable(
MainNavItem.DetailView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),

View File

@@ -2,6 +2,7 @@ package com.owenlejeune.tvtime
import android.app.Application
import com.facebook.stetho.Stetho
import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.di.modules.appModule
import com.owenlejeune.tvtime.di.modules.networkModule
import com.owenlejeune.tvtime.di.modules.preferencesModule
@@ -28,6 +29,8 @@ class TvTimeApplication: Application() {
)
}
MonetCompat.enablePaletteCompat()
if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this)
}

View File

@@ -98,7 +98,13 @@ class TmdbClient: KoinComponent {
private inner class V4Interceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val builder = chain.request().newBuilder()
builder.header("Authorization", "Bearer ${BuildConfig.TMDB_Api_v4Key}")
with(chain.request()) {
if (url.encodedPathSegments.contains("auth")) {
builder.header("Authorization", "Bearer ${BuildConfig.TMDB_Api_v4Key}")
} else {
builder.header("Authorization", "Bearer ${SessionManager.currentSession!!.accessToken}")
}
}
val locale = Locale.current
val languageCode = "${locale.language}-${locale.region}"

View File

@@ -23,4 +23,7 @@ interface AuthenticationApi {
@POST("authentication/token/validate_with_login")
suspend fun validateTokenWithLogin(@Body body: TokenValidationBody): Response<CreateTokenResponse>
@POST("authentication/session/convert/4")
suspend fun createSessionFromV4Token(@Body body: V4TokenBody): Response<CreateSessionResponse>
}

View File

@@ -27,4 +27,8 @@ class AuthenticationService {
suspend fun validateTokenWithLogin(body: TokenValidationBody): Response<CreateTokenResponse> {
return service.validateTokenWithLogin(body)
}
suspend fun createSessionFromV4Token(body: V4TokenBody): Response<CreateSessionResponse> {
return service.createSessionFromV4Token(body)
}
}

View File

@@ -0,0 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
class V4TokenBody(
@SerializedName("access_token") val accessToken: String
)

View File

@@ -59,9 +59,10 @@ fun MainNavGraph(
appNavController: NavHostController,
navController: NavHostController,
appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({})
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({}),
startDestination: String = BottomNavItem.Items[0].route
) {
NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) {
NavHost(navController = navController, startDestination = startDestination) {
composable(BottomNavItem.Movies.route) {
appBarActions.value = {}
MediaTab(appNavController = appNavController, mediaType = MediaViewType.MOVIE)

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -7,13 +8,11 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -52,7 +51,13 @@ fun AccountTab(
appBarTitle: MutableState<String>,
appBarActions: MutableState<@Composable (RowScope.() -> Unit)> = mutableStateOf({})
) {
if (appBarTitle.value.equals(stringResource(id = R.string.nav_account_title))) {
val lastSelectedOption = remember { mutableStateOf("") }
if (SessionManager.isV4SignInInProgress) {
v4SignInPart2(lastSelectedOption)
}
if (appBarTitle.value == stringResource(id = R.string.nav_account_title)) {
when (SessionManager.currentSession?.isAuthorized) {
false -> {
appBarTitle.value =
@@ -74,34 +79,30 @@ fun AccountTab(
}
}
val lastSelectedOption = remember { mutableStateOf("") }
appBarActions.value = {
AccountDropdownMenu(session = SessionManager.currentSession, lastSelectedOption = lastSelectedOption)
}
if (lastSelectedOption.value.isNotBlank() || lastSelectedOption.value.isBlank()) {
SessionManager.currentSession?.let { session ->
val tabs = if (session.isAuthorized) {
AccountTabNavItem.AuthorizedItems
} else {
AccountTabNavItem.GuestItems
SessionManager.currentSession?.let { session ->
val tabs = if (session.isAuthorized) {
AccountTabNavItem.AuthorizedItems
} else {
AccountTabNavItem.GuestItems
}
Column {
when(session.isAuthorized) {
true -> { AuthorizedSessionIcon() }
false -> { GuestSessionIcon() }
}
Column {
when(session.isAuthorized) {
true -> { AuthorizedSessionIcon() }
false -> { GuestSessionIcon() }
}
val pagerState = rememberPagerState()
ScrollableTabs(tabs = tabs, pagerState = pagerState)
AccountTabs(
appNavController = appNavController,
tabs = tabs,
pagerState = pagerState
)
}
val pagerState = rememberPagerState()
ScrollableTabs(tabs = tabs, pagerState = pagerState)
AccountTabs(
appNavController = appNavController,
tabs = tabs,
pagerState = pagerState
)
}
}
}
@@ -338,6 +339,11 @@ private fun NoSessionMenuItems(
text = { Text(text = stringResource(id = R.string.action_sign_in)) },
onClick = { showSignInDialog.value = true }
)
// val context = LocalContext.current
// DropdownMenuItem(
// text = { Text(text = stringResource(id = R.string.action_sign_in)) },
// onClick = { v4SignInPart1(context) }
// )
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_sign_in_as_guest)) },
@@ -348,6 +354,21 @@ private fun NoSessionMenuItems(
)
}
private fun v4SignInPart1(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
SessionManager.signInWithV4Part1(context)
}
}
private fun v4SignInPart2(lastSelectedOption: MutableState<String>) {
CoroutineScope(Dispatchers.IO).launch {
val signIn = SessionManager.signInWithV4Part2()
if (signIn) {
lastSelectedOption.value = NO_SESSION_SIGN_IN
}
}
}
@Composable
private fun GuestSessionIcon() {
val guestName = stringResource(id = R.string.account_name_guest)

View File

@@ -1,10 +1,18 @@
package com.owenlejeune.tvtime.utils
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountService
import com.owenlejeune.tvtime.api.tmdb.api.v3.AuthenticationService
import com.owenlejeune.tvtime.api.tmdb.api.v3.GuestSessionService
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.api.tmdb.api.v4.AuthenticationV4Service
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AuthAccessBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AuthDeleteBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AuthRequestBody
import com.owenlejeune.tvtime.preferences.AppPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -21,7 +29,10 @@ object SessionManager: KoinComponent {
val currentSession: Session?
get() = _currentSession
var isV4SignInInProgress: Boolean = false
private val authenticationService by lazy { TmdbClient().createAuthenticationService() }
private val authenticationV4Service by lazy { TmdbClient().createV4AuthenticationService() }
fun clearSession(onResponse: (isSuccessful: Boolean) -> Unit) {
currentSession?.let { session ->
@@ -39,6 +50,22 @@ object SessionManager: KoinComponent {
}
}
fun clearSessionV4(onResponse: (isSuccessful: Boolean) -> Unit) {
currentSession?.let { session ->
CoroutineScope(Dispatchers.IO).launch {
val deleteResponse = authenticationV4Service.deleteAccessToken(AuthDeleteBody(session.sessionId))
withContext(Dispatchers.Main) {
if (deleteResponse.isSuccessful) {
_currentSession = null
preferences.guestSessionId = ""
preferences.authorizedSessionId = ""
}
onResponse(deleteResponse.isSuccessful)
}
}
}
}
suspend fun initialize() {
if (preferences.guestSessionId.isNotEmpty()) {
val session = GuestSession()
@@ -91,7 +118,57 @@ object SessionManager: KoinComponent {
return false
}
abstract class Session(val sessionId: String, val isAuthorized: Boolean) {
suspend fun signInWithV4Part1(context: Context) {
isV4SignInInProgress = true
val service = AuthenticationV4Service()
val requestTokenResponse = service.createRequestToken(AuthRequestBody(redirect = ""))
if (requestTokenResponse.isSuccessful) {
requestTokenResponse.body()?.let { ctr ->
_currentSession = InProgressSession(ctr.requestToken)
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(
context.getString(R.string.tmdb_auth_url, ctr.requestToken)
)
)
context.startActivity(browserIntent)
}
}
}
suspend fun signInWithV4Part2(): Boolean {
if (isV4SignInInProgress && _currentSession is InProgressSession) {
val requestToken = _currentSession!!.sessionId
val authResponse = authenticationV4Service.createAccessToken(AuthAccessBody(requestToken))
if (authResponse.isSuccessful) {
authResponse.body()?.let { ar ->
if (ar.success) {
val sessionResponse = authenticationService.createSessionFromV4Token(V4TokenBody(ar.accessToken))
if (sessionResponse.isSuccessful) {
sessionResponse.body()?.let { sr ->
preferences.authorizedSessionId = sr.sessionId
preferences.guestSessionId = ""
_currentSession = AuthorizedSession(accessToken = ar.accessToken)
_currentSession?.initialize()
isV4SignInInProgress = false
return true
}
}
// preferences.authorizedSessionId = ar.accessToken
// preferences.guestSessionId = ""
// _currentSession = AuthorizedSession()
// _currentSession?.initialize()
// isV4SignInInProgress = false
// return true
}
}
}
}
return false
}
abstract class Session(val sessionId: String, val isAuthorized: Boolean, val accessToken: String = "") {
protected open var _ratedMovies: List<RatedMovie> = emptyList()
val ratedMovies: List<RatedMovie>
get() = _ratedMovies
@@ -187,7 +264,18 @@ object SessionManager: KoinComponent {
}
}
private class AuthorizedSession: Session(preferences.authorizedSessionId, true) {
private class InProgressSession(requestToken: String): Session(requestToken, false) {
override suspend fun initialize() {
// do nothing
}
override suspend fun refresh(changed: Array<Changed>) {
// do nothing
}
}
private class AuthorizedSession(accessToken: String = ""): Session(preferences.authorizedSessionId, true, accessToken) {
private val service by lazy { AccountService() }
override suspend fun initialize() {

View File

@@ -100,4 +100,8 @@
<string name="username_label">Username</string>
<string name="password_label">Password</string>
<string name="no_account_message">Don\'t have an account?</string>
<string name="tmdb_auth_url">"https://www.themoviedb.org/auth/access?request_token=%1$s"</string>
<!-- intent routes -->
<string name="intent_route_auth_return">tvtime.auth.return</string>
</resources>