Tempo Di Valse

[Android] Onedrive SDK 적용기 본문

개발/Android

[Android] Onedrive SDK 적용기

TempoDiValse 2022. 1. 25. 18:07

 

안드로이드에서 Onedrive 를 사용하기 위해서는, 

 

 

GitHub - OneDrive/onedrive-sdk-android: OneDrive SDK for Android!

OneDrive SDK for Android! Contribute to OneDrive/onedrive-sdk-android development by creating an account on GitHub.

github.com

 

 

해당 SDK 를 사용하여 개발을 했었다. OneDrive 에서 직접 제공하는 오피셜 SDK 이기도 하고 잘 되기도 하고 했는데.. 몇 가지 단점이 있었다. 그 단점은,

 

1. Git 업데이트가 2016 년에서 멈춰져있다. (일해라)

2. Silent Login 이 올바르게 동작하지 않는다.

 

본인들이 완벽하다 생각하니 업데이트 안했을 수도 있겠지만 나날히 발전하여 기능이 추가되고 없어지는 안드로이드에 비해서 보완이나 개선되는 그런건 보이지 않는 듯 했다. 그리고 Silent Login 이 필요 했는데 문제는 Silent Login 는 작동을 하지만, 이것도 Dialog 를 한번 살짝 띄워야지만 로그인이 작동되는 구조라서.. 백그라운드에서 로그인 처리를 해 주는게 맞는 정의 아니인가...? 싶었다. 실제로도 이 문제 때문에 처음 앱에서 백그라운드로 드라이브들에 로그인을 하는데 갑자기 Context 부분에서 에러가 뜨는게 생겨서 왜 그런가 찾아봤더니 silent 인데도 불구하고 Dialog 을 띄우더라.. 해서 찾다찾다 Microsoft 에서 제공하는 Graph API 를 이용하여 Onedrive 에 접근하도록 설정하게 되었다.

 

 

GitHub - microsoftgraph/msgraph-sdk-java: Microsoft Graph SDK for Java

Microsoft Graph SDK for Java. Contribute to microsoftgraph/msgraph-sdk-java development by creating an account on GitHub.

github.com

 

MS 에서 통합 SDK 인 Graph API 를 이용하여 MS 가 가지고 있는 여러 API 들에 접근할 수 있도록 만들어주었다. 이 Github 는 Java 플랫폼을 지원하는 SDK 인데, 안드로이드도 공용으로 사용하는 SDK 이다. 그래서 해당 Github 에 있는 문서대로 앱에 적용해보려 했는데, 문서가 개똥 같아서 알아듣기도 어렵고 Readme 의 내용도 제대로 따라 할 수 없었다. Android API 호환 버전이 26(Oreo) 부터 가능하다고 하니 그 이하는 문서대로 하게 되면 여러 라이브러리의 부재로 에러가 발생할 것이다. 그래서 다음의 Github 로 참조한다.

 

 

GitHub - microsoftgraph/msgraph-training-android: Microsoft Graph Training Module - Building Android Native Apps

Microsoft Graph Training Module - Building Android Native Apps - GitHub - microsoftgraph/msgraph-training-android: Microsoft Graph Training Module - Building Android Native Apps

github.com

 

그리고 해당 문서가 제일 Android 친화적으로 작성한 것 같아서 이 문서를 보고 하면 좋을 것 같다.

 

 

자습서: 인증을 위해 Microsoft ID 플랫폼을 사용하는 Android 앱 만들기 - Microsoft identity platform

이 자습서에서는 Microsoft ID 플랫폼을 사용하여 사용자를 로그인하고 사용자를 대신하여 Microsoft Graph API를 호출하는 액세스 토큰을 가져오는 Android 앱을 빌드합니다.

docs.microsoft.com

 

그럼 이제 어떻게 개발을 했는지 기억을 더듬어가며 써내려가 보도록 한다.


먼저 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 시키도록 한다. (사용된 라이브러리 확인은 하단 링크에서..)

 

 

Maven Central Repository Search

 

search.maven.org

 

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 언어가 지원이 되어야 하기 떄문에 만약에 에러가 난다면 다음의 문서를 확인해보기 바란다.

 

 

자바 8 언어 기능 및 API 사용  |  Android 개발자  |  Android Developers

사용할 수 있는 Java 8 언어 기능, 이러한 기능을 사용할 프로젝트를 올바르게 구성하는 방법 및 발생할 수 있는 알려진 문제를 알아봅니다.

developer.android.com

 

다음은 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 를 연동하는 방법에 대해 정리해 보았다. 다소 어려울 수 있지만 또 하나 배워가는 드라이브 연동이었다.

 

반응형
Comments