Android/XMPP2010. 10. 28. 21:11


http://blog.jayway.com/2008/11/21/give-back-my-xmpp-in-android/

SASL 관련 XMPPConnection클래스의 버그를 수정한 패치가 올려져 있다.

아래와 같은 에러 발생시 이 패치가 유용할 것이다.

10-28 19:40:09.640: ERROR/AndroidRuntime(2342): java.lang.VerifyError: org.jivesoftware.smack.sasl.SASLMechanism
10-28 19:40:09.640: ERROR/AndroidRuntime(2342):     at java.lang.Class.getDeclaredConstructors(Native Method)
10-28 19:40:09.640: ERROR/AndroidRuntime(2342):     at java.lang.Class.getConstructor(Class.java:477)
10-28 19:40:09.640: ERROR/AndroidRuntime(2342):     at org.jivesoftware.smack.SASLAuthentication.authenticate(SASLAuthentication.java:303)

10-28 19:40:09.640: ERROR/AndroidRuntime(2342):     at
org.jivesoftware.smack.XMPPConnection.login(XMPPConnection.java:395)
10-28 19:40:09.640: ERROR/AndroidRuntime(2342):     at
org.jivesoftware.smack.XMPPConnection.login(XMPPConnection.java:349)
10-28 19:40:09.640: ERROR/AndroidRuntime(2342):     at com.yamaia.mobilebridge.delivery.PushService$LoginThread.run(PushService.java:404)
10-28 19:40:09.640: ERROR/AndroidRuntime(2342):     at java.lang.Thread.run(Thread.java:1102)



Posted by 삼스
Android/XMPP2010. 10. 28. 19:47


http://davanum.wordpress.com/2007/12/31/android-just-use-smack-api-for-xmpp/


Android – Just Use Smack API For XMPP

Filed under: Uncategorized — Davanum Srinivas @ 9:36 am

OUTDATED SAMPLE – Updated code is here:


http://davanum.wordpress.com/2008/12/29/updated-xmpp-client-for-android/

Using Smack XMPP API From Android

Once you get tired of the limitations of android’s built-in IMProvider and the corresponding API – IXmppSession and IXmppService, try the sample below. Inside the source/binary zip (bottom of this article) you will find a smack.jar that works with android. To build the jar yourself, You can download the Smack 3.0.4 sources from here and apply the patch here.

Here Is A Screen Shot Of The XMPP Settings Dialog.

1

Notes



  • For GTalk, use “gtalk.google.com” as host with port 5222. The service name is “gmail.com”

  • Don’t add “@gmail.com” in the user name, just the id will do

Here’s The Code For The Settings Dialog

01 package org.apache.android.xmpp;
02  
03 import android.app.Dialog;
04 import android.util.Log;
05 import android.view.View;
06 import android.widget.Button;
07 import android.widget.EditText;
08 import org.jivesoftware.smack.ConnectionConfiguration;
09 import org.jivesoftware.smack.XMPPConnection;
10 import org.jivesoftware.smack.XMPPException;
11 import org.jivesoftware.smack.packet.Presence;
12  
13 /**
14  * Gather the xmpp settings and create an XMPPConnection
15  */
16 public class SettingsDialog extends Dialog implements android.view.View.OnClickListener {
17     private XMPPClient xmppClient;
18  
19     public SettingsDialog(XMPPClient xmppClient) {
20         super(xmppClient);
21         this.xmppClient = xmppClient;
22     }
23  
24     protected void onStart() {
25         super.onStart();
26         setContentView(R.layout.settings);
27         getWindow().setFlags(44);
28         setTitle("XMPP Settings");
29         Button ok = (Button) findViewById(R.id.ok);
30         ok.setOnClickListener(this);
31     }
32  
33     public void onClick(View v) {
34         String host = getText(R.id.host);
35         String port = getText(R.id.port);
36         String service = getText(R.id.service);
37         String username = getText(R.id.userid);
38         String password = getText(R.id.password);
39  
40         // Create a connection
41         ConnectionConfiguration connConfig =
42                 new ConnectionConfiguration(host, Integer.parseInt(port), service);
43         XMPPConnection connection = new XMPPConnection(connConfig);
44  
45         try {
46             connection.connect();
47             Log.i("XMPPClient""[SettingsDialog] Connected to " + connection.getHost());
48         catch (XMPPException ex) {
49             Log.e("XMPPClient""[SettingsDialog] Failed to connect to " + connection.getHost());
50             xmppClient.setConnection(null);
51         }
52         try {
53             connection.login(username, password);
54             Log.i("XMPPClient""Logged in as " + connection.getUser());
55  
56             // Set the status to available
57             Presence presence = new Presence(Presence.Type.available);
58             connection.sendPacket(presence);
59             xmppClient.setConnection(connection);
60         catch (XMPPException ex) {
61             Log.e("XMPPClient""[SettingsDialog] Failed to log in as " + username);
62             xmppClient.setConnection(null);
63         }
64         dismiss();
65     }
66  
67     private String getText(int id) {
68         EditText widget = (EditText) this.findViewById(id);
69         return widget.getText().toString();
70     }
71 }

Here Is A Screen Shot Of The Main Window.

1

Notes



  • In the Recipient field, make sure you add the “@gmail.com”, not just the user id

Here’s The Code For The Main Activity

001 package org.apache.android.xmpp;
002  
003 import android.app.Activity;
004 import android.os.Bundle;
005 import android.os.Handler;
006 import android.util.Log;
007 import android.view.View;
008 import android.widget.ArrayAdapter;
009 import android.widget.Button;
010 import android.widget.EditText;
011 import android.widget.ListView;
012 import org.jivesoftware.smack.PacketListener;
013 import org.jivesoftware.smack.XMPPConnection;
014 import org.jivesoftware.smack.filter.MessageTypeFilter;
015 import org.jivesoftware.smack.filter.PacketFilter;
016 import org.jivesoftware.smack.packet.Message;
017 import org.jivesoftware.smack.packet.Packet;
018 import org.jivesoftware.smack.util.StringUtils;
019  
020 import java.util.ArrayList;
021  
022 public class XMPPClient extends Activity {
023  
024     private ArrayList<String> messages = new ArrayList();
025     private Handler mHandler = new Handler();
026     private SettingsDialog mDialog;
027     private EditText mRecipient;
028     private EditText mSendText;
029     private ListView mList;
030     private XMPPConnection connection;
031  
032     /**
033      * Called with the activity is first created.
034      */
035     @Override
036     public void onCreate(Bundle icicle) {
037         super.onCreate(icicle);
038         setContentView(R.layout.main);
039  
040         mRecipient = (EditText) this.findViewById(R.id.recipient);
041         mSendText = (EditText) this.findViewById(R.id.sendText);
042         mList = (ListView) this.findViewById(R.id.listMessages);
043         setListAdapter();
044  
045         // Dialog for getting the xmpp settings
046         mDialog = new SettingsDialog(this);
047  
048         // Set a listener to show the settings dialog
049         Button setup = (Button) this.findViewById(R.id.setup);
050         setup.setOnClickListener(new View.OnClickListener() {
051             public void onClick(View view) {
052                 mHandler.post(new Runnable() {
053                     public void run() {
054                         mDialog.show();
055                     }
056                 });
057             }
058         });
059  
060         // Set a listener to send a chat text message
061         Button send = (Button) this.findViewById(R.id.send);
062         send.setOnClickListener(new View.OnClickListener() {
063             public void onClick(View view) {
064                 String to = mRecipient.getText().toString();
065                 String text = mSendText.getText().toString();
066  
067                 Log.i("XMPPClient""Sending text [" + text + "] to [" + to + "]");
068                 Message msg = new Message(to, Message.Type.chat);
069                 msg.setBody(text);
070                 connection.sendPacket(msg);
071                 messages.add(connection.getUser() + ":");
072                 messages.add(text);
073                 setListAdapter();
074             }
075         });
076     }
077  
078     /**
079      * Called by Settings dialog when a connection is establised with the XMPP server
080      *
081      * @param connection
082      */
083     public void setConnection
084             (XMPPConnection
085                     connection) {
086         this.connection = connection;
087         if (connection != null) {
088             // Add a packet listener to get messages sent to us
089             PacketFilter filter = new MessageTypeFilter(Message.Type.chat);
090             connection.addPacketListener(new PacketListener() {
091                 public void processPacket(Packet packet) {
092                     Message message = (Message) packet;
093                     if (message.getBody() != null) {
094                         String fromName = StringUtils.parseBareAddress(message.getFrom());
095                         Log.i("XMPPClient""Got text [" + message.getBody() + "] from [" + fromName + "]");
096                         messages.add(fromName + ":");
097                         messages.add(message.getBody());
098                         // Add the incoming message to the list view
099                         mHandler.post(new Runnable() {
100                             public void run() {
101                                 setListAdapter();
102                             }
103                         });
104                     }
105                 }
106             }, filter);
107         }
108     }
109  
110     private void setListAdapter
111             () {
112         ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
113                 R.layout.multi_line_list_item,
114                 messages);
115         mList.setAdapter(adapter);
116     }
117 }

Download Source And APK From Here – XMPPClient.Zip


Posted by 삼스
Android/App개발2010. 10. 28. 14:18

코드 난독화툴을 안드로이드 프로젝트에 적용하는 포스트를 이미 올린바 있습니다.
이번에는 프로젝트에서 일부 클래스들만 묶어서 배포하고자 하는 경우 jar파일로 만드는 경우가 있을 것인데 이 경우 jar file하나만 proguard를 적용하는 방법에 대해 설명하겠습니다.

프로가드 gui툴을 이용하면 편하게 진행이 가능합니다.
프로가드 사용시 중요한 것이 옵션을 어떻게 지정할 것인가에 대한 것인데.. 그 중에도 -keep옵션이 아주 중요하다 할 것입니다.
안드로이드 소스코드 난독화를 위해서 레퍼런스로 사용할 만한 옵션을 아래 나열해 보았습니다.

# 각종 옵션들 : 설명은 생략~~ proguard site의 설명을 참조하시길...

-dontskipnonpubliclibraryclassmembers

-optimizationpasses 5

-dontusemixedcaseclassnames

-dontpreverify

-verbose


# keep option들.. 

# keep옵션이란 난독화시 난독화를 하지 않아야 하는 코드들을 미리 지정하는 것으로 

# 안드로이드 jar파일이나 프로젝트의 경우 아래의 keep옵션들이 필요할 것입니다.

-keep public class * extends android.app.Activity

-keep public class * extends android.app.Application

-keep public class * extends android.app.Service

-keep public class * extends android.content.BroadcastReceiver

-keep public class * extends android.content.ContentProvider

-keep public class com.android.vending.licensing.ILicensingService

-keepclasseswithmembers,allowshrinking class * {

    public <init>(android.content.Context,android.util.AttributeSet);

}

-keepclasseswithmembers,allowshrinking class * {

    public <init>(android.content.Context,android.util.AttributeSet,int);

}

# Also keep - Enumerations. Keep the special static methods that are required in

# enumeration classes.

-keepclassmembers enum  * {

    public static **[] values();

    public static ** valueOf(java.lang.String);

}

# Keep names - Native method names. Keep all native class/method names.

-keepclasseswithmembers,allowshrinking class * {

    native <methods>;

}


위 옵션들과 함께 입력파일과 출력파일을 지정하고 참조하고 있는 라이브러리까지 지정하는 스크립트 코드는 아래와 같습니다.


# 난독화를 진행할 입력 파일명

-injars myjar.jar

# 난독화를 거친 출력 파일명

-outjars out_myjar.jar


# 입력파일이 참조하는 라이브러리들...

# 안드로이드용 jar라면 android.jar를 반드시 포함해야 할것이다.

# 그 외에 혹시 추가로 참조하는 라이브러리가 있다면 추가해 주어야 한다.

-libraryjars /Users/yosamlee/Desktop/android-sdk-mac_x86/platforms/android-7/android.jar

-libraryjars /Users/yosamlee/_TOOL/workspace/MyJar/lib/referencelib.jar



위의 스크립트를 모두 모아서 확장자 *.pro로 저장한다.


proguard설치 폴더로 가서 bin폴더의 proguardgui를 실행한다. 아래와 같은 커맨드라인 명령을 콘솔에서 입력하면 됩니다.


java -jar ../lib/proguardgui.jar




위 화면에서 "Load configuration ..."을 선택 후 앞서 저장해 둔 스크립트파일을 오픈합니다


스크립트 파일이 성공적으로 열렸다면 Process tab에서 "Process!"버튼을 선택하면 난독화 과정을 거칩니다.


...

  Number of branch peephole optimizations:     0

  Number of simplified instructions:           0

  Number of removed instructions:              0

  Number of removed local variables:           0

  Number of removed exception blocks:          0

  Number of optimized local variable frames:   0

Shrinking...

Removing unused program classes and class elements...

  Original number of program classes: 26

  Final number of program classes:    26

Obfuscating...

Writing output...

Preparing output jar [/Users/yosamlee/_TOOL/workspace/MyJar/bin/out_myjar.jar]

  Copying resources from program jar [/Users/yosamlee/_TOOL/workspace/MyJar/bin/myjar.jar]

Processing completed successfully


정상적으로 과정이 진행되었다면 위와 같은 성공하였다는 메세지를 보게 될것입니다.


이제 out_myjar.jar파일을 리버스엔지니어링 해보길 바랍니다.

원하는 만큼 난독화가 진행되었는지 말이죠~!

만족할 만한 결과가 나왔기를 바랍니다.


ps) 이미 느끼신 분들도 계시겠지만 위 과정은 proguardgui버전에서 그대로 수행이 가능합니다. 저는 왜 수동으로 스크립트를 만들었냐면요.... 옵션들이 하도 많아서 헤깔려서요. GUI버전이 편하신 분들은 그걸 사용하셔도 됩니다.







Posted by 삼스
Android/App개발2010. 10. 14. 18:20

안드로이드는 사정에 따라 서비스를 죽이기도 하며 나중에 다시 살리기도 한다.
만일 항상 떠있는 서비스를 구현하고자 하는 경우에는 이런 일이 발생하는것에 대해 아주 당황할것이다.
이럴경우 서비스를 죽지 않도록 하고자 할것인데.
이런 경우 알람서비스를 이용하여 서비스가 죽으면 다시 살리는 방법이 있다.
많은 경우 이런 방식을 이용하는것으로 보인다.

PersistentService가 죽지 않아야 할 서비스이다. 아래 예제에서 보면 onCreate시 기존 알람이 있으면 제거하고 onDestroy시 알람을 기동한다. 
알람은 일정시간이 지나면 PendingIntent를 날리는 알람이며 이 인텐트를 받을 수 있는 BroadcastReceiver가 있게 된다. 여기서는 RestartService receiver가 해당 인텐트를 받아서 이 때 종료된 PersistentService를 재기동 시키는 역할을 한다.

PersistentService.java
class PersistentService extends Service {

  onCreate(..) {
    unregisterRestartAlram(); //이미 등록된 알람이 있으면 제거
  }

  onDestroy(..) {
    registerRestartAlram(); // 서비스가 죽을때 알람을 등록
  }
 
  // support persistent of Service 
  void registerRestartAlarm() {
    Log.d(TAG"registerRestartAlarm");
    Intent intent = new Intent(PersistentService.this, RestartService.class);
    intent.setAction(RestartService. ACTION_RESTART_PERSISTENTSERVICE);
    PendingIntent sender = PendingIntent.getBroadcast(PersistentService.this, 0, intent, 0);
    long firstTime = SystemClock.elapsedRealtime();
    firstTime += 10*1000; // 10초 후에 알람이벤트 발생
    AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE);
    am.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, firstTime, 10*1000, sender);
  }

  void unregisterRestartAlarm() {
    Log.d(TAG"unregisterRestartAlarm");
    Intent intent = new Intent(PersistentService.this, RestartService.class);
    intent.setAction(RestartService.ACTION_RESTART_PERSISTENTSERVICE);
    PendingIntent sender = PendingIntent.getBroadcast(PersistentService.this, 0, intent, 0);
    AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE);
    am.cancel(sender);
  }
}

RestartService.java
public class RestartService extends BroadcastReceiver {
  public static final String ACTION_RESTART_PERSISTENTSERVICE = "ACTION.Restart. PersistentService";

@Override
  public void onReceive(Context context, Intent intent) {
    Log.d("RestartService""RestartService called!@!@@@@@#$@$@#$@#$@#");
    if(intent.getAction().equals(ACTION_RESTART_PERSISTENTSERVICE)) {
      Intent i = new Intent(ctx, PushService.class);
      context.startService(i);
  }
}

manifest.xml
<application android:icon="@drawable/icon" android:label="@string/app_name">
<receiver android:name="RestartService" android:process=":remote"/>
Posted by 삼스
Android/App개발2010. 2. 7. 15:50

Controls for the Android OS
Keyboard OS function
Escape Back button
Home Home button
F2, PageUp Menu (Soft-Left) button
Shift-F2, PageDown Star (Soft-Right) button
F3 Call/Dial button
F4 Hangup/EndCall button
F5 Search button
F7 Power button
Ctrl-F3, Ctrl-KEYPAD_5 Camera button
Ctrl-F5, KEYPAD_PLUS Volume up button
Ctrl-F6, KEYPAD_MINUS Volume down button
KEYPAD_5 DPad center
KEYPAD_4 DPad left
KEYPAD_6 DPad right
KEYPAD_8 DPad up
KEYPAD_2 DPad down
Controls for the Android Emulator
Keyboard Emulator function
F8 toggle cell network on/off
F9 toggle code profiling (when -trace option set)
Alt-ENTER toggle fullscreen mode
Ctrl-T toggle trackball mode
Ctrl-F11, KEYPAD_7 switch to previous layout
Ctrl-F12, KEYPAD_9 switch to next layout
KEYPAD_MULTIPLY increase onion alpha
KEYPAD_DIVIDE decrease onion alpha
Posted by 삼스
Android/정리2010. 1. 21. 12:01
Activity의 메인 윈도우에 어떻게 on-screen soft keyboard가 동작할것인지에 대한 방법을 기술한다.
이 설정은 두가지에 대해 영향을 준다.
 1. soft keyboard의 상태 - activity가 사용자의 포커스를 받을때 보여질지 숨겨질지
 2. activity의 메인윈도우를 조정할지 - soft keyboard의 크기를 줄이거나 soft keyboard에의 해 덮어씌워질때 윈도우 사이즈를 조정하거나 한다.

아래 나열한 값들 중에 하나이상을 나열할 수 있으며 "state.."값하나와 "adjust.."값 하나씩 사용할 수 있다.
각 그룹에 여러개의 값을 지정하는것은 정의되어 있지 않으며 각 값은 |로 분리하여 나열할 수 있다.

stateUnspecified : hidden 또는 visible을 지정하지 않음. 시스템이 알아서 선택하거나 theme의 값을 참조한다.
stateUnchanged : activity가 foreground로 올때 마지막 상태(visible, hidden)를 유지하고 있는다.
stateHidden : activity 실행중에 hidden이다. that is, when the user affirmatively navigates forward to the activity, rather than backs into it because of leaving another activity.
stateAlwaysHidden : activity가 focus를 가지고 있으면 무조건 softkeyboard는 hidden으로 함.
stateVisible : 일반적인 모드로 동작
stateAlwaysVisible : activity 실행중에 visible이다.  that is, when the user affirmatively navigates forward to the activity, rather than backs into it because of leaving another activity.

adjustUnspecified : resize나 pan하지 않는다. 메인윈도우의 디폴트셋팅이다. 시스템은 자동으로 하나의 모드를 선택하는데 자신의 콘텐츠를 스크롤할 수 있는 layout view를 윈도우가 가지고 있는지에 따라 결정된다. 만일 그런 view라면 윈도우는 resize되어 더 작은 영역에서 모든 윈도우의 콘텐츠가 스크롤되어 표시될수 있어야 한다.
adjustResize : Activity의 메인윈도우가 softkeyboard에 따라 항상 resize된다. 
adjustPan : Activity의 메인윈도우가 resize되지 않고 pan된다. 따라서 keyboard에 의해 현재 포커스가 가려지지 않아서 사용자는 항상 타이핑결과를 볼수가 있다. resizing옵션보다 권장되지 않는데 사용자는 윈도우상에 보이지 않는 부분을 보기 위해 softkeyboard를 닫아야 하기 때문이다. 


화면의 하단에 EditText가 하나 있다고 가정후 두 옵션의 차이점은 아래와 같다.
 1. adjustResize : 메인윈도우 사이즈가 줄어든다, 하지만 EditText는 보이지 않는다. 스크롤이 지원되면 스크롤하여 볼수는 있다.
 2. adjustPan : 메인윈도우 사이즈가 줄어들지 전체적으로 않고 EditText가 보이도록 위로 올라간다. EditText는 보이나 다른 UI가 보이지 않는다.







Posted by 삼스
Android/정리2010. 1. 13. 16:24
Android concept이다.
Keyguard가 표시중이거나 Hidden일경우 PowerManager.enableUserActivity(true)에 의해 mUserActivityAllowed변수가 set되면서 PowerManager.userActivity(..)에서 setPowerState가 정상적으로 호출될 수 있다.
PowerManager.userActivity는 WindowManagerService.InputDispatcherThread에서 이벤트큐의 이벤트를 처리할때마다 호출된다. 따라서 PowerManager.enableUserActivity()로 screen timeout을 ON/OFF할 수 있다.

Keyguard에서 이를 이용해 LockScreen이 표시중에는 사용자 입력이 있더라도 무조건 5초후에 꺼지도록 되어있다.
KeyguardViewMediator.java
private void adjustUserActivityLocked() {
        // disable user activity if we are shown and not hidden
        if (DEBUG) Log.d(TAG, "adjustUserActivityLocked mShowing: " + mShowing + " mHidden: " + mHidden);
        boolean enabled = !mShowing || mHidden;
        mRealPowerManager.enableUserActivity(enabled);
        if (!enabled && mScreenOn) {
            // reinstate our short screen timeout policy
            pokeWakelock();
        }
    }


Posted by 삼스
Android/정리2010. 1. 6. 23:01
http://learnandroid.blogspot.com/2008/01/run-android-application-from-command.html

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="com.iftitah.android.contact">
  <application android:icon="@drawable/icon">
   <activity class=".Contact" android:label="@string/app_name">
    <intent-filter>
    <action android:value="android.intent.action.MAIN" />
    <category android:value="android.intent.category.LAUNCHER" />
   </intent-filter>
  </activity>
 </application>
.
.
</manifest>

위와 같이 activity가 정의되어 있을 때... 아래와 같이 실행하면 됨

am start -a android.intent.action.MAIN -n
com.iftitah.android.contact/com.iftitah.android.contact.Contact
Posted by 삼스
Android/정리2009. 12. 9. 17:55
API Level : 6

Android 2.0.1은 android handset에 적용가능한 minor platform release이다.
minor API change, bug-fixes, framework behavioral의 변경이 되었다. framework에 대한 변경은  Framework API 를 참조하라.

Built-in app로 Sync Tester app가 추가되어 thirdparty sync adapter의 테스트를 돕는다.

API 변경 요약
  • quickContactBadgeStyle* 속성이 QuickContactBadge에 추가됨. -> style을 지정가능
  • ACTION_CONFIGURATION_CHANGED를 받기 위해 manifest에 기술할 필요 없으며 run-time에 registerReceiver()로 하면 됨
Framework관련 behavior변경 요약

블루투스
  ACTION_REQUEST_ENABLE과 ACTION_REQUEST_DISCOVERABLE의 리턴값 변경
  • ACTION_REQUEST_ENABLE이 성공하면 RESULT_OK를 사용자가 취소하면 RESULT_CANCELED를 리턴함
  • ACTION_REQUEST_DISCOVERABLE은 사용자가 취소하거나 BT가 enable상태가 아니면 RESULT_CANCELED를 리턴함.

Contacts
  ACTION_INSERT intent는 해당 contact가 더이상 존재하지 않으면(not persist) RESULT_CANCELED를 리턴한다.

Bug fixes
  일부 framework API의 버그를 수정함.

Resource  
  API level에 따른 app resource를 잘못 로딩하는 경우 수정됨. ex) drawable-v4는 API level4이상에서만 로드함.

Contacts
  ACTION_INSERT intent가 Contacts API로 생성한 요구에 대해 적절한 종류의 URI를 리턴함.

Others
  getCallingPackage()가 process명보다 package명으로 리턴함.

API differences report

For a detailed view of API changes in Android 2.0.1 (API Level 6), as compared to API Level 5, see the API Differences Report. Note that this difference report compares only to the most recent API Level, and there are few changes, so to see changes introduces in Android 2.0 (API Level 5), see the API Differences between 4 and 5.









Posted by 삼스
Android/App개발2009. 11. 6. 16:21

[Intro]

 

Android에서 사용하는 이미지는 Bitmap이라는 클래스에서 다~ 알아서 해줍니다.
그리고 이런 Bitmap Object를 쉽게 만들 수 있도록 도와주는 
BitmapFactory 클래스 라는 것도 있습니다.

 

BitmapFactory는 여러가지 소스로 부터 Bitmap Object를 만들어 주는 일을 하는데,
전부 static이며 decodeXXX 라는 이름을 가진 메소드들로 이루어져 있습니다.

XXX에는 어떤 것으로 부터 decode를 하여 
Bitmap Object를 만들어 낼지에 대한 말들이 들어 가겠죠.

 


[Decoding Methods]

 

BitmapFactory.decodeByteArray() 메소드는 Camera.PictureCallback 으로 부터 받은
Jpeg 사진 데이터를 가지고 Bitmap으로 만들어 줄 때 많이 사용 합니다.
Camera.PictureCallback에서 들어오는 데이터가 byte[] 형식이기 때문에
저 메소드를 사용 해야 하는 것이죠.

 

BitmapFactory.decodeFile() 메소드는 파일을 그대로 읽어 옵니다.
내부적으로는 파일 경로를 가지고 FileInputStream을 만들어서 decodeStream을 합니다.
그냥 파일 경로만 쓰면 다 해주는게 편리 한 것이죠.

 

BitmapFactory.decodeResource() 메소드는 Resource로 부터 Bitmap을 만들어 내며
BitmapFactory.decodeStream() 메소드는 InputStream으로 부터 Bitmap을 만들어 냅니다.
뭐 그냥 이름만 봐도 알 수 있는 것들이지요.

 


[OutOfMemoryError??]

 

보통 이미지 파일을 읽어서 Resizing을 해야 할 때가 있는데, 
그럴때는 BitmapFactory로 읽어서 Bitmap.createScaledBitmap() 메소드를 사용하여 줄이면

간단하게 처리 할 수 있습니다.

 

그런데 BitmapFactory를 사용할 때 주의해야 할 점이 있습니다.
아래의 예를 한번 보시죠.

Bitmap src = BitmapFactory.decodeFile("/sdcard/image.jpg");
Bitmap resized = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, true);

이미지 파일로부터 Bitmap을 만든 다음에

다시 dstWidth, dstHeight 만큼 줄여서 resized 라는 Bitmap을 만들어 냈습니다.
보통이라면 저렇게 하는게 맞습니다.

 

읽어서, 줄인다.

 

그런데 만약 이미지 파일의 크기가 아주 크다면 어떻게 될까요?
지금 Dev Phone에서 카메라로 촬영하면
기본적으로 2048 x 1536 크기의 Jpeg 이미지가 촬영된 데이터로 넘어옵니다.
이것을 decode 하려면 3MB 정도의 메모리가 필요 할 텐데,

과연 어떤 모바일 디바이스에서 얼마나 처리 할 수 있을까요?

 

실제로 촬영된 Jpeg 이미지를 여러번 decoding 하다보면

아래와 같은 황당한 메세지를 발견 할 수 있습니다.

java.lang.OutOfMemoryError: bitmap size exceeds VM budget

네... OutOfMemory 입니다.
더 이상 무슨 말이 필요 하겠습니까...
메모리가 딸려서 처리를 제대로 못합니다.

 

이것이 실제로 decoding 후 메모리 해제가 제대로 되지 않아서 그런 것인지, 
하더라도 어디서 Leak이 발생 하는지에 대한 정확한 원인은 알 수 없습니다.
이것은 엔지니어들이 해결해야 할 문제 겠죠...

 

하지만 메모리 에러를 피할 수 있는 방법이 있습니다.

 


[BitmapFactory.Options.inSampleSize]

 

BitmapFactory.decodeXXX 시리즈는 똑같은 메소드가 두 개씩 오버로딩 되어 있습니다.
같은 이름이지만 Signature가 다른 메소드의 차이점은
BitmapFactory.Options를 파라메터로 받느냐 안받느냐의 차이죠.

BitmapFactory.Options를 사용하게 되면 decode 할 때 여러가지 옵션을 줄 수 있습니다.


여러가지 많지만 저희가 지금 사용할 것은 inSampleSize 옵션 입니다.

 

inSampleSize 옵션은,
애초에 decode를 할 때 얼마만큼 줄여서 decoding을 할 지 정하는 옵션 입니다.

 

inSampleSize 옵션은 1보다 작은 값일때는 무조건 1로 세팅이 되며,
1보다 큰 값, N일때는 1/N 만큼 이미지를 줄여서 decoding 하게 됩니다.
즉 inSampleSize가 4라면 1/4 만큼 이미지를 줄여서 decoding 해서 Bitmap으로 만들게 되는 것이죠.

 

2의 지수만큼 비례할 때 가장 빠르다고 합니다.
2, 4, 8, 16... 정도 되겠죠?

 

그래서 만약 내가 줄이고자 하는 이미지가 1/4보다는 작고 1/8보다는 클 때,
inSampleSize 옵션에 4를 주어서 decoding 한 다음에,

Bitmap.createScaledBitmap() 메소드를 사용하여 한번 더 줄이면 됩니다.

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
Bitmap src = BitmapFactory.decodeFile("/sdcard/image.jpg", options);
Bitmap resized = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, true);

당연한 이야기 이겠지만,
내가 원하고자 하는 사이즈가 딱 1/4 크기라면

Bitmap.createScaledBitmap() 메소드를 쓸 필요가 없지요.

 

inSampleSize 옵션을 잘 활용하면 메모리 부족 현상을 대략적으로 해소 할 수 있습니다.
참고로 제가 저 옵션을 사용한 뒤로는 메모리 에러를 본적이 한~번도 없답니다.

 


[Appendix]

 

inSampleSize 옵션을 사용하면

SkScaledBitmapSampler Object (Library Level) 를 생성 하게 되는데,
Object를 만들때 정해진 SampleSize 만큼 축소하여 width와 height를 정한 뒤에 만들게 됩니다.
그러니까 애초에 축소된 사이즈로 이미지를 decoding 하는 것이죠.

 


[Outro]

 

Android의 기본 어플리케이션 소스를 분석 하다보면
상당히 테크니컬한 기법들을 많이 얻을 수 있습니다.
어떻게 이런 방법으로 만들었나 싶을 정도로 매우 정교하고 복잡하게 만들어져 있지요.
참 대단한 것 같습니다.

 

아 그리고 왜 dstWidth와 dstHeight 변수 선언이 없냐고 따지시는 분들 설마 없겠죠?


Posted by 삼스