[Android] adb를 이용해 터치 이벤트 생성하기.

출처 : http://dry-kiss.blogspot.kr/2012/03/android-adb.html
0. 배경 지식
안드로이드의 사용자 이벤트 처리 과정은 크게, H/W(device) > Linux kernel > Android input system 정도의 3단계로 나눠 볼 수 있다.

터치스크린, 키보드, 센서 등의 입력 기기는 사용자의 실제 입력을 받게 되고, 이는 각 기기의 드라이버를 통해 Linux 커널에게 Linux 표준 방식으로 자신의 이벤트를 알리게 된다.
Linux 커널의 표준 방식이란, 널리 알려진 대로 기기 자신을 커널의 파일 시스템 상에 존재하는 하나의 파일로 추상화 시키고, 해당 기기를 사용하고자 하는 사용자는 해당 파일을 open 하고, read / write / ioctl 과 같은 표준 방식으로 조작하는 방식이다. 이때, 사용되는 각 이벤트의 데이터 형식은 Linux 표준을 따른다.

안드로이드 input system 은 EventHub 라는 모듈이 직접 각 입력 기기의 device file 에 접근, 이벤트를 읽어들이고, 이는 InputReader 라는 모듈에게 Linux kernel 표준 이벤트 형태로 전달된다.
이 이벤트는 InputReader 에서 Android 에서 사용하는 형식의 이벤트로 다듬어지고, Input dispatcher 라는 모듈에게 전달된다. Input dispatcher 는 현재 시스템에서 이벤트를 받아야 할 윈도우에게 Android 에서 사용하는 형식의 이벤트를 던져 준다.

안드로이드 기기는 자신의 파일시스템 상에 getevent, sendevent 라는 native 프로그램을 내장하고 있으며, adb shell 명령을 이용해 안드로이드 기기의 shell 에 접근 하면 두 프로그램을 실행 할 수 있다.

getevent 는 위에서 말한 input device file 들의 내용을 실시간으로 읽어서 표시해 주는 일을 하고, sendevent 는 사용자가 전달한 인자를 가지고 이벤트를 만들어 input device file 에 넣어준다. 즉, 사용자가 의도한 이벤트를 발생시킨다.
때문에, getevent 를 가지고 end-user 의 입력이 어떤 모습으로 커널에 전달되는지 확인 할 수 있고, 이를 분석함으로써 얻은 구조에 대한 이해를 가지고 sendevent 로 원하는 이벤트를 발생 시킬 수 있다.


1. 터치 이벤트 분석하기
# adb shell getevent
를 입력하면, 현재 adb 에 연결된 기기 또는 에뮬레이터의 입력 기기 목록과 해당 기기들의 device file 경로가 나오고, 이후 몇몇 기기의 이벤트가 계속해서 실시간으로 화면에 나타날 것이다.
다음은 Nexus One 을 연결하고 getevent 했을 때의 출력 내용이다.


$ adb shell getevent
add device 1: /dev/input/event6
  name:     "mahimahi-nav"
add device 2: /dev/input/event5
  name:     "mahimahi-keypad"
add device 3: /dev/input/event4
  name:     "proximity"
add device 4: /dev/input/event3
  name:     "synaptics-rmi-touchscreen"
add device 5: /dev/input/event2
  name:     "compass"
add device 6: /dev/input/event1
  name:     "h2w headset"
add device 7: /dev/input/event0
  name:     "lightsensor-level"
/dev/input/event2: 0003 0001 fffffd3e
/dev/input/event2: 0003 0002 00000008
/dev/input/event2: 0000 0000 00000000
/dev/input/event2: 0003 0000 ffffffef
/dev/input/event2: 0003 0002 0000000b
/dev/input/event2: 0000 0000 00000000
/dev/input/event2: 0003 0000 ffffffec
/dev/input/event2: 0000 0000 00000000
/dev/input/event2: 0003 0000 ffffffe9
/dev/input/event2: 0003 0001 fffffd41
/dev/input/event2: 0000 0000 00000000
/dev/input/event2: 0003 0001 fffffd3e
/dev/input/event2: 0000 0000 00000000
/dev/input/event2: 0003 0002 00000008
/dev/input/event2: 0000 0000 00000000
/dev/input/event2: 0003 0000 ffffffec
/dev/input/event2: 0003 0001 fffffd3b
/dev/input/event2: 0000 0000 00000000
/dev/input/event2: 0003 0000 ffffffe9


아무짓도 하지 않고 있는데 계속 이벤트를 뿌려주는 입력 기기는 대부분 light sensor 등과 같은 센서들이다.
여기서 직접 화면에 손가락을 대서 터치 이벤트를 발생시키면, 그때만 이벤트를 뿌리는 device file 이 있을 것이다. 그게 터치스크린 device file 이다.
꼭 그러지 않더라도, 최초의 기기 목록의 이름을 가지고도 어느정도 짐작은 할 수 있다. 위의 출력 내용에서는 /dev/input/event3 가 터치스크린일 것임을 짐작 할 수 있다.

이벤트의 내용은 좌측부터 순서대로 device file path, type, code, value 이다.

위의 명령은 light sensor 등과 같은 필요치 않은 기기의 이벤트를 뿌려 정신을 산만하게 만드니까 grep 명령 등을 이용하면 좋다.

이제, getevent 를 통해 터치스크린의 이벤트에 집중하면서 짧게 화면을 터치해 보자.
다음과 같은 이벤트 내용을 얻을 수 있을 것이다.


$ adb shell getevent | grep event0
add device 7: /dev/input/event0
/dev/input/event0: 0003 0035 0000035d
/dev/input/event0: 0003 0036 000000e0
/dev/input/event0: 0003 003a 00000044
/dev/input/event0: 0003 0030 00000007
/dev/input/event0: 0003 0039 00000000
/dev/input/event0: 0000 0002 00000000
/dev/input/event0: 0000 0000 00000000
/dev/input/event0: 0000 0002 00000000
/dev/input/event0: 0000 0000 00000000



이번엔 터치 위치를 좌우로 또는 아래로 약간 움직여 한번 더 터치해 보자.
여기선 x축 좌표는 거의 그대로 두고, 꽤 아래 쪽을 터치했다.

$ adb shell getevent | grep event0
add device 7: /dev/input/event0
/dev/input/event0: 0003 0035 0000035a
/dev/input/event0: 0003 0036 00000286
/dev/input/event0: 0003 003a 00000037
/dev/input/event0: 0003 0030 00000007
/dev/input/event0: 0003 0039 00000000
/dev/input/event0: 0000 0002 00000000
/dev/input/event0: 0000 0000 00000000
/dev/input/event0: 0000 0002 00000000
/dev/input/event0: 0000 0000 00000000




참고로, getevent 가 표시해 주는 값들은 16진수다. 10진수로 바꿔주면 가지고 있는 기기의 해상도와 터치한 손가락의 위치를 바탕으로 보다 쉽게 어느 type / code 가 어느 값을 의미하는지 알 수 있다.
보면 중복되는 값들이 있고, x/y축의 이동을 생각해 보면, 최초 두개의 이벤트는 x/y 축을 의미하며, 세번째는 터치 압력을 의미한다. 나머지는 두 이벤트가 똑같다.

이제, 위와 같은 내용의 이벤트를 x / y 축의 값만 바꿔 입력하면, 원하는 위치에 터치 이벤트를 만들어 줄 수 있다.
setevent 는 10진수를 입력받으므로, 위의 값들은 10진수로 변환해야 한다.
파이썬을 사용한다면 다음과 같은 스크립트를 만들 수 있을 것이다.


os.system("adb shell sendevent /dev/input/event1 3 53 " + x_axis)
os.system("adb shell sendevent /dev/input/event1 3 54 " + y_axis)
os.system("adb shell sendevent /dev/input/event1 3 58 55")
os.system("adb shell sendevent /dev/input/event1 3 48 7")
os.system("adb shell sendevent /dev/input/event1 3 57 0")
os.system("adb shell sendevent /dev/input/event1 0 2 0")
os.system("adb shell sendevent /dev/input/event1 0 0 0")
os.system("adb shell sendevent /dev/input/event1 0 2 0")
os.system("adb shell sendevent /dev/input/event1 0 0 0")

추가 내용
http://source.android.com/tech/input/getevent.html

항상 최상위에 나오는 뷰 만들기2 (팝업 비디오 + Q슬라이드)

[출처 : http://blog.daum.net/mailss/35]

이전에 쓴 글 '항상 최상위에 나오는 뷰 만들기'는 뷰는 나오지만 터치 이벤트를 받지 못했다. 터치 이벤트를 받더라도 ACTION_OUTSIDE 이벤트만 받을 수 있었다.


이제는 그냥 최상위 뷰만 나오게 하는 것이 아니라 뷰를 최상위에 나오게 하면서 모든 터치 이벤트를 받아보자. 터치로 뷰를 이동해보고(갤럭시의 팝업 비디오 처럼) 투명도를 조절해보자!!(옵티머스의 Q슬라이드)


이전에 쓴 '항상 최상위에 나오는 뷰 만들기' 와 방식은 같다.

1. 최상위에 나오게 하기 위해서는 Window에 뷰는 넣는다.

2. 다른 화면에서도 나오게 하기위해서는 서비스에서 뷰를 생성하여야 한다.

3. 뷰에 들어오는 터치 이벤트를 OnTouchListener를 통해서 받는다.


1. 서비스 생성

자신의 앱이 종료된 후에도 항상 해당 뷰가 떠 있어야 한다. 그래서 Activity에서 뷰를 추가하는 것이 아니라 Service에서 뷰를 추가 해야 한다.


AlwaysOnTopService.java

public class AlwaysOnTopService extends Service {
    @Override
    public IBinder onBind(Intent arg0) { return null; }
    
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}


2. 뷰 생성 및 최상위 윈도우에 추가

간단하게 텍스트뷰 하나 추가하는 코드이다.

    private TextView mPopupView;                            //항상 보이게 할 뷰
    private WindowManager.LayoutParams mParams;  //layout params 객체. 뷰의 위치 및 크기
    private WindowManager mWindowManager;          //윈도우 매니저

    @Override
    public void onCreate() {
        super.onCreate();

        mPopupView = new TextView(this);                                         //뷰 생성
        mPopupView.setText("이 뷰는 항상 위에 있다.");                        //텍스트 설정
        mPopupView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18); //텍스트 크기 18sp
        mPopupView.setTextColor(Color.BLUE);                                  //글자 색상
        mPopupView.setBackgroundColor(Color.argb(127, 0, 255, 255)); //텍스트뷰 배경 색

        //최상위 윈도우에 넣기 위한 설정
        mParams = new WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.TYPE_PHONE,//항상 최 상위. 터치 이벤트 받을 수 있음.
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,  //포커스를 가지지 않음
            PixelFormat.TRANSLUCENT);                                        //투명
        mParams.gravity = Gravity.LEFT | Gravity.TOP;                   //왼쪽 상단에 위치하게 함.
        
        mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);  //윈도우 매니저
        mWindowManager.addView(mPopupView, mParams);      //윈도우에 뷰 넣기. permission 필요.
    }


 이전 글에서는 TYPE을 TYPE_SYSTEM_OVERLAY로 주었다. 이러면 화면 전체를 대상으로 뷰를 넣지만 터치 이벤트를 받지는 못한다.

 하지만 TYPE을 TYPE_PHONE으로 설정하면 터치 이벤트를 받을 수 있다. 하지만 Status bar 밑으로만 활용가능하고 뷰가 Focus를 가지고 있어 원래 의도대로 뷰 이외의 부분에 터치를 하면 다른 앱이 터치를 사용해야 하는데 이것이 불가능 하고 키 이벤트 까지 먹어 버린다.

 이것을 해결하기 위해 FLAG 값으로 FLAG_NOT_FOCUSABLE을 주면 뷰가 포커스를 가지지 않아 뷰 이외의 부분의 터치 이벤트와 키 이벤트를 먹지 않아서 자연스럽게 동작할 수 있게 된다.


3. 매니페스트에 퍼미션 설정

WinodwManager에 addView 메소드를 사용하려면 android.permission.SYSTEM_ALERT_WINDOW 퍼미션이 필요하다.


<manifest  ................ >
    <application ................ >
        <activity
           ................
        </activity>
        <service 
           ................
        </service>
    </application>
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    <uses-sdk android:minSdkVersion="7" />
</manifest>


이전 글에는 service 태그 안에 퍼미션을 주라고 했지만 service에 주지 않아도 된다. 그냥 uses-permission을 주면 된다.


4. 터치 이벤트 받기

뷰에 터치 리스너를 등록하면 터치 이벤트를 받을 수 있다.


mPopupView.setOnTouchListener(mViewTouchListener);              //팝업뷰에 터치 리스너 등록


private OnTouchListener mViewTouchListener = new OnTouchListener() {
    @Override public boolean onTouch(View v, MotionEvent event) {
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:                //사용자 터치 다운이면
                if(MAX_X == -1)
                    setMaxPosition();
                START_X = event.getRawX();                    //터치 시작 점
                START_Y = event.getRawY();                    //터치 시작 점
                PREV_X = mParams.x;                            //뷰의 시작 점
                PREV_Y = mParams.y;                            //뷰의 시작 점
                break;
            case MotionEvent.ACTION_MOVE:
                int x = (int)(event.getRawX() - START_X);    //이동한 거리
                int y = (int)(event.getRawY() - START_Y);    //이동한 거리
                
                //터치해서 이동한 만큼 이동 시킨다
                mParams.x = PREV_X + x;
                mParams.y = PREV_Y + y;
                
                optimizePosition();        //뷰의 위치 최적화
                mWindowManager.updateViewLayout(mPopupView, mParams);    //뷰 업데이트
                break;
        }
        
        return true;
    }
};


터치로 뷰를 이동하거나 크기 조절을 하려면 WindowManager.LayoutParams 객체의 값을 변경해 주면 된다. x, y는 뷰의 시작점 좌표이다. Q슬라이드 처럼 투명도 조절은alpha값을 변경하면 된다. 0~1사의 값을 넣어 주면 된다.

이렇게 WindowManager.LayoutParams의 값을 변경해준 다음 WindowManager의 updateViewLayout메소드를 사용하여 뷰의 변경사항을 적용한다.



5. 뷰 제거

서비스 종료시 뷰를 제거 해야 한다.

    @Override
    public void onDestroy() {
        if(mWindowManager != null) {        //서비스 종료시 뷰 제거. *중요 : 뷰를 꼭 제거 해야함.
            if(mPopupView != null) mWindowManager.removeView(mPopupView);
            if(mSeekBar != null) mWindowManager.removeView(mSeekBar);
        }
        super.onDestroy();
    }


6. 서비스 실행/중지 할 activity 만들기

AlwaysOnTopActivity.java


public class AlwaysOnTopActivity extends Activity implements OnClickListener {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        findViewById(R.id.start).setOnClickListener(this);        //시작버튼
        findViewById(R.id.end).setOnClickListener(this);            //중시버튼
    }
    
    @Override
    public void onClick(View v) {
        int view = v.getId();
        if(view == R.id.start)
            startService(new Intent(this, AlwaysOnTopService.class));    //서비스 시작
        else
            stopService(new Intent(this, AlwaysOnTopService.class));    //서비스 종료
    }
}


실행 결과


 앱 시작뷰 추가 바탕화면 (위치 이동)

 동영상 재생
 Dragon Flight 게임
인터넷 (투명값 변경)




 AlwaysOnTop.zip

전체 샘플 코드 첨부하였습니다.

1.     Multipart Message 종류

Message

정의

Mixed

다른 Content-type 헤더를 갖는 파일을 전송하는데 사용.

메일 클라이언트가 읽기 쉬운 포맷일 경우는 바로 표시고, 그렇지 않으면 첨부파일 형태로 표시.

메일 클라이언트에서 지원해야 할 MIME typetext/plain

Digest

하나 이상의 메시지 포워드시 사용

Message/rfc822

메일 클라이언트에서 지원해야 할 MIME type.

Alternative

동일한 내용을 다른 형식으로 표현 할 때 사용.

Related

상호 연관된 여러 개의 파트로 구성.

HTML 같은 문서에서 이미지 참조 할 때 뒤따르는 파트를 내부에서 참조.

Report

이 메시지 타입은 text/plain message/delivery-status로 나뉨.

Signed

본문과 서명 포함을 뜻함.

Encypted

메시지 타입 암호화

Application/octet-stream와 이를 풀기 위한 제어파트로 구분.

Form-data

파일, 비아스키, 데이터, 이진수 데이터 등을 전송할 때 사용.

 

2.     Content-type : multipart/form-data를 이용한 파일 전송.

장점 : GET POST 보다 더 많은 데이터를 전송할 수 있고, 여러 개의 파일을 한꺼번에 보낼 수 있음.

조건 : multipart/form-data로 전송할 시에는 boundary를 지정해 주어야 함.

      Boundary는 지정된 규칙은 존재하지 않음. 단지 맨 마지막 부분에 [Boundary ]—식으로 하이픈[-]

      두 개 넣어줘야 함.

헤더 구성

è  Content-type: multipart/form-data, boundary=12345 [Boundary ]

바디 구성

è  --12345 [Boundary]

è  Content-Diposition:form-data; name=”abc” [파라미터 이름]

è  0000 [파라미터 값]

è  --12345 [Boundary]

è  Content-Diposition:form-data; name=”attach_file_name” [파라미터 이름]; filename=”c:\test\123.jpg” [파일 경로]

è  Content-type: image/jpeg

è  Content-Transfer-Encoding : binary

è  ….[파일 바이너리]

è  --12345 [Boundary]--

 

Body에서의 각각의 파라미터에서의 Content-Disposition Content-type은 헤더 부분과 바디 부분으로 작게 나눠지게 됨.[헤더부분은 파라미터의 속성을 뜻하고, 바디 부분은 데이터의 유형을 나타냄.]

 

Content-Transfer-EncodingDefault Encoding(7BIT)과 일치하지 않으면 헤더에서 제공되어야 함.

종류 : 7bit, 8bit, binary, quoted-printable, base64, ietf-token, x-token

참조 : [RFC2045 http://www.ietf.org/rfc/rfc2045.txt]

 

Content-disposition : 컨텐트 타입의 옵션이기도 하면서, 실제로 지정된 파일명을 지정함으로써 더 자세한

 파일의 속성을 알려줄 수 있음.

disposition := "Content-Disposition" ":"

                    disposition-type

                    *(";" disposition-parm)

 

     disposition-type := "inline"

                       / "attachment"

                       / extension-token

                       ; values are not case-sensitive

 

     disposition-parm := filename-parm

                       / creation-date-parm

                       / modification-date-parm

                       / read-date-parm

                       / size-parm

                       / parameter

 

     filename-parm := "filename" "=" value

 

     creation-date-parm := "creation-date" "=" quoted-date-time

 

     modification-date-parm := "modification-date" "=" quoted-date-time

 

     read-date-parm := "read-date" "=" quoted-date-time

 

     size-parm := "size" "=" 1*DIGIT

 

     quoted-date-time := quoted-string

                      ; contents MUST be an RFC 822 `date-time'

                      ; numeric timezones (+HHMM or -HHMM) MUST be used

   Inline : 브라우저 인식 파일 확장자를 가진 파일들에 대해서는 웹브라우저 상에서 바로 파일을 자동으로

보여줄 수 있어서 의미상인 멀티파트 메시지를 표현하는데 있음.

그 외의 파일들에 대해서는 파일 다운로드 대화상자가 나타나도록 하는 헤더 속성임.

Attachment : 브라우저 인식 파일 확장자 구분 없이 모든 파일이 파일 다운로드 시 파일 다운로드

대화상자가 나타나도록 하는 헤더 속성 임.

참조 : [RFC2183 http://www.ietf.org/rfc/rfc2183.txt]

 

  


 

3.     Content-type/mixed

헤더 구성

è  Content-type: multipart/form-data, boundary=12345 [Boundary ]

바디 구성

è  --12345 [Boundary]

è  Content-Diposition:form-data; name=”abc” [파라미터 이름]

è  0000 [파라미터 값]

è  --12345 [Boundary]

è  Content-Diposition:form-data; name=”files”

è  Content-type:multipart/mixed; boundary=abcde [Boundary]

è  -- abcde [Boundary]

è  Content-Diposition: attachment; filename=”c:\test\123.jpg” [파일 경로]

è  Content-type: image/jpeg

è  ….[파일 바이너리]

è  -- abcde [Boundary]

è  Content-Diposition: attachment; filename=”c:\test\456.jpg” [파일 경로]

è  Content-type: image/jpeg

è  ….[파일 바이너리]

è  -- abcde [Boundary]--

è  --12345 [Boundary]--

 

+ Recent posts