아래는 안드로이드 개발시 AspectJ 라이브러리를 사용하는 경우 기본적인 ProGuard 설정에 추가해주어야 할 내용입니다.

# aspect 클래스 및 aspect가 적용되는 클래스에서 AspectJ 라이브러리를 참조할 수 없다는 경고 제거
# (can't find referenced class)
-dontwarn org.aspectj.**

# AspectJ 클래스 변경금지
-keep class org.aspectj.**

# Aspect 클래스 보존
-keep @org.aspectj.lang.annotation.Aspect class * { *; }
-keepclasseswithmembers class * {
  public static *** aspectOf();
}

# 패키지 변경 금지(주석해제시 런타임에 java.lang.NoSuchMethodError 에러 발생)
#-repackageclasses ''
#-allowaccessmodification

# around 어드바이스가 적용되는 target 클래스에서 around 어드바이스 메소드를
# 참조할 수 없다는 경고 제거(can't find referenced method) : aspect 클래스를 지정
-dontwarn my.app.aop.**
(또는 -dontwarn my.app.**.*Aspect 식으로 설정)


Posted by 에코지오
,

저같은 경우 안드로이드 앱 개발시 JSON 데이터 처리를 위해 주로 Codehaus의 jackson json 라이브러리 (jackson-core-asl-x.x.jar, jackson-mapper-asl-x.x.jar)를 사용합니다. 

이렇게 앱에서 Jackson JSON 라이브러리를 사용하는 경우 ProGuard 설정을 어떻게 해줘야하는지 제대로 정리된 자료가 별로 없더군요.

그동안은 릴리스 빌드시에 잘못된 proguard 설정때문에 발생하는 에러나, 앱 실행시에 잘못된 소스난독화로 인한 런타임 에러를 만날때마다

임시방편으로 설정의 의미도 모른채 설정을 조금씩 고쳐가면서 소뒷걸음질하다 운좋게 에러를 해결하곤 했습니다. 에러가 없게 설정했다고 해도 왜 그런 설정이 필요한지 자세한 이유를 알지는 못했죠.

미루고 미루다 이제야 ProGuard 홈페이지 문서 Gson에서 사용하는 proguard 설정을 참고하여 아래와 같이 정리해보았습니다.

실제로 제가 개인적으로 개발중인 앱에 적용해서 빌드하고 테스트해보니 이상이 없었습니다.

# 어노테이션 보존

-keepattributes *Annotation*,EnclosingMethod


# 제너릭 타입 정보 보존

-keepattributes Signature


# Jackson이 참조하는 다른 라이브러리(joda-time 등) 없다는 경고 제거

-dontwarn org.codehaus.jackson.**


# java.lang.NoSuchFieldError: PUBLIC_ONLY 에러 제거

-keepnames class org.codehaus.jackson.** { *; }


# Jackson에 의해 JSON데이터와 매핑(바인딩)되는 모델(POJO) 클래스 보존

-keep public class my.app.model.** { *; }


# 모델 클래스외에 getter/setter 보존이 필요한 경우

#-keep public class my.app.** {

#  public void set*(***);

#  public *** get();

#}



Posted by 에코지오
,
* 제스처 관련 용어
Gesture Guide Iphone 4
http://devicegadget.com/iphone/gesture-guide-iphone-4-2/378/

Touch Gesture Reference Guide
http://www.lukew.com/ff/entry.asp?1071

[스마트폰] 제스처의 정의와 종류
http://ashura4.blog.me/130110732530

* 애니메인션 효과 관련 용어
모바일 폰 UI 애니메인션 효과 정리
http://monodream77.blog.me/130086852732

150가지 베스트 jQuery Effect
http://www.webdesignshock.com/best-jquery-effects/

* 위젯/패턴 관련 용어
http://www.androidpatterns.com/uap_category/dealing-with-data
안드로이드 UI 패턴에 대한 자세한 동작 묘사, 적용 및 장단점 설명, 사례 이미지 제공

http://www.androiduipatterns.com/p/android-ui-pattern-collection.html
안드로이드 UI 패턴을 적용해야 하는 상황과 안드로이드에서 실제 구현 가이드 제시

http://www.mobiledesignpatterngallery.com/mobile-patterns.php
http://mobile-patterns.com/
http://www.mobilepatterns.com/
모바일 UI 패턴별 사례 이미지 제공(패턴에 대한 설명은 없음)

Posted by 에코지오
,
아래글에서는 WebView와 웹서버간의 HTTP 통신내용을 모니터링하는 방법을 설명했습니다. 이번에는 Spring-Android 라이브러리를 이용하여 앱과 웹서버간에 REST로 통신하는 경우에 통신 내용을 엿보는 방법입니다.

import org.apache.http.HttpHost;
import org.apache.http.conn.params.ConnRoutePNames;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
...

public class MyRestActivity extends Activity {

  public xxx someMethod() {
  
    RestTemplate restTemplate = new RestTemplate();
    ... ...
    
    if (isDebugMode(this.getApplicationContext())) { 
      HttpComponentsClientHttpRequestFactory factory =
                   (HttpComponentsClientHttpRequestFactory) restTemplate.getRequestFactory();
      HttpHost proxy = new HttpHost("본인로컬PC의 IP주소", 프록시포트, "http");
      factory.getHttpClient().getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
    }

    ... ...
    ResponseEntity<Xxx> result = 
restTemplate.exchange(url, HttpMethod.POST, requestEntity, Xxx.class);
    ... ...
 
  }
}  


아래글에서 지적했듯이 개발자 PC의 IP주소에 localhost나 127.0.0.1 루프백 주소를 쓰면 안됩니다.

이클립스 TCP/IP 모니터 뷰 설정하는 방법은 아래글과 동일합니다.


실제 앱 개발시에는 위와 같이 프록시를 설정한 RestTemplate 인스턴스를 공통모듈단에서 한개만 만들어놓고 재사용하면 됩니다.
 
Posted by 에코지오
,
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 에코지오
,
jQuery에서 ajax() 함수의 기본 Content-Type은 "application/x-www-form-urlencoded"이므로 전송할 데이터는 query string 형식으로 작성해야 합니다. 쿼리스트링은 "key1=value&key2=value2&…"와 같은 포맷을 갖습니다.

1. form 요소 값들을 query string으로 만들기

$('#폼ID').serialize() 


2. javascript array 객체를 query string으로 만들기

jQuery.param(array객체)


만약 요청 Content-Type을 "application/json" 으로 변경한 경우에는 쿼리스트링이 아니라 json 형식의 스트링으로 데이터를 전송해야 합니다.
form엘리먼트 값들을 json 객체로 만들어주는 방법은 다음 사이트에 다양한 방법이 나와있습니다.


실제 ajax() 함수에서는 json 객체를 스트링 타입으로 바꿔서 대입합니다.
 

JSON.stringify(json객체)


그러나 쿼리스트링 형식이 가능한 상황에서 굳이 json 형식으로 데이터를 보내는 것은 별로 권장하지 않는 듯합니다.
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 에코지오
,