Connect with us

Разработка

Резервное копирование для Android-приложения в стиле WhatsApp с использованием API Google Drive

В этой статье вы узнаете, как за 10 простых шагов добавить функцию резервного копирования и восстановления на основе Google Drive в ваше Android-приложение, чтобы резервная копия оставалась конфиденциальной и надежно хранилась в облаке.

Опубликовано

/

     
     

Большинство приложений хранят важные пользовательские данные локально в Room или любом другом локальном хранилище, но что делать, если пользователь сменит телефон или переустановит ваше приложение? В этой статье вы узнаете, как за 10 простых шагов добавить функцию резервного копирования и восстановления на основе Google Drive в ваше Android-приложение, чтобы резервная копия оставалась конфиденциальной и надежно хранилась в облаке.

Мы будем использовать:

  • Google Sign-In для аутентификации
  • REST API Google Диска (v3) для загрузки/выгрузки
  • корутины и ViewModel для фоновых операций

Что мы рассмотрим:

  • Быструю настройку Google Cloud Console
  • Создание GoogleDriveService для обработки всех взаимодействий API
  • Подключение сервиса к вашему пользовательскому интерфейсу с помощью ViewModel
  • Обработка базовой логики резервного копирования и восстановления

Шаг 1: быстрая настройка Google Cloud

  1. Перейдите в Google Cloud Console.
  2. Создайте новый проект или выберите существующий.
  3. Включите Google Drive API из библиотеки API.
  4. Перейдите в раздел Credentials → Create Credentials → OAuth client ID → Android:
    • Введите имя пакета вашего приложения, например, com.example.app
    • Введите отпечаток SHA-1 (его можно получить в Android Studio или с помощью keytool)

Готово — теперь ваше приложение сможет использовать скоуп appDataFolder в Drive через Google Sign-In.

Шаг 2: добавьте необходимые зависимости

В файле app/build.gradle:

dependencies {
    // Google Sign-In
    implementation 'com.google.android.gms:play-services-auth:20.7.0'
    
    // Google API Client for Drive
    implementation 'com.google.api-client:google-api-client-android:2.2.0'
    implementation 'com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0'
    implementation('com.google.api-client:google-api-client-gson:2.2.0') {
        exclude group: 'org.apache.httpcomponents'
    }
}

Добавьте разрешения в AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

Шаг 3: настройка Google Диска

Чтобы сохранить чистоту кода, мы создадим отдельный класс сервиса для обработки всех вызовов API Google Диска.

Во-первых, нам нужен способ создать авторизованный сервисный объект Drive. Эта функция использует учётную запись пользователя, вошедшего в систему, для создания учётных данных, связанных с папкой appDataFolder.

class GoogleDriveService(private val context: Context) {

    private fun getDriveService(account: GoogleSignInAccount): Drive {
        val credential = GoogleAccountCredential.usingOAuth2(
            context, Collections.singleton(DriveScopes.DRIVE_APPDATA)
        ).setSelectedAccount(account.account)

        return Drive.Builder(
            AndroidHttp.newCompatibleTransport(),
            GsonFactory.getDefaultInstance(),
            credential)
            .setApplicationName("YourAppName") // Replace with your app name
            .build()
    }
}

Шаг 4: обработка входа пользователя

Для выполнения любого действия пользователь должен сначала войти в систему. Эта функция создаёт Intent, которое запускает стандартный процесс входа в систему Google.

 fun getSignInIntent(): Intent {
    val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
        .requestEmail()
        .requestScopes(Scope(DriveScopes.DRIVE_APPDATA)) // Request access to the AppData folder
        .build()

    val client = GoogleSignIn.getClient(context, gso)
    return client.signInIntent
}

Это инициализирует клиент Drive, аутентифицированный с использованием вошедшего в систему пользователя.

Резервное копирование для Android-приложения в стиле WhatsApp с использованием API Google Drive

Шаг 5: загрузка бекапа локальной базы данных

Это основная функция резервного копирования. Она берёт файл локальной базы данных, создаёт для него метаданные и загружает их в папку appDataFolder. Чтобы избежать беспорядка, сначала удаляем все старые резервные копии, если таковые имеются.

suspend fun uploadBackup(account: GoogleSignInAccount, databasePath: String): File? {
    val driveService = getDriveService(account)
    val dbFile = java.io.File(databasePath)

    if (!dbFile.exists() || !dbFile.canRead() || dbFile.length() == 0L) {
        throw Exception("Database file is invalid.")
    }

    // Delete the previous backup file, if it exists.
    getLatestBackup(account)?.id?.let { fileId ->
        driveService.files().delete(fileId).execute()
    }

    val fileMetadata = File().apply {
        name = "your_app_database_backup_${System.currentTimeMillis()}.db"
        parents = listOf("appDataFolder") // This is key!
    }

    val mediaContent = com.google.api.client.http.FileContent("application/octet-stream", dbFile)

    return driveService.files().create(fileMetadata, mediaContent)
        .setFields("id, name, size, modifiedTime")
        .execute()
}

Это загрузит .db файл вашего приложения в приватную папку на Диске.

Шаг 6: поиск и восстановление резервной копии

Для восстановления сначала нужно найти файл резервной копии. Эта функция запрашивает у appDataFolder самый последний файл.

suspend fun getLatestBackup(context: Context, account: GoogleSignInAccount): File? {
    val drive = getDriveService(context, account)
    val files = drive.files().list()
        .setSpaces("appDataFolder")
        .setFields("files(id, name, modifiedTime, size)")
        .setPageSize(1)
        .execute()
suspend fun restoreBackup(account: GoogleSignInAccount, fileId: String, destinationPath: String) {
    val driveService = getDriveService(account)
    val destinationFile = java.io.File(destinationPath)
    val tempBackupPath = "$destinationPath.tmp"

    if (destinationFile.exists()) {
        destinationFile.copyTo(java.io.File(tempBackupPath), overwrite = true)
    }

    try {
        FileOutputStream(destinationFile).use { outputStream ->
            driveService.files().get(fileId).executeMediaAndDownloadTo(outputStream)
        }
        java.io.File(tempBackupPath).delete() // Success, so delete the temp copy
    } catch (e: Exception) {
        // If restore fails, copy the temporary backup back
        val tempBackupFile = java.io.File(tempBackupPath)
        if (tempBackupFile.exists()) {
            tempBackupFile.copyTo(destinationFile, overwrite = true)
            tempBackupFile.delete()
        }
        throw Exception("Database restore failed: ${e.message}")
    }
}

Шаг 7: восстановление файла резервной копии

После получения идентификатора файла эта функция загружает его и заменяет локальную базу данных. В целях безопасности сначала создаётся временная локальная копия текущей базы данных, которая восстанавливается в случае сбоя загрузки.

suspend fun restoreBackup(account: GoogleSignInAccount, fileId: String, destinationPath: String) {
    val driveService = getDriveService(account)
    val destinationFile = java.io.File(destinationPath)
    val tempBackupPath = "$destinationPath.tmp"

    if (destinationFile.exists()) {
        destinationFile.copyTo(java.io.File(tempBackupPath), overwrite = true)
    }

    try {
        FileOutputStream(destinationFile).use { outputStream ->
            driveService.files().get(fileId).executeMediaAndDownloadTo(outputStream)
        }
        java.io.File(tempBackupPath).delete() // Success, so delete the temp copy
    } catch (e: Exception) {
        // If restore fails, copy the temporary backup back
        val tempBackupFile = java.io.File(tempBackupPath)
        if (tempBackupFile.exists()) {
            tempBackupFile.copyTo(destinationFile, overwrite = true)
            tempBackupFile.delete()
        }
        throw Exception("Database restore failed: ${e.message}")
    }
}

Шаг 8: подключение к UI с ViewModel

В вашей ViewModel вы будете использовать GoogleDriveService для обработки логики, запускаемой действиями пользователя.

Процесс входа

Ваша Activity или Fragment запустит намерение входа из ViewModel и вернет результат.

// In your ViewModel
fun initiateGoogleSignIn() {
    // Expose this intent to your UI to be launched by an ActivityResultLauncher
    _uiState.update {
        it.copy(signInIntent = driveService.getSignInIntent())
    }
}

fun handleSignInResult(task: Task<GoogleSignInAccount>) {
    try {
        val account = task.getResult(ApiException::class.java)
        // Sign-in success, update UI with account info
    } catch (e: ApiException) {
        // Sign-in failed
    }
}

Шаг 9: резервное копирование

Это самый важный этап. Для безопасного резервного копирования базы данных Room необходимо закрыть подключение к базе данных перед доступом к файлу на диске.

После завершения резервного копирования перезапуск приложения — самый простой способ обеспечить корректное восстановление подключения к базе данных.

// In your BackupViewModel
private fun performBackup() {
    viewModelScope.launch {
        _uiState.update { it.copy(isBackingUp = true) }
        try {
            val account = _uiState.value.account ?: throw Exception("No account found")
            val dbPath = getApplication<Application>().getDatabasePath("your_app_database.db").absolutePath

            withContext(Dispatchers.IO) {
                // IMPORTANT: Close the database before accessing the file
                database.close()
                kotlinx.coroutines.delay(500) // Brief delay to ensure file handle is released
                driveService.uploadBackup(account, dbPath)
            }
            
            _uiState.update { it.copy(snackbarMessage = "Backup successful! Restarting...") }
            restartApp() // Helper function to restart the application

        } catch (e: Exception) {
            _uiState.update { it.copy(snackbarMessage = "Backup failed: ${e.message}") }
        } finally {
            _uiState.update { it.copy(isBackingUp = false) }
        }
    }
}

Шаг 10: запуск восстановления

Логика восстановления аналогична процессу резервного копирования. Мы закрываем базу данных, вызываем службу для загрузки и замены файла, а затем перезапускаем приложение для загрузки новых данных.

 // In your BackupViewModel
fun startRestore() {
    viewModelScope.launch {
        _uiState.update { it.copy(isRestoring = true) }
        try {
            // ... get account and fileId from uiState ...
            val destinationPath = getApplication<Application>().getDatabasePath("your_app_database.db").absolutePath

            withContext(Dispatchers.IO) {
                // IMPORTANT: Close the database before overwriting the file
                database.close()
                kotlinx.coroutines.delay(500)
                driveService.restoreBackup(account, fileId, destinationPath)
            }

            _uiState.update { it.copy(snackbarMessage = "Restore successful! Restarting...") }
            restartApp()

        } catch(e: Exception) {
            _uiState.update { it.copy(snackbarMessage = "Restore failed: ${e.message}") }
        } finally {
            _uiState.update { it.copy(isRestoring = false) }
        }
    }
}

Ключевые моменты и подсказки

  • appDataFolder — приватная папка — пользователи не увидят её в интерфейсе Диска
  • Всегда закрывайте базу данных Room перед копированием
  • Добавьте debug и release SHA-1 к своим учётным данным Cloud Console
  • Протестируйте на реальном устройстве с той же учётной записью Google, которую вы использовали для настройки OAuth
  • Если восстановление не удастся, ваш сервис уже сохранит .backup копию локально

Заключение

Менее чем 200 строк кода Kotlin позволят вам создать полноценную систему резервного копирования в Google Drive — без необходимости использования внешних серверов, без лишних пользовательских файлов и с полной конфиденциальностью данных.

Этот подход отлично подходит для приложений, использующих единую базу данных SQLite или Room и требующих резервного копирования в стиле WhatsApp.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: