post list

2013년 2월 10일

[Android] Wi-Fi Direct


Android Wi-Fi Direct

 이번 포스트는 구글 Connecting with Wi-Fi Direct 에 있는 글을 그대로 번역한 것입니다. 여러분들에게 조금이라도 도움이 되길 바랍니다. (실은 내가 영어 다시 읽어보기 귀찮아서.. ㅋㅋ) 또한 구글에서 배포한 Wi-Fi Direct Example을 포스트 마지막에 올려두겠습니다. 내용과 코드를 참조하시면 많은 도움이 될껍니다. 그럼 시작할께요. 

 Wi-Fi Direct 는 네트워크나 핫스팟에 접속할 필요 없이 근처의 기계들과의 연결을 가능하게 해준다. 블루투스보다 먼 거리에서도 서로 연결할 수 있으며 빠르게 기계간의 상호 연결이 가능하다. 


 Permissions 설정하기

 먼저 Wi-Fi Direct 를 사용하기 위해서는 Manifest에 추가해야할 Permission이 있다. Wi-Fi Direct는 인터넷 연결을 요구하지는 않지만 표준 Java Socket을 사용하기 때문에 Internet Permission을 요구한다. 그래서 우리는 다음의 Permission들을 추가해줘야 한다.

    <uses-permission
        android:required="true"
        android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission
        android:required="true"
        android:name="android.permission.CHANGE_WIFI_STATE"/>
    <uses-permission
        android:required="true"
        android:name="android.permission.INTERNET"/>
    ...


 P2P Manager와 Broadcast Receiver 설정하기

  Wi-Fi Direct를 사용하기 위해서, 특정 이벤트가 일어났을 때 그 상황을 우리의 앱에게 말해줄 broadcast intent가 필요하다. 이것을 위해 우리 앱에 IntentFilter를 초기화시켜서 다음의 intent에 연결해줘야한다.

WIFI_P2P_STATE_CHANGED_ACTION
Indicates whether Wi-Fi Peer-To-Peer (P2P) is enabled

WIFI_P2P_PEERS_CHANGED_ACTION
Indicates that the available peer list has changed.

WIFI_P2P_CONNECTION_CHANGED_ACTION
Indicates the state of Wi-Fi P2P connectivity has changed.

WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
Indicates this device's configuration details have changed.

 각각이 뭘 하는지는 적혀 있으니 이제 실제 코드에서 어떻게 구현되는지 살펴보자.

private final IntentFilter intentFilter = new IntentFilter();
...
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    //  Indicates a change in the Wi-Fi Peer-to-Peer status.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);

    // Indicates a change in the list of available peers.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);

    // Indicates the state of Wi-Fi P2P connectivity has changed.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);

    // Indicates this device's details have changed.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);

    ...
}


 이제 onCreate method 마지막에 WifiP2pManager 인스턴스를 생성해보자. 또한 이 인스턴스의 initialize() method를 이용하면 WifiP2pManager.Channel 객체를 리턴받을 수 있다. 이 녀석은 나 중에 Wi-Fi Direct 프레임워크에 우리의 앱이 접속하도록 해준다.

@Override

Channel mChannel;

public void onCreate(Bundle savedInstanceState) {
    ....
    mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
    mChannel = mManager.initialize(this, getMainLooper(), null);
}
 자, 이제 System의 P2P 상태가 변하는 것을 알기 위해서 BroadcastReceiver 클래스를 생성하라. onReceive() 메서드에서 아래의 나열된 P2P 상태 변화를 핸들링하기 위한 코드를 추가하자.



@Override
public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
            // Determine if Wifi Direct mode is enabled or not, alert
            // the Activity.
            int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
            if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
                activity.setIsWifiP2pEnabled(true);
            } else {
                activity.setIsWifiP2pEnabled(false);
            }
        } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {

            // The peer list has changed!  We should probably do something about
            // that.

        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

            // Connection state changed!  We should probably do something about
            // that.

        } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
            DeviceListFragment fragment = (DeviceListFragment) activity.getFragmentManager()
                    .findFragmentById(R.id.frag_list);
            fragment.updateThisDevice((WifiP2pDevice) intent.getParcelableExtra(
                    WifiP2pManager.EXTRA_WIFI_P2P_DEVICE));

        }
    }
마지막으로 intent filter를 등록하기 위한 코드를 추가하고, 메인 액티비티가 활성화 될때 broadcast receiver를 등록하자. 그리고 그 액티비티가 멈추게 될 때(paused) 등록을 해제하도록 한다. 이것을 위해서 onResume(), onPause() 메서드를 이용하면 된다.

/** register the BroadcastReceiver with the intent values to be matched */
    @Override
    public void onResume() {
        super.onResume();
        receiver = new WiFiDirectBroadcastReceiver(mManager, mChannel, this);
        registerReceiver(receiver, intentFilter);
    }

    @Override
    public void onPause() {
        super.onPause();
        unregisterReceiver(receiver);
    }

 Peer (피어) 탐색 초기화하기

 Wi-Fi Direct를 이용해서 근처의 디바이스를 찾기 위해서는 discoverPeers()를 호출해야 한다. 이 메서드는 다음의 것들을 인자로 요구한다.

  • P2P Manager를 초기화했을 때 리턴받은 WiFiP2pManager.Channel
  • 디바이스 찾는 것을 성공했는지 못했는지를 의미하는 메서드를 포함하는 WifiP2pManager.ActionListener의 구현


mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() {

        @Override
        public void onSuccess() {
            // Code for when the discovery initiation is successful goes here.
            // No services have actually been discovered yet, so this method
            // can often be left blank.  Code for peer discovery goes in the
            // onReceive method, detailed below.
        }

        @Override
        public void onFailure(int reasonCode) {
            // Code for when the discovery initiation fails goes here.
            // Alert the user that something went wrong.
        }
});
 이 코드에서 명심해야 할 점이 있다. discoverPeers()는 오직 peer discovery만 초기화한다는 것이다. discoverPeers() 메서드는 탐색과정을 시작하고 즉각적으로 그 결과를 리턴한다. 이 시스템은 당신에게 Peer(피어) 탐색 과정이 성공적으로 시작되었는지 아닌지 알려준다. 이 과정은 제공된 ActionListener의 메서드를 호출함으로 이루어진다. 또한, 탐색은 접속이 성공적으로 초기화되던지 P2P group 이 형성이 될때까지 활성화 될 것이다.


 Peer(피어) 목록 가져오기

 자 이제 피어목록을 가져오고 처리하기 위한 코드를 작성해보자. 먼저 Wi-Fi Direct가 발견한 피어들에 대한 정보를 제공하는 WifiP2pManager.PeerListListener 인터페이스를 구현해야 한다. 다음의 코드를 보자.
private List peers = new ArrayList();
    ...

    private PeerListListener peerListListener = new PeerListListener() {
        @Override
        public void onPeersAvailable(WifiP2pDeviceList peerList) {

            // Out with the old, in with the new.
            peers.clear();
            peers.addAll(peerList.getDeviceList());

            // If an AdapterView is backed by this data, notify it
            // of the change.  For instance, if you have a ListView of available
            // peers, trigger an update.
            ((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged();
            if (peers.size() == 0) {
                Log.d(WiFiDirectActivity.TAG, "No devices found");
                return;
            }
        }
    }
 먼저 WIFI_P2P_PEERS_CHANGED_ACTION 을 가진 intent를 받았을 때 requestPeers() 메서드를 호출하기 위해서 broadcast receiver의 onReceive() method를 수정하자. 이제 우리는  이 listener를 receiver에게 어떻게든 전해줘야한다. 한가지 방법은 broadcast receiver의 생성자에게 인자로 이것을 넘겨주는 것이다.
public void onReceive(Context context, Intent intent) {
    ...
    else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {

        // Request available peers from the wifi p2p manager. This is an
        // asynchronous call and the calling activity is notified with a
        // callback on PeerListListener.onPeersAvailable()
        if (mManager != null) {
            mManager.requestPeers(mChannel, peerListListener);
        }
        Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
    }...
}
 이제, WIFI_P2P_PEERS_CHAGNED_ACTION 을 가진 intent는 업데이트된 피어 목록을 요청하게 될 것이다.


 Peer(피어)에게 접속하기

 피어에게 접속하기 위해서는 WifiP2pConfig 객체를 만들어야 한다. 그리고 우리가 접속하고자 하는 디바이스를 의미하는 WifiP2pDevice로부터 데이터를 얻어서 WifiP2pConfig 에 카피해 줘야 한다.


@Override
    public void connect() {
        // Picking the first device found on the network.
        WifiP2pDevice device = peers.get(0);

        WifiP2pConfig config = new WifiP2pConfig();
        config.deviceAddress = device.deviceAddress;
        config.wps.setup = WpsInfo.PBC;

        mManager.connect(mChannel, config, new ActionListener() {

            @Override
            public void onSuccess() {
                // WiFiDirectBroadcastReceiver will notify us. Ignore for now.
            }

            @Override
            public void onFailure(int reason) {
                Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }이 코드에 구현된 WifiP2pManager.ActionListener는 초기화(initiation)이 성공하거나 실패했을 때 당신에게 알려주는 기능만을 갖추고 있다. 그래서 접속 상태의 변화를 알고 싶다면 WifiP2pManager.ConnectionInfoListener 인터페이스를 구현해야한다. 이 인터페이스의 onConnectionInfoAvailable() 콜백은 접속 상태가 변화되었을 때 우리에게 알려줄 것이다. 만약에 여러대의 디바이스가 하나의 디바이스에 접속하고자 할 때에는 (3명이나 그 이상의 플레이어가 즐기는 게임이나 채팅 앱같은), 한 디바이스가 “그룹의 오너”(group owner)가 된다.

@Override
    public void onConnectionInfoAvailable(final WifiP2pInfo info) {

        // InetAddress from WifiP2pInfo struct.
        InetAddress groupOwnerAddress = info.groupOwnerAddress.getHostAddress());

        // After the group negotiation, we can determine the group owner.
        if (info.groupFormed && info.isGroupOwner) {
            // Do whatever tasks are specific to the group owner.
            // One common case is creating a server thread and accepting
            // incoming connections.
        } else if (info.groupFormed) {
            // The other device acts as the client. In this case,
            // you'll want to create a client thread that connects to the group
            // owner.
        }
    } 이번에는 broadcast receiver의 onReceive() 메서드로 돌아가서 WIFI_P2P_CONNECTION_CHANGED_ACTION intent 부분을 수정하자. 이 intent를 받았을 때 requestConnectionInfo()를 호출하도록 하자. 이것은 동기화적인 호출이기 때문에 호출결과는 우리가 파라미터로 제공한 connection info listener에 의해서 받게 될 것이다.

...
        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

            if (mManager == null) {
                return;
            }

            NetworkInfo networkInfo = (NetworkInfo) intent
                    .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);

            if (networkInfo.isConnected()) {

                // We are connected with the other device, request connection
                // info to find group owner IP

                mManager.requestConnectionInfo(mChannel, connectionListener);
            }
            ...


Download project source code

[Android] Surface View 에 움직이는 그림 그리기


Surface View 에서 터치에 의해 움직이는 물체 표현

 이번 포스트는 Surface View를 여러 Tile 로 쪼갠 다음에 도형들을 움직여보는 프로그램을 만들어 볼 것이다. 특히 다음의 주제를 중점으로 다룬다.  
  1. Tile 
  2. Map
  3. Rendering 
 이 부분은 Tiled based game이라고 하는 주제로 Google 검색을 통해 더욱 자세히 알 수 있다. 만약 모르는 부분이 생긴다면 검색을 하거나 댓글을 달아주시길 바란다. 코드는 포스트 제일 하단에 첨부해 두었으니 다운로드 받아서 분석해 보셔도 좋다.
 이 주제는 Tiled based game을 만드는 기초중에 기초를 설명하고 있다. 그런데 이 정보를 찾기가 조금 힘들었다. 이 내용이 많은 한국 분들에게 도움이 되길 바란다. 


 Tile

 먼저 Tile에 대하여 알아보자.
 여기서 Tile이란 Map을 이루는 한 단위이다. 즉, Tile 들이 모여서 Map을 완성시킨다. 본 프로그램에서는 하나의 Tile을 80 x  80 pixel로 정의했으며 2종류가 있다. 


 첫번째는 Black Tile이고 아무것도 없는 상태(Nothing)를 의미한다. 다음의 그림을 보자.



 두번째는 White Tile이고 물체로 존재(Is)를 의미한다.



 간단히 말해서 하얀색 Tile은 우리가 터치를 해서 움직일 수 있는 물체다. 검은색 Tile은 마치 배경처럼 보이게 된다. 
 이 Tile들은 코드를 보면 알겠지만 하나의 클래스(MAP_Tile)로 정의가 되어 있다. 이 클래스는 Member 변수로 흰색인지 검은색인지, 크기는 얼마인지, 자신의 현재 위치가 어디인지에 대한 정보들을 저장하고 있다.
 이 Tile들이 다음 목차에서 알아볼 Map의 기본 단위이다. 이것들이 모여서 Map을 이루게 되며 Map에서 다룰 내용이지만 이것들이 존재하는 곳 그렇지 않은 곳은 배열로서 미리 저장되어 있다. 


Map

 Map은 Tile들이 모여서 구성된다. Map 역시 코드를 보면 하나의 클래스(Map_Street_1)로 정의되어 있다. 여기서 Tile의 이미지를 메모리에 저장시킨다. 만약 화면에 Map을 뿌릴때마다 이미지를 불러오게 되면 아주 급격한 느림을 경험하게 된다. 이래서 미리 이미지를 불러오는 것이 중요하다. 

 Map은 초기에 화면이 구성되어 있어야 한다. 이 정보를 MAP_DATA라는 배열에 구현했다. 이 배열에는 오직 0 아니면 1인 값만 존재한다. 0인 값은 흑색타일이 자리 잡을 것이며, 1인 값은 흰색타일이 자리하게 될 것이다. MAP_DATA는 오직 초기 타일들의 구성에만 관심이 있을 뿐이다.
 실제 코드로는 다음과 같이 정의되어 있다.

        public int[][] MAP_DATA = { 
{ 1, 1, 1, 1, 1, 1 }, 
{ 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0 }, 
{ 0, 0, 0, 0, 0, 0 }, 
{ 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0 }, 
{ 0, 0, 0, 0, 0, 0 }, 
{ 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0 }, 
{ 1, 1, 1, 1, 1, 1 }, 
{ 0, 0, 0, 0, 0, 0 }, };


 다음의 그림은 이러한 초기 상태를 나타낸다. 아래 위로 흰색 타일을 배치했다. MAP_DATA의 첫번째 row의 값은 모두 1이라는 의미다. 맨 아래도 마찬가지다.




 실질적인 Map의 정보는 map 이라는 배열에 구현되어 있다. 이 배열은 타일의 개수만큼으로 2차원으로 구성되어 있다. 각 배열에는 TILE 객체가 들어간다. 그리고 Activity로부터 명령을 받아 Tile의 색상을 구분해서 boolean값을 돌려주거나, Tile의 색상을 변화시키는 등의 일을 수행하기도 한다.
 만약에 흰색을 터치하게 되면 그 자리의 타일은 검은색으로 변하게 된다. 그리고 드래그를 하는 위치를 따라서 흰색 타일이 그려지게 되며 손을 떼는 순간 그 자리에 흰색 타일이 다시 그려지게 된다. 물론 흰색 타일이 이미 존재할 때에는 본래 있었던 자리에 흰색 타일을 그려넣는다. 
 여기서 그린다 라는 의미는 map에서 그 좌표의 타일 색깔을 0(BLACK) 또는 1(WHITE)로 바꾼다는 의미다.


Rendering
 이 부분은 Surface View 및 그에 대응되는 Thread에 관한 내용이다. 이것에 대한 기초 내용은 이미 포스팅을 한 바 있으므로 자세한 설명은 생략한다. 물론 구글링을 통해서 쉽게 정보를 얻을 수도 있다.

 다음의 그림을 보자. 위에서 타일이 초기화 된 모습에서 실제로 몇몇 타일을 움직였을 때의 화면이다.



 보다 시피 일정한 흰색 타일의 개수를 유지하고 있으며 그 자리가 변경된 것을 알 수 있다. 여기서는 SurfaceView를 이용해서 Rendering을 하고 있다. 또한 Rendering을 하는 Thread를 새롭게 만들어서 지속적으로 화면을 update해준다. 본 프로그램에서는 MyThread라는 클래스로 정의되어 있다.
 많은 시행착오를 겪었는데 이 시행착오를 나열해 보면 다음과 같다.

1. 하나의 Surface View 에서 여러개의 Thread를 돌리는 것.
 배경화면을 그대로 계속 그려주는 Thread와 드래그를 할때 타일을 표현해주는 Thread를 따로 두려고 한 경우다. 그러나 Surface View에는 오직 하나의 Canvas가 있고 그것을 여러개의 Thread가 그려내는 것은 Canvas를 Thread 간에 공유를 해야하기 때문에 비효율적이라고 판단했다. 그래서 Surface View 하나에 Thread 하나를 접목시켰다.

2. 여러개의 Surface View 에서 각각 하나의 Thread를 돌리는 것.

 1번의 시행착오를 겪고 나서 생각했던 것은 Surface View를 2개를 만들어서 하나는 이미 존재하는 타일들을 그려주고 두번째 Surface View에서는 드래그 중인 타일을 그려주는 것을 생각해 보았다. 아마도 앞으로 Game 프로그래밍을 하다보면 이 방법을 자주 쓰게 될 것으로 보인다. 그러나 현재는 단지 드래그만 쓸 뿐이어서 이 방법은 나중에 필요할 때 구현할 것이다.

 그렇게 해서 현재 하나의 Surface View와 하나의 Thread가 본 프로그램을 구현하고 있다. 아주 단순한 방법이기 때문에 그다지 어렵지 않았다. 소스코드는 아래에 첨부해 놨다. 또한 코드 안에도 설명이 되어 있기 때문에 곰곰히 분석해 본다면 이해하는데 많은 시간이 소요되지 않을 것이다.