레이블이 android인 게시물을 표시합니다. 모든 게시물 표시
레이블이 android인 게시물을 표시합니다. 모든 게시물 표시

2022년 4월 10일 일요일

uiautomator2 로 screenshot 비교하기

uiautomator2로 android에서 자동화를 해보기위해서 이것저것 해보고 있습니다. 도움이 될만한 내용들을 정리해 보았습니다.


1. 현재앱 알아내기

print(d.app_current())

출력 형태

{'package': 'com.android.settings', 'activity': '.homepage.SettingsHomepageActivity', 'pid': 14781}


2. 앱 실행키시기

app_start로 실행합니다. 이때인자는 패키지 이름입니다. 앱을 실행 시킨 후 app_wait로 pid를 얻어 올 수 있습니다.

		d.app_start(test_package)
		pid = d.app_wait(test_package, timeout=20.0)
		if not pid:
			print(f"{test_package} is not running")
		else:
			print(f"{test_package} pid is {pid}")


3. 화면 구성값 가져오기

xml 형태로 저장됩니다.

		xml = d.dump_hierarchy()
		print("saving xml")
		write_file("hierarchy.xml",str(xml))

형태가 복잡해서 전체 예제를 실행시키면 저장하도록 해놨습니다. 파일을 열어보시길 추천합니다.


4. 스크린샷 찍어 저장하기

확장자에 따라서 png, jpg 형태로 저장이 가능합니다.

		image = d.screenshot()
		if not exists(test_image_name):
			image.save(test_image_name)
			print("save screenshot")


5. 키보내기

여러 종류의 키를 보낼 수 있습니다.

d.press("home")

숫자값으로 보낼때는 여기 참고

https://developer.android.com/reference/android/view/KeyEvent

텍스트는 아래 참고

home

back

left

right

up

down

center

menu

search

enter

delete ( or del)

recent (recent apps)

volume_up

volume_down

volume_mute

camera

power


6. 종합 screenshot 찍어서 이전 저장된 사진과 비교하기

전체 소스

from os.path import exists
import uiautomator2 as u2
import adbutils
import time
from PIL import Image
from PIL import ImageChops
import math, operator

test_image_name = "testimage.png"
test_package = "com.android.settings"


def write_file(filename,str_data):
	with open(filename, 'w', encoding="utf-8") as fp:
		fp.write(str_data)
		fp.close()
		
def images_are_similar(img1, img2, error=90):
	print(img1.size[0],img1.size[1]) # xsize, ysize
	diff = ImageChops.difference(img1, img2).histogram()
	# [ r 256 , g 256 , b 256 ] = 768 list
	print(len(diff))
	print(diff)
	sq = (value * (i % 256) ** 2 for i, value in enumerate(diff))
	sum_squares = sum(sq)
	print(sum_squares)
	rms = math.sqrt(sum_squares / float(img1.size[0] * img1.size[1]))
	print(rms)
	return rms < error
	
if __name__ == "__main__":
	d = None
	for dev in adbutils.adb.device_list():
		print("Dev:", dev)
		d = u2.connect(dev.serial)

	if d==None:
		print("please check connected devices (adb devices)")
	else:
		d.screen_on()
		d.unlock()
		d.press("home")

		print(d.app_current())
		d.app_start(test_package)
		pid = d.app_wait(test_package, timeout=20.0)
		if not pid:
			print(f"{test_package} is not running")
		else:
			print(f"{test_package} pid is {pid}")

		xml = d.dump_hierarchy()
		print("saving xml")
		write_file("hierarchy.xml",str(xml))
		image = d.screenshot()
		if not exists(test_image_name):
			image.save(test_image_name)
			print("save screenshot")
		else:
			# compare
			image.save("new"+test_image_name)
			pimage = Image.open(test_image_name)
			#for test sdiff = images_are_similar(image,image)
			sdiff = images_are_similar(image,pimage)
			print(sdiff)
		print(d.app_current())
		d.press("home")


7.이미지 비교 함수 설명

images_are_similar

여기에 넘어가는 이미지는 PIL type입니다. size[0], size[1]은 각각 가로 세로 크기입니다.

기본 원리는 image의 diff를 구하는것입니다. 이것은 ImageChops.difference(img1, img2) 와 같은 형태로 구할 수 있습니다.

다음으로는 histogram()을 취하게 되는데 histogram은 R, G, B 각각 256개로 모두 768 개를 지닙니다. 

일반적으로 delta를 수치로 표현할때 표준편차를 구해서 비교하는데 여기에서는 이미지 histogram의 표준 편차를 구해서 특정값보다 크게 되면 차이가 난다고 판단합니다. 임의의 기준을 여기에서는 90으로 정했으며 조정 가능한 값입니다.

같은 이미지로 시험하면 아래와 같은 histogram을 얻게 됩니다. 여기에 들어가 있는 2592000 는 가로*세로 즉 점의 갯수가 됩니다. 1080 2400 화면에서는 아래와 같습니다.

[2592000, 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, 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, 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, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2592000, 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, 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, 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, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2592000, 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, 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, 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, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

그리고 첫번째는 R의 0번값이며 2592000 수치를 나타낸다는 의미는 점을 빼줬기 때문에 0을 가진다는 의미입니다.

만약 다른 이미지로 테스트했다면 아래와 같은 형태가 될것입니다.


중간이미지가 diff(차)연산으로 나온 이미지인데 이것을 histogram을 해서 표준 편차를 구한다는 의미입니다.  

여기에서는 이미지가 같게 되면 rms 수치가 작아지게 되고 images_are_similar True값이 리턴됩니다.



2022년 4월 3일 일요일

android 자동화 테스트 소개 ( UiAutomator, openatx/uiautomator2 , appium )


1. Ui Automator 

android에서 앱을 자동으로 테스트 할 수 있게 만들어놓은 테스트 Framework 입니다.

크게 Espresso 테스트 프레임워크, UI Automator 테스트 프레임워크가 있는데

그 중에 Ui Automator는 사용자 플로우가 다른 앱이나 시스템 UI로 넘어갈 수 있도록 제공되고 있습니다.

https://developer.android.com/training/testing/ui-testing/uiautomator-testing?hl=ko

단점으로는 apk 형태로 자동화하려는 코드를 직접 만들어야 합니다.


2. openatx, uiautomator2

안드로이드 기반의 Ui Automator 기반의 외부 실행 자동화 프로그램입니다. 안드로이드 Ui Automator 는 안드로이드 app을 컴파일하고 빌드해서 원하는 자동화를 만들어야하지만, 전체적인 테스트 세트를 미리 준비하여 외부에서 안드로이드 테스트 app에 명령을 줘서 동작시키는 원리(http rpc)입니다.

uiautomator2 라고 하는 이유는 github에 이미 uiautomator가 존재하여 fork한 후 이름을 2를 붙였다고 합니다.

python wrapper : https://pypi.org/project/uiautomator2/

python에서는 uiautomator2 를 pip로 설치만 하면 간단하게 사용이 가능합니다.

아래는 단말을 개발자 모드 adb로 enable된 상태에서 connect 명령을 내린 상태

>>> import uiautomator2 as u2
>>> d = u2.connect()
[W 220403 22:00:13 __init__:218] [pid:12328] atx-agent has something wrong, auto recovering
[D 220403 22:00:13 __init__:322] [pid:12328] [xxxxxx] device xxxxxx is online
[I 220403 22:00:14 init:156] uiautomator2 version: 2.16.14
[D 220403 22:00:14 init:167] Shell: ('/data/local/tmp/atx-agent', 'server', '--stop')
[I 220403 22:00:14 init:346] Install atx-agent 0.10.0
[D 220403 22:00:14 init:62] Download https://tool.appetizer.io/openatx/atx-agent/releases/download/0.10.0/atx-agent_0.10.0_linux_armv7.tar.gz
atx-agent_0.10.0_linux_armv7.tar.gz |⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿| 4.1 MB/4.1 MB
[D 220403 22:00:15 init:238] Push to /data/local/tmp/atx-agent:0755
[D 220403 22:00:16 init:167] Shell: ('/data/local/tmp/atx-agent', 'server', '--nouia', '-d', '--addr', '127.0.0.1:7912')
[I 220403 22:00:16 init:350] Check atx-agent version
[D 220403 22:00:16 init:359] Forward: local:tcp:57410 -> remote:tcp:7912
[D 220403 22:00:16 init:359] Forward: local:tcp:57410 -> remote:tcp:7912
[D 220403 22:00:16 init:362] atx-agent version 0.10.0
[D 220403 22:00:16 init:365] device wlan ip: 192.168.0.xx

단말은 ADB로 연결한 상태에서 connect()메소드를 사용하면 atx-agent를 자동으로 다운로드 하여 단말에 설치하게 됩니다.

atx-agent는 단말에 설치하는 전체적인 테스트 세트라고 생각하면 됩니다.

python에서 여러가지 기능들이 준비가 되어있어서 쉽게 android 단말을 제어 가능합니다.

홈페이지 : https://github.com/openatx/uiautomator2


3. appium

uiautomator2와 동작 원리는 같습니다. 좀더 덩치가 크고 여러가지 언어를 이용해서 test 코드(자동화)를 만들 수 있다. 특히 javascript 를 이용한 예제 코드가 많이 보입니다.

홈페이지 : http://appium.io/


4. 끝으로

JavaScript가 편하다면 appium을 python이 편하다면 uiautomator2 를 추천합니다.




2021년 9월 22일 수요일

scrcpy (안드로이드 단말을 PC에 연결해서 화면 보기, 스크린 미러링) #4 소스 분석


앞에서 jar파일의 main이 sever.java여기에 있고 실행시키는것까지 확인하였습니다.

server.java에서는 아마도(?) 소켓을 만들고 대기하고 있을것입니다.

전형적인 PME(Property Method Event)프로그래밍 방식에서는 무한 루프에 빠지는 형태를 취하지는 않습니다. 그러나 여기 소스는 android의 cmd line 형태를 취하고 있고 전혀 android activity 구조를 가지고 있지 않습니다.


scrcpy 함수에서 아래 함수를 볼 수 있는데

screenEncoder.streamScreen(device, connection.getVideoFd());

이 부분이 화면을 압축하여 지속적으로 전달하기 위한 함수 입니다.

내부에는 주요 함수들도 많지만, 특히 아래함수가 주요 함수로 보면됩니다.

private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {





다음으로는 입력에대한 처리입니다.

scrcpy 소스 근처의 controller 쪽을 참고 하면됩니다.

                final Controller controller = new Controller(device, connection);


                // asynchronous

                controllerThread = startController(controller);

thread를 만들고 control 함수를 실행시키게 되는데 이부분의 소스는 Controller.java가 담당합니다.

public void control() throws IOException {
// on start, power on the device
if (!Device.isScreenOn()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER);
// dirty hack
// After POWER is injected, the device is powered on asynchronously.
// To turn the device screen off while mirroring, the client will send a message that
// would be handled before the device is actually powered on, so its effect would
// be "canceled" once the device is turned back on.
// Adding this delay prevents to handle the message before the device is actually
// powered on.
SystemClock.sleep(500);
}
while (true) {
handleEvent();
}
}

별건 없고 hadleEvenet게 주업무를 담당하고 있습니다.

키를 누르거나 터치를 하게 되면 아래 함수에 의해서 처리가 되는데


private void handleEvent() throws IOException {
ControlMessage msg = connection.receiveControlMessage();
switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE:


누군가가 메세지를 보내준걸 읽게 됩니다.

public ControlMessage receiveControlMessage() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {
reader.readFrom(controlInputStream);
msg = reader.next();
}
return msg;
}

그렇다면 그 누군가가 누구일까요? 바로 PC의 connection온 데이터 겠죠. 모든 입력은 PC쪽에서 발생하게 되고 server jar 쪽으로 전달되게 됩니다.

아래와 같은 함수로 touch가 눌린것처럼 행동하게 됩니다.

injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());

좀 더 따라가보면

아래 형태로 inject 를 하게 됩니다.

public static boolean injectEvent(InputEvent inputEvent, int displayId) {
if (!supportsInputEvents(displayId)) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
}
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) {
return false;
}
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}

여기에서 inputmanager는 이렇고,

public InputManager getInputManager() {
if (inputManager == null) {
inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager"));
}
return inputManager;
}

여기에 전달하는 inputevent는 아래와 같습니다

Controller.java 소스를 참고 하면 됩니다.

MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
0);
return device.injectEvent(event);