Большинство приложений хранят важные пользовательские данные локально в Room или любом другом локальном хранилище, но что делать, если пользователь сменит телефон или переустановит ваше приложение? В этой статье вы узнаете, как за 10 простых шагов добавить функцию резервного копирования и восстановления на основе Google Drive в ваше Android-приложение, чтобы резервная копия оставалась конфиденциальной и надежно хранилась в облаке.
Мы будем использовать:
- Google Sign-In для аутентификации
- REST API Google Диска (v3) для загрузки/выгрузки
- корутины и ViewModel для фоновых операций
Что мы рассмотрим:
- Быструю настройку Google Cloud Console
- Создание GoogleDriveService для обработки всех взаимодействий API
- Подключение сервиса к вашему пользовательскому интерфейсу с помощью ViewModel
- Обработка базовой логики резервного копирования и восстановления
Шаг 1: быстрая настройка Google Cloud
- Перейдите в Google Cloud Console.
- Создайте новый проект или выберите существующий.
- Включите Google Drive API из библиотеки API.
- Перейдите в раздел 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, аутентифицированный с использованием вошедшего в систему пользователя.
Шаг 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.

