2013년 5월 30일 목요일

[Java] Json는 Gson으로...(서버부터 안드로이드까지)

Android App을 개발하면서 정말 많이 쓰는 기술 가운데 하나가 Json 처리 입니다. 예전에는 XML을 많이 사용 하였는데 요세는 XML보다는 가벼운 Json을 많이 사용 하는거 같습니다. 저는 주로 Android에서만 작업을 하기 때문에 Json 처리를 별도의 라이브러리를 사용하지 않고 JsonObeject 와 JsonArray를 사용하여 개발해 왔었습니다. 그러던 중에 간단한 테스트를 하려고 제 데스크탑에 서버 설정을 하면서 Gson을 처음 사용해 봤는데 정말 편하고 좋았습니다. 다만 서버개발자와 서로 맞춰야 될게 몇가지가 있습니다.

먼저 Gson을 사용 하려면 라이브러리를 받아서 등록해야 됩니다. 서버든 Android든 라이브러리 등록 방법은 따로 설명드리지 않겠습니다. 서버에서 Gson을 사용하여 Json형태로 만드는 방법과  Android에서 Gson을 사용하여 Json 을 처리하는 과정을 순차적으로 포스팅 하겠습니다.

Gson 라이브러리 다운로드
Gson User Guide

서버 Gson
public class Intro {
 private int nIndex;
 private String nFirstName;
 private String nSecondName;
 private String nThirdName;
 
 public Intro(int nIndex, String nFirstName, String nSecondName,
   String nThirdName) {
  this.nIndex = nIndex;
  this.nFirstName = nFirstName;
  this.nSecondName = nSecondName;
  this.nThirdName = nThirdName;
 }
}
public class Intros {

 public ArrayList names = new ArrayList();

}
  String json = null;
  String[] nName = {"ㄱ","ㄴ","ㄷ","ㄹ","ㅁ","ㅂ","ㅅ","ㅇ","ㅈ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ"};
  Gson gson = new GsonBuilder().setPrettyPrinting().create();
//  Gson gson = new Gson();
  Intros model = new Intros();
  
  for (int i = 0; i < nName.length; i++) {
   model.names.add(new Intro(i, nName[i], nName[i], nName[i]));
  }
  json = gson.toJson(model);
  System.out.println(json);

대략적인 소스 설명을 하면 Intro bean 객체에 데이터를 넣어서 names라는 ArrayList<intro>에 넣습니다. Gson에서는 데이터를 만들때는 toJson(object) 메서드를 사용합니다. 이러면 Json포맷이 만들어 졌습니다. 정말 간단하게 Json을 만들 수 있는걸 볼 수 있습니다. Gson의 생성 방법은 2가지가 있습니다. new GsonBuilder().setPrettyPrinting().create();와 같이 생성하면 Json양식이 갈끔하게 보이게 되고 new Gson();으로 생성하게 되면 보통의 Json 처럼 직렬로 보이게 됩니다.

GsonBuilder().setPrettyPrinting().create() 로 생성
{
  "names": [
    {
      "nIndex": 0,
      "nFirstName": "ㄱ",
      "nSecondName": "ㄱ",
      "nThirdName": "ㄱ"
    },
    {
      "nIndex": 1,
      "nFirstName": "ㄴ",
      "nSecondName": "ㄴ",
      "nThirdName": "ㄴ"
    },
    {
      "nIndex": 2,
      "nFirstName": "ㄷ",
      "nSecondName": "ㄷ",
      "nThirdName": "ㄷ"
    },
    ......... 이하 생략 .........
  ]
}

new Gson()로 생성
{"names":[{"nIndex":0,"nFirstName":"ㄱ","nSecondName":"ㄱ","nThirdName":"ㄱ"},{"nIndex":1,"nFirstName":"ㄴ","nSecondName":"ㄴ","nThirdName":"ㄴ"},{"nIndex":2,"nFirstName":"ㄷ","nSecondName":"ㄷ","nThirdName":"ㄷ"},    

......... 이하 생략 .........

{"nIndex":13,"nFirstName":"ㅎ","nSecondName":"ㅎ","nThirdName":"ㅎ"}]}


Gson 라이브러리를 이용하여 Json 포맷을 만드는 코드를 살펴 보았으니 이제 Android에서 Gson 라이브러리를 이용하여 Json 포맷의 데이터를 어떻게 사용하는지 포스팅하겠습니다.
Gson을 사용할때 한가지 주의 사항은 위에서도 언급했듯이 서버와 클라이언트가 서로 맞추어야 되는 부분이 있습니다. 바로 Namming입니다. ArrayList<Name>를 만들때는 서버에서 사용하고 있는 변수명인 names로 맞춰주고 Bean 파일의 변수명도 서버와 동일하게 맞춰 줍니다. ArrayList<Name>의 변수명이 서버와 다른 경우 데이터가 ArrayList<Name>에 들어가지 않습니다.

Android Gson
public class Name { 
 private int nIndex;
 private String nFirstName;
 private String nSecondName;
 private String nThirdName;
 
 public int getnIndex() {
  return nIndex;
 }
 public void setnIndex(int nIndex) {
  this.nIndex = nIndex;
 }
 public String getnFirstName() {
  return nFirstName;
 }
 public void setnFirstName(String nFirstName) {
  this.nFirstName = nFirstName;
 }
 public String getnSecondName() {
  return nSecondName;
 }
 public void setnSecondName(String nSecondName) {
  this.nSecondName = nSecondName;
 }
 public String getnThirdName() {
  return nThirdName;
 }
 public void setnThirdName(String nThirdName) {
  this.nThirdName = nThirdName;
 }
}
public class Names {
 private ArrayList names = new ArrayList();
 public ArrayList getnList() {
  return names;
 }
 public void setnList(ArrayList names) {
  this.names = names;
 }
}
 String introJson = HttpConnecter.getJson(url주소);
 FLLog.e(TAG, "introJson : " + introJson);
   
 this.mNames = new Gson().fromJson(introJson, Names.class);
 FLLog.e(TAG, "mNames : " + mNames.getnList().size()); 
Bean파일을 보면 변수명은 똑같은걸 확인 할 수 있습니다. 다만 서버에서는 데이터를 넣기만 하면 되기때문에 생성자에서 세팅되게 하였으며 Android에서는 가져다가 써야 하므로 Getter and Setter를 만들어준 차이 밖에 없습니다. HttpConnecter.getJson(url주소)를 이용하여 Json 데이터를 가지고 옵니다. 소스에서는 보이지 않지만 당연히 Thread를 이용하여 데이터를 가지고 오고 있습니다. 가지고온 데이터는 introJson 이라는 String 변수에 저장을 했습니다. Gson으로 생성된 Json 포맷을 받을때는 formJson(data, format)으로 받으면 됩니다. JsonObject를 사용 할 때 처럼 하나하나 받아주지 않고 이런식으로 한번에 통으로 list에 넣을 수가 있습니다.

2013년 5월 20일 월요일

[Android] ActionBar Tabs

Android UI를 구성할때 많이 사용하는 것이 Tab기능입니다. 이런 Tab기능을 ActionBar에서 사용 할 수 있습니다. 대표적으로 Google Play 나 YouTube을 보시면 확인 할 수 있습니다.
ActionBar에서 Tab 기능을 사용하면 ActionBar의 여유공간에 따라 Tabs의 위치가 달라질 수 있습니다.


ActionBar에 Tab 추가

  1. ActionBar에 Tab을 추가하기 위해서는 getActionBar()을 이용하여 ActionBar를 가지고 온다.
  2. bar.addTab(bar.newTab().setText("Tab 1").setTabListener(this));를 사용하여 Tab을 추가한다
  3. bar.setNavigationMode()를 설정해 준다.(ActionBar.NAVIGATION_MODE_TABS로 설정해 주시면 Tabs 영역이 나타나게 된다.)
  4. setTabListener를 사용하여 TabListener를 등록 해준다.








 @Override
 protected void onCreate(Bundle savedInstanceState) {
  // TODO Auto-generated method stub
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_navigator);
  
  findViewById(R.id.btnNavi01).setOnClickListener(this);
  final ActionBar bar = getActionBar();

  bar.addTab(bar.newTab().setText("Tab 1").setTabListener(this));
  bar.addTab(bar.newTab().setText("Tab 2").setTabListener(this));
  bar.addTab(bar.newTab().setText("Tab 3").setTabListener(this));
 }

 @Override
 public void onClick(View v) {
  // TODO Auto-generated method stub
  final ActionBar bar = getActionBar();
  int flags = 0;
  switch (v.getId()) {
  case R.id.btnNavi01:
   bar.setNavigationMode(bar.getNavigationMode() == ActionBar.NAVIGATION_MODE_STANDARD ? ActionBar.NAVIGATION_MODE_TABS : ActionBar.NAVIGATION_MODE_STANDARD);
   return;
  }
  int change = bar.getDisplayOptions() ^ flags;
  bar.setDisplayOptions(change, flags); 
 }

TabListener를 implements 받아서 처리 하면 아래와 같은 메서드를 Override하게 됩니다.
 @Override
 public void onTabReselected(Tab tab, FragmentTransaction ft) {
  // TODO Auto-generated method stub
  Log.d(TAG, "onTabReselected " + tab.getText());
 }

 @Override
 public void onTabSelected(Tab tab, FragmentTransaction ft) {
  // TODO Auto-generated method stub
  Log.d(TAG, "onTabSelected " + tab.getText());
 }

 @Override
 public void onTabUnselected(Tab tab, FragmentTransaction ft) {
  // TODO Auto-generated method stub
  Log.d(TAG, "onTabUnselected " + tab.getText());
 }
ActionBar에서 Tab을 추가하는 것을 간단하게 포스팅 하였습니다. 실제 App을 개발할때는 Fragment와 결합하여 사용하게 되며 ViewPager와 연계하여 Swipe기능을 사용하여 개발 할 수도 있습니다.


참조

2013년 5월 16일 목요일

[Flume] Flume 설치부터 예제까지...

Flume OG는 구버전이니 Flume NG를 사용하여 설치부터 사용 예제까지 포스팅 하겠습니다. 여러 머신에서 사용하려면 각각의 머신에 동일하게 Flume를 설치해야 되며 Hadoop이 설치되어있다면 Flume의 라이브러리를 Hadoop 라이브러리에 복사해야 됩니다.

설치
  • http://flume.apache.org/download.html 에서 Apache Flume binary(tar.gz)를 다운로드 받습니다.
  • tar -xvfz apache-flume-1.3.1-bin.tar.gz 
  • ln -s apache-flume-1.3.1-bin.tar.gz flume

환경변수 설정 
export FLUME_HOME=/apps/apache-flume-1.3.1-bin.tar.gz
PATH=$PATH:$HOME/bin:${FLUME_HOME}/bin
export PATH

환경설정
  • cd flume
  • cp flume-conf.properties.template flume.conf
  • vi flume.conf

# The configuration file needs to define the sources, 
# the channels and the sinks.
# Sources, channels and sinks are defined per agent, 
# in this case called 'agent'

agent.sources = seqGenSrc
agent.channels = memoryChannel
agent.sinks = loggerSink

# For each one of the sources, the type is defined
agent.sources.seqGenSrc.type = seq //source 타입

# The channel can be defined as follows.
agent.sources.seqGenSrc.channels = memoryChannel  // source를 channel과 연결

# Each sink's type must be defined
agent.sinks.loggerSink.type = logger  // sink 타입

#Specify the channel the sink should use
agent.sinks.loggerSink.channel = memoryChannel  // sink를 channel과 연결

# Each channel's type is defined.
agent.channels.memoryChannel.type = memory //channel 타입

# Other config values specific to each type of channel(sink or source)
# can be defined as well
# In this case, it specifies the capacity of the memory channel
agent.channels.memoryChannel.capacity = 100 // channel의 용량

이 파일에서 Source, Sink, Channel를 어떻게 설정하냐에 따라 여러 Flow을 구성 하실 수가 있습니다.

환경설정 샘플
제가 구축한 설정은 서버1, 서버2, 서버3에다가 각각 Flume을 설치한 후 서버1에서는 서버2와 서버3에서 보내는 데이터를 파일로 저장합니다. 서버2와 서버3은 로그파일을 읽어 서버 1로 보내줍니다.

서버1 flume.conf
agent01.sources = avroGenSrc
agent01.channels = memoryChannel
agent01.sinks = fileSink

# For each one of the sources, the type is defined
agent01.sources.avroGenSrc.type = avro
agent01.sources.avroGenSrc.bind = localhost
agent01.sources.avroGenSrc.port = 3333

# The channel can be defined as follows.
agent01.sources.avroGenSrc.channels = memoryChannel

# Each sink's type must be defined
agent01.sinks.fileSink.type = file_roll
agent01.sinks.fileSink.sink.directory = /home/aaaaa/flume/data
agent01.sinks.fileSink.sink.rollInterval = 30
agent01.sinks.fileSink.sink.batchSize = 100


#Specify the channel the sink should use
agent01.sinks.fileSink.channel = memoryChannel

# Each channel's type is defined.
agent01.channels.memoryChannel.type = memory

# Other config values specific to each type of channel(sink or source)
# can be defined as well
# In this case, it specifies the capacity of the memory channel
agent01.channels.memoryChannel.capacity = 100000
agent01.channels.memoryChannel.transactionCapacity = 10000


서버2, 서버3 flume.conf
서버2와 서버3의 환경설정은 똑같습니다. 단지 agent 명만 다릅니다.
agent02.sources = execGenSrc
agent02.channels = memoryChannel
agent02.sinks = avroSink

# For each one of the sources, the type is defined
agent02.sources.execGenSrc.type = exec
agent02.sources.execGenSrc.command = tail -F /home/aaaaa/hadoop/logs/logsample.log
agent02.sources.execGenSrc.batchSize = 10

# The channel can be defined as follows.
agent02.sources.execGenSrc.channels = memoryChannel

# Each sink's type must be defined
agent02.sinks.avroSink.type = avro
agent02.sinks.avroSink.hostname = 데이타가 보내져야될 호스트 주소
agent02.sinks.avroSink.port = 3333
agent02.sinks.avroSink.batch-size = 10


#Specify the channel the sink should use
agent02.sinks.avroSink.channel = memoryChannel

# Each channel's type is defined.
agent02.channels.memoryChannel.type = memory

# Other config values specific to each type of channel(sink or source)
# can be defined as well
# In this case, it specifies the capacity of the memory channel
agent02.channels.memoryChannel.capacity = 100000
agent02.channels.memoryChannel.transactionCapacity = 10000

실행

  • ./bin/flume-ng agent --conf-file ./conf/flume.conf --name agent01
  • ./bin/flume-ng agent --conf-file ./conf/flume.conf --name agent02
  • ./bin/flume-ng agent --conf-file ./conf/flume.conf --name agent03
데이터가 이동되는 것을 볼 수 있습니다. 한가지 이슈는 exec는 파일을 읽을때 버퍼링을 좀 있다고 합니다. 파일을 수정해보면 파일 내용이 잘 전달되어 오는것을 확인 하실 수 있습니다.

참고

[Flume] 내가 바로 Flume 이다.

최근 IT업계에서 가장 핫 이슈는 빅데이터가 아닐까 생각합니다.
여러 관련 프로그램 중에서 Flume을 설정 할 일이 있어 알아 본 내용을 포스팅 하겠습니다.

About Flume

일단 Flume의 뜻은 용수로 라는 뜻입니다. 여러 머신에 있는 데이타를 한 곳에 저장시켜주는 역할을 하기때문에 Flume 이란 의미와 잘 맞습니다. Flume은 0.9.x 버전과 1.x버전으로 나눠 볼 수가 있습니다. 0.9.x는 Flume OG라고 불리며 1.x는 Flume NG라고 불려집니다. Flume이 1.x로 버전이 바뀌면서 단순 기능 추가가 아닌 아키텍쳐 자체가 바뀌어 버렸기 때문에 설정하는 부분이 완전 틀려졌습니다. Flume 검색해 보면 Flume NG보다 Flume OG의 예제가 더 많이 나오나 저는 Flume NG로 구성하였습니다.(사실 Flume OG로 설정하다가 원하는데로 구현이 잘 안되서 Flume NG로 구현했습니다.) Flume NG가 나온 이유는 설정이 다소 번거로운 Flume OG를 개선하기 위해서 나왔습니다. Flume OG와 Flume NG 둘 다 설정해 본 결과 확실해 Flume NG의 개념이 확장하거나 설정하기가 훨씬 간편해 진걸 느낄 수 있었습니다.



Flume OG 와 Flume NG의 차이점

가장 큰 변화는 아키텍쳐 자체가 변했다는 점입니다. Flume OG의 아키텍쳐를 보면 Agent, Collector, Master로 나눠져 있습니다.(HDFS는 Hadoop 저장 장치를 의미합니다.) Flume은 하나의 머신에서 논리적으로 나눠서 구성할 수도 있고 각각의 머신으로 물리적으로 구성 할 수도 있습니다. 저는 쉽게 설명드리기 위해 물리적 구성으로 설명 드리겠습니다. Flume OG는 각각의 머신에 있는 Agent가 모은 데이터를 Collector라는 애가 있는 머신으로 보냅니다. Collector라는 애는 각각의 머신에 있는 Agent에게서 받은 데이터를 최종 저장 장치에 보내게 됩니다. 이런 데이터 흐름을 관리하는 곳이 Master 입니다. 간단한 예를 들어 보자면 각각의 배(Agent)에서 잡은 생선들은 경매장(Collector)에 모이고 경매된 생선들은 다시 노량진 수산시장(HDFS같은 최종 저장장치) 같은 최종 소비자가 구매 할 수 있는 곳으로 모이게 되져. Flume OG의 개념이 이와 같습니다. Flume OG는 각각의 역할이 정해져 있어서 설정할 때도 각각의 역할에 맞게 명시적으로 설정을 해줘야 되는데 한번 구성하고 땡이면 상관없는데 수평적으로 Agent를 확장 할 수도 있고 Collector를 확장 할 수 있는 구조에서 확장 될때 마다 Master를 다시 설정해야 되는 구조가 불편했습니다. 사실 Collector는 Agent입니다. Master에서 어떻게 설정 하느냐에 따라 Agent가 Collector가 되는 것입니다. 이런 불편함을 개선한게 Flume NG입니다.


Flume NG에서는 Master가 사라졌습니다. Collector 개념도 없어졌습니다. Collector는 위에서 설명했듯이 Master에서 Collector라고 이름만 바꿔준 Agent 입니다. 그래서 Master가 없어졌으니 당연히 Collector도 없어진 것입니다.(이래서 줄을 잘서야 됩니다.) 아키텍쳐를 처음 보시면 Flume OG와 Flume NG의 시점이 달라서 눈에 확 안들어 올 수 있지만 잘 보시면 둘다 데이터의 Flow를 나타내고 있다는 걸 아실 수 있습니다.  Flume NG의 아키텍쳐는  Agent안에서의 데이터 흐름만 나타내고 있는데 Flume NG 에서는 Agent만 알면 되고 Flume OG는 Agent, Collector, Master 를 알아야지만 데이터의 Flow를 설명 할 수 있기 때문입니다. 또 설명 드리지만 Flume NG에서는 Master와 Collector 개념이 빠졌습니다. Flume OG의 아키텍쳐에서 Master와 Collector를 빼보면 Flume NG의 아키텍쳐와 똑같다는 걸 알 수 있습니다.
Flume NG의 아키텍쳐를 보면 Agent안에 Source, Sink, Channel이 있는 것을 확인 할 수 있습니다. Source, Sink는 Flume OG에서도 Agent 안에 있던 아이들 입니다. Source는 데이터가 Agent로 들어오는 곳이고 Sink는 Agent가 데이터를 내보낼때 사용 하는곳 입니다.사람을 Agent라고 하면 Source는 입이고 Sink는...똥......꼬?가 되겠네요. 음식물(데이터)이 입(Source)을 통해 몸(Channel)에 들어오면 똥꼬(Sink)를 통해 똥(데이터)으로 나오고 그 똥들은 정화조(HDFS)에 모입니다. Channel은 일종의 큐라고 생각하면 됩니다. Source로 부터 데이터가 들어오면 Channel에 쌓이고 Sink는 Channel에 있는 데이터를 꺼내와 내보내게 됩니다. 이렇게 함으로써 Flume OG에서는 전체 Flow를 관리하던 Master가 있어야 됐지만 Flume NG는 Agent 하나하나의 설정만 알고 있으면 되게 되었습니다.

Flume NG Flow 구성 종류

Setting multi-agent flow



Consolidation



Multiplexing the flow





Flume NG로 구성 할 수 있는 Flow 구성도 입니다. 가운데 있는 Consolidation Flow가 Flume OG의 아키텍쳐와 비슷 하다는걸 알 수 있습니다. Flume NG는 각각의 Agent만 알고 있으면 된다고 했습니다. 그 의미는 Source의 Type과 Sink의 Type만 결정하여 설정해 놓으면 Agent가 수평적으로 확장이 되어도 다른 Agent는 신경 쓸 필요가 없어집니다. Consolidation Flow를 보면 각각의 Agent가 하나의 Agent로 데이터를 보내고 있는것을 알 수 있습니다. Flume OG의 용어로 말하자면 각각의 Agent에서 Collector로 데이터를 보내는 것입니다. 하지만 Flume NG에서는 Collector가 아닌 Agent에서 Agent로 데이터를 이동하는 것 뿐입니다. Multiplexing Flow를 보면 Channel과 Sink를 여러개 만들어 각각의 저장소에 저장하거나 다른 Agent로 보낼 수도 있음을 확인 할 수 있습니다. 굉장히 심플해 지면서 유연성있게 변경된 걸 확인 할 수 있습니다.

Agent Type
모든 Type들을 살펴본 것은 아니며 몇가지 테스트한 Type에 대해서만 설명 드리도록 하겠습니다.

Source Type

  • Avro Source : Agent 와 Agent를 연결해 주는 Type입니다. Agent끼리 연결 할 경우는 Source와 Sink의 Type를 avro로 해주면 됩니다.
  • Exec Source : 파일을 읽을때 사용 하는 Type으로서 Command 속성을 이용하여 tail 명령어로 데이터를 읽어 올 수 있습니다.
  • NetCat Type : 터미널에서 데이터를 입력 받을 때 사용 합니다.
  • Syslog Source : SyslogTCP와 SyslogUDP로 나뉘며 SyslogTCP는 로그 한줄 한줄이 새로운 Event이이나 SyslogUDP는 전체로그를 한번의 Event로 보냅니다.

이밖에도 Scribe Source, Json Source, Custom Source 등 여러 Type이 있습니다.

Sink Type

  • Avor Sink : Agent 와 Agent를 연결해 주는 Type입니다. Agent끼리 연결 할 경우는 Source와 Sink의 Type를 avro로 해주시면 됩니다.
  • HDFS Sink : Hadoop 저장장치에 저장할때 사용 하는 Type 입니다.
  • File_Roll Sink : 원하는 저장장치에 파일로 저장한다.
  • HBase Sink : HBase에 형태로 저장한다.

이밖에도 Logger Sink, Custom Sink등 여러 Type이 있습니다.

Channel Type

  • Memory Channel : Source에서 받은 Event를 Memory에 가지고 있는 방식으로 빠르고 간편하지만 Event를 잃어버릴 수도 있습니다.
  • JDBC Channel : JDBC 형태로 저장한다.
  • File Channel : File 형태로 저장한다.

이밖에도 Custom Channel도 있습니다.

모든 Type를 다 테스트하기에는 어려운 점이 있어 제가 알아본 Type만 작성 하였습니다. Flume User Guide를 참고하시면 여러 Type에 관해 알 수 있습니다.

참고

[Flume] Flume 설치부터 예제까지...

2013년 4월 25일 목요일

[Android] APK Decompile (APK 추출)

안드로이드 앱을 보다보면 어떻게 만들었는지 궁금 할 때가 있을 겁니다. 이럴때 Apk Decompile을 통해서 전체적인 코드를 확인 하실 수가 있습니다. Apk Decompile이 가능하다고 악용하시지 마시고요 자기발전을 통한 재능기부로 이여졌으면 합니다. 참고로 Apk Decompile은 Mac, Window, Linux 에 따라 JD-GUI만 OS별로 설치하시면 나머지는 똑같습니다.

Apk Decompile을 하긴 위한 전체적인 프로세스
Apk Decompile을 하기 위해서는 우선 Apk 파일이 필요하겠습니다. Apk 파일을 추출 하는 방법은 여러 방법이 있으나 저는 Es File Explorer 앱을 이용하여 Apk 파일을 추출하는 방법을 소개시켜 드리겠습니다.(Astro, ApkManager,ApkTool 등등.. Apk  파일을 추출하는 방법은 많이 있습니다.) Apk 파일 추출 후 Apk 파일를 압축파일로 변환하고 압축을 풀어 dex2jar를 이용하여 classes.dex 파일을 jar로 만들어면 JD-GUI를 통해 보실 수가 있습니다. XML은 바이너리 코드로 보여지므로 AXMLPrinter2.jar를 통한 XML  코드 변환까지 포스팅 하겠습니다.

간단히 순서를 요약해 보면..
  1. APK 파일 추출
  2. APK 파일을 zip으로 변환 후 압축풀기
  3. dex2jar를 이용해 classes.dex를 jar로 변환(dex2jar 다운받기)
  4. JD-GUI 툴을 통해 java 소스보기(JD-GUI 다운받기기)
  5. AXMLPrinter2.jar를 통해 XML 보기 (AMLPrinter2.jar 다운받기)
APK 추출

Google Play에서 Es File Explorer를 다운로드 받습니다. Astro에서도 가능 합니다만 제가 Es File Explorer를 사용하고 있는 관계로 Es File Explorer로 설명 드리겠습니다. 사실 Apk 파일을 추출하는 용도로 사용 할 것이기 때문에 다른 방법으로 Apk 파일을 추출하실 수 있는 분이시나 이미 Apk 파일이 있으신 분들은 굳이 다운로드 받지 않아도 상관없습니다. Es File Explorer의 Backup기능을 이용 하여 Apk 파일을 추출 해야 되나 최근에  Es File Explorer의 업데이트로 인해 UI 변경이 있어으니 Backup 기능을 사용 하실 줄 모르시면 아래 글을 참조하면 됩니다.


Es File Explorer 앱을 실행 시킨 후 메뉴를 클릭하시면 Facebook 메뉴처럼 이동하면서 그림과 같은 화면이 나타납니다. 메뉴 화면에서 Tools을 클릭하시고 App Manager를 클릭하시면 디바이스에 설치되어있는 앱들이 나타나게 됩니다. 그중 Decompile을 하고 싶은 앱을 Long Click을 하시면 앱이 선택되면서 하단 메뉴도 바뀝니다. 그중에 Backup 메뉴를 클릭하시면 Apk 파일이 자동으로 저장됩니다. 저장된 Apk 파일은 backups 폴더에 자동으로 저장되어 집니다. 이제 저장된 폴더를 작업하시고자 하시는 컴퓨터로 가지고 오시면 됩니다.
  1. Es File Explorer 다운
  2. Fast Access 버튼 클릭 (실행하자마자 왼쪽 상단버튼, 그림으로는 녹색화면 화면)
  3. Tools  클릭
  4. App Manager 클릭
  5. Apk 파일을 추출하고 하는 App Long Click
  6. 하단 Backup 버튼 클릭
Apk 파일을 컴퓨터로 가지고 오셨으면 이제 Apk 파일을 zip으로 변경해 주셔야됩니다. 변경방법은 확장자를 .zip으로 바꿔주시면 됩니다. app.apk 을 app.zip으로 바꿔주시면 zip파일로 변환 되며 바로 압축을 풀어 줍니다.

dex2jar사용하기

다운받은 dex2jar의 압축을 풉니다. 이번 단계에서 풀어놓았던 Apk 파일의 압축파일을 푼 폴더를 보면  src 폴더가 보이지가 않습니다. XML 파일은 다 바이너리 코드로 보일 거구요. 이제부터 하나하나 Decompile를 해보도록 하겠습니다.
터미널을 여시고 다운 받은 dex2jar 폴더로 이동하셔서 아래의 명령어를 입력합니다. 참고로 저는 Mac에서 Decompile 를 하고 있어서 경로를 Mac 경로를 입력하였습니다. Window에서는 Window 경로를 입력해 주시면 됩니다.아래 경로는 classes.dex의 경로 이니 OS에 맞게 경로를 입력해 주시면 됩니다. 아래 명령어를 실행시키면 그림 처럼 해당 폴더에 classes_dex2jar.jar파일이 생성된걸 볼 수 있습니다. 이제 JD-GUI를 이용하여 .java파일을 보시기만 하면 됩니다.

sh [dex2jar.sh 경로] [classes.dex파일 경로]

sh dex2jar.sh /Users/Downloads/appFolder/classes.dex


JD-GUI 사용하기

위의 주소로 들어가면 그림과 같은 화면이 나옵니다.

여기서 해당 OS에 맞는 JD-GUI를 다운로드 받으시면 됩니다. 저는 이것도 모르고 Mac에서는 다른 특별한 방법이 있는 줄 알고 한참 구글링 했었습니다. 하지만 별거 없었습니다. 그냥  JD-GUI 툴만 OS별로 다운 로드 받아서 보시면 됩니다. 그러니 Mac이나 Linux를 사용하시는 분들도 JD-GUI만 작업하시는 OS버전으로 다운 받아서 보면 됩니다. 저는 Mac을 사용하니 Mac 버전을 다운 받았습니다.
JD-GUI을 실행 시키고 classes_dex2jar.jar를 open 하시면 앱의 package부터 .java 파일 까지 전부 확인 하 실 수 있습니다. 이제 마지막으로 xml을 복구해 보도록 하겠습니다.


AXMLPrinter2.jar 사용하기

위의 주소에서 AXMLPrinter2.jar를 다운받은 후 터미널에서 AXMLPrinter2.jar가 있는 경로로 들어가 줍니다. 그후 아래와 같이 입력하시면 xml이 보여집니다.

java -jar [AXMLPrinter2.jar] [xml 파일 위치] > [변환 될 파일 위치]

java -jar AXMLPrinter2.jar /Users/aaaaa/Downloads/call_in.xml > /Users/aaaaa/Downloads/asdfg/call_in.xml

변환 될 파일위치로 가셔서 확인해 보시면 바이너리 코드로 있어던 xml 파일이 변환되어 보입니다. 완벽하게 변환 되는것은 아니면 값들은 코드값으로 보여지게 됩니다.

2013년 4월 24일 수요일

[Android] Action Bar Usage (Action Bar 사용방법 예제)

ActionBar 사용방법에 대한 예제가 Open Source에 있어서 포스팅 하려고 합니다. 소스에 특별한 내용은 없으나 우리가 Action Bar를 사용 할 때 생각해야 될 부분 중에 하나인 작은 화면에서의 Action Bar의 item을 효율에 관한 소스라고 생각 되어 포스팅 합니다. Action Bar내용이 아닌 소스 내용은 주석을 참고 해서 보시면 됩니다.  Open Source 클래스 중 ActionBarUsage클래스 내용입니다.

<menu xmlns:android=”http://schemas.android.com/apk/res/android”>
    <item android:id=”@+id/action_search”
          android:icon=”@android:drawable/ic_menu_search”
          android:title=”@string/action_bar_search”
          android:showAsAction=”ifRoom”
          android:actionViewClass=”android.widget.SearchView” />
    <item android:id=”@+id/action_add”
          android:icon=”@android:drawable/ic_menu_add”
          android:title=”@string/action_bar_add” 
          android:showAsAction=”collapseActionView”/>
    <item android:id=”@+id/action_edit”
          android:icon=”@android:drawable/ic_menu_edit”
          android:showAsAction=”always”
          android:title=”@string/action_bar_edit” />
    <item android:id=”@+id/action_share”
          android:icon=”@android:drawable/ic_menu_share”
          android:title=”@string/action_bar_share”
          android:showAsAction=”ifRoom” />
    <item android:id=”@+id/action_sort”
          android:icon=”@android:drawable/ic_menu_sort_by_size”
          android:title=”@string/action_bar_sort”
          android:showAsAction=”ifRoom”>
        <menu>
            <item android:id=”@+id/action_sort_size”
                  android:icon=”@android:drawable/ic_menu_sort_by_size”
                  android:title=”@string/action_bar_sort_size”
                  android:onClick=”onSort” />
            <item android:id=”@+id/action_sort_alpha”
                  android:icon=”@android:drawable/ic_menu_sort_alphabetically”
                  android:title=”@string/action_bar_sort_alpha”
                  android:onClick=”onSort” />
        </menu>
    </item>
</menu>
SearchView를 사용 하기 위해서 android:actionViewClass를 사용 하여 Android에서 제공하고 있는 android.widget.SearchView 경로를 사용한 것을 확인 할 수 있습니다. SearchViw는 ShareActionProvider와 마찬가지로 Android에서 제공하고 있는 클래스를 가져다가 사용만 하면 됩니다. android:showAsAction의 값이 "ifRoom"으로 설정하면 item들이 순차적으로 Action Bar에 보이다가 공간이 부족하면 Menu 버튼을 클릭해야지 보이는걸 확인 할 수 있습니다.
id가 action_sort인 item은 서브 메뉴 아이템을 가지고 있는것을 보실 수 있습니다.

public class ActionBarUsage extends Activity implements OnQueryTextListener {
 private static final String TAG = "ActionBar_Api_Usage";
 
 private TextView mSearchText;
 private int mSortMode = -1;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  // TODO Auto-generated method stub
  super.onCreate(savedInstanceState);
  mSearchText = new TextView(this);
  setContentView(mSearchText);
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // TODO Auto-generated method stub
  MenuInflater inflater = getMenuInflater();
  inflater.inflate(R.menu.actions, menu);
  
//  Action Bar에서 SearchView를 보여주고 싶을때 사용하는 클래스입니다.
  SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView();
  searchView.setOnQueryTextListener(this);
  return true; 
 }

 @Override
 public boolean onOptionsItemSelected(MenuItem item) {
  // TODO Auto-generated method stub
   Toast.makeText(this, "Selected Item: " + item.getTitle(), 
     Toast.LENGTH_SHORT).show();
  return true;
 }
 
// Menu의 sort item에서 android:onClick="onSort" 기능으로 호출한다.
    public void onSort(MenuItem item) {
        mSortMode = item.getItemId();
        // onPrepareOptionMenu를 호출 할 수 있어서 sort icon을 바꿀 수 있다.
        invalidateOptionsMenu();
    }
 
//  invalidateOptionsMenu()메서드를 실행하면 호출된다.
 @Override
 public boolean onPrepareOptionsMenu(Menu menu) {
  // TODO Auto-generated method stub
  if (mSortMode != -1) {
   Drawable icon = menu.findItem(mSortMode).getIcon();
   menu.findItem(R.id.action_sort).setIcon(icon);
  }
  return super.onPrepareOptionsMenu(menu);
 }

// SearchView에 텍스트를 입력하면 TextView에 바로 적용된다.
 @Override
 public boolean onQueryTextChange(String newText) {
  // TODO Auto-generated method stub
  newText = newText.isEmpty() ? "" : "Query so far : " + newText;
  mSearchText.setText(newText);
  return false;
 }

// Submit을 누르면 onQueryTextSubmit 메소드가 호출된다.
 @Override
 public boolean onQueryTextSubmit(String query) {
  // TODO Auto-generated method stub
  Toast.makeText(this, "Searching for: " + query + "...", 
    Toast.LENGTH_SHORT).show();
  return true;
 }
}


참조

[Android] Action Bar ShareActionProvider( 공유 )

ShareActionProvider 

ShareActionProvider는 공유를 위한 Provider입니다. 데이터를 공유할수 있고, 만약 overflow 메뉴에 배치된다면 서브 메뉴에서 공유 activity들을 보여 줍니다.
ShareActionProvider는 ActionProvider를 상속 받고 있는 클래스 이며, Action Bar에서 공유하는 기능을 담당하고, 간단하게 공유를 사용 할 수 있게 해줍니다.

이번 포스팅에서는 Open Source 중 ActionBarShareActionProviderActivity에 대해 살펴 볼려고 합니다.
<menu xmlns:android=”http://schemas.android.com/apk/res/android”>
    <item android:id=”@+id/menu_item_share_action_provider_action_bar”
        android:showAsAction=”always”
        android:title=”@string/btn_api_provider_share”
        android:actionProviderClass=”android.widget.ShareActionProvider” />

    <item android:id=”@+id/menu_item_share_action_provider_overflow”
        android:showAsAction=”never”
        android:title=”@string/btn_api_provider_share”
        android:actionProviderClass=”android.widget.ShareActionProvider” />
</menu>

이전 포스팅한 Action Bar ActionProvider와 같은 구조로 되어 있습니다. 다만 한가지 차이점은 ActionProvider를 사용하기 위해 설정해 줘야 했던 android:actionProviderClass의 경로가 안드로이드에서 기본으로 제공하고 있는 android.widget.ShareActionProvider로 설정해야 됩니다. 당연한 말이지만 android.widget.ShareActionProvider는 ShareActionProvider의 경로 입니다. Action Bar ActionProvider포스팅에서는 직접 ActionProvider를 상속받아 구현한 클래스가 있었으므로 ActionProvider를 상속받은 클래스의 경로를 사용 했었습니다. 상단 item 의 android:showAsAction 값은 "always"이며 하단 item의 android:showAsAction 값은 "never"입니다. "always"는 항상 ActionBar에 보여진다는 뜻이며 "never" Action Bar에 보여지지 않는다는 뜻입니다.
public class ActionBarShareActionProviderActivity extends Activity {
 private static final String TAG = "ActionBar_Api_Provider_Share";
 private static final String SHARED_FILE_NAME = "shared.png";

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  // TODO Auto-generated method stub
  super.onCreate(savedInstanceState);
  copyPrivateRawResuorceToPubliclyAccessibleFile();
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // TODO Auto-generated method stub
  getMenuInflater().inflate(R.menu.action_bar_share_action_provider, menu);
  
  MenuItem actionitem = menu.findItem(R.id.menu_item_share_action_provider_action_bar);
  ShareActionProvider actionProvider = (ShareActionProvider) actionitem.getActionProvider();
  actionProvider.setShareHistoryFileName(ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME);
  actionProvider.setShareIntent(createShareIntent());
  
  MenuItem overflowItem = menu.findItem(R.id.menu_item_share_action_provider_overflow);
  ShareActionProvider overflowprovider = (ShareActionProvider) overflowItem.getActionProvider();
  overflowprovider.setShareHistoryFileName(ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME);
  overflowprovider.setShareIntent(createShareIntent());
  return true;
 }

 private Intent createShareIntent() {
  Intent shareIntent = new Intent(Intent.ACTION_SEND);
  shareIntent.setType("image/*");
  Uri uri = Uri.fromFile(getFileStreamPath("shared.png"));
  shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
  return shareIntent;
 }

 private void copyPrivateRawResuorceToPubliclyAccessibleFile() {
  InputStream is = null;
  OutputStream os = null;
  try {
   is = getResources().openRawResource(R.raw.robot);
   os = openFileOutput(SHARED_FILE_NAME, Context.MODE_APPEND);
   byte[] buffer = new byte[1024];
   int length = 0;
   while ((length = is.read(buffer)) > 0) {
    os.write(buffer, 0, length);
   }
  } catch (Exception e) {
   // TODO: handle exception
   Log.e(TAG, "Exception : " + e);
  } finally {
   try {
    is.close();
    os.close();
   } catch (Exception e2) {
    // TODO: handle exception
    Log.e(TAG, "Exception : " + e2);
   }
  }
 }
}
copyPrivateRawResuorceToPubliclyAccessibleFile() 메서드는 raw 폴더안의 robot.png 파일을 device안에 저장하는 간단한 소스 입니다.
ShareActionProvider를 사용 할때는 onCreateOptionsMenu(Menu menu)에서 이미 ActionProvider를 상속받고 있는 ShareActionProvider 클래스를 사용하면 됩니다.
MenuItem으로 가져온 Menu의 item을 ShareActionProvider로 가져온 후 setShareHistoryFileName(ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME);를 통해 공유 기록을 저장할 파일 이름을 설정하고 setShareIntent로 공유 할 앱들을 설정한 후 호출하면 된다.(DEFAULT_SHARE_HISTORY_FILE_NAME가 기본)


참조

2013년 4월 23일 화요일

[Android] Action Bar ActionProvider

ActionProvider

ActionProvider는 하나의 component 안에서 다양한 메뉴의 상호작용을 의미한다. ActionProvider는 Action Bar에서 사용 할 수 있는 Action View를 생성하며, MenuItem에 서브메뉴를 동적으로 채운다. 그리고 기본 메뉴의 아이템을 처리합니다. ActionProvider는 MenuItem만을 위한 옵션을 설정할 수도 있으며, 간단한 버튼을 표시할때는 Action View를 만들어 표시 할 것이다.

ActionProvider는 추상 메서드로서 필요한 메서드를 Override해서 사용 할 수 있습니다. API Level 16부터 onCreateActionView()는 deprecated 되었으며 onCreateActionView(MenuItem forItem)이 추가 되었습니다. onCreateActionView() 메서드를 사용 하려면 android:targetSdkVersion을 16미만으로 적용 하시면 됩니다. onCreateActionView(MenuItem forItem)에서 MenuItem이 매개변수로 추가된 이유는 MenuItem을 재정의 할 수 있게 할려고 한게 아닌가 싶습니다. onCreateOptionsMenu(Menu menu)에서 정의된 Menu를 사용 할 수도 있겠지만 MenuItem만을 위한 재정의를 할 수 있게 추가 된게 아닐까 생각합니다. targetSdkVersion만 맞춰준다면 현재로서는 어떤 메서드를 사용하든지 상관 없으나 이왕이면 deprecated된 메서드 보다는 새로 추가 된 메서드를 사용 하시는걸 추천 합니다.

이번 포스팅에서는 Open Source 중 ActionBarSettingsActionProviderActivity에 대해 살펴 볼려고 합니다.
<?xml version=”1.0” encoding=”utf-8”?>
<menu xmlns:android=”http://schemas.android.com/apk/res/android”>

    <item android:id=”@+id/menu_item_action_provider_action_bar”
        android:showAsAction=”ifRoom”
        android:title=”@string/btn_api_provider_setting”
        android:actionProviderClass=”com.example.wawoops_test_actionbar.ActionBar_Api_Provider_Setting$SettingActionProvider”/>

    <item android:id=”@+id/menu_item_action_provider_overflow”
        android:showAsAction=”never”
        android:title=”@string/btn_api_provider_setting”
        android:actionProviderClass=”com.example.wawoops_test_actionbar.ActionBar_Api_Provider_Setting$SettingActionProvider”/>
</menu>
Menu를 보면 item이 2개인 것을 보실 수 있습니다. 여기서 주의깊에 봐야 할 부분은 android:showAsAciton부분입니다. 첫번째 item은 "ifRoom"으로 되어있고 두번째 item은 "never"로 되어있습니다. 첫번째 item은 만약 ActionBar에 공간이 있다면 표시하라는 뜻이며 두번째  item의 "never"는 overflow 영역으로 무조건 빼라는 뜻입니다. 즉 화면서 바로 보지 않겠다는 뜻입니다.
android:actionProviderClass를 설정하면 ActionProvider를 사용 할 수 있습니다. ActionProvider를 extends 하고 있는 클래스 경로명을 적어주면 되며, setActionProvider(ActionProvider)를 사용하여 자바코드에서도 설정해 줄 수 있습니다. 만약 공간이 부족해서 Overflow부분으로 빠진다면 텍스트가 보여져야 되므로 이왕이면 Title은 각 item마다 설정해 주는게 좋습니다.

public class ActionBarSettingsActionProviderActivity extends Activity {

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // TODO Auto-generated method stub
  getMenuInflater().inflate(R.menu.action_bar_settings_action_provider, menu);
  return true;
 }

 @Override
 public boolean onOptionsItemSelected(MenuItem item) {
  // TODO Auto-generated method stub
  Toast.makeText(this, "선택된 버튼의 타이틀 : " + item.getTitle(), Toast.LENGTH_SHORT).show();
  return true;
 }
 
 public static class SettingActionProvider extends ActionProvider{
  private static final String TAG = "SettingActionProvider";
  private final Context mContext;
  private final Intent sSettingsIntent = new Intent(Settings.ACTION_SETTINGS);
  public SettingActionProvider(Context context) {
   super(context);
   // TODO Auto-generated constructor stub
   mContext = context;
  }

  @Override
  public View onCreateActionView(MenuItem forItem) {
   // TODO Auto-generated method stub
//   actionBar 영역에 들어가는 itme을 재정의 해 줄 수 있다. 
//   메뉴 버튼과 actionBar의 버튼을 서로 다르게 정의 할 수 있다.
//   forItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
   LayoutInflater layoutInflater = LayoutInflater.from(mContext);
   View view = layoutInflater.inflate(R.layout.action_bar_settings_action_provider, null);
   ImageButton button = (ImageButton) view.findViewById(R.id.button);
   button.setOnClickListener(new View.OnClickListener() {
    
    @Override
    public void onClick(View v) {
     // TODO Auto-generated method stub
     mContext.startActivity(sSettingsIntent);
    }
   });
   return view;
  }

  @Override
  @Deprecated
  public View onCreateActionView() {
   // TODO Auto-generated method stub
   return null;
  }

  @Override
  public boolean onPerformDefaultAction() {
   // TODO Auto-generated method stub
   Log.e(TAG, "onPerformDefaultAction()");
   mContext.startActivity(sSettingsIntent);
   return true;
  }
 }
}
onCreateOptionsMenu(Menu menu)에서  Menu를 inflate 해주면 Menu 버튼과 Action Bar 두곳 모두에서 사용 할 수가 있습니다. 이전 포스팅한 글에서 Menu는 Menu 버튼을 클릭하면 나타나는 것이고 MenuItem은 Action Bar에서 나타난다고 말했습니다. ActionProvider가 onCreateOptionsMenu(Menu menu)에서 inflate되는 Menu를 MenuItem에 추가 하는 기능을 합니다. 만약 onOptionsItemSelected(MenuItem item)가 호출 되지 않았다면 ActionProvider의 onPerformDefaultAction()이 호출 되었을 겁니다.  onPerformDefaultAction()은 Overflow 영역에 있을때 호출되며 이 메서드가 호출되면 onOptionsItemSelected(MenuItem item)은 호출 되지 않습니다.

소스에서는 onCreateActionView(MenuItem forItem)에서 inflate해준 view에 클릭 이벤트를  줬습니다. 이 결과 onPerformDefaultAction()와  onOptionsItemSelected(MenuItem item)가 호출 되지 않았으며 바로 onClick(View v)메서드가 호출되는 것을 확인 하였습니다. 간단하게 onCreateActionView(MenuItem forItem)메서드에서 만들어 놓은 레이아웃을 inflate 해주고 바로 이벤트를 걸어 사용 하면 되지 않을까 생각됩니다. onCreateActionView(MenuItem forItem)에서 버튼 하나를 만들어서 붙여 본 결과  onCreateOptionsMenu(Menu menu)에서 추가된 item 개수만큼 버튼이 생기는걸 확인 할 수 있었습니다.(물론 Action Bar에서 보여 질수 있는 item에 한해서 말하는 것입니다.)


참조

2013년 4월 21일 일요일

[Android] Action Bar Mechanics

Android에서는 고맙게도 Open Source를 제공해 주고 있습니다.
Android의 어떤 기능을 배우고 싶을때 Open Source를 보고 분석하는 것 보다 더 좋은 방법은 없다고 생각합니다.
Action Bar 역시 Open Source를 분석하면서 습득하는게 가장 확실한 방법이 아닐까 생각합니다. Open Source에서 ActionBar의 예제는 총 6가지 입니다. 각각의 클래스 이름은ActionBarDisplayOptions, ActionBarMechanics, ActionBarSettingsActionProviderActivity, ActionBarShareActionProviderActivity, ActionBarTab, ActionBarUsage 입니다. 이 중 블로그 제목처럼 ActionBarMechanics에 대해 작성해 보겠습니다. ActionBarMechanics  클래스는 이름처럼 ActionBar를 구현하기 위한 기본 구조를 설명하는 클래스입니다.

public class ActionBar_Api_Mechanics extends Activity {

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  // TODO Auto-generated method stub
  super.onCreate(savedInstanceState);
  getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // TODO Auto-generated method stub
//  Menu 버튼
  menu.add("Normal item");
  
//  Action Bar  버튼
  MenuItem actionItem = menu.add("Action Button");
  actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
  actionItem.setShowAsAction(android.R.drawable.ic_menu_share);
  return true;
 
 }

 @Override
 public boolean onOptionsItemSelected(MenuItem item) {
  // TODO Auto-generated method stub
  Toast.makeText(this, "Selected Item : " + item.getTitle(), Toast.LENGTH_SHORT).show();
  return true;
 }
}

Action Bar를 설정하기 위해서는 Content View를 Setting하기전에 getWindow().requestFeature(Window.FEATURE_ACTION_BAR)를 설정해 줘야 됩니다. 그러나 보통 Activity의 테마가 기본으로 적용하고 있어서 위와 같이 getWindow().requestFeature(Window.FEATURE_ACTION_BAR)를 적용하지 않고도 Action Bar를 사용 할 수 있습니다. 물론 Theme.NoTitleBar를 적용하여 Action Bar영역을 사라지게 할 수 도 있습니다.

Action Bar에 기능을 추가 하는 방법은 Menu버튼을 사용 할 때와 크게 다르지 않습니다.
onCreateOptionMenu(Menu menu)에서 Menu를 추가해주고 onOptionsItemSelected(MenuItem item)에서 추가해준 Component의 이벤트를 받아서 처리해 주면 됩니다. 한가지 차이점은 ActionBar에 추가되는 Component는 MenuItem 클래스 추가해 줘야 된다는 점입니다. 위의 Source를 실행해 보면 Menu 버튼을 클릭하면 "Normal item"이 출력이 되며, Action Bar에 있는 Share 버튼을 클릭하면 "Action Bar"가 Toast에 출력 됩니다. 만약 Theme.NoTitleBar를 적용해 주면 Menu 버튼을 클릭했을때 "Normal item" 과 "Action Bar"가 Menu 버튼에 표시 되게 됩니다.
Action Bar에 많은 Item를 추가해 주면 Overflow 영역으로 빠지게 됩니다. setShowAsAction() method를 이용하여 Action Bar에서 item의 보여지는 방법을 설정 할 수 있습니다. 위의 Source 에서 설정한 SHOW_AS_ATION_IF_ROOM은 ActionBar의 공간이 있으면 item의 icon만으로 Action Bar에 나타나도록 합니다.

Constants
intSHOW_AS_ACTION_ALWAYSAlways show this item as a button in an Action Bar.
intSHOW_AS_ACTION_COLLAPSE_ACTION_VIEWThis item's action view collapses to a normal menu item.
intSHOW_AS_ACTION_IF_ROOMShow this item as a button in an Action Bar if the system decides there is room for it.
intSHOW_AS_ACTION_NEVERNever show this item as a button in an Action Bar.
intSHOW_AS_ACTION_WITH_TEXTWhen this item is in the action bar, always show it with a text label even if it also has an icon specified.


참조

2013년 4월 18일 목요일

[Android] 이제 Action Bar 사용은 선택이 아닌 필수.


ActionBar는 Android 3.0(API Level 11)부터 추가되었으며 사용 할 때는 Manifest에서 targetSdkVersion 이나 minSdkVersion을 11로 맞춰줘야 되며, Theme.Holo 테마를 사용하여야 됩니다.(Theme.Holo가 Default  테마입니다.)

Android 3.0 이전에는 Title Bar에서 단순 Application의 이름정도만 보여주는게 전부 였으나 Android 3.0 부터는 Action Bar라는 놈이 추가되어서 Application의 이름 뿐만 아니라, 사용자가 보고 있는 화면의 위치, Navigation Button, Tab, Share 등을 빠르게 사용 할 수 있게 되었습니다.
사실 Android 3.0 이전부터 우리는 Action Bar의 기능을 전부 사용하고 있었습니다.
초창기 아이폰 App들의 UI를 벤치마킹 하여 디자인했기 때문에 안드로이드에는 back버튼이 있음에도 불구하고 상단에 back 버튼은 안드로이드 App에서도 흔히 볼 수 있었습니다.(물론 ActionBar의 Navigation Button과 back Button은 기능적으로 차이가 있습니다.) Back Button 뿐만 아니라 App에서 Home으로 바로 이동 할 수 있는 Button역시 관련 Activity에서 Inflate를 사용하던지 해당 View에 바로 적용하던지 하는 방법으로 사용 했었습니다. 뿐만 아니라 아이폰의 Tab이 아래에 있다는 이유만으로 안드로이드에서도 Tab을 하단에 적용해야 했던 적도 있었습니다.(안드로이드는 아이폰과 달리 하단에 Menu, Home, Back Button 이 있어서 Tab을 잘못 눌러 하단의 기본 Button들을 클릭 하는걸 방지하고자 Tab이 위에 있었던 겁니다.) 우리는 Share Button 역시 만들어서 사용 했었습니다. 그러나!! 이제는 Action Bar를 사용하여 위에 열거했던 모든 기능들을 간편하게 사용 할 수 있게 되었습니다.

안드로이드 개발자 사이트에 있는 Action Bar 이미지입니다. 여기서는 기능별로 4가지로 분류했습니다.


1. App Icon

Action Bar의 좌측에 App의 Icon을 넣을 수 있습니다. Icon을 터치하면 App의 Home으로 보낼 수도 있으며, Back Button과 마찬가지로 Task의 상위단계로 이동하게 만들 수도 있습니다.
ActionBar에서 Home로 이동하게 만들경우는 setHomeButtonEnabled(true);를 사용하여 나타낼수 있으며 Intent.FLAG_ACTIVITY_CLEAR_TOP를 추가해 주는것이 좋습니다. 그렇지 않으면 Back Button을 누를때 엄청난 양의 Task를 목격 하실 수도 있으실 겁니다.
ActionBar에서 Task의 상위 단계로 이동하게 만든다면setDisplayHomeAsUpEnabled(true); 사용하여 나타낼 수 있습니다. 상위단계로 이동하게 만들때의 주의점은 Back 버튼과 동일한 프로세스로 만드는게 아니라 상위 카테고리로 보내게끔 만드는게 더 나을 수도 있다는 점입니다. 예를 들자면 Home -> A -> B -> a -> b 와 같이 화면전환이 이루어 졌다면 (A와 B는 같은 레벨이며 a, b는 A, B의 하위 레벨 입니다.)  b 에서 Back Button을 누르면 a로 이동되나 ActionBar의 Navigation Button은 B로 이동되어져야 된다는 것입니다. Back Button이 있는 안드로이드에서 굳이 똑같은 기능을 하는 버튼을 하나 더 만들어야 되는지 생각해 볼 필요가 있습니다.


Home과 Task의 상위 단계로 보낼때의 Icon 모양은 아래와 같이 차이가 있습니다.강제성이 있는것은 아니나 이런 약속들은 지키는게 좋습니다. 이미지에 나타난 데로 Home으로 가는 버튼에는 " < " 기호가 없으며  상위 Task로 이동하는 버튼에는 " < "기호가 있음을 알 수 있습니다.


2. View control

다른 화면으로 이동 하고 싶을때 위의 이미지 처럼 drop-down 형태로 나타낼 수도 있으면 Tab방식으로 나타낼 수도 있습니다. 만약 화면 전환 메뉴가 필요없으면 해당 위치에 사용자의 위치를 표시 해 줄 수도 있으며, App의 Title를 표시해 놓을 수도 있습니다. 물론 사용자의 위치 와 Tab을 동시에 표시할 수도 있습니다. 

Tab을 사용 할때는 device화면의 크기에 따라 그림과 같이 Tablet 처럼 큰 화면에서의 배치와 작은 화면에서의 배치가 다르게 표현 된다는 걸 알 수 있습니다.



3. Action Button

Action Bar에 버튼을 추가하는 영역입니다. 보통 화면에서 가장 중요한 기능을 한 두가지 넣어주거나 Search 기능이나 Share기능을 추가 할 수도 있습니다. Action Bar에서 사용 할 Icon은 안드로이드 UX팀에 의해 디자인된 Action Icon을 사용하는것을 추천합니다. 만약 Custom 하려고 한다면 Android에서 제안하는 기준에 맞게 디자인 하기를 추천합니다. Action Bar안의 ActionView영역은 사라졌다가 확장되었다가 할 수 있기 때문에 device 화면의 크기에 따라 유동적으로 바뀌게끔 설정해 주면 됩니다. 이런 기능은 device화면이 작을 때 공간 효율을 더욱 좋게 해줍니다. 사용자가 Back Button이나 Navigation Button을 터치하면 확장되었던 화면은 다시 사라지게 되며 MenuItem의 expandActionView()와 colapseActionView()를 이용하여 ActionView를 사용자가 컨트롤 할수 있게 만들 수도 있습니다.

4. Action overflow

ActionBar에 메뉴 버튼이라고 생각하면 됩니다. 자주 사용하지 않은 기능들이나 화면에
넘치는 기능 또는 메뉴버튼으로 사용 되어지고 싶은 기능을 추가하는 영역입니다. Android 3.0이전에는 물리적인 메뉴버튼을 이용하였지만 Android 3.0이후에는 물리적인 메뉴 버튼 대신 Action Bar의 overflow 영역이 생겼습니다. Android 3.0이전의 모든 device에는 메뉴 버튼이 있었으나 Android 3.0 이후 부터는 물리적인 메뉴 버튼이 없는 device도 있습니다.  Android 3.0이전에 옵션메뉴로 개발되어진 App은 targetSdkVersion 이나 minSdkVersion를 11또는 그 이상으로 설정한다면 이전에 설정했던 옵션 기능들은 API 레벨에 근거하여 Action overflow에 추가하지 않습니다. 
보통 메뉴버튼은 해당 Activity의 옵션기능을 설정하였습니다. overflow영역 역시 마찬가지로 해당 Activity나 Fragment의 옵션 기능을 설정하는 영역입니다.



참조


2013년 4월 2일 화요일

[UML] ClassDiagram (클래스 다이어그램)

ClassDiagram을 구체적으로 설명 한다기 보다는 실무에서 쓰는 정도와 개념에 대해 설명 하겠습니다.

ClassDiagram이란?
: 소스코드에 나타나는 클래스 사이의 의존 관계를 표기한 Diagram이다.


Class (클래스)
◆ 일반 클래스

public class Dialer{

}



public class Dialer{
     private Vector digits;
     private int nDigits;
     public void digit(int n);
     protected boolean recordDigit(int n):
}


둘 중 어떤 방식을 사용 해도 상관 없으나 단순 클래스의 연관을 표현 할려고 하면 Class명만 사용하여 연관 관계를 표시하는게 더 직관적이며 눈에 잘 들어 옵니다.

◆ 추상 클래스

public abstract class shape{
     private Point itsAnchorPoint;
     public abstract void draw();
}









추상 클래스는 Class명을 이텔릭체로 쓰거나 {abstract}라고 명시해 주면 됩니다.

  • ClassDiagram 기호 및 구조
    • 기호
      • Class 안의 기호 중에 -, +, # 을 볼 수 있습니다..
        • 대시 (-): private
        • 더하기 (+) : public
        • 해시 (#) : protected
    • 구조
      • 제일 상단은 클래스 이름.
      • 두번째는 변수타입 및 변수명.
      • 세번째는 메소드 타입 및 메소드 명.
◆ 내부 클래스

public class A{
     private class B{
     }
}




◆ 익명 내부 클래스

public Class A{
     public void c(){
          new B().start();
     }
}



◆연관


public class phone{
     private Button itspnos;
}





Phone Class에 Button을 가지고 있다는 표현입니다..
여기서 *는 버튼이 여러개일 수 있다는 의미 이며 * 대신 10, 20 등과 같이 숫자로 표현 할 수 도 있습니다.

◆상속


public class Employee{
   . . .
}





public class SalariedEmployee extends Employee{
   . . .
}
SalariedEmpoloyee Class는 Employee Class를 상속하는 모습입니다.
연관과 상속의 차이점은 화살표 모양입니다. 또한 일반적으로 상속은 위 아래로 표현하며 연관은 좌우로 표현합니다.

interface 표시는 아래 그림 중 하나를 선택해서 표시하면 됩니다.


Class Diagram에는 더 많은 기능과 기호들이 있습니다. UML 전문가가 되려면 이 외에 많은 기능들을 알아야되나 실무에서 사용하는 정도로는 이정도만 알고 있으면 되지 않을까 싶습니다. 사실 이 이상 사용할 일은 거의 없다고 생각됩니다. 

2013년 2월 19일 화요일

[Java] TCP

TCP 프로토콜?
  • TCP는 Socket 과 ServerSocket를 사용하여 데이터를 주고 받습니다.
  • Socket 은 목적지 정보를 전송 수신하는 역할을 합니다.
  • ServerSocket 은 포트 번호와 Client와 연결되는 서버의 최대 연결 수를 지정 할 수 있습니다.
◆ 장점
  • 신뢰성(데이타 손실이 없습니다.)
  • 양뱡향 통신 가능
  • 바이트 스트림 지원
◆ 단점
  • UDP에 비해 다소 느릴 수 있음.
Client 예제
/*  * 클라이언트에서 작업할 내용  
 * 1)소켓 객체 생성(ip, port지정) ==> 해당 ip에 접속하여 정상적인 접속이 완료되면 객체가 생성  
 * 2)I.O 스트림 열기  
 * 3)자료 송/수신  
 * 4)소켓 닫기 
 * */
public class TestClient {
 static Socket s;

 public TestClient() throws IOException {
  s = new Socket("127.0.0.1", 9999); // 서버주소

  BufferedReader reader = new BufferedReader(new InputStreamReader(
    s.getInputStream()));
  PrintWriter writer = new PrintWriter(s.getOutputStream());
  String msg = "Strat!!";
  String rMsg = null;
  BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
  while (!msg.equals("bye")) {
   System.out.print("전송입력 : ");
   msg = in.readLine();
   writer.print(msg);
   writer.flush();
   System.out.println("가는 중...");
   rMsg = reader.readLine();
   System.out.print("받은 메세지:" + rMsg);
  }
 }

 public static void main(String[] args) throws IOException {
  try {
   new TestClient();
  } catch (IOException e) {
   e.printStackTrace();
  } finally {
   try {
    s.close();
   } catch (IOException e) {
    e.printStackTrace();
   }
  }
 }
}

  • 주소와 포트번호와 함께 Socket을 생성 합니다.
  • 바이트 스트림을 지원 하므로 바이트 형태로 데이타를 주고 받습니다.
  • in.readLine()으로 입력 받은 문자를 writer.print(msg)을 사용하여 Server에 보내고 writer.flush() 시켜줍니다.
  • rMsg = reader.readLine()은 Server에서 받은 메세지를 한줄씩 읽습니다.
  • BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream())) 와 PrintWriter writer = new PrintWriter(s.getOutputStream())에서 전송한 데이타와 수신한 데이터를 알 수 있습니다.

Server 예제
/*  * 서버에서 수행하는 일  
 *   1)서버소켓 객체 생성  
 *   2)클라이언트 접속 대기  
 *   3)자료 송/수신  
 *   4)소켓 닫기 */
public class TestServer {
 static ServerSocket ss = null;
 static Socket s = null;

 public TestServer() throws IOException {
  ss = new ServerSocket();
  s = new Socket();
  ss = new ServerSocket(9999);
  System.out.println("클라이언트 접속 대기 중...");
  s = ss.accept();
  System.out.println("클라이언트 접속!!");
  BufferedReader reader = new BufferedReader(new InputStreamReader(
    s.getInputStream()));
  PrintWriter write = new PrintWriter(s.getOutputStream());
  String msg = "Start!";
  while (!msg.equals("bye")) {
   System.out.println("받는 중...");
   msg = reader.readLine();
   System.out.println("수신데이타 : " + msg);
   write.println(msg);
   write.flush();
  }
 }

 public static void main(String[] args) {
  try {
   new TestServer();
  } catch (IOException e) {
   e.printStackTrace();
  } finally {
   try {
    ss.close();
    s.close();
   } catch (IOException e) {
    e.printStackTrace();
   }
  }
 }
}

  • ServerSocket으로 포트 번호를 맞춰 줍니다.
  • s = ss.accept() 부분이 Client가 접속하기를 기다리는 부분입니다. 서버는 클라이언트가 접속하기 전까지 대기하고있는 상태입니다.
  • msg = reader.readLine() 위에 스트림으로 받은 데이터를 다시 Client로 전송하는 부분입니다.

TCP를 설명하기 위해 간단히 작성된 소스입니다. 요약하자면 Client에서 Socket 를 사용하여 목적지(서버)의 주소와 포트번호를 설정하여 보내면 서버에서는 ServerSocket 에서 포트를 맞추고 accept()에서 접속하기를 기달리고 있다가 스트림을 이용하여 다시 전송하는 코드입니다.

관련글
  1. [Java] UDP
  2. [Java] 객체 저장(OutputStream, inputStream, BufferStream)

[Java] UDP

UDP 프로토콜?

  • UDP는 DatagramSocket 와 DatagramPacket를 사용하여 데이터를 전송한다는 것만 알면 됩니다.
  • DatagramSocket은 DatagramPacket를 전송하거나 수신 받기 위해 사용 합니다.
  • DatagramPacket은 UDP 통신에서 데이터를 담아서 전송하거나 받기 위해 사용 합니다.

◆ 장점
  • 복잡하지 않음
  • 시스템 로드를 줄일 수 있음
  • 빠름

◆ 단점
  • 데이타 손실이 있을 수 있음
Client 예제
public class BroadClient {
 public BroadClient() {
  new Thread(new ReadThread()).start();
 }

 class ReadThread implements Runnable {
  DatagramSocket ds = null;
  DatagramPacket packet = null;
  byte[] brr = new byte[200];

  public void run() {
   try {
    ds = new DatagramSocket(12345); // 서버 port를 맞춘다.
    packet = new DatagramPacket(brr, brr.length);// brr : 데이터그램 패킷을 수신할 바이트 배열, brr.length:데이터그램 수신 크기.
    while (true) {
     ds.receive(packet); // 블럭 되어 있는 부분.
     System.out.println("받은 문자열 : "+ new String(packet.getData())); // 바이트로 받은 내용을 문자로바꾼다.
     System.out.println(packet.getAddress().getHostAddress());// 보낸사람 address를 알 수 있다.
     Thread.yield();
    }
   } catch (Exception e) {
    System.out.println("받기 오류 :" + e);
    e.printStackTrace();
   }
  }
 }

 public static void main(String[] args) {
  new BroadClient();
 }
}

  • 수신받는 부분의 DatagramPacket 부분입니다. packet = new DatagramPacket(brr, brr.length)
  • ds.receive(packet) 부분이 Server에서 데이타를 받는 부분입니다. 데이터를 받을때까지 블럭 되어있습니다.

Server 예제
public class BroadServer {
 boolean onAir = false;
 DatagramSocket ds;
 DatagramPacket packet;
 int port = 12345;// 받을 사람 port // 상용화 포트(1024 ~ 10000)

 InetAddress address;

 public BroadServer() {
  HowServer server = new HowServer();
  Thread trd = new Thread(server);
  onAir = true;
  trd.start();
 }

 static class Unse {
  static Random ran = new Random();
  static final String[] arr = { "오늘은 모든 일이 잘 풀릴것 같음.",
    "비가 올 것 같이 기분이 좋지 않음.", "안드로이드 공부 중.", "xml 중요.",
    "프로젝트 준비 철저.", "마무리를 잘하자!!" };

  static String getUnse() {
   return arr[ran.nextInt(arr.length)];
  }
 }

 class HowServer implements Runnable {
  String data;
  byte[] brr;

  public void run() {
   try {
    ds = new DatagramSocket();
    address = InetAddress.getByName("127.0.0.1");// 서버 주소
   } catch (Exception e) {
    e.printStackTrace();
   }
   try {
    while (onAir) {
     data = Unse.getUnse(); // 운세의 내용을 가져온다.
     brr = data.getBytes(); // String을 byte 배열로 만들기.
     packet = new DatagramPacket(brr, brr.length, address, port); // 배열, 배열길이, 주소, 포트번호.
     ds.send(packet);
     System.out.println("서버전송 문자열 :" + data);
     Thread.sleep(1000);
    }
   } catch (Exception e) {
    System.out.println("서버생성 오류 :" + e);
    e.printStackTrace();
   }
  }
 }

 public static void main(String[] args) {
  new BroadServer();
 }
}
  • 전송하는 부분의 DatagramPacket 부부입니다.packet = new DatagramPacket(brr, brr.length, address, port)
  • ds.send(packet) 부분이 Client로 데이타를 보내는 부분입니다.
UDP는 특별한 내용이 없습니다. DatagramSocket 와 DatagramPacket를 사용하며 send()로 보내며 receive()로 받는 다는 것만 기억하시면 됩니다.

관련글
  1. [Java] TCP
  2. [Java] 객체 저장(OutputStream, inputStream, BufferStream)

2013년 2월 17일 일요일

[Java] 객체 저장(OutputStream, InputStream, BufferStream)

객체를 저장하는 방법에는 2가지가 있습니다.
  1. 직렬화를 사용하여 저장
    • 자바 프로그램에서만 사용한다는 조건에서 사용합니다.
    • 클래스를 직렬화 할려면 implements Serializable를 사용해야 됩니다.
    • static 변수는 직렬화 하지 못합니다. 
    • 클래스 중에 직렬화를 하고 싶지 않은 변수에는 transient를 사용합니다.
  2. 문자로 저장
    • 타 프로그램에서도 저장된 객체를 사용 할 수도 있을때 사용합니다.
◆ 직렬화를 사용할 경우
 try {
 //  "dg.ser"로 저장합니다.
  ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dg.ser"));
  oos.writeObject(d);
  oos.close();
  
 //  파일이름이 "dg.ser"인 파일을 가져옵니다.
  ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dg.ser"));
  d = (DungeonGame)ois.readObject();
  ois.close();
  
 } catch (Exception e) {
  // TODO: handle exception
  e.printStackTrace();
 }

《저장》
  • FileOutputStream를 사용하여 바이트 형태로 저장합니다.
  • ObjectOutputStream를 사용하여 직렬화 하여 저장합니다.
  • ObjectInputStream를 닫아 줍니다. ObjectInputStream를 닫으므로서 밑에 있는 FileOutputStream는 자동으로 닫힙니다.
《가져오기》
  • FileOutputStream를 사용하여 바이트 형태로 읽습니다.
  • ObjectOutputStream를 사용하여 클래스를 찾아서 불러온 다음 저장되었던 인스턴스 변수값을 다시 대입합니다. 직렬화된 객체의 생성자는 실행되지 않습니다.
  • ObjectInputStream를 닫아 줍니다. ObjectInputStream를 닫으므로서 밑에 있는 FileOutputStream는 자동으로 닫힙니다.
◆ 문자로 저장
    try {
     //  "wawoo.txt"로 저장합니다.
      BufferedWriter writer = new BufferedWriter(new FileWriter("wawoo.txt"));
      writer.write("wawoo oops!");
      writer.close();
      
     //  파일 이름이 "wawoo.txt"인 것을 불러옵니다.
      BufferedReader reader = new BufferedReader(new FileReader(new File("wawoo.txt")));
      String line = null;
      while ((line = reader.readLine()) != null) {
       System.out.println(line);
      }
      reader.close();
      
     } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
     }

    《저장》
    • FileWriter를 사용하여 문자 형태로 저장합니다.
    • BufferedWriter를 사용하여 효율을 높입니다. 버퍼를 사용하는 목적중에 하나는 시스템 효율입니다. 버퍼는 box라고 생각하고 박스가 차면 한번에 데이터를 처리한다고 생각하면 됩니다.
    • BufferedWriter를 닫아줍니다.여기서도 역시 BufferedReader만 관리하면 하위단에 있는 FileReader는 자동으로 닫힙니다.
    《가져오기》
    • FileReader를 사용하여 저장된 파일을 연결합니다.
    • BufferedReader를 사용하여 효율을 높입니다.
    • BufferedReader를 닫아줍니다. 

    2013년 2월 6일 수요일

    [Android] Fragment example(프레그먼트 예제)

    Fragment 예제가 제일 잘 나와 있는 곳은 Android API 소스입니다.

    • App → Fragment 에 들어가 보면 많이 나와있습니다.


    사실 Fragment가 만들어지게 된 큰 이유중에 하나가 타블렛에서의 화면 비효율성 때문입니다. 이제는 스마트폰의 크기도 커지면서 LandScape일때와 Portrait일때의 화면 구성도 달라져야 합니다. 예제 중 Fragment의 활용을 가장 잘 보여준다고 생각하는 FragmentLayout 예제를 가지고 Fragment를 설명 하겠습니다.

    Portrait XML(res - layout)
    <?xml version="1.0" encoding="utf-8"?>
    <linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <fragment
            android:id="@+id/fragment1"
            android:name="com.example.wawoops_fragment_test.FragmentLayoutActivity$TitlesFragment"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    
    </LinearLayout>


    LandScape XML(res - layout-land)
    <?xml version="1.0" encoding="utf-8"?>
    <linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" >
    
        <fragment
            android:id="@+id/fragment1"
            android:name="com.example.wawoops_fragment_test.FragmentLayoutActivity$TitlesFragment"
            android:layout_width="0dip"
            android:layout_height="match_parent" 
            android:layout_weight="1"/>
    
        <FrameLayout
            android:id="@+id/flLandFragmentLayout"
            android:layout_width="0dip"
            android:layout_height="match_parent" 
            android:layout_weight="1">
        </FrameLayout>
    
    </LinearLayout/>
    


    일단 위와 같이 portrait 화면 일 때와 LandScape 화면일때를 구성해 줍니다. 두 파일명은 같아야 합니다. 저는 activity_fragment_layout.xml로 만들어 줬습니다.

    android:name="com.example.wawoops_fragment_test.FragmentLayoutActivity$TitlesFragment" 의 의미는 com.example.wawoops_fragment_test 패키지 안에 FragmentLayoutActivity 클래스 안에 TitlesFragment 라는 Fragment를 상속받은 클래스가 있다는것을 의미 합니다. 이처럼 Fragment Component는 Fragment를 extends 하는 클래스를 만들고 그 경로를 써줘야 됨을 알 수 있습니다.

    두 화면의 차이점은 LandScape 화면일때 옆에 화면을 하나 더 구성하기 위해 FrameLayout을 추가해 주었습니다.

    간단하게 프로세스를 설명 드리자면 Activity 클래스에서 setcontentView(activity_fragment_layout); 와 같이 layout을 호출하면 layout안에 fragment 태그안에서 java 파일의 TitlesFragment 를 호출 하게 됩니다.

    FragmentLayoutActivity$TitlesFragment
    public static class TitlesFragment extends ListFragment{
      boolean mLandPane;
      int mCurCheckPosition = 0;
      @Override
      public void onActivityCreated(Bundle savedInstanceState) {
       // TODO Auto-generated method stub
       super.onActivityCreated(savedInstanceState);
       Log.v(TAG, "onActivityCreated");
       
       setListAdapter(new ArrayAdapter(getActivity(), android.R.layout.simple_list_item_activated_1, Shakespeare.TITLES));
       
       View detailsFrame = getActivity().findViewById(R.id.flLandFragmentLayout);
       mLandPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;
       
       if (savedInstanceState != null) {
        mCurCheckPosition = savedInstanceState.getInt("curChoice", 0);
       }
       
       if (mLandPane) {
        showDetails(mCurCheckPosition);
       }
      }
      
      @Override
      public void onSaveInstanceState(Bundle outState) {
       // TODO Auto-generated method stub
       super.onSaveInstanceState(outState);
       Log.e(TAG, "onSaveInstanceState");
       outState.putInt("curChoice", mCurCheckPosition);
      }
    
      @Override
      public void onListItemClick(ListView l, View v, int position, long id) {
       // TODO Auto-generated method stub
       showDetails(position);
      }
    
      void showDetails(int index){
       Log.v(TAG, "index : " + index);
       mCurCheckPosition = index;
       getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
       getListView().setItemChecked(index, true);
       
       if (mLandPane) {
        Log.v(TAG, "mLandPane index : " + index);
        DetailsFragment details = (DetailsFragment) getFragmentManager().findFragmentById(R.id.flLandFragmentLayout);
        if (details == null || details.getShowIndex() != index) {
         details = DetailsFragment.newInstance(index);
         
         FragmentTransaction ft = getFragmentManager().beginTransaction();
         ft.replace(R.id.flLandFragmentLayout, details);
         ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
         ft.commit();
        }
       }else {
        Intent intent = new Intent();
        intent.setClass(getActivity(), DetailsActivity.class);
        intent.putExtra("index", index);
        startActivity(intent);
       }
    
      }
     }
    
    • TitlesFragment는 ListFragment를 상속 받았습니다.
    • public void onActivityCreated(Bundle savedInstanceState) 는 Fragment의 생명주기중 화면에 보여지기 바로 직전의 method 입니다.
    • View detailsFrame 에 LandScape 화면일때 상세화면을 보여줄 FrameLayout를 findViewById를 해줍니다. 여기서 getActivity()는 TitlesFragment를 띄우고 있는 Activity를 말합니다. Fragment를 사용하다보면 많이 사용하게 됩니다.
    • mLandPane는 화면이 LandScape일때를 구분해 주는 flag 입니다.
    • public void onListItemClick(ListView l, View v, int position, long id) 는 ListView를 클릭 했을때의 CallBack Method  입니다.
    • void showDetails(int index) 부분이 FragmentLayout 프로젝트의 핵심 부분으로 LandScape 화면일때는 FrameLayout에 DetailsFragment를 매칭시키며 Portrait 화면일 경우에는 DetailsActivity를 호출하는 내용입니다.
      • (DetailsFragment)getFragmentManager().findFragmentById(R.id.flLandFragmentLayout) :  Fragment 안에서 Activity의 findViewById와 마찬가지로 findFragmentById를 사용하여 fragment로 선언해 줄수 있습니다.
      • details = DetailsFragment.newInstance(index) : DetailsFragment클래스안에 static newInstance  method를 만들어 놨습니다.
      • FragmentTransaction ft = getFragmentManager().beginTransaction() : FragmentManager().beginTransaction()를 통해 FragmentTransaction를 가져옵니다.
      • ft.replace(R.id.flLandFragmentLayout, details) : FragmentTransaction를 이용하여 fragment를 Activity에 추가하였다가 삭제할 수 있습니다. 현재는 R.id.flLandFragmentLayout에 details를 replace 하는 Method 입니다.
      • ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) : setTransition는 Fragment 에서 애니메이션 효과를 주는 기능입니다.
      • ft.commit() : 마지막에 꼭 commit 해줘야 화면에서 보입니다.
    참고)
    ◆ setTranstion (int transit) : 애니메이션 효과를 준다.
    • TRANSIT_NONE : 애니메이션 효과를 제공하지 않는다.
    • TRANSIT_FRAGMENT_OPEN : 스택에서 꺼내는 효과를 창출한다.
    • TRANSIT_FRAGMENT_CLOSE : 스택에 넣는 효과를 창출한다.
    ◆ setCustomAnimation (int enter, int exit) : 사용자가 제작한 애니메이션을 사용하여 Fragment를 출력한다.
    • enter : 시작시점 효과
    • exit : 끝나는 시점의 효과
    FragmentLayoutActivity$DetailsFragment
    public static class DetailsFragment extends Fragment {
      
      public static DetailsFragment newInstance(int index){
       
       DetailsFragment f = new DetailsFragment();
       Bundle args = new Bundle();
       args.putInt("index", index);
       f.setArguments(args);
       return f;
      }
      
      @Override
      public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
       // TODO Auto-generated method stub
       if (container == null) {
        return null;
       }
       ScrollView scroller = new ScrollView(getActivity());
       TextView text = new TextView(getActivity());
       int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getActivity().getResources().getDisplayMetrics());
       text.setPadding(padding, padding, padding, padding);
       scroller.addView(text);
       text.setText(Shakespeare.DIALOGUE[getShowIndex()]);
       return scroller;
      }
      
      public int getShowIndex(){
       return getArguments().getInt("index", 0);
      }
     }
    

    DetailsFragment에서 유심히 봐야할 메소드는 setArguments입니다. Activity에서는 Intent.putExtra()와 Intent.getExtra()와 같은 method를 이용하여 데이터를 주고 받았지만 Fragment에서는 setArguments()와 getArguments()메소드를 이용하여 데이터를 주고 받을 수 있습니다.
    • onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) 는 Fragment의 생명주기중 View를 inflater할때 사용할수 있는 곳입니다. 매개변수를 보면 여기에는 화면을 붙일 수 있는 inflater와 부모View를 가지고 올 수 있는 container와 저장된 정보를 사용 할 수 있는 savedInstanceState로 구성되어 있습니다. 하단 코딩은 UI를 자바 코드로 표현한 것입니다.
    • getShowIndex()에서 getArguments()를 사용하여setArguments()한 데이타를 가지고 오도록 했습니다.

    FragmentLayout.java 소스 전문
    package com.example.wawoops_fragment_test;
    
    import android.app.Activity;
    import android.app.Fragment;
    import android.app.FragmentTransaction;
    import android.app.ListFragment;
    import android.content.Intent;
    import android.content.res.Configuration;
    import android.os.Bundle;
    import android.util.Log;
    import android.util.TypedValue;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ArrayAdapter;
    import android.widget.ListView;
    import android.widget.ScrollView;
    import android.widget.TextView;
    
    public class FragmentLayoutActivity extends Activity {
     private static final String TAG = "FragmentLayoutActivity";
    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
      // TODO Auto-generated method stub
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_fragment_layout);
     }
     
     public static class DetailsActivity extends Activity{
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
       // TODO Auto-generated method stub
       super.onCreate(savedInstanceState);
       
       if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
        finish();
        return;
       }
       
       if (savedInstanceState == null) {
    //    DetailsFragment details = new DetailsFragment();
    //    details.setArguments(getIntent().getExtras());
    //    Log.e(TAG, "getIntExtra : " + getIntent().getIntExtra("index", 0));
        DetailsFragment details = DetailsFragment.newInstance(getIntent().getIntExtra("index", 0));
        getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
       }
      }
     }
     
     public static class TitlesFragment extends ListFragment{
      boolean mLandPane;
      int mCurCheckPosition = 0;
      @Override
      public void onActivityCreated(Bundle savedInstanceState) {
       // TODO Auto-generated method stub
       super.onActivityCreated(savedInstanceState);
       Log.v(TAG, "onActivityCreated");
       
       setListAdapter(new ArrayAdapter(getActivity(), android.R.layout.simple_list_item_activated_1, Shakespeare.TITLES));
       
       View detailsFrame = getActivity().findViewById(R.id.flLandFragmentLayout);
       mLandPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;
       
       if (savedInstanceState != null) {
        mCurCheckPosition = savedInstanceState.getInt("curChoice", 0);
       }
       
       if (mLandPane) {
        showDetails(mCurCheckPosition);
       }
      }
      
      @Override
      public void onSaveInstanceState(Bundle outState) {
       // TODO Auto-generated method stub
       super.onSaveInstanceState(outState);
       Log.e(TAG, "onSaveInstanceState");
       outState.putInt("curChoice", mCurCheckPosition);
      }
    
      @Override
      public void onListItemClick(ListView l, View v, int position, long id) {
       // TODO Auto-generated method stub
       showDetails(position);
      }
    
      void showDetails(int index){
       Log.v(TAG, "index : " + index);
       mCurCheckPosition = index;
       getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
       getListView().setItemChecked(index, true);
       
       if (mLandPane) {
        Log.v(TAG, "mLandPane index : " + index);
        DetailsFragment details = (DetailsFragment) getFragmentManager().findFragmentById(R.id.flLandFragmentLayout);
        if (details == null || details.getShowIndex() != index) {
         details = DetailsFragment.newInstance(index);
         
         FragmentTransaction ft = getFragmentManager().beginTransaction();
         ft.replace(R.id.flLandFragmentLayout, details);
         ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
         ft.commit();
        }
       }else {
        Intent intent = new Intent();
        intent.setClass(getActivity(), DetailsActivity.class);
        intent.putExtra("index", index);
        startActivity(intent);
       }
    
      }
     }
     
     public static class DetailsFragment extends Fragment {
      
      public static DetailsFragment newInstance(int index){
       
       DetailsFragment f = new DetailsFragment();
       Bundle args = new Bundle();
       args.putInt("index", index);
       f.setArguments(args);
       return f;
      }
      
      @Override
      public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
       // TODO Auto-generated method stub
       if (container == null) {
        return null;
       }
       ScrollView scroller = new ScrollView(getActivity());
       TextView text = new TextView(getActivity());
       int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getActivity().getResources().getDisplayMetrics());
       text.setPadding(padding, padding, padding, padding);
       scroller.addView(text);
       text.setText(Shakespeare.DIALOGUE[getShowIndex()]);
       return scroller;
      }
      
      public int getShowIndex(){
       return getArguments().getInt("index", 0);
      }
     }
    }
    }
    

    API 소스중 App → Fragment → FragmentLayout 의 소스이며 약간 수정한 부분이 있습니다.
    수정한 부분은 API에서는 DetailsActivity에서 DetailsFragment를 새로 만들어서 사용하던것을 newInstance를 활용하는 방법을 사용 하였으며, 자동화면 전환 시 마지막 클릭한 정보를 Portrait 와 LandScape와 상관없이 알고 있다가  LandScape 화면 전화시 보여주게 하기 위하여 getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE), getListView().setItemChecked(index, true)의 위치를 수정 하였습니다.

    관련글