Post

Android Widget 제작 참고 자료 및 주의 사항

기본적인 위젯 제작과 ListView

http://blog.naver.com/PostView.nhn?blogId=horajjan&logNo=220578698191

전체적인 설명

RemoteViews

ListView, StackView : RemoteViewsService/RemoteViewsFactory

https://docs.huihoo.com/android/3.0/resources/samples/StackWidget/src/com/example/android/stackwidget/StackWidgetService.html

액티비티의 연산 결과를 위젯에서 가져오기 위해 SharedPreferences

https://developer.android.com/guide/topics/appwidgets/#persisting-data여기서 Adding behavior to individual items

PendingIntent 시 주의할 것

1
2
3
4
val repoSelectIntent = Intent(context, RepoSelectActivity::class.java)
repoSelectIntent.putExtra(AppWidgetManager.EXTRA\_APPWIDGET\_ID, appWidgetId)
val repoSelectPendingIntent = PendingIntent.getActivity(context, 0, repoSelectIntent,**PendingIntent.FLAG\_UPDATE\_CURRENT** )
views.setOnClickPendingIntent(R.id.repo\_select\_btn, repoSelectPendingIntent)

이런 식으로 extra를 인텐트에 담아서 전달할 때, 매번 extra 값이 변경된다면 반드시 FLAG\_UPDATE_CURRENT를 주어야 한다. 이 플래그는 PendingIntent가 이미 존재하는 경우에 이를 유지하고 extra data만 바꿔주도록 지정하는 역할을 한다. 따라서 이 플래그를 주지 않으면 이미 PendingIntent가 존재하기 때문에 별다른 작업을 하지 않아 최초에 담긴 extra값(INVALID같은)만 계속 전달된다.

OAuth 2.0

private repo의 issue도 가져오도록 하고 싶었기 때문에, 로그인 기능이 필요했다. 로그인 기능을 구현할 때 Basic Auth로 처리하게 되면 매번 id/pw가 오가니까 이게 찜찜해서 토큰으로 처리하고 싶었다. API를 이용해 Personal Access Token을 발급 받는 방법과 OAuth를 사용하는 방법이 있었는데, 만드려는게 가벼운 위젯이다 보니 OAuth로 로그인 버튼을 만들고 누르면 브라우저로 인텐트를 보내 로그인 하고 토큰만 가져오는 식으로 구현하면 되지 않을까? 싶은 생각이었다. OAuth를 사용하려면 Resource Server측(github, google, facebook, …)에 내 어플리케이션을 OAuth 어플리케이션으로 등록 해야한다. 근데 github에 OAuth 앱으로 등록하려고 보니, callback URL을 적으라고 하는 것이다. 아니 나는 안드로이드 앱개발하려고 하는건데 웬 callback URL이지? 하고 좀 찾아보니, google cloud api나 네아로 같은 경우 Android에서 OAuth를 사용하는 것도 지원해서 애초에 어플리케이션 유형을 “웹 어플리케이션”, “Android” 같이 선택할 수 있도록 되어 있었으나 깃헙은 그냥 웹앱만 지원하는 듯 보였다. 그래서 github OAuth를 사용하려면 code를 수신하고, client_id/client_secret/code를 되돌려 보내주는 역할을 하는 웹서버를 두거나 해야 한다. firebase를 써도 되겠지만 일단은 기초부터 하자는 마음으로 데스크탑에 간단한 웹서버를 열고 callback 을 처리하도록 구성했다.

이렇게 구성하면, client_secret을 별도의 웹서버에 저장하기 때문에 앱이 client_secret을 가지고 있지 않아도 된다.

callback 서버 예제 코드

웹 서버 따로 안두고, 앱에서 모두 처리할 수 없나?

다른 깃허브 앱은 어떻게 처리하고 있나 궁금해서 Fasthub 쪽 깃헙을 뒤져봤다.

https://github.com/k0shk0sh/FastHub/blob/f40109e7f84dcc1d30d86563b698e1642e48b1b4/app/src/main/java/com/fastaccess/ui/modules/login/LoginPresenter.java#L68-L96 여기 보면, code를 요청하는 uri를 만드는 함수가 있고… code를 직접 수신한 다음, 이 code와 앱에 저장된 client_secret를 이용해 직접 request를 보내고 access_token을 가져오는 것으로 보인다. 즉, 별도의 웹서버를 사용하지 않는 것 같았다. 보면, redirect_uri도 이런 식이다. https://github.com/k0shk0sh/FastHub/blob/f40109e7f84dcc1d30d86563b698e1642e48b1b4/app/src/main/java/com/fastaccess/helper/GithubConfigHelper.java#L10

쿼리스트링으로 지정하는 redirect_uri는 처음 깃헙에 OAuth 앱으로 등록할 때 입력했던 callback URL과 같은 도메인/포트여야 하며 path는 입력한 path의 subdir이어야 한다는 제약이 있으므로, OAuth 앱으로 등록할 때 callback URL에 github-issue-widget://login 따위로 넣어주어야 한다.

그럼 안드로이드 인앱 브라우저에서 fasthub://login같은 곳으로 리다이렉트하면 특정 인텐트가 호출되거나 하는 기능이 있다는 소리일 것 같다. https://github.com/k0shk0sh/FastHub/blob/f40109e7f84dcc1d30d86563b698e1642e48b1b4/app/src/main/java/com/fastaccess/ui/modules/login/LoginActivity.java#L86-L94

또한, 앱에서 인앱브라우저로 이동해서 로그인하고 access_token을 가져오는 것을 CustomTabsIntent로 처리했다. 원래는 단순히 브라우저에 인텐트를 보내거나, 웹뷰를 사용하면 될거같다고 생각했는데, CustomTabsIntent를 사용하면 더 깔끔하게 처리할 수 있다. 이 쪽으로 좀 더 찾아보니까 fasthub://login같은걸 액티비티 인텐트 필터로 등록해, url을 이용해 액티비티를 호출하는 방법이 있었다. https://medium.com/@ajinkyabadve/do-authentication-in-android-using-custom-chrome-tab-cct-2a0edffc93fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<activity
android:name=".ui.modules.login.LoginActivity"
android:configChanges="keyboard|orientation|screenSize"
android:label="@string/app\_name"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/LoginTheme">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:host="login"
android:scheme="fasthub"/>
</intent-filter>
</activity>

근데 이런 식으로 처리하는건 매우 위험하다. 어쨌든 어플리케이션 내에 client_secret이 저장 되어야 하기 때문이다. 상수 리터럴이든, xml에 적어 놓든, gradle.properties에 적어두고 build.gradle에서 불러오든, 어딘가에는 client_secret을 적어야 한다. 더불어 암호화도 마찬가지로, E(client_secret)을 적어 둔다고 하더라도 이를 풀 수 있는 키 역시 같이 앱에 적어 두어야 한다. 그렇다는건, 어쨌든 리버싱 하고 뭐 하고 하면 결국 키를 얻어낼 수 있다는 얘기다. 그래서 OAuth는 별도의 callback 서버를 두고, 거기에 secret을 저장해 두도록 하는 것이고. 따라서 별도의 callback 서버를 두고 거기다가 secret을 두는 방법이 올바른 방법이고, 이런 식으로 별도의 서버 없이 앱에서 콜백을 받아서 처리하는건 위험할 수 있다는 점을 염두에 두고 있어야 한다. * 상수에 저장하는건 디컴파일해서 jar 보면 바로 보이니까 제일 비추천하는 방법. * 단순 스트링 저장이 아니라 native로 작성한 함수가 복잡한 로직을 통해 키 문자열을 조합하도록 구성해서 리버싱을 어렵게 만들 수는 있다. * Proguard를 활성화하면 상수가 바로 드러나지는 않는다. 꼭 활성화 할 것.

access_token은 원래 expired되기 때문에 저장하지 않지만, github api는 expired되지 않아서 이걸 저장해도 상관 없음. 저장할 곳은 https://stackoverflow.com/questions/43629251/how-to-save-oauth-access-token-securely-in-android

This post is licensed under CC BY 4.0 by the author.