...
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,53 @@
|
||||
plugins {
|
||||
id ("com.android.library")
|
||||
id ("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "kr.bums.lunatic.utils.gdrive"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.activity:activity-ktx:1.8.2") // ActivityResultLauncher용
|
||||
|
||||
// 구글 로그인
|
||||
implementation("com.google.android.gms:play-services-auth:20.7.0")
|
||||
|
||||
// 구글 드라이브 API
|
||||
implementation("com.google.api-client:google-api-client-android:1.33.0") {
|
||||
exclude(group = "org.apache.httpcomponents")
|
||||
}
|
||||
implementation("com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0") {
|
||||
exclude(group = "org.apache.httpcomponents")
|
||||
}
|
||||
// implementation("androidx.core:core-ktx:1.17.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("com.google.android.material:material:1.13.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.3.0")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user