Skip to content

Comments

feat: add Google Drive support#13

Merged
SmilingPixel merged 32 commits intomainfrom
feat/cloud_drive_0113
Feb 12, 2026
Merged

feat: add Google Drive support#13
SmilingPixel merged 32 commits intomainfrom
feat/cloud_drive_0113

Conversation

@SmilingPixel
Copy link
Owner

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

  • Added a multiplatform CloudDriveClient interface, including models for DriveFile and UserInfo, to abstract cloud drive operations (composeApp/src/commonMain/kotlin/io/github/smiling_pixel/client/CloudDriveClient.kt).
  • Implemented the Android-specific GoogleDriveClient with 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).
  • Added GoogleSignInHelper to handle Google Sign-In flows and activity result management on Android (composeApp/src/androidMain/kotlin/io/github/smiling_pixel/client/GoogleSignInHelper.kt).

UI Enhancements

  • Updated the SettingsScreen to 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]
  • Registered the Google Sign-In launcher in MainActivity to support the authorization flow (composeApp/src/androidMain/kotlin/io/github/smiling_pixel/MainActivity.kt).

Dependency and Configuration Updates

  • Added required Google Drive and Google Sign-In libraries to Android and JVM targets in build.gradle.kts (composeApp/build.gradle.kts). [1] [2]
  • Updated application namespace, package name, and application ID to io.github.smiling_pixel.mark_day for consistency across Android and desktop builds (composeApp/build.gradle.kts, AndroidManifest.xml). [1] [2] [3]

Copilot AI review requested due to automatic review settings January 22, 2026 01:32
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CloudDriveClient interface 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.

Comment on lines +75 to +76
expect fun getCloudDriveClient(): CloudDriveClient

Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 1 to 50
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()
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +113 to +115
// Sanitize folderId to prevent injection
val sanitizedFolderId = folderId.replace("'", "\\'")
val query = "'$sanitizedFolderId' in parents and trashed = false"
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
SmilingPixel and others added 2 commits February 3, 2026 09:54
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.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +200 to +216
// 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()

Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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()

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +247
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()

Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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()

Copilot uses AI. Check for mistakes.
Comment on lines 33 to 38
override suspend fun isAuthorized(): Boolean {
return false
}

override suspend fun authorize(): Boolean {
throw NotImplementedError("Google Drive is not supported on Web target yet.")
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
suspend fun getUserInfo(): UserInfo?
}

expect fun getCloudDriveClient(): CloudDriveClient
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +126
override suspend fun listFiles(parentId: String?): List<DriveFile> = withContext(Dispatchers.IO) {
val folderId = parentId ?: "root"
// Sanitize folderId to prevent injection
val sanitizedFolderId = folderId.replace("'", "\\'")
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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)

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +55 to +56
private const val TOKENS_DIRECTORY_PATH = "tokens"

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Logger.e("GoogleDriveClient", "Failed to authorize: ${e.message}")
// Ensure we don't keep a potentially inconsistent cached service
driveServiceCache = null
false
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
false
throw e

Copilot uses AI. Check for mistakes.
}

override suspend fun isAuthorized(): Boolean {
return false
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
return false
throw NotImplementedError("Google Drive is not supported on Web target yet.")

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +110
expect fun getCloudDriveClient(): CloudDriveClient

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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()

Copilot uses AI. Check for mistakes.
@SmilingPixel SmilingPixel merged commit ad8ed4e into main Feb 12, 2026
2 checks passed
@SmilingPixel SmilingPixel deleted the feat/cloud_drive_0113 branch February 12, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant