제가 요즘 폰갭(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 버전을 분석한 것입니다.
 
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by 에코지오

댓글을 달아 주세요

  1. BlogIcon tkHWANG 2011.06.27 10:36 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 내용 고맙습니다. 잘 봤습니다. :)

  2. 바캉스 2011.07.26 12:40 신고  댓글주소  수정/삭제  댓글쓰기

    과장님 잘 보고 갑니다. 요샌 하이브리드앱 개발 중이시네요~ 좋은 정보 감사합니다.

  3. BlogIcon 김민지 2011.10.14 11:20 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요
    폰갭으로 개발 테스트 중인데 글을 보다 궁금한 점이 생겨서 이렇게 글을 남깁니다.
    웹인 html파일에서 자바스크립트로 window.plugins.PhoneGapTestCamera.scan 이렇게 만들어서 사용중입니다.
    그럼 플러그인 페이지에서는
    public PluginResult execute(String action, JSONArray args, String callbackId) {
    ....
    }
    위의 함수로 이동하여 실행이 되는데
    여기서 카메라 함수를 불러서 찍으면 바로 저장 되면서 웹 화면에서 찍었던 이미지를 띄우고 싶은데요

    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    File path = new File(Environment.getExternalStorageDirectory(),
    "mypicture.jpg");

    Log.d("Picture Path : ", Uri.fromFile(path).toString());

    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(path));
    // 인텐트 결과받 받아고기
    // REQUEST_TAKEPICTURE 결과값 받응ㄹ 코드
    this.ctx.startActivityForResult(intent, REQUEST_TAKEPICTURE2);
    이렇게 해서 카메라를 불렀고 사진 찍는것 까지는 되는데
    원래 startActivityForResult 함수 실행이 완료된다면 onActivityResult 함수가 실행되는것으로 아는데
    onActivityResult 함수가 실행되지 않네요
    카메라 실행이 완료 된 후 사진 경로를 받아와서 처리를 하고 싶은데
    어떻게 처리를 해야 할지 조언좀 부탁드립니다.

  4. 유우현 2011.12.08 10:36 신고  댓글주소  수정/삭제  댓글쓰기

    jsp소스에서 <body onload="init();"> 로 폰갭플러그인을 셋업해서 잘 사용하고 있습니다.
    그런데 jsp 소스내에서 폰갭 플로그인 수행 결과 값을 받을수는 없는지요?
    아무리 찾아봐도 javascript에서는 가능한데...jsp소스에서 판별하는 방법이 없네요.
    방법이 있으면 알려주세요.

    구분해야 하는 이유는 스마트폰 브라우져를 통해서 접속해서 첨부화일 다운로드 하는것과
    폰갭을 이용한 다운로드 방법이 달라서요...

    jsp내에서 구분을 했으면 합니다.

    방법이 있으면 알려주시면, 고맙겠습니다.


    • BlogIcon 에코지오 2011.12.20 13:16 신고  댓글주소  수정/삭제

      폰갭 플러그인은 javascript를 통해서 수행되는 것입니다. 서버단의 jsp와는 관계가 없지요. 원하시는 로직을 javascript로 작성하셔야 가능합니다.

  5. 소중한너 2012.08.15 23:39 신고  댓글주소  수정/삭제  댓글쓰기

    좋은글 감사합니다. 제 블로그에 링크 추가했습니다.

메이븐에서는 기본적으로 자바 소스 폴더를 한 개만 지정할 수 있습니다.

 <build>
    <sourceDirectory>src/main/java</sourceDirectory>
 ....
 </build>

하지만 때로는 자바 소스가 여러 폴더에 흩어져 있는 경우도 있습니다.
메이븐에서는 그럴 경우 프로젝트를 분리하라고 권장합니다.
그러나 프로젝트를 분리할 형편(?)이 안되거나 분리하기 싫다면 어떻게 할까요?

Build Helper Maven 플러그인을 이용하면 됩니다.
build-helper 플러그인의 add-source 모조는 POM에 소스 디렉토리를 추가해줍니다. 아래처럼 사용할 있습니다.

  <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>1.2</version>
    <executions>
     <execution>
      <id>add-source-dir</id>
      <phase>generate-sources</phase>
      <goals>
       <goal>add-source</goal>
      </goals>
      <configuration>
       <sources>
         <source>another/src/main/java</source>
         <source>others/src</source>
       </sources>
      </configuration>
     </execution>
    </executions>
  </plugin>

 

신고
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by 에코지오

댓글을 달아 주세요

1. (maven을 사용한다면) pom.xml에 보고자 하는 report 플러그인을 설정
   <!-- FindBugs 리포트 생성 플러그인 -->
   <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>findbugs-maven-plugin</artifactId>
    <version>1.2</version>
    <configuration>
     <threshold>Low</threshold><!-- High, Default, Low, Ignore -->
     <xmlOutput>true</xmlOutput>
    </configuration>
   </plugin>
   <!-- PMD 리포트 생성 플러그인 -->
   <plugin>
    <artifactId>maven-pmd-plugin</artifactId>
    <configuration>
     <rulesets>
      <ruleset>/rulesets/basic.xml</ruleset>
      <ruleset>/rulesets/unusedcode.xml</ruleset>
     </rulesets>
     <sourceEncoding>${maven.compiler.encoding}</sourceEncoding>
     <targetJdk>${maven.compiler.source}</targetJdk>
    </configuration>
   </plugin>

2. 허든슨에서 관련된 허드슨 플러그인을 설치. (Manage Hudson > Manage Plugins)



3. 허드슨에서 빌드시 maven의 site goal을 실행하도록 설정하고 리포트 publish 설정(빌드 Configure 화면). 필요시 pmd.xml, findbugs.xml 같은 리포트 xml 위치 설정.


4. 이제 빌드작업 화면에서 차트를 볼 수 있다~

신고
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by 에코지오

댓글을 달아 주세요



티스토리 툴바