2017년 1월 16일 월요일

Android에서 REST client 구현 관련.. TLS 1.2 지원

예전에 구현했던 것을 다시 업데이트 하다 보니
많은 부분이 변경이 되어 관련 정리를 해봄.

일단 REST architecture 개념은 다음 논문을 참고하시라.
http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm


먼저 HTTPS 연결이 안드로이드 내부에서 자동으로 되는 것 같음.
예전에는 별도 class를 만들어서 연결 했었던 것 같은데

https://developer.android.com/training/articles/security-ssl.html#HttpsExample

잘 알려진 CA가 발급한 인증서가 여러분의 웹 서버에 있다고 가정하면, 다음과 같은 간단한 코드로 보안 요청을 수행할 수 있습니다.
URL url = new URL("https://wikipedia.org");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
그렇습니다. 정말로 이렇게 간단합니다. 맞춤형 HTTP 요청을 수행하려면, HttpURLConnection으로 캐스트하면 됩니다. HttpURLConnection의 Android 문서에는 더 자세한 예시가 있으며, 이 예시에서는 요청과 응답 헤더 처리, 콘텐츠 게시, 쿠키 관리, 프록시 사용, 응답 캐싱 등에 대해 설명합니다. 그러나 인증서와 호스트 이름을 확인하는 자세한 절차는 Android 프레임워크가 이러한 API를 통해 자동으로 처리합니다. 가능하면 여기에서 모든 것을 해결할 수 있습니다. 하지만, 아래와 같은 다른 고려사항도 있습니다.

기본적인 HTTPURLConnection 사용법은 다음과 같음.

https://developer.android.com/reference/java/net/HttpURLConnection.html

URL url = new URL("http://www.android.com/");
   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
   try {
     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
     readStream(in);
   } finally {
     urlConnection.disconnect();
   }

정말 간단하고
REST client를 만들기 위해서 필요한 method 조정이나 upload 관련 부분도 API를 지원해서 간단하게 처리할 수 있을 것 같음.

HTTP Methods

HttpURLConnection uses the GET method by default. It will use POST if setDoOutput(true) has been called. Other HTTP methods (OPTIONSHEADPUTDELETE and TRACE) can be used with setRequestMethod(String).

Posting Content

To upload data to a web server, configure the connection for output using setDoOutput(true).
For best performance, you should call either setFixedLengthStreamingMode(int) when the body length is known in advance, or setChunkedStreamingMode(int) when it is not. Otherwise HttpURLConnection will be forced to buffer the complete request body in memory before it is transmitted, wasting (and possibly exhausting) heap and increasing latency.
For example, to perform an upload:
   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
   try {
     urlConnection.setDoOutput(true);
     urlConnection.setChunkedStreamingMode(0);

     OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
     writeStream(out);

     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
     readStream(in);
   } finally {
     urlConnection.disconnect();
   }


근데...

TLS 1.2 지원에 좀 문제가 있는지 지원을 안하는지..
개발 중인 서비스 서버에 연결이 제대로 안된다.
일단 예전에 사용하던 방식을 다시 사용하긴 했는데 찾아보니 다른 방법들도 꽤 있는 것 같음.

일단은 그냥 사용하게 된 예전 코드
 : https://github.com/hallower/wimple/blob/master/app/src/main/java/kr/blogspot/charlie0301/wimple/impl/RestAPIInvoker.java#L49



2017년 1월 12일 목요일

Android 6.0 버전 대상 App에서의 permission 요청

기존에 만들었던 앱을 변경해서 올리던 중
SMS receving이 안되어 원인을 찾다보니
아래 내용을 깜박 잊고 있었음.. ㅎㅎ

하긴 2015년 7월에 들은 내용이니....
개인 앱 개발이라 뭐.. 게을러도 너무 게으르다는 생각을 ㅎㅎ

http://charlie0301.blogspot.kr/2015/07/google-io-extended-2015-android-track.html

[런타임 앱 권한]

- 기존에 설치 시 권한을 요청하고 일괄 승인 했던 것을
M target app에서는 이전처럼 manifest에 권한을 명시하더라도
권한을 사용할 때 사용자에게 다시 물어보도록 함.
또한 환경설정에서 앱별 권한을 설정할 수 도 있음.

- Runtime 시 개별적으로 권한을 요청 할 때 시스템 프로세스 상에서 권한 요청이 실행 됨.
- 액티비티 뿐만 아니라 서비스에서 Runtime시 permission 요청을 해야 할 수 있음.

- 콜백으로 사용자 응답 받게 되므로 기존 권한을 사용하던 부분을 callback trigger를 전후로 나눠 처리 해야 함.

- M 이전 Android 버전들 타겟 앱은 이전 방식 처럼 권한을 수락하게 됨.

- M 버전 기반 앱의 권한이 거부되면 안드로이드에서는 가짜 데이터를 제공할 예정이고 앱의 동작은 가늠할 수 없으므로 앱이 권한 거부 상황을 잘 처리하도록 만들어야 함.

- 권한이 변경되더라도 자동으로 업데이트 가능함. (설치 시 권한 수락이 필요 없으니)



Android 6.0 버전 이상을 대상으로 하는 App의 Manifest에 명시된 앱 권한이 사용자로 부터 허락되지 않을 경우 아래와 같은 에러 메세지가 log로 표시된다.

Permission Denial: receiving Intent { act=android.provider.Telephony.SMS_RECEIVED flg=0x18000010 (has extras) } to com.blogspot.charlie0301/kr.blogspot.charlie0301.wimple.SMSReceiver requires android.permission.RECEIVE_SMS due to sender com.android.phone (uid 1001)

동일 case : http://stackoverflow.com/questions/32537156/basic-error-in-androidmanifest-xml-for-sms-receiving-permission


그래서 App에서 해당 권한이 필요한 기능을 사용할 때 사용자에게 팝업을 띄워 권한을 요청해야 한다. 자세한 설명은 아래에 잘 번역 되어 있음.
: https://developer.android.com/training/permissions/declaring.html

그래서 대충 copy & paste로 다음과 같이 붙임.

권한 요청 시점
 : 설정에서 SMS read 하는 기능을 on/off 하는 부분이 있어 on 될 때 권한을 요청하도록 함.
 : https://github.com/hallower/wimple/blob/master/app/src/main/java/kr/blogspot/charlie0301/wimple/SettingsFragment.java#L121

권한 요청 팝업 표시
 : ContextCompat.checkSelfPermission()로 권한이 수락되지 않았는지 확인하여 ActivityCompat.shouldShowRequestPermissionRationale()로 권한 수락에 대한 설명이 필요한 지 확인함.

[구글 가이드내 코드] 
// 권한이 필요한 시점에
if (ContextCompat.checkSelfPermission(thisActivity,
                Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {

    // 사용자가 이미 Deny를 했는지 확인하는 절차
    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
            Manifest.permission.READ_CONTACTS)) {

        // Deny를 했다면 적절한 설명을 보여주고 사용자의 반응에 따라 다시 요청 해야 함.

    } else {

        // 권한 요청을 하지 않았었으면 권한을 요청함.

        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.READ_CONTACTS},
                MY_PERMISSIONS_REQUEST_READ_CONTACTS);

        // MY_PERMISSIONS_REQUEST_READ_CONTACTS 은 
        // callback에서 권한 처리 결과를 받기 위한 상수값 
    }
}

실제 구현은 귀찮아서 그냥 deny하면 설명만 보여주게 하였음.

 : https://github.com/hallower/wimple/blob/master/app/src/main/java/kr/blogspot/charlie0301/wimple/WimpleActivity.java#L168



사용자의 권한 수락/거절 이후 처리
 : 이후 사용자의 수락/거절에 따라 callback method가 호출되어 결과를 알 수 있음.


@Override
public void onRequestPermissionsResult(int requestCode,
        String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
            // 요청이 취소되었을 경우 result가 null일 수 있어 에러 처리 필요
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                // 권한이 수락되었음

            } else {

                // 권한이 거절됨...
            }
            return;
        }

        // 다른 권한에 대해서 case 추가 처리 하면 됨.
    }
}

일단 구현에서는 그냥 메세지만 보여주는 것으로 간단하게 대충 구현함.

 : https://github.com/hallower/wimple/blob/master/app/src/main/java/kr/blogspot/charlie0301/wimple/WimpleActivity.java#L181


안드로이드 사이트에서 설명하고 있지만 특히나 고려해야할 사항이

- 권한 요청 시점을 필요한 시점으로 선택해야 한다.
 : 일부 앱에서는 시작 시 무조건 권한을 확인하게 하던데 별로 좋지 않은 방법인 듯 하다.
 : 특히나 위 예제를 시작 시점에 붙여 넣게 되면 Deny 되었을 경우 앱 실행 시 마다 권한 설명을 표시 되거나 권한을 재요청을 하게 하는 loop이 생길 수 있음.
 : 권한을 사용하는 시점에 요청 하는 것이 가장 바람직한 듯 함.


- 사용자가 거절 했을 경우 이후 처리를 잘해야 겠음.
 : 제한된 메세지 내용을 통해 안드로이드 설정의 앱 목록에서 선택하여 권한을 설정하라는 가이드하는 것은 어려울 것 같음.
 : 그래서 간단한 권한 설명과 사용자가 메세지를 읽고 적절한 시점에 권한을 요청해야 할 것 같음.
 : 그냥 무조건 선택하라는 안내는 더 도움이 안될 것 같고

Xamarin.Forms DependencyService

Xamarin.Forms를 사용하다보면
Xamarin.Form에서 제공하지 않는 Platform 특화 기능들을 사용해야 할 필요가 있는데
이때 필수적으로 사용해야 하는 것이 Dependency Service임.

Cross Platform App을 만들기 위해
Shared Project 구조를 가진 Xamarin.Forms 프로젝트에서는
어쩔 수 없이 Platform project의 기능을 호출해야 하지만 이는 Shared code project에서 Platform project로의 strong dependency를 가지는 것이라 바람직하지 않음.
그래서 Dependency Service을 이용하면 다소 불편하기 하지만 언급된 문제를 회피하여 필요한 Platform dependent한 기능을 사용할 수 있음.

아래는 Xamarin documentation URL임.

Accessing Native Features with DependencyService
: https://developer.xamarin.com/guides/xamarin-forms/dependency-service/


간단히 보자면 DependencyService를 사용하기 위해서는 다음의 사항들이 필요하다.


DependencyService를 사용하는 Shared code project app 구조는 대충 이런 느낌이다.

App 만들 때 가장 기본적인 Device Orientation을 조회하는 기능을 DependencyService를 사용해서 해결하는 예제를 보면(... 이런것도 Xamarin.Forms에 없다는 것은 뭐.. 그냥.. )

Checking Device Orientation



구조는 대충 이런 느낌이다.

먼저 Shared code project에서 아래와 같이 Interface를 정의한다.
namespace DependencyServiceSample.Abstractions
{
    public enum DeviceOrientations
    {
        Undefined,
        Landscape,
        Portrait
    }

    public interface IDeviceOrientation
    {
        DeviceOrientations GetOrientation();
    }
}

Platform project에서 정의된 IDeviceOrientation에 맞춰 Class를 구현한다.
단 default 생성자 작성을 잊지 말자. default 생성자가 구현되어 있지 않다면 dependency service로 호출 시 exception이 발생한다.

using DependencyServiceSample.Droid;
using Android.Hardware;

namespace DependencyServiceSample.Droid
{
    public class DeviceOrientationImplementation : IDeviceOrientation
    {
        public DeviceOrientationImplementation() { }

        public static void Init() { }

        public DeviceOrientations GetOrientation()
        {
            IWindowManager windowManager = Android.App.Application.Context.GetSystemService(Context.WindowService).JavaCast<IWindowManager>();

            var rotation = windowManager.DefaultDisplay.Rotation;
            bool isLandscape = rotation == SurfaceOrientation.Rotation90 || rotation == SurfaceOrientation.Rotation270;
            return isLandscape ? DeviceOrientations.Landscape : DeviceOrientations.Portrait;
        }
    }
}

DependencyService에 IDeviceOrientation를 구현한 DeviceOrientationImplementation class를 dependency service에 등록을 위해 metadata를 선언하고 Xamarin.Forms의 dependency service의 register를 호출하여 등록한다.

using DependencyServiceSample.Droid; //enables registration outside of namespace
using Android.Hardware;

[assembly: Xamarin.Forms.Dependency (typeof (DeviceOrientationImplementation))]
namespace DependencyServiceSample.Droid {
    ...
Xamarin.Forms.Forms.Init(e, assembliesToInclude);
// register the dependencies in the same
Xamarin.Forms.DependencyService.Register<TextToSpeechImplementation>();

그럼 Shared code project에서 아래와 같이 사용 가능하다. 끝..

public MainPage ()
{
    var orient = new Button {
        Text = "Get Orientation",
        VerticalOptions = LayoutOptions.CenterAndExpand,
        HorizontalOptions = LayoutOptions.CenterAndExpand,
    };
    orient.Clicked += (sender, e) => {
       var orientation = DependencyService.Get<IDeviceOrientation>().GetOrientation();
       switch(orientation){
           case DeviceOrientations.Undefined:
                orient.Text = "Undefined";
                break;
           case DeviceOrientations.Landscape:
                orient.Text = "Landscape";
                break;
           case DeviceOrientations.Portrait:
                orient.Text = "Portrait";
                break;
       }
    };
    Content = orient;
}

2017년 1월 9일 월요일

[Links] UnitTest in Visual Studio, C#, Xamarin.

개발하는 app의 Unit Test를 생각하던차에 Visual Studio에서 제공해주는 기능을 사용해 봄.
솔직히 개발하는 측면에서는 MS에서 제공하는 tool들은 정말 넘사벽인듯..

Visual Studio 2015에서 제공하는 Unit Test project를 사용하는 방법임.

다른 Framework을 사용하지 않은 C#으로 구현된 로직의 검증을 위해서 사용가능하여
Xamarin 같은 framework을 사용하는 코드는 이 Unit Test project를 사용해서 검증하기는 어려울 것 같다. (ex Xamarin init이 되어야 Xamarin asset 사용 가능)

Unit Test Basics
: https://msdn.microsoft.com/en-us/library/hh694602.aspx

간단히 말하면 unit test용 project를 solution에 추가한뒤
Unit Test Class를 구현하여 Text Explorer(테스트 탐색기, 테스트 메뉴 > 창 에 있음)를
에서 실행하여 결과를 확인함.


간략히 정리된 Workthrough 문서
Visual Studio Unit Test Case 만들기 Workthrough
: https://msdn.microsoft.com/en-us/library/ms182532.aspx


Testing 시 필수적인 Testing Coverage를 확인할 수 있는 방법이 있지만..
Visual Studio Enterprise에서만 된다..

Using Code Coverage to Determine How Much Code is being Tested
: https://msdn.microsoft.com/en-us/library/hh694602.aspx


찾다 보니 Xamarin에서는 UI Test를 지원하던데. 좀 더 봐야 겠음.
: https://developer.xamarin.com/guides/testcloud/uitest/intro-to-uitest/

[Link] GivenWhenThen


GivenWhenThen




Given-When-Then is a style of representing tests - or as its advocates would say - specifying a system's behavior using SpecificationByExample. It's an approach developed by Dan North and Chris Matts as part of Behavior-Driven Development (BDD). [1] It appears as a structuring approach for many testing frameworks such as Cucumber. You can also look at it as a reformulation of the Four-Phase Test pattern.
The essential idea is to break down writing a scenario (or test) into three sections:
  • The given part describes the state of the world before you begin the behavior you're specifying in this scenario. You can think of it as the pre-conditions to the test.
  • The when section is that behavior that you're specifying.
  • Finally the then section describes the changes you expect due to the specified behavior.


INTRODUCING BDD
: https://dannorth.net/introducing-bdd/

As a [X]
I want [Y]
so that [Z]
where Y is some feature, Z is the benefit or value of the feature, and X is the person (or role) who will benefit. Its strength is that it forces you to identify the value of delivering a story when you first define it. When there is no real business value for a story, it often comes down to something like ” . . . I want [some feature] so that [I just do, ok?].” This can make it easier to descope some of the more esoteric requirements.

Given some initial context (the givens),
When an event occurs,
then ensure some outcomes.
To illustrate, let’s use the classic example of an ATM machine. One of the story cards might look like this:
+Title: Customer withdraws cash+
As a customer,
I want to withdraw cash from an ATM,
so that I don’t have to wait in line at the bank.
So how do we know when we have delivered this story? There are several scenarios to consider: the account may be in credit, the account may be overdrawn but within the overdraft limit, the account may be overdrawn beyond the overdraft limit. Of course, there will be other scenarios, such as if the account is in credit but this withdrawal makes it overdrawn, or if the dispenser has insufficient cash.
Using the given-when-then template, the first two scenarios might look like this:
+Scenario 1: Account is in credit+
Given the account is in credit
And the card is valid
And the dispenser contains cash
When the customer requests cash
Then ensure the account is debited
And ensure cash is dispensed
And ensure the card is returned
Notice the use of “and” to connect multiple givens or multiple outcomes in a natural way.
+Scenario 2: Account is overdrawn past the overdraft limit+
Given the account is overdrawn
And the card is valid
When the customer requests cash
Then ensure a rejection message is displayed
And ensure cash is not dispensed
And ensure the card is returned

2017년 1월 8일 일요일

Android App 시작 시점에 표시되는 virtual keyboard 숨기기

App의 시작 화면에 EditText가 있어 시작될 때 마다
Virtual Keyboard가 항상 표시되는 상황이라 귀찮아서 숨기려 하여 찾아봄.

일단 인터넷에서 소개되는 방법은..

InputMethodManager를 사용하여 virtual keyboard를 숨기는 방법

http://stackoverflow.com/a/1109108

You can force Android to hide the virtual keyboard using the InputMethodManager, calling hideSoftInputFromWindow, passing in the token of the window containing your focused view.
// Check if no view has focus:
View view = this.getCurrentFocus();
if (view != null) {  
    InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
    imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
This will force the keyboard to be hidden in all situations. In some cases you will want to pass in InputMethodManager.HIDE_IMPLICIT_ONLY as the second parameter to ensure you only hide the keyboard when the user didn't explicitly force it to appear (by holding down menu).
shareimprove this answer

여기가 좀 더 설명이 자세하다.
https://realm.io/kr/news/tmi-dismissing-keyboard-ios-android/

하지만 난 Activity내의 fragment 중 EditText로 인한 virtual keyboard를
App 시작 시점부터 띄위지 않도록 하기 위한 방법을 찾던지라
위 방법은 내가 원한 상황에서 적용되기는 어려웠음.

결국 찾아낸 방법은 다음과 같다.

EditText가 아닌 다른 item에 focus를 주어 Virtual Keyboard를 띄우지 않는 방법

http://stackoverflow.com/a/1662088


1419down voteaccepted
Excellent answers from Luc and Mark however a good code sample is missing:
<!-- Dummy item to prevent AutoCompleteTextView from receiving focus -->
<LinearLayout
    android:focusable="true" 
    android:focusableInTouchMode="true"
    android:layout_width="0px" 
    android:layout_height="0px"/>

<!-- :nextFocusUp and :nextFocusLeft have been set to the id of this component
to prevent the dummy from receiving focus again -->
<AutoCompleteTextView android:id="@+id/autotext"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"
    android:nextFocusUp="@id/autotext" 
    android:nextFocusLeft="@id/autotext"/>
shareimprove this answer


좀 예제가 일반적이지는 않아 나의 경우를 말하면
EditText의 다른 item에 android:focusable="true", android:focusableInTouchMode="true"를 주어
EditText에 focus가 가지 않도록 하여 virtual keyboard를 자동으로 띄우지 않도록 하였음.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:focusable="true"
    android:focusableInTouchMode="true">

.........
                    <EditText
                        android:id="@+id/insert_entry_title"
                        android:layout_width="fill_parent"
                        android:layout_height="32dp"