Os desenvolvedores de software, haсkers e segurança as vezes precisam-se de métodos de automação de atividades dos usuários. Como, por exemplo, escrever os scriptes que vão apertar os botões pelo software externo de jeito que o software levaria isto como seu próprio ação. Quem disse: “por trás automaticamente mexer no seu banco móvel”? Toma vergonha amigão! Nós estamos por automação pacífica. Hoje vamos ensinar o Accessibility Service fazer este trabalho no nosso aparelho para nós!
Aperte esses botões por mim!
Ainda no século 19, Hegel disse: “Trabalho de máquina deve deixar para as máquinas”. É difícil de descutir com este filósofo clássico alemão: acredito que Georg Wilhelm Friedrich não recusaria algumas ações automáticas, tais como: desligamento de som e apertar botão de “pular propaganda comercial” quando assistiria um vídeo no YouTube ou resgate de bonus por acesso de aplicativo. Além disso, que para tudo isso já temos uns ferramentas prontas!
Accessibility Service pode receber eventos que ocorrem na tela e ainda pode chamá-los. Por exemplo, procurar elementos desejados no aplicativo e clicar neles.
Instalar aplicativo do outro desenvolvedor com essa funcionalidade é bem perigoso. Todos nós sabemos sobre reputação do GooglePlay e mais ou menos conseguem se imaginar o que pode fazer este aplicativo com seu cliente bancário no celular. Portanto existem duas opções: ou descompilar o software de outra pessoa e ver para onde ele clica ou desenvolver alguma coisa própria por exatamente suas tarefas.
Para objetivos de pesquisa, eu criei um aplicativo que vai clicar no si mesmo a partir de seu próprio serviço.
Preparação de serviço
Para iniciar o nosso serviço temos que habilitar permissões de root na seção de configurações especiais. Para fazer isso é desejável saber como redirecionar o usuário para lá. Além disso, podemos verificar se aplicativo tem essas permissões.
protected boolean checkAccess() { String string = getString(R.string.accessibilityservice_id); for (AccessibilityServiceInfo id : ((AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE)).getEnabledAccessibilityServiceList(AccessibilityEvent.TYPES_ALL_MASK)) { if (string.equals(id.getId())) { return true; } } return false; }
Sendo accessibilityservice_id é uma linha “nome do pacote/.serviço” que no nosso caso é ru.androidtools.selfclicker/.ClickService
Aqui é descrição do serviço de manifest:
<service android:name="ru.androidtools.selfcliker.ClickService" android:label="@string/accessibility_service_label" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/serviceconfig" /> </service>
O parâmetro label é responsável pelo nome do aplicativo nas configurações de serviço de recursos especiais. Na seção de meta-data você determina a descrição das funções necessárias para funcionamento de serviço. Aqui está arquivo serviceconfig:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews|flagReportViewIds" android:canRetrieveWindowContent="true" android:settingsActivity="ru.androidtools.selfcliker.MainActivity" />
O arquivo descreve atribuições de serviço e tipos de eventos que ele pode manipular e as atividades que serão lançadas para configurar o serviço.
Descrição completa dos parâmetros sempre está presente na documentação.
O ciclo de vida do serviço está controlado pelo sistema. A gente não pode parar o serviço. O sistema operacional vai encerrar serviços desnecessários (para que rodar um serviço para aplicativo que não está sendo executado?)
Podemos vincular o serviço a um aplicativo desejado. Quando a gente permitiu funcionamento, o serviço chamará o método onServiceConnected. E ai, vamos chamar o método setServiceInfo() com argumento AccessibilityServiceInfo. A matriz de linha packageNames é responsável por filtração de aplicativos com os quais o serviço opera.
@Override protected void onServiceConnected() { super.onServiceConnected(); Log.v(TAG, "onServiceConnected"); AccessibilityServiceInfo info = new AccessibilityServiceInfo(); info.flags = AccessibilityServiceInfo.DEFAULT | AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS | AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS; info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; info.packageNames = new String[]{"ru.androidtools.selfcliсker"}; setServiceInfo(info); }
Trabalhamos com eventos AccessibilityEvent
Após de distribuir todas as permissões é necessário iniciar aplicativo desejado. Seja muito fácil, se saber o nome do pacote
private void startApp() { Intent launchIntent = getPackageManager().getLaunchIntentForPackage("ru.androidtools.selfclicker"); // Corra do lugar certo sem o histórico da aplicação launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(launchIntent); }
Para executar aplicativo de zero usamos marcador Intent.FLAG_ACTIVITY_CLEAR_TOP. No outro caso aplicativo pode voltar para tela de estado antigo (diferente do estado da tela inicial).
Agora vamos procegir eventos no método onAccessibilityEvent. Este evento tem um tipo que pode determinar o quê aconteceu (por exemplo, mudou de janela, clicou no elemento, focou no elemento). Para conseguir a fonte de evento AccessibilityNodeInfo é necessário no objeto de evento chamar o método getSource().
A fonte tem muitas propriedades úteis que ajudem bastante durante o trabalho: texto, ID e nome da classe. Ela pode ter os elementos primários ou secundários. Pode clicar nela isClickable() mas para isso tem que chamar o método performAction(AccesibilityNodeInfo.ACTION_CLICK). Se quer ações mais globais, como por exemplo, apertar o botão “Voltar” no aparelho, tem que chamar o método performGlobalAction() com argumento desejado.
Para achar na tela AccessibilityNodeInfo, podemos chamar um dos métodos: buscar por ID (findAccessibilityNodeInfosByViewId) e buscar por texto (findAccessibilityNodeInfosByText). Fique pronto que vai devolver uma matriz dos elementos ou nenhum.
Vamos treinar
Aqui está o layout de nossa tela experimental:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin"> <Button android:id="@+id/buttonTest" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:onClick="testButtonClick" android:text="id/buttonTest" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="Sem identificação" android:onClick="noIdClick" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorAccent" android:onClick="linearLayoutClick" android:orientation="vertical"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="Clickable LinearLayout" android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large" /> <Button android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="Botão de não funcionamento" /> </LinearLayout> </LinearLayout>
Alguns elementos têm parâmetros de ID e texto, outros apenas texto e nos outros não pode clicar. As vezes os desenvolvedores colocam cliques nas áreas que são maiores do que elemento com texto ou imagem.
Vamos estudar essa tarefa pelo método debugClick
private void debugClick(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) { AccessibilityNodeInfo nodeInfo = event.getSource(); if (nodeInfo == null) { return; } nodeInfo.refresh(); Log.d(TAG, "ClassName:" + nodeInfo.getClassName() + " Text:" + nodeInfo.getText() + " ViewIdResourceName:" + nodeInfo.getViewIdResourceName() + " isClickable:" + nodeInfo.isClickable()); } }
Vemos no log informação seguinte:
03-03 16:23:15.220 24461-24461/ru.androidtools.selfclicker D/ClickService: ClassName:android.widget.Button Text:ID/BUTTONTEST ViewIdResourceName:ru.androidtools.selfclicker:id/buttonTest isClickable:true 03-03 16:23:26.356 24461-24461/ru.androidtools.selfclicker D/ClickService: ClassName:android.widget.Button Text:БЕЗ ID ViewIdResourceName:null isClickable:true 03-03 16:23:36.697 24461-24461/ru.androidtools.selfclicker D/ClickService: ClassName:android.widget.LinearLayout Text:null ViewIdResourceName:null isClickable:true 03-03 16:23:44.320 24461-24461/ru.androidtools.selfclicker D/ClickService: ClassName:android.widget.Button Text:НЕРАБОЧАЯ КНОПКА ViewIdResourceName:ru.androidtools.selfclicker:id/button3 isClickable:true
Para reproduzir a sequência de cliques, primeiramente, tem que estudar elementos que vão ser apertados. As vezes a sequência de cliques também é necessária.
Para apertar os primeiros dois botões pode usar findAccessibilityNodeInfosByText e findAccessibilityNodeInfosByViewId. Se texto é repetido mais que isso pode verificar ClassName ou parente.
Para clicar no nosso LinearLayout deve ter seu AccessibilityNodeInfo, ele não tem ID mas tem elementos secundários TextView e Button que já tem texto.
Vamos buscar um deles e depois clicar no pai dele.
private boolean linearClick(AccessibilityNodeInfo nodeInfo) { List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("Нерабочая кнопка"); if (list.size() > 0) { for (AccessibilityNodeInfo node : list) { AccessibilityNodeInfo parent = node.getParent(); parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); } return true; } else return false; }
Existem situações contrárias quando têm primário mas clicamos nos secundários. Para isso deve utilizar nodeInfo.getChildCount() e chamar o elemento em ciclo pelo ID nodeInfo.getChild(Id) (se não me engano, a numeração de ID começa de zero).
Iniciar trabalho de serviço seja melhor com evento de troca de janela:
event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
Se todo algoritmo de ações já está pronto, pode acionar serviço automaticamente através de AlarmManager, por exemplo uma vez por 24 h.
private void setRepeatTask() { Intent alarmIntent = new Intent(this, ClickService.class); PendingIntent pendingIntent = PendingIntent.getService( this, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); // Começamos às 10:00 Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); calendar.set(Calendar.HOUR_OF_DAY, 10); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); manager.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, // Repeat every 24 hours pendingIntent); }
Cancelar pode assim:
public void cancelRepeat() { Intent intent = new Intent(this, ClickService.class); final PendingIntent pIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager alarm = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); alarm.cancel(pIntent); }
Conclusão
A classe AccessibilityService permite evitar operações de rotina no seu aparelho de Android. Ele tem bastante recursos para a realização quase qualquer tarefa, só tem que dar permissões e achar um elemento que pode ser clicado na tela.