WebView를 사용하는 앱 개발시 Eclipse의 TCP/IP Monitor를 통해 WebView와 웹서버간의 HTTP 통신내용을 보는 방법을 설명합니다.

1. 프록시 세팅

import myapp.ProxySettings;
...
public class MyWebActivity extends Activity {

   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      
      if (isDebugMode(
this.getApplicationContext())) { 
         ProxySettings.setProxy(this.getApplicationContext(), "본인로컬PC의 IP주소", 프록시포트);
      } 
      ...
      ...
      myWebView.loadUrl("http://웹서버IP:웹서버포트/xxxx.jsp");
   }


웹페이지를 로딩하기 전에 ProxySettings를 통해 프록시를 설정합니다. ProxySettings 클래스는 http://manojtk.blogspot.com/2011/01/android-webview-proxy-setting.html 사이트에서 구할 수 있습니다.

여기서 주의할 점이 2가지가 있습니다.

(1) 개발자 PC의 IP주소에 localhost나 127.0.0.1 루프백 주소를 쓰면 안됩니다. 앱은 PC가 아니라 폰에서 실행되므로 루프백 주소는 폰의 로컬 IP입니다.
(2) 프록시 설정은 실제 마켓에 올라가는(production 환경) 앱에 반영되면 안되므로 개발시점(development 환경)에서만 적용되도록 해야합니다. 위 예제소스에서는 앱이 debug모드로 서명되었는지를 체크하여 프록시를 세팅하고 있습니다.

    private boolean isDebugMode(Context context) {
        try {
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), 0);
            int flags = packageInfo.applicationInfo.flags;
            return (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
        } catch (NameNotFoundException e) {
            return true;
        }
    }

    

2. 이클립스에서 TCP/IP 모니터링 설정
 



(1) 이클립스 Preferences 창을 띄웁니다.
(2) TCP/IP Monitor 메뉴를 선택합니다.
(3) Add 버튼을 눌러 모니터를 추가합니다.

* Local monitoring port : 앞서 설정한 프록시 포트
* Host name : 웹서버 IP
* Port : 웹서버 포트

(4) Start 버튼을 눌러 모니터링을 시작합니다.


3. 이클립스에서 TCP/IP Monitor 뷰를 통해 HTTP통신내용 확인
폰에서 폰갭앱을 실행하여 웹페이지를 띄우면 이클립스에서 TCP/IP Monitor뷰가 뜨면서 통신내용을 볼 수 있습니다.

 
Posted by 에코지오
,
안드로이드에서 Ant build.xml 스크립트를 통해 앱을 빌드하는 경우에 ant release 명령어를 실행하면 기본적으로는 서명 안된 앱이 만들어집니다. 서명된 앱을 만들려면 build.properties에 아래처럼 키 파일 위치를 지정해주면 되죠.

key.store=path/to/mykeystore.jks
key.alias=mykeystorealias


그러나 문제는 이렇게 설정하고 ant release를 다시 실행하면 키스토어 비밀번호를 묻는 프롬프트가 뜨면서 우리에게 입력을 요구한다는 겁니다.

... ....
-package-release:
[apkbuilder] Creating MyApp-unsigned.apk for release...

-release-prompt-for-password:
    [input] Please enter keystore password (store:mykeystore.jks):
mykeystorepass (입력값)
    [input] Please enter password for alias 'mykeystorealias':
mykeystorealiaspass (입력값)
... ....


어쩌다가 빌드한다면 별 문제 아니지만 수시로 소스 수정해서 빌드해야하는 경우라면 무지 귀찮은 일입니다. 한마디로 완전한 자동화가 아닌겁니다.
버뜨, 방법이 있습니다. build.properties에 아래 프로퍼티를 추가해주면 비밀번호 프롬프트 없이 release모드로 서명된 apk가 만들어집니다.

key.store.password=mykeystorepass
key.alias.password=mykeystorealiaspass


보안이 염려스럽다면 ant 실행옵션에 추가해주셔도 됩니다.

ant -Dkey.store.password=mykeystorepass  -Dkey.alias.password=mykeystorealiaspass release


Posted by 에코지오
,
앱의 규모가 커지고 여러가지 내부 로직/기능이 추가되다보면 전체 애플리케이션 라이프사이클 동안 하나의 인스턴스만 유지될 필요가 있는 클래스들이 늘어나게 됩니다. 그렇다고 이러한 공통모듈 성격의 클래스들에 싱글턴 패턴을 일일이 적용하는 것은 그리 좋은 방법은 아닙니다.

Spring프레임워크를 써보신 분은 다 아시겠지만 스프링에는 빈들을 체계적으로 생성해서 관리해주는 BeanFactory라는 놈이 있습니다. 안드로이드에서도 이러한 bean들의 컨테이너 역할을 하는 뭔가가 있으면 좋겠다고 생각해서 안드로이드에 Spring 프레임워크를 포함시키면 자칫 배보다 배꼽이 커질 수 있습니다.
다행히 안드로이드에는 android.app.Application 클래스가 있어서 이러한 컨테이너 역할을 수행하게 할 수 있습니다.

android.app.Application 클래스
이 클래스는 액티비티들보다 좀더 상위레벨 라이프사이클을 갖는 클래스라고 볼 수 있습니다. 우리는 이 클래스를 상속받아 간단하게나마 여러 컴포넌트의 인스턴스를 초기화하고 (하나의 인스턴스만 유지되도록) 관리할 수 있습니다.

나만의 Application 만들기
먼저 android.app.Application 클래스를 상속받아 우리만의 MyApplication을 만듭니다. 그리고 onCreate()에서 싱글턴으로 존재하길 원하는 객체를 생성합니다.

public class MyApplication extends Application {

    private static Context context;
    private static MyComponent myComponent;
    ... ... 

    public void onCreate() {
        MyApplication.context = getApplicationContext();
        MyApplication.myComponent = new MyComponent(MyApplication.context);
        ... .... 
    }
 

    /**
     * Activity와 관계없는 컴포넌트에서 부득이 Application Context를 참조하기 위한 용도.
     * Activity에 대한 참조가 가능한 티어에서는 굳이 이 메소드를 통해 context를 구하지 않아도 된다.
     */
    public static Context getAppContext() {
        return context;
    }

    public static MyComponent getMyComponent() {
        return myComponent;
    } 
... ... 


매니페스트에 Application 등록
이렇게 만든 MyApplication을 매니페스트 파일에 등록해두면 MyApplication은 앱 실행시 다른 액티비티들보다 가장 먼저 먼저 초기화됩니다.
따라서 MyApplication의 onCreate()메소드는 애플리케이션의 entry point에 해당하므로 공통컴포넌트들을 초기화할 수 있는 적당한 장소가 됩니다.

<application 

     android:icon="@drawable/icon"

     android:label="@string/app_name"

     android:name="myapp.MyApplication">

        <activity android:name=".xxx.MyActivity">

         … …

         … …

</application>  


사용법
이제 Activity든 어디든 MyComponent 인스턴스가 필요하다면 아래와 같이 불러다 쓰면 됩니다.

SomeResult result = MyApplication.getMyComponent().someMethod();


주의점
개발자가 임의로 MyComponent의 인스턴스를 생성하지 않도록 적절히 가이드해야합니다. 가이드가 잘 안지켜지면 MyApplication과 MyComponent를 별도의 패키지로 분리하고 MyComponent 생성자에 대한 접근자를 default로 만들수도 있습니다. 

참고
http://stackoverflow.com/questions/987072/using-application-context-everywhere 
http://code.google.com/p/roboguice/  : 좀더 그럴듯한 빈컨테이너(Dependency Injection 지원)

Posted by 에코지오
,
android.os.AsyncTask는 시간이 오래 걸리는 작업을 백그라운드로 처리하기 위한 코드를 단순화해주는 유틸리티 클래스입니다. (AsyncTask에 대한 사용법은 http://tigerwoods.tistory.com/28 사이트에 친절히 설명되어있습니다.)

그런데 앱에서 AsyncTask를 이용하여 비슷비슷한 작업들을 처리하다보면 AsyncTask에 공통기능을 넣어야할 필요를 느끼게됩니다. 작업을 처리하는 동안 사용자에게 처리중임을 알리는 progress dialog를 띄운다든가, 처리중에 에러가 발생한 경우 에러 toast를 보여준다든가, 사용자가 취소키를 눌렀을 때 작업을 취소시키는 등의 기능이 주요 공통된 사항으로 들어갑니다.

추가된 기능
그래서 기존 AsyncTask에 몇가지 기능을 추가한 EnhancedAsyncTask를 만들어봤습니다.

AsyncTask와 다른 점은 다음과 같습니다.

1. Activity에 대한 약한 참조(weak reference) 유지
2. onPreExecute()에서 '작업처리중' 프로그레스 다이얼로그 자동 시작
3. onPostExecute()에서 프로그레스 다이얼로그 자동 종료
4. doInBackground()에서 에러발생시 프로그레스 다이얼로그 자동 종료 및 에러메시지 토스트보여줌
    - 참고로 doInBackground()에서 에러발생시 하위 클래스의 onPostExecute()는 실행되지 않음
5. '작업처리중' 프로그레스 다이얼로그에서 사용자가 취소키 누르면 onCancelled() 실행
6. onCancelled()에서 프로그레스 다이얼로그 자동 종료 및 작업취소 메시지 보여줌


EnhancedAsyncTask 사용법
EnhancedAsyncTask를 상속받아 생성자를 정의하고 doInBackground(WeakTarget, Params) 메소드를 구현합니다. 필요시 WeakTarget을 아규먼트로 받는 onPreExecute(), onPostExecute(), onCancelled() 메소드를 오버라이드합니다. 이렇게 구현한 EnhancedAsyncTask는 아래처럼 실행시킵니다.

MyLongTask task = new MyLongTask(this); //this는 Activity 인스턴스
task.execute(someParamObjects);

 
* 참고로 onPreExecute(), onPostExecute(), onCancelled() 메소드는 메인 UI쓰레드에서 실행되므로 UI 관련 작업이 가능하지만 doInBackground()는 별도의 쓰레드에서 실행되므로 직접적인 UI 작업은 불가합니다.

LoginTask 예제
다음 코드는 사용자가 입력한 ID/PW를 원격서버에 보내어 인증을 처리하는 작업을 구현한 예제입니다.
 

public class LoginTask extends EnhancedAsyncTask<LoginModel, Void, Boolean, LoginActivity> {
    public LoginTask(LoginActivity target) {
        super(target);
    }
 
    @Override
    protected Boolean doInBackground(LoginActivity target, LoginModel... params) {
        // ID/PW를 리모트 서버로 보내 원격으로 로그인 처리
        // return true or false;
    }

    @Override
    protected void onPostExecute(LoginActivity target, Boolean loginSuccess) {
        if (loginSuccess) {
            // 메인메뉴 화면 진입
        } else {
            // 로그인 실패 메시지 보여주고 로그인 화면 유지
        }
    }


액티비티에서는 아래처럼 LoginTask를 실행하면 됩니다.

 LoginModel loginModel = ...;
 LoginTask task = new LoginTask(this); //this는 LoginActivity 인스턴스
 task.execute(loginModel);


* 물론 EnhancedAsyncTask는 AsyncTask보다는 general하지 않기때문에 프로젝트에 따라서 적절히 수정을 가해야할 것입니다.
Posted by 에코지오
,
웹구간 보안을 위해 SSL을 적용하려면 인증서를 생성해서 웹서버에 설치/설정해주고 브라우저에서는 "https" 프로토콜로 접속하면 됩니다. 그러나 만약 인증서가 Verisign 등 신뢰된 기관(root CA)에서 서명된 인증서가 아니라면, 브라우저는 사용자에게 보안 경고창을 보여주어 웹페이지를 볼 건지 말건지 물어봅니다.


root CA로부터 서명된 인증서라면 이런 경고창 없이 페이지 내용이 바로 보여집니다(참고로 특정 모바일기기에서 어떤 인증기관의 인증서를 root CA로 인식하는지는 http://www.ssltest.net/ 사이트에서 테스트 가능합니다).

root CA로부터 서명을 받기 위한 비용은 대략 1년에 수 십만원이기 때문에 가난한 프로젝트 또는 개인들은 root CA 서명이 아닌 자체(self) 서명된 인증서를 사용할 수밖에 없습니다. 문제는 이렇게 셀프 서명된 인증서는 공식적으로는 신뢰할 수 없으므로 위와 같은 보안 경고창이 뜬다는 것이죠.
사실 보안 경고창이 뜬다고 해도 사용자에게 "계속" 버튼을 누르도록 교육시키면 그리 큰 문제는 아닙니다. 그러나 폰갭에서 똑같은 페이지를 불러온다면 어떤 일이 벌어질까요?

loadUrl("https://yourserver.com/some/page.html");


웁스.. 기대와 달리 그냥 텅빈 화면만 나옵니다.-.-


공백 화면이 나오는 이유는 신뢰되지 않은 인증서인 경우 android.webkit.WebViewClient에서 아래와 같이 페이지로딩 작업을 cancel시키기 때문입니다.

public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
    handler.cancel();
}


자 이제 원인을 알았으니 해결해보죠.

먼저 SSL에러가 발생하더라도 페이지 로딩을 계속 진행하도록 onReceivedSslError() 메소드를 재정의한 클래스를 만듭니다.(DroidGap을 상속한 Activity의 inner 클래스로 정의)

private class MyGapViewClient extends GapViewClient {
    public MyGapViewClient(DroidGap ctx) {
        super(ctx);
    }

   @Override
   public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        handler.proceed();  //SSL 에러가 발생해도 계속 진행!
    }
}


이렇게 만든 MyGapViewClient 클래스를 onCreate()에서 아래처럼 세팅해줍니다.

public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   … …
   super.init();
   this.setWebViewClient(this.appView, new MyGapViewClient(this)); 
   … …
   loadUrl("https://yourserver.com/some/page.html");
}

 
빌드하고 앱을 다시 실행해보면 정상적으로 SSL보안이 적용된 웹페이지가 뜨는 것을 확인할 수 있습니다.
Posted by 에코지오
,
PhoneGap(폰갭)에서는 anrdoid.app.Activity.onKeyDown() 메소드를 아래와 같이 재정의하여 사용자가 하드웨어 취소키(back키)를 누르면 브라우저의 뒤로가기를 수행합니다.

// com.phonegap.DroidGap.onKeyDown(int, KeyEvent) 메소드 

public boolean onKeyDown(int keyCode, KeyEvent event) {
 ... ...
     // If back key
     if (keyCode == KeyEvent.KEYCODE_BACK) {

     // If back key is bound, then send event to JavaScript
     if (this.bound) {
     this.appView.loadUrl("javascript:PhoneGap.fireEvent('backbutton');");
     }

     // If not bound
     else {

     // Go to previous page in webview if it is possible to go back
     if (this.appView.canGoBack()) {
     this.appView.goBack();
     }

     // If not, then invoke behavior of super class
     else {
     return super.onKeyDown(keyCode, event);
     }
     }
     }
... ...


그러나 때로는 사용자가 취소키를 누르면 이전 웹페이지로 이동하지 않고 앱을 종료시키는 것으로 정책을 바꾸고 싶은 경우도 있습니다. (특히나 아이폰을 고려하여 이전버튼을 포함하도록 웹페이지를 디자인한 경우).

폰갭에서는 취소키 동작을 재정의하는 2가지 방법이 있습니다.
 
1. DroidGap의 onKeyDown 메소드 오버라이딩
DroidGap을 상속받은 Activity에서 onKeyDown 메소드를 오버라이딩해서 백버튼을 적절히 핸들링해줍니다. 아래코드에서는 앱 종료여부를 묻는 확인창을 띄우도록 했습니다.

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        confirmAppExit();
        return true;
    }
    return super.onKeyDown(keyCode, event);
}

private void confirmAppExit() {
    AlertDialog.Builder db = new AlertDialog.Builder(this);
    db.setTitle(R.string.exit_app_title)
         .setMessage(R.string.exit_app_message) // "프로그램을 종료하시겠습니까?"
         .setCancelable(true)
         .setPositiveButton(android.R.string.ok, new OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int which) {
                 finish(); //확인버튼 누루면 앱 종료
             }
         })
         .setNegativeButton(android.R.string.cancel, null)
         .show();
}



2. 폰갭 javascript의 'backbutton' 이벤트 재정의(phonegap 0.9.5 이상)

function onLoad() {
    document.addEventListener("backbutton", backKeyDown, true);
}
function backKeyDown() {
    if (confirm('프로그램을 종료하시겠습니까?')) {
        navigator.app.exitApp();
    }
}
...
...
<body onload="onLoad()">


단순히 취소키 눌렀을 때 페이지 뒤로가기를 막으려면 아래코드로 충분합니다.

navigator.app.overrideBackbutton(true);


Posted by 에코지오
,
제가 요즘 폰갭(PhoneGap)을 이용하여 하이브리드 형태의 안드로이드앱을 개발하고 있습니다. 폰갭은 하이브리드앱(웹앱) 개발 프레임워크 중 하나인데요, 웹페이지에서 폰의 다양한 기능/자원을 불러다 쓸 수 있게 해줍니다. 폰갭에서 이러한 기능들은 플러그인(plugin)이라 불리는 것으로 제공됩니다.

폰갭 플러그인 구분
폰갭 플러그인은 앱영역(java)에서 플러그인이 실행되는 방식에 따라 동기식 플러그인과 비동기식 플러그인으로 나눌 수 있습니다. 좀더 자세히 설명하자면, 웹영역(webview)에서 javascript 코드를 통해 플러그인을 호출한 후 플러그인의 실행이 완료되기를 기다리면서 블럭되면 동기식이고 그렇지 않으면 비동기식입니다.

폰갭 플러그인을 제작하려면 com.phonegap.api.Plugin 클래스를 상속받아야 하는데, 이 Plugin 클래스에 isSynch() 메소드가 있습니다. 기본적으로 false를 리턴합니다. 그러니까 이 메소드를 오버라이딩해서 true를 리턴하지 않는 이상 해당 플러그인은 비동기식으로 작동합니다.
비동기식 플러그인의 경우 플러그인 실행결과를 수신(콜백함수 호출)하기 위해 XMLHttpRequest를 이용하거나 또는 Polling 방식을 사용합니다.


 
동기식 플러그인 실행메커니즘


1. Javascript로 "window.plugins.플러그인명.액션명(...)"과 같이 플러그인 함수를 호출하게 되면, phonegap.js 내부적으로 PhoneGap.exec() 함수가 실행됩니다. PhoneGap.exec()에서는 prompt 함수를 실행하면서 플러그인 실행에 필요한 파라미터를 넘겨줍니다.

var r = prompt(xxx, "gap:xxx");


Javascript의 prompt() 함수가 호출되면 앱영역에서는 DroidGap$GapClient.onJsPrompt() 메소드가 실행됩니다. (아하, 폰갭은 Javascript -> Java 호출을 위해 prompt()를 활용하는군요.)

2. DroidGap$GapClient.onJsPrompt() 메소드에서는 prompt의 default value가 "gap:xxx" 형태의 문자열인 경우 PluginManager.exec(...) 메소드를 실행합니다. PluginManager.exec()에서는 아래와 같이 플러그인을 실행하고 그 결과를 JSON 스트링으로 리턴합니다.

// 우선 Plugin이 동기식인지 아닌지 판단 : Plugin.isSynch()
// 동기식인 경우 current thread에서 플러그인을 실행
// (아래 코드는 원래 코드를 단순화한 것입니다)

PluginResult cr = plugin.execute(xxx);
return cr.getJSONString();


3. DroidGap$GapClient는 PluginManager로부터 리턴받은 플러그인 실행결과 스트링을 prompt의 confirmation 응답으로 설정합니다.

jsPromptResult.confirm(플러그인 실행결과);


4. 비로소 웹영역에서는 앱영역에서의 플러그인 실행결과를 prompt 함수의 리턴값(var r 변수)으로 받아냈습니다. 동기식 플러그인의 경우 r 리턴문자열이 반드시 존재하며 r.status에 따라 success 또는 failure 콜백 함수를 호출합니다. 폰갭은 (일단 동기식 플러그인에 대해서) Java -> Javascript 콜백 호출을 위해 prompt 리턴값을 활용함을 알 수 있습니다.


비동기 플러그인 실행메커니즘(XMLHttpRequest 적용시)


1. com.phonegap.CallbackServer는 java.net.ServerSocket을 이용하여 간단한 폰내부 로컬 웹서버를 띄웁니다. 이 웹서버는 웹영역의 XMLHttpRequest(XHR) 요청에 응답합니다.

2. phonegap.js의 PhoneGap.JSCallback 함수에서 로컬 웹서버와 ajax 통신하기 위한 연결을 맺습니다.

xmlhttp.open("GET", "http://127.0.0.1:xxx/xxx", true);
xmlhttp.send(); 


이 커넥션은 오랫동안 살아있는(long-lived) 연결이며, CallbackServer는  연결을 계속 유지하기 위해 10초간격으로 비어있는 응답을 계속해서 송신합니다(callback ping).

3. (동기식 플러그인과 동일하므로 설명생략)

4. PluginManager.exec()에서는 아래와 같이 비동기 플러그인의 경우 별도의 쓰레드를 만들어서 플러그인을 실행하고 그 결과를 CallbackServer에 적재합니다.

// 우선 Plugin이 동기식인지 아닌지 판단 : Plugin.isSynch()
// 비동기식으로 정의된 경우 새로운 thread에서 플러그인을 실행
// (아래 코드는 원래 코드를 단순화한 것입니다)

new Thread(new Runnable() {
    public void run() {
       PluginResult cr = plugin.execute(xxx);
       ....
       ctx.sendJavascript(xxx);
    }
});
thread.start();
return ""; // 플러그인 실행이 끝날때까지 기다리지 않고 즉시 리턴

5. CallbackServer는 새로운 플러그인 실행결과(javascript문자열)가 공급되면 그것을 기존에 연결된 XHR에 응답으로 씁니다. 

// (아래 코드는 원래 코드를 단순화한 것입니다)
response = "HTTP/1.1 200 OK\r\n\r\n";
String js = this.getJavascript();
response += URLEncoder.encode(js, "UTF-8");
output.writeBytes(response); 


6. 앞서 콜백서버와 연결된 xmlhttp 자바스크립트객체는 응답으로 수신한 문자열을 통해 콜백 함수를 호출합니다(PhoneGap.JSCallback 함수 참조).

// (아래 코드는 원래 코드를 단순화한 것입니다)
if (xmlhttp.status === 200) {
    var t = eval(xmlhttp.responseText);
}



비동기 플러그인 실행메커니즘(Polling 적용시)


1. phonegap.js에서는 PhoneGap.JSCallbackPolling 함수를 주기적으로 실행합니다(기본 50ms 마다).

var msg = prompt("","gap_poll:");


2 ~ 3. (XHR 적용 비동기식 플러그인과 동일하므로 설명생략)

4. 폴링에 의해 호출된 DroidGap$GapClient.onJsPrompt() 메소드에서는 CallbackServer에 적재된 javascript문자열을 prompt의 confirmation 응답으로 설정합니다.

5. prompt 함수의 리턴값(var msg 변수)이 존재하면 그것을 evaluation하여 콜백 함수를 호출합니다.

XHR을 적용할 건가 Polling을 적용할 건가
폰갭은 비동기식 플러그인 실행결과를 수신하기 위해 default로 XMLHttpRequest를 이용합니다. 그러나 외부 웹서버로부터 로딩된 html에서는 결과를 수신하지  못하는 문제가 있습니다. 이는 cross domain 보안제약 때문이며, 안드로이드 WebView가 강제로 Ajax 응답연결을 끊어버려 javascript 콜백함수가 실행되지 않게 됩니다(file:// 주소에 의해 로딩된 html에는 이런 제약이 없습니다). 이 경우 XHR 방식 대신 Polling 방식을 적용해야 합니다.
즉, file:// 주소에 의해 로딩된 웹페이지가 아닌 경우 Cross-Domain Security 제한이 존재하므로 phonegap.js에서 PhoneGap.UsePolling 값을 true로 수정해주어야합니다.

PhoneGap.UsePolling = true;

 
Polling 방식 적용시 주의점
그러나 폴링방식 적용시 폰갭은 비동기식 플러그인의 실행결과를 수신하기 위해 매우 짧은 주기로 App 영역과 통신하므로, 폰갭 기능이 필요하지 않은 웹페이지에서는 phonegap.js를 포함하지 않는 것이 좋습니다. 이 문제는 향후 어떻게든 개선될 것으로 보입니다.

ps. 위 내용은 폰갭 0.9.5 버전을 분석한 것입니다.
 
Posted by 에코지오
,
android.text.InputFilter를 이용해서 사용자의 텍스트입력을 다양한 방식으로 필터링할 수 있습니다. 
입력문자를 모두 대문자로 바꾸거나(InputFilter.AllCaps 이용), 문자열의 길이를 제한(InputFilter.LengthFilter 이용)할 수 있죠. 그밖에 다양한 필터를 만들 수 있을 겁니다. (정규식을 적용한 필터 예제 : http://flysky.thoth.kr/blog/4208673)

그런데 안드로이드가 기본으로 제공하는 InputFilter.LengthFilter는, 글자수(캐릭터 수)로 문자열의 길이를 계산하기 때문에 바이트 수로 길이를 제한하고 싶은 경우에는 사용할 수 없습니다. (한글처럼 문자열에 non-ascii 글자가 포함되면 글자 수 < 바이트 수).

아래 ByteLengthFilter는 바이트 길이로 입력을 제한해주는 필터입니다.

import android.text.InputFilter;
import android.text.Spanned;

/**
 * EditText 등의 필드에 텍스트 입력/수정시 
 * 입력문자열의 바이트 길이를 체크하여 입력을 제한하는 필터.
 *
 */
public class ByteLengthFilter implements InputFilter {

    private String mCharset; //인코딩 문자셋

    protected int mMaxByte; // 입력가능한 최대 바이트 길이

    public ByteLengthFilter(int maxbyte, String charset) {
        this.mMaxByte = maxbyte;
        this.mCharset = charset;
    }

    /**
     * 이 메소드는 입력/삭제 및 붙여넣기/잘라내기할 때마다 실행된다.
     *
     * - source : 새로 입력/붙여넣기 되는 문자열(삭제/잘라내기 시에는 "")
     * - dest : 변경 전 원래 문자열
     */
    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
            int dend) {

        // 변경 후 예상되는 문자열
        String expected = new String();
        expected += dest.subSequence(0, dstart);
        expected += source.subSequence(start, end);
        expected += dest.subSequence(dend, dest.length());

        int keep = calculateMaxLength(expected) - (dest.length() - (dend - dstart));

        if (keep <= 0) {
            return ""; // source 입력 불가(원래 문자열 변경 없음)
        } else if (keep >= end - start) {
            return null; // keep original. source 그대로 허용
        } else {
            return source.subSequence(start, start + keep); // source중 일부만 입력 허용
        }
    }

    /**
     * 입력가능한 최대 문자 길이(최대 바이트 길이와 다름!).
     */
    protected int calculateMaxLength(String expected) {
        return mMaxByte - (getByteLength(expected) - expected.length());
    }    
    
    /**
     * 문자열의 바이트 길이.
     * 인코딩 문자셋에 따라 바이트 길이 달라짐.
     * @param str
     * @return
     */
    private int getByteLength(String str) {
        try {
            return str.getBytes(mCharset).length;
        } catch (UnsupportedEncodingException e) {
            //e.printStackTrace();
        }
        return 0;
    }
}    

EditText에는 이렇게 적용하면 됩니다.

int maxByte = ...;
EditText editText = ...;

InputFilter[] filters = new InputFilter[] {new ByteLengthFilter(maxByte, "KSC5601")};
editText.setFilters(filters);

Posted by 에코지오
,

* SQLite 데이터베이스에서 ERD 추출하기
 - ERD 추출 지원툴 => SQLite Maestro : http://www.sqlmaestro.com/products/sqlite/maestro/

* LogCat 한글 메시지 보는 방법
 - 현재 이클립스에서는 불가하며, DOS 콘솔에서만 가능
 
* DOS 콘솔에서 sqlite 한글 데이터 보는 방법
 - Logcat 한글메시지 확인방법과 동일한 원리

* 네트워크 패킷 모니터링 방법
 - WebKit 브라우저의 경우 프록시 설정하는 방법도 가능
 - 이클립스에서 편하게 모니터링하는 방법 찾는 중...(TCP/IP Monitor 플러그인 + adb forward 조합으로 가능할지?)

* 현재 실행중인 화면이 어떤 액티비티 클래스인지 알아내는 방법
 - Hierachy Viewer 이용
 - Activity Stack 조사 : ActivityManager 이용
 - Activity Stack 내용을 뿌려주는 adb 명령어가 있으면 좋을 듯...

* 프로세스에서 현재 살아있는 View/Activity 객체 개수 알아내기
 - dumpsys meminfo 명령어 이용

* Android.mk 구조 및 사용법 

* 코드를 통한 Activity 콘트롤
 - Activity 생명주기 제어 및 강제 이벤트 발생 가능


* 아직 찾지 못한 팁
- 안드로이드 class loader 구조 : 프레임워크 클래스, 애플리케이션 클래스. 우선 순위, 변경 방법
- Dalvik GC log 파일 만드는 방법
- 반복 테스트(동일 작업을 반복)를 쉽게 하는 방법

Posted by 에코지오
,
메소드 프로파일링을 통해 애플리케이션이 실행되는 동안 어떤 쓰레드의 어떤 메소드가 얼마나 오래 수행되었는지 알 수 있습니다. 
(안드로이드에서는 profiling을 tracing이라고도 부르더군요)

Trace 파일 생성

1. 코딩을 통한 생성
Debug 클래스의 startMethodTracing("trace-base-name") 메소드와 stopMethodTracing() 메소드를 통해 프로파일링을 시작/종료할 수 있습니다. 보통 activity의 onCreate()에 Debug.startMethodTracing("trace-base-name"); 를 넣고, onDestroy()에 Debug.stopMethodTracing();를 넣어 액티비티가 생성되고 소멸되는 동안 프로파일링을 수행하게 합니다.

프로파일링이 끝나면 /sdcard/<trace-base-name>.trace 파일이 생성됩니다. (반드시 sdcard가 있어야하며 애플리케이션이WRITE_EXTERNAL_STORAGE 퍼미션을 가져야합니다)

2. DDMS에서 생성
DDMS에서 프로세스 선택후 "Start Method Profiling" 메뉴를 선택하면 프로파일링이 시작됩니다. 다시 메뉴를 선택하면 프로파일링이 종료됩니다.

프로파일링이 끝나면 /sdcard/<패키지명>.trace 파일이 생성되면서 자동으로 TraceView가 실행됩니다.

3. shell에서 생성
adb shell로 진입해서 am profile 명령어로 특정 프로세스에 대해 프로파일링할 수 있습니다.

$ adb shell
# am profile <PPRCESS ID> start <TRACE FILE> (프로파일링 시작)
# am profile <PPRCESS ID> stop (프로파이링 종료)

예를 들면,
# am profile 16434 start /sdcard/myprofile.trace

(이제 애플리케이션에서 문제가 되는 기능을 사용해본다)

# am profile 16434 stop

4. 에뮬레이터 시작옵션
에뮬레이터를 사용한다면 에뮬레이터 시작시 emulator.exe -trace <name> 옵션을 준 후, 에뮬에서 F9 누르면 트레이싱이 시작되고 다시 F9 누르면 트레이싱이 종료됩니다. 이 경우 패키지 관계없이 전체가 프로파일링 됩니다(native tracing).
트레이싱이 끝나면 C:\Documents and Settings\Administrator\.android\avd\AVD이름.avd\traces\<name> 폴더에 qtrace.* 형태의 여러 파일들이 생성됩니다. 그러나 이들 파일은 테스트해 본 바로는 현재 TraceView로 분석 불가합니다.


Trace 파일 분석

1. TraceView
trace 파일은 SDK에 포함된 traceview 툴을 이용하여 그래픽컬하게 분석할 수 있습니다.

$traceview <trace파일의 절대경로> (단, .trace 확장자 제외)

예를들어 trace파일이 C:/myapp.trace 이면 $ traceview C:/myapp

traceview 분석화면은 timeline 패널과 profile 패널로 나뉩니다.

2. Timeline 패널 
timeline 패널은 각 쓰레드별로 메소드가 언제 시작/종료됐는지 색상으로 표시합니다. 주로 main 쓰레드에서 대부분 일이 처리되고 있음을 알 수 있습니다.(마우스를 드래그하여 구간을 선택하면 확대됩니다)



3. Profile 패널 
profile 패널은 메소드 내부 상세 수행시간을 표시합니다.



profile 패널의 각 컬럼의 대략적인 의미는 다음과 같습니다.
  • Name : 클래스의 메소드명, 맨 앞의 숫자는 메소드 호출 순서(call reference)
  • Inclusive : 해당 메소드 및 그것이 호출하는 메소드(자식메소드)를 포함한 수행시간.
  • Exclusive : 해당 메소드 자체 순수 수행시간(메소드는 여러번 수행될 수 있으며 이들을 모두 합친것임)
  • Calls : 전체 tracing 기록 중에서 몇번 호출되었는지.
  • Recur Calls : recursive call이 몇 번인지(?)
  • Time/Call : 해당 메소드 1번 호출 당 수행시간.
각 컬럼을 클릭하면 해당 컬럼 기준으로 정렬하여 볼 수 있습니다. 

또한 각 메소드를 선택하면 가지가 펼쳐지면서 Parents와 Children이 나타납니다.
  • Parents : 해당 메소드를 호출한 놈. caller
  • Children : 해당 메소드가 호출한 놈. callee
Children에서 self는 다른 메소드를 호출하지 않는 순수 자체 코드가 점유한 시간을 의미합니다.



분석 요령
profile 패널을 통해서 어떤 메소드가 애플리케이션 수행시간을 다 까먹고 있는지, 어떤 메소드가 많이 호출되었는지 등의 중요한 정보를 구할 수 있습니다. 

먼저 "Incl% 컬럼"을 기준으로 수행시간 점유비율이 뚝 떨어지는 곳들을 찾습니다. 아래 그림에서는 1번과 2번 메소드의 차이가 대략 30% 포인트 차이가 납니다. 1번 메소드를 펼쳐 Children을 봅니다. Children 중에서 2번과 10번이 대부분의 시간을 차지하는 것을 알 수 있습니다.(Chilren에서 Incl% 값은 현재 펼쳐진 메소드에 국한된 상대 비율입니다). 2번(또는 10번)을 선택하여 그 2번 메소드는 또 어떤 Child가 대부분의 수행시간을 차지하고 있는지 들어갑니다. 이런 식으로 퍼센티지가 높은 Child 위주로 계속해서 파고들다보면 근본적으로 어떤 메소드에서 쓸데없이 많은 시간을 까먹고 있는지 찾아낼 수 있습니다.


나머지 Excl% 컬럼, Calls 컬럼, Time/Call 컬럼을 기준으로 정렬하여 특별히 Exclusive 비율이 높은 메소드. Call 횟수가 너무 많은 메소드, 한번 호출시 수행시간이 오래 걸리는 메소드 위주로 분석하면 됩니다.

* 내용추가 (2010-11-15)
timeline 패널을 통해서는 쓰레드별로 작업을 분담해서 처리하는 모습을 한눈에 파악할 수 있습니다. 어느 쓰레드가 대부분의 작업시간을 점유하는지를 금방 알 수 있고, 한 쓰레드에서 다른 쓰레드로 작업이 전이되는 모습을 볼 수도 있으며, 반복적인 패턴 등 우리에게 영감을 주는 패턴을 발견할 수도 있습니다.
아래의 예제는 "AsyncTask : 어떤 작업수행 -> AsyncQueryWorker : 목록조회 쿼리 실행 -> main : 화면다시그리기" 작업이 반복되는 패턴을 보여줍니다. 실제로 AsyncTask에서 핵심적인 작업을 처리하는 시간보다, 그 결과를 UI에 곧바로 반영하기 위해 쿼리를 다시 날리고 main 쓰레드에서 화면 갱신하는데 대부분의 시간을 까먹고 있는 것을 알 수 있습니다. 이 경우 AsyncTask에서의 작업이 모두 완료될 때까지 쿼리실행을 막는 것으로 성능을 개선할 수 있었습니다.




* 안드로이드 프로파일링에 대한 좀더 심층적인 자료는 아래 링크를 참조하세요.

Posted by 에코지오
,