Automatizamos el trabajo de programas para Android utilizando el Accessibility Service

por | 01.08.2017

Para la mayoria de usuarios existe la nesecidad de automatizar sus acciones – es decir, escribir los scripts que puede hacer clicks sobre los botones en las aplicaciones de otras personas para que parece que lo haga usted. ¿Quién dijo «automatizar ilegal el trabajo con el banco móvil»? Estamos apoyando la automatización legal. En el último artículo sobre Accessibility Service hemos llamado la atención en la obtención de los datos de pantalla del usuario, y hoy enseñaremos a Accessibility Service hacer el trabajo en el dispositivo por nosotros!

Haga clicks sobre estos botones por mí

Incluso en el siglo XIX, Hegel dice: «El trabajo de las maquinas es necesario dar a las maquinas». Y entonces con uno de los creadores de la filosofía clásica alemana es difícil de argumentar: No creo que Georg Wilhelm Friedrich se habría negado a automatizar unas acciones, tales como Silenciar el sonido y pulsar el botón «Omitir la publicidad» durante la visualización de un video en YouTube o recibir bonos diarios por visitar aplicación. Para todo esto tenemos una herramienta preparada!

Como comentamos en un artículo anterior, Accessibility Service puede recibir eventos que pasan en la pantalla, pero también puede hacerlos. Por ejemplo, encontrar un elemento en la aplicación, y hacer un clic en el.

Poner una aplicación de los otros con funciones similares es peligroso – todos sabemos la reputación de Google Play y aproximadamente imaginamos que este aplicación puede hacer con su cuenta bancaria en el teléfono. Hay dos opciones: o descompilar el software de otra persona y ver exactamente que hace o compilar uno para las tareas estrictas.

Para una investigación he creado una aplicación que va a hacer los clicks sobre los botones en sí mismo desde su servicio.

Preparación para el funcionamiento de servicio

Para que nuestro servicio comenzó a funcionar tenemos que proporcionar una configuración correcta en una sección especial. Cómo reenviar el usuario  ya losabes desde el primer artículo. Además, nosotros mismos podemos comprobar si existe el derecho en la aplicación.

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;
}

Aquí accessibilityservice_id es una cadena de forma «el nombre del paquete /.servicio», en nuestro caso es ru.androidtools.selfclicker/.ClickService.

Aquí está la descripción del servicio desde el manifiesto:

<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>

El parámetro label responde del nombre de la aplicación en la configuración del servicio de poderes especiales. En la sección de meta-data se ponen la referencia a la descripción de las funciones, necesarias para este tipo de servicio. Aquí esta archivo 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" />

En él se describen los poderes de servicio, los tipos de eventos que se puede procesar, y actividades, que se pone en marcha para ajustar el funcionamiento de operación.

Una descripción completa de estos parámetros, como siempre, puede encontrar  en la documentación.

El ciclo de vida de servicio esta controlado por el sistema. Nosotros no podemos detener el servicio. SO detenga de forma independiente los servicios innecesarios – por ejemplo, ¿para qué gastar la energia si la aplicación no se está ejecutando?

Podemos adjuntar el servicio con la aplicación estrictamente necesaria. Una vez que se ha dado autorización para trabajar, apareserá el método onServiceConnected. En ella, tenemos que solicitar setServiceInfo() con el parámetro AccessibilityServiceInfo. De la filtración de aplicaciones con que trabaja un servicio responde matriz de cadenas packageNames.

@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);
}

Trabajamos con con la función AccessibilityEvent

Después de la distribución de todos los permisos, tenemos que ejecutar una aplicación especial. Si conocemos el nombre del paquete hacerlo sea fácil:

private void startApp() {
  Intent launchIntent = getPackageManager().getLaunchIntentForPackage("ru.androidtools.selfclicker");
  // Run from the right place without the history of the application
  launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
  startActivity(launchIntent);
}

Para iniciar una aplicación de cero utilizamos una bandera Intent.FLAG_ACTIVITY_CLEAR_TOP. De lo contrario la aplicación se puede volver a la pantalla anterior, que está muy lejos de la pantalla de inicio.

Ahora hay que recoger eventos en el método onAccessibilityEvent. Este evento tiene su propia tarea, le ayudará a encontrar lo que sucedió (por ejemplo, ha cambiado la ventana, hice clic sobre el elemento). Para obtener el código de evento de AccessibilityNodeInfo, es necesario solicitar de este objeto de evento el método getSource().

Este fuente tiene muchas ventajas útiles para ayudar con el trabajo: texto, ID, nombre de la clase. Puede tener elementos base y unos sub elementos.

Puede ser clicable isClickable(), y para hacer clic en él como un usuario normal, hay que solicitar un método performAction (AccessibilityNodeInfo.ACTION_CLICK).

Si queremos hacer algo más global, por ejemplo pulsar «Volver» en el dispositivo, tiene que solicitar el método performGlobalAction() con el parametro requerido.

Para encontrar en la pantalla del AccessibilityNodeInfo necesario, podemos utilizar uno de los métodos: la búsqueda por ID (findAccessibilityNodeInfosByViewId) y la búsqueda por el texto (findAccessibilityNodeInfosByText). Esté preparado de que él nos va a devolver un conjunto de elementos, o ninguno de ellos.

Practicamos por las ventanillas

Aquí está la marcación de nuestra pantalla de pruebas:

<?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="Without ID"
    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="Non-working button" />
  </LinearLayout>
</LinearLayout>

Algunos elementos tienen ID y el texto, otros tienen el texto solamente, sobre algunos no podemos hacer un clic.

A veces los manipuladores de clics ponen en una zona que sobresale del dimensión del elemento con el texto o con la imagen.

Vamos a estudiar esta tarea utilizando 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());
    }
}

Eso es lo que ha salido en log file:

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 reproducir una secuencia de clics, tenemos que explorar los elementos que van a ser presionados. Pero a veces también es importante seguir por una secuencia de clics.

Para presionar los dos primeros botones podemos utilizar findAccessibilityNodeInfosByText y findAccessibilityNodeInfosByViewId. Si el texto por los elementos se repite, podemos comprobarlos con ClassName o por el elemento base.

Para hacer clic en nuestro LinearLayout, necesitamos conseguir su AccessibilityNodeInfo, No tiene ID, pero tiene sub elemento TextView y Button, que tiene un texto.

En primer lugar tenemos que conseguir uno de ellos y, a continuación, hacer clic a su elemento base.

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;
}

Hay una situación inversa, cuando hay un elemento base, y hacemos un click en sub elemento. Para ello, utilice el nodeInfo.getChildCount() y solicite un elemento en el ciclo por su Identificación nodeInfo.getChild(id) (si no me equivoco, la numeración de ID empieza de cero). Empezar el funcionamiento de servicio debe con una acción de cambio de ventana:

event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED

Si el algoritmo de acciones está preparado, puede iniciar el servicio en modo automatico a través de AlarmManager, por ejemplo una vez al día.

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);

  // We start at 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 esta tarea se puede haciendo lo siguiente:

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);
}

Conclusion

Clase de AccessibilityService te librará de las operaciones rutinarias en su dispositivo con sistema Android. Sus posibilidades pueden realizar casi cualquier tarea, lo más importante – dar permiso y encontrar un elemento en la pantalla sobre que se puede hacer un clic.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *