diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2542c1c3d8..3cd56e7a1d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -257,6 +257,9 @@ dependencies {
// For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation(libs.leakcanary.android)
implementation(libs.leakcanary.plumber)
+
+ implementation(libs.google.api.services.drive)
+ implementation(libs.google.api.client.oauth)
}
androidComponents {
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index e8f9b52202..51c91f9c52 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -73,4 +73,10 @@
# Firebase
-keep class com.google.firebase.installations.** { *; }
--keep interface com.google.firebase.installations.** { *; }
\ No newline at end of file
+-keep interface com.google.firebase.installations.** { *; }
+
+# Google Drive
+-keep class com.google.api.services.** { *; }
+
+# Google OAuth
+-keep class com.google.api.client.** { *; }
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5eefefd442..3ba6e730c4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -183,6 +183,20 @@
android:scheme="tachiyomi" />
+
+
+
+
+
+
+
+
+
+
emptyList()
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
+ SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
} +
if (syncServiceType == SyncManager.SyncService.NONE) {
emptyList()
@@ -456,6 +460,74 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
}
}
+ @Composable
+ private fun getGoogleDrivePreferences(): List {
+ val context = LocalContext.current
+ val googleDriveSync = Injekt.get()
+ return listOf(
+ Preference.PreferenceItem.TextPreference(
+ title = stringResource(R.string.pref_google_drive_sign_in),
+ onClick = {
+ val intent = googleDriveSync.getSignInIntent()
+ context.startActivity(intent)
+ },
+ ),
+ getGoogleDrivePurge(),
+ )
+ }
+
+ @Composable
+ private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
+ val scope = rememberCoroutineScope()
+ val showPurgeDialog = remember { mutableStateOf(false) }
+ val context = LocalContext.current
+ val googleDriveSync = remember { GoogleDriveSyncService(context) }
+
+ if (showPurgeDialog.value) {
+ PurgeConfirmationDialog(
+ onConfirm = {
+ showPurgeDialog.value = false
+ scope.launch {
+ val result = googleDriveSync.deleteSyncDataFromGoogleDrive()
+ if (result) {
+ context.toast(R.string.google_drive_sync_data_purged)
+ } else {
+ context.toast(R.string.google_drive_sync_data_not_found)
+ }
+ }
+ },
+ onDismissRequest = { showPurgeDialog.value = false },
+ )
+ }
+
+ return Preference.PreferenceItem.TextPreference(
+ title = stringResource(R.string.pref_google_drive_purge_sync_data),
+ onClick = { showPurgeDialog.value = true },
+ )
+ }
+
+ @Composable
+ fun PurgeConfirmationDialog(
+ onConfirm: () -> Unit,
+ onDismissRequest: () -> Unit,
+ ) {
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(text = stringResource(R.string.pref_purge_confirmation_title)) },
+ text = { Text(text = stringResource(R.string.pref_purge_confirmation_message)) },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(text = stringResource(R.string.action_cancel))
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onConfirm) {
+ Text(text = stringResource(android.R.string.ok))
+ }
+ },
+ )
+ }
+
@Composable
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List {
return listOf(
diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
index 3949565ab4..ab8cf126d9 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.data.saver.ImageSaver
+import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.JavaScriptEngine
@@ -151,6 +152,8 @@ class AppModule(val app: Application) : InjektModule {
get()
}
+
+ addSingletonFactory { GoogleDriveService(app) }
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt
index 7bb5f16103..49312fb7db 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.sync.models.SyncData
import eu.kanade.tachiyomi.data.sync.models.SyncDevice
import eu.kanade.tachiyomi.data.sync.models.SyncStatus
+import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
import eu.kanade.tachiyomi.data.sync.service.SyncYomiSyncService
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
@@ -56,6 +57,7 @@ class SyncManager(
enum class SyncService(val value: Int) {
NONE(0),
SYNCYOMI(1),
+ GOOGLE_DRIVE(2),
;
companion object {
@@ -107,6 +109,10 @@ class SyncManager(
)
}
+ SyncService.GOOGLE_DRIVE -> {
+ GoogleDriveSyncService(context, json, syncPreferences)
+ }
+
else -> {
logcat(LogPriority.ERROR) { "Invalid sync service type: $syncService" }
null
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt
new file mode 100644
index 0000000000..e517a9ce83
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/GoogleDriveSyncService.kt
@@ -0,0 +1,332 @@
+package eu.kanade.tachiyomi.data.sync.service
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.util.Log
+import com.google.api.client.auth.oauth2.TokenResponseException
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest
+import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
+import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse
+import com.google.api.client.http.ByteArrayContent
+import com.google.api.client.http.javanet.NetHttpTransport
+import com.google.api.client.json.JsonFactory
+import com.google.api.client.json.jackson2.JacksonFactory
+import com.google.api.services.drive.Drive
+import com.google.api.services.drive.DriveScopes
+import com.google.api.services.drive.model.File
+import eu.kanade.tachiyomi.data.sync.models.SyncData
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import logcat.LogPriority
+import tachiyomi.core.util.system.logcat
+import tachiyomi.domain.sync.SyncPreferences
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.InputStreamReader
+import java.util.zip.GZIPInputStream
+import java.util.zip.GZIPOutputStream
+
+class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: SyncPreferences) : SyncService(context, json, syncPreferences) {
+ constructor(context: Context) : this(
+ context,
+ Json {
+ encodeDefaults = true
+ ignoreUnknownKeys = true
+ },
+ Injekt.get(),
+ )
+
+ private val remoteFileName = "tachiyomi_sync_data.gz"
+
+ private val googleDriveService = GoogleDriveService(context)
+
+ override suspend fun beforeSync() = googleDriveService.refreshToken()
+
+ override suspend fun pushSyncData(): SyncData? {
+ val drive = googleDriveService.googleDriveService
+
+ // Check if the Google Drive service is initialized
+ if (drive == null) {
+ logcat(LogPriority.ERROR) { "Google Drive service not initialized" }
+ return null
+ }
+
+ val fileList = getFileList(drive)
+
+ if (fileList.isEmpty()) {
+ return null
+ }
+ val gdriveFileId = fileList[0].id
+
+ val outputStream = ByteArrayOutputStream()
+ drive.files().get(gdriveFileId).executeMediaAndDownloadTo(outputStream)
+ val jsonString = withContext(Dispatchers.IO) {
+ val gzipInputStream = GZIPInputStream(ByteArrayInputStream(outputStream.toByteArray()))
+ gzipInputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
+ }
+
+ return json.decodeFromString(SyncData.serializer(), jsonString)
+ }
+
+ override suspend fun pullSyncData(syncData: SyncData) {
+ val jsonData = json.encodeToString(syncData)
+
+ val drive = googleDriveService.googleDriveService
+
+ // Check if the Google Drive service is initialized
+ if (drive == null) {
+ logcat(LogPriority.ERROR) { "Google Drive service not initialized" }
+ return
+ }
+
+ // delete file if exists
+ val fileList = getFileList(drive)
+ if (fileList.isNotEmpty()) {
+ drive.files().delete(fileList[0].id).execute()
+ }
+
+ val fileMetadata = File()
+ fileMetadata.name = remoteFileName
+ fileMetadata.mimeType = "application/gzip"
+
+ val byteArrayOutputStream = ByteArrayOutputStream()
+
+ withContext(Dispatchers.IO) {
+ val gzipOutputStream = GZIPOutputStream(byteArrayOutputStream)
+ gzipOutputStream.write(jsonData.toByteArray(Charsets.UTF_8))
+ gzipOutputStream.close()
+ }
+
+ val byteArrayContent = ByteArrayContent("application/octet-stream", byteArrayOutputStream.toByteArray())
+ val uploadedFile = drive.files().create(fileMetadata, byteArrayContent)
+ .setFields("id")
+ .execute()
+
+ logcat(LogPriority.DEBUG) { "Created sync data file in Google Drive with file ID: ${uploadedFile.id}" }
+ }
+
+ private fun getFileList(drive: Drive): MutableList {
+ // Search for the existing file by name
+ val query = "mimeType='application/gzip' and trashed = false and name = '$remoteFileName'"
+ val fileList = drive.files().list().setQ(query).execute().files
+ Log.d("GoogleDrive", "File list: $fileList")
+
+ return fileList
+ }
+
+ suspend fun deleteSyncDataFromGoogleDrive(): Boolean {
+ val drive = googleDriveService.googleDriveService
+
+ if (drive == null) {
+ logcat(LogPriority.ERROR) { "Google Drive service not initialized" }
+ return false
+ }
+ googleDriveService.refreshToken()
+
+ return withContext(Dispatchers.IO) {
+ val query = "mimeType='application/gzip' and trashed = false and name = '$remoteFileName'"
+ val fileList = drive.files().list().setQ(query).execute().files
+
+ if (fileList.isNullOrEmpty()) {
+ logcat(LogPriority.DEBUG) { "No sync data file found in Google Drive" }
+ false
+ } else {
+ val fileId = fileList[0].id
+ drive.files().delete(fileId).execute()
+ logcat(LogPriority.DEBUG) { "Deleted sync data file in Google Drive with file ID: $fileId" }
+ true
+ }
+ }
+ }
+}
+
+class GoogleDriveService(private val context: Context) {
+ var googleDriveService: Drive? = null
+ private val syncPreferences = Injekt.get()
+
+ init {
+ initGoogleDriveService()
+ }
+
+ /**
+ * Initializes the Google Drive service by obtaining the access token and refresh token from the SyncPreferences
+ * and setting up the service using the obtained tokens.
+ */
+ private fun initGoogleDriveService() {
+ val accessToken = syncPreferences.getGoogleDriveAccessToken()
+ val refreshToken = syncPreferences.getGoogleDriveRefreshToken()
+
+ if (accessToken == "" || refreshToken == "") {
+ googleDriveService = null
+ return
+ }
+
+ setupGoogleDriveService(accessToken, refreshToken)
+ }
+
+ /**
+ * Launches an Intent to open the user's default browser for Google Drive sign-in.
+ * The Intent carries the authorization URL, which prompts the user to sign in
+ * and grant the application permission to access their Google Drive account.
+ * @return An Intent configured to launch a browser for Google Drive OAuth sign-in.
+ */
+ fun getSignInIntent(): Intent {
+ val authorizationUrl = generateAuthorizationUrl()
+
+ return Intent(Intent.ACTION_VIEW).apply {
+ data = Uri.parse(authorizationUrl)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ }
+
+ /**
+ * Generates the authorization URL required for the user to grant the application permission to access their Google Drive account.
+ * Sets the approval prompt to "force" to ensure that the user is always prompted to grant access, even if they have previously granted access.
+ * @return The authorization URL.
+ */
+ private fun generateAuthorizationUrl(): String {
+ val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
+ val secrets = GoogleClientSecrets.load(
+ jsonFactory,
+ InputStreamReader(context.assets.open("client_secrets.json")),
+ )
+
+ val flow = GoogleAuthorizationCodeFlow.Builder(
+ NetHttpTransport(),
+ jsonFactory,
+ secrets,
+ listOf(DriveScopes.DRIVE_FILE),
+ ).setAccessType("offline").build()
+
+ return flow.newAuthorizationUrl()
+ .setRedirectUri("eu.kanade.google.oauth:/oauth2redirect")
+ .setApprovalPrompt("force")
+ .build()
+ }
+ internal suspend fun refreshToken() = withContext(Dispatchers.IO) {
+ val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
+ val secrets = GoogleClientSecrets.load(
+ jsonFactory,
+ InputStreamReader(context.assets.open("client_secrets.json")),
+ )
+
+ val credential = GoogleCredential.Builder()
+ .setJsonFactory(jsonFactory)
+ .setTransport(NetHttpTransport())
+ .setClientSecrets(secrets)
+ .build()
+
+ credential.refreshToken = syncPreferences.getGoogleDriveRefreshToken()
+
+ logcat(LogPriority.DEBUG) { "Refreshing access token with: ${syncPreferences.getGoogleDriveRefreshToken()}" }
+
+ try {
+ credential.refreshToken()
+ val newAccessToken = credential.accessToken
+ val oldAccessToken = syncPreferences.getGoogleDriveAccessToken()
+ // Save the new access token
+ syncPreferences.setGoogleDriveAccessToken(newAccessToken)
+ setupGoogleDriveService(newAccessToken, credential.refreshToken)
+ logcat(LogPriority.DEBUG) { "Google Access token refreshed old: $oldAccessToken new: $newAccessToken" }
+ } catch (e: TokenResponseException) {
+ if (e.details.error == "invalid_grant") {
+ // The refresh token is invalid, prompt the user to sign in again
+ logcat(LogPriority.ERROR) { "Refresh token is invalid, prompt user to sign in again" }
+ throw e.message?.let { Exception(it) } ?: Exception("Unknown error")
+ } else {
+ // Token refresh failed; handle this situation
+ logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" }
+ logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" }
+ }
+ } catch (e: IOException) {
+ // Token refresh failed; handle this situation
+ logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" }
+ logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" }
+ }
+ }
+
+ /**
+ * Sets up the Google Drive service using the provided access token and refresh token.
+ * @param accessToken The access token obtained from the SyncPreferences.
+ * @param refreshToken The refresh token obtained from the SyncPreferences.
+ */
+ private fun setupGoogleDriveService(accessToken: String, refreshToken: String) {
+ val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
+ val secrets = GoogleClientSecrets.load(
+ jsonFactory,
+ InputStreamReader(context.assets.open("client_secrets.json")),
+ )
+
+ val credential = GoogleCredential.Builder()
+ .setJsonFactory(jsonFactory)
+ .setTransport(NetHttpTransport())
+ .setClientSecrets(secrets)
+ .build()
+
+ credential.accessToken = accessToken
+ credential.refreshToken = refreshToken
+
+ googleDriveService = Drive.Builder(
+ NetHttpTransport(),
+ jsonFactory,
+ credential,
+ ).setApplicationName("Tachiyomi")
+ .build()
+ }
+
+ /**
+ * Handles the authorization code returned after the user has granted the application permission to access their Google Drive account.
+ * It obtains the access token and refresh token using the authorization code, saves the tokens to the SyncPreferences,
+ * sets up the Google Drive service using the obtained tokens, and initializes the service.
+ * @param authorizationCode The authorization code obtained from the OAuthCallbackServer.
+ * @param activity The current activity.
+ * @param onSuccess A callback function to be called on successful authorization.
+ * @param onFailure A callback function to be called on authorization failure.
+ */
+ fun handleAuthorizationCode(authorizationCode: String, activity: Activity, onSuccess: () -> Unit, onFailure: (String) -> Unit) {
+ val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
+ val secrets = GoogleClientSecrets.load(
+ jsonFactory,
+ InputStreamReader(context.assets.open("client_secrets.json")),
+ )
+
+ val tokenResponse: GoogleTokenResponse = GoogleAuthorizationCodeTokenRequest(
+ NetHttpTransport(),
+ jsonFactory,
+ secrets.installed.clientId,
+ secrets.installed.clientSecret,
+ authorizationCode,
+ "eu.kanade.google.oauth:/oauth2redirect",
+ ).setGrantType("authorization_code").execute()
+
+ try {
+ // Save the access token and refresh token
+ val accessToken = tokenResponse.accessToken
+ val refreshToken = tokenResponse.refreshToken
+
+ // Save the tokens to SyncPreferences
+ syncPreferences.setGoogleDriveAccessToken(accessToken)
+ syncPreferences.setGoogleDriveRefreshToken(refreshToken)
+
+ setupGoogleDriveService(accessToken, refreshToken)
+ initGoogleDriveService()
+
+ activity.runOnUiThread {
+ onSuccess()
+ }
+ } catch (e: Exception) {
+ activity.runOnUiThread {
+ onFailure(e.localizedMessage ?: "Unknown error")
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt
index 39eac54834..5440567fc5 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt
@@ -106,8 +106,8 @@ abstract class SyncService(
localMangaMap.forEach { (key, localManga) ->
val remoteManga = remoteMangaMap[key]
if (remoteManga != null) {
- val localInstant = localManga.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
- val remoteInstant = remoteManga.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
+ val localInstant = localManga.lastModifiedAt.let { Instant.ofEpochMilli(it) }
+ val remoteInstant = remoteManga.lastModifiedAt.let { Instant.ofEpochMilli(it) }
val mergedManga = if ((localInstant ?: Instant.MIN) >= (
remoteInstant
@@ -164,8 +164,8 @@ abstract class SyncService(
localChapterMap.forEach { (url, localChapter) ->
val remoteChapter = remoteChapterMap[url]
if (remoteChapter != null) {
- val localInstant = localChapter.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
- val remoteInstant = remoteChapter.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
+ val localInstant = localChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) }
+ val remoteInstant = remoteChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) }
val mergedChapter =
if ((localInstant ?: Instant.MIN) >= (remoteInstant ?: Instant.MIN)) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/GoogleDriveLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/GoogleDriveLoginActivity.kt
new file mode 100644
index 0000000000..ae95b919b1
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/GoogleDriveLoginActivity.kt
@@ -0,0 +1,52 @@
+package eu.kanade.tachiyomi.ui.setting.track
+
+import android.net.Uri
+import android.widget.Toast
+import androidx.lifecycle.lifecycleScope
+import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
+import tachiyomi.core.util.lang.launchIO
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class GoogleDriveLoginActivity : BaseOAuthLoginActivity() {
+ private val googleDriveService = Injekt.get()
+ override fun handleResult(data: Uri?) {
+ val code = data?.getQueryParameter("code")
+ val error = data?.getQueryParameter("error")
+ if (code != null) {
+ lifecycleScope.launchIO {
+ googleDriveService.handleAuthorizationCode(
+ code,
+ this@GoogleDriveLoginActivity,
+ onSuccess = {
+ Toast.makeText(
+ this@GoogleDriveLoginActivity,
+ "Authorization successful.",
+ Toast.LENGTH_LONG,
+ ).show()
+
+ returnToSettings()
+ },
+ onFailure = { error ->
+ Toast.makeText(
+ this@GoogleDriveLoginActivity,
+ "Authorization failed: $error",
+ Toast.LENGTH_LONG,
+ ).show()
+ returnToSettings()
+ },
+ )
+ }
+ } else if (error != null) {
+ Toast.makeText(
+ this@GoogleDriveLoginActivity,
+ "Authorization failed: $error",
+ Toast.LENGTH_LONG,
+ ).show()
+
+ returnToSettings()
+ } else {
+ returnToSettings()
+ }
+ }
+}
diff --git a/domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt b/domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt
index 7b01c1d6f6..8e7fa8499f 100644
--- a/domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt
+++ b/domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt
@@ -15,4 +15,20 @@ class SyncPreferences(
fun deviceName() = preferenceStore.getString("device_name", android.os.Build.MANUFACTURER + android.os.Build.PRODUCT)
fun syncService() = preferenceStore.getInt("sync_service", 0)
+
+ private fun googleDriveAccessToken() = preferenceStore.getString("google_drive_access_token", "")
+
+ fun setGoogleDriveAccessToken(accessToken: String) {
+ googleDriveAccessToken().set(accessToken)
+ }
+
+ fun getGoogleDriveAccessToken() = googleDriveAccessToken().get()
+
+ private fun googleDriveRefreshToken() = preferenceStore.getString("google_drive_refresh_token", "")
+
+ fun setGoogleDriveRefreshToken(refreshToken: String) {
+ googleDriveRefreshToken().set(refreshToken)
+ }
+
+ fun getGoogleDriveRefreshToken() = googleDriveRefreshToken().get()
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c1070683d6..916d76aa4f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -91,6 +91,9 @@ voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
+google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev197-1.25.0"
+google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1"
+
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
[bundles]
diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml
index 319f644e38..56ae1dfb22 100644
--- a/i18n/src/main/res/values/strings.xml
+++ b/i18n/src/main/res/values/strings.xml
@@ -530,6 +530,18 @@
SyncYomi
Done in %1$s
Last Synchronization: %1$s
+ Google Drive
+ Sign in
+ Signed in successfully
+ Sign in failed
+ Authentication
+ Clear Sync Data from Google Drive
+ Sync data purged from Google Drive
+ No sync data found in Google Drive
+ Not signed in to Google Drive
+ Purge confirmation
+ Purging sync data will delete all your sync data from Google Drive. Are you sure you want to continue?
+
Network