diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 88ea3aa..3b25318 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,39 @@ + + + diff --git a/app/build.gradle b/app/build.gradle index cb6e54b..2b37ae8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,17 +48,18 @@ dependencies { implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-messaging' implementation 'com.google.firebase:firebase-auth' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.firebaseui:firebase-ui-auth:9.1.1' - // Pinned: play-services-auth 22.0.0 removed GoogleSignIn/GoogleSignInClient. - // Unpin once LoginActivity/SettingsActivity move to Credential Manager. - implementation 'com.google.android.gms:play-services-auth:21.3.0' + implementation 'com.google.android.gms:play-services-auth:22.0.0' + implementation 'androidx.credentials:credentials:1.6.0' + implementation 'androidx.credentials:credentials-play-services-auth:1.6.0' + implementation 'com.google.android.libraries.identity.googleid:googleid:1.2.0' implementation 'com.squareup.okhttp3:okhttp:5.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0' implementation 'com.google.code.gson:gson:2.14.0' implementation 'androidx.appcompat:appcompat:1.8.0' implementation 'androidx.core:core-ktx:1.19.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.2' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0' testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/java/com/cornellappdev/coursegrab/CourseDetailsActivity.kt b/app/src/main/java/com/cornellappdev/coursegrab/CourseDetailsActivity.kt index 05a863b..2627e0d 100644 --- a/app/src/main/java/com/cornellappdev/coursegrab/CourseDetailsActivity.kt +++ b/app/src/main/java/com/cornellappdev/coursegrab/CourseDetailsActivity.kt @@ -10,6 +10,7 @@ import android.widget.ImageView import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.cornellappdev.coursegrab.databinding.ActivityCourseDetailsBinding @@ -21,7 +22,6 @@ import com.cornellappdev.coursegrab.networking.Request import com.cornellappdev.coursegrab.networking.addTracking import com.cornellappdev.coursegrab.networking.removeTracking import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -65,7 +65,7 @@ class CourseDetailsActivity : AppCompatActivity() { fun addCourse(courseId: Int, context: Context) { val addTracking = Endpoint.addTracking(preferencesHelper.sessionToken.toString(), courseId) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { val typeToken = object : TypeToken>() {}.type val response = withContext(Dispatchers.IO) { Request.makeRequest>( @@ -87,7 +87,7 @@ class CourseDetailsActivity : AppCompatActivity() { val removeTracking = Endpoint.removeTracking(preferencesHelper.sessionToken.toString(), courseId) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { val typeToken = object : TypeToken>() {}.type val response = withContext(Dispatchers.IO) { Request.makeRequest>( diff --git a/app/src/main/java/com/cornellappdev/coursegrab/LoginActivity.kt b/app/src/main/java/com/cornellappdev/coursegrab/LoginActivity.kt index 1692912..cb4c6bb 100644 --- a/app/src/main/java/com/cornellappdev/coursegrab/LoginActivity.kt +++ b/app/src/main/java/com/cornellappdev/coursegrab/LoginActivity.kt @@ -1,9 +1,21 @@ package com.cornellappdev.coursegrab import android.content.Intent +import android.content.MutableContextWrapper import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity +import androidx.credentials.ClearCredentialStateRequest +import androidx.credentials.CredentialManager +import androidx.credentials.CredentialOption +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.credentials.exceptions.ClearCredentialException +import androidx.credentials.exceptions.GetCredentialCancellationException +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.NoCredentialException +import androidx.lifecycle.lifecycleScope import com.cornellappdev.coursegrab.databinding.ActivityLoginBinding import com.cornellappdev.coursegrab.models.ApiResponse import com.cornellappdev.coursegrab.models.Course @@ -14,22 +26,27 @@ import com.cornellappdev.coursegrab.networking.deviceToken import com.cornellappdev.coursegrab.networking.initializeSession import com.cornellappdev.coursegrab.networking.setNotification import com.cornellappdev.coursegrab.networking.updateSession -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInClient -import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.google.android.gms.common.api.ApiException +import com.google.android.libraries.identity.googleid.GetGoogleIdOption +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import com.google.android.material.snackbar.Snackbar import com.google.firebase.messaging.FirebaseMessaging import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class LoginActivity : AppCompatActivity() { private lateinit var binding: ActivityLoginBinding - private lateinit var googleSignInClient: GoogleSignInClient - private val RC_SIGN_IN = 10032 + + private val credentialManager: CredentialManager by lazy { + CredentialManager.create(this) + } + private val credentialContext: MutableContextWrapper by lazy { + MutableContextWrapper(this) + } private val preferencesHelper: PreferencesHelper by lazy { PreferencesHelper(this) @@ -44,41 +61,180 @@ class LoginActivity : AppCompatActivity() { val intent = Intent(this@LoginActivity, MainActivity::class.java) startActivity(intent) } else { - if (preferencesHelper.updateToken != null) - try { - val updateSession = - Endpoint.updateSession(preferencesHelper.updateToken.toString()) + val updateToken = preferencesHelper.updateToken + if (!updateToken.isNullOrBlank()) { + val updateSession = Endpoint.updateSession(updateToken) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { + val userSession = try { val typeToken = object : TypeToken>() {}.type - val userSession = withContext(Dispatchers.IO) { + withContext(Dispatchers.IO) { Request.makeRequest>( updateSession.okHttpRequest(), typeToken ) - }!!.data - - verifySession(userSession) + }?.data + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.d(TAG, "Could not resume previous session", e) + null } - } catch (e: ApiException) { - e.printStackTrace() + if (userSession != null) verifySession(userSession) } + } } - val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(getString(R.string.default_web_client_id)) - .requestEmail() + binding.signInButton.setOnClickListener { signIn() } + } + + private fun signIn() { + lifecycleScope.launch { + // Step 1: accounts already authorized for this app. Returning users get a + // streamlined sheet, or no prompt at all when there is exactly one match. + val googleIdOption = GetGoogleIdOption.Builder() + .setServerClientId(getString(R.string.default_web_client_id)) + .setFilterByAuthorizedAccounts(true) + .setAutoSelectEnabled(true) + .build() + + when (val authorized = requestCredential(googleIdOption)) { + is CredentialResult.Success -> { + handleSignIn(authorized.response) + return@launch + } + + CredentialResult.NoneAvailable -> Unit + CredentialResult.Cancelled, CredentialResult.Failed -> return@launch + } + + // Step 2: first-time (or de-authorized) users get the full account picker. + val signInWithGoogleOption = GetSignInWithGoogleOption + .Builder(getString(R.string.default_web_client_id)) + .build() + + when (val result = requestCredential(signInWithGoogleOption)) { + is CredentialResult.Success -> handleSignIn(result.response) + CredentialResult.NoneAvailable -> + showLoginError("No Google account found. Add one in system settings.") + + CredentialResult.Cancelled, CredentialResult.Failed -> Unit + } + } + } + + private suspend fun requestCredential(option: CredentialOption): CredentialResult { + val request = GetCredentialRequest.Builder() + .addCredentialOption(option) .build() - googleSignInClient = GoogleSignIn.getClient(this, gso) + // TODO: this runs in lifecycleScope, so rotating while the sheet is up cancels + // sign-in. Move the call into a ViewModel (viewModelScope) to survive recreation. + return try { + CredentialResult.Success( + credentialManager.getCredential(credentialContext, request) + ) + } catch (e: NoCredentialException) { + Log.d(TAG, "No matching credential for ${option::class.simpleName}", e) + CredentialResult.NoneAvailable + } catch (e: GetCredentialCancellationException) { + Log.d(TAG, "Sign-in cancelled by user", e) + CredentialResult.Cancelled + } catch (e: GetCredentialException) { + Log.e(TAG, "Credential Manager sign-in failed", e) + showLoginError("Sign-in failed. Please try again.") + CredentialResult.Failed + } + } - binding.signInButton.setOnClickListener { signIn() } + private sealed interface CredentialResult { + data class Success(val response: GetCredentialResponse) : CredentialResult + data object NoneAvailable : CredentialResult + data object Cancelled : CredentialResult + data object Failed : CredentialResult } - private fun signIn() { - val signInIntent = googleSignInClient.signInIntent - startActivityForResult(signInIntent, RC_SIGN_IN) + private fun handleSignIn(response: GetCredentialResponse) { + val credential = response.credential + when (credential) { + is CustomCredential -> { + // GetSignInWithGoogleOption returns the SIWG credential type, while the + // bottom-sheet GetGoogleIdOption flow returns the plain one. Accept either. + if (credential.type != GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL && + credential.type != GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_SIWG_CREDENTIAL + ) { + Log.e(TAG, "Unexpected credential type: ${credential.type}") + showLoginError("Sign-in failed. Please try again.") + return + } + } + + else -> { + Log.e(TAG, "Unexpected type of credential: ${credential::class}") + showLoginError("Sign-in failed. Please try again.") + return + } + } + + val googleCredential = try { + GoogleIdTokenCredential.createFrom(credential.data) + } catch (e: GoogleIdTokenParsingException) { + Log.e(TAG, "Failed to parse Google ID token", e) + showLoginError("Sign-in failed. Please try again.") + return + } + + if (!isAllowedAccount(googleCredential.id)) { + showLoginError("Please use a @cornell.edu account") + clearCredentialState() + return + } + + val initializeSession = Endpoint.initializeSession(googleCredential.idToken, null) + + lifecycleScope.launch { + val userSession = try { + val typeToken = object : TypeToken>() {}.type + withContext(Dispatchers.IO) { + Request.makeRequest>( + initializeSession.okHttpRequest(), + typeToken + ) + }?.data + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize session", e) + null + } + + if (userSession == null) { + showLoginError("Sign-in failed. Please try again.") + return@launch + } + + verifySession(userSession) + } + } + + private fun isAllowedAccount(email: String): Boolean = + email.endsWith("@cornell.edu") || + email == "appstoreappdev@gmail.com" || + email == "coursegrab.droid@gmail.com" + + private fun clearCredentialState() { + lifecycleScope.launch { + try { + credentialManager.clearCredentialState(ClearCredentialStateRequest()) + } catch (e: ClearCredentialException) { + Log.w(TAG, "Failed to clear credential state", e) + } + } + } + + private fun showLoginError(message: String) { + Snackbar.make(binding.loginRootView, message, Snackbar.LENGTH_LONG).show() } private fun sendRegistrationToServer(token: String?) { @@ -87,7 +243,7 @@ class LoginActivity : AppCompatActivity() { token.toString() ) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { val typeToken = object : TypeToken>() {}.type val response = withContext(Dispatchers.IO) { Request.makeRequest>( @@ -102,9 +258,10 @@ class LoginActivity : AppCompatActivity() { } private fun verifySession(userSession: UserSession) { - if (userSession.session_expiration.isEmpty() || - userSession.session_token.isEmpty() || - userSession.update_token.isEmpty() + + if (userSession.session_expiration.isNullOrBlank() || + userSession.session_token.isNullOrBlank() || + userSession.update_token.isNullOrBlank() ) return preferencesHelper.sessionToken = userSession.session_token @@ -115,7 +272,7 @@ class LoginActivity : AppCompatActivity() { sendRegistrationToServer(token) } - setNotificationsStatus(true) + setNotificationsStatus(preferencesHelper.mobileAlertSetting) val intent = Intent(this@LoginActivity, MainActivity::class.java) startActivity(intent) @@ -123,11 +280,11 @@ class LoginActivity : AppCompatActivity() { private fun setNotificationsStatus(enabled: Boolean) { val setNotifs = Endpoint.setNotification( - preferencesHelper.sessionToken.toString(), - if (enabled) "ANDROID" else "NONE" + accessToken = preferencesHelper.sessionToken.toString(), + notifSetting = if (enabled) "ANDROID" else "NONE" ) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { val typeToken = object : TypeToken>() {}.type withContext(Dispatchers.IO) { Request.makeRequest>( @@ -138,46 +295,7 @@ class LoginActivity : AppCompatActivity() { } } - public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...); - if (requestCode == RC_SIGN_IN) { - val task = GoogleSignIn.getSignedInAccountFromIntent(data) - try { - // Google Sign In was successful, authenticate with Firebase - val account = task.getResult(ApiException::class.java) - if (account?.email?.contains("@cornell.edu") == true || - account?.email == "appstoreappdev@gmail.com" || - account?.email == "coursegrab.droid@gmail.com" - ) { - val initializeSession = - Endpoint.initializeSession(account.idToken.toString(), null) - - CoroutineScope(Dispatchers.Main).launch { - val typeToken = object : TypeToken>() {}.type - val userSession = withContext(Dispatchers.IO) { - Request.makeRequest>( - initializeSession.okHttpRequest(), - typeToken - ) - }!!.data - - verifySession(userSession) - } - } else { - Snackbar.make( - binding.loginRootView, - "Please use a @cornell.edu account", - Snackbar.LENGTH_LONG - ).show() - googleSignInClient.signOut() - } - - - } catch (e: ApiException) { - Log.e("HELP", null, e) - } - } + companion object { + private const val TAG = "LoginActivity" } } diff --git a/app/src/main/java/com/cornellappdev/coursegrab/MainActivity.kt b/app/src/main/java/com/cornellappdev/coursegrab/MainActivity.kt index bb3cbfb..b559f86 100644 --- a/app/src/main/java/com/cornellappdev/coursegrab/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/coursegrab/MainActivity.kt @@ -21,6 +21,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.cornellappdev.coursegrab.databinding.ActivityMainBinding @@ -35,7 +36,6 @@ import com.cornellappdev.coursegrab.networking.getCourseByID import com.cornellappdev.coursegrab.networking.getTracking import com.cornellappdev.coursegrab.networking.removeTracking import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -140,22 +140,16 @@ class MainActivity : AppCompatActivity() { override fun onResume() { super.onResume() - refreshAwaiting() } - @Deprecated("Deprecated in Java") - override fun onBackPressed() { - super.onBackPressed() - } - private fun refreshAwaiting() { val listOpen = mutableListOf() val listAwaiting = mutableListOf() val getTracking = Endpoint.getTracking(preferencesHelper.sessionToken.toString()) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { val typeToken = object : TypeToken>() {}.type val courseList = withContext(Dispatchers.IO) { Request.makeRequest>( @@ -206,7 +200,7 @@ class MainActivity : AppCompatActivity() { private fun addCourse(courseId: Int, context: Context) { val addTracking = Endpoint.addTracking(preferencesHelper.sessionToken.toString(), courseId) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { val typeToken = object : TypeToken>() {}.type val response = withContext(Dispatchers.IO) { Request.makeRequest>( @@ -230,7 +224,7 @@ class MainActivity : AppCompatActivity() { val removeTracking = Endpoint.removeTracking(preferencesHelper.sessionToken.toString(), courseId) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { val typeToken = object : TypeToken>() {}.type val response = withContext(Dispatchers.IO) { Request.makeRequest>( @@ -254,7 +248,7 @@ class MainActivity : AppCompatActivity() { val editCourse = Endpoint.getCourseByID(preferencesHelper.sessionToken.toString(), courseId) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { val typeToken = object : TypeToken>() {}.type val course = withContext(Dispatchers.IO) { Request.makeRequest>( @@ -263,15 +257,10 @@ class MainActivity : AppCompatActivity() { ) }!!.data - -// context.setOnClickListener { val intent = Intent(context, CourseDetailsActivity::class.java).apply { putExtra("courseDetails", course) } context.startActivity(intent) -// } - - } } diff --git a/app/src/main/java/com/cornellappdev/coursegrab/SearchActivity.kt b/app/src/main/java/com/cornellappdev/coursegrab/SearchActivity.kt index 3468c2e..228bef2 100644 --- a/app/src/main/java/com/cornellappdev/coursegrab/SearchActivity.kt +++ b/app/src/main/java/com/cornellappdev/coursegrab/SearchActivity.kt @@ -13,6 +13,7 @@ import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.widget.doOnTextChanged +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.cornellappdev.coursegrab.databinding.ActivitySearchBinding @@ -23,7 +24,6 @@ import com.cornellappdev.coursegrab.networking.Endpoint import com.cornellappdev.coursegrab.networking.Request import com.cornellappdev.coursegrab.networking.searchCourses import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -66,7 +66,7 @@ class SearchActivity : AppCompatActivity() { this, R.drawable.ic_status_warning ) - ); + ) binding.noResultsTitle.text = getString(R.string.requires_longer_search) binding.noResultsSubtitle.text = getString(R.string.requires_longer_search_subtext) } @@ -78,7 +78,7 @@ class SearchActivity : AppCompatActivity() { private fun searchCourses(query: String) { val getTracking = Endpoint.searchCourses(preferencesHelper.sessionToken.toString(), query) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { val typeToken = object : TypeToken>() {}.type val courseList = withContext(Dispatchers.IO) { Request.makeRequest>( @@ -88,7 +88,7 @@ class SearchActivity : AppCompatActivity() { }!!.data.courses if (binding.editTextSearch.text.toString() != query) - return@launch; + return@launch // Results Courses Adapter searchViewManager = LinearLayoutManager(this@SearchActivity) @@ -108,7 +108,7 @@ class SearchActivity : AppCompatActivity() { this@SearchActivity, R.drawable.ic_status_closed ) - ); + ) binding.noResultsTitle.text = getString(R.string.no_courses_alert) binding.noResultsSubtitle.text = getString(R.string.no_results_alert_subtext_try_another) diff --git a/app/src/main/java/com/cornellappdev/coursegrab/SettingsActivity.kt b/app/src/main/java/com/cornellappdev/coursegrab/SettingsActivity.kt index 10355dc..31702fd 100644 --- a/app/src/main/java/com/cornellappdev/coursegrab/SettingsActivity.kt +++ b/app/src/main/java/com/cornellappdev/coursegrab/SettingsActivity.kt @@ -5,22 +5,23 @@ import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.util.Log import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.net.toUri +import androidx.credentials.ClearCredentialStateRequest +import androidx.credentials.CredentialManager +import androidx.credentials.exceptions.ClearCredentialException +import androidx.lifecycle.lifecycleScope import com.cornellappdev.coursegrab.databinding.ActivitySettingsBinding import com.cornellappdev.coursegrab.models.ApiResponse import com.cornellappdev.coursegrab.models.Course import com.cornellappdev.coursegrab.networking.Endpoint import com.cornellappdev.coursegrab.networking.Request import com.cornellappdev.coursegrab.networking.setNotification -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInClient -import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.firebase.messaging.FirebaseMessaging import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -28,7 +29,10 @@ import kotlinx.coroutines.withContext class SettingsActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsBinding - private lateinit var googleSignInClient: GoogleSignInClient + + private val credentialManager: CredentialManager by lazy { + CredentialManager.create(this) + } private val preferencesHelper: PreferencesHelper by lazy { PreferencesHelper(this) @@ -67,13 +71,6 @@ class SettingsActivity : AppCompatActivity() { } } - val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(getString(R.string.default_web_client_id)) - .requestEmail() - .build() - - googleSignInClient = GoogleSignIn.getClient(this, gso) - binding.classRoster.setOnClickListener { val browserIntent = Intent(Intent.ACTION_VIEW, "https://classes.cornell.edu/".toUri()) @@ -115,7 +112,7 @@ class SettingsActivity : AppCompatActivity() { if (enabled) "ANDROID" else "NONE" ) - CoroutineScope(Dispatchers.Main).launch { + lifecycleScope.launch { val typeToken = object : TypeToken>() {}.type val response = withContext(Dispatchers.IO) { Request.makeRequest>( @@ -134,10 +131,20 @@ class SettingsActivity : AppCompatActivity() { } private fun signOut() { - // Google sign out - googleSignInClient.signOut().addOnCompleteListener(this) { - val mStartActivity = Intent(this@SettingsActivity, LoginActivity::class.java) - startActivity(mStartActivity) + lifecycleScope.launch { + // Clears the Credential Manager provider state so the account picker + // reappears on the next sign-in attempt. + try { + credentialManager.clearCredentialState(ClearCredentialStateRequest()) + } catch (e: ClearCredentialException) { + Log.w(TAG, "Failed to clear credential state", e) + } + + startActivity(Intent(this@SettingsActivity, LoginActivity::class.java)) } } + + companion object { + private const val TAG = "SettingsActivity" + } } diff --git a/app/src/main/java/com/cornellappdev/coursegrab/models/UserSession.kt b/app/src/main/java/com/cornellappdev/coursegrab/models/UserSession.kt index 7f6efe6..6f65d12 100644 --- a/app/src/main/java/com/cornellappdev/coursegrab/models/UserSession.kt +++ b/app/src/main/java/com/cornellappdev/coursegrab/models/UserSession.kt @@ -1,7 +1,7 @@ package com.cornellappdev.coursegrab.models class UserSession( - val session_token: String, - val update_token: String, - val session_expiration: String + val session_token: String?, + val update_token: String?, + val session_expiration: String? ) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/coursegrab/networking/Request.kt b/app/src/main/java/com/cornellappdev/coursegrab/networking/Request.kt index 35e87df..dc8f7e0 100644 --- a/app/src/main/java/com/cornellappdev/coursegrab/networking/Request.kt +++ b/app/src/main/java/com/cornellappdev/coursegrab/networking/Request.kt @@ -1,6 +1,5 @@ package com.cornellappdev.coursegrab.networking -import android.util.Log import com.google.gson.Gson import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.Call @@ -19,7 +18,7 @@ object Request { suspend inline fun makeRequest(request: okhttp3.Request, typeToken: Type): T? { val response = httpClient.newCall(request).await() val responseBody = response.body - val responseBodyString = responseBody?.string() ?: "" + val responseBodyString = responseBody.string() val responseBodyJSON = Gson() return responseBodyJSON.fromJson(responseBodyString, typeToken) @@ -40,7 +39,7 @@ object Request { } override fun onFailure(call: Call, e: IOException) { - // Don't bother with resuming the continuation if it is already cancelled. + // Don't bother with resuming the continuation if it is already canceled. if (recordStackTrace != null) { recordStackTrace.initCause(e) continuation.resumeWithException(recordStackTrace) @@ -53,7 +52,7 @@ object Request { continuation.invokeOnCancellation { try { cancel() - } catch (ex: Throwable) { + } catch (_: Throwable) { //Ignore cancel exception } } diff --git a/build.gradle b/build.gradle index 64de89f..7e799d1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.21' + ext.kotlin_version = '2.4.10' repositories { google() mavenCentral() @@ -9,7 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:9.3.2' classpath 'com.google.gms:google-services:4.5.0' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.10' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.8' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files