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

2022년 12월 31일 토요일

making file explorer in android (API 31 Android 12.0(S)) 파일 탐색기 소스

오래전에 File Explorer를 안드로이드 예제로 만든적이 있는데 아직도 많은 분들이 소스 요청을 하는 경우가 있었습니다. 

https://swlock.blogspot.com/2017/03/making-file-explorer-in-android.html

위 링크에 있는 구글 drive에 있는 파일이 예전에는 누구나 소스 접근이 가능 했는데 지금은 권한 승락을 해야지만 가져갈 수 있도록 되어있어서 이번 기회에 API 31 Android 12.0(S) 로 새로 작업해서 공유 합니다. 

내용은 이전과 같아서 앞에 내용을 이해하시고 소스는 새로 올린 github 에서 다운 받으시기 바랍니다.

https://github.com/donarts/sourcecode/blob/main/android/_01_fileexplorer/fileexplorer.zip


위의 예제를 실행시키면 파일이 전체적으로 나오지 않고 media 파일만 나오게 됩니다.


저장소 액세스 권한 요청

소스는 아래에 의해서 동작되며,
// 안드로이드 R 이상 전체 저장소 엑세스 권한
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
try {
Intent intent = new Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.addCategory("android.intent.category.DEFAULT");
intent.setData(Uri.parse(String.format("package:%s",
getApplicationContext().getPackageName())));
startActivity(intent);
} catch (Exception e) {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
}
}
}

androidmanifest.xml에 아래 내용 추가해야 합니다.

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

앱을 실행시키면 아래와 같은 화면이 뜨는데 사용자가 앱에 권한을 허용해 주어야 합니다. 


(screenshot에서는 file explorer 라는 앱입니다.)


그리고 나서 다시 앱으로 돌아오면 미디어 파일 허용 액세스 권한을 요청하게 됩니다.

해당 내용은 아래 코드에 의해서 동작됩니다.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED ||
checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
}
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
}
}

물론 androidmanifest.xml에도 아래 내용을 추가해 주어야 합니다.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>



추가 작업된 내용

파일 선택시 RUN버튼에 선택된 파일 갯수 출력



버튼을 누를때 갯수를 업데이트 해주는 아래 함수를 호출 해주도록 구현함
private void updateRunButtonText() {
List<String> data = getSelectedItems();
int len = data.size();
if (len>0) {
runButton.setText("(" + len + ") RUN");
}else{
runButton.setText("RUN");
}
}

updateRunButtonText() 함수를 호출 해주는 부분은 listview에서 checkbox 버튼을 누를때

public View getView(int position, View convertView, ViewGroup parent) {
View retView = super.getView(position,convertView,parent);
final int pos = position;
final View parView = retView;
CheckBox cb = (CheckBox)retView.findViewById(R.id.checkBox_saved);
cb.setOnClickListener(new View.OnClickListener() {
// 앞쪽의 checkbox 를 클릭했을때
@Override
public void onClick(View view) {
MyListItem item = listDispItems.get(pos);
item.checked = !item.checked;
if( item.checked ) totalSelected++;
item.selectedNumber=totalSelected;
Toast.makeText(MainActivity.this,
"1: Click "+pos+ "th " + item.checked +
" "+totalSelected, Toast.LENGTH_SHORT).show();
printDebug();
updateRunButtonText();
}
});


RUN 버튼 누를때 화면에 문구 출력

CurPath에는 현재 경로가 들어있고, item에는 파일 이름이 들어있게 됩니다. 

그것을 화면에 출력하는 부분입니다.

public void onClick(View view) {
StringBuilder sb = new StringBuilder();
sb.append("Count:"+getSelectedItemCount()+"\n");
sb.append("path:"+CurPath+"\n");
List<String> data = getSelectedItems();
for(int i=0;i<data.size();i++){
String item = data.get(i);
// item 에는 file 이름이 들어옵니다.
// 만약 path라면 마지막에 "/" 이 더 붙도록 되어 있습니다.
// CurPath + item : 전체 path 를 가진 파일 이름이 됩니다.
sb.append(item+"\n");
}

new AlertDialog.Builder(MainActivity.this)
.setTitle("[ RUN ]")
.setMessage(sb.toString())
.setPositiveButton("OK",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
}
}).show();
// 뭔가를 실행후에는 초기화 한다는 예제로 만들어 보았습니다.
setupAdapter();
updateRunButtonText();
}

참고로 Run버튼을 누른 뒤에는 선택된 내용을 모두 초기화하는 setupAdapter()를 넣었습니다.



아래는 개선된 전체 소스입니다.

https://github.com/donarts/sourcecode/blob/main/android/_01_fileexplorer/fileexplorer2.zip




2022년 11월 13일 일요일

누가 나의 알람을 껐을까? (안드로이드 실전 디버깅) 삼성 휴대폰, android debugging

당신은 가끔 당신 휴대폰의 알람 소리를 못들을 때가 있지 않은가?

내가 잠결에 껐겠지 생각하거나 귀신이 끈건 아닐까라고 생각했던건 아닌지? 

실제로 그런 일이 나에게 일어났습니다.

여기에서는 휴대폰 오류로 알람이 안울린 것인지 누군가에 의해서 꺼진것인지 로그를 바탕으로 분석하는 방법을 다룰 것입니다. (삼성 휴대폰 기준, 가지고 있는 단말이 삼성 휴대폰 밖에 없어서...)


1. 안드로이드 단말 로그 획득 방법

일반적인 안드로이드 단말은 adb 연결된 상태에서 dumpstate 로 로그를 생성 시킵니다. 

하지만 이것은 PC를 연결해야지만 로그 생성이 가능하기 때문에 굉장히 불편합니다.

삼성 단말에서는 다이얼에서 *#9900# 을 누르면 이러한 dumpstate를 파일을 단말에 저장하는 기능을 제공합니다.

sysdump라는 위 메뉴에 진입 하게 되는데 Run dumpstate/logcat 메뉴를 이용하면 됩니다.
그리고 Copy to sdcard 라는 메뉴는 생성된 로그를 log 폴더로 복사하는 역할을 합니다. (로그 생성시 수분 정도 대기해야 합니다.)

해야 할 일은 이 부분을 숙지하고 있다가 문제가 발생하면 해당 메뉴를 실행시켜 주면 됩니다.


2. 디버깅의 기본

안드로이드 뿐만 아니라 모든 SW의 디버깅을 할때 해당 Domain의 지식이 없는 경우 맨땅에 헤딩(?) 하게 됩니다.

이 글을 읽는 분도 입력 또는 알람이나 소리 관련해서는 전혀 아는게 없다고 판단하고 정상적인 상황에서 로그가 어떻게 나오는지 미리 확인해야 합니다.

이때 단말을 PC에 연결해서 정상적인 상황에서 로그가 어떤 Text로 나오는지 익혀 둡니다.

기본적으로 실시간 디버깅을 위해서는 adb tool을 단말에 설치해야 하는데 이것은 android sdk(Android Studio)를 설치하거나 adb 실행 파일만 따로 구해야 합니다.

Android Studio > Tools > SDK Manager 에서 platform-tools 다운로드 가능합니다.

Sdk 경로는 기본 경로가 아래 경로입니다.

C:\Users\사용자 이름\AppData\Local\Android\Sdk\platform-tools


SDK를 설치하지 않고 사용할 수 있는 adb link를 공유합니다. 

Android Debug Bridge version 1.0.41

Version 33.0.3-8952118

https://drive.google.com/file/d/1ezVHiS0Cd-c2y8bPgxRobhk7T-FmXGmr/view?usp=sharing


2.1 로그 파일 쉽게 보기

adb logcat으로 실시간 로그를 볼 수 있긴 하지만 많은 로그들이 한꺼번에 지나가므로 좀 더 편한 툴이 필요합니다.

가장 많이 사용하는 툴이 logfilter라는 어플이 있는데 LogFilter_1.8.jar / LogFilterCmd.ini 파일을 구했습니다.

cmd line에서 java -jar LogFilter_1.8.jar 와 같이 실행 시킵니다.

Java가 설치 안되어 있다면 java 설치해야 합니다.

저의 경우 Android Studio 설치시 jre 가 같이 설치되어 C:\Program Files\Android\Android Studio\jre 여기 경로에서 java를 이용 가능합니다. (java 또는 jre를 직접 설치하면 됩니다.)

아래와 같이 실행 하였습니다.

C:\Users\계정이름\AppData\Local\Android\Sdk\platform-tools>"C:\Program Files\Android\Android Studio\jre\bin\java" -jar LogFilter_1.8.jar

단말은 개발자 모드에서 USB디버깅을 활성화 해줍니다.


2.1 일반 상태에서 키를 누를때 발생하는 로그

Tag에 Input 을 넣어서 필터를 해주면 터치 할때 마다 아래와 같은 형태의 로그가 발생됩니다.

2.1.1 Volume Up 키

input event(3value=1) 버튼 누를때
input event(3value=0) 버튼 해제 될때
Delivering key to (숫자VU action: 0x0)
Delivering key to (숫자VU action: 0x1)

2.1.2 Volume Down 키

input event(9value=1) 버튼 누를때
input event(9value=0) 버튼 해제 될때
Delivering key to (숫자VD action: 0x0)
Delivering key to (숫자VD action: 0x1)

2.1.3 Power 키


input event(9value=1) 버튼 누를때

input event(9value=0) 버튼 해제 될때

!@notifyKey(116), action=0

!@notifyKey(116), action=1


2.2 일반 상태에서 터치를 할때 발생하는 로그

터치 누를때 Touch events action is 0x0 

터치에서 손을 뗄때 Touch events action is 0x1 


3. 문제 발생한 상황에서의 로그

1번에서 파일을 생성했으면 단말을 PC와 연결 후 log 폴더에서 dumpstate로 시작하는 파일을 검색합니다. 해당 파일에서 앞서 설명한 버튼과 터치에 관한 로그를 검색하도록 합니다.

실제 로그를 찾아보니 "'" 문자를 표시 안하는 경우가 있어서 logfilter의 로그와 조금 다릅니다. (분석시 참고)

5:59분 알림인데 실제 아래와 같은 로그가 발견 되었습니다.

줄   267138: 11-08 21:26:50.175  1000  1244  2072 I InputReader: Touch event's action is 0x1 (id=2, t=0) [pCnt=1, s=] when=418904.535647

줄   316374: 11-09 05:59:15.281  1000  1244  2072 I InputReader: Touch event's action is 0x0 (id=2, t=0) [pCnt=1, s=0.16732 ] when=419384.247445

줄   317546: 11-09 05:59:15.948  1000  1244  2072 I InputReader: Touch event's action is 0x1 (id=2, t=0) [pCnt=1, s=] when=419384.915882

줄   340006: 11-09 06:58:29.906  1000  1244  2072 I InputReader: Touch event's action is 0x0 (id=2, t=0) [pCnt=1, s=0.16733 ] when=419443.865649


5:59분 15초에 0.6초 가량 터치했다가 손을 뗀 기록이 남아 있습니다.


4. 결론

알림이 어떤 휴대폰 오류로 안울린 것이 아니라 무의식 중에 터치하여 알람을 끈것으로 확인되었습니다.


5. 주의점

로그는 내부적으로 크기가 정해져 있기 때문에 가능하면 문제 발생 즉시 로그 확보가 되어야 분석이 가능합니다.


2022년 9월 4일 일요일

android 전화 번호 읽기, getNumber, getLine1Number, getPhoneNumber

 Android 전화 번호 읽기

getNumber, getLine1Number, getPhoneNumber 함수에 대해서 정리해보았습니다.

getNumber, getLine1Number 두개의 함수는 API 33 부터 deprecated 되고 getPhoneNumber 함수가 사용됩니다. 따라서 미리 코드를 정리해 보았습니다.


특히 getNumber / getPhoneNumber 함수의 경우 DualSIM에서도 번호를 읽을 수 있도록 구현 되어있습니다. 시험 환경이 없어서 시험을 해보지는 못했습니다.


권한은 아래 두 개 Runtime 권한이 필요합니다.

android.Manifest.permission.READ_PHONE_STATE,
android.Manifest.permission.READ_PHONE_NUMBERS

AndroidManifest.xml 에도 추가해줍니다.

<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />


함수 사용에 기본적인 예제

단말에서도 시험을 해봤는데 33 API의 경우 현재 대상 단말을 구하기 힘들어서 가상 device를 이용하여 시험 하였습니다.



실행 화면

로그 화면


전체 소스

package com.example.javatestapp2;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;

import com.google.android.material.snackbar.Snackbar;

import androidx.appcompat.app.AppCompatActivity;

import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.View;

import androidx.core.app.ActivityCompat;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;

import com.example.javatestapp2.databinding.ActivityMainBinding;

import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    public static List<SubscriptionInfo> subInfoList;
    public static ArrayList<String> Numbers;
    private SubscriptionManager mSubscriptionManager;

    private AppBarConfiguration appBarConfiguration;
    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        setSupportActionBar(binding.toolbar);

        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
        appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

        binding.fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });

        TextView textView = (TextView) findViewById(R.id.textview_first);

        // 권한이 허용되어있지않다면 권한요청
        if (!hasPermissions(this, PERMISSIONS)) {
            getPermission(PERMISSIONS);
            textView.setText("Please re run");
        }else {
            Numbers = new ArrayList<String>();
            mSubscriptionManager = SubscriptionManager.from(this);
            GetCarriorsInformation();
            textView.setText(Numbers.toString());
        }

    }

    String[] PERMISSIONS = {
            android.Manifest.permission.READ_PHONE_STATE,
            android.Manifest.permission.READ_PHONE_NUMBERS
    };

    public boolean hasPermissions(Context context, String... permissions) {
        if (context != null && permissions != null) {
            for (String permission : permissions) {
                if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
                    return false;
                }
            }
        }
        return true;
    }
    private void getPermission(String... permissions){
        ActivityCompat.requestPermissions(this,
                permissions,1000);
    }
    private void GetCarriorsInformation() {
        subInfoList = mSubscriptionManager.getActiveSubscriptionInfoList();
        Log.d("Test",subInfoList.toString());
        int subId;
        // https://developer.android.com/reference/android/telephony/SubscriptionInfo#getNumber()
        // This method was deprecated in API level 33.
        // use SubscriptionManager#getPhoneNumber(int) instead.
        for (SubscriptionInfo subscriptionInfo : subInfoList) {
            subId = subscriptionInfo.getSubscriptionId();
            Log.d("Test","SubscriptionInfo:"+subId);
            Log.d("Test","SubscriptionInfo:"+subscriptionInfo.getNumber());
            Numbers.add("SubscriptionInfo/"+subId+"/"+subscriptionInfo.getNumber()+"\n");
        }
        // https://developer.android.com/reference/android/telephony/TelephonyManager#getLine1Number()
        // This method was deprecated in API level 33.
        // use SubscriptionManager#getPhoneNumber(int) instead.
        TelephonyManager mgr = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
        String myNumber = mgr.getLine1Number();
        Log.d("Test","getLine1Number:"+myNumber);
        Numbers.add("getLine1Number/"+myNumber+"\n");

        if (android.os.Build.VERSION.SDK_INT >= 33) {
            // https://developer.android.com/reference/android/telephony/SubscriptionManager#getPhoneNumber(int)
            for (SubscriptionInfo subscriptionInfo : subInfoList) {
                subId = subscriptionInfo.getSubscriptionId();
                Log.d("Test", "SubscriptionManager:" + subId);
                Log.d("Test", "SubscriptionManager:" + mSubscriptionManager.getPhoneNumber(subId));
                Numbers.add("SubscriptionManager/" + subId + "/" + mSubscriptionManager.getPhoneNumber(subId) + "\n");
            }
        }
} @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } @Override public boolean onSupportNavigateUp() { NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main); return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp(); } }

로그

2022-09-04 12:39:22.569 7433-7433/com.example.javatestapp2 D/Test: [{id=1 iccId= simSlotIndex=0 carrierId=1 displayName=T-Mobile carrierName=T-Mobile nameSource=3 iconTint=-16746133 number=[****] dataRoaming=0 iconBitmap=null mcc=310 mnc=260 countryIso=us isEmbedded=false nativeAccessRules=null cardString= cardId=0 portIndex=0 isOpportunistic=false groupUUID=null isGroupDisabled=false profileClass=-1 ehplmns=null hplmns=null subscriptionType=0 groupOwner=null carrierConfigAccessRules=[cert: 92B5F8117FBD9BD5738FF168A4FA12CBE284BE834EDE1A7BB44DD8455BA15920 pkg: null access: 0, cert: 3D1A4BEF6EE7AF7D34D120E7B1AAC0DD245585DE6237CF100F68333AFACFF562 pkg: null access: 0, cert: 6892793FC413019D2DF609DFED7AF622D0F2D8FCF96EFA7E3FB87EEA34E10B93 pkg: null access: 0, cert: 7B68FD9D4E7610C9CB35FC0C6CC06EA04C6906E3DFA9F48F9A05460AF36BFFFC pkg: null access: 0] areUiccApplicationsEnabled=true usageSetting=0}]
2022-09-04 12:39:22.569 7433-7433/com.example.javatestapp2 D/Test: SubscriptionInfo:1
2022-09-04 12:39:22.569 7433-7433/com.example.javatestapp2 D/Test: SubscriptionInfo:+15551234567
2022-09-04 12:39:22.573 7433-7433/com.example.javatestapp2 D/Test: getLine1Number:+15551234567
2022-09-04 12:39:22.573 7433-7433/com.example.javatestapp2 D/Test: SubscriptionManager:1
2022-09-04 12:39:22.583 7433-7433/com.example.javatestapp2 D/Test: SubscriptionManager:+15551234567


빌드시 아래와 같이 설정하였습니다.

compileSdk 33
minSdk 31
targetSdk 31


2022년 6월 5일 일요일

android monkey test with python adbutils(파이썬을 이용한 몽키 테스트)

Monkey Test

Monkey는 애플리케이션을 반복 랜덤 방식으로 스트레스 테스트할 수 있는 도구 입니다.

이번에는 python을 이용해서 monkey test를 수행하고 에러가 발생하면 로그를 획득하는 코드입니다. 90% 이상은 이전에 작성한 코드를 재사용 하였습니다.


도움말 링크

https://developer.android.com/studio/test/monkey?hl=ko


기본 구문

$ adb shell monkey [options] <event-count>

예제

$ adb shell monkey -p your.package.name -v 500

명령어 옵션

카테고리옵션설명
일반--help간단한 사용 가이드를 인쇄합니다.
-v명령줄의 각 -v는 상세 레벨을 증가합니다. 레벨 0(기본값)에서는 시작 알림, 테스트 완료, 최종 결과 이외의 정보를 거의 제공하지 않습니다. 레벨 1에서는 실행되는 테스트에 관한 세부정보를 제공합니다(예: 활동에 전송되는 개별 이벤트). 레벨 2에서는 테스트에 선택되거나 선택되지 않은 활동과 같은 더 자세한 설정 정보를 제공합니다.
이벤트-s <seed>의사 랜덤 숫자 생성기의 시드값입니다. 같은 시드값으로 Monkey를 다시 실행하면 동일한 이벤트 시퀀스가 생성됩니다.
--throttle <milliseconds>이벤트 사이에 고정 지연을 삽입합니다. 이 옵션을 사용하여 Monkey의 속도를 늦출 수 있습니다. 지정하지 않으면 지체 없이 최대한 빠르게 이벤트가 생성됩니다.
--pct-touch <percent>터치 이벤트 비율을 조정합니다 (터치 이벤트는 화면의 단일 위치에서의 다운/업 이벤트입니다).
--pct-motion <percent>모션 이벤트 비율을 조정합니다 (모션 이벤트는 화면 어딘가의 다운 이벤트와 일련의 의사 랜덤 이동, 업 이벤트로 구성됩니다).
--pct-trackball <percent>트랙볼 이벤트 비율을 조정합니다 (트랙볼 이벤트는 하나 이상의 랜덤 이동으로 구성되며 때로는 클릭이 뒤따릅니다).
--pct-nav <percent>'기본' 탐색 이벤트 비율을 조정합니다 (탐색 이벤트는 방향 입력 기기의 입력처럼 위/아래/왼쪽/오른쪽으로 구성됩니다).
--pct-majornav <percent>'주요' 탐색 이벤트 비율을 조정합니다 (5방향 패드의 가운데 버튼 또는 메뉴 키와 같이 일반적으로 UI 내에 작업을 발생시키는 탐색 이벤트입니다).
--pct-syskeys <percent>'시스템' 키 이벤트 비율을 조정합니다 (이러한 키는 홈, 돌아가기, 통화 시작, 통화 종료, 볼륨 조절과 같이 일반적으로 시스템에서 사용하도록 예약됩니다).
--pct-appswitch <percent>활동 실행 비율을 조정합니다. 랜덤 간격으로 Monkey는 startActivity() 호출을 실행하며 이는 패키지 내의 모든 활동의 적용 범위를 최대화하는 방법입니다.
--pct-anyevent <percent>다른 유형의 이벤트 비율을 조정합니다. 이것은 다른 모든 유형의 이벤트를 포괄합니다(예: 키 누르기, 기기에서 덜 사용되는 다른 버튼 등).
제약 조건-p <allowed-package-name>이런 식으로 하나 이상의 패키지를 지정하면 Monkey는 시스템이 지정된 패키지 내의 활동 방문하도록 허용합니다. 애플리케이션이 다른 패키지(예: 연락처 선택)의 활동에 액세스해야 한다면 그 패키지도 지정해야 합니다. 어떤 패키지도 지정하지 않으면 Monkey는 시스템이 모든 패키지에서 활동을 시작하도록 허용합니다. 여러 패키지를 지정하려면 -p 옵션을 여러 번 사용하세요(패키지당 -p 옵션 하나).
-c <main-category>이 방법으로 하나 이상의 카테고리를 지정하면 Monkey는 시스템이 지정된 카테고리 중 하나와 함께 나열된 활동 방문하도록 허용합니다. 어떤 카테고리도 지정하지 않으면 Monkey는 Intent.CATEGORY_LAUNCHER 또는 Intent.CATEGORY_MONKEY 카테고리와 함께 나열된 활동을 선택합니다. 여러 카테고리를 지정하려면 -c 옵션을 여러 번 사용하세요(카테고리당 -c 옵션 하나).
디버깅--dbg-no-events지정된 경우 Monkey는 테스트 활동으로 초기 실행을 이행하지만 더 이상의 이벤트를 생성하지는 않습니다. 최상의 결과를 얻으려면 -v와 하나 이상의 패키지 제약 조건, 0이 아닌 스로틀을 결합하여 Monkey가 30초 이상 계속 실행되도록 합니다. 이는 애플리케이션에서 호출한 패키지 전환을 모니터링할 수 있는 환경을 제공합니다.
--hprof설정된 경우 이 옵션은 Monkey 이벤트 시퀀스 직전 및 직후에 프로파일링 보고서를 생성합니다. 이 경우 데이터/misc에 대용량(~5Mb) 파일이 생성되므로 주의해서 사용하세요. 프로파일링 보고서 분석에 관한 자세한 내용은 앱 성능 프로파일링을 참조하세요.
--ignore-crashes일반적으로 Monkey는 애플리케이션이 비정상 종료되거나 처리되지 않은 예외 유형이 발생하면 중지됩니다. 이 옵션을 지정하면 Monkey는 계산이 완료될 때까지 시스템에 이벤트를 계속 전송합니다.
--ignore-timeouts일반적으로 Monkey는 애플리케이션에 모든 유형의 시간 제한 오류가 발생하면 중지됩니다(예: '애플리케이션 응답 없음' 대화상자). 이 옵션을 지정하면 Monkey는 계산이 완료될 때까지 시스템에 이벤트를 계속 전송합니다.
--ignore-security-exceptions일반적으로 Monkey는 애플리케이션에 모든 유형의 권한 오류가 발생하면 중지됩니다(예: 특정 권한이 필요한 활동을 시작하려고 하는 경우). 이 옵션을 지정하면 Monkey는 계산이 완료될 때까지 시스템에 이벤트를 계속 전송합니다.
--kill-process-after-error일반적으로 Monkey가 오류로 인해 중지되면 실패한 애플리케이션은 계속 실행됩니다. 이 옵션을 설정하면 시스템에 오류가 발생한 프로세스를 중지하라는 신호를 보냅니다. 참고: 정상적(성공적)으로 완료가 되면 실행된 프로세스는 중지되지 않으며 기기는 최종 이벤트 후 마지막 상태로 유지됩니다.
--monitor-native-crashesAndroid 시스템 네이티브 코드에서 발생하는 비정상 종료를 감시하고 보고합니다. --kill-process-after-error를 설정하면 시스템은 중지됩니다.
--wait-dbg디버거가 연결될 때까지 Monkey 실행을 중지합니다.


!!주의!!

이벤트 갯수는 제일 마지막에 넣어야 합니다. 

틀린 예제
./adb shell monkey -p your.company.name -v 50 --throttle 1000

맞는 예제
./adb shell monkey -p your.company.name --throttle 1000 -v 50


소스 코드

from adbutils import adb
import re

class adb_utils_rp():
	def adb_connect(self, serial=None):
		self.d = adb.device(serial=serial)
		if self.d.serial==None :
			return None
		print("adb conneced",self.d.serial)
		return self.d

	def get_prop(self, keyname=None):
		ret_dict = {}
		# 전체 poperty 는 getprop 명령으로 획득 가능합니다.
		ret = self.d.shell("getprop")
		#print(ret)
		retlines = ret.replace("]\n","]\0")
		retlines = retlines.split("\0")
		
		for one_line in retlines:
			one_line = one_line.replace("]: [","]"+"\0"+"[")
			dat = one_line.split("\0")
			#print(dat)
			dat1 = dat[1].strip()
			ret_dict[dat[0][1:len(dat[0])-1]] = dat1[1:len(dat1)-1]
			#print(dat[0][1:len(dat[0])-1],ret_dict[dat[0][1:len(dat[0])-1]])
		
		if keyname==None:
			return ret_dict
		
		return ret_dict.get(keyname)

	def make_bugreportz(self, log_zip_filename="log.zip"):
		# bugreportz 는 /data/user_de/0/com.android.shell/files/bugreports/ 에 로그를 생성하며 생성시 이전로그가 삭제 됩니다.
		#            리턴값은 OK:로 시작하며 뒤에는 경로명이 넘어 옵니다.
		#            예) OK:/data/user_de/0/com.android.shell/files/bugreports/dumpstate-2022-05-14-17-33-03.zip
		#
		# 동작중 다시 호출하면 아래와 같은 string이 리턴됩니다.
		# Previous sys dump or full dump is running, so skip this one

		ret = self.d.shell("bugreportz")
		print(ret)

		isok = ret.split(":")
		if len(isok)!=2 or isok[0]!='OK':
			return -1
		
		# 로그 뜬뒤 로그 버퍼를 초기화한다.
		self.d.shell("logcat -b all -c")
		
		# return 은 int size가 넘어옵니다.
		# 파일이 없으면 exception 발생합니다.
		#    adbutils.errors.AdbError: open failed: No such file or directory
		ret=self.d.sync.pull(isok[1], log_zip_filename)
		if ret==0:
			return -2
		
		# int size 가 리턴됩니다.
		return ret
		
	#EVENT LOG (logcat -b events -v threadtime -v printable -v uid -d *:v)
	def get_event_log(self):
		ret = self.d.shell("logcat -b events -v threadtime -v printable -v uid -d *:v")
		#print(ret)
		return ret
		
	# @return
	# None : 오류 없음
	# else : 오류 있음
	def check_event_log(self):
		find_str = "(am_crash)|(am_anr)"
		ret = self.get_event_log()
		#ret = " \n\n\n\n am_cras \n  am_anr "
		finded = re.search(find_str,ret)
		return finded
		
	def monkey(self,package,count,seed=0,throttle=300,extra=""):
		return self.d.shell(f"monkey -p {package} -s {seed} --throttle {throttle} {extra} -v {count}")
		
if __name__ == "__main__":
	adbrp = adb_utils_rp()
	adbrp.adb_connect()
	print(adbrp.get_prop("ro.serialno"))
	print(adbrp.get_prop("ro.build.fingerprint"))
	#print(adbrp.make_bugreportz())
	
	# test가 종료될때까지 기다리게 됩니다.
	print(adbrp.monkey("com.android.chrome",500))
	
	#print(adbrp.get_event_log())
	#print(adbrp.check_event_log())
	if adbrp.check_event_log()!=None:
		adbrp.make_bugreportz()


예제 설명

기존 예제 재활용입니다. monkey를 실행시키고 앱이 중지했거나 anr이 발생하면 dump를 생성하도록 만들었습니다.

앱에서 에러가 발생했는지는 event log를 통해 확인합니다. monkey의 리턴값을 이용해도 될듯 한데 아직 crash 되었을때 monkey의 리턴값이 어떻게 넘어오는지 확인을 하지 못한 상태입니다.

monkey test에서 중요한 인자는 다음과 같습니다.

-p 테스트 하고자 하는 특정 패키지를 실행시킵니다.

--throttle 인자는 테스트 하는 앱으로 키를 보내게 되는데 ms 단위로 보내게됩니다. 즉 너무 짧으면 순식간에 휙 지나가 버리고 테스트도 제대로 안되는 경우가 많습니다. 그래서 시간을 넉넉히 여유를 주도록 합니다.

adbrp.monkey("com.android.chrome",500)

여기에서는 크롬앱을 500개 이벤트를 보내서 테스트 하도록 합니다. 이벤트라는것은 키를 누르거나 스크롤을 하거나 특정 좌표를 클릭하는것도 하나의 이벤트가 됩니다.

기본적으로 throttle은 300ms으로 해두었습니다. 너무 짧으면 순식간에 테스트가 끝나버리고 동작도 제대로 안되는 경우가 많습니다.