From 007db6324e9989123d8fccd5411dbe07b56349c5 Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Mon, 20 Jun 2022 15:48:31 -0400 Subject: [PATCH] some work on v4 authentication --- app/src/main/AndroidManifest.xml | 8 +- .../com/owenlejeune/tvtime/MainActivity.kt | 55 ++++++++--- .../owenlejeune/tvtime/TvTimeApplication.kt | 3 + .../owenlejeune/tvtime/api/tmdb/TmdbClient.kt | 8 +- .../api/tmdb/api/v3/AuthenticationApi.kt | 3 + .../api/tmdb/api/v3/AuthenticationService.kt | 4 + .../api/tmdb/api/v3/model/V4TokenBody.kt | 7 ++ .../tvtime/ui/navigation/Routes.kt | 5 +- .../ui/screens/tabs/bottom/AccountTab.kt | 75 +++++++++------ .../tvtime/utils/SessionManager.kt | 92 ++++++++++++++++++- app/src/main/res/values/strings.xml | 4 + 11 files changed, 216 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/V4TokenBody.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3b256a4..32e4374 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,12 +18,16 @@ android:exported="true" android:theme="@style/Theme.TVTime" android:windowSoftInputMode="adjustResize"> - - + + + + + + diff --git a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt index f409337..0c7645b 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt @@ -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, - 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, - 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, - 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, - appBarActions: MutableState Unit> = mutableStateOf({}) + appBarActions: MutableState 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}}"), diff --git a/app/src/main/java/com/owenlejeune/tvtime/TvTimeApplication.kt b/app/src/main/java/com/owenlejeune/tvtime/TvTimeApplication.kt index 9e9bfc0..864b286 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/TvTimeApplication.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/TvTimeApplication.kt @@ -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) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt index 9efc94c..f91d4eb 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt @@ -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}" diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/AuthenticationApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/AuthenticationApi.kt index 09b766b..ce576aa 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/AuthenticationApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/AuthenticationApi.kt @@ -23,4 +23,7 @@ interface AuthenticationApi { @POST("authentication/token/validate_with_login") suspend fun validateTokenWithLogin(@Body body: TokenValidationBody): Response + + @POST("authentication/session/convert/4") + suspend fun createSessionFromV4Token(@Body body: V4TokenBody): Response } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/AuthenticationService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/AuthenticationService.kt index dd4f9c6..f24e76d 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/AuthenticationService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/AuthenticationService.kt @@ -27,4 +27,8 @@ class AuthenticationService { suspend fun validateTokenWithLogin(body: TokenValidationBody): Response { return service.validateTokenWithLogin(body) } + + suspend fun createSessionFromV4Token(body: V4TokenBody): Response { + return service.createSessionFromV4Token(body) + } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/V4TokenBody.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/V4TokenBody.kt new file mode 100644 index 0000000..a6e43bf --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/V4TokenBody.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt index 241f6ac..0049745 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt @@ -59,9 +59,10 @@ fun MainNavGraph( appNavController: NavHostController, navController: NavHostController, appBarTitle: MutableState, - 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) diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt index b00fb4f..50d21e5 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/AccountTab.kt @@ -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, 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) { + 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) diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt index b67553d..c928db4 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/SessionManager.kt @@ -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 = emptyList() val ratedMovies: List 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) { + // do nothing + } + + } + + private class AuthorizedSession(accessToken: String = ""): Session(preferences.authorizedSessionId, true, accessToken) { private val service by lazy { AccountService() } override suspend fun initialize() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 17f4fa1..0606b8b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,4 +100,8 @@ Username Password Don\'t have an account? + "https://www.themoviedb.org/auth/access?request_token=%1$s" + + + tvtime.auth.return \ No newline at end of file