와우 드디어 안드로이드 스튜디오에 DB inspector 가 들어왔습니다.

해당 기능은 Android 스튜디오 4.1 Canary 6 이상(이 글을 쓰는 시점보다 약 6개월 정도 전?) 에서 확인할 수 있습니다.

4.1 업데이트 이후 inspector 를 기본 탭에 추가시켜줬기에 알아차렸지 그렇지 않았더라면 알지도 못했을 것 같습니다.(developer.android.com/studio/preview/features?hl=ko#database-inspector)

 

앱을 개발하면서, DB 에 어떤 내용이 들어가 있는지 확인하는 것은 매우 귀찮고 까다로운 일이었습니다.

왜냐하면, 지금까지는 adb 를 통해, db 파일을 내려받은 뒤, 그 파일을 다시 SQLite Browser 같은 application 을 사용해서 일일이 확인해야 했기 때문입니다.

게다가 Room Database 를 사용하는 경우, 보통 비동기로 동작하기 때문에 값을 확인하는 것은 매우 귀찮은(?), 까다로운 일이었습니다.

 

사용법은 그리 어렵지 않습니다. 단순히 디버그 모드의 앱을 실행시키면, DB 의 내용은 쉽게 볼 수 있습니다.

developer.android.com/studio/inspect/database?utm_source=android-studio

 

Debug your database with the Database Inspector  |  Android 개발자

In Android Studio 4.1 and higher, the Database Inspector allows you to inspect, query, and modify your app's databases while your app is running. This is especially useful for database debugging. The Database Inspector works with plain SQLite and with libr

developer.android.com

 

하지만, 아직 GUI 에서 바로 수정하는 것은 허용하고 있지 않습니다.

아직 많은 기능을 제공하고 있지는 않지만, New Query Tab 을 이용해서, row 를 삭제하거나 삽입할 수는 있습니다. 

아이콘을 클릭하면 New Query Tab 이 생성됩니다.
New Query Tab

 

그나마 조금 편리한 기능이라고 한다면, Room Database 를 사용할 경우 @Query annotation 과 New Query

Tab 과 연동시켜놓았다는 것 정도입니다.

- 아래 그림에서 @Query annotation 옆에 표와 돋보기가 그려진 아이콘을 클릭하면, 실행할 수 있습니다.

Parameter 가 필요할 경우, 생성된 창에서 값을 추가해 준 뒤, Run 을 클릭하면 됩니다.

(medium.com/androiddevelopers/database-inspector-9e91aa265316)

Line Number 오른쪽에 돋보기 모양 아이콘을 누르면 Query parameter 를 추가해서 실행할 수 있습니다.

 

 

DB insert/update/select 가 쉽지 않더라도, 단순히 DB 내용을 볼 수 있다는 것 만으로도 앱 디버깅에는 많은 시간 절약이 될 것으로 보입니다.

 

 

 

 

어느 한가한 날 오후 github 을 보다보니 README.md 파일에 예쁜 뱃지들이 보였습니다.

출처 : github Facebook SDK README.mdown

 

'build : passing, maven central : 8.1.0 저런 단추들은 어디서 만들 수 있는 것일까?' 하는 궁금증이 생겼습니다.

검색을 하다 보니, 가장 쉬운 방법 중 하나는 shields.io 를 사용하는 방법이었습니다.

사이트의 설명대로 아래 형식으로 링크를 걸어주면 간단한 이미지가 나옵니다.

https://img.shields.io/badge/<LABEL>-<MESSAGE>-<COLOR>

예를 들어, 위 그림과 같은 뱃지를 만들고 싶다면, 아래 코드와 같이 url 을 생성해서 만들면 됩니다.

https://img.shields.io/badge/build-passing-green 
https://img.shields.io/badge/maven central-8.1.0-green

 

 

이젠, README.md 파일에는 아래와 같은 코드 형식으로 넣어주면 됩니다.

![](https://img.shields.io/badge/build-passing-green)
![](https://img.shields.io/badge/maven central-8.1.0-green)

 

 

shields.io 를 둘러보다 보니 github 관련 기능들이 많이 있습니다.

이를 테면, github 에서 last-commit 같은 경우 입니다.

https://img.shields.io/github/last-commit/:user/:repo

출처 : https://shields.io/category/activity

 

그런데, 막상 원래 궁금했었던 Facebook SDK README.mdown 파일은 아래와 같은 url 로 이미지를 만들고 있었습니다.

![Build Status](https://travis-ci.org/facebook/facebook-android-sdk.svg?branch=master)

 

그래서, SVG 가 무엇인지 궁금해 졌습니다.

SVG, 스케일러블 벡터 그래픽스(Scalable Vector Graphics, SVG)는 2차원 벡터 그래픽을 표현하기 위한 XML 기반의 파일 형식으로, 1999년 W3C(World Wide Web Consortium)의 주도하에 개발된 오픈 표준의 벡터 그래픽 파일 형식입니다.

SVG 형식의 이미지와 그 작동은 XML 텍스트 파일들로 정의 되어 검색화·목록화·스크립트화가 가능하며 필요하다면 압축도 가능합니다. (출처 : WikiPedia)

 

간단히 설명하면, xml 로 그림을 그리는 표준 방식입니다.

이를 테면, Rect x=10 y=10 width = 100 height = 100, 이런 식으로 써놓으면, (10,10) 좌표에 너비=100, 높이=100 인 사각형을 그대로 그려주는 것입니다.

예제 코드는 아래와 같습니다.

<svg xmlns="http://www.w3.org/2000/svg" >
	<rect x="10" y="10" width="100" height="100"/>
</svg>

<rect x="10" y="10" width="100" height-"100"/>

 

위 Facebook SDK 예제에서 나온 이미지(travis-ci.org/facebook/facebook-android-sdk.svg?branch=master) 를 받아보면, 아래 코드와 같습니다. (실제 더 긴 코드이지만, 중요한 부분만 골라봤습니다.)

<svg>
  <rect rx="3" width="90" height="20" fill="#555"/>
  <rect rx="3" x="37" width="53" height="20" fill="#4c1"/>
  <g fill="#fff" text-anchor="middle" font-size="11">
    <text x="19.5" y="14">build</text>
    <text x="62.5" y="14">passing</text>
  </g>
</svg>


이렇게 필요한 텍스트와 칼라 값들만 입력해주면, 적당한 모양의 그림을 만들어 줄 수 있는 이미지를 만들어 줄 수 있습니다. shields.io 에서 생성된 파일 역시 비슷합니다.

 

참고한 사이트


Github Facebook Android SDK : https://github.com/facebook/facebook-android-sdk
Shield IO : https://shields.io
Shield IO 사용법 : https://velog.io/@loakick/Shield-IO-%EC%82%AC%EC%9A%A9%EB%B2%95-iojyndy4pi

W3C SVG : www.w3.org/Graphics/SVG/
SVG Tutorial : tutorials.jenkov.com/svg/index.html
생활 코딩 SVG : https://opentutorials.org/course/2418/13666

 

 

 

 

며칠 전, "조치 필요: 앱이 Google Play 정책을 준수하지 않음" 이란 제목의 메일을 받았습니다.

제 기억으로는 앱 등록정보의 제목에 한 두 단어를 추가했던가, 삭제했던가 매우 사소한 것이라 생각되는 작업을 한 직후 였습니다.

원인은 앱 하단의 애드몹 광고에서 약간 생소한(?) 광고가 노출되었기 때문입니다.

덕분에 사소한 업데이트조차도 거부되었습니다.

 

조치 필요: 앱이 Google Play 정책을 준수하지 않음

 

첨부된 스크린 샷

 

자동화된 테스트로 나온 결과물일 것이라 생각되지만,

어떻게 하면 저런 광고가 나올 수 있는걸까 라는 생각도 들고,

1000만 이상 다운로드 라는 숫자를 보면 한편으로 부럽(?)기도 하고,...

 

어쨋거나 이 모든 것의 발단은 콘텐츠 등급을 3세 이상으로 처리해 놓았기 때문입니다.

플레이 콘솔 > 앱 정보 > 콘텐츠 등급 에서 확인하실 수 있습니다.

 

유틸성 앱이 아니더라도 왠간한 앱의 특성상 폭력적인 게임이거나 19금 콘텐츠를 제공하는 것이 아니라면 딱히 이 등급이상으로 처리할 만한 앱들도 생각보다 많지 않습니다.

플레이 콘솔 > 앱 정보 > 콘텐츠 등급

 

그래서, 이 문제를 해결하려면, 둘 중 하나를 선택해야 합니다. 1. 앱의 콘텐츠 등급의 연령을 올릴 것인가 vs 2. 광고 콘텐츠 등급을 내릴 것인가

  • 앱의 콘텐츠 등급 연령을 올릴 것인가?
    • IARC 설문지를 재작성하다보면, 왠간해선 답을 내놓기 쉽지 않습니다.
    • 제 앱처럼 유틸성일 경우, 폭력적이거나 19금 콘텐츠가 있지도 않고 제공할 수도 없는데, 애드몹에서 저런 류의 광고가 나갈 수 있을지 모른다는 이유로 앱 콘텐츠 등급을 올리는 것은 생각보다 회의적입니다.
  • 광고 콘텐츠 등급을 내릴 것인가?
    • 애드몹 > 앱 > 차단관리 > 광고 콘텐츠 등급 을 내릴 경우, 수익의 81%가 하락할 수 있을지도 모른다는 메시지를 보고 있노라면 망설여 집니다.
      • 아무리 다운로드도 얼마 안되고 광고 수익도 얼마 안 되지만, 한 달에 맥주 한 캔 정도 였던 수익이 츄파츕스 한 개로 바뀔 수 있다고 하면 아쉬운 건 당연한 일일 것입니다.
      • 전에 말도 안되는 클릭 단가가 나오는 경우가 있어서 확인해 봤더니, 위의 사례와 같은 19금 스러운 광고였습니다.
      • (비아그라 유사제품 광고 였던걸로 기억...) 돈의 유혹이 크긴 하지만, 어느 정도 수익 감소는 감내해야하는 것이 아닐까 싶습니다.

 

 

결과적으로 후자(광고 콘텐츠 등급 하향)를 선택하였지만, 이 결과는 애드몹에 적용되어 있는것이지, 구글 플레이 스토어에 적용된 것이 아닙니다.

그래서, 플레이 콘솔 > 앱 정보 > 콘텐츠 등급 > 에서 IARC 설문을 다시 한번 작성한 뒤, 적용합니다.

그렇게 한 이후에야 비로소 "Your update is live" 라는 이메일을 받을 수 있었습니다.

 

'Software Develop' 카테고리의 다른 글

Android Studio - Database Inspector  (0) 2020.11.04
Github Badge - 깃헙 뱃지, 그리고, SVG  (0) 2020.10.20
Android Playstore Library Core - Update Apk  (0) 2020.09.04
Android AlertDialog Style 변경  (0) 2020.08.05
Android Custom Lint  (0) 2020.07.28

 

플레이 스토어에서 apk 혹은 aab 를 업로드한 뒤, 사용자에게 앱의 버전이 업데이트 되었음을 알리고, 업데이트하도록 독려하는 UX 를 구현하는 것은 그리 어려운 일은 아닙니다. 하지만, 이를 구현하는 것은 약간 귀찮은 일임에는 틀림없습니다. 앱이 플레이스토어에 업로드 된 앱 버전 이름 혹은 코드을 체크한 뒤, 현재의 앱 버전 이름 혹은 코드와 비교하여 일정한 차이를 보일 때,(혹은 항상) 업데이트를 유도하는 UX 를 추가하여야 합니다. 예전에는 별도의 서버 API 를 사용하는 경우도 있었고, 그 때 그 때 플레이스토어를 크롤링하는 방법을 사용하는 경우도 있었습니다.

 

그런데, 이제 플레이 스토어 라이브러리를 이용하면 되기 때문에, 그런 번거로움은 생각하지 않아도 됩니다. 개발자 사이트의 참조 주소는 developer.android.com/guide/playcore/in-app-updates 입니다.

 

1. gradle dependency 를 설정합니다.

dependencies {
...
	implementation 'com.google.android.play:core:1.8.0'
	implementation 'com.google.android.play:core-ktx:1.8.1'
...
}

 

주의해야할 점은 VERSION.SDK_INT >= 21 을 체크하셔야 합니다. 체크하지 않아도 앱이 죽지는 않지만 UX 를 해칠 수 있습니다.

그리고, 플레이 스토어가 설치되어 있지 않은 에뮬레이터에서는 당연히 동작하지 않습니다. 또한, 이런 속성이 있기 때문에, 플레이 스토어에 게재(Publish)된 apk 버전보다 versionCode 를 낮게 설정해놓아야 동작을 확인할 수 있습니다.

 

2. 추가할 코드

 

// Creates instance of the manager.
val appUpdateManager = AppUpdateManagerFactory.create(context)

// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo

// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
    if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
        // For a flexible update, use AppUpdateType.FLEXIBLE
        && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
    ) {
        // Request the update.
        appUpdateManager.startUpdateFlowForResult(
        // Pass the intent that is returned by 'getAppUpdateInfo()'.
        appUpdateInfo,
        // Or 'AppUpdateType.FLEXIBLE' for flexible updates.
        AppUpdateType.IMMEDIATE,
        // The current activity making the update request.
        this,
        // Include a request code to later monitor this update request.
        1)        
    }
}

 

appUpdateInfo 로 부터 최신 VersionCode 를 가져올 수 있습니다. 이 값과 현재 앱 버전을 비교하여, update 버튼을 활성화시키고, 버튼 클릭에 따라, startUpdateFlowForResult 를 호출해주시면 적절한 update UX 를 구현할 수 있습니다.


appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
	val versionCode = appUpdateInfo.availableVersionCode()
}

 

-------

(아직 작성 중인 글입니다.)

 

 

 

이 글은 Android AlertDialog 를 style.xml 로 얼마나 어디까지 변경시킬 수 있는가를 알아볼 수 있는 글입니다.

코드는 여기를 참고하시면 됩니다.

androidx.appcompat:appcompat:1.1.0 을 사용했습니다.

 

// :app/build.gradle

dependencies {
...
    implementation 'androidx.appcompat:appcompat:1.1.0'
...
}

 

Android Studio 를 열고, 새로운 프로젝트 템플릿으로 간단한 안드로이드 앱 프로젝트를 만든 뒤, 아래와 같은 코드를 MainActivity 에 추가합니다. 그리고, 앱을 실행하면, 그림과 같은 화면이 보여질 것입니다. (다이얼로그의 스트링은 Android Resource 를 사용했습니다. string.xml 파일에 추가하기가 매우 귀찮아서요. --;)

 

    fun showDialog(){
        AlertDialog.Builder(this)
            .setTitle("Exercise Title")
            .setMessage("Exercise Message")
            .setPositiveButton(android.R.string.yes, null)
            .setNegativeButton(android.R.string.no, null)
            .setNeutralButton(android.R.string.untitled, null)
            .show()        
    }

 

원문 : Note: Attribute names from the support library do not use the android: prefix. That's used only for attributes from the Android framework.

 

템플릿에서 생성해주는 코드에는 application theme 로 AppTheme 를 activity theme 로는 AppTheme.NoActionBar 를 자동으로 설정해줍니다.

 

<!-- AndroidManifest.xml -->
    <application
	...
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:label="Main"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".NextActivity" android:label="Next"/>
    </application>
<!-- src/main/res/values/style.xml -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>    
    </style>
    
<!-- src/main/res/values/color.xml -->    
    <color name="colorPrimary">#6200EE</color>
    <color name="colorPrimaryDark">#3700B3</color>
    <color name="colorAccent">#03DAC5</color>    

 

기본으로 생성해주는 코드를 실행했을 때의 AlertDialog 의 모습은 아래와 같습니다. OK, Cancel 버튼의 색깔이 AppTheme 에서 설정한 colorAccent 의 값(#03DAC5)입니다.

 

Android AlertDialog 실행 화면

 

간단하게 버튼의 색깔을 바꾸는 것은 colorAccent 값을 바꿔 주는 것만으로도 가능합니다. 이를 테면, 아래 코드처럼 activity theme 인 AppTheme.NoActionBar 에 colorAccent 아이템을 추가해주는 것 만으로도 쉽게 변경할 수 있습니다.

<!-- src/main/res/values/style.xml -->
    <style name="AppTheme.NoActionBar">
		...
        <item name="colorAccent">@android:color/holo_green_dark</item>
    </style> 

 

하지만, 이런 식으로 변경하면, 그림 우 하단의 편지 모양의 색깔도 같이 바뀌어 버립니다.

만약, 이런 색상 변경이 의도된 것이라면 상관없지만, alertDialog 의 색상만 바꾸고자한다면, alertDialogTheme 스타일을 만들어서 별도로 설정해야 합니다.

    <style name="AppTheme.NoActionBar">
        <!--        <item name="colorAccent">@android:color/holo_green_dark</item>-->
        <item name="alertDialogTheme">@style/AppTheme.AlertDialogTheme</item>
    </style>

    <style name="AppTheme.AlertDialogTheme" parent="ThemeOverlay.AppCompat.Dialog.Alert">
        <item name="colorAccent">@android:color/holo_green_dark</item>
    </style>

 

 

 

 

다이얼로그 테마에 android:textColorPrimary 아이템을 추가해주면, 다이얼로그 타이틀의 색상을 변경할 수 있습니다.

    <style name="AppTheme.AlertDialogTheme" parent="ThemeOverlay.AppCompat.Dialog.Alert">
        <item name="colorAccent">@android:color/holo_green_dark</item>
        <item name="android:textColorPrimary">@android:color/holo_red_dark</item>
    </style>

 

음... 메시지 색상까지 같이 변경되었습니다. --; 타이틀과 메시지를 각각 다른 색으로 지정하려면, 역시 스타일을 지정해주어야 합니다. 타이틀 스타일은 android:windowTitleStyle 을 사용합니다.

    <style name="AppTheme.AlertDialogTheme" parent="ThemeOverlay.AppCompat.Dialog.Alert">
        <item name="colorAccent">@android:color/holo_green_dark</item>
        <item name="android:textColorPrimary">@android:color/holo_red_dark</item>
        <item name="android:windowTitleStyle">@style/AppTheme.AlertDialogTheme.WindowTitle</item>
    </style>

    <style name="AppTheme.AlertDialogTheme.WindowTitle" parent="RtlOverlay.DialogWindowTitle.AppCompat">
        <item name="android:textColor">@android:color/holo_orange_dark</item>
    </style>

 

 

마지막으로 Cancel 만 한 번 바꾸어 보도록 하겠습니다. buttonBarButtonStyle 을 추가하면 되는데, 그러면, 모두 바뀌어 버리니, buttonBarNegativeButtonStyle 스타일만 하나 추가해 봅니다.

    <style name="AppTheme.AlertDialogTheme" parent="ThemeOverlay.AppCompat.Dialog.Alert">
        <item name="colorAccent">@android:color/holo_green_dark</item>
        ...
        <!--        <item name="buttonBarButtonStyle">@style/AppTheme.AlertDialogTheme.Button</item>-->
        <!--        <item name="buttonBarPositiveButtonStyle">@style/AppTheme.AlertDialogTheme.Button</item>-->
        <item name="buttonBarNegativeButtonStyle">@style/AppTheme.AlertDialogTheme.Button</item>
        <!--        <item name="buttonBarNeutralButtonStyle">@style/AppTheme.AlertDialogTheme.Button</item>-->
    </style>

    <style name="AppTheme.AlertDialogTheme.Button" parent="Widget.AppCompat.Button.ButtonBar.AlertDialog">
        <item name="android:textColor">@android:color/holo_purple</item>
    </style>

 

 

 

 

위에서 말한 모든 값들의 참조가 될 수 있는 것들의 힌트는 appcompat-1.1.0/res/values/values.xml 에 모두 있습니다. 알고보면, abc_alert_dialog_material.xml 레이아웃의 DialogTitle 과 message TextView 의 textColor 는 동일하게 ?android:textColorPrimary 로 설정되어 있다는 것도 확인할 수 있습니다.

<!-- appcompat-1.1.0/res/values/values.xml -->
<style name="Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light.DarkActionBar"/>

<style name="Base.Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light">

<style name="Base.Theme.AppCompat.Light" parent="Base.V7.Theme.AppCompat.Light">

<style name="Base.V7.Theme.AppCompat.Light" parent="Platform.AppCompat.Light">
	...
        <item name="alertDialogTheme">@style/ThemeOverlay.AppCompat.Dialog.Alert</item>
        <item name="alertDialogStyle">@style/AlertDialog.AppCompat.Light</item>
        <item name="alertDialogCenterButtons">false</item>
        <item name="textColorAlertDialogListItem">@color/abc_primary_text_material_light</item>
        <item name="listDividerAlertDialog">@null</item>
	...
</style>

<style name="AlertDialog.AppCompat.Light" parent="Base.AlertDialog.AppCompat.Light"/>

<style name="Base.AlertDialog.AppCompat.Light" parent="Base.AlertDialog.AppCompat"/>

<style name="Base.AlertDialog.AppCompat" parent="android:Widget">
  <item name="android:layout">@layout/abc_alert_dialog_material</item>
  <item name="listLayout">@layout/abc_select_dialog_material</item>
  <item name="listItemLayout">@layout/select_dialog_item_material</item>
  <item name="multiChoiceItemLayout">@layout/select_dialog_multichoice_material</item>
  <item name="singleChoiceItemLayout">@layout/select_dialog_singlechoice_material</item>
  <item name="buttonIconDimen">@dimen/abc_alert_dialog_button_dimen</item>
</style>
    

 

 

 

덧붙임.

 

Android Studio 에서 theme.xml 파일에서 마우스 오버만 해도 관련된 정보들을 상당수 확인할 수 있습니다.

 

안드로이드 스튜디오에서 parent 속성에 마우스 오버했을 때 나오는 정보 : 속성값이 어떤 style 로 부터 상속된 것인지 상세하게 보여줍니다.

 

 

 

 

 

이 글은 "AndroIdiots Podcast E18: Custom Lints with Hitanshu Dhawan"(https://medium.com/androidiots/androidiots-podcast-e18-custom-lints-349f0651d458) 을 읽고 그대로 따라해 보면서 몇 가지 보충설명을 담아보았습니다. 이 글의 내용은 이 팀에서 UI 정책 같은 것들 때문에 RadioButton 을 Customize 한 IdiotRadioButton 사용하는데, 만약, RadioButton 을 그대로 사용할 경우, 경고 메시지를 주고, IdiotRadioButton 으로 수정/테스트하는 과정을 담은 것입니다.

 

Lint(린트) : 린트는 소스 코드를 분석하여 프로그래밍 오류, 버그, 문체 오류 및 의심스러운 구성을 표시하는 도구입니다.

 

Step 1. Java/Kotlin 라이브러리 모듈 생성

 

Step 2. Dependency 추가

:LintLibraryModule/build.gradle

dependencies {
    def lintVersion = '26.5.3'
    // Lint
    compileOnly "com.android.tools.lint:lint-api:$lintVersion"
    compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
}

:app/build.gradle

dependencies {
	lintChecks project(path: ':LintLibraryModule')
}

 

Step 3. IssueRegistry 생성

CustomCodeIssueDetector.ISSUE 는 Step 6 에서 만듭니다.

class CustomIssueRegistry : IssueRegistry() {
    override val issues: List<Issue>
        get() = listOf(
            CustomCodeIssueDetector.ISSUE
        )
}

 

Step 4. IssueRegistry 선언

안드로이드 스튜디오에서도 아래 그림과 같이 경고 팝업으로 알려주게 하려면 (1) gradle 코드를 추가하거나, (2) resources 에 텍스트 파일을 추가해 주어야 합니다.

 

1. gradle 에 추가하는 방법 : package name = com.example 일 경우,

dependencies {
    def lintVersion = '26.5.3'
    compileOnly "com.android.tools.lint:lint-api:$lintVersion"
    ...
}

jar {
    manifest {
        attributes("Lint-Registry-v2": "com.example.CustomIssueRegistry")
    }
}

2. Resource File 추가 하는 방법 : package name = com.example 일 경우,

 

Step 5. Custom Detector 클래스 만들기

class CustomCodeIssueDetector : Detector()

 

Step 6. Custom Detector Issue 만들기

Issue 를 생성할 때 필요한 값들에 대한 설명은 굳이 하지 않아도 대략 알 수 있도록 네이밍되어 있어 생략합니다. ^^

class CustomCodeIssueDetector : Detector() {
    companion object {
        val ISSUE = Issue.create(
            id = "IdiotRadioButtonUsageWarning",
            briefDescription = "Android's RadioButton should not be used",
            explanation = "Don't use Android Radio button, be an idiot and use IdiotRadioButton instead",
            category = Category.CORRECTNESS,
            priority = 3,
            severity = Severity.WARNING,
            implementation = Implementation(
                CustomCodeIssueDetector::class.java,
                Scope.RESOURCE_FILE_SCOPE
            )
        )
    }
}

 

Step 7.  Scanner 추가하기

class CustomCodeIssueDetector : Detector(), XmlScanner {
    override fun getApplicableElements(): Collection<String> {
        return listOf(
            "RadioButton"
        ) // will look for Radio Button //Text in all xml
    }

    override fun visitElement(context: XmlContext, element: Element) {
        context.report(
            issue = ISSUE,
            location = context.getNameLocation(element),
            message = "Usage of RadioButton is prohibited" // Error message
        )
    }

    companion object {
        val ISSUE = Issue.create(...) // Step 6 참고
    }
}

 

Step 8. Lint (Quick) Fix 추가

 

class CustomCodeIssueDetector : Detector(), XmlScanner {
    override fun getApplicableElements(): Collection<String> { ... } // Step 7 참고

    override fun visitElement(context: XmlContext, element: Element) {
        val idiotRadioButtonFix = LintFix.create()
            .name("Use IdiotRadioButton")
            .replace()
            .text("RadioButton")
            .with("com.androidiots.playground.IdiotRadioButton")
            .robot(true)
            .independent(true)
            .build()

        context.report(...) // Step 7 참고
    }

    companion object {
        val ISSUE = Issue.create(...) // Step 6 참고
    }
}

 

여기까지가 "AndroIdiots Podcast E18: Custom Lints with Hitanshu Dhawan"(https://medium.com/androidiots/androidiots-podcast-e18-custom-lints-349f0651d458) 에서 RadioButton 을 추가할 경우, Lint 를 이용해 경고메시지를 주고, RadioButton IdiotRadioButton 으로 Quick Fix 하는 코드입니다.

 

테스트 코드 작성

1. 테스트를 위한 dependency 추가

:LintLibraryModule/build.gradle

dependencies {
    def lintVersion = '26.5.3'
    ...

    testImplementation "com.android.tools.lint:lint:$lintVersion"
    testImplementation "com.android.tools.lint:lint-tests:$lintVersion"
    testImplementation "junit:junit:4.12"
}

2. 테스트 코드 작성

:LintLibraryModule/src/test/

layout.xml 에서 RadioButton 을 사용하면 Warning 을 표시하는지 확인할 수 있는 코드

class CustomCodeIssueDetectorTest {
    val testInputString = """
            <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
                <RadioButton android:id="@+id/someidlowercase" />
            </LinearLayout>
              """
    @Test
    fun testRadioButton() {
        lint().files(
            xml(
                "res/layout/layout.xml", testInputString
            ).indented())
            .issues(CustomCodeIssueDetector.ISSUE)
            .allowMissingSdk(true)
            .run()
            .expectWarningCount(1)
    }
}

 

 

Lint 관련된 여러 글들을 보면, 안드로이드 그 자체의 Lint 기능이 강력해서 이딴 기능을 굳이 쓸필요가 있느냐하는 식의 멘트들이 많이 있습니다. 이 글의 사례, RadioButton -> IdiotRadioButton 으로 사용하는 것도 그닷 좋은 방법은 아니라 ,예제만 보아도 실제 이런 Lint 가 필요하다고 보기에는 조금 설득력이 떨어집니다. 개발에 참여하는 인원이 적을 수록 별로 의미가 없는 작업이지만, 안드로이드 앱 하나에 4~50명의 개발자가 동시에 개발할 경우, 코드 컨벤션처럼 강력하게 정착시켜야 할 것은 아니지만, 개발자들의 성향(?)에 따라 느슨한 룰같은 것들이 생기기 때문에, 큰 조직이라면 어쩌면 필요할 수도 있겠다는 생각이 듭니다. "Writing a Custom Android UI Inheritance Lint Rule"(https://medium.com/@roderiklagerweij/writing-a-custom-android-ui-inheritance-lint-rule-9af254480399) 에서 나온 것들이 아마도 대표적인 사례(i.e. IncorrectViewId)가 아닐까 싶습니다. 조금 익숙해지신 분들이라면, 글보다는 github(https://github.com/roderiklagerweij/AndroidCustomLint) 코드를 보시는 것이 훨씬 빠를 것입니다.

 

 

2020/03/09 - [Android/Widget] - 안드로이드 위젯 개발하기 (1)

2020/03/10 - [Android/Widget] - 안드로이드 위젯 개발하기 (2)

2020/03/13 - [Android/Widget] - 안드로이드 위젯 개발하기 (3)

 

누군가가 내가 만든 앱에 View 를 만들고 제멋대로 동작하게 만들어 놓았다고 생각해본다면, 매우 짜증나는 일일 것입니다.

위젯이란 것도 결국 내가 남의 앱위에 View 를 만들고 동작하도록 만드는 것이기 때문에, 위젯을 만들어 사용하는데는 매우 제한적인 방법을 사용해야 합니다.

 

단순히 TextView 의 Text 를 수정하는 것도 아래와 같이 별도의 API 를 사용해야 합니다.

        val remoteViews = RemoteViews(
            "com.tistory.develop_branch.widget",
            R.layout.widget_layout
        )
        remoteViews.setTextViewText(R.id.widget_text, "setTextViewText")

 

안드로이드 위젯의 경우, 사용할 수 있는 View 의 종류도 정해져 있습니다.

처음 위젯을 개발할 때, 가장 쉽게 부딪히는 오류가 이런 View 를 사용하지 않아서 입니다. 상속한 View 조차 사용하면 안됩니다.

위젯에서 사용할 수 있는 View 의 종류는 여기를 확인하세요.

 

이런 제약사항들은 RemoteView 에 대한 문서를 읽어보면, 좀 더 자세히 알 수 있습니다.

 

이 제약사항들 중 아마도 가장 중요한 것은 setOnClickPendingIntent 일 것입니다.

여기서 PendingIntent 에 대한 개발자 문서도 한 번 읽어보고 넘어가는 것이 좋습니다.

 

A PendingIntent itself is simply a reference to a token maintained by the system describing the original data used to retrieve it. This means that, even if its owning application's process is killed, the PendingIntent itself will remain usable from other processes that have been given it. If the creating application later re-retrieves the same kind of PendingIntent (same operation, same Intent action, data, categories, and components, and same flags), it will receive a PendingIntent representing the same token if that is still valid, and can thus call cancel() to remove it.

 

PendingIntent 는 시스템에 의해 유지되는 레퍼런스 토큰이라는 말이 나옵니다.

같은 intent action, data, category, component, flag 를 가지고 있다면 같은 PendingIntent 를 사용하게 된다는 의미로, PendingIntent를 생성할 때 사용하는 Intent 에 다른 Extra 값을 지정한다고 해서 다른 PendingIntent 가 생성되지는 않습니다.

그래서, widget Click 시 다른 동작을 지정하고자 할 때는 Extra 값이 아닌, Action 혹은 Data 를 변경해야 합니다.

이를 테면, 보통 A 라는 Activity 를 실행하기 위해 putExtra("extra",1), putExtra("extra",2) 로 구분해서 사용했다고 해서, PendingIntent 에 동일하게 적용하면 낭패를 보기 쉽습니다.

만약, PendingIntent 적용하고자 한다면, extra 값이 아닌, action = "com.tistory.develop_branch.widget.1", action = "com.tistory.develop_branch.widget.2" 와 같이 action 을 변경해서 사용해야 합니다.

 

 

2020/03/09 - [Android/Widget] - 안드로이드 위젯 개발하기 (1)

2020/03/10 - [Android/Widget] - 안드로이드 위젯 개발하기 (2)

2020/03/13 - [Android/Widget] - 안드로이드 위젯 개발하기 (3)

Android Platform 은 사용자에게 좀 더 안전한 환경을 제공하고자 단문 통신인 http 를 지양(!)하고자했습니다.

그래서, Android 6.0(M) 에서부터 AndroidManifest.xml 파일에 android:useClearTextTraffic 이란 attribute 를 제공하기 시작,

이 attribute 를 false 로 선택할 경우, App 내부에서 http 단문 통신을 하고자 할 경우, Platform 에서 트래픽을 막았습니다.

물론, 이 속성은 Api level = 28 (Android 8.0(P)) 까지 기본적으로 false 입니다.

<application android:usesCleartextTraffic="true"/>

 

이 값으로는 약간 부족하다고 생각했었는지, Android 7.0(N, 24) 에서는 android:networkSecurityConfig 라는 attribute 를 추가했습니다. 이 속성 값에는 cleartextTrafficPermitted 를 기술한 xml 파일(아래 코드 참조)을 값으로 추가할 수 있습니다. 이 xml 파일에는 이전보다는 좀 더 자세한 사항들을 기술할 수 있습니다. 예를 들어, 아래 예제처럼 도메인 단위의 http 트래픽을 허용할 것인지 아닌지를 기술할 수 있습니다.

<application
	android:networkSecurityConfig="@xml/network_security_config"/>
<?xml version="1.0" encoding="utf-8"?> 
<network-security-config> 
	<base-config cleartextTrafficPermitted="false"/> 
	<domain-config cleartextTrafficPermitted="true">         
		<domain includeSubdomains="true">develop-branch.tistory.com</domain>        
	</domain-config> 
</network-security-config>

 

 

여기서 주의할 점은 <base-config> 를 설정하지 않을 경우 기본 값을 따르며, android:useClearTextTraffic 보다 android:networkSecurityConfig 를 더 우선한다는 점입니다. 쉽게 얘기하면, android:useClearTextTraffic=false 로 지정하더라도 android:networkSecurityConfig xml 값이 지정되어 있다면, xml 에 기술된 값을 우선시 한다는 것입니다.

 

아래 표는 AndroidManifest.xml 에서 android:useClearTextTraffic=[값없음,true,false] 일 때, android:networkSecurityConfig 파일에서 cleartextTrafficPermitted=[값없음,true,false] 값에 대해 각각 NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted() 값을 보여주고 있다.

 

networkSecurityConfig :

xml 파일 지정하지 않음

networkSecurityConfig :

xml 파일 지정함

   

cleartextTrafficPermitted = 값없음

cleartextTrafficPermitted = true

cleartextTrafficPermitted = false

useClearTextTraffic = 값없음

true (Default)

true (Default)

true

false

useClearTextTraffic = true

true

true (Default)

true

false

useClearTextTraffic = false

false

true (Default)

true

false

물론, 이 값은 AndroidManifest.xml 파일에서 targetSdkVersion < 28 로 설정한 경우입니다.

targetSdkVersion >= 28 이라면, true (Default) 가 false (Default) 로 바뀝니다.

 

* 코드 상에서 이 값을 확인하는 방법은 크게 두 가지입니다.

A)

context.packageManager.getApplicationInfo("com.example").flags & ApplicationInfo.FLAG_USES_CLEARTEXT_TRAFFIC

B)

NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted()

 

A) 안은 일반적으로 AndroidManifest 에 기술된 값을 얻을 때 사용하는 방법이며, B) 안은 Api Level 23 에서 정의된 클래스를 이용한 것입니다.

B) 는 A) 를 포함하고 있고, 코드도 간결하기 때문에, B) 를 사용하는 것이 좋습니다.

추가로 A) 안은 API Level 23 이하에서도 사용가능한 방법이기 때문에, Build.VERSION.SDK_INT 로 제한하지 않을 경우, 오동작할 수 있습니다. 물론, 위의 내용은 일반적인 HTTP 통신인 경우(URL, UrlConnection 같은 객체를 사용하는 경우)입니다.

 

WebView 에서는 위와 조금 다르게 동작합니다. https://developer.android.com/reference/android/security/NetworkSecurityPolicy 을 참고하면, 아래와 같은 노트를 확인할 수 있습니다.

isCleartextTrafficPermitted
...
NOTE: WebView honors this flag for applications targeting API level 26 and up.

말인 즉슨,  WebView 에서는  targetSdkVersion이 26 보다 같거나 큰 경우에만, 동작한다는 것이다.

위에서 언급한 useClearTextTraffic 이나 networkSecurityConfig 값을 false 로 설정하더라도 targetSdkVersion 이 26 보다 낮을 경우, WebView 에서는 HTTP 단문 통신이 가능하다는 것이다.

 

엄밀히 말하면, 이는 Android 7.0, 24 에서 기본 적용된 Chrome WebKit 53.0 이상 버전에서 적용되는 사항입니다.

Android 6.0, 23 에서 적용된 Chrome WebKit 44.0 버전에서는 적용되지 않습니다.

즉, Android 6.0, 23, 의 기본 Chrome WebKit 44.0 을 업데이트 하지 않은 상황이라면, 어떤 설정을 적용하더라도 WebView 에서 HTTP 통신이 가능합니다.

 

종합하면 WebView 의 경우, targetSdkVersion>=26 으로 설정하고, Chrome WebKit 53.0 이상 버전을 사용한다면, (일반적으로 Android 7.0 24 이상의 단말), isCleartextTrafficPermitted 설정이 적용될 수 있습니다.

'Software Develop' 카테고리의 다른 글

Android Custom Lint  (0) 2020.07.28
안드로이드 위젯 개발하기 (4)  (0) 2020.07.27
구글 플레이스토어 등록정보 - 코로나 키워드  (0) 2020.07.05
Code Kata  (0) 2020.04.23
Android Studio - Image Asset Tool  (0) 2020.03.13