mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	feat: added google drive service.
Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
		| @@ -183,6 +183,20 @@ | ||||
|                     android:scheme="tachiyomi" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ui.setting.track.GoogleDriveLoginActivity" | ||||
|             android:label="GoogleDrive" | ||||
|             android:exported="true"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
|  | ||||
|                 <data | ||||
|                     android:scheme="eu.kanade.google.oauth" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
|         <receiver | ||||
|             android:name=".data.notification.NotificationReceiver" | ||||
|   | ||||
							
								
								
									
										1
									
								
								app/src/main/assets/client_secrets.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/src/main/assets/client_secrets.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"installed":{"client_id":"1046609911130-tbp79niehhuii976ekep1us06e9a8lne.apps.googleusercontent.com","project_id":"tachiyomi","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}} | ||||
| @@ -52,6 +52,8 @@ import eu.kanade.tachiyomi.data.backup.BackupRestoreJob | ||||
| import eu.kanade.tachiyomi.data.backup.models.Backup | ||||
| import eu.kanade.tachiyomi.data.sync.SyncDataJob | ||||
| import eu.kanade.tachiyomi.data.sync.SyncManager | ||||
| import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService | ||||
| import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.system.DeviceUtil | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| @@ -94,6 +96,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings { | ||||
|                         entries = mapOf( | ||||
|                             SyncManager.SyncService.NONE.value to stringResource(R.string.off), | ||||
|                             SyncManager.SyncService.SYNCYOMI.value to stringResource(R.string.syncyomi), | ||||
|                             SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(R.string.google_drive), | ||||
|                         ), | ||||
|                         onValueChanged = { true }, | ||||
|                     ), | ||||
| @@ -448,6 +451,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings { | ||||
|         return when (syncServiceType) { | ||||
|             SyncManager.SyncService.NONE -> 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<Preference> { | ||||
|         val context = LocalContext.current | ||||
|         val googleDriveSync = Injekt.get<GoogleDriveService>() | ||||
|         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<Preference> { | ||||
|         return listOf( | ||||
|   | ||||
| @@ -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<DownloadManager>() | ||||
|         } | ||||
|  | ||||
|         addSingletonFactory { GoogleDriveService(app) } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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<SyncPreferences>(), | ||||
|     ) | ||||
|  | ||||
|     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<File> { | ||||
|         // 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<SyncPreferences>() | ||||
|  | ||||
|     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") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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)) { | ||||
|   | ||||
| @@ -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<GoogleDriveService>() | ||||
|     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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user