github Copilot에서는 IDE 지원이 JetBrains 제품들과 MS 제품들 위주만 공식 지원함.

 

Eclipse에서는 Genuitec에서 개발한 Copilot4Eclipse 플러그인을 사용해야 함.

https://marketplace.eclipse.org/content/copilot4eclipse

 

Copilot4Eclipse

Copilot4Eclipse (Copilot for Eclipse) is a free plugin that seamlessly integrates the GitHub Copilot AI developer tools into your Eclipse IDE to create a powerful AI-assisted coding experience.

marketplace.eclipse.org

 

Copilot4Eclipse는 Eclipse 2023-06(4.28) 및 최신 버전과 호환되며, 이러한 버전을 기반으로 구축된 Eclipse 기반 제품과도 호환됩니다.

  • Eclipse 4.28(2023-06 릴리스) 또는 최신 버전을 기반으로 하는 Eclipse IDE 또는 Eclipse 기반 제품입니다.
  • GitHub Copilot 구독( 개인 수준 또는 비즈니스 수준)

 

플러그인 설치

https://www.genuitec.com/products/copilot4eclipse/docs/installation/

 

플러그인 사용을 위한 Copilot 로그인

https://www.genuitec.com/products/copilot4eclipse/docs/basics/signin

 

코드 제안

https://www.genuitec.com/products/copilot4eclipse/docs/basics/completions

https://marketplace.eclipse.org/content/copilot4eclipse#screenshots

'IT > ETC' 카테고리의 다른 글

Copilot 리서칭  (1) 2024.10.17
[SonarQube] SonarQube & Jenkins 이야기  (0) 2020.02.14
[SonarQube] SonarQube 도입기?  (0) 2020.02.07
[Docker] Docker란?  (0) 2018.03.23
Expert Beginner란...  (0) 2018.03.12

Copilot이란?

OpenAI에서 만든 OpenAI Codex 모델을 사용한 실시간 코드 제안 AI 페어 프로그래머.

(OpenAI Codex란 자연어를 코드로 변환하는 머신러닝 모델로 미세조정된 GPT-3 버전 사용)

GitHub 공개 리포지토리에 있는 코드를 포함하여 공개적으로 사용 가능한 소스의 자연어 텍스트 및 소스 코드에 대해 교육 받음.

주석과 코드에서 컨텍스트를 가져와 기능 코드를 즉시 제안함.

GitHub Copilot이 개발자를 대체할 것으로 기대하지 않으며, 개발자와 협력할 것을 기대함.

  • 그들의 기능을 강화하고,
  • 생산성을 높이고,
  • 수동 작업을 줄이고,
  • 흥미로운 작업에 집중할 수 있도록 도와줄 것
  • 진입 장벽을 낮추고 더 많은 사람들이 소프트웨어 개발을 탐색하고 차세대 개발자에 합류할 수 있도록

Copilot 장점

상용구나 반복 코드 패턴 생성에 소요되는 시간을 줄여줌.

프로젝트의 컨텍스트 및 스타일 규칙을 기반으로 권장 사항을 공유함.

Neovim, JetBrains IDE, Visual Studio, Visual Studio Code 에서 플러그인 형태로 사용

특히 새로운 언어나 프레임워크에서 작업하거나 코딩을 배우는 중이라면 도움이 됨.

낯선 언어로 모든 것을 인터넷 검색하여 코딩하려고 하는 것은 외국어 숙어집만 가지고 외국을 항해하는 것과 같습니다. GitHub Copilot을 사용하는 것은 통역사를 고용하는 것과 같습니다.

Copilot 주의점

Copilot이 완변한 코드를 작성하지는 않음.

제안하는 코드를 테스트하지 않으며, 코드가 작동하지 않거나 의미가 없을 수도 있음.

안전하지 않은 코딩 패턴, 버그 또는 오래된 API 또는 관용구에 대한 참조가 포함될 수 있음.

신중한 테스트 및 코드 및 보안 검토 그리고 자신의 판단과 함께 사용해야 함.

책임은 항상 개발자에게 있음. (GitHub는 Copilot이 제공한 제안을 소유하지 않음)

공개 소스가 주로 영어로 되어 있어 자연어 프롬프트가 영어가 아니거나 문법적으로 잘못된 시나리오에서 제대로 작동하지 않을 수 있음.

Copilot 보안

GitHub의 공개 코드가 포함된 제안을 필터로 막을 수 있음.

(개인 설정이나 엔터프라이즈 관리자가 관리)

제안되었던 코드가 공유되거나 재사용되지는 않음.

경우에 따라 개인 데이터로 보이는 것을 제안하지만 이것은 가상 정보임.

Copilot 제안을 사용하여 저작권 침해로 고발되면? 제품별 약관에 제공된 대로 변호해준다고 함.

Copilot 관련 데이터

사용자 초기 관찰 및 인터뷰 결과 (2,000명 이상의 개발 대상)

  • 개발자 만족도 향상 : 사용자의 60~75% 작업에 만족, 코딩시 좌절감 덜 느끼고 더 만족스러운 작업에 집중할 수 있었음.
  • 정신 에너지 절약 : 흐름을 유지하고(73%) 반복적인 작업 중에 정신적 노력을 보존하는데 도움이 되었음(87%)

Copilot에 표시된 모든 완료의 평균 26%가 수락을 함.

(Python과 같은 특정 언어에서는 최대 40%까지 생성)

Copilot 잘 부려먹는 법

작은 함수로 나누고, 매개변수에 의미있는 이름을 사용

좋은 doc와 주석을 작성

익숙하지 않은 라이브러리나 프레임워크 탐색시 사용

Copilot 과금

  • 개인
    • 월 10$, 연 100$
    • 30일 평가판 사용 가능 (⚠️자동으로 유료 구독으로 전환됨)
    • 학생, 교사나 유명 오픈소스 리포지토리 관리자는 무료 구독 가능
  • 비즈니스
    • 인당 월 19$

Android Studio Plugins

Plugins에서 GitHub Copilot 설치

https://docs.github.com/ko/copilot/getting-started-with-github-copilot

 

 

Android Studio + Copilot

주석으로 한국어로 할 일을 적고 Copilot의 제안 받아보기

번역이 필요한 경우

테스트 코드 작성시

정규표현식 패턴 매칭

알고리즘 풀이

Android Room 사용하기

Android Activity xml 생성

 

참고문서

https://github.com/features/copilot/

https://docs.github.com/ko/copilot/getting-started-with-github-copilot

https://dev.to/github/a-beginners-guide-to-prompt-engineering-with-github-copilot-3ibp

https://github.blog/2022-09-14-8-things-you-didnt-know-you-could-do-with-github-copilot/

https://plugins.jetbrains.com/plugin/17718-github-copilot

 

'IT > ETC' 카테고리의 다른 글

Eclipse Copilot  (0) 2024.10.17
[SonarQube] SonarQube & Jenkins 이야기  (0) 2020.02.14
[SonarQube] SonarQube 도입기?  (0) 2020.02.07
[Docker] Docker란?  (0) 2018.03.23
Expert Beginner란...  (0) 2018.03.12

https://www.droidknights.dev/

 

드로이드나이츠 2024

대한민국 최대 안드로이드 개발자들만을 위한 컨퍼런스 드로이드나이츠 2024 입니다. 주니어부터 시니어까지 모두가 공감하고 즐길 수 있는 지식의 장으로 만들고자 합니다.

www.droidknights.dev

 

지난 6월 11일에 드로이드나이츠 2024 행사가 있어 다녀왔습니다.

오랜만에 개발자 컨퍼런스를 가다보니 코엑스에서 길도 헤매기도 했고, 겨우 찾은 현장에서는 예전보다 다소 줄어든 부스들을 보며 여러 감정이 들었습니다.

몇년 전까지는 Kotlin, Coroutines, MVVM 등의 주제들이 주를 이루었던 것 같은데요.

이번 컨퍼런스에서는 Compose가 관심을 많이 끄는 주제가 되었던 것 같습니다.

대충 들었던 세션들의 간략한 정리를 해볼까 합니다.

 

키노트

안드로이드 개발자이시자 빅네임이신 박상권님이 나오셔서 드로이드나이츠 소개를 해주셨습니다.

 

Compose 성능 최적화를 위한 Stability 마스터하기

Compose 동작 구조와 Recomposition의 효율적인 사용을 위한 부분들을 소개해주셨습니다.

Compose 사용이 점차 많아지면서 개발자 문서 정독 후 들으면 좋을 내용이었습니다.

유튜브 영상도 업로드 되어 개인적으로 시간 날때 다시 정독할 첫번째 세션입니다.

 

시니어와 주니어의 협업 다리: 온라인 및 오프라인 페어코딩의 통찰

페어 프로그래밍을 한 학습 및 경험담에 대해 소개해주셨습니다.

페어코딩을 하기 위한 각자의 룰이나 필요한 자세 등을 알려주었습니다.

페어코딩시 주의할 점이라던지 장단점 등 참고할 좋은 레퍼런스를 만들어 주신 것 같네요.

 

Compose UI 컴포넌트 설계와 테스트

물론 개발자 문서에도 잘 정리가 되어 있지만, 이 세션을 본다면 훨씬 이해가 용이해질 듯 싶습니다.

Compose 테스트에 대한 궁금증도 있었는데, 참고가 많이 되었습니다.

 

Github Actions로 효율적인 배포 환경 만들기

때마침 업무적으로 하고 싶은 주제 중 하나였던 Github Actions 세션이었습니다.

Github Actions를 이용한 다양한 활용에 대해서 소개해주셨습니다.

조만간 다시 정독하면서 활용 아이디어를 구상하고자 합니다.

 

당신의 앱 빌드는 안녕하십니까?

평소에 가장 많이 접하는 작업이면서 제대로 알기엔 뭔가 서먹한 빌드 관련 세션입니다.

세션에 소개된 내용을 안다고 업무 환경에 바로 큰 개선은 어렵겠지만 참고할만한 정보가 참 많았습니다.

 

Compose로 Animation 만들기 feat.holgraphic Card

포켓몬카드로 시작된 애니메이션 개발 연대기 세션입니다.

목표를 설정하고 문제해결을 위해 노력한 시간들이 엿보이며, 센스 있는 발표도 좋았습니다.

 

 

후기

역시나 이런 행사를 다녀오면 다시 찾아봐야할 것들, 공부할 것들이 쌓입니다.

조만간 하나씩 다시 보면서 좋은 시간을 가져봐야 겠습니다.

'IT > 행사' 카테고리의 다른 글

[행사] 2019 NHN Forward  (3) 2019.11.27
[행사] Naver Tech Concert Day-2 요약  (0) 2019.01.14
[행사] Naver Tech Concert Day-1 요약  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 06  (1) 2019.01.14
[행사] Naver Tech Concert Day-2 05  (0) 2019.01.14

오랫만에 데이터바인딩 설정을 잡을 일이 있어서 안드로이드 개발 문서를 살펴보던 중이었습니다.

https://developer.android.com/topic/libraries/data-binding/start?hl=ko

시작하기  |  Android 개발자  |  Android Developers

시작하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 스튜디오의 데이터 결합 코드 지원을 비롯하여 개발 환경에서 데이터 결합 라이브러리를 함

developer.android.com

 

android {
        ...
        dataBinding {
            enabled = true
        }
    }

New Project로 빈 프로젝트를 만듭니다.

(최근의 Android Studio가 권장하는 Kotlin DSL (build.gradle.kts)를 선택하였습니다.)

그리고 개발 문서의 가이드대로 데이터바인딩 설정을 해봅니다.

하지만 build.gradle이 불이 붙은 채로 꺼지지 않습니다.

 

 

엇.. 뭐지? 이런 저런 것들을 건드려 보고, StackOverflow를 뒤져보고요.

구글께서 시키는 대로 하였는데, 왜 나에게 이런 시련을 주시지? 라는 믿음이 흔들리는 마음도 먹어봅니다.

불현듯 생각나서 개발 문서를 다시 확인해봅니다.

 

한국어???

설마???

 

오잉????

 

다시 영문 버전으로 바꿔봅니다.

https://developer.android.com/topic/libraries/data-binding/start?authuser=1

android {
    ...
    buildFeatures {
        dataBinding true
    }
}

 

코드가 달라졌습니다......

 

잊고 있었습니다.

많은 선후배 동료님들의 개발문서의 한글버전을 믿지 말라는 그 말을요....

 

히스토리를 찾아보니 Android Gradle Plugin 4.0 버전부터의 차이가 있었네요.

https://developer.android.com/build/releases/past-releases/agp-4-0-0-release-notes?authuser=1#buildFeatures

 

Android 스튜디오  |  Android Developers

Android Gradle 플러그인 4.0.0 출시 노트

developer.android.com

 

기존 방식은 deprecated가 되었고, buildFeatures의 dataBinding으로 설정하는 방법으로요.

제 어리석음을 탓하며 시간낭비 삽질을 한 케이스였습니다.

최근에야 많이들 Compose를 쓰시느라 볼 일이 별로 없으시겠지만요...

혹여나 저처럼 방황하실 분들을 위해 남겨봅니다. :)

이전 회사에서 네이버 지도를 사용하다 발생하였던 이슈에 대해서 정리를 한번 하고자 합니다.

개발 상에서는 이슈가 전혀 없던 앱이 상용 배포 후 Crashlytics를 통해 MissingLibraryException를 종종 발생한다는 이슈였었죠.

 

발단

네이버 지도를 붙인 기능이 배포된 후 얼마 후 Crashlytics를 통해서 이상한 오류를 잡아내기 시작합니다.

해당 오류에서 가르키는 부분들은 개발 중에는 본 적이 없는 키워드 들이었습니다.

Caused by com.getkeepsafe.relinker.MissingLibraryException
Could not find 'libnavermap.so'. Looked for: [arm64-v8a, armeabi-v7a, armeabi], but only found: [].

 

멀쩡히 잘 넣어둔 라이브러리가 실종이라니 처음에는 황당한 이슈였습니다.

 

분석

어쨋든 발생하고 있는 이슈이니 분석을 해봅니다.

발견 당시 기준으로 소수의 사용자들이 반복적으로 크래쉬를 경험하고 있었습니다.

단말도 넥서스, 픽셀나 에뮬레이터로 보이는 단말명들이었죠.

 

그래서 첫번째 추정을 하였습니다. 특정 칩셋을 쓰는 단말들에서만 발생하는건가??

그러면서 추가 수집된 정보로 arm, x86 단말들이 섞여 있었죠.

사무실에 가지고 있는 단말과 에뮬레이터로 테스트 해보았지만 이상이 없었습니다.

애초에 네이버 지도가 arm, x86을 다 지원하고 있으므로 문제가 될 것이 없었죠.

 

구글플레이에 업로드하였던 aab 파일을 받아서 다시 까보았습니다.

amr, x86 so 파일들이 다소곳이 잘 들어가 있었습니다.

첫번째 추정은 정답이 아니었습니다.

 

두번째 추정을 해보았습니다. 누군가 앱 설치 후 APK를 백업하여 재설치를 해본건가??

저희는 당시 apk가 아닌 aab로 버전을 업로드하였습니다.

aab는 split apk를 통해 다운받는 단말에 필요한 조각들로 앱이 설치됩니다.

https://developer.android.com/guide/app-bundle/app-bundle-format

 

Android App Bundle 형식  |  Android 개발자  |  Android Developers

Android App Bundle 형식 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android App Bundle은 Google Play에 업로드하는 파일(파일 확장자는 .aab)입니다. App Bundle은 그림 1

developer.android.com

하지만 설치된 단말과 백업한 앱을 다시 설치한 단말의 칫셉이 다르다면? 혹은 split apk의 base apk만 설치된다면??

한번 설치된 앱의 APK를 추출하여 다른 단말에 설치 후 실행해봅니다.

두둥. 재현이 되기 시작합니다.

테스트

당시의 테스트 방법이었습니다.

1. 일반
    1-1. 구글플레이를 통해 설치한 앱의 APK 를 추출.
        a. CX파일탐색기 같은 앱 사용하여 APK 추출. (구글플레이 검색)
        b. Android Studio의 Device File Explorer 등을 사용하여 APK 복사
    1-2. AAB로 업로드된 앱을 설치 후 1번 방법으로 APK 를 추출하면 정상적으로 설치가능한 APK가 나오지 않음.
    1-3. 추출한 APK 속의 base.apk 를 추출하여 단말에 설치.
    1-4. 앱 실행. (이때 크래시 발생)


2. aab로 split apks 변환 후 테스트시
    2-1. bundletool을 이용해 aab 파일을 split apks로 변환
        a. bundletool 다운로드 : https://github.com/google/bundletool/releases
        b. 아래 명령어로 변환

        java -jar bundletool-all-1.14.0.jar build-apks \
          --bundle=app-googlePlayStore-dev-debugWithProguard.aab \
          --output=output.apks \
          --connected-device

    2-2. output.apks 파일을 zip 파일로 확장자 변경 후 압축 해제
    2-3. splits 폴더 안의 base-master.apk 을 단말에 설치
    2-4. 앱 실행. (이때 크래시 발생)


3. split apks 설치 방법 (만약에 split apks를 정상적으로 설치해보고 싶다면!!)
    1. adb install-multiple *.apk

 

해결?

원인은 확인하였지만, 당시에 이슈를 바로 해결할 수가 없었습니다.

다음과 같은 이유에서였죠.

  • 확인 결과 MissingLibrary는 네이버 맵 SDK에서 사용하는 라이브러리에서 제공하는 것임.
  • 네이버 맵 SDK에서 해당 부분을 사용하는 부분은 static 영역인데, static 영역이 초기화 중에 exception이 발생하여 ExceptionInInitializerError가 발생함.
  • Exception이 아니라 Error라 핸들링할 수가 없음.

네이버 맵 SDK 에서 수정되지 않는 이상 Exception 핸들링도 불가하였고, 일반적이지 않고 극소수의 이슈라 우선순위도 낮아 다른 업무에 조금씩 치여가고 있었습니다.

그리고 네이버 클라우드 플랫폼에 해당 현상을 문의하였고, 답변에서도 비정상적 활동이고 빈도가 높지 않을 것으로 보이니 무시하는 것을 권장한다고 하였죠. 나름 뭉갤 수 있는 정당성이 부여되었다라고 죄책감을 조금 내려놓을 때 쯔음....

 

불현듯이 찾아온 컨퍼런스 영상 속에서 그 해답을 찾게 되었습니다.

[드로이드나이츠 2021] 차영호 - AppBundle 괴담 영상 속에서 제가 필요로 하던 답이 있었습니다.

https://youtu.be/EVYnTe6aXWQ?t=706

 

그리고 위 이슈를 SideLoading이라는 키워드로 부르는 것을 알게 되었고, 아래와 같은 포스팅에서도 도움을 얻게 되었습니다.

 

App Bundle and Sideloading: how to prevent crashes

Read here, why the new Google Android App Bundle format can cause sideloading crashes when using a native library. Learn how to fix it.

objectbox.io

 

 

덕분에 최종 수정은 아래와 같이 하였습니다.

  • 앱 실행시 split apk 체크로 문제가 인지되면, 토스트 띄우고 앱 종료함.
  • split apk 체크로직은 앱이 build 타입이 release일때만 체크하도록 함.
fun Context.checkSplitApks(): Boolean {
    val splitNames = packageManager.getPackageInfoCompat(packageName, 0).splitNames ?: emptyArray()

    // 네이버 지도 라이브러리는 arm/x86으로 split apk 확인이 가능하네 ㅎㅎ
    val armPattern = "arm"
    val x86Pattern = "x86"

    // split apk 추출 후 base or master apk만 설치시 dpi가 없으니 확인하자!!
    val dpiPattern = "dpi"
    return splitNames.any { it.contains(armPattern) || it.contains(x86Pattern) }
            && splitNames.any { it.contains(dpiPattern) }
}

 

추가로 해당 처리로 앱 종료 처리된 경우 Crashlytics 로그 추가하여 추이를 보기로 하였습니다.

위 수정사항으로 더이상 저런 헤괴망측한 오류는 나오지 않았습니다.

하지만 간헐적으로 종종 로그는 수집되고 있었답니다.

오늘의 괴담은 여기까지입니다. ^^

Jenkins로 Google Play에 APK, AAB를 업로드하기 위해서는 플러그인 설치 및 Google Play 콘솔에서 서비스 계정 설정이 필요함.

 

[Google Play] Google Play 콘솔 로그인 후 설정 - API 액세스 선택

 

[Google Play] 새 프로젝트 만들기 선택 후 저장

 

[Google Play] 프로젝트 보기 선택하여 Google Cloud Platform 으로 이동

 

[Google Cloud ] 메뉴 - IAM 및 관리자 - 서비스 계정 선택

 

[Google Cloud Platform] + 서비스 계정 만들기 선택

 

[Google Cloud Platform] 서비스 계정 세부 정보의 서비스 계정 이름, 서비스 계정 설명 등 기입 후 만들고 계속하기 선택

 

[Google Cloud Platform] 이 서비스 계정에 프로젝트에 대한 액세스 권한 부여 에서 역할소유자 선택 후 계속

 

[Google Cloud Platform] 사용자에게 이 서비스 계정에 대한 액세스 권한 부여 에서는 필요시 추가 후 완료

 

[Google Cloud Platform] 생성된 서비스 계정을 선택

 

[Google Cloud Platform] 키 탭을 선택 후 키 추가 선택

 

[Google Cloud Platform] 새 키 만들기 - JSON - 저장 선택하여 json 파일 저장

 

 

[Google Play] 다시 Google Play 콘솔로 넘어와서 설정 - API 액세스 에서 서비스 계정의 서비스 계정 새로고침 선택

 

[Google Play] 서비스 계정에서 권한 부여 선택

 

[Google Play] 사용자 초대에서 계정 권한 탭에서 출시 관련 항목들 선택

 

[Google Play] 앱 권한 탭 선택 후 **애플리케이션 추가**로 앱을 선택

 

[Google Play] 앱 추가시 권한 확인 후 적용 선택

 

[Google Play] 사용자 초대 선택

 

[Google Play] 사용자 초대 완료 후 추가된 사용자 확인

 

[Jenkins] Jenkins 플러그인 설정에서 Google Play Android Publisher Plugin 을 설치

 

[Jenkins] Add Credentials 에서 Google Service Account from private key 선택

 

[Jenkins] Project Name 기입 후 GCP에서 서비스 계정 생성시 저장한 json 파일을 선택한 후 OK 선택

 

[Jenkins] 업로드할 job 에서 플러그인과 추가한 Credentials로 업로드 설정

  • Pipeline 사용시 Jenkinsfile에 추가
stage('Publish artifacts') {
    steps {
        androidApkUpload googleCredentialsId: 'test',
            apkFilesPattern: '**/outputs/**/*.aab',
            trackName: 'internal',
            rolloutPercentage: '100'
    }
}
  • job 설정에서 직접 설정시
    • 빌드 후 조치에 Upload Android AAB/APKs to Google Play 추가
    • Google Play account - Specific credentials 선택 후 추가하였던 Credentials ****의 Project Name 선택
    • APK/AAB 위치 설정
    • 배포할 트랙 선택, 단계적 배포 수준, In-app Update priority 등을 설정
    • 업데이트 문구도 설정 가능함
    • 설정 저장 후 사용

  •  
결론부터 말하자면 View의 Background를 리소스로 설정시에 color 리소스가 아닌 drawable 리소스만을 사용해야 합니다.
(물론 대다수의 경우들은 drawable만 사용합니다....)

Android UI 작업을 하다보면 View의 Background를 설정하는 경우는 매우 흔한 일입니다.

이때 pressed 효과를 위해 StateListDrawable 형태의 drawable 리소스를 쓰는 경우도 많이 생깁니다.

그리고 Android OS 10에서부터는 ColorStateList 형태의 color 리소스도 View의 Background에서 사용이 가능해졌습니다.

(흔히 말하는 selector 리소스 형태들입니다.)

  • ColorStateList 리소스 : https://developer.android.com/guide/topics/resources/color-list-resource
    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="<http://schemas.android.com/apk/res/android>">
        <item android:color="@color/white" android:state_selected="true" />
        <item android:color="@color/white" android:state_pressed="true" />
        <item android:color="@color/black" />
    </selector>
    color/selector_rect_color.xml
  • Drawable 리소스 : https://developer.android.com/guide/topics/resources/drawable-resource
    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="<http://schemas.android.com/apk/res/android>">
        <item android:drawable="@color/white" android:state_selected="true" />
        <item android:drawable="@color/white" android:state_enabled="true" />
        <item android:drawable="@color/black" />
    </selector>
    drawable/selector_rect.xml

즉 최신 Android OS에서 ColorStateList 리소스로 View.setBackgroundResource() 를 사용할 수 있지만, OS 9 이하에서는 Resource NotFoundException을 겪게 됩니다.

1. 문제 재현해보기

먼저 문제를 재현해 보고, 정확히 문제가 되는 부분을 찾아봅니다.

try {
    val drawable = context.getDrawable(R.drawable.selector_rect)
    Log.d(TAG, "ResourceTest :: 0. $drawable")
    val drawable2 = context.getDrawable(R.color.selector_rect_color)
    Log.d(TAG, "ResourceTest :: 1. $drawable2")
} catch(e: Exception) {
    Log.d(TAG, "ResourceTest :: 2. $e")
}

앱에서 테스트할 부분에 위 코드를 추가 후 OS 10과 OS 9 기기에 올려서 확인해봅니다.

(기기가 없어도 에뮬레이터로 확인 가능합니다.)

각가 아래와 같은 로그를 얻을 수 있습니다.

  • Android OS 10
ResourceTest :: 0. android.graphics.drawable.StateListDrawable@ba98d70
ResourceTest :: 1. android.graphics.drawable.ColorStateListDrawable@c5c796e
  • Android OS 9
ResourceTest :: 0. android.graphics.drawable.StateListDrawable@e71743d
ResourceTest :: 2. android.content.res.Resources$NotFoundException: Drawable com.test.app:color/selector_rect_color with resource ID #0x7f0601c3

 

2. 문제 범위 확인

문제 발생시 CallStack을 확인하여 비교할 코드 범위를 좁혀봅니다.

android.content.res.Resources$NotFoundException: Drawable com.test.app:color/selector_rect_color with resource ID #0x7f0601c3
Caused by: android.content.res.Resources$NotFoundException: File res/color/selector_rect_color.xml from drawable resource ID #0x7f0601c3
    at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:847)
    at android.content.res.ResourcesImpl.loadDrawable(ResourcesImpl.java:631)
    at android.content.res.Resources.getDrawableForDensity(Resources.java:888)
    at android.content.res.Resources.getDrawable(Resources.java:827)
    at android.content.Context.getDrawable(Context.java:626)
    ...
Caused by: org.xmlpull.v1.XmlPullParserException: Binary XML file line #3: <item> tag requires a 'drawable' attribute or child tag defining a drawable
    at android.graphics.drawable.StateListDrawable.inflateChildElements(StateListDrawable.java:190)
    at android.graphics.drawable.StateListDrawable.inflate(StateListDrawable.java:122)
    at android.graphics.drawable.DrawableInflater.inflateFromXmlForDensity(DrawableInflater.java:142)
    at android.graphics.drawable.Drawable.createFromXmlInnerForDensity(Drawable.java:1332)
    at android.graphics.drawable.Drawable.createFromXmlForDensity(Drawable.java:1291)
    at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:833)
	... 20 more

위 로그로 ResourcesImpl.loadDrawableForCookie 에서 Exception을 발생함을 확인할 수 있습니다.

 

3. AOSP를 통한 소스 확인

이 부분은 aosp 소스로 그 차이를 확인할 수 있습니다.

	@Nullable
    private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
    ...
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
        LookupStack stack = mLookupStack.get();
        try {
            // Perform a linear search to check if we have already referenced this resource before.
            if (stack.contains(id)) {
                throw new Exception("Recursive reference in drawable");
            }
            stack.push(id);
            try {
                if (file.endsWith(".xml")) {
                    final XmlResourceParser rp = loadXmlResourceParser(
                            file, id, value.assetCookie, "drawable");
                    dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
                    rp.close();
                } else {
                    final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    AssetInputStream ais = (AssetInputStream) is;
                    dr = decodeImageDrawable(ais, wrapper, value);
                }
            } finally {
                stack.pop();
            }
        } catch (Exception | StackOverflowError e) {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            final NotFoundException rnf = new NotFoundException(
                    "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
            rnf.initCause(e);
            throw rnf;
        }
    ...

 

@Nullable
    private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
    ...
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
        LookupStack stack = mLookupStack.get();
        try {
            // Perform a linear search to check if we have already referenced this resource before.
            if (stack.contains(id)) {
                throw new Exception("Recursive reference in drawable");
            }
            stack.push(id);
            try {
                if (file.endsWith(".xml")) {
                    if (file.startsWith("res/color/")) {
                        dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
                    } else {
                        dr = loadXmlDrawable(wrapper, value, id, density, file);
                    }
                } else {
                    final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    AssetInputStream ais = (AssetInputStream) is;
                    dr = decodeImageDrawable(ais, wrapper, value);
                }
            } finally {
                stack.pop();
            }
        } catch (Exception | StackOverflowError e) {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            final NotFoundException rnf = new NotFoundException(
                    "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
            rnf.initCause(e);
            throw rnf;
        }
    ...
    private Drawable loadColorOrXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
            int id, int density, String file) {
        try {
            ColorStateList csl = loadColorStateList(wrapper, value, id, null);
            return new ColorStateListDrawable(csl);
        } catch (NotFoundException originalException) {
            // If we fail to load as color, try as normal XML drawable
            try {
                return loadXmlDrawable(wrapper, value, id, density, file);
            } catch (Exception ignored) {
                // If fallback also fails, throw the original exception
                throw originalException;
            }
        }
    }
    ...
    private Drawable loadXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
            int id, int density, String file)
            throws IOException, XmlPullParserException {
        try (
                XmlResourceParser rp =
                        loadXmlResourceParser(file, id, value.assetCookie, "drawable")
        ) {
            return Drawable.createFromXmlForDensity(wrapper, rp, density, null);
        }
    }

 

위 소스 확인으로 OS 10 이후부터는 ColorStateList 리소스도 처리가 가능하지만 그 미만 버전에서 문제가 되는 경우가 발생하는 부분을 확인할 수 있습니다.

 

컴파일시 에러가 나거나 버전 분기가 필요한 부분으로 체크 되지도 않기 때문에 생각없이 지나치기 쉬운 부분이 될 수도 있습니다.

만약 앱의 minSdkVersion이 29 미만이라면 항상 위 내용을 주의할 필요가 있습니다.

그야말로 별 생각없이 단순작업을 하다 벌어진 일이었습니다.

Java로 작성된 코드들을 일부 Kotlin으로 변환하고 문법을 조금씩 손을 보는 작업을 진행 중이었죠.

컴파일 오류도 없고 빨간 줄 하나 없이 잘 빌드되었고 문제가 없는 줄 알았습니다.

하지만 앱에는 내부적으로 문제가 있었죠. 그 원인은 이런 것이었습니다.

 

Java 클래스 2개가 있었고 일부 동일한 멤버변수들이 가지고 있는 클래스들이었습니다.

이 클래스들을 서로 다른 레이어의 Model로 있으면 레이어 간 데이터 전달시 서로 Mapping되는 역할이 있었죠.

그러다보니 동일한 멤버들을 서로의 멤버에 부어서 변환을 해주는 메서드들이 있었고요.

여기서 apply로 빌더처럼 데이터를 전달하다가 문제가 발생하였습니다.

 

해당코드를 샘플로 다시 작성 해봤습니다.

class Human {
    var sleep: Int = 0
    var eat: Int = 0
    var play: Int = 0
}

class Student {
    var sleep: Int = 0
    var eat: Int = 0
    var study: Int = 0
    
    fun getHuman(): Human {
        return Human().apply {
            sleep = sleep
            eat = eat
        }
    }
}

Human 과 Student 클래스가 있고 Student 클래스를 Human으로 맵핑하면서 동일한 데이터를 전달하는 과정입니다.

아래를 보면 바로 Human 생성 후 return을 하는 것이 보이고 apply로 필요한 데이터를 전달하는 것을 볼 수 있습니다.

하지만 이때 sleep과 eat 변수들을 주의깊게 보질 않았던 것이 문제였습니다.

        return Human().apply {
            sleep = sleep
            eat = eat
        }

위에서 보이는 sleep과 eat는 apply 내에서 this 즉 Human의 멤버들을 뜻하게 됩니다.

그래서 제가 Human에 넘기려던 Student의 데이터들이 제대로 복사가 안되었던 것입니다.

this.sleep = sleep 도 결과는 동일하였습니다.

그래서 아래와 같이 변경하였습니다.

        return Human().also {
            it.sleep = sleep
            it.eat = eat
        }

apply를 also로 변경하고 Human은 it으로 명시적으로 지정을 해주었습니다.

Kotlin의 Scope 함수를 잘 이해하고 조금만 신경 썼으면 틀릴 부분은 아니었습니다.

다만 IDE에서도 문법적으로는 문제가 없어 컴파일 오류로 잡아주질 않아 자칫 잘못하면 헤매기 쉬웠던 문제였죠.

문법들이 편해지는 만큼 방심하기 쉬워지는 것을 다시 한번 느끼긴 했습니다.

 

// 오랫만에 남기는 포스팅이라 뭐라 끝을 맺을지는 모르겠습니다... 그냥 끝!!!

Android WebView는 OS 버전에 따라 몇번의 변화가 있었습니다.

Android 10에서도 또 한번의 변화가 있어 간단히 정리해봤습니다.

Android 4.4 (Kitkat) 이전

커스텀하게 내장된 엔진을 사용하였으며, OS 업데이트시만 업데이트 되었습니다.

Android 4.4 (Kitkat)

Chromium 특정 버전을 사용하며, OS 업데이트시만 업데이트 되었습니다.

Android 5 (Lollipop)

OS 번들이 아니라 System WebView 로 제공되었으며, Play Store를 통해 업데이트가 되었습니다.

Android 7 (Nouga)

Chrome 앱의 엔진이 WebView에 사용되었으며, Chrome 앱 업데이트로 업데이트시 WebView도 업데이트가 되었습니다.

기존 System WebView와 Chrome WebView 중 선택이 가능합니다.

Android 10 (Q)

다시 System WebView로 제공되었고 Play Store를 통해 업데이트가 되었습니다.

System WebView Canary/Dev/Beta 버전들이 제공되며, 선택이 가능합니다.

Chrome WebView는 제공하지 않습니다.

관련 링크

https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/prerelease.md#Android-10-Q

https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/channels.md

https://www.androidpolice.com/2019/10/24/android-10-no-longer-uses-chrome-app-to-render-pages-inside-apps/

https://www.xda-developers.com/google-chrome-no-longer-webview-provider-android-10/

참고링크 : https://developer.android.com/training/location/geofencing#kotlin

개요

Android 에서는 Geofencing 기능을 통해 현재 위치가 특정 위치 반경에 진입/머물기/이탈 등의 이벤트를 처리 가능합니다.

단말 유저 당, 앱 당 최대 100개의 geofence 를 사용할 수 있습니다.

You can have multiple active geofences, with a limit of 100 per app, per device user.

Location Services에 geofence 를 위한 이벤트들을 요청해놓을 수 있습니다.

요청한 변경들이 감지되면 BroadcastReciever 등을 통해 처리가 가능합니다.

  • 반경 진입
  • 반경 이탈
  • 반경 진입 후 지정한 시간 동안 이탈 안함
  • geofence 만료 시간 (만료 시간 후 Location Service는 geofence를 알아서 지웁니다.)
항목 설명
최대 등록 수 100 per app, per device user
만료시간 Geofence 마다 millisecond 로 지정.
NEVER_EXPIRE 옵션 지원.
시간 지정 후 만료시 자동으로 Geofence 제거됨.
영역 지정 Circle 형태로 등록 되며, 위경도와 반경(m) 지정.
최상의 결과는 반경 100~150m 사이로 설정해야 함.
Wi-FI 사용 가능시 20~40m 까지 가능.
실내 위치 사용시는 5m 정도로 작을 수도 있음.
Geofence 내부 위치를 알수 없다면 Wi-Fi 정확도는 약 50m라고 가정함.
Wi-Fi 위치 사용할 수 없다면 수백m ~ 수Km 편차 생길수도 있음.
머물기 시간 지정 Geofence 진입 후 머물기로 체크할 시간을 millisecond로 지정.
Geofence 감지할 변경 타입 진입 (GEOFENCE_TRANSITION_ENTER)
이탈 (GEOFENCE_TRANSITION_EXIT)
머뭄 (GEOFENCE_TRANSITION_DWELL)
응답시간 지정 응답 시간이 큰 경우 전력을 크게 절약하고
응답 시간을 낮춘다고 바로 알림을 받지는 않음.
Geofence 추가시 초기 알림 설정 진입 (INITIAL_TRIGGER_ENTER)
이탈 (INITIAL_TRIGGER_EXIT)
머뭄 (INITIAL_TRIGGER_DWELL)
Geofence 사용 이유 설명 앱의 백그라운드 위치에 액세스하므로 사용자에게 권한 설명 필요.
Geofence 재등록이 필요없는 경우 Google Play 서비스 업그레이드나 리소스 제한으로 종료 후 재시작시
Location 프로세스 충돌시
Geofence 재등록이 필요한 경우 단말 재부팅시 부팅 완료 시점을 받아 다시 등록해야 함.
앱이 제거 후 재 설치시
앱 데이터 삭제시
Google Play 서비스 데이터 삭제시
앱에 GEOFENCE_NOT_AVAILABLE 알림 수신시
(일반적으로 Android's Network Location Provider 비활성화시 발생함)
Geofence 진입이 잘 동작하지 않는 경우 (GEOFENCE_TRANSITION_ENTER) Geofence 정확한 위치를 사용할 수 없거나 반경이 너무 작은 경우
Wi-Fi가 꺼져 있는 경우 (SettingsClient를 사용해 장치 설정 체크 필요)
- 참고로 Android 10에서는 WifiManager.setEnabled() 호출 불가하므로 설정 패널 사용.
- 시스템 앱이나 Device policy controller 에서는 가능.
Geofence 내 안정적인 네트워크 연결이 없을 경우
단순히 알림이 늦는 경우.
- 지속적으로 위치 쿼리하는 형식이 아니라 대기 시간이 필요함.
- 일반적인 대기 시간은 2 분 미만이며 장치가 움직일 때는 훨씬 적음.
- 백그라운드 위치 제한 적용시 대기 시간은 평균 약 2~3분 정도.
- 단말이 장시간 정지된 경우 대기 시간이 증가할 수 있음. (최대 6분)

 

Android App 샘플 만들기

1. 권한 추가

먼저 권한을 아래와 같이 추가 후 앱에서 permission 체크 및 요청 로직을 추가합니다.

ACCESS_BACKGROUND_LOCATION 권한은 Android 10 부터 필요합니다.

App 권한 요청 처리

Background 위치 접근 권한 처리

AndroidManifest.xml

<manifest... >
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

    <!-- Required if your app targets Android 10 (API level 29) or higher -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    ...
</manifest>

MainActivity.kt

    private val MY_PERMISSIONS_REQ_ACCESS_FINE_LOCATION = 100
    private val MY_PERMISSIONS_REQ_ACCESS_BACKGROUND_LOCATION = 101

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        checkPermission()
    }

    override fun onRequestPermissionsResult(requestCode: Int,
                                            permissions: Array<String>, grantResults: IntArray) {
        when (requestCode) {
            MY_PERMISSIONS_REQ_ACCESS_FINE_LOCATION,
            MY_PERMISSIONS_REQ_ACCESS_BACKGROUND_LOCATION -> {
                grantResults.apply {
                    if (this.isNotEmpty()) {
                        this.forEach {
                            if (it != PackageManager.PERMISSION_GRANTED) {
                                checkPermission()
                                return
                            }
                        }
                    } else {
                        checkPermission()
                    }
                }
            }
        }
    }

    private fun checkPermission() {
        val permissionAccessFineLocationApproved = ActivityCompat
            .checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
                PackageManager.PERMISSION_GRANTED

        if (permissionAccessFineLocationApproved) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                val backgroundLocationPermissionApproved = ActivityCompat
                    .checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
                        PackageManager.PERMISSION_GRANTED

                if (!backgroundLocationPermissionApproved) {
                    ActivityCompat.requestPermissions(
                        this,
                        arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                        MY_PERMISSIONS_REQ_ACCESS_BACKGROUND_LOCATION
                    )
                }
            }
        } else {
            ActivityCompat.requestPermissions(this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                MY_PERMISSIONS_REQ_ACCESS_FINE_LOCATION
            )
        }
    }

2. dependency 추가

Location Service 및 Geofence 사용을 위해서 location 라이브러리를 추가합니다.

app/build.gradle

dependencies {
    ...
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    ...
}

3. Geofence List 추가

테스트 용으로 추가할 Geofence 데이터를 생성합니다.

Geofence 객체 생성시 여러가지 설정을 할 수 있습니다.

ex) 위치 및 반경, 어떤 이벤트를 걸건지, 만료나 머무는 체크 시간을 얼마나 할지 등등

MainActivity.kt

    val geofenceList: MutableList<Geofence> by lazy {
        mutableListOf( 
            getGeofence("현대백화점", Pair(37.5085864,127.0601149)),
            getGeofence("삼성역", Pair(37.5094518,127.063603))
        )
    }

    private fun getGeofence(reqId: String, geo: Pair<Double, Double>, radius: Float = 100f): Geofence {
        return Geofence.Builder()
            .setRequestId(reqId)    // 이벤트 발생시 BroadcastReceiver에서 구분할 id
            .setCircularRegion(geo.first, geo.second, radius)    // 위치 및 반경(m)
            .setExpirationDuration(Geofence.NEVER_EXPIRE)        // Geofence 만료 시간
            .setLoiteringDelay(10000)                            // 머물기 체크 시간
            .setTransitionTypes(
                Geofence.GEOFENCE_TRANSITION_ENTER                // 진입 감지시
                        or Geofence.GEOFENCE_TRANSITION_EXIT    // 이탈 감지시
                        or Geofence.GEOFENCE_TRANSITION_DWELL)    // 머물기 감지시
            .build()
    }

4. Geofence Client 생성

Location API 사용을 위하여 Geofencing Client 인스턴스를 생성해야 합니다.

MainActivity.kt

    private val geofencingClient: GeofencingClient by lazy {
        LocationServices.getGeofencingClient(this)
    }

5. Geofencing Request 빌드

Geofence 지정 및 관련 이벤트 트리거 방식을 설정하기 위해 GeofencingRequest 를 빌드합니다.

MainActivity.kt

    private fun getGeofencingRequest(list: List<Geofence>): GeofencingRequest {
        return GeofencingRequest.Builder().apply {
            // Geofence 이벤트는 진입시 부터 처리할 때
            setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)                
            addGeofences(list)    // Geofence 리스트 추가
        }.build()
    }

6. Broadcast Receiver 추가

Geofencing 변경 이벤트를 받을 BroadcastReceiver 를 추가 후 등록해줍니다.

GeofenceBroadcastReceiver.kt

class GeofenceBroadcastReceiver: BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        val geofencingEvent = GeofencingEvent.fromIntent(intent)
        if (geofencingEvent.hasError()) {
            val errorMessage = GeofenceStatusCodes.getStatusCodeString(geofencingEvent.errorCode)
            Log.e("GeofenceBR", errorMessage)
            return
        }

        // Get the transition type.
        val geofenceTransition = geofencingEvent.geofenceTransition    // 발생 이벤트 타입

        // Test that the reported transition was of interest.
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
        geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {

            // Get the geofences that were triggered. A single event can trigger
            // multiple geofences.
            val triggeringGeofences = geofencingEvent.triggeringGeofences

            val transitionMsg = when(geofenceTransition) {
                Geofence.GEOFENCE_TRANSITION_ENTER -> "Enter"
                Geofence.GEOFENCE_TRANSITION_EXIT -> "Exit"
                else -> "-"
            }
            triggeringGeofences.forEach {
                Toast.makeText(context, "${it.requestId} - $transitionMsg", Toast.LENGTH_LONG).show()
            }

        } else {
            Toast.makeText(context, "Unknown", Toast.LENGTH_LONG).show()
        }
    }
}

AndroidManifest.xml

<application... >
    ...
    <receiver android:name=".GeofenceBroadcastReceiver"/>
</application>

MainActivity.kt

    private val geofencePendingIntent: PendingIntent by lazy {
        val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
        PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
    }

7. Geofence Request 및 Callback 추가

Geofencing Client에 Geofence 정보 및 트리거 방식을 가지고 있는 Geofencing Request과 이벤트 발생시 처리할 Broadcast Receiver를 추가해줍니다.

MainActivity.kt

private fun addGeofences() {
    geofencingClient.addGeofences(getGeofencingRequest(geofenceList), geofencePendingIntent).run {
        addOnSuccessListener {
            Toast.makeText(this@MainActivity, "add Success", Toast.LENGTH_LONG).show()
        }
        addOnFailureListener {
            Toast.makeText(this@MainActivity, "add Fail", Toast.LENGTH_LONG).show()
        }
    }
}

준비는 끝났습니다. 앱을 실행해보아 동작을 확인합니다.

+ Recent posts