소곤소곤 ad

2015년 2월 24일 화요일

카카오(Kakao) SDK 설치시에 발생하는 오류들 (Eclipse/Android)

카카오(Kakao, 카카오톡, 카톡, Kakaotalk) SDK 설치시에 발생하는 오류들 (Eclipse/Android)


카카오는 여러가지 API들을 공개하고 있는데, https://developers.kakao.com/ 를 방문하면 자세한 정보를 얻을 수 있다. 그런데 여기서 제공하는 SDK를 그냥 설치하면 에러가 쏟아진다. 여기저기를 검색해봐도 이 오류들에 대한 설명도 해결방법도 찾기가 힘들었다.






여러군데서 오류가 발생한다. 오류가 발생한 부분의 소스코드를 살펴봐도 왜인지 잘 모르겠다. 

자, 심호흡을 한번 하고, 이 오류들을 싸그리 없애보자!


1. google_play_service_lib 라이브러리 위치



첫번째 할 일은, google_play_service_lib 의 위치를 제대로 지정해 주는 것이다. 빨갛게 X표시된 것은 카카오 SDK를 만드신분의 개발환경으로 이 라이브러리의 경로가 지정되어 있기 때문에 문제가 생겼음을 알 수 있다. 이것들을 제거하고, 내 개발환경의 google_play_service_lib 프로젝트를 지정해주면 해결. (이 라이브러리를 설치하는 방법에 대한 것은 이 글의 범위를 벗어난다)




2. Eclipse의 TextEncoding 문제

여전히 소스코드 여러곳에서 에러가 발생하는데, 이번 문제는 애매한 코드영역에서 발생한다. 이 문제의 주요한 원인은, 카카오 SDK를 만드신분이 UTF-8코드의 한글로 주석을 달아서이다.  필요충분조건인 또하나의 원인은, 내 개발환경이 UTF-8이 아니기 때문이다. 




UTF-8로 작성된 주석들이 윈도우개발환경에서 코드페이지가 맞지 않는 경우에는 코드들과 섞여버린다. 즉, 주석 다음줄의 코드까지 주석처리되어 버려서 오류들이 발생하는 것.

문제 해결하는 방법은 둘 중 하나이다. 

1) 내 개발환경을 모두 UTF-8로 바꾼다. 


Window - Preference - General - Content Types - Text - Java Source File 을 찾아서 맨 아래 Default encoding 에 "UTF-8"로 적어주고 업데이트 하면 된다. 아니면 더 쉬운 방법이 있다! 그것은 바로... 


2) 카카오 SDK 소스코드내의 한글 주석을 영문으로 바꾸던가 지워버린다. 

(오류나는 부분)

(수정된 소스)

엔터 하나 때려넣고 수정 완료! -_-v


3. WebView와 Android 버전문제


SDK에 포함된 WebView의 일부 기능들은 안드로이드 API 버전 "11" 부터 지원하는데, Kakao SDK의 AndroidManifest.xml에는 

minSdkVersion="10" 

으로 되어 있다. 11로 바꿔준다. 이게 문제가 된다면 빌드가 끝나고 나중에 나중에 슬그머니 10으로 바꿔 놓으면 된다. 카카오 개발자도 이렇게 하지 않았을까 싶다 ㅎㅎ


4. PowerManger.isInteractive() 함수


이 오류가 생긴 이유는 카카오 SDK개발자님이 최신기능을 사용할수 있다고 뽐내려고 했기 때문으로 추정된다. 이 함수는 Android SDK 버전 "20" (롤리팝)부터 지원하는 신기능이다. 킷캣 이전버전에서는 지원하지 않아 오류가 생긴다. sdk의 Build Target을 20이상으로 해줘야 한다. 그런데 그래봤자 구버전에서는 isScreenOn()을 사용하도록 코딩되어 있으므로, 아예 이 부분을 삭제해 버리는것이 낫겠다는 판단. 

(롤리팝의 이 기능을 사용하기 위해 앱빌드 타겟을 20으로 강제하는것보다는, 타겟을 19이하로 하게 내버려두고 동작은 그냥 운영체제에 맡기는게 낫지 않았을까?)

 private boolean isApplicationActive(Context context) {
        try {
            final PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            return powerManager.isScreenOn();

            /*
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
                return powerManager.isScreenOn();
            } else {
                return powerManager.isInteractive();
            }
            */
        } catch (Exception e) {
            // nothing to do;
        }

        return true;
    }

이렇게 수정하여 드디어, 오류없이 컴파일 성공!! 야호~


HttpURLConnection을 이용하여 간단하게 이미지 다운로드받기,Example for Downloading an image with HttpURLConnection (no Apache)

HttpURLConnection을 이용하여 간단하게 이미지 다운로드받기 Example for Downloading an image with HttpURLConnection (no Apache)


실시간으로 웹서버에서 이미지를 가져다가 출력할 필요가 종종 있다. WebView나 HttpGet등의 고차원적인 기능의 라이브러리를 이용할 수도 있겠지만, 복잡한 옵션없이 아주 간단하게 이미지만 가져오고 싶다면?

정보의 바다에서 쉽게 샘플 소스를 구할 수 있을것 같았지만, 제대로 문제없이 동작하는 샘플을 구할 수가 없었다. 여러가지 제한사항들이 있는데...

1) 크로스플랫폼에서 (나 같은경우는 Android + Windows Desktop) 동작해야 한다.
2) java.* 이외의 외부라이브러리를 가급적 사용하지 않고 구현해야 한다 - 여러가지 이유가 있지만, 순수 java라이브러리만으로 가볍게 구현하고 싶었다.

기본적인 구현은 아래와 같다.

 static private boolean getHttpFile(String url, String fileName) throws MalformedURLException, IOException
 {
  HttpURLConnection conn;

  conn = (HttpURLConnection) new URL( url ).openConnection();
  
  conn.setConnectTimeout( timeOut );
  conn.setReadTimeout( timeOut );
// oops! conn.setDoOutput(true);
  conn.setUseCaches(true);
  conn.setRequestMethod( "GET" );

  conn.setRequestProperty("Connection", "Keep-Alive" );
  conn.setRequestProperty("Content-Type", "image/png");
//  conn.setRequestProperty("Content-Type","application/x-www-form-urlendcoded");
  
  conn.connect();
  
  int response = conn.getResponseCode(); // System.out.println(String.format("HTTP_RESPONSE:%s CODE:%d TYPE:%s SIZE:%d", url, response, conn.getContentType(), conn.getContentLength()));
  if( response == HttpURLConnection.HTTP_OK ){
   InputStream is = conn.getInputStream();
   FileOutputStream outputStream = new FileOutputStream(fileName);
   
   if ("gzip".equals(conn.getContentEncoding())){
    System.out.println("zipped image");
       is = new GZIPInputStream(is);
   }
   
                        // opens an output stream to save into file
   int bytesRead = -1;
                        byte[] buffer = new byte[2048];
                        while ((bytesRead = is.read(buffer)) != -1) {
                            outputStream.write(buffer, 0, bytesRead);
                        }
                        is.close();
                        outputStream.close();
            
                        return true;
  } 
  return false;
 }


setDoOutput(true)
일단, HttpURLConnection.setDoOutput(true);를 절대 하면 안된다. 안드로이드 4.0 이후부터는 이것이 true인 경우 method가 강제적으로 "POST"방식으로 변경되어져서 결과적으로는 데이터를 가져올 수 없고, HTTP/405 Error 를 만나게 된다.

getInputStream()의 함정
인터넷에서 구하는 샘플소스들은

  if( response == HttpURLConnection.HTTP_OK ){ 
                    return conn.getInputStream();
                }

이렇게 InputStream을 돌려주면서 끝나는데, 사실 InputStream은 그 끝을 알 수 없는 파이프의 한쪽만을 보여주는 형태라서, 파일사이즈가 좀 커지면 다 받지 않았는데도 리턴한다. 가볍게 생각하고 InputStream으로부터 이미지를 저장해보면 난리가 난다.

여기에서는 InputStream이 종료될때까지 2k씩 계속 받아다가 파일로 저장한다. 메모리를 더 써도 된다면 팍팍 읽어다가 팍팍 저장!


사용하기, 그리고 예외처리들

 static public Texture getHttpTexture(String fileId, FileHandle handle)
 {
  String url = "http://xxxx.com/images/" + fileId+ ".png";
  
  try {
   if (getHttpFile(url, handle.path()) ){
    try {
     Texture td = new Texture(handle);
     td.setFilter(TextureFilter.Linear,  TextureFilter.Linear);
     return td;
    } catch ( GdxRuntimeException e){
     e.printStackTrace();
    }
   }
  } catch (MalformedURLException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  return null;
 }

이 함수는 libGDX 엔진기반의 게임개발에서 사용하는 실제 코드이다. 정해진 웹서버에서 fileID 형태의 png 파일을 다운로드 받고, 이를 이용하여 Texture를 생성한다.


Thread로 다운로드받고 이미지 그리기

마지막으로 큰 걸림돌인데, 여태까지 했던 작업들은 모두 별도 Thread 로 돌아야 한다. Activity.runOnUIThread()에 넣거나, View.postDelayed()를 이용해서 다운로드받고 UI를 갱신해주면 된다. 실제로 우리가 만들고 있는 게임에서는 libGDX의 postRunnable를 이용해서 오래걸리는 작업을 처리하도록 했다.

   Gdx.app.postRunnable(new Runnable(){
    @Override
    public void run() {
     Texture tx = HttpGetImage.getHttpImage(fileID, fileHandle);
     TextureRegionDrawable trd = new TextureRegionDrawable(new TextureRegion(tx));
     imageSample.setDrawable(trd);
    }
   });

2015년 1월 13일 화요일

안드로이드 서버 클라이언트 소켓 통신 예제 (Android Java TCP/IP Client/Server Socket Example)

안드로이드 서버 클라이언트 소켓 통신 예제 (Android Java TCP/IP Client/Server Socket Communication Example) 


간단한 통신 프로그램을 짜보려고 인터넷을 다 뒤져봤지만, 제대로 구현된 예제 프로그램을 찾기가 어려웠다.

단순한 에코 프로그램 (echo server) 정도의 구현은 아주 쉽게 작성이 가능하지만, 화면 인터페이스 (UI)에 맞게 사용자가 입력한 내용을 전송한다거나, 소켓에서 받은 데이터로 TextView에라도 표시하려 하면 여러가지 문제에 봉착하고 만다. 실전에 사용하려 하면 어떤 문제들이 생길까? 왜 이런 문제들을 해결해 놓은 예제 프로그램은 찾을 수 없는 것일까?


A. 규칙! 이제 안드로이드의 모든 Network Function은 Main Thread (UI Thread)에서 실행하면 안된다. 


E/AndroidRuntime(673): java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.example/com.example.ExampleActivity}:android.os.NetworkOnMainThreadException

별 생각없이 Socket 함수들을 호출하면 이런 에러를 만나고 만다. 메인 쓰레드에서 네트워크기능을 사용할 수 없다는 뜻이다. 모든 네트워크 기능은 별도의 Thread에서 처리해야 한다. 하지만 여기서부가 또다른 골치아픈 문제의 시작점인것을...


B. 규칙! 메인 쓰레드 (Main Thread = UI Thread)가 아니면 UI를 변경해서는 안된다. 


내가 만든 쓰레드에서 직접 UI의 내용변경을 하면 안되고, 반드시 메인쓰레드가 처리하도록 요청하여야 한다. 이것을 구현하는 방법은 여러가지가 있는데,

a. activity.runOnUiThread()를 이용하는 방법
b. view.post()를 이용하는 방법
c. AsyncTask를 이용하는 방법
d. Message Handler를 이용하는 방법
e. ...

보통의 프로그램이라면 취향껏 고르면 될 일이다. 하지만 소켓 통신 프로그램은 그렇지 못하다!!

왜냐면, 소켓이 이미 하나의 Thread이고, 소켓에서 들어온 데이터를 계속 처리해야 하기 때문에 이 녀석 역시 Thread일 수 밖에 없기 때문이다. 그래서 규칙A와 B가 서로를 방해하는 형태로 계속 꼬여 버린다.

삽질끝에 얻은 가장 단순한 처리는 모든 소켓처리를 1개의 Thread에서 처리하고, UI 와의 통신은 Message Handler를 이용하는 것.

이렇게 구현된 SimpleSocket 클래스는 정말 아주 심플하다!!



import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;

import android.os.Handler;
import android.os.Message;
import android.util.Log;

public class SimpleSocket extends Thread {
	private Socket  mSocket;

	private BufferedReader buffRecv;
	private BufferedWriter buffSend;

	private String  mAddr = "localhost";
	private int     mPort = 8080;
	private boolean mConnected = false;
	private Handler mHandler = null;

	static class MessageTypeClass {
		public static final int SIMSOCK_CONNECTED = 1;
		public static final int SIMSOCK_DATA = 2;
		public static final int SIMSOCK_DISCONNECTED = 3;
	};
	public enum MessageType { SIMSOCK_CONNECTED, SIMSOCK_DATA, SIMSOCK_DISCONNECTED };

	public SimpleSocket(String addr, int port, Handler handler) 
	{
		mAddr = addr;
		mPort = port;
		mHandler = handler;
	}

	private void makeMessage(MessageType what, Object obj)
	{
		Message msg = Message.obtain(); 
		msg.what = what.ordinal();
		msg.obj  = obj;
		mHandler.sendMessage(msg);
	}

	private boolean connect (String addr, int port) 
	{
		try {
			InetSocketAddress socketAddress  = new InetSocketAddress (InetAddress.getByName(addr), port); 
			mSocket = new Socket();
			mSocket.connect(socketAddress, 5000);
		} catch (IOException e) {
			System.out.println(e);
			e.printStackTrace();
			return false;
		}
		return true;
	} 

	@Override
	public void run() {
		if(! connect(mAddr, mPort)) return; // connect failed
		if(mSocket == null)         return;

		try {
			buffRecv = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
			buffSend = new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream()));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		mConnected = true;

		makeMessage(MessageType.SIMSOCK_CONNECTED, "");
		Log.d("SimpleSocket", "socket_thread loop started");

		String aLine = null;

		while( ! Thread.interrupted() ){ try {
			aLine = buffRecv.readLine(); 
			if(aLine != null) makeMessage(MessageType.SIMSOCK_DATA, aLine);
			else break;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}}

		makeMessage(MessageType.SIMSOCK_DISCONNECTED, "");
		Log.d("SimpleSocket", "socket_thread loop terminated");

		try {
			buffRecv.close(); 
			buffSend.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		mConnected = false;
	}

	synchronized public boolean isConnected(){
		return mConnected;
	}

	public void sendString(String str){
		PrintWriter out = new PrintWriter(buffSend, true);
		out.println(str);
	}
}





이 클래스를 사용하려면, 메세지를 처리할 Handler를 하나 구현하고, SimpleSocket을 생성한다음 start()만 해주면 된다. Looper.getMainLooper() 덕택에 UI 변경은 모두 메인쓰레드에서 처리할 수 있다.




public class ChatMain extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.chat_main);

		...
		ed1 = (EditText) findViewById(R.id.editText1);
		Button buttonSend = (Button) findViewById(R.id.button1);

		buttonSend.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				ssocket.sendString(ed1.getText());
				ed1.setText("");
			}
		});

		Handler mHandler = new Handler(Looper.getMainLooper()) {
			@Override
			public void handleMessage(Message inputMessage) {
				switch(inputMessage.what){
				case SimpleSocket.MessageType.SIMSOCK_DATA : 
					String msg = (String) inputMessage.obj;
					Log.d("OUT",  msg);
					// do something with UI
					break;

				case SimpleSocket.MessageType.SIMSOCK_CONNECTED : 
					// do something with UI
					break;

				case SimpleSocket.MessageType.SIMSOCK_DISCONNECTED : 
					// do something with UI
					break;

				}
			}    
		};  

		ssocket = new SimpleSocket("192.168.0.1", 8080, mHandler);
		ssocket.start();

		...
이방식으로 구현해놓으면, 추가로 Thread를 만들지 않고서도 Socket Thread 하나로 모든 소켓통신과 UI 변경을 처리할 수 있다.