2016-07-20 20 views
19

Ich versuche, Google Now benutzerdefinierte Befehle akzeptieren zu lassen und eine Absicht an meine App zu senden, wenn eine bestimmte Anfrage gestellt wird.Benutzerdefinierte Befehle für Google Now

Ich habe dies erfolgreich mit Tasker und Autovoice, aber ich möchte das gleiche tun, ohne diese Anwendungen zu verwenden.

Ich fand diese link in der Dokumentation. Wo kann ich mit gemeinsamen Absichten umgehen, die meine Aufgabe nicht erfüllten?

Ich habe auch die Voice Interaction API von Google, die fast die gleiche Sache ist, versucht, aber das hat nicht geholfen.

Hat jemand das hier erreicht, ohne andere Anwendungen wie Commander, Autovoice oder Tasker zu benutzen?

Antwort

18

Google Now akzeptiert derzeit keine benutzerdefinierten Befehle. Die Apps, die Sie detailliert verwenden, verwenden einen AcccessibilityService "Hack", um den Sprachbefehl abzufangen, oder für gerootete Geräte, die xposed framework.

Sie handeln dann entweder gleichzeitig töten Google Now, oder ignorieren Sie sie und erlauben Sie Google, seine Ergebnisse wie üblich anzuzeigen.

Aus vielen Gründen ist dies eine schlechte Idee:

  1. Google einen Weg findet, wird diese Art der Interaktion zu verhindern, wenn es Common-Platz wird, da sie offensichtlich nicht ihren Now-Dienst sein will negativ beeinflusst.
  2. Es verwendet fest codierte Konstanten, die sich auf die Ansichtsklassen beziehen, die Google zum Anzeigen des Sprachbefehls verwendet. Dies kann sich natürlich mit jeder Veröffentlichung ändern.
  3. Hacks Pause!

Haftungsausschluss vollständig! Benutzung auf eigene Gefahr....

Sie benötigen einen AccessibilityService im Manifest registrieren:

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

und fügen Sie die Konfigurationsdatei 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"/> 

Sie optional hinzufügen:

android:packageNames="xxxxxx" 

oder verlängern die Funktionalität durch Hinzufügen weiterer Ereignistypen:

android:accessibilityEventTypes="typeViewTextSelectionChanged|typeWindowContentChanged|typeNotificationStateChanged" 

Fügen Sie die folgende AccessibilityService Beispielklasse:

/* 
* 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"); 
     } 
    } 
} 

ich die Klasse so ausführlich wie möglich eine eingekerbte gemacht, so dass es hoffentlich einfacher zu folgen.

Es macht folgendes:

  1. Überprüfen Sie, ob der Ereignistyp den richtigen Typ ist
  2. Überprüfen Sie, ob das Paket von Googles ‚Now‘ ist
  3. den Knoten-Info für das hartcodierte prüfen Klassentypen
  4. prüfen für die Zwischensprachbefehl, wie es in der Ansicht
  5. prüfen für die endgültige Sprachbefehl geladen wird, wenn es in die Ansicht geladen wird
  6. Recursively die Ansichten überprüfen für Sprachbefehle
  7. den Zeitunterschied zwischen den Ereignissen Prüfen
  8. Prüfen, ob der Sprachbefehl auf den zunächst

Um Test nachgewiesen identisch ist:

  1. das Enable Service in den Einstellungen für die Barrierefreiheit von Android
  2. Ihre Anwendung muss möglicherweise neu gestartet werden, damit der Dienst ordnungsgemäß registriert wird
  3. beginnen Google-Spracherkennung und sagen: „tun Zwischenergebnisse arbeiten“
  4. Beenden Sie Google Now
  5. Starten Sie die Anerkennung und sagen: „Endergebnisse arbeiten tun“

Die oben demonstriert die extrahierte Text/Befehl aus beiden hartcodierten Ansichten. Wenn Sie Google Now nicht neu starten, wird der Befehl weiterhin als Interim erkannt.

Mit dem extrahierten Sprachbefehl müssen Sie einen eigenen Sprachvergleich durchführen, um festzustellen, ob dies ein Befehl ist, an dem Sie interessiert sind. Wenn dies der Fall ist, müssen Sie Google daran hindern, die Ergebnisse zu sprechen oder anzuzeigen. Dies wird erreicht, indem Google Now getilgt oder eine leere Suchabsicht für die Stimme gesendet wird, die Flags enthält, die clear/reset task lauten sollten.

Sie werden in einem Race-Zustand sein, dies zu tun, so dass Ihre Sprachverarbeitung ziemlich schlau sein muss, oder ziemlich einfach ....

Hoffnung, das hilft.

EDIT:

Für diejenigen fragen, zu ‚töten‘ Google Now, müssen Sie entweder die Erlaubnis haben, Prozesse zu töten, oder eine leere („“) Suchabsicht die aktuelle Suche löschen senden :

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; 

} 
+0

Vielen Dank für Ihre Bemühungen in diese ausführliche Antwort schreiben –

+0

die logcat zeigt diese. 10 I/MyAccessibilityService: onAccessibilityEvent: childCount: 4 I/MeinAccessibilityService: examineChild I/MyAccessibilityService: onAccessibilityEvent: untergeordneter Text: null wenn ich spreche "tun Zwischenergebnisse arbeiten". –

+0

@Hardeep danke für die Prämie. Hast du immer noch das Problem oben? Wenn die Anzahl der Kinder 4 ist, sollte sie durchgeschleift werden. Ich nehme an, Ihr Logcat ist voller Output und Sie filtern es nicht? Wenn es nicht funktioniert, lassen Sie mich wissen die Versionsnummer von Google Jetzt verwenden Sie von der Android-Anwendung Info – brandall

2

Nicht das, was Sie hören wollen, aber die aktuelle Version der API keine benutzerdefinierten Stimme Befehle zum:

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

Anmerkung: Wir sind keine Anfragen für Custom Voice Actions zu akzeptieren. Bleiben Sie auf Voice-Aktionen abgestimmt - Google-Entwickler und + GoogleDevelopers für Produktupdates.

+0

Dank für die Beantwortung, aber dieser Teil habe ich schon in der Frage erwähnt und es nicht viel geholfen haben ... :( –