Conversation
…rieval for Google Drive
There was a problem hiding this comment.
Pull request overview
This PR adds Google Drive integration to the multiplatform application, enabling cloud storage and synchronization capabilities. The implementation includes platform-specific clients for Android and JVM (desktop), with stub implementations for Web and a missing iOS implementation.
Changes:
- Added a multiplatform
CloudDriveClientinterface with platform-specific implementations - Integrated Google Drive SDK dependencies for Android and JVM targets
- Updated SettingsScreen UI to support Google Drive authorization and account management
- Updated application namespace and package identifiers to
io.github.smiling_pixel.mark_day
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| gradle/libs.versions.toml | Added Google Drive and OAuth client library dependencies |
| composeApp/src/commonMain/kotlin/io/github/smiling_pixel/client/CloudDriveClient.kt | Defined common CloudDriveClient interface with DriveFile and UserInfo models |
| composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt | Android implementation using Google Play Services Auth and Drive API |
| composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleSignInHelper.kt | Helper object for managing Google Sign-In flow via ActivityResult API |
| composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt | JVM implementation using OAuth2 with browser-based authorization |
| composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt | Stub implementation with NotImplementedError responses |
| composeApp/src/commonMain/kotlin/io/github/smiling_pixel/SettingsScreen.kt | Added Google Drive authorization UI with sign-in/sign-out functionality |
| composeApp/src/androidMain/kotlin/io/github/smiling_pixel/MainActivity.kt | Registered ActivityResultLauncher for Google Sign-In flow |
| composeApp/src/androidMain/AndroidManifest.xml | Updated activity name to use fully qualified package |
| composeApp/build.gradle.kts | Added Google Drive dependencies and updated namespace/applicationId |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleSignInHelper.kt
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleSignInHelper.kt
Outdated
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleSignInHelper.kt
Outdated
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
| expect fun getCloudDriveClient(): CloudDriveClient | ||
|
|
There was a problem hiding this comment.
The iOS target is missing an implementation of the getCloudDriveClient() expected function. The common code defines expect fun getCloudDriveClient(): CloudDriveClient, and while Android, JVM, and wasmJs provide actual implementations, iOS does not. This will cause a compilation error for iOS targets. You need to add an iOS-specific implementation file similar to the wasmJs stub implementation.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 28 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Show resolved
Hide resolved
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/wasmJsMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
| package io.github.smiling_pixel.client | ||
|
|
||
| /** | ||
| * Implementation of [CloudDriveClient] for Google Drive on Web. | ||
| * CURRENTLY NOT IMPLEMENTED. | ||
| */ | ||
| class GoogleDriveClient : CloudDriveClient { | ||
|
|
||
| override suspend fun listFiles(parentId: String?): List<DriveFile> { | ||
| throw NotImplementedError("Google Drive is not supported on Web target yet.") | ||
| } | ||
|
|
||
| override suspend fun createFile(name: String, content: ByteArray, mimeType: String, parentId: String?): DriveFile { | ||
| throw NotImplementedError("Google Drive is not supported on Web target yet.") | ||
| } | ||
|
|
||
| override suspend fun createFolder(name: String, parentId: String?): DriveFile { | ||
| throw NotImplementedError("Google Drive is not supported on Web target yet.") | ||
| } | ||
|
|
||
| override suspend fun deleteFile(fileId: String) { | ||
| throw NotImplementedError("Google Drive is not supported on Web target yet.") | ||
| } | ||
|
|
||
| override suspend fun downloadFile(fileId: String): ByteArray { | ||
| throw NotImplementedError("Google Drive is not supported on Web target yet.") | ||
| } | ||
|
|
||
| override suspend fun updateFile(fileId: String, content: ByteArray): DriveFile { | ||
| throw NotImplementedError("Google Drive is not supported on Web target yet.") | ||
| } | ||
|
|
||
| override suspend fun isAuthorized(): Boolean { | ||
| return false | ||
| } | ||
|
|
||
| override suspend fun authorize(): Boolean { | ||
| throw NotImplementedError("Google Drive is not supported on Web target yet.") | ||
| } | ||
|
|
||
| override suspend fun signOut() { | ||
| throw NotImplementedError("Google Drive is not supported on Web target yet.") | ||
| } | ||
|
|
||
| override suspend fun getUserInfo(): UserInfo? { | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| actual fun getCloudDriveClient(): CloudDriveClient = GoogleDriveClient() |
There was a problem hiding this comment.
The wasmJsMain implementation is missing a corresponding iOS implementation. All other platform-specific implementations (Android, JVM, WasmJS) have been provided, but there is no GoogleDriveClient.kt file in the iosMain source set. This will cause compilation errors for iOS targets since the expect fun getCloudDriveClient(): CloudDriveClient declaration in commonMain requires an actual implementation for all configured targets (including iosArm64 and iosSimulatorArm64 as defined in build.gradle.kts lines 27-28).
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/MainActivity.kt
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
… optimize authorization flow
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 12 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleSignInHelper.kt
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Show resolved
Hide resolved
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Show resolved
Hide resolved
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Show resolved
Hide resolved
composeApp/src/commonMain/kotlin/io/github/smiling_pixel/SettingsScreen.kt
Outdated
Show resolved
Hide resolved
| // Sanitize folderId to prevent injection | ||
| val sanitizedFolderId = folderId.replace("'", "\\'") | ||
| val query = "'$sanitizedFolderId' in parents and trashed = false" |
There was a problem hiding this comment.
The sanitization approach using replace("'", "\\'") is insufficient and potentially incorrect for Google Drive API queries. The backslash itself may need escaping depending on the context. Additionally, this doesn't protect against other special characters. The Google Drive API query language has its own escaping rules. Consider using proper query building methods or ensuring the escaping matches Google's documented requirements. Note: The same issue exists in the Android implementation at line 186.
composeApp/src/commonMain/kotlin/io/github/smiling_pixel/SettingsScreen.kt
Outdated
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Refactors checkAuthStatus to use rememberUpdatedState to guarantee that the callback executed by LifecycleEventEffect always captures the most recent composition scope and state references.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 13 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/MainActivity.kt
Outdated
Show resolved
Hide resolved
| // Retrieve current metadata to keep name/mimeType if needed, or just update content | ||
| // Creating a new File object with empty metadata to only update content is possible, | ||
| // but often we might want to update modified time etc. | ||
| val fileMetadata = File() | ||
|
|
||
| // We need to guess the mime type or retrieve it. For update, let's assume we keep existing or use generic. | ||
| // But ByteArrayContent needs a type. | ||
| // Let's fetch the file first to get the mimeType. | ||
| val existingFile = getDriveService().files().get(fileId).setFields("mimeType").execute() | ||
| val mimeType = existingFile.mimeType | ||
|
|
||
| val mediaContent = ByteArrayContent(mimeType, content) | ||
|
|
||
| val updatedFile = getDriveService().files().update(fileId, fileMetadata, mediaContent) | ||
| .setFields("id, name, mimeType") | ||
| .execute() | ||
|
|
There was a problem hiding this comment.
The updateFile method makes two sequential API calls - first to retrieve the existing mimeType, then to update the file. This doubles the network overhead and latency. Consider one of these alternatives: (1) pass mimeType as a parameter to updateFile, (2) use a generic mime type like "application/octet-stream" without fetching, or (3) add an optional parameter to specify whether to preserve the existing mime type. The current approach impacts performance unnecessarily.
| // Retrieve current metadata to keep name/mimeType if needed, or just update content | |
| // Creating a new File object with empty metadata to only update content is possible, | |
| // but often we might want to update modified time etc. | |
| val fileMetadata = File() | |
| // We need to guess the mime type or retrieve it. For update, let's assume we keep existing or use generic. | |
| // But ByteArrayContent needs a type. | |
| // Let's fetch the file first to get the mimeType. | |
| val existingFile = getDriveService().files().get(fileId).setFields("mimeType").execute() | |
| val mimeType = existingFile.mimeType | |
| val mediaContent = ByteArrayContent(mimeType, content) | |
| val updatedFile = getDriveService().files().update(fileId, fileMetadata, mediaContent) | |
| .setFields("id, name, mimeType") | |
| .execute() | |
| // Use empty metadata to only update content; we don't need to fetch existing mimeType. | |
| // A generic content type avoids an extra network call while still allowing upload. | |
| val fileMetadata = File() | |
| val mediaContent = ByteArrayContent("application/octet-stream", content) | |
| val updatedFile = getDriveService().files().update(fileId, fileMetadata, mediaContent) | |
| .setFields("id, name, mimeType") | |
| .execute() |
| val fileMetadata = File() | ||
| val existingFile = getService().files().get(fileId).setFields("mimeType").execute() | ||
| val mimeType = existingFile.mimeType | ||
|
|
||
| val mediaContent = ByteArrayContent(mimeType, content) | ||
|
|
||
| val updatedFile = getService().files().update(fileId, fileMetadata, mediaContent) | ||
| .setFields("id, name, mimeType") | ||
| .execute() | ||
|
|
There was a problem hiding this comment.
The updateFile method makes two sequential API calls - first to retrieve the existing mimeType, then to update the file. This doubles the network overhead and latency. Consider one of these alternatives: (1) pass mimeType as a parameter to updateFile, (2) use a generic mime type like "application/octet-stream" without fetching, or (3) add an optional parameter to specify whether to preserve the existing mime type. The current approach impacts performance unnecessarily.
| val fileMetadata = File() | |
| val existingFile = getService().files().get(fileId).setFields("mimeType").execute() | |
| val mimeType = existingFile.mimeType | |
| val mediaContent = ByteArrayContent(mimeType, content) | |
| val updatedFile = getService().files().update(fileId, fileMetadata, mediaContent) | |
| .setFields("id, name, mimeType") | |
| .execute() | |
| val fileMetadata = File() | |
| val mediaContent = ByteArrayContent("application/octet-stream", content) | |
| val updatedFile = getService().files().update(fileId, fileMetadata, mediaContent) | |
| .setFields("id, name, mimeType") | |
| .execute() |
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
| override suspend fun isAuthorized(): Boolean { | ||
| return false | ||
| } | ||
|
|
||
| override suspend fun authorize(): Boolean { | ||
| throw NotImplementedError("Google Drive is not supported on Web target yet.") |
There was a problem hiding this comment.
The isAuthorized method returns false while authorize throws NotImplementedError. This is inconsistent - if authorization is not supported, isAuthorized should also throw NotImplementedError to maintain consistent behavior across all unimplemented methods. Alternatively, if returning false is intentional to indicate the feature is unavailable, then authorize could return false instead of throwing to avoid unexpected exceptions.
composeApp/src/commonMain/kotlin/io/github/smiling_pixel/SettingsScreen.kt
Outdated
Show resolved
Hide resolved
| suspend fun getUserInfo(): UserInfo? | ||
| } | ||
|
|
||
| expect fun getCloudDriveClient(): CloudDriveClient |
There was a problem hiding this comment.
The expect function getCloudDriveClient() is declared in commonMain but lacks an actual implementation for iOS (iosMain). The PR includes implementations for Android, JVM, and wasmJs targets, but iOS is missing. This will cause compilation failures when building for iOS targets (iosArm64, iosSimulatorArm64). An iOS implementation must be provided to match the expect declaration.
composeApp/src/commonMain/kotlin/io/github/smiling_pixel/SettingsScreen.kt
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleSignInHelper.kt
Outdated
Show resolved
Hide resolved
| override suspend fun listFiles(parentId: String?): List<DriveFile> = withContext(Dispatchers.IO) { | ||
| val folderId = parentId ?: "root" | ||
| // Sanitize folderId to prevent injection | ||
| val sanitizedFolderId = folderId.replace("'", "\\'") |
There was a problem hiding this comment.
The sanitization approach using replace("'", "\\'") is insufficient for preventing query injection in Google Drive queries. While it handles single quotes, it doesn't account for backslashes in the input. An attacker could provide a parentId like root\' or '1'='1 which would become root\\' or '1'='1 after sanitization, still allowing injection. Additionally, other special characters like backslash should be escaped. Consider using a more robust escaping function or validating that parentId matches expected patterns (e.g., alphanumeric with specific allowed characters).
| override suspend fun listFiles(parentId: String?): List<DriveFile> = withContext(Dispatchers.IO) { | |
| val folderId = parentId ?: "root" | |
| // Sanitize folderId to prevent injection | |
| val sanitizedFolderId = folderId.replace("'", "\\'") | |
| /** | |
| * Escape a value for safe use as a single-quoted string literal in a Google Drive query. | |
| * | |
| * According to the Drive query syntax, backslashes and single quotes must be escaped inside | |
| * single-quoted string literals. This helper ensures that user-controlled values cannot break | |
| * out of the literal and alter the query structure. | |
| */ | |
| private fun escapeForDriveQueryLiteral(value: String): String { | |
| return value | |
| .replace("\\", "\\\\") | |
| .replace("'", "\\'") | |
| } | |
| override suspend fun listFiles(parentId: String?): List<DriveFile> = withContext(Dispatchers.IO) { | |
| val folderId = parentId ?: "root" | |
| // Sanitize folderId to prevent injection in the Drive query | |
| val sanitizedFolderId = escapeForDriveQueryLiteral(folderId) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private const val TOKENS_DIRECTORY_PATH = "tokens" | ||
|
|
There was a problem hiding this comment.
TOKENS_DIRECTORY_PATH = "tokens" stores OAuth tokens relative to the process working directory, which is often read-only or unpredictable for packaged desktop apps. This can cause authorization to fail at runtime. Store tokens in a user-writable per-app location (e.g., user home / app data dir) and ensure the directory exists.
| Logger.e("GoogleDriveClient", "Failed to authorize: ${e.message}") | ||
| // Ensure we don't keep a potentially inconsistent cached service | ||
| driveServiceCache = null | ||
| false |
There was a problem hiding this comment.
authorize() catches all exceptions and returns false, which makes unrecoverable errors indistinguishable from user cancellation and contradicts the interface KDoc (throws Exception for unrecoverable errors). Prefer rethrowing unexpected exceptions (or mapping them to a distinct error) so the UI can show a correct message.
| false | |
| throw e |
composeApp/src/jvmMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| override suspend fun isAuthorized(): Boolean { | ||
| return false |
There was a problem hiding this comment.
This Web implementation is marked “NOT IMPLEMENTED”, but isAuthorized() returns false instead of failing like the other operations. This can make the UI look supported-but-signed-out rather than clearly unsupported. Consider throwing NotImplementedError here as well (or providing a single consistent unsupported behavior across all methods).
| return false | |
| throw NotImplementedError("Google Drive is not supported on Web target yet.") |
composeApp/src/commonMain/kotlin/io/github/smiling_pixel/SettingsScreen.kt
Outdated
Show resolved
Hide resolved
composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt
Show resolved
Hide resolved
| expect fun getCloudDriveClient(): CloudDriveClient | ||
|
|
There was a problem hiding this comment.
expect fun getCloudDriveClient() is declared in commonMain, and the project builds iOS targets (iosArm64/iosSimulatorArm64), but there is no iOS actual fun getCloudDriveClient() implementation. This will fail compilation for iOS. Add an actual in src/iosMain (even if it’s a stub that throws NotImplementedError) or refactor to provide a default implementation for unsupported targets.
| expect fun getCloudDriveClient(): CloudDriveClient | |
| private class UnsupportedCloudDriveClient : CloudDriveClient { | |
| override suspend fun listFiles( | |
| parentId: String?, | |
| query: String?, | |
| pageToken: String? | |
| ): List<DriveFile> { | |
| throw NotImplementedError("CloudDriveClient is not implemented for this platform.") | |
| } | |
| override suspend fun getFile(fileId: String): DriveFile? { | |
| throw NotImplementedError("CloudDriveClient is not implemented for this platform.") | |
| } | |
| override suspend fun createFolder( | |
| name: String, | |
| parentId: String? | |
| ): DriveFile { | |
| throw NotImplementedError("CloudDriveClient is not implemented for this platform.") | |
| } | |
| override suspend fun uploadFile( | |
| name: String, | |
| mimeType: String, | |
| parentId: String?, | |
| content: ByteArray | |
| ): DriveFile { | |
| throw NotImplementedError("CloudDriveClient is not implemented for this platform.") | |
| } | |
| override suspend fun downloadFile(fileId: String): ByteArray { | |
| throw NotImplementedError("CloudDriveClient is not implemented for this platform.") | |
| } | |
| override suspend fun deleteFile(fileId: String) { | |
| throw NotImplementedError("CloudDriveClient is not implemented for this platform.") | |
| } | |
| override suspend fun isAuthorized(): Boolean { | |
| throw NotImplementedError("CloudDriveClient is not implemented for this platform.") | |
| } | |
| override suspend fun authorize(): Boolean { | |
| throw NotImplementedError("CloudDriveClient is not implemented for this platform.") | |
| } | |
| override suspend fun signOut() { | |
| throw NotImplementedError("CloudDriveClient is not implemented for this platform.") | |
| } | |
| override suspend fun getUserInfo(): UserInfo? { | |
| throw NotImplementedError("CloudDriveClient is not implemented for this platform.") | |
| } | |
| } | |
| fun getCloudDriveClient(): CloudDriveClient = UnsupportedCloudDriveClient() |
This pull request introduces Google Drive integration into the application, enabling users to authorize, access, and manage their files in Google Drive from within the app. The changes include new client interfaces, Android implementation, UI updates for authorization, and necessary dependency and configuration updates.
Google Drive Integration
CloudDriveClientinterface, including models forDriveFileandUserInfo, to abstract cloud drive operations (composeApp/src/commonMain/kotlin/io/github/smiling_pixel/client/CloudDriveClient.kt).GoogleDriveClientwith full support for authorization, file/folder management, and user info retrieval, using Google APIs (composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleDriveClient.kt).GoogleSignInHelperto handle Google Sign-In flows and activity result management on Android (composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleSignInHelper.kt).UI Enhancements
SettingsScreento allow users to authorize Google Drive, view account info, and revoke access, reflecting authorization state and user details in the UI (composeApp/src/commonMain/kotlin/io/github/smiling_pixel/SettingsScreen.kt). [1] [2] [3]MainActivityto support the authorization flow (composeApp/src/androidMain/kotlin/io/github/smiling_pixel/MainActivity.kt).Dependency and Configuration Updates
build.gradle.kts(composeApp/build.gradle.kts). [1] [2]io.github.smiling_pixel.mark_dayfor consistency across Android and desktop builds (composeApp/build.gradle.kts,AndroidManifest.xml). [1] [2] [3]