studio home google espaƱol developer actions java android google-now google-voice google-voice-search

java - home - ok google android studio



Comandos personalizados para Google Now (2)

Google Now actualmente no ''acepta'' comandos personalizados. Las aplicaciones que detalla utilizan un ''truco'' de AcccessibilityService para interceptar el comando de voz, o para dispositivos rooteados, el marco de referencia .

Entonces, actúan sobre ellos, matando simultáneamente a Google Now, o los ignoran y permiten que Google muestre sus resultados como de costumbre.

Por muchas razones, esta es una mala idea:

  1. Google encontrará una manera de evitar este tipo de interacción si se convierte en un lugar común, ya que obviamente no querrán que su servicio de Now se vea afectado negativamente.
  2. Utiliza constantes codificadas, relacionadas con las clases de vista que usa Google para mostrar el comando de voz. Esto, por supuesto, está sujeto a cambios con cada lanzamiento.
  3. Hacks se rompen!

Descargo de responsabilidad completa! Úselo bajo su propio riesgo....

Necesita registrar un Servicio de AccessibilityService en el Manifest :

<service android:name="com.something.MyAccessibilityService" android:enabled="true" android:label="@string/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/accessibilityconfig" /> </service>

Y agrega el archivo de configuración a res/xml :

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowContentChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagIncludeNotImportantViews" android:canRetrieveWindowContent="true" android:description="@string/accessibility_description" android:notificationTimeout="100" android:settingsActivity="SettingsActivity"/>

Opcionalmente puede agregar:

android:packageNames="xxxxxx"

o amplíe la funcionalidad agregando más tipos de eventos:

android:accessibilityEventTypes="typeViewTextSelectionChanged|typeWindowContentChanged|typeNotificationStateChanged"

Incluya la siguiente clase de ejemplo de AccessibilityService :

/* * Copyright (c) 2016 Ben Randall * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.your.package; import android.accessibilityservice.AccessibilityService; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; /** * @author benrandall76 AT gmail DOT com */ public class MyAccessibilityService extends AccessibilityService { private final boolean DEBUG = true; private final String CLS_NAME = MyAccessibilityService.class.getSimpleName(); private static final String GOOGLE_VOICE_SEARCH_PACKAGE_NAME = "com.google.android.googlequicksearchbox"; private static final String GOOGLE_VOICE_SEARCH_INTERIM_FIELD = "com.google.android.apps.gsa.searchplate.widget.StreamingTextView"; private static final String GOOGLE_VOICE_SEARCH_FINAL_FIELD = "com.google.android.apps.gsa.searchplate.SearchPlate"; private static final long COMMAND_UPDATE_DELAY = 1000L; private long previousCommandTime; private String previousCommand = null; private final boolean EXTRA_VERBOSE = false; @Override protected void onServiceConnected() { super.onServiceConnected(); if (DEBUG) { Log.i(CLS_NAME, "onServiceConnected"); } } @Override public void onAccessibilityEvent(final AccessibilityEvent event) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent"); } if (event != null) { switch (event.getEventType()) { case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: checking for google"); } if (event.getPackageName() != null && event.getPackageName().toString().matches( GOOGLE_VOICE_SEARCH_PACKAGE_NAME)) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: checking for google: true"); Log.i(CLS_NAME, "onAccessibilityEvent: event.getPackageName: " + event.getPackageName()); Log.i(CLS_NAME, "onAccessibilityEvent: event.getClassName: " + event.getClassName()); } final AccessibilityNodeInfo source = event.getSource(); if (source != null && source.getClassName() != null) { if (source.getClassName().toString().matches( GOOGLE_VOICE_SEARCH_INTERIM_FIELD)) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: className interim: true"); Log.i(CLS_NAME, "onAccessibilityEvent: source.getClassName: " + source.getClassName()); } if (source.getText() != null) { final String text = source.getText().toString(); if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: interim text: " + text); } if (interimMatch(text)) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: child: interim match: true"); } if (commandDelaySufficient(event.getEventTime())) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: commandDelaySufficient: true"); } if (!commandPreviousMatches(text)) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: commandPreviousMatches: false"); } previousCommandTime = event.getEventTime(); previousCommand = text; killGoogle(); if (DEBUG) { Log.e(CLS_NAME, "onAccessibilityEvent: INTERIM PROCESSING: " + text); } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: commandPreviousMatches: true"); } } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: commandDelaySufficient: false"); } } break; } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: child: interim match: false"); } } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: interim text: null"); } } } else if (source.getClassName().toString().matches( GOOGLE_VOICE_SEARCH_FINAL_FIELD)) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: className final: true"); Log.i(CLS_NAME, "onAccessibilityEvent: source.getClassName: " + source.getClassName()); } final int childCount = source.getChildCount(); if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: childCount: " + childCount); } if (childCount > 0) { for (int i = 0; i < childCount; i++) { final String text = examineChild(source.getChild(i)); if (text != null) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: child text: " + text); } if (finalMatch(text)) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: child: final match: true"); } if (commandDelaySufficient(event.getEventTime())) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: commandDelaySufficient: true"); } if (!commandPreviousMatches(text)) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: commandPreviousMatches: false"); } previousCommandTime = event.getEventTime(); previousCommand = text; killGoogle(); if (DEBUG) { Log.e(CLS_NAME, "onAccessibilityEvent: FINAL PROCESSING: " + text); } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: commandPreviousMatches: true"); } } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: commandDelaySufficient: false"); } } break; } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: child: final match: false"); } } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: child text: null"); } } } } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: className: unwanted " + source.getClassName()); } if (EXTRA_VERBOSE) { if (source.getText() != null) { final String text = source.getText().toString(); if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: unwanted text: " + text); } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: unwanted text: null"); } } final int childCount = source.getChildCount(); if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: unwanted childCount: " + childCount); } if (childCount > 0) { for (int i = 0; i < childCount; i++) { final String text = examineChild(source.getChild(i)); if (text != null) { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: unwanted child text: " + text); } } } } } } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: source null"); } } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: checking for google: false"); } } break; default: if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: not interested in type"); } break; } } else { if (DEBUG) { Log.i(CLS_NAME, "onAccessibilityEvent: event null"); } } } /** * Check if the previous command was actioned within the {@link #COMMAND_UPDATE_DELAY} * * @param currentTime the time of the current {@link AccessibilityEvent} * @return true if the delay is sufficient to proceed, false otherwise */ private boolean commandDelaySufficient(final long currentTime) { if (DEBUG) { Log.i(CLS_NAME, "commandDelaySufficient"); } final long delay = (currentTime - COMMAND_UPDATE_DELAY); if (DEBUG) { Log.i(CLS_NAME, "commandDelaySufficient: delay: " + delay); Log.i(CLS_NAME, "commandDelaySufficient: previousCommandTime: " + previousCommandTime); } return delay > previousCommandTime; } /** * Check if the previous command/text matches the current text we are considering processing * * @param text the current text * @return true if the text matches the previous text we processed, false otherwise. */ private boolean commandPreviousMatches(@NonNull final String text) { if (DEBUG) { Log.i(CLS_NAME, "commandPreviousMatches"); } return previousCommand != null && previousCommand.matches(text); } /** * Check if the interim text matches a command we want to intercept * * @param text the intercepted text * @return true if the text matches a command false otherwise */ private boolean interimMatch(@NonNull final String text) { if (DEBUG) { Log.i(CLS_NAME, "interimMatch"); } return text.matches("do interim results work"); } /** * Check if the final text matches a command we want to intercept * * @param text the intercepted text * @return true if the text matches a command false otherwise */ private boolean finalMatch(@NonNull final String text) { if (DEBUG) { Log.i(CLS_NAME, "finalMatch"); } return text.matches("do final results work"); } /** * Recursively examine the {@link AccessibilityNodeInfo} object * * @param parent the {@link AccessibilityNodeInfo} parent object * @return the extracted text or null if no text was contained in the child objects */ private String examineChild(@Nullable final AccessibilityNodeInfo parent) { if (DEBUG) { Log.i(CLS_NAME, "examineChild"); } if (parent != null) { for (int i = 0; i < parent.getChildCount(); i++) { final AccessibilityNodeInfo nodeInfo = parent.getChild(i); if (nodeInfo != null) { if (DEBUG) { Log.i(CLS_NAME, "examineChild: nodeInfo: getClassName: " + nodeInfo.getClassName()); } if (nodeInfo.getText() != null) { if (DEBUG) { Log.i(CLS_NAME, "examineChild: have text: returning: " + nodeInfo.getText().toString()); } return nodeInfo.getText().toString(); } else { if (DEBUG) { Log.i(CLS_NAME, "examineChild: text: null: recurse"); } final int childCount = nodeInfo.getChildCount(); if (DEBUG) { Log.i(CLS_NAME, "examineChild: childCount: " + childCount); } if (childCount > 0) { final String text = examineChild(nodeInfo); if (text != null) { if (DEBUG) { Log.i(CLS_NAME, "examineChild: have recursive text: returning: " + text); } return text; } else { if (DEBUG) { Log.i(CLS_NAME, "examineChild: recursive text: null"); } } } } } else { if (DEBUG) { Log.i(CLS_NAME, "examineChild: nodeInfo null"); } } } } else { if (DEBUG) { Log.i(CLS_NAME, "examineChild: parent null"); } } return null; } /** * Kill or reset Google */ private void killGoogle() { if (DEBUG) { Log.i(CLS_NAME, "killGoogle"); } // TODO - Either kill the Google process or send an empty intent to clear current search process } @Override public void onInterrupt() { if (DEBUG) { Log.i(CLS_NAME, "onInterrupt"); } } @Override public void onDestroy() { super.onDestroy(); if (DEBUG) { Log.i(CLS_NAME, "onDestroy"); } } }

Hice la clase lo más verbalmente posible, así que espero que sea más fácil de seguir.

Hace lo siguiente:

  1. Compruebe si el tipo de evento es del tipo correcto
  2. Compruebe si el paquete es de ''Ahora'' de Google
  3. Verifique la información del nodo para los tipos de clase codificados
  4. Verifique el comando de voz provisional mientras se carga en la vista
  5. Verifique el comando de voz final a medida que se carga en la vista
  6. Compruebe recursivamente las vistas para los comandos de voz
  7. Consulta la diferencia horaria entre los eventos.
  8. Compruebe si el comando de voz es idéntico al detectado anteriormente

Probar:

  1. Habilitar el Service en la configuración de accesibilidad de Android
  2. Es posible que deba reiniciar su aplicación para que el servicio se registre correctamente.
  3. Inicie el reconocimiento de voz de Google y diga " hacer el trabajo de resultados interinos "
  4. Salir de Google ahora
  5. Iniciar el reconocimiento y decir " hacer trabajar los resultados finales ".

Lo anterior demostrará el texto / comando extraído de ambas vistas codificadas. Si no reinicia Google Now, el comando aún se detectará como provisional.

Con el comando de voz extraído, debe realizar su propia coincidencia de idioma para determinar si este es un comando en el que está interesado. Si es así, debe evitar que Google hable o muestre los resultados. Esto se logra matando a Google Now o enviándole un intento de búsqueda de voz vacío, que contiene marcas que deberían clear/reset task .

Estarás en una condición de carrera haciendo esto, por lo que el procesamiento de tu idioma debe ser bastante inteligente o bastante básico ...

Espero que ayude.

EDITAR:

Para aquellos que preguntan, para ''matar'' a Google Now, debe tener permiso para detener los procesos o enviar un intento de búsqueda vacío ("") para borrar la búsqueda actual:

public static final String PACKAGE_NAME_GOOGLE_NOW = "com.google.android.googlequicksearchbox"; public static final String ACTIVITY_GOOGLE_NOW_SEARCH = ".SearchActivity"; /** * Launch Google Now with a specific search term to resolve * * @param ctx the application context * @param searchTerm the search term to resolve * @return true if the search term was handled correctly, false otherwise */ public static boolean googleNow(@NonNull final Context ctx, @NonNull final String searchTerm) { if (DEBUG) { Log.i(CLS_NAME, "googleNow"); } final Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); intent.setComponent(new ComponentName(PACKAGE_NAME_GOOGLE_NOW, PACKAGE_NAME_GOOGLE_NOW + ACTIVITY_GOOGLE_NOW_SEARCH)); intent.putExtra(SearchManager.QUERY, searchTerm); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); try { ctx.startActivity(intent); return true; } catch (final ActivityNotFoundException e) { if (DEBUG) { Log.e(CLS_NAME, "googleNow: ActivityNotFoundException"); e.printStackTrace(); } } catch (final Exception e) { if (DEBUG) { Log.e(CLS_NAME, "googleNow: Exception"); e.printStackTrace(); } } return false; }

Estoy tratando de hacer que Google Now acepte comandos personalizados y envíe un Intent a mi aplicación cuando se realiza una consulta en particular.

Hice esto con éxito usando Tasker y Autovoice, pero quiero hacer lo mismo sin usar estas aplicaciones.

He encontrado este link a la documentación. ¿Dónde puedo manejar intenciones comunes que no cumplieron mi tarea?

También probé la API de interacción de voz proporcionada por Google, que es casi lo mismo, pero esto no ayudó.

¿Alguien aquí ha logrado esto sin usar otras aplicaciones como Commander, Autovoice o Tasker?


No es lo que quieres escuchar, pero la versión actual de la API no permite comandos de voz personalizados:

Desde https://developers.google.com/voice-actions/custom-actions

Nota: No estamos aceptando solicitudes de acciones de voz personalizadas. Permanezca atento a las acciones de voz: Google Developers y + GoogleDevelopers para actualizaciones de productos.