ADB 핵심 구성 마인드 맵
설명
안녕하세요! 오늘은 ADB(Android Debug Bridge)를 사용하여 PC에서 안드로이드 기기를 자동화하는 방법에 대해 간단히 알아보겠습니다.
1. 준비 단계
- 안드로이드에서 개발자 모드를 활성화
- 안드로이드 기기에서 개발자 옵션 및 USB 디버깅 활성화
- 안드로이드와 PC를 USB연결 ( 무선 방식도 있으나 별도 SDK가 필요해서 생략)
-윈도우에 설치된 "휴대폰과 연결" 앱을 실행시켜서 PC에서 휴대폰 미러링
- Android SDK Platform-Tools 설치
- ADB SDK 설치 후 설치된 경로를 환경변수(PATH)에 등록
- USB 케이블로 PC와 안드로이드 기기 연결
- 한글 Send 시 별도의 SDK가 필요함 (https://github.com/senzhk/ADBKeyBoard)
* 하기 포스팅 내용을 참고하시면 편합니다
https://realnotepad.tistory.com/23
ADB 기본 세팅 방법(안드로이드 개발자모드 방법, PC에서 환경변수 등록 방법)
휴대폰 - 개발자 모드 활성화 먼저 휴대폰에서 개발자 모드를 활성화 시키는 절차입니다미리 휴대폰과 PC를 USB선으로 연결 해두시고 시작 해주세요* 휴대전화 정보 진입* 소프트웨어 정보 진입*
realnotepad.tistory.com
2. 스크립트 실행
작성한 스크립트를 실행하여 안드로이드 기기를 자동으로 제어합니다.
3. 주의사항
- 윈도우 기본 앱 중 "휴대폰과 연결"과 연결된 안드로이드를 기준으로 작동합니다 이 외에 에뮬레이터는 코드는 작동하나 뷰어가 제대로 작동하지 않을 수 있습니다- 보안을 위해 필요할 때만 USB 디버깅을 활성화하세요.
이렇게 ADB를 활용하면 안드로이드 기기의 다양한 작업을 PC에서 자동화할 수 있습니다.
사용 가능한 에뮬레이터
- 윈도우 기본 어플 "휴대폰과 연결" : 연결 상태가 고르지 못해 끊김 현상이 일어나므로 라이브러리의 예제는 본 에뮬레이터로 진행 하였지만 추천 드리지 않습니다
- scrcpy : 20241103일 구동 확인 완료, 연결 상태 엄청 좋습니다
https://github.com/Genymobile/scrcpy/releases/tag/v2.7
Release scrcpy v2.7 · Genymobile/scrcpy
To receive a notification when a new release is available, click on Watch > Custom > Releases at the top. scrcpy v2.7 Changes since v2.6.1: Add gamepad support (#99, #2130, #5270) Fix workarounds...
github.com
작동영상
ADB 뷰어 사용 방법
뷰어 설치
예제파일
#Include lib.ahk
#SingleInstance force
Gui, Add, ListView,AltSubmit gevent x12 y9 w640 h500 , index|text
Gui, Add, Button, x662 y9 w190 h40 genv venvbtn , adb env check
Gui, Add, Button, w190 h40 gcon vconnectbtn , Connect
Gui, Add, Button, Disabled w190 h40 gget vgetbtn, get
Gui, Add, Button, Disabled w190 h40 gsend vsendbtn, send
Gui, Add, Button, Disabled w190 h40 gsendk vsendkeybtn, sendkey
Gui, Add, Button, Disabled w190 h40 gclick vclickbtn, Click
Gui, Add, Button, Disabled w190 h40 vclickkeypad g전화번호누르기로직 , 키패드입력
Gui, Add, edit,vfill w190 h40 gfind, 찾기
Gui, Show, , Test Tool
return
env: ; ADB 환경변수 등록 여부 확인
EnvGet,env,path
if(instr(env,"platform-tools"))
{
msgbox,0,,adb 경로 환경변수 등록됨.
guicontrol,disabled,envbtn
}else{
msgbox,16,,adb 경로 환경변수 등록안됨.
}
return
sendk:
ADB.Sendkeys:="66" ; 엔터 테스트
return
find: ; 내용 찾기
gui,submit,nohide
loop % lv_getcount()
{
LV_GetText(t,a_index,2)
if(fill="")
LV_Modify(a_index,"-select")
else if(instr(t,fill))
LV_Modify(a_index,"select")
else if(!instr(t,fill))
LV_Modify(a_index,"-select")
}
return
전화번호누르기로직:
InputBox,pn,전화번호 입력 창,전화번호를 입력해주세요
ADB.pushPhoneNumber:=pn
return
con: ; 연결확인
resultCheck:=ADB.connect()
objchk:=strsplit(resultCheck,"`n")
if(instr(resultCheck,"attached") && RegExMatch(resultCheck,"\bdevice\b") && objchk.length()>1){
id:=StrSplit(objchk[2]," ")[1]
msgbox,0,success,연결 성공 (%id%)
guicontrol,disabled,connectbtn
guicontrol,-disabled,getbtn
guicontrol,-disabled,sendbtn
guicontrol,-disabled,sendkeybtn
guicontrol,-disabled,clickbtn
guicontrol,-disabled,clickkeypad
}else{
msgbox,16,Error,연결 실패
}
return
get: ;전체 내용 읽기
LV_Delete()
ADB.run
alltext := ADB.Text
for _,v in strsplit(alltext,"`n")
if(v)
LV_Add(,a_index,v)
FileDelete,window_dump.xml
return
send: ; 한글 입력 (APK 별도 설치 필요함)
Inputbox,io,,작성할 값을 넣어주세요
ADB.Send:=io
return
click: ; 마우스 비활성 클릭
Inputbox,p,,view에서 복사한 값을 넣어주세요
ADB.click:=p ; 좌표는 안드로이드 개발자옵션 포인터 위치 를 켜서 확인하거나 뷰어 이용.
return
event:
switch A_GuiControlEvent
{
Case "RightClick":
Menu,m,add,delete
Menu,m,show
}
return
delete:
while(lv_getnext())
LV_Delete(lv_getnext())
return
GuiClose:
ExitApp
return
라이브러리 파일
;Lib.ahk 파일로 저장하세요
Class ADB
{
/*
Creator : NOTEPAD
DATE : 2024-10-16
Last Update : 2025-05-18
Language : ahk v1 100%
License : MIT License
Copyright (c) 2025 NOTEPAD
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Functions,Methods Args:
Click : 좌표를 기준으로 클릭 -> adb.click:= x,y
pushPhoneNumber : 입력된 휴대폰 번호를 안드로이드 휴대폰 입력을 진행합니다
-> adb.pushPhoneNumber:=01099990000
-> 조건 :키패드 창에서 입력
Sendkeys : key를 입력합니다
-> adb.SendKeys:=66
-> 엔터를 입력함(66)
Send : 한글을 입력합니다
-> adb.send:="안녕하세요"
-> Adb키보드로 전환 후 입력합니다(sdk 설치 필요)
adb.CameraSound : 카메라 셔터음을 제거합니다
-> adb.CameraSound:=true
-> true = sound off, false= sound on(default)
adb.run : 화면 정보를 취득함
adb.text : 저장된 화면 정보를 파싱하여 반환함 일반적으로 adb.run 명령어 다음 라인에 선언함
adb.GetTouchPos : 터치한 화면 정보를 얻음
adb.GetWindowPos() : 에뮬레이터의 크기를 동적으로 받아옵니다.
-> 터치 이벤트 정보를 먼저 취득함 PosLog.txt 그 후 GetWindowPos call하여 가공
*/
Static maxX:="ABS_MT_POSITION_X"
Static maxY:="ABS_MT_POSITION_Y"
Click
{
Set
{
p:=this.pos(value)
prompt:="adb shell input tap " p.x " " p.y
RunWait, %ComSpec% /c %prompt%,, Hide
}
}
numbering(n) ; 키패드 배열 좌표값 반환
{
numbering:=["551,1675"
,"230,1021"
,"517,1006"
,"828,1002"
,"233,1262"
,"547,1238"
,"871,1222"
,"204,1393"
,"527,1470"
,"852,1446"]
return numbering[n+1]
}
pushPhoneNumber{ ; 숫자 입력 시 번호를 누름
Set{
for _,v in StrSplit(value)
ADB.Click:=ADB.numbering(v)
}}
GetTouchPos(){ ;실제 터치한 좌표를 얻는 함수
;~ ;자동 스크린 크기 할당, 거의 정적이므로 정적으로 우선 사용, 작동안할 시 사용하기 위한 함수임.
ScreenPos:=this.GetWindowPos()
newx:=ScreenPos.X
newy:=ScreenPos.Y
; 아래는
;~ maxX:=[]
;~ maxY:=[]
;~ r:=FileOpen(A_Temp "\PosLog.txt","r"),t:=r.read(),r.close()
;~ for _,v in strsplit(t,"`n"){
;~ if(instr(v,"ABS_MT_POSITION_X"))
;~ maxX.push(v)
;~ else if(instr(v,"ABS_MT_POSITION_Y"))
;~ maxY.push(v)
;~ }
;~ RegExMatch(maxX[maxX.Length()],this.maxX "(?<X>.*)",Pos)
;~ RegExMatch(maxY[maxY.Length()],this.maxY "(?<Y>.*)",Pos)
;~ sleep,100
;~ X:="0x" RegExReplace(PosX,"\s"),x+=0
;~ Y:="0x" RegExReplace(PosY,"\s"),Y+=0
;~ newx:=Round((x*1080) / 4095)
;~ newy:=Round((y*2340) / 4095)
return { X : newx
, Y : newy}
}
GetWindowPos() ; 자동 스크린 크기 할당 계산
{
;~ ADB.Getpos -> 터치 이벤트 정보를 먼저 취득함 PosLog.txt 그 후 GetWindowPos call하여 가공
maxX:=[]
maxY:=[]
r:=FileOpen(A_Temp "\PosLog.txt","r"),t:=r.read(),r.close()
for _,v in strsplit(t,"`n")
{
if(instr(v,"ABS_MT_POSITION_X"))
maxX.push(v)
else if(instr(v,"ABS_MT_POSITION_Y"))
maxY.push(v)
}
RegExMatch(maxX[maxX.Length()],this.maxX "(?<X>.*)",Pos)
RegExMatch(maxY[maxY.Length()],this.maxY "(?<Y>.*)",Pos)
sleep,100
X:="0x" RegExReplace(PosX,"\s"),x+=0
Y:="0x" RegExReplace(PosY,"\s"),Y+=0
Maxpos:=this.maxXY()
Window:=this.WSIZE()
x:=Round((x*Window.w) / Maxpos.maxX) ;동적할당
y:=Round((y*Window.h) / Maxpos.maxY) ;동적할당
IF(!x && !y)
msgbox,16,,반환값에 오류가 있습니다
return { X : X
,Y : Y}
}
maxXY(){
prompt:="adb shell getevent -p -l>" A_Temp "\max.txt"
RunWait, %ComSpec% /c %prompt%,, Hide
sleep,100
r:=FileOpen(A_Temp "\max.txt","r"),txt:=r.read(),r.close()
for _,v in strsplit(txt,"`n")
{
if(instr(v,this.maxX)){
RegExMatch(v,"POSITION_X.*?max (?<X>.*?),",max)
}else if(instr(v,this.maxY)){
RegExMatch(v,"POSITION_Y.*?max (?<Y>.*?),",max)
}
}
FileDelete,% A_Temp "\max.txt"
return {maxX:maxX,maxY:maxY}
}
WSIZE(){
prompt:="adb shell wm size>" A_Temp "\size.txt"
RunWait, %ComSpec% /c %prompt%,, Hide
sleep,100
r:=FileOpen(A_Temp "\size.txt","r"),txt:=r.read(),r.close()
RegExMatch(txt,"size: (?<w>.*?)x(?<h>.*)",window)
FileDelete,% A_Temp "\size.txt"
return {W:windowW,H:windowH}
}
pos(pos){
o:=StrSplit(pos,",")
return {X : o[1] , Y : o[2]}
}
Run
{
Get
{
prompt:="adb shell uiautomator dump && adb pull /sdcard/window_dump.xml"
RunWait, %ComSpec% /c %prompt%,, Hide
}
}
LoadXMLFile(filePath) {
xmlDoc := ComObjCreate("MSXML2.DOMDocument.6.0")
xmlDoc.async := false
xmlDoc.load(filePath)
return xmlDoc
}
Text {
Get {
boundShow:=True ; 좌표도 같이 반환 여부
this.driver:=this.LoadXMLFile("window_dump.xml")
contentDescs := []
boundsobj := [],boundsobj2 := []
textobj:=[]
nodes := this.driver.selectNodes("//*[@content-desc]") ; 핵심요소 얻기
nodes2:=this.driver.selectNodes("//*[@bounds]") ; 좌표 박스 얻기
nodes3:=this.driver.selectNodes("//*[@text]") ; 좌표 박스 얻기
if(!nodes2.length)
return "undefined"
Loop % nodes2.length {
contentDesc := RegExReplace(nodes.item(A_Index - 1).getAttribute("content-desc"),"`n") ;내용1
contentDesc3 := RegExReplace(nodes3.item(A_Index - 1).getAttribute("text"),"`n") ; 내용2
bounds:=nodes2.item(A_Index - 1).getAttribute("bounds")
bounds2:=nodes3.item(A_Index - 1).getAttribute("bounds")
if (contentDesc && RegExReplace(bounds,"[\s`r`n]") && contentDesc!=contentDesc3)
{
boundsobj.push(bounds)
contentDescs.Push(contentDesc)
}
if(contentDesc3 && RegExReplace(bounds2,"[\s`r`n]") && contentDesc!=contentDesc3)
{
boundsobj2.push(bounds2)
textobj.push(contentDesc3)
}
}
for index, desc in contentDescs {
if(boundShow)
resultText .= desc "," boundsobj[A_Index] "`n"
else
resultText .= desc "`n"
}
for index, desc in textobj {
if(boundShow)
resultText .= desc "," boundsobj2[A_Index] "`n"
else
resultText .= desc "`n"
}
return resultText
}
}
RealtimeLog
{
get
{
gui,listview,list2
LV_Delete()
prompt:="adb logcat -c && adb logcat -t 50>" A_Temp "\RT_Log.txt"
RunWait, %ComSpec% /c %prompt%,, Hide
}
}
Getpos
{
get
{
gui,listview,list2
prompt:="adb shell getevent -l>" A_Temp "\PosLog.txt"
;~ prompt:="adb shell getevent /dev/input/event6 -l>" A_Temp "\PosLog.txt" ; 특정 event6만 감시
RunWait, %ComSpec% /c %prompt%,, Hide
}
}
Pointer
{
Set
{
if(value)
prompt:="adb shell settings put system pointer_location 1" ; 휴대폰 포인터 옵션 활성화
else
prompt:="adb shell settings put system pointer_location 0" ; 휴대폰 포인터 옵션 비활성화
RunWait, %ComSpec% /c %prompt%,, Hide
}
}
LayOut
{
Set
{
if(value)
prompt:="adb shell setprop debug.layout true && timeout 1 && adb shell service call activity 1599295570" ; 휴대폰 레이아웃 활성화
else
prompt:="adb shell setprop debug.layout false && timeout 1 && adb shell service call activity 1599295570" ; 휴대폰 레이아웃 활성화
RunWait, %ComSpec% /c %prompt%,, Hide
}
}
Send
{
/*
apk 설치 => cd로 apk 경로 지정 후 adb install 이름.apk
;com.samsung.android.honeyboard/.service.HoneyBoardService 기본 키보드
*/
Set
{
prompt:="adb shell ime enable com.android.adbkeyboard/.AdbIME && timeout 1 && adb shell ime set com.android.adbkeyboard/.AdbIME && timeout 1 && adb shell am broadcast -a ADB_INPUT_TEXT --es msg '" value "' && timeout 1 && adb shell ime set com.samsung.android.honeyboard/.service.HoneyBoardService"
RunWait, %ComSpec% /c %prompt%,, Hide
}
}
Sendkeys
{
/*
KEYCODE_ENTER = 66
*/
Set
{
prompt:="adb shell input keyevent " value
RunWait, %ComSpec% /c %prompt%,, Hide
}
}
reset
{
get
{
prompt:="taskkill /f /im adb.exe && timeout 2 && adb devices" value
RunWait, %ComSpec% /c %prompt%,, Hide
FileDelete,%A_Temp%\RT_Log.txt
}
}
Connect()
{
prompt:="adb devices > " A_Temp "\check.txt"
RunWait, %ComSpec% /c %prompt%,, Hide
o:=FileOpen(A_Temp "\check.txt","r"),chk:=o.read(),o.close()
FileDelete,%A_Temp%\check.txt
return chk
}
; Android control Functions
CameraSound
{
Set ; true = sound off, false= sound on
{
prompt:=value ? "adb shell settings put system csc_pref_camera_forced_shuttersound_key 0" : "adb shell settings put system csc_pref_camera_forced_shuttersound_key 1" ; 1 = On, 0 = OFF
RunWait, %ComSpec% /c %prompt%,, Hide
}
}
}
업데이트
2025-05-18 : -adb.text 함수 안정화 -> 얻는 요소 정보 추가 반환 작업