2019년 10월 23일 수요일

Testing Camera and Gallery Intents with Espresso 를 읽고

https://proandroiddev.com/testing-camera-and-galley-intents-with-espresso-218eb9f59da9

사진을 찍거나 갤러리에 접근하려면 권한이 필요하기 때문에 관련 작업을 하기전에 안드로이드에 권한을 요청해야 합니다. 앱의 코드에서는 권한을 요청하면 권한이 없는 경우 다이얼로그가 뜨고 여기서 OK를 하면 다음으로 진행이 되게 됩니다.
그러면, 테스트 코드에서는 어떻게 하면 될까요? 아래와 같은 코드를 테스트쪽에 넣어주면 테스트시 권한을 자동으로 부여합니다.

@get:Rule
var mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)

Activity parameter는 다음의 코드로 가져올 수 있습니다.

@get:Rule
var mActivityTestRule = IntentsTestRule(MyActivity::class.java)

- 갤러리로부터 이미지 가져와서 ImageView에 넣는 코드 테스트하기

a. 가장 먼저 image를 로컬에 저장합니다.

b. 그리고 Intent.ACTION_CHOOSER action이 발생하면 응답으로 줄 ActivityResult를 등록해 줍니다.

intending(hasAction(Intent.ACTION_CHOOSER)).respondWith(imgGalleryResult)

c. ActivityResult는 아래와 같은 코드로 만들 수 있습니다.

Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)

d. View에서의 동작을 지정해줍니다.

onView(withId(R.id.gallery)).perform(click())

onView(withId(R.id.image_viewer)).check(matches(hasImageSet()))

R.id.gallery에 click 이벤트가 발생하면 image_viewer에 hasImageSet에 일치하는지 확인하는 코드입니다.

hasImageSet은 BoundedMatcher를 상속해서 matchesSafely에 확인하고 싶은 것을 체크합니다.

관련 구글 코드 : https://github.com/android/testing-samples/blob/master/ui/espresso/IntentsAdvancedSample/app/src/androidTest/java/com/example/android/testing/espresso/intents/AdvancedSample/ImageViewerActivityTest.java

2019년 10월 20일 일요일

libp2p - Circuit Relay 를 읽고

https://docs.libp2p.io/concepts/circuit-relay/

Circuit Relay : 별도의 peer를 통해 통신하고자 하는 두 peer를 연결하는 transport protocol

보통 NAT 내부에 있는 peer의 경우 다른 peer와 쉽게 통신 할 수 없다. 이러한 경우 통신이 가능하도록 하기 위해 libp2p는 p2p-circuit이라는 프로토콜을 사용한다.

중간에 relay해주는 peer에게 연결은 어떻게 할 수 있을까? 기본적으로는 내가 연결이 가능한 relay peer의 주소를 peer에게 알려주면 될 것이다. 주소는 multiaddr을 사용한다.
또는 libp2p의 content routing interface를 사용해서 연결이 가능한 relay peer를 찾고 연결을 한 다음에 peer routing을 사용해서 연결한 relay peer의 주소를 다른 peer에게 알린다.(Autorelay)

2019년 10월 18일 금요일

libp2p : NAT Traversal 을 읽고

https://docs.libp2p.io/concepts/nat/

사용하는 컴퓨터가 많아지면서 IPv4에서 쓸 수 있는 IP 만으로는 전체 기기를 감당할 수가 없습니다. 그래서 NAT라고 하는 것이 나왔습니다. 내부 네트워크에서는 내부에서만 알아볼 수 있는 IP 대역(private)을 사용하고 외부로 나갈 때 외부에서 인식 가능한 IP(public)로 바꾸어서 내보내는 거지요. 외부에서 내부로 들어올 때는 public ip를 private ip로 바꾸는 변환이 일어나서 내부 기기로 데이타가 전달되게 됩니다.

NAT를 쓰는 경우에는 내부에서 외부로 나가는 건 간단하다고 할 수 있지만 외부에서 내부로 들어오는 건 쉽지 않습니다. 내부의 기기는 private ip로 연결되어 있기 때문에 외부에서 인식할 수 없기 때문이지요.

이러한 점이 p2p에서는 문제가 됩니다. route등의 설정을 바꾸다던지 해서 어떻게 외부에서 내부로 들어오도록 할 수도 있겠지만 이렇게 불편하면 아무도 p2p를 사용하지 않겠지요. 그래서 p2p에서 사용하는 여러 방법이 있습니다.

- Automatic route configuration

많은 라우터들은 UPnp나 nat-pmp같은 port forwarding 기능을 제공합니다. 따라서, libp2p는 자동으로 이러한 설정을 하는 기능을 제공합니다.

- Hole-punching(STUN)

내부에서 외부로 연결을 맺으려 하면 라우터는 이에 대한 기록(private ip <-> public ip)을 남겨서 내부와 외부가 서로 연결될 수 있도록 해줍니다. 이 경우 외부에서 이 연결고리인 public ip/port를 안다면 외부에서 내부로 연결이 가능하게 됩니다.(좀 더 정확하게는 NAT의 종류에 따라서 이것이 가능하지 않을 수도 있습니다. 참고) 그런데 보통의 경우에는 내부의 기기는 라우터를 통할 때 자신의 ip:port가 무었이 될지 알 수 없습니다. 하지만! 외부의 커넥션이 맺어진 기기는 알 수 있겠죠. 이것을 이용한 것이 STUN입니다.

내부의 기기에서 이 STUN 서버에 접속을 하면 STUN 서버는 연결이 들어온 기기의 ip와 port 정보를 응답으로 보내줍니다. 그러면 나는 내가 외부로 나갈 때의 ip와 port를 알 수 있게 되고 다른 외부의 기기들에게 '내 ip와 port는 이거야'라고 알려주면 외부의 기기들은 나한테 접속할 수 있게 되는 거죠.

libp2p의 경우는 identity protocol이라고 하는 것을 통해서 이와 유사한 기능을 구현하고 있습니다.

- AutoNAT

앞에서 말했듯이 NAT에 따라서 STUN이 제대로 동작하지 않을 수 있습니다. 그러므로 외부의 기기가 접속을 해봐서 접속이 가능한지 아닌지를 알려주면 좋을 겁니다. 이 정보를 얻은 후 접속이 잘 되는 경우에만 외부에 '내 ip와 port는 이거야'라고 알려주는 것이 좋을테니까요.

- Circuit Relay(TURN)

대상이 되는 두 기기가 직접 통신하지 않고 중간에 relay를 해주는 기기를 두고 이 기기를 통해서 통신을 하도록 하는 방법입니다.

2019년 10월 17일 목요일

Convenient and idiomatic conversions in Rust 를 읽고

https://ricardomartins.cc/2016/08/03/convenient_and_idiomatic_conversions_in_rust

Rust에서 타입을 conversion하는데에 사용되는 trait가 있습니다.

From<T>, Into<U>, TryFrom<T>, TryInto<U>, AsRef<U>, AsMut<U>

=> T가 원본 타입이고 U가 타겟이 되는 타입이라고 보면 된다.

From<T> for U

T라는 타입을 U으로 변경하는 trait
reflexive trait임. From<T> for T 의 경우 자기자신을 그대로 리턴한다.
실패하지 않는다.
From<T> for U를 구현하면 Into<U> for T도 같이 자동으로 구현된다.

Into<U> for T

T라는 타입을 U로 변경하는 trait

TryFrom<T>

From<T>와 같은데 변환에 에러가 있을 수 있는 경우 사용한다. 따라서, 결과가 Result<Self, Self::Error>이다.

TryInto<U>

TryFrom<T>와 같다고 보면 된다. 결과가 Result<T, Self::Error> 이다.

AsRef<U>

immutable reference를 다른 타입의 immutable reference로 변환한다.

AsMut<U>

mutable reference를 다른 타입의 mutable reference로 변환한다.

AsRef<U>와 AsMut<U>에는 generic implementation이 있다.

=> AsRef<U>나 AsMut<U>를 구현하고 있는 type에의 reference에 대한 구현으로서
&&&&vec이나 &&&& mut vec 같은 multiple-level deep reference를 &vec와 같게 만들어 준다.(왜 이렇게 되는건지 어떤 사용 예가 있는 건지 이해 못함ㅠㅠ)

2019년 10월 16일 수요일

Testing two consecutive LiveData emissions in Coroutines 를 읽고

https://medium.com/androiddevelopers/testing-two-consecutive-livedata-emissions-in-coroutines-5680b693cbf8

LiveData에서의 데이타 발생을 테스트할 때 발생했던 문제를 이야기 합니다.

예를 들어 네트워크로부터 데이타를 가져오는데 시간이 걸리는 경우 우선은 화면에 더미? 또는 간단한 데이타를 보여주고 실제 데이타를 얻어오면 다시 화면을 업데이트 하게 할 수 있습니다.
이에 대한 테스트 코드를 만들어서 테스트를 한다면 하나의 LiveData를 만들어서 emit이 두번 일어나는지를 확인해 보게 될겁니다. 그런데 LiveData는 최종 데이타만을 가지고 있기 때문에 Dispatcher.UnConfined의 코루틴으로 테스트를 했더니 항상 첫번째 emit은 일어나지 않고 두번째 emit만 일어나서 테스트가 실패했다고 합니다.

이에 대한 해결책으로 TestCoroutineDispatcher의 pauseDispatcher와 resumeDispatcher를 사용해서 두번 emit이 일어날 수 있도록 변경했다고 합니다. TestCoroutineDispatcher에 코루틴의 동작을 제어(중단 및 다시 실행)할 수 있는 함수가 있어서 이를 통해 해결이 가능했네요.

이외에 liveData coroutines builder를 사용해서도 해결할 수 있다고 합니다. 이 builder를 쓰면 데이타를 전달할 때 emit() 함수를 사용하게 되는데 이 경우 데이타 전달이 확실히 일어나게 되는거 같네요.

Good Practices

- 코루틴을 사용할 때 Dispatcher는 꼭 Dependency Injection의 형태로 사용해라.

- Dispatchers.UnConfined 대신에 TestCoroutineDispatcher를 사용해라.


ViewModel with SavedStateHandle

ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders

Saving UI state with ViewModel SavedState and Dagger

ViewModels: State persistence — SavedState

A Deep Dive into Extensible State Saving

요즘 안드로이드 개발에서는 ViewModel을 많이 사용합니다. Configuration 변경에 대한 처리를 위해 사용하기도 하겠지만 저같은 경우는 MVVM 아키텍쳐 적용에 따라 ViewModel을 사용하고 있습니다.
안드로이드에서는 보통은 view에 관련된 data를 저장하고 싶을 때 onSaveInstanceState를 통해 저장합니다. 그리고 나서 나중에 Activity나 Fragment가 생성될 때 savedInstanceState를 확인해서 필요한 값이 있으면 꺼내서 사용하죠.
그런데 MVVM을 사용하게 되면서 data를 ViewModel쪽으로 이동하게 되고 이럴경우 data를 어떻게 유지할 것인가가 이슈가 됩니다.
이에따라 구글에서는 이에 관련된 lifecycle-viewmodel-savedstate 라는 모듈을 내놓았습니다. 이 모듈에서는 어떻게 ViewModel과 Activity/Fragment 사이에서 state(data)를 유지하는지 살펴보겠습니다.

크게는 Activity/Fragment에서의 처리와 ViewModel에서의 처리 이렇게 둘로 나누어서 볼 수 있습니다.

Activity/Fragment의 경우는 기본 컨셉은 간단합니다. 종료시 ViewModel에서 제공하는 state를 저장하고 다시 시작할 때 이렇게 저장된 state를 꺼내와서 ViewModel에 전달해 줍니다.

Activity/Fragment -> SavedStateRegistryController -> SavedStateRegistry

위의 관계로 SavedStateRegistry의 performSave/performRestore 함수에서 state를 저장하고 꺼내오는 역할을 해줍니다.(코드로 보면 onCreate에서 performRestore를 호출하고, onSavedInstanceState에서 performSave를 호출해 줍니다.)
또한 Activity/Fragment는 SavedStateRegistryOwner를 구현합니다. 이때의 구현 함수인 getSavedStateRegistry()를 통해 Activity/Fragment로부터 SavedStateRegistry를 얻어서 사용할 수 있습니다.(나중에 ViewModelFactory쪽에서 사용하게 됩니다.)

ViewModel을 생성하기 위해 SavedStateViewModelFactory를 사용합니다. 아래처럼 사용하는 건 다들 아실겁니다.

private val viewModel by viewModels { SavedStateViewModelFactory(SavedStateRegistryOwner) }

짜잔~ Factory에서 SavedStateRegistryOwner를 파라메터로 받네요. Factory는 SavedStateRegistryOwner로부터 SavedStateRegistry를 얻어오고 이것으로 부터 자신이 저장했던 state를 가져와서 ViewModel에 다시 파라메터로 넘겨줍니다.

간단하게 코드를 살펴보려면 AbstractSavedStateVMFactory의 create 함수를 보면 됩니다.
SavedStateRegistry의 consumeRestoredStateForKey를 호출해서 state를 얻고, 또한 ViewModel에서 저장한 state가 저장될 수 있도록하기 위해 SavedStateRegistry의 registerSavedStateProvider를 호출해서 지금 생성하는 ViewModel의 key와 SavedStateProvider를 등록해 줍니다.

앞에서 state를 저장할 때 performSave를 호출한다고 했습니다. 이때 저장된 모든 (key, provider)에서 provider를 꺼내와서 provider.savedState()를 통해 state를 저장합니다. ViewModel에서 저장하고자 하는 state는 SavedStateHandle에 저장되게 되는데요. SavedStateProvider의 savedSate()를 호출하면 SavedStateHandle에 저장된 state를 가져오게 됩니다.

ViewModel -> SavedStateHandle -> SavedStateProvider

2019년 10월 12일 토요일

libp2p: Transport 를 읽고

https://docs.libp2p.io/concepts/transport/

libp2p는 p2p 관련 프로토콜의 집합입니다.
p2p 기능을 원하는 다른 곳에서 쓰일 수 있도록 라이브러리로 되어 있습니다.

libp2p가 동작하기 위한 기본이 되는 네트워크 연결은 다양한 프로토콜을 통해 이루어 질 수 있게 되어 있습니다.(libp2p에서는 이를 transport라 부릅니다.)
이를 위해 libp2p는 TCP, WebSocket, WebRTC, QUIC등 다양한 프로토콜(참고)을 지원하고 있습니다.

개별 peer간에 연결이 이루어지려면 peer를 구별하는 이름같은 것이 있어야 할 텐데요. 이를 위해 multiaddress(참고)를 사용합니다.
간단하게 예를 들어보면 다음과 같은 형태가 됩니다.

/ip4/1.2.3.4/tcp/4321/p2p/QmcEPrat8ShnCph8WjkREzt5CPXF2RwhYxYBALDcLC1iV6

ip4 주소는 1.2.3.4 이고, tcp 포트는 4321이고, QmcEPrat...는 PeerId를 의미하는 데요. 누구에게 접속하는지를 표시하는 거라고 보면 됩니다.

PeerId(참고)를 좀 더 자세히 볼까요? peer를 유일하게 구별하는 이름인데요.
기술적으로는 public key의 해시 값입니다. 개별 peer들은 동작시 private/public key 쌍을 생성해서 유일한 값을 만들어 내고 이를 자기를 구별하는 값으로 사용합니다.

libp2p를 사용하는 애플리케이션이 하나의 transport만을 사용할 필요는 없습니다. 동시에 여러개의 transport를 지원할 수 있습니다.
이를 switch라고 합니다. switch를 통해 protocol negotiation, stream multiplexing, secure communications, connection upgrading을 할 수 있습니다.

Generic interfaces 요점

 https://go.dev/blog/generic-interfaces  Generic interface를 정의할 때 최소한의 제약만을 정의하고 실제 구현체들이 자신만의 필요한 제약을 추가할 수 있도록 하는 것이 좋다. pointer receiver를...