Intro
이전 글에서 개발자 모드를 추가하는 방법에 대해서 간단히 작성해보았습니다.
아직 개발자 모드를 추가하지 않았다면 이전 글을 먼저 보고 와주시면 도움이 될 것 같습니다.
이번 게시글에선 개발자 모드 내에 각종 정보들을 포함하는 방법에 대해 작성해보고자 합니다.
해당 글을 참고하여 각자의 프로젝트에 맞게 응용 해보시면 좋을 것 같습니다.
이 글은 헤이딜러의 Ted Park님의 게시글을 보고 작성하게 되었습니다.
해당 내용을 기반으로 응용한 내용들을 작성한 것이니 같이 읽어보시는 것도 좋을 것 같습니다.
1. JWT 토큰 추가하기
개발자 모드를 추가하게 된 가장 큰 이유이기도 합니다.
서버 담당자 분들이 토큰을 요청했을 때 기존엔 직접 빌드를 해서 추출 했었기에, 안드 팀원들이 밖에 있으면 서버 분들은 무작정 기다릴 수 밖에 없었습니다.
이런 과정들이 너무 비효율적이라고 느껴졌고, 앱만 설치 되어있다면 바로 토큰을 전달할 수 있도록 개발자 모드를 추가하기로 다짐하였습니다.
구현 방법
토큰을 개발자 모드에 추가하는 것은 크게 어렵지 않았습니다.
저희 프로젝트에선 토큰을 Preferences에 저장하고 있었는데요, 이걸 간단히 출력해주기만 하면 끝이었습니다.
private fun initUserInfo() {
val ctx: Context = context ?: return
val accessToken = PreferenceManager.getString(ctx, TokenAuthenticator.TOKEN_KEY_ACCESS) ?: ""
val refreshToken = PreferenceManager.getString(ctx, TokenAuthenticator.TOKEN_KEY_REFRESH) ?: ""
setPreferenceSummary("dev_pref_key_access_token", accessToken)
setPreferenceSummary("dev_pref_key_refresh_token", refreshToken)
}
실행 결과
이렇게 토큰이 바로 보여지게 되고, 클릭하면 바로 클립보드에 복사되게 됩니다.
이로 인해 밖에 있어 노트북을 쓸 수 없는 상황이더라도 서버 분들에게 토큰을 바로 전달 할 수 있게 되었습니다.
2. 디바이스 정보 추가하기
현재 진행 중인 사이드 프로젝트에 QA 팀은 없지만 각자가 QA를 한다는 마음으로 임하고 있습니다.
저 같은 경우에는 여러 버전의 디바이스에서 테스트를 해보는 편인데, 이슈가 생겼을 때 버전과 모델명을 기록해두면 원인 파악에 조금 더 도움이 되는 것 같았습니다.
특히, 특정 디바이스에서만 발생하는 이슈를 파악할 때 가장 유용한 것 같아요
디바이스 정보를 개발자 모드에 추가 해두면 좋겠다 싶었고, 정말 간단하게 추가 할 수 있어서 필요 없더라도 추가해두는 것을 추천드립니다.
구현 방법
디바이스 정보를 추가하는 방법은 정말 간단합니다.
안드로이드 버전 : Build.VERSION.RELEASE
모델 명 : Build.BRAND, Build.MODEL
SDK 버전 : Build.VERSION.SDK_INT
private fun initDeviceInfo() {
setPreferenceSummary("dev_pref_android_version", Build.VERSION.RELEASE)
setPreferenceSummary("dev_pref_model_name", "${Build.BRAND} ${Build.MODEL}")
setPreferenceSummary("dev_pref_sdk_version", "${Build.VERSION.SDK_INT}")
}
실행 결과
코드 3줄만으로 디바이스 정보가 추가 되었습니다.
모델 명 말고 실제 기기 이름 (ex. Galaxy S22+)와 같이 출력하는 것도 봤는데, 이 부분은 어떻게 하는 지 조금 더 찾아봐야 할 것 같습니다,
(찾으면 글 수정하러 올게요)
3. 화면 정보 추가하기
저는 UI를 구현하게 되면 필수로 여러 해상도의 기기에서 테스를 해보는 편입니다.
안드로이드는 특히 폴드와 같은 이상한 해상도를 가진 휴대폰이 많기 때문에.. 웬만한 기기에서는 잘 나와도 폴드에서는 깨지는 경우가 많았습니다.
폴드 뿐만 아니라 구현을 어떻게 하느냐에 따라 화면 크기 별로 문제가 되는 경우가 많더라구요
그럴 때 기기의 화면 정보를 알아두면 UI 구현에 있어 조금 더 신경 쓸 수 있는 부분이 늘어나는 것 같았습니다.
구현 방법
화면 정보를 추가하는 것도 displayMetrics를 이용하면 어렵지 않게 추가할 수 있습니다.
단, 여기서 주의해야 할 점이 있습니다.
metrics에서 hegihtPixels을 구하면 상단바와 네비게이션 바의 높이는 제외된 화면의 높이가 측정 됩니다.
따라서 상단바와 네비게이션 바의 높이는 별도로 구하여 더해주어야 합니다.
화면 정보 코드
private fun initDisplayInfo() {
val metrics = activity?.resources?.displayMetrics ?: return
val windowManager = activity?.windowManager ?: return
val statusBarHeight = getStatusBarHeight(windowManager)
val naviBarHeight = getNaviBarHeight(windowManager)
with(metrics) {
setPreferenceSummary("dev_pref_display_ratio", "$widthPixels x ${heightPixels + statusBarHeight + naviBarHeight}")
setPreferenceSummary("dev_pref_display_density", "${densityDpi}dp")
setPreferenceSummary("dev_pref_display_resource_bucket", getDeviceResourseBucket(this))
}
}
글이 길어질 것 같으니 상단바와 네비게이션 바 높이 구하는 코드는 접어 놓도록 하겠습니다.
상단바 높이 구하기
private fun getStatusBarHeight(windowManager: WindowManager): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val windowMetrics = windowManager.currentWindowMetrics
val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.statusBars())
insets.top
} else {
val context = context ?: return 0
val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
context.resources.getDimensionPixelSize(resourceId)
} else {
0
}
}
}
네비게이션 바 높이 구하기
private fun getNaviBarHeight(windowManager: WindowManager): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val windowMetrics = windowManager.currentWindowMetrics
val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars())
insets.bottom
} else {
val context = context ?: return 0
val resourceId = context.resources.getIdentifier("navigation_bar_height", "dimen", "android")
if (resourceId > 0) {
context.resources.getDimensionPixelSize(resourceId)
} else {
0
}
}
}
실행 결과
4. 서버 모드 변경
이번에 추가한 개발자 모드에서 가장 핵심적인 기능 중 하나입니다.
현재 진행하는 사이드 프로젝트에는 3가지 종류의 서버가 운영되고 있습니다.
1. 상용 서버
2. 테스트 서버
3. 새로운(?) 상용 서버
상용 서버가 NodeJs로 되어 있는데, Spring Boot로 바꾼 후 이걸 상용 서버로 쓸 예정입니다.
그래서 현재는 3개의 서버가 운영되고 있는데요
기존에는 서버를 바꿀 때마다 local.properties를 직접 수정하여 재빌드를 해야 했습니다.
이런 부분도 너무 비효율적이라는 생각이 들었고, 서버 모드를 바꿀 수 있는 기능도 추가하게 되었습니다.
구현 방법
- 서버 모드를 Enum으로 정의 합니다.
- ListPreference에 서버 모드를 추가합니다.
- 서버를 선택하면 Preferences에 저장하고 앱을 종료(+ 로그아웃)합니다.
- 서버가 바뀌게 되므로 재로그인을 하도록 해주는게 좋습니다.
- 현재 선택된 서버의 주소를 Retrofit에 할당해줍니다.
서버 모드 Enum 정의
먼저 서버 모드들을 Enum으로 정의해줍니다.
해당 Enum 내에 현재 선택된 서버 모드를 가져오는 메소드도 추가해줍니다.
추후 BaseUrl을 지정할 때 사용하게 됩니다.
enum class ApiMode {
NODE,
TEST,
JAVA;
companion object {
private fun asValue(mode: String): ApiMode = when(mode.uppercase()) {
"NODE" -> NODE
"JAVA" -> JAVA
else -> TEST
}
fun getCurrentApiMode(context: Context): ApiMode {
return asValue(
PreferenceManager.getString(context, ApplicationClass.API_MODE) ?: ""
)
}
}
}
ListPreference 세팅
리스트 메뉴로 서버 모드를 추가하는 코드입니다.
서버 선택시 선택한 서버를 저장하고 앱 종료와 동시에 로그아웃 시킵니다.
private fun initApiMode() {
val ctx:Context = context ?: ApplicationClass.appContext
val currentApi = ApiMode.getCurrentApiMode(ctx)
findPreference<ListPreference>("dev_pref_key_api_mode")?.apply {
val entries = ApiMode.values().map { it.name }.toTypedArray()
val selectIndex = ApiMode.values().indexOf(currentApi)
this.entries = entries
this.entryValues = entries
title = currentApi.name
setValueIndex(selectIndex)
setOnPreferenceChangeListener { preference, newValue ->
val selectItem = newValue.toString()
this.title = selectItem
PreferenceManager.apply {
setString(ctx, ApplicationClass.API_MODE, selectItem)
setString(ctx, MySettingAccountInfoFragment.TOKEN_KEY_ACCESS, "none")
setString(ctx, MySettingAccountInfoFragment.TOKEN_KEY_REFRESH, "none")
}
destroyApp(ctx)
true
}
}
}
BaseUrl 세팅
선택한 서버에 따라 BaseUrl을 다르게 반환해주어야 합니다.
각 서버의 Url들은 BuildConfig를 통해서 관리하고 있습니다.
선택한 서버에 맞게 Url을 반환해주도록 구현하였고, 릴리즈 모드이거나 context가 초기화 되어있지 않은 경우 상용 서버를 반환하도록 해주었습니다.
private fun initApiMode() {
val currentApi = ApiMode.getCurrentApiMode(appContext)
PreferenceManager.setString(appContext, API_MODE, currentApi.name)
}
companion object {
lateinit var appContext: Context
const val API_MODE = "API_MODE"
fun getBaseUrl(): String {
return when {
!BuildConfig.DEBUG || !::appContext.isInitialized -> {
BuildConfig.RUNNECT_NODE_URL // 추후 Prod 서버로 변경
}
else -> {
val mode = ApiMode.getCurrentApiMode(appContext)
when(mode) {
ApiMode.JAVA -> BuildConfig.RUNNECT_PROD_URL
ApiMode.TEST -> BuildConfig.RUNNECT_DEV_URL
else -> BuildConfig.RUNNECT_NODE_URL
}
}
}
}
}
Retrofit에 BaseUrl 할당
마지막으로 Retrofit을 초기화할 때 getBaseUrl() 메소드를 이용해주면 됩니다.
이렇게 되면 선택한 서버 모드에 맞는 Url로 Api 통신이 이루어지게 됩니다.
@OptIn(ExperimentalSerializationApi::class, InternalCoroutinesApi::class)
@Provides
@Singleton
@Runnect
fun provideRunnectRetrofit(json: Json, client: OkHttpClient): Retrofit {
kotlinx.coroutines.internal.synchronized(this) {
val baseUrl = ApplicationClass.getBaseUrl()
val retrofit = Retrofit.Builder().baseUrl(baseUrl).client(client)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
return retrofit ?: throw RuntimeException("Retrofit creation failed.")
}
}
실행 결과
이제 원하는 서버를 선택하고 앱이 재실행 되면 앱은 해당 서버를 바라보게 됩니다.
마치며
이번 게시글에선 개발자 모드에 여러 기능을 추가하는 방법에 대해서 작성해보았습니다.
에러 로그를 기록하거나 서버 주소를 동적으로 설정하는 등 기능을 더 추가하고 개선하고 싶지만 아직까지는 오버스펙이라는 느낌이 없지 않아 있네요
이러한 기능들이 필요하다고 느껴질 때 추가해도 늦지 않을 것 같습니다.
메모용으로 글을 작성하긴 했는데 해당 내용을 활용해서 더 좋은 기능을 추가 해보는 것도 좋을 것 같습니다.
'안드로이드 > 이론' 카테고리의 다른 글
[ 안드로이드 ] 액티비티 배경 투명하게 설정 (+ 투명 배경 유지 안되는 이슈 해결) (0) | 2024.03.25 |
---|---|
[ 안드로이드 ] Multi ViewType RecyclerView ViewHolder 순서 고정하기 (0) | 2024.01.14 |
[ 안드로이드 ] 개발자의 실수를 줄여주는 어노테이션 (0) | 2023.12.12 |
[ 안드로이드 ] 앱 내에 개발자 모드 추가하기 (0) | 2023.12.08 |
[ 안드로이드 ] style.xml을 이용하여 공통 속성 정의하기 (0) | 2023.10.10 |