Add ability to start voice interaction session directly

Add internal API's for SystemUI to start a voice interaction session
directly, without using an intent.

Make the assist gesture use that ability, if available.

Change-Id: I88ce3c7514714eb45666884847193585a07417a9
diff --git a/api/current.txt b/api/current.txt
index fb487ee..912f08f 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -28170,6 +28170,7 @@
     method public void showSession(android.os.Bundle, int);
     field public static final java.lang.String SERVICE_INTERFACE = "android.service.voice.VoiceInteractionService";
     field public static final java.lang.String SERVICE_META_DATA = "android.voice_interaction";
+    field public static final int START_SOURCE_SYSTEM = 4; // 0x4
     field public static final int START_WITH_ASSIST = 1; // 0x1
     field public static final int START_WITH_SCREENSHOT = 2; // 0x2
   }
diff --git a/api/system-current.txt b/api/system-current.txt
index 84f9dbf..5f795de 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -30241,6 +30241,7 @@
     method public void showSession(android.os.Bundle, int);
     field public static final java.lang.String SERVICE_INTERFACE = "android.service.voice.VoiceInteractionService";
     field public static final java.lang.String SERVICE_META_DATA = "android.voice_interaction";
+    field public static final int START_SOURCE_SYSTEM = 4; // 0x4
     field public static final int START_WITH_ASSIST = 1; // 0x1
     field public static final int START_WITH_SCREENSHOT = 2; // 0x2
   }
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index 419b92b..bc020e9 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -82,6 +82,12 @@
      */
     public static final int START_WITH_SCREENSHOT = 1<<1;
 
+    /**
+     * Flag for use with {@link #showSession}: indicate that the session has been started from the
+     * system.
+     */
+    public static final int START_SOURCE_SYSTEM = 1<<2;
+
     IVoiceInteractionService mInterface = new IVoiceInteractionService.Stub() {
         @Override public void ready() {
             mHandler.sendEmptyMessage(MSG_READY);
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index 6450d52..144dca4 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -80,4 +80,20 @@
      */
     int stopRecognition(in IVoiceInteractionService service, int keyphraseId,
             in IRecognitionStatusCallback callback);
+
+    /**
+     * Indicates whether any voice interaction service is currently active.
+     */
+    boolean isServiceActive();
+
+    /**
+     * Shows the session for the currently active service. Used to start a new session from system
+     * affordances.
+     */
+    void showSessionForActiveService();
+
+    /**
+     * Indicates whether there is a voice session running (but not necessarily showing).
+     */
+    boolean isSessionRunning();
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b0771dd..f427f2b 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3009,6 +3009,14 @@
         android:description="@string/permdesc_bindCarrierMessagingService"
         android:protectionLevel="signature|system" />
 
+    <!-- Allows an application to interact with the currently active
+         {@link android.service.voice.VoiceInteractionService}.
+         @hide -->
+    <permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE"
+        android:protectionLevel="signature"
+        android:description="@string/permdesc_accessVoiceInteractionService"
+        android:label="@string/permlab_accessVoiceInteractionService" />
+
     <!-- The system process is explicitly the only one allowed to launch the
          confirmation UI for full backup/restore -->
     <uses-permission android:name="android.permission.CONFIRM_FULL_BACKUP"/>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6cd3139..4d90932 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2462,6 +2462,11 @@
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_bindCarrierMessagingService">Allows the holder to bind to the top-level interface of a carrier messaging service. Should never be needed for normal apps.</string>
 
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_accessVoiceInteractionService">interact with voice interaction service</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_accessVoiceInteractionService">Allows the holder to interact with the currently active voice interaction service. Should never be needed for normal apps.</string>
+
     <!-- Policy administration -->
 
     <!-- Title of policy access to limiting the user's password choices -->
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 7a58c87..6a3e89e 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -120,6 +120,9 @@
     <!-- Screen Capturing -->
     <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" />
 
+    <!-- Assist -->
+    <uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE" />
+
     <application
         android:name=".SystemUIApplication"
         android:persistent="true"
diff --git a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java b/packages/SystemUI/src/com/android/systemui/SearchPanelView.java
index 445b499..f0e6c43 100644
--- a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/SearchPanelView.java
@@ -27,16 +27,20 @@
 import android.media.AudioAttributes;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.os.Vibrator;
 import android.provider.Settings;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.Slog;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
+import com.android.internal.app.IVoiceInteractionManagerService;
 import com.android.systemui.statusbar.BaseStatusBar;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarPanel;
@@ -70,6 +74,8 @@
     private float mStartDrag;
     private boolean mLaunchPending;
 
+    private IVoiceInteractionManagerService mVoiceInteractionManagerService;
+
     public SearchPanelView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
@@ -80,6 +86,14 @@
         mThreshold = context.getResources().getDimensionPixelSize(R.dimen.search_panel_threshold);
     }
 
+    private void startAssist() {
+        if (isVoiceInteractorActive()) {
+            startVoiceInteractor();
+        } else {
+            startAssistActivity();
+        }
+    }
+
     private void startAssistActivity() {
         if (!mBar.isDeviceProvisioned()) return;
 
@@ -106,6 +120,23 @@
         }
     }
 
+    private void startVoiceInteractor() {
+        try {
+            mVoiceInteractionManagerService.showSessionForActiveService();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call showSessionForActiveService", e);
+        }
+    }
+
+    private boolean isVoiceInteractorActive() {
+        try {
+            return mVoiceInteractionManagerService.isServiceActive();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call isServiceActive", e);
+            return false;
+        }
+    }
+
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
@@ -113,6 +144,9 @@
         mCircle = (SearchPanelCircleView) findViewById(R.id.search_panel_circle);
         mLogo = (ImageView) findViewById(R.id.search_logo);
         mScrim = findViewById(R.id.search_panel_scrim);
+        mVoiceInteractionManagerService = (IVoiceInteractionManagerService)
+                IVoiceInteractionManagerService.Stub.asInterface(
+                        ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
     }
 
     private void maybeSwapSearchIcon() {
@@ -320,7 +354,7 @@
             return;
         }
         mLaunching = true;
-        startAssistActivity();
+        startAssist();
         vibrate();
         mCircle.setAnimatingOut(true);
         mCircle.startExitAnimation(new Runnable() {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index f032ccf..d8b9140 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -707,6 +707,60 @@
         }
 
         @Override
+        public boolean isServiceActive() {
+            synchronized (this) {
+                if (mContext.checkCallingPermission(
+                        Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
+                                != PackageManager.PERMISSION_GRANTED) {
+                    throw new SecurityException("Caller does not hold the permission "
+                            + Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE);
+                }
+                return mImpl != null;
+            }
+        }
+
+        @Override
+        public void showSessionForActiveService() {
+            synchronized (this) {
+                if (mContext.checkCallingPermission(
+                        Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
+                        != PackageManager.PERMISSION_GRANTED) {
+                    throw new SecurityException("Caller does not hold the permission "
+                            + Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE);
+                }
+                if (mImpl == null) {
+                    Slog.w(TAG, "showSessionForActiveService without running voice interaction"
+                            + "service");
+                    return;
+                }
+                final int callingPid = Binder.getCallingPid();
+                final int callingUid = Binder.getCallingUid();
+                final long caller = Binder.clearCallingIdentity();
+                try {
+                    mImpl.showSessionLocked(callingPid, callingUid, new Bundle() /* sessionArgs */,
+                            VoiceInteractionService.START_SOURCE_SYSTEM
+                                    | VoiceInteractionService.START_WITH_ASSIST
+                                    | VoiceInteractionService.START_WITH_SCREENSHOT);
+                } finally {
+                    Binder.restoreCallingIdentity(caller);
+                }
+            }
+        }
+
+        @Override
+        public boolean isSessionRunning() {
+            synchronized (this) {
+                if (mContext.checkCallingPermission(
+                        Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
+                        != PackageManager.PERMISSION_GRANTED) {
+                    throw new SecurityException("Caller does not hold the permission "
+                            + Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE);
+                }
+                return mImpl != null && mImpl.mActiveSession != null;
+            }
+        }
+
+        @Override
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
                     != PackageManager.PERMISSION_GRANTED) {