- 전체
- 명
- 오늘 찾아주신 분
- 명
안드로이드에서 Onedrive 를 사용하기 위해서는,
해당 SDK 를 사용하여 개발을 했었다. OneDrive 에서 직접 제공하는 오피셜 SDK 이기도 하고 잘 되기도 하고 했는데.. 몇 가지 단점이 있었다. 그 단점은,
1. Git 업데이트가 2016 년에서 멈춰져있다. (일해라)
2. Silent Login 이 올바르게 동작하지 않는다.
본인들이 완벽하다 생각하니 업데이트 안했을 수도 있겠지만 나날히 발전하여 기능이 추가되고 없어지는 안드로이드에 비해서 보완이나 개선되는 그런건 보이지 않는 듯 했다. 그리고 Silent Login 이 필요 했는데 문제는 Silent Login 는 작동을 하지만, 이것도 Dialog 를 한번 살짝 띄워야지만 로그인이 작동되는 구조라서.. 백그라운드에서 로그인 처리를 해 주는게 맞는 정의 아니인가...? 싶었다. 실제로도 이 문제 때문에 처음 앱에서 백그라운드로 드라이브들에 로그인을 하는데 갑자기 Context 부분에서 에러가 뜨는게 생겨서 왜 그런가 찾아봤더니 silent 인데도 불구하고 Dialog 을 띄우더라.. 해서 찾다찾다 Microsoft 에서 제공하는 Graph API 를 이용하여 Onedrive 에 접근하도록 설정하게 되었다.
MS 에서 통합 SDK 인 Graph API 를 이용하여 MS 가 가지고 있는 여러 API 들에 접근할 수 있도록 만들어주었다. 이 Github 는 Java 플랫폼을 지원하는 SDK 인데, 안드로이드도 공용으로 사용하는 SDK 이다. 그래서 해당 Github 에 있는 문서대로 앱에 적용해보려 했는데, 문서가 개똥 같아서 알아듣기도 어렵고 Readme 의 내용도 제대로 따라 할 수 없었다. Android API 호환 버전이 26(Oreo) 부터 가능하다고 하니 그 이하는 문서대로 하게 되면 여러 라이브러리의 부재로 에러가 발생할 것이다. 그래서 다음의 Github 로 참조한다.
그리고 해당 문서가 제일 Android 친화적으로 작성한 것 같아서 이 문서를 보고 하면 좋을 것 같다.
그럼 이제 어떻게 개발을 했는지 기억을 더듬어가며 써내려가 보도록 한다.
먼저 Microsoft Graph SDK 는 Azure 에 가입을 해야 사용할 수 있는 SDK 이기 때문에 Azure 에 가입을 하고, 프로젝트 등록까지 한다. 프로젝트 등록 한 후에 프로젝트 상세 페이지에 들어가게 되면,
여기서 OneDrive 를 사용하기 위해서 설정해야하는 메뉴는 "브랜딩", "인증", "API 사용권한" 이다.
브랜딩은 프로젝트의 기본 정보를 나타내는 것이기 때문에 간단하고, 인증은 앱 프로젝트가 있어야 설정이 가능하다. 그래서 "인증" 을 하기 전에 "API 사용권한" 을 설정하도록 한다.
원래에는 저 왼쪽의 리스트는 비워져 있는게 정상이고 권한을 추가하게되면 리스트에 하나씩 생성이 되는 것이다. 권한 추가를 누르게되면 오른쪽 같이 "API 사용 권한 요청" 메뉴가 뜨는데 여기서 "일반적으로 사용되는 Microsoft API" 인 Microsoft Graph 를 누른다. 그러면 "위임된 권한" 과 "애플리케이션 사용 권한" 두개의 버튼으로 나뉘어지는데, 여기서 "위임된 권한" 눌러 준 다음 왼쪽 리스트에 있는 권한을 차례대로 검색하여 추가하도록 한다.
"User.Read", "Files.Read", "Files.Read.All", "Files.Read.Selected"
추가가 되었으면, 인증 창으로 넘어간다.
각 플랫폼별로 리디렉션에 대한 정보를 적어주는 공간이 있다. 여기서 Android 항목 및에 있는 "URI 추가" 버튼을 통해 정보를 적어주면 되는데, 적어주어야 할 항목은 앱의 패키지명 그리고 서명해시 값이다. 디버그용 릴리즈용 따로 추가해야 하기 때문에 두개를 추가하도록 한다.
여기까지가 Azure 사이트에서 설정해야되는 항목이다. 그러면 이제 코딩하는 부분으로 넘어가도록 한다.
코딩하는 부분도 처음에는 Github 에 있는 대로 따라 해본 후 안되었을 때 방법을 시도해보길 권장한다. 왜냐면,각 플랫폼이나 JDK 에 따라서도 되는 경우가 있는 것 같기 때문이다.
먼저, Gradle 을 설정해본다.
buildscript {
repositories { /*...*/ }
dependencies { /*...*/ }
}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
maven { url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1' }
}
}
// 프로젝트 레벨의 Gradle 파일
dependencies {
implementation 'com.microsoft.identity.client:msal:2.1.0'
implementation ('com.microsoft.graph:microsoft-graph:4.2.0'){
exclude group: 'com.azure', module: 'azure-core'
}
}
// 모듈 레벨의 Gradle 파일
현재 microsoft-graph API 는 5.5.0 까지 되어있고, identity client 는 2.2.1 까지 업데이트 되어있다.
graph sdk 안에 있는 azure-core 부분을 제외시켰는데, 해당 부분을 사용하지 않는 데다가 에러가 발생하기 떄문에 제외를 시켰다. 이외에 프로젝트에서 사용되고 있는 라이브러리와 충돌되는 것이 있다면 확인한 후 exclude 시키도록 한다. (사용된 라이브러리 확인은 하단 링크에서..)
Gradle 설정을 마치게 되었다면, Azure 포털의 "인증" 메뉴에서 데이터를 참조하여 json 파일을 만들어 raw 폴더에 넣어주어야 한다. 복사할 데이터는 "클라이언트ID" 와 "리디렉션 URI" 이며, 다음과 같은 형태의 json 파일을 만든다.
{
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"broker_redirect_uri_registered": true,
"account_mode" : "SINGLE",
"authorities" : [
{
"type": "AAD",
"audience": {
"type": "AzureADandPersonalMicrosoftAccount",
"tenant_id": "common"
}
}
],
"authorization_user_agent": "DEFAULT",
"logging": {
"log_level": "VERBOSE"
}
}
해당 파일이 로그인에 필요한 설정파일이라고 생각하면 된다. 이 파일도 디버그와 릴리즈를 따로 구분해야하기 때문에 각각 2개를 만들어주면 되겠다.
다음은 AndroidManifest.xml 파일을 설정해주어야 한다. 로그인 자체가 Chrome Custom Tab 을 이용한 팝업이기 때문에 특정 URL 이 감지되면 해당 CustomTab 으로 팅기는 설정을 한다.
<manifest>
...
<application>
...
<activity android:name="com.microsoft.identity.client.BrowserTabActivity">
<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:scheme="msauth"
android:host="PACKAGE_NAME" />
<intent-filter>
</activity>
</application>
...
</manifest>
특정 Scheme 과 Host 가 호출되면 자동으로 BrowserTabActivity 가 열리게 된다. 공식 문서에는 서명 해쉬까지 data 에 걸어 놓으라고 써있지만, 특정을 하게되면 Debug 와 Release 를 구분하지 못하고 하나만 사용하게 되기 때문에 특정하지 않도록 한다.
그 다음 본격적으로 로그인하는 부분을 코딩해보도록 하자.
로그인을 하기 위해서는 클라이언트 객체를 초기화 시켜주어야 한다.
private var app : ISingleAccountPublicClientApplication? = null
/*...*/
private fun getClientApplication(context: Context) : Flow<ISingleAccountPublicClientApplication>
= callbackFlow {
app?.let { offer(it); close() }
?: PublicClientApplication.createSingleAccountPublicClientApplication(
context,
if(BuildConfig.DEBUG) R.raw.msal_configure_debug else R.raw.msal_configure_release,
object : IPublicClientAppliation.ISingleAccountapplicationCreatedListener {
override fun onCreated(application: ISingleAccountPublicClientApplication?){
app = application // 다른 로직에서도 공통으로 사용할 것이기 때문에 변수로 뺀다
offer(application!!)
close()
}
override fun onError(exception :MsalException?){ close(exception) }
}
awaitClose()
}
샘플 코드를 Flow와 함께 섞었는데, OneDrive 자체가 Network Thread 에서 일을 하기 때문에 Main Thread 와 분리를 위해서 비동기 처리를 하도록 만들었다. 이건 각자 로직 구성하기 편한대로 만들면 될 것이다.
다음 로그인 된 계정 정보를 받아오는데, 로그인이 되어있지 않기 때문에 에러로 빠지게 만들 것이다
private fun getAccount(app: ISingleAccountPublicClientApplication): Flow<IAccount>
= callbackFlow {
application.getCurrentAccountAsync(object: ISingleAccountPublicClientApplication.CurrentAccountCallback {
override fun onAccountLoaded(active: IAccount?){
// 로그인 되었다면 active 는 Not null
if(active == null) close(NotLoginException())
else{
offer(active)
close()
}
}
override fun onAccountChanged(prior: IAccount?, current: IAccount?){
offer(current!!)
close()
}
override fun onError(exception: MsalException){
close(exception)
}
}
awaitClose()
}
여기에서 NotLoginException 은 임의로 만든 Exception 클래스 이다. 첫 로그인이기 때문에 당연히 exception을 던질 것이고, 로그인을 하기 위한 팝업을 띄울 것이다.
Flow 를 사용하였기 때문에 Chaining 으로 호출하도록 해보자.
private fun login(activity: Activity){
CoroutineScope(Dispatchers.IO).launch {
getClientApplication(activity)
.flatMapConcat(::getAccount)
.catch {
when(it){
is NotLoginException -> app?.signIn(activity, null, SCOPES, callback)
else -> it.printStackTrace()
}
}.collect()
}
}
getAccount 를 하기 위해서 flatMap 을 사용했기 때문에 ISingleAccountPublicClientApplication 형태의 데이터를 받아올 수 없어 따로 저장해놓은 변수를 통하여 로그인 팝업을 띄우게 된다. 팝업을 띄우려면 다음의 4가지의 파라미터가 필요하다.
- Activity : 로그인 팝업을 띄울 액티비티
- Login Hint : 로그인 힌트인데 그냥 NULL 로 보내도록 한다. 쓸일이 없음
- Scope : GraphAPI 를 통해 어떤 기능을 사용할 지 알려주는 것. 값은 Azure 의 "API 사용권한" 에서 허용했던 값을 넣는다
- Callback : 로그인이 완료되면 받을 Callback
Scope 는 Azure 콘솔에서 허용했던 4개의 권한을 적어주면 된다. 그리고, Callback 은 AuthenticationCallback 이라는 인터페이스 형으로 여러 곳에서 사용할 것이기 때문에 전역 변수로 선언해주도록 한다.
companion object {
val SCOPES = arrayOf("User.Read", "Files.Read", "Files.Read.All", "Files.Read.Selected"
}
private var api : GraphServiceClient<Request>? = null
private val callback : AuthenticationCallback = object : AuthenticationCallback {
override fun onSuccess(result: IAuthenticationResult?) { result ?: return
api = GraphServiceClient.builder()
.authenticationProvider { CompletableFuture.completedFuture(result.accessToken) }
.buildClient()
}
override fun onError(exception : MsalException?){
// 로그인 시도할 때 에러가 발생되는 경우
}
override fun onCancel(){
// 로그인 하지 않고 그냥 팝업을 닫을 때
}
}
로그인 팝업에서 로그인에 성공하게 되면 onSuccess 로 데이터가 떨어지고, result 로 받아오는 accessToken 값으로 Graph API 를 사용하기 위해 GraphServiceClient 를 초기화 시킨다. GraphServiceClient 를 처리할 때에 CompletableFuture 라는 Java concurrent 관련 API 를 사용하게 되는데, 해당 API를 사용하기 위해서는 Java 8 언어가 지원이 되어야 하기 떄문에 만약에 에러가 난다면 다음의 문서를 확인해보기 바란다.
다음은 Silent 로그인 기능을 넣어보도록 한다. Slient 로그인을 한다고 하지만 Graph API 는 토큰값을 이용하여 API를 사용하는 것이기 때문에 토큰 값을 가져오는 것이라고 생각하면 된다.
suspend fun silent(){
getClientApplication().collect {
it.acquireTokenSilentAsync(
SCOPES,
it.configuration.defaultAuthority.authorityUri.toString(),
object : SilentAuthenticationCallback {
override fun onSuccess(result: IAuthenticationResult?) { callback.onSuccess(result) }
override fun onError(exception: MsalException?) { callback.onError(exception) }
}
}
}
클라이언트 객체에 있는 acquireTokenSlientAsync 라는, 비동기로 토큰을 가져올 수 있도록 하는 메소드를 이용하여 토큰을 가져온다. 필요 파라미터는 로그인 할 때에도 쓰였던 SCOPES 와 Authority URI 그리고 Callback 인데, Authority URI 는 클라이언트 객체에서 접근하면 얻어올 수 있으며, 해당 값은 json 으로 저장해놓았던 데이터를 기반으로 한다. 마지막 Callback 은 AuthenticationCallback 과 비슷해 보이지만, 서로 상속관계가 아니기 때문에 SilentAuthenticationCallback 을 정의한 후에 callback 변수로 Bypass 하는 식으로 데이터를 받아와 처리할 수 있다. 이렇게되면 callback 변수에서 팝업에서의 로그인과 Silent 로그인 둘 다 같은 로직을 태울 수 있게 되는 것이다. 해당 메소드는 Network Thread 에서 처리해야 하기 때문에 suspend 로 정의를 했다.
로그인 하는 방법은 여기까지 개발하면 끝이고, 이번에는 OneDrive API 를 사용해보기로 한다.
API 는 로그인 onSuccess 할 때 초기화 시켜 준, GraphServiceClient 객체를 사용한다.
fun list(){
val d = api?.drive ?: return
val page = d.items(DIRECTORY_ID)
.children()
.buildRequest()
.expand("thumbnails")
.get()
.currentPage
/* ... */
}
대충 사용자의 OneDrive 내의 데이터를 파싱하기 위해서는 다음의 형식이 필요한데, OneDrive 는 Item 들의 ID 값을 가지고 폴더나 파일 객체에 접근하는 구조인 것 같다. 필요에 맞는 대로 더 개발을 하게되면 OneDrive 내부를 탐색 할 수 있는 것도 만들 수 있다. OneDrive 최상위 폴더의 ID 값은 root 이며, items 에 root 를 넣게 되면 최상위 폴더 내에 있는 각 폴더/파일들의 데이터가 넘어와진다.
단, 주의 해야 할 점은 해당 메소드 묶음도 Network Thread 에서 돌아가야 한다는 것이다. 그리고 buildRequest에 get 메소드와 async 가 있는데, get 은 동기형식이라 데이터를 불러올 때 Block 이 발생하고 async 는 비동기 형식으로 받아오게 된다.
여기까지 Android 와 OneDrive 를 연동하는 방법에 대해 정리해 보았다. 다소 어려울 수 있지만 또 하나 배워가는 드라이브 연동이었다.
[Android] Web 에서 App 으로 데이터를 받아보자 (0) | 2022.02.04 |
---|---|
[Android] Glide with PDF -3(완결)- (0) | 2022.02.04 |
[Android] Glide with PDF -2(ModelLoader, DataFetcher)- (0) | 2022.01.31 |
[Android] Glide with PDF -1(기초공사)- (0) | 2022.01.26 |
[Kotlin] 문자열 배열 한글, 영문, 숫자로 정렬 시키기 (0) | 2022.01.26 |