From 9463a47565c3f0f6da26bfd589985f2120ad70b4 Mon Sep 17 00:00:00 2001 From: Vikas Sharma Date: Tue, 30 Sep 2025 23:38:56 +0530 Subject: [PATCH 1/7] feat: Add kotlinx-coroutines-play-services dependency This commit introduces the `kotlinx-coroutines-play-services` dependency to the project. This library provides integration with Google Play Services Tasks API, enabling the use of coroutines with Play Services. The dependency is added to `gradle/libs.versions.toml` and `core/build.gradle.kts`. --- core/build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index bd63b10..568c854 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -81,7 +81,7 @@ dependencies { implementation(libs.androidx.credentials.play.services.auth) implementation(libs.googleid) implementation(libs.play.services.auth) - + implementation(libs.kotlinx.coroutines.play.services) /*google drive api*/ implementation(libs.google.api.client.android) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9b2c8e..5aa9b91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ coreKtx = "1.10.1" junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1" +kotlinxCoroutinesPlayServices = "1.10.2" kotlinxSerializationJson = "1.9.0" lifecycleRuntimeKtx = "2.6.1" activityCompose = "1.8.0" @@ -70,6 +71,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutinesPlayServices" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } From 80d3b4234c7f534c97e38e43c85fb83014c9b4af Mon Sep 17 00:00:00 2001 From: Vikas Sharma Date: Tue, 30 Sep 2025 23:41:52 +0530 Subject: [PATCH 2/7] feat: Implement token refresh and expiration handling This commit introduces functionality to refresh Google Drive access tokens and manage their expiration within the `AuthManager`. Key changes: - A new `getNewToken()` suspend function is added to `AuthManager`. This function requests a new authorization token from Google Identity Services and stores the new access token and its expiration time (5 minutes from now) in `DataStorePref`. - The `getDrivePermission` function now also saves the token's expiration time (5 minutes from issue) to `DataStorePref` upon successful authorization. - The `AuthManager` constructor now accepts a `Context` parameter, which is used by the new `getNewToken()` function. - Logging has been added to trace the token refresh process. --- .../open/piccollab/core/auth/AuthManager.kt | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/com/app/open/piccollab/core/auth/AuthManager.kt b/core/src/main/java/com/app/open/piccollab/core/auth/AuthManager.kt index 79ff127..7928afb 100644 --- a/core/src/main/java/com/app/open/piccollab/core/auth/AuthManager.kt +++ b/core/src/main/java/com/app/open/piccollab/core/auth/AuthManager.kt @@ -28,12 +28,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await import java.security.MessageDigest +import java.util.Date import java.util.UUID private const val TAG = "AuthManager" -class AuthManager(private val dataStorePref: DataStorePref) { +class AuthManager(private val context: Context, private val dataStorePref: DataStorePref) { fun startGoogleAuthentication(context: Context): Flow { return flow { try { @@ -100,7 +102,6 @@ class AuthManager(private val dataStorePref: DataStorePref) { Identity.getAuthorizationClient(activity) .authorize(authorizationRequest) .addOnSuccessListener { authorizationResult -> - if (authorizationResult.hasResolution()) { val pendingIntent = authorizationResult.pendingIntent // Access needs to be granted by the user @@ -128,28 +129,50 @@ class AuthManager(private val dataStorePref: DataStorePref) { TAG, "getDrivePermission: result authorizationResult: $authorizationResult" ) - RestApiManager.accessToken = authorizationResult.accessToken CoroutineScope(Dispatchers.IO).launch { dataStorePref.setAccessToken( authorizationResult.accessToken ?: "" ) - + dataStorePref.setExpiresIn(Date().time + 300_000) delay(1000) dataStorePref.getAccessToken().collect { accessToken -> Log.d(TAG, "getDrivePermission: accessTokenSaved: $accessToken") } - - - } //how to call this suspend method - // Access was previously granted, continue with user action - /*saveToDriveAppFolder(authorizationResult);*/ + } } } .addOnFailureListener { e -> Log.e(TAG, "Failed to authorize", e) } } - fun logout(activity: Activity){ + + suspend fun getNewToken() { + Log.d(TAG, "getNewToken: ") + + val requestedScopes = listOf(Scope(DriveScopes.DRIVE_FILE)) + val authorizationRequest = AuthorizationRequest.Builder() + .setRequestedScopes(requestedScopes) + .build() + + try { + // Use await() to suspend until the Task is complete + val result = Identity.getAuthorizationClient(context) + .authorize(authorizationRequest) + .await() + + Log.d(TAG, "getNewToken: result: $result") + + dataStorePref.setAccessToken(result.accessToken ?: "") + dataStorePref.setExpiresIn(Date().time + 300_000) // 5 minutes from now + + } catch (e: Exception) { + Log.e(TAG, "Error getting new token", e) + // handle errors properly + } + } + + + fun logout(activity: Activity) { val signInClient = Identity.getSignInClient(activity) // This clears the account and forces re-consent next time signInClient.signOut() From 8a74f44acdd1aca0d2f223a4d304218f735da408 Mon Sep 17 00:00:00 2001 From: Vikas Sharma Date: Tue, 30 Sep 2025 23:42:08 +0530 Subject: [PATCH 3/7] refactor: Make getExpiresIn a suspend function This commit changes the `getExpiresIn` function in `DataStorePref` from returning a `Flow` to a suspend function returning a `Long`. The implementation now uses `.first()` on the mapped `DataStore` data flow to retrieve the `EXPIRES_IN_KEY` value directly, simplifying the call site by removing the need to collect the Flow. --- .../com/app/open/piccollab/core/db/datastore/DataStorePref.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/app/open/piccollab/core/db/datastore/DataStorePref.kt b/core/src/main/java/com/app/open/piccollab/core/db/datastore/DataStorePref.kt index bc26705..59e37f4 100644 --- a/core/src/main/java/com/app/open/piccollab/core/db/datastore/DataStorePref.kt +++ b/core/src/main/java/com/app/open/piccollab/core/db/datastore/DataStorePref.kt @@ -35,10 +35,10 @@ class DataStorePref(private val context: Context) { } } - fun getExpiresIn(): Flow { + suspend fun getExpiresIn(): Long { return context.dataStore.data.map { preferences -> preferences[EXPIRES_IN_KEY] ?: 0L - } + }.first() } suspend fun setExpiresIn(expiresIn: Long) { From 41ff8070a5016ce9918739372268a57a5db2da5c Mon Sep 17 00:00:00 2001 From: Vikas Sharma Date: Tue, 30 Sep 2025 23:43:09 +0530 Subject: [PATCH 4/7] refactor: Pass ApplicationContext to AuthManager This commit modifies the `NetworkModule` to provide the `ApplicationContext` to the `AuthManager`. The `providesAuthManager` function in `NetworkModule.kt` now accepts `@ApplicationContext context: Context` as a parameter and passes it to the `AuthManager` constructor. This change allows `AuthManager` to access application-level context if needed. --- .../open/piccollab/core/network/module/NetworkModule.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/app/open/piccollab/core/network/module/NetworkModule.kt b/core/src/main/java/com/app/open/piccollab/core/network/module/NetworkModule.kt index 4f6b75f..85f1fd0 100644 --- a/core/src/main/java/com/app/open/piccollab/core/network/module/NetworkModule.kt +++ b/core/src/main/java/com/app/open/piccollab/core/network/module/NetworkModule.kt @@ -1,11 +1,13 @@ package com.app.open.piccollab.core.network.module +import android.content.Context import com.app.open.piccollab.core.auth.AuthManager import com.app.open.piccollab.core.db.datastore.DataStorePref import com.app.open.piccollab.core.network.module.apiservices.DriveApiService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -38,7 +40,10 @@ class NetworkModule { @Singleton @Provides - fun providesAuthManager(dataStorePref: DataStorePref): AuthManager{ - return AuthManager(dataStorePref) + fun providesAuthManager( + @ApplicationContext context: Context, + dataStorePref: DataStorePref + ): AuthManager { + return AuthManager(context, dataStorePref) } } \ No newline at end of file From 6ea721ddcde8229ecfeac21a4915e536aada97dc Mon Sep 17 00:00:00 2001 From: Vikas Sharma Date: Tue, 30 Sep 2025 23:44:20 +0530 Subject: [PATCH 5/7] refactor: Improve DriveManager token handling and error management This commit refactors `DriveManager` to enhance token handling and error management when interacting with the Google Drive API. **Key changes:** - **Token Refresh Logic:** - `getDriveService` now checks the access token's expiry time before making API calls. - If the token is expired, it requests a new token via `authManager.getNewToken()`. - This ensures that API calls are always made with a valid access token. - **Error Handling:** - `getDriveService`, `createFolder`, `rootFolderId`, and `getEventFolderFromDrive` now include `try-catch` blocks to handle potential exceptions during API interactions. - Methods that return a value now return `null` or an empty string/list in case of an error, providing more robust error propagation. - `getDriveService` can now return `null` if the Drive service cannot be initialized. - **Dependency Injection:** - `DriveManager` now accepts an `AuthManager` instance in its constructor to facilitate token refresh. - **Suspend Functions:** - `createFolder`, `rootFolderId`, and `getEventFolderFromDrive` are now `suspend` functions to align with the asynchronous nature of network operations and token refresh. - **Logging:** - Added more detailed logging for token status, API calls, and errors. - **Removed Unused Code:** - Commented out `cancelDriveManagerCoroutine` as it's not currently used. - Removed unused `ROOT_FOLDER_KEY` import. - **Null Safety:** - `createFolder` now handles potential `null` responses from the Drive API more safely. These changes improve the reliability and robustness of Drive interactions by proactively managing token lifecycles and gracefully handling API errors. --- .../core/network/module/drive/DriveManager.kt | 123 ++++++++++++------ 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/core/src/main/java/com/app/open/piccollab/core/network/module/drive/DriveManager.kt b/core/src/main/java/com/app/open/piccollab/core/network/module/drive/DriveManager.kt index 1db7143..804142a 100644 --- a/core/src/main/java/com/app/open/piccollab/core/network/module/drive/DriveManager.kt +++ b/core/src/main/java/com/app/open/piccollab/core/network/module/drive/DriveManager.kt @@ -2,8 +2,8 @@ package com.app.open.piccollab.core.network.module.drive import android.content.Context import android.util.Log +import com.app.open.piccollab.core.auth.AuthManager import com.app.open.piccollab.core.db.datastore.DataStorePref -import com.app.open.piccollab.core.db.datastore.ROOT_FOLDER_KEY import com.app.open.piccollab.core.db.room.entities.EventFolder import com.app.open.piccollab.core.db.room.repositories.DEFAULT_PROJECT_FOLDER_NAME import com.app.open.piccollab.core.models.event.NewEventItem @@ -17,7 +17,6 @@ import com.google.auth.oauth2.GoogleCredentials import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex @@ -27,7 +26,11 @@ import java.util.Date private const val TAG = "DriveManager" -class DriveManager(private val context: Context, private val dataStorePref: DataStorePref) { +class DriveManager( + private val context: Context, + private val dataStorePref: DataStorePref, + private val authManager: AuthManager +) { val tokenMutex = Mutex() @Volatile @@ -46,22 +49,27 @@ class DriveManager(private val context: Context, private val dataStorePref: Data coroutineScope.launch { dataStorePref.getAccessToken().collect { token -> tokenMutex.withLock { + Log.d(TAG, "token: $token") _token = token } } } } - fun createFolder(eventItem: NewEventItem, projectFolderId: String? = null): String { + suspend fun createFolder(eventItem: NewEventItem, projectFolderId: String? = null): String { + Log.d( + TAG, + "createFolder() called with: eventItem = $eventItem, projectFolderId = $projectFolderId" + ) val file = File() file.mimeType = "application/vnd.google-apps.folder" file.name = eventItem.eventName file.description = eventItem.eventDescription file.parents = listOf(projectFolderId) try { - val outFile = getDriveService().files().create(file).execute() - Log.d(TAG, "createFolder() returned: id: ${outFile.id}") - return outFile.id + val outFile = getDriveService()?.files()?.create(file)?.execute() + Log.d(TAG, "createFolder() returned: id: ${outFile?.id}") + return outFile?.id ?: "" } catch (e: Exception) { Log.w(TAG, "createFolder: ", e) return "" @@ -69,50 +77,85 @@ class DriveManager(private val context: Context, private val dataStorePref: Data } - private fun getDriveService(): Drive { + private suspend fun getDriveService(): Drive? { Log.d(TAG, "getDriveService() called") - val accessToken = AccessToken(token, Date(Date().time + 300_000)) - val googleCredential = GoogleCredentials.create(accessToken) - - val requestInitializer = HttpCredentialsAdapter(googleCredential) - val driveService: Drive = Drive.Builder( - NetHttpTransport(), - GsonFactory.getDefaultInstance(), - requestInitializer - ).setApplicationName(context.packageName).build() + val driveService: Drive = try { + /*checking accessToken expiry*/ + val expiryTime = dataStorePref.getExpiresIn() + val currentTime = Date().time + + Log.d(TAG, "getDriveService: expiryTime: $expiryTime") + Log.d(TAG, "getDriveService: currentTime: $currentTime") + if (currentTime > expiryTime) { + Log.d(TAG, "getDriveService: getting new token") + authManager.getNewToken() + Log.d(TAG, "getDriveService: new token received") + } + + + val accessToken = AccessToken(token, null) + val googleCredential = GoogleCredentials.create(accessToken) + + val requestInitializer = HttpCredentialsAdapter(googleCredential) + Drive.Builder( + NetHttpTransport(), GsonFactory.getDefaultInstance(), requestInitializer + ).setApplicationName(context.packageName).build() + } catch (e: Exception) { + Log.d(TAG, "getDriveService: ", e) + return null + } return driveService } +/* fun cancelDriveManagerCoroutine() { coroutineScope.cancel() } - - fun rootFolderId(): String? { - val fileList = getDriveService().files().list().setQ( - "mimeType='application/vnd.google-apps.folder'" - ) - .setFields("files(id, name, createdTime, mimeType)") - .execute().files - Log.d(TAG, "rootFolderId: files: $fileList") - val rootFolder = fileList.find { file -> file.name == DEFAULT_PROJECT_FOLDER_NAME } - val rootFolderId = rootFolder?.id - Log.d(TAG, "rootFolderId() returned: $rootFolderId") - return rootFolderId +*/ + + suspend fun rootFolderId(): String? { + Log.d(TAG, "rootFolderId() called") + val fileList = try { + getDriveService()?.files()?.list()?.setQ( + "mimeType='application/vnd.google-apps.folder'" + )?.setFields("files(id, name, createdTime, mimeType)")?.execute()?.files + } catch (e: Exception) { + Log.w(TAG, "rootFolderId: ", e) + return null + } + if (fileList != null) { + Log.d(TAG, "rootFolderId: files: $fileList") + val rootFolder = fileList.find { file -> file.name == DEFAULT_PROJECT_FOLDER_NAME } + val rootFolderId = rootFolder?.id + Log.d(TAG, "rootFolderId() returned: $rootFolderId") + return rootFolderId + } else { + return null + } } - fun getEventFolderFromDrive(rootProjectId :String): List { + suspend fun getEventFolderFromDrive(rootProjectId: String): List { Log.d(TAG, "getEventFolderFromDrive: ") - val fileList = getDriveService().files().list().setQ( - "'$rootProjectId' in parents and mimeType='application/vnd.google-apps.folder'" - ) - .setFields("files(id, name, createdTime, mimeType, description)") - .execute().files - val eventFolderList = mutableListOf() - fileList.forEach { file -> - eventFolderList.add(EventFolder(file.id, file.name, file.description)) + val driveService = getDriveService() + if (driveService != null) { + + val fileList = try { + driveService.files().list().setQ( + "'$rootProjectId' in parents and mimeType='application/vnd.google-apps.folder'" + ).setFields("files(id, name, createdTime, mimeType, description)").execute().files + } catch (e: Exception) { + Log.w(TAG, "getEventFolderFromDrive: ", e) + return emptyList() + } + val eventFolderList = mutableListOf() + fileList.forEach { file -> + eventFolderList.add(EventFolder(file.id, file.name, file.description)) + } + Log.d(TAG, "getEventFolderFromDrive: event folder list $eventFolderList") + return eventFolderList + } else { + return emptyList() } - Log.d(TAG, "getEventFolderFromDrive: event folder list $eventFolderList") - return eventFolderList } } \ No newline at end of file From 4fa6e1b63dc5e43e40667bde493bff32d3dc842c Mon Sep 17 00:00:00 2001 From: Vikas Sharma Date: Tue, 30 Sep 2025 23:44:52 +0530 Subject: [PATCH 6/7] refactor: Inject AuthManager into DriveManager This commit updates the `DriveManager` to accept an `AuthManager` instance as a dependency. The `providesDriveManager` function in the Dagger module (`Provider.kt`) has been modified to inject `AuthManager` into `DriveManager` during its construction. This change likely facilitates authentication-related operations within `DriveManager`. --- .../main/java/com/app/open/piccollab/core/di/Provider.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/app/open/piccollab/core/di/Provider.kt b/core/src/main/java/com/app/open/piccollab/core/di/Provider.kt index 4867210..06c4da4 100644 --- a/core/src/main/java/com/app/open/piccollab/core/di/Provider.kt +++ b/core/src/main/java/com/app/open/piccollab/core/di/Provider.kt @@ -1,6 +1,7 @@ package com.app.open.piccollab.core.di import android.content.Context +import com.app.open.piccollab.core.auth.AuthManager import com.app.open.piccollab.core.db.datastore.DataStorePref import com.app.open.piccollab.core.db.room.database.UserDatabase import com.app.open.piccollab.core.network.module.drive.DriveManager @@ -32,8 +33,12 @@ class Provider { @Singleton @Provides - fun providesDriveManager(@ApplicationContext context: Context, dataStorePref: DataStorePref) = - DriveManager(context, dataStorePref) + fun providesDriveManager( + @ApplicationContext context: Context, + dataStorePref: DataStorePref, + authManager: AuthManager + ) = + DriveManager(context, dataStorePref, authManager) } \ No newline at end of file From 215dfe6229e35cfdd27cc8125785238d31dedf9d Mon Sep 17 00:00:00 2001 From: Vikas Sharma Date: Tue, 30 Sep 2025 23:45:06 +0530 Subject: [PATCH 7/7] feat: Improve project folder retrieval logic This commit enhances the `EventFolderRepository.getOrCreateProjectFolder` function. The function now checks if the `rootFolderIfFromPref` retrieved from `dataStorePref` is null or blank. Previously, it only checked for null. Additionally, debug logs have been added to trace the different execution paths within the function, such as retrieving the folder ID from local storage, Google Drive, or creating a new root folder. --- .../core/db/room/repositories/EventFolderRepository.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/app/open/piccollab/core/db/room/repositories/EventFolderRepository.kt b/core/src/main/java/com/app/open/piccollab/core/db/room/repositories/EventFolderRepository.kt index 9dfc01c..9d1497c 100644 --- a/core/src/main/java/com/app/open/piccollab/core/db/room/repositories/EventFolderRepository.kt +++ b/core/src/main/java/com/app/open/piccollab/core/db/room/repositories/EventFolderRepository.kt @@ -28,15 +28,18 @@ class EventFolderRepository( suspend fun getOrCreateProjectFolder(): String { /*getting root folder id from local data store*/ + Log.d(TAG, "getOrCreateProjectFolder: getting root folder id from local data store") val rootFolderIfFromPref = dataStorePref.getDataValue(ROOT_FOLDER_KEY) - if (rootFolderIfFromPref == null) { + if (rootFolderIfFromPref.isNullOrBlank()) { /*getting root folder if from google drive*/ + Log.d(TAG, "getOrCreateProjectFolder: getting root folder if from google drive") val rootFolderId = driveManager.rootFolderId() if (rootFolderId != null) { Log.d(TAG, "getOrCreateProjectFolder: from drive") return rootFolderId } else { /*creating new root folder to google drive*/ + Log.d(TAG, "getOrCreateProjectFolder: creating new root folder to google drive") val rootFolderItem = NewEventItem( eventName = DEFAULT_PROJECT_FOLDER_NAME, eventDescription = DEFAULT_PROJECT_FOLDER_DESCRIPTION