This commit is contained in:
2026-03-05 18:11:08 +09:00
parent 83546e5e10
commit 70869d7e1e
33 changed files with 1359 additions and 935 deletions
@@ -0,0 +1,24 @@
package bums.lunatic.utils.gdrive
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("bums.lunatic.utils.gdrive", appContext.packageName)
}
}
+1
View File
@@ -0,0 +1 @@
<manifest package="kr.bums.lunatic.utils.gdrive"/>
@@ -0,0 +1,76 @@
package kr.gdrive.bums.lunatic.utils
import android.content.Context
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.client.http.ByteArrayContent
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.drive.Drive
import com.google.api.services.drive.DriveScopes
import com.google.api.services.drive.model.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class GDriveBackupTask(
private val context: Context,
private val account: GoogleSignInAccount
) {
// 💡 백업 실행 (IO 스레드에서 돌아야 함)
suspend fun executeBackup(payload: BackupPayload): Result<String> = withContext(Dispatchers.IO) {
try {
val credential = GoogleAccountCredential.usingOAuth2(context, listOf(DriveScopes.DRIVE_APPDATA))
credential.selectedAccount = account.account
val driveService = Drive.Builder(
NetHttpTransport(), GsonFactory.getDefaultInstance(), credential
).setApplicationName("LunaticLauncherBackup").build()
// 1. 매니페스트 업데이트
uploadFile(driveService, "appDataFolder", "manifest.json", payload.manifestJson)
// 2. 폴더 가져오기 or 생성
val folderId = getOrCreateFolder(driveService, payload.folderName)
// 3. 파일들 업로드
payload.files.forEach { (fileName, jsonContent) ->
uploadFile(driveService, folderId, fileName, jsonContent)
}
Result.success("백업이 완료되었습니다. (${payload.folderName})")
} catch (e: Exception) {
e.printStackTrace()
Result.failure(e)
}
}
private fun getOrCreateFolder(driveService: Drive, folderName: String): String {
val query = "mimeType='application/vnd.google-apps.folder' and name='$folderName' and 'appDataFolder' in parents and trashed=false"
val fileList = driveService.files().list().setSpaces("appDataFolder").setQ(query).execute()
if (fileList.files.isNotEmpty()) return fileList.files[0].id
val folderMetadata = File().apply {
name = folderName
mimeType = "application/vnd.google-apps.folder"
parents = listOf("appDataFolder")
}
return driveService.files().create(folderMetadata).setFields("id").execute().id
}
private fun uploadFile(driveService: Drive, parentId: String, fileName: String, contentStr: String) {
val content = ByteArrayContent.fromString("application/json", contentStr)
val query = "name='$fileName' and '$parentId' in parents and trashed=false"
val fileList = driveService.files().list().setSpaces("appDataFolder").setQ(query).execute()
if (fileList.files.isNotEmpty()) {
driveService.files().update(fileList.files[0].id, null, content).execute()
} else {
val fileMetadata = File().apply {
name = fileName
parents = listOf(parentId)
}
driveService.files().create(fileMetadata, content).execute()
}
}
}
@@ -0,0 +1,79 @@
package kr.gdrive.bums.lunatic.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.Scope
import com.google.api.services.drive.DriveScopes
class GDriveLoginManager(
private val activity: ComponentActivity,
private val onLoginResult: (Boolean, GoogleSignInAccount?, String?) -> Unit
) {
// 필수 권한: 숨겨진 App Data 폴더 접근 권한
private val driveScope = Scope(DriveScopes.DRIVE_APPDATA)
private val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestScopes(driveScope)
.build()
private val signInClient = GoogleSignIn.getClient(activity, signInOptions)
// 액티비티 생성 시점에 등록되어야 하는 런처
private val signInLauncher: ActivityResultLauncher<Intent> =
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
try {
val account = task.getResult(Exception::class.java)
if (account != null && GoogleSignIn.hasPermissions(account, driveScope)) {
onLoginResult(true, account, null)
} else {
onLoginResult(false, null, "드라이브 접근 권한이 부족합니다.")
}
} catch (e: Exception) {
onLoginResult(false, null, "로그인 실패: ${e.message}")
}
} else {
onLoginResult(false, null, "로그인이 취소되었습니다.")
}
}
/**
* 이미 로그인된 유효한 계정이 있는지 확인 (백그라운드에서 유용함)
*/
fun getSignedInAccount(context: Context = activity): GoogleSignInAccount? {
val account = GoogleSignIn.getLastSignedInAccount(context)
return if (account != null && GoogleSignIn.hasPermissions(account, driveScope)) {
account
} else {
null
}
}
/**
* 로그인 화면 띄우기
*/
fun signIn() {
val account = getSignedInAccount()
if (account != null) {
onLoginResult(true, account, null)
} else {
signInLauncher.launch(signInClient.signInIntent)
}
}
/**
* 로그아웃 (연결 해제)
*/
fun signOut(onComplete: () -> Unit) {
signInClient.signOut().addOnCompleteListener { onComplete() }
}
}
@@ -0,0 +1,16 @@
package kr.gdrive.bums.lunatic.utils
sealed class GDriveState {
object Idle : GDriveState()
object StartingLogin : GDriveState()
object Uploading : GDriveState()
class Success(val message: String) : GDriveState()
class Error(val message: String, val exception: Exception? = null) : GDriveState()
}
// 메인 앱에서 백업할 데이터를 담아 보낼 데이터 클래스
data class BackupPayload(
val manifestJson: String, // 최상단에 저장될 버전 정보
val folderName: String, // 예: "2026-03-05" (날짜 폴더명)
val files: Map<String, String> // 파일명(키)과 JSON 텍스트(값)의 쌍
)
@@ -0,0 +1,17 @@
package bums.lunatic.utils.gdrive
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}