<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
: 네트워크 상태를 확인하여 전송 작업을 조정<uses-permission android:name="android.permission.INTERNET" />
: 인터넷 통신을 위한 필수 권한<service android:name="com.amazonaws.mobileconnectors.s3.transferutility.TransferService" android:enabled="true" />
: AWS S3 전송을 관리하는 서비스
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher_pres"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_pres_round"
android:supportsRtl="true"
android:theme="@style/Theme.Pie"
tools:targetApi="31">
<service
android:name="com.amazonaws.mobileconnectors.s3.transferutility.TransferService"
android:enabled="true" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".PresActivity" />
</application>
</manifest>
💡 AWS 자격 증명 보안 관리 방법
1. AWS의 Access key와 Secret key를 local.properties에 저장해 자격 증명 로드
2. BuildConfig에 자격 증명 설정
3. 추후 키 사용 시BuildConfig.설정한 Key명
형식으로 사용
📌 자격 증명 Key 숨기기 관련 포스팅
build.gradle.kt
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
// 자격 증명 로드
val localProperties = Properties()
localProperties.load(project.rootProject.file("local.properties").inputStream())
val awsAccessKey = localProperties.getProperty("awsAccessKey")?:""
val awsSecretKey = localProperties.getProperty("awsSecretKey")?:""
android {
namespace = "com.example.pie"
compileSdk = 34
defaultConfig {
applicationId = "com.example.pie"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// 자격 증명 설정
buildConfigField("String", "AWS_ACCESS_KEY", awsAccessKey)
buildConfigField("String", "AWS_SECRET_KEY", awsSecretKey)
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
dataBinding = true
viewBinding = true
buildConfig = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
// AWS S3 의존성
implementation("com.amazonaws:aws-android-sdk-s3:2.57.0")
implementation("com.amazonaws:aws-android-sdk-core:2.57.0")
// coroutines 의존성
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
}
💡 S3 다운로드 로직 최적화
withTimeout
: 작업 타임아웃을 설정하여 무한 대기 방지TransferListener
: 다운로드 상태 세밀하게 관리
🚨 파일을 동기로 받을 때 이슈
사진 파일을 받아올 때 시간이 60,0000ms 이상 걸리게 됨
⇒Coroutine
과suspendCancellableCoroutine
을 조합하여 비동기 작업의 중단 가능성을 보장
⇒ 800ms로 실행 시간 개선
💡 suspendCancellableCoroutine
kotlin.coroutines.suspendCancellableCoroutine은 코루틴에서 실행되는 작업이 중단 가능(cancellable)하도록 만들어줌핵심 기능
- 코루틴이 실행되는 동안 작업 취소를 요청받으면 즉시 중단 가능
Continuation
객체를 사용해 작업이 완료되거나 실패했을 시 코루틴을 재개(resume())하거나, 작업 취소 요청 시 중단(cancel()) 가능
S3Utils.kt
package com.example.pie
import android.content.Context
import android.util.Log
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.mobileconnectors.s3.transferutility.TransferListener
import com.amazonaws.mobileconnectors.s3.transferutility.TransferState
import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility
import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtilityOptions
import com.amazonaws.services.s3.AmazonS3Client
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import java.io.File
import java.util.Properties
import kotlin.coroutines.resume
object S3Utils {
private const val TAG = "S3Utils"
// S3 클라이언트 초기화
fun initializeS3Client(context: Context): TransferUtility {
val accessKey = BuildConfig.AWS_ACCESS_KEY
val secretKey = BuildConfig.AWS_SECRET_KEY
val bucketRegion = "ap-northeast-2"
// AmazonS3Client 생성
val awsCredentials = BasicAWSCredentials(accessKey, secretKey)
val s3Client = AmazonS3Client(awsCredentials)
s3Client.setRegion(com.amazonaws.regions.Region.getRegion(bucketRegion))
// 전송 설정(스레드 풀 크기) 정의
val options = TransferUtilityOptions()
options.transferThreadPoolSize = 5
return TransferUtility.builder()
.context(context)
.s3Client(s3Client)
.transferUtilityOptions(options)
.build()
}
// 파일 비동기 다운로드
suspend fun downloadFileFromS3Async(
transferUtility: TransferUtility,
bucketName: String,
fileKey: String,
destinationFile: File
): Boolean = withTimeout(30_000L) { // 30초 타임아웃
suspendCancellableCoroutine { continuation ->
val transferObserver = transferUtility.download(bucketName, fileKey, destinationFile)
transferObserver.setTransferListener(object : TransferListener {
override fun onStateChanged(id: Int, state: TransferState?) {
when (state) {
TransferState.COMPLETED -> {
Log.d(TAG, "Download completed for $fileKey")
continuation.resume(true)
}
TransferState.FAILED -> {
Log.e(TAG, "Download failed for $fileKey")
continuation.resume(false)
}
else -> Log.d(TAG, "Download state changed for $fileKey: $state")
}
}
override fun onError(id: Int, ex: Exception?) {
Log.e(TAG, "Error during download for $fileKey", ex)
continuation.resume(false)
}
// 다운로드 진행률 업데이트
override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {
if (bytesTotal > 0) {
val progress = (bytesCurrent * 100 / bytesTotal).toInt()
Log.d(TAG, "Download progress for $fileKey: $progress%")
}
}
})
}
}
}
MainActiviy.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// TransferNetworkLossHandler 초기화
TransferNetworkLossHandler.getInstance(applicationContext)
// S3 클라이언트를 생성 (파일 전송에 필요한 TransferUtility 객체)
val transferUtility = S3Utils.initializeS3Client(this)
val bucketName = "버켓 이름"
val fileKey = "파일루트/파일명"
val destinationFile = File(getExternalFilesDir(null), "downloaded_file.jpeg") // 로컬 저장 경로
binding.btnPrescription.setOnClickListener {
lifecycleScope.launch {
Log.d(TAG, "Download started...")
// S3 다운로드
val downloadTime = measureTimeMillis {
val isSuccess = withContext(Dispatchers.IO) {
S3Utils.downloadFileFromS3Async(
transferUtility, bucketName, fileKey, destinationFile
)
}
if (isSuccess) {
Log.d(TAG, "Download completed successfully.")
val intent = Intent(this@MainActivity, PresActivity::class.java)
intent.putExtra("filePath", destinationFile.absolutePath)
startActivity(intent)
} else {
Log.e(TAG, "File download failed!")
}
}
Log.d(TAG, "Download time: $downloadTime ms")
}
}
}
}
💡 비트맵(Bitmap)
- 이미지를 픽셀 단위로 표현하는 방식
- 각 픽셀의 색상 정보를 저장하여 그래픽을 렌더링하는 데 사용
PresActivity
class PresActivity : AppCompatActivity() {
private lateinit var binding: ActivityPresBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPresBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnBack.setOnClickListener {
finish()
}
val filePath = intent.getStringExtra("filePath")
if (filePath != null) {
val file = File(filePath)
if (file.exists()) {
// 이미지 파일을 Bitmap으로 변환하여 ImageView에 표시
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
binding.presImageView.setImageBitmap(bitmap)
}
}
}
}