The voice assist may now be launched above the lockscreen

A possibility was introduced to launch voice assist over
the lockscreen using the left keyguard affordance.

Change-Id: Ic4618d24256b65441a50d77d0ef59b0ec99b6ead
diff --git a/api/current.txt b/api/current.txt
index 07a4ff7..7db6d5dd 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1190,6 +1190,7 @@
     field public static final int summaryOff = 16843248; // 0x10101f0
     field public static final int summaryOn = 16843247; // 0x10101ef
     field public static final int supportsAssist = 16844012; // 0x10104ec
+    field public static final int supportsLaunchVoiceAssistFromKeyguard = 16844022; // 0x10104f6
     field public static final int supportsRtl = 16843695; // 0x10103af
     field public static final int supportsSwitchingToNextInputMethod = 16843755; // 0x10103eb
     field public static final int supportsUploading = 16843419; // 0x101029b
@@ -29141,6 +29142,7 @@
     method public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(java.lang.String, java.util.Locale, android.service.voice.AlwaysOnHotwordDetector.Callback);
     method public static boolean isActiveService(android.content.Context, android.content.ComponentName);
     method public android.os.IBinder onBind(android.content.Intent);
+    method public void onLaunchVoiceAssistFromKeyguard();
     method public void onReady();
     method public void onShutdown();
     method public void showSession(android.os.Bundle, int);
diff --git a/api/system-current.txt b/api/system-current.txt
index 2880446..3431a29 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1265,6 +1265,7 @@
     field public static final int summaryOff = 16843248; // 0x10101f0
     field public static final int summaryOn = 16843247; // 0x10101ef
     field public static final int supportsAssist = 16844012; // 0x10104ec
+    field public static final int supportsLaunchVoiceAssistFromKeyguard = 16844022; // 0x10104f6
     field public static final int supportsRtl = 16843695; // 0x10103af
     field public static final int supportsSwitchingToNextInputMethod = 16843755; // 0x10103eb
     field public static final int supportsUploading = 16843419; // 0x101029b
@@ -31253,6 +31254,7 @@
     method public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(java.lang.String, java.util.Locale, android.service.voice.AlwaysOnHotwordDetector.Callback);
     method public static boolean isActiveService(android.content.Context, android.content.ComponentName);
     method public android.os.IBinder onBind(android.content.Intent);
+    method public void onLaunchVoiceAssistFromKeyguard();
     method public void onReady();
     method public void onShutdown();
     method public void showSession(android.os.Bundle, int);
diff --git a/core/java/android/service/voice/IVoiceInteractionService.aidl b/core/java/android/service/voice/IVoiceInteractionService.aidl
index e8265a2..e3d68a6 100644
--- a/core/java/android/service/voice/IVoiceInteractionService.aidl
+++ b/core/java/android/service/voice/IVoiceInteractionService.aidl
@@ -23,4 +23,5 @@
     void ready();
     void soundModelsChanged();
     void shutdown();
+    void launchVoiceAssistFromKeyguard();
 }
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index fee0c75..8c89ddb 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -98,6 +98,10 @@
         @Override public void soundModelsChanged() {
             mHandler.sendEmptyMessage(MSG_SOUND_MODELS_CHANGED);
         }
+        @Override
+        public void launchVoiceAssistFromKeyguard() throws RemoteException {
+            mHandler.sendEmptyMessage(MSG_LAUNCH_VOICE_ASSIST_FROM_KEYGUARD);
+        }
     };
 
     MyHandler mHandler;
@@ -113,6 +117,7 @@
     static final int MSG_READY = 1;
     static final int MSG_SHUTDOWN = 2;
     static final int MSG_SOUND_MODELS_CHANGED = 3;
+    static final int MSG_LAUNCH_VOICE_ASSIST_FROM_KEYGUARD = 4;
 
     class MyHandler extends Handler {
         @Override
@@ -127,6 +132,9 @@
                 case MSG_SOUND_MODELS_CHANGED:
                     onSoundModelsChangedInternal();
                     break;
+                case MSG_LAUNCH_VOICE_ASSIST_FROM_KEYGUARD:
+                    onLaunchVoiceAssistFromKeyguard();
+                    break;
                 default:
                     super.handleMessage(msg);
             }
@@ -134,6 +142,19 @@
     }
 
     /**
+     * Called when a user has activated an affordance to launch voice assist from the Keyguard.
+     *
+     * <p>This method will only be called if the VoiceInteractionService has set
+     * {@link android.R.attr#supportsLaunchVoiceAssistFromKeyguard} and the Keyguard is showing.</p>
+     *
+     * <p>A valid implementation must start a new activity that should use {@link
+     * android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} to display
+     * on top of the lock screen.</p>
+     */
+    public void onLaunchVoiceAssistFromKeyguard() {
+    }
+
+    /**
      * Check whether the given service component is the currently active
      * VoiceInteractionService.
      */
diff --git a/core/java/android/service/voice/VoiceInteractionServiceInfo.java b/core/java/android/service/voice/VoiceInteractionServiceInfo.java
index 997d586..463eb5b 100644
--- a/core/java/android/service/voice/VoiceInteractionServiceInfo.java
+++ b/core/java/android/service/voice/VoiceInteractionServiceInfo.java
@@ -44,6 +44,7 @@
     private String mRecognitionService;
     private String mSettingsActivity;
     private boolean mSupportsAssist;
+    private boolean mSupportsLaunchFromKeyguard;
 
     public VoiceInteractionServiceInfo(PackageManager pm, ComponentName comp)
             throws PackageManager.NameNotFoundException {
@@ -98,6 +99,9 @@
             mSupportsAssist = array.getBoolean(
                     com.android.internal.R.styleable.VoiceInteractionService_supportsAssist,
                     false);
+            mSupportsLaunchFromKeyguard = array.getBoolean(com.android.internal.
+                    R.styleable.VoiceInteractionService_supportsLaunchVoiceAssistFromKeyguard,
+                    false);
             array.recycle();
             if (mSessionService == null) {
                 mParseError = "No sessionService specified";
@@ -148,4 +152,8 @@
     public boolean getSupportsAssist() {
         return mSupportsAssist;
     }
+
+    public boolean getSupportsLaunchFromKeyguard() {
+        return mSupportsLaunchFromKeyguard;
+    }
 }
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index 4c6db24..644adb6 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -97,6 +97,12 @@
     void showSessionForActiveService(IVoiceInteractionSessionShowCallback showCallback);
 
     /**
+     * Notifies the active service that a launch was requested from the Keyguard. This will only
+     * be called if {@link #activeServiceSupportsLaunchFromKeyguard()} returns true.
+     */
+    void launchVoiceAssistFromKeyguard();
+
+    /**
      * Indicates whether there is a voice session running (but not necessarily showing).
      */
     boolean isSessionRunning();
@@ -106,4 +112,10 @@
      * assist gesture.
      */
     boolean activeServiceSupportsAssist();
+
+    /**
+     * Indicates whether the currently active voice interaction service is capable of being launched
+     * from the lockscreen.
+     */
+    boolean activeServiceSupportsLaunchFromKeyguard();
 }
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index fe5862b..29148178 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7306,6 +7306,9 @@
         <!-- Flag indicating whether this voice interaction service is capable of handling the
              assist action. -->
         <attr name="supportsAssist" format="boolean" />
+        <!-- Flag indicating whether this voice interaction service is capable of being launched
+             from the keyguard. -->
+        <attr name="supportsLaunchVoiceAssistFromKeyguard" format="boolean" />
     </declare-styleable>
 
     <!-- Use <code>voice-enrollment-application</code>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 91c3d2e..83ac6c1 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2685,4 +2685,5 @@
 
   <public type="attr" name="assistBlocked" />
   <public type="attr" name="stylusButtonPressable" />
+  <public type="attr" name="supportsLaunchVoiceAssistFromKeyguard" />
 </resources>
diff --git a/packages/SystemUI/res/drawable/ic_mic_26dp.xml b/packages/SystemUI/res/drawable/ic_mic_26dp.xml
new file mode 100644
index 0000000..83e4ba8
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_mic_26dp.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+   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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="26.0dp"
+        android:height="26.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:pathData="M12.000000,14.000000c1.700000,0.000000 3.000000,-1.300000 3.000000,-3.000000l0.000000,-6.000000c0.000000,-1.700000 -1.300000,-3.000000 -3.000000,-3.000000c-1.700000,0.000000 -3.000000,1.300000 -3.000000,3.000000l0.000000,6.000000C9.000000,12.700000 10.300000,14.000000 12.000000,14.000000zM17.299999,11.000000c0.000000,3.000000 -2.500000,5.100000 -5.300000,5.100000c-2.800000,0.000000 -5.300000,-2.100000 -5.300000,-5.100000L5.000000,11.000000c0.000000,3.400000 2.700000,6.200000 6.000000,6.700000L11.000000,21.000000l2.000000,0.000000l0.000000,-3.300000c3.300000,-0.500000 6.000000,-3.300000 6.000000,-6.700000L17.299999,11.000001z"
+        android:fillColor="#FF000000"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index 1057464..5d0367e 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -52,7 +52,7 @@
         android:contentDescription="@string/accessibility_camera_button" />
 
     <com.android.systemui.statusbar.KeyguardAffordanceView
-        android:id="@+id/phone_button"
+        android:id="@+id/left_button"
         android:layout_height="@dimen/keyguard_affordance_height"
         android:layout_width="@dimen/keyguard_affordance_width"
         android:layout_gravity="bottom|start"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 80fecac..e5a39c8 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -205,6 +205,8 @@
     <string name="accessibility_camera_button">Camera</string>
     <!-- Content description of the phone button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_phone_button">Phone</string>
+    <!-- Content description of the phone button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
+    <string name="accessibility_voice_assist_button">Voice Assist</string>
     <!-- Content description of the unlock button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_unlock_button">Unlock</string>
     <!-- Click action label for accessibility for the unlock button. [CHAR LIMIT=NONE] -->
@@ -212,6 +214,8 @@
     <!-- Click action label for accessibility for the phone button. [CHAR LIMIT=NONE] -->
     <string name="phone_label">open phone</string>
     <!-- Click action label for accessibility for the phone button. [CHAR LIMIT=NONE] -->
+    <string name="voice_assist_label">open voice assist</string>
+    <!-- Click action label for accessibility for the phone button. [CHAR LIMIT=NONE] -->
     <string name="camera_label">open camera</string>
     <!-- Caption for "Recents resize" developer debug feature. [CHAR LIMIT=NONE] -->
     <string name="recents_caption_resize">Select new task layout</string>
@@ -753,11 +757,14 @@
     <!-- Shows when people have pressed the unlock icon to explain how to unlock. [CHAR LIMIT=60] -->
     <string name="keyguard_unlock">Swipe up to unlock</string>
 
-    <!-- Shows when people have clicked at the left edge of the screen to explain how to open the phone. In right-to-left languages, this is the opposite direction. [CHAR LIMIT=60] -->
-    <string name="phone_hint">Swipe right for phone</string>
+    <!-- Shows when people have clicked on the phone icon [CHAR LIMIT=60] -->
+    <string name="phone_hint">Swipe from icon for phone</string>
 
-    <!-- Shows when people have clicked at the right edge of the screen to explain how to open the phone. In right-to-left languages, this is the opposite direction. [CHAR LIMIT=60] -->
-    <string name="camera_hint">Swipe left for camera</string>
+    <!-- Shows when people have clicked on the voice assist icon [CHAR LIMIT=60] -->
+    <string name="voice_hint">Swipe from icon for voice assist</string>
+
+    <!-- Shows when people have clicked on the camera icon [CHAR LIMIT=60] -->
+    <string name="camera_hint">Swipe from icon for camera</string>
 
     <!-- Interruption level: None. [CHAR LIMIT=40] -->
     <string name="interruption_level_none">Total silence</string>
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistGestureManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/assist/AssistGestureManager.java
rename to packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index d9f2324..d1f8963 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistGestureManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -33,11 +33,11 @@
 import com.android.systemui.statusbar.phone.PhoneStatusBar;
 
 /**
- * Class to manage everything around the assist gesture.
+ * Class to manage everything related to assist in SystemUI.
  */
-public class AssistGestureManager {
+public class AssistManager {
 
-    private static final String TAG = "AssistGestureManager";
+    private static final String TAG = "AssistManager";
     private static final String ASSIST_ICON_METADATA_NAME =
             "com.android.systemui.action_assist_icon";
 
@@ -77,7 +77,7 @@
         }
     };
 
-    public AssistGestureManager(PhoneStatusBar bar, Context context) {
+    public AssistManager(PhoneStatusBar bar, Context context) {
         mContext = context;
         mBar = bar;
         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
@@ -194,6 +194,14 @@
         }
     }
 
+    public void launchVoiceAssistFromKeyguard() {
+        try {
+            mVoiceInteractionManagerService.launchVoiceAssistFromKeyguard();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call launchVoiceAssistFromKeyguard", e);
+        }
+    }
+
     private boolean getVoiceInteractorSupportsAssistGesture() {
         try {
             return mVoiceInteractionManagerService.activeServiceSupportsAssist();
@@ -203,7 +211,16 @@
         }
     }
 
-    private ComponentName getVoiceInteractorComponentName() {
+    public boolean canVoiceAssistBeLaunchedFromKeyguard() {
+        try {
+            return mVoiceInteractionManagerService.activeServiceSupportsLaunchFromKeyguard();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to call activeServiceSupportsLaunchFromKeyguard", e);
+            return false;
+        }
+    }
+
+    public ComponentName getVoiceInteractorComponentName() {
         try {
             return mVoiceInteractionManagerService.getActiveServiceComponentName();
         } catch (RemoteException e) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java
index 23810f9..ee5eb38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarter.java
@@ -25,4 +25,5 @@
  */
 public interface ActivityStarter {
     public void startActivity(Intent intent, boolean dismissShade);
+    void preventNextAnimation();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
index 8343497..8bffdc9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
@@ -131,6 +131,10 @@
         mCenterIcon = mCallback.getCenterIcon();
         mRightIcon = mCallback.getRightIcon();
         mRightIcon.setIsLeft(false);
+        updatePreviews();
+    }
+
+    public void updatePreviews() {
         mLeftIcon.setPreviewView(mCallback.getLeftPreview());
         mRightIcon.setPreviewView(mCallback.getRightPreview());
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index fabc1a6..efc3ea0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -25,17 +25,13 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.InsetDrawable;
-import android.hardware.fingerprint.FingerprintManager;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.os.Vibrator;
 import android.provider.MediaStore;
-import android.provider.Settings;
 import android.telecom.TelecomManager;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -54,6 +50,7 @@
 import com.android.systemui.EventLogConstants;
 import com.android.systemui.EventLogTags;
 import com.android.systemui.R;
+import com.android.systemui.assist.AssistManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardAffordanceView;
 import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -85,12 +82,12 @@
     private static final long TRANSIENT_FP_ERROR_TIMEOUT = 1300;
 
     private KeyguardAffordanceView mCameraImageView;
-    private KeyguardAffordanceView mPhoneImageView;
+    private KeyguardAffordanceView mLeftAffordanceView;
     private LockIcon mLockIcon;
     private TextView mIndicationText;
     private ViewGroup mPreviewContainer;
 
-    private View mPhonePreview;
+    private View mLeftPreview;
     private View mCameraPreview;
 
     private ActivityStarter mActivityStarter;
@@ -104,6 +101,8 @@
 
     private final Interpolator mLinearOutSlowInInterpolator;
     private boolean mPrewarmSent;
+    private boolean mLeftIsVoiceAssist;
+    private AssistManager mAssistManager;
 
     public KeyguardBottomAreaView(Context context) {
         this(context, null);
@@ -133,8 +132,12 @@
                 label = getResources().getString(R.string.unlock_label);
             } else if (host == mCameraImageView) {
                 label = getResources().getString(R.string.camera_label);
-            } else if (host == mPhoneImageView) {
-                label = getResources().getString(R.string.phone_label);
+            } else if (host == mLeftAffordanceView) {
+                if (mLeftIsVoiceAssist) {
+                    label = getResources().getString(R.string.voice_assist_label);
+                } else {
+                    label = getResources().getString(R.string.phone_label);
+                }
             }
             info.addAction(new AccessibilityAction(ACTION_CLICK, label));
         }
@@ -149,8 +152,8 @@
                 } else if (host == mCameraImageView) {
                     launchCamera();
                     return true;
-                } else if (host == mPhoneImageView) {
-                    launchPhone();
+                } else if (host == mLeftAffordanceView) {
+                    launchLeftAffordance();
                     return true;
                 }
             }
@@ -164,29 +167,28 @@
         mLockPatternUtils = new LockPatternUtils(mContext);
         mPreviewContainer = (ViewGroup) findViewById(R.id.preview_container);
         mCameraImageView = (KeyguardAffordanceView) findViewById(R.id.camera_button);
-        mPhoneImageView = (KeyguardAffordanceView) findViewById(R.id.phone_button);
+        mLeftAffordanceView = (KeyguardAffordanceView) findViewById(R.id.left_button);
         mLockIcon = (LockIcon) findViewById(R.id.lock_icon);
         mIndicationText = (TextView) findViewById(R.id.keyguard_indication_text);
         watchForCameraPolicyChanges();
         updateCameraVisibility();
-        updatePhoneVisibility();
         mUnlockMethodCache = UnlockMethodCache.getInstance(getContext());
         mUnlockMethodCache.addListener(this);
         mLockIcon.update();
         setClipChildren(false);
         setClipToPadding(false);
         mPreviewInflater = new PreviewInflater(mContext, new LockPatternUtils(mContext));
-        inflatePreviews();
+        inflateCameraPreview();
         mLockIcon.setOnClickListener(this);
         mLockIcon.setOnLongClickListener(this);
         mCameraImageView.setOnClickListener(this);
-        mPhoneImageView.setOnClickListener(this);
+        mLeftAffordanceView.setOnClickListener(this);
         initAccessibility();
     }
 
     private void initAccessibility() {
         mLockIcon.setAccessibilityDelegate(mAccessibilityDelegate);
-        mPhoneImageView.setAccessibilityDelegate(mAccessibilityDelegate);
+        mLeftAffordanceView.setAccessibilityDelegate(mAccessibilityDelegate);
         mCameraImageView.setAccessibilityDelegate(mAccessibilityDelegate);
     }
 
@@ -247,9 +249,26 @@
         mCameraImageView.setVisibility(visible ? View.VISIBLE : View.GONE);
     }
 
-    private void updatePhoneVisibility() {
-        boolean visible = isPhoneVisible();
-        mPhoneImageView.setVisibility(visible ? View.VISIBLE : View.GONE);
+    private void updateLeftAffordanceIcon() {
+        mLeftIsVoiceAssist = canLaunchVoiceAssist();
+        int drawableId;
+        int contentDescription;
+        if (mLeftIsVoiceAssist) {
+            mLeftAffordanceView.setVisibility(View.VISIBLE);
+            drawableId = R.drawable.ic_mic_26dp;
+            contentDescription = R.string.accessibility_voice_assist_button;
+        } else {
+            boolean visible = isPhoneVisible();
+            mLeftAffordanceView.setVisibility(visible ? View.VISIBLE : View.GONE);
+            drawableId = R.drawable.ic_phone_24dp;
+            contentDescription = R.string.accessibility_phone_button;
+        }
+        mLeftAffordanceView.setImageDrawable(mContext.getDrawable(drawableId));
+        mLeftAffordanceView.setContentDescription(mContext.getString(contentDescription));
+    }
+
+    public boolean isLeftVoiceAssist() {
+        return mLeftIsVoiceAssist;
     }
 
     private boolean isPhoneVisible() {
@@ -287,9 +306,9 @@
     @Override
     public void onStateChanged(boolean accessibilityEnabled, boolean touchExplorationEnabled) {
         mCameraImageView.setClickable(touchExplorationEnabled);
-        mPhoneImageView.setClickable(touchExplorationEnabled);
+        mLeftAffordanceView.setClickable(touchExplorationEnabled);
         mCameraImageView.setFocusable(accessibilityEnabled);
-        mPhoneImageView.setFocusable(accessibilityEnabled);
+        mLeftAffordanceView.setFocusable(accessibilityEnabled);
         mLockIcon.update();
     }
 
@@ -297,8 +316,8 @@
     public void onClick(View v) {
         if (v == mCameraImageView) {
             launchCamera();
-        } else if (v == mPhoneImageView) {
-            launchPhone();
+        } else if (v == mLeftAffordanceView) {
+            launchLeftAffordance();
         } if (v == mLockIcon) {
             if (!mAccessibilityController.isAccessibilityEnabled()) {
                 handleTrustCircleClick();
@@ -363,6 +382,7 @@
                 @Override
                 public void run() {
                     mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+                    mActivityStarter.preventNextAnimation();
                 }
             });
         } else {
@@ -373,7 +393,35 @@
         }
     }
 
-    public void launchPhone() {
+    public void launchLeftAffordance() {
+        if (mLeftIsVoiceAssist) {
+            launchVoiceAssist();
+        } else {
+            launchPhone();
+        }
+    }
+
+    private void launchVoiceAssist() {
+        Runnable runnable = new Runnable() {
+            @Override
+            public void run() {
+                mAssistManager.launchVoiceAssistFromKeyguard();
+                mActivityStarter.preventNextAnimation();
+            }
+        };
+        if (mPhoneStatusBar.isKeyguardCurrentlySecure()) {
+            AsyncTask.execute(runnable);
+        } else {
+            mPhoneStatusBar.executeRunnableDismissingKeyguard(runnable, false /* dismissShade */,
+                    false /* afterKeyguardGone */);
+        }
+    }
+
+    private boolean canLaunchVoiceAssist() {
+        return mAssistManager.canVoiceAssistBeLaunchedFromKeyguard();
+    }
+
+    private void launchPhone() {
         final TelecomManager tm = TelecomManager.from(mContext);
         if (tm.isInCall()) {
             AsyncTask.execute(new Runnable() {
@@ -397,19 +445,19 @@
         }
     }
 
-    public KeyguardAffordanceView getPhoneView() {
-        return mPhoneImageView;
+    public KeyguardAffordanceView getLeftView() {
+        return mLeftAffordanceView;
     }
 
-    public KeyguardAffordanceView getCameraView() {
+    public KeyguardAffordanceView getRightView() {
         return mCameraImageView;
     }
 
-    public View getPhonePreview() {
-        return mPhonePreview;
+    public View getLeftPreview() {
+        return mLeftPreview;
     }
 
-    public View getCameraPreview() {
+    public View getRightPreview() {
         return mCameraPreview;
     }
 
@@ -432,23 +480,35 @@
         updateCameraVisibility();
     }
 
-    private void inflatePreviews() {
-        mPhonePreview = mPreviewInflater.inflatePreview(PHONE_INTENT);
+    private void inflateCameraPreview() {
         mCameraPreview = mPreviewInflater.inflatePreview(getCameraIntent());
-        if (mPhonePreview != null) {
-            mPreviewContainer.addView(mPhonePreview);
-            mPhonePreview.setVisibility(View.INVISIBLE);
-        }
         if (mCameraPreview != null) {
             mPreviewContainer.addView(mCameraPreview);
             mCameraPreview.setVisibility(View.INVISIBLE);
         }
     }
 
+    private void updateLeftPreview() {
+        View previewBefore = mLeftPreview;
+        if (previewBefore != null) {
+            mPreviewContainer.removeView(previewBefore);
+        }
+        if (mLeftIsVoiceAssist) {
+            mLeftPreview = mPreviewInflater.inflatePreviewFromService(
+                    mAssistManager.getVoiceInteractorComponentName());
+        } else {
+            mLeftPreview = mPreviewInflater.inflatePreview(PHONE_INTENT);
+        }
+        if (mLeftPreview != null) {
+            mPreviewContainer.addView(mLeftPreview);
+            mLeftPreview.setVisibility(View.INVISIBLE);
+        }
+    }
+
     public void startFinishDozeAnimation() {
         long delay = 0;
-        if (mPhoneImageView.getVisibility() == View.VISIBLE) {
-            startFinishDozeAnimationElement(mPhoneImageView, delay);
+        if (mLeftAffordanceView.getVisibility() == View.VISIBLE) {
+            startFinishDozeAnimationElement(mLeftAffordanceView, delay);
             delay += DOZE_ANIMATION_STAGGER_DELAY;
         }
         startFinishDozeAnimationElement(mLockIcon, delay);
@@ -543,4 +603,14 @@
             KeyguardIndicationController keyguardIndicationController) {
         mIndicationController = keyguardIndicationController;
     }
+
+    public void setAssistManager(AssistManager assistManager) {
+        mAssistManager = assistManager;
+        updateLeftAffordance();
+    }
+
+    public void updateLeftAffordance() {
+        updateLeftAffordanceIcon();
+        updateLeftPreview();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index a8ecc42..c3ede75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -952,6 +952,10 @@
         } else {
             mKeyguardStatusBar.setAlpha(1f);
             mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE);
+            if (keyguardShowing && oldState != mStatusBarState) {
+                mKeyguardBottomArea.updateLeftAffordance();
+                mAfforanceHelper.updatePreviews();
+            }
         }
         resetVerticalPanelPosition();
         updateQsState();
@@ -1831,7 +1835,7 @@
         if (start) {
             EventLogTags.writeSysuiLockscreenGesture(
                     EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_DIALER, lengthDp, velocityDp);
-            mKeyguardBottomArea.launchPhone();
+            mKeyguardBottomArea.launchLeftAffordance();
         } else {
             EventLogTags.writeSysuiLockscreenGesture(
                     EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_CAMERA, lengthDp, velocityDp);
@@ -1912,15 +1916,19 @@
         if (rightIcon) {
             mStatusBar.onCameraHintStarted();
         } else {
-            mStatusBar.onPhoneHintStarted();
+            if (mKeyguardBottomArea.isLeftVoiceAssist()) {
+                mStatusBar.onVoiceAssistHintStarted();
+            } else {
+                mStatusBar.onPhoneHintStarted();
+            }
         }
     }
 
     @Override
     public KeyguardAffordanceView getLeftIcon() {
         return getLayoutDirection() == LAYOUT_DIRECTION_RTL
-                ? mKeyguardBottomArea.getCameraView()
-                : mKeyguardBottomArea.getPhoneView();
+                ? mKeyguardBottomArea.getRightView()
+                : mKeyguardBottomArea.getLeftView();
     }
 
     @Override
@@ -1931,22 +1939,22 @@
     @Override
     public KeyguardAffordanceView getRightIcon() {
         return getLayoutDirection() == LAYOUT_DIRECTION_RTL
-                ? mKeyguardBottomArea.getPhoneView()
-                : mKeyguardBottomArea.getCameraView();
+                ? mKeyguardBottomArea.getLeftView()
+                : mKeyguardBottomArea.getRightView();
     }
 
     @Override
     public View getLeftPreview() {
         return getLayoutDirection() == LAYOUT_DIRECTION_RTL
-                ? mKeyguardBottomArea.getCameraPreview()
-                : mKeyguardBottomArea.getPhonePreview();
+                ? mKeyguardBottomArea.getRightPreview()
+                : mKeyguardBottomArea.getLeftPreview();
     }
 
     @Override
     public View getRightPreview() {
         return getLayoutDirection() == LAYOUT_DIRECTION_RTL
-                ? mKeyguardBottomArea.getPhonePreview()
-                : mKeyguardBottomArea.getCameraPreview();
+                ? mKeyguardBottomArea.getLeftPreview()
+                : mKeyguardBottomArea.getRightPreview();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 1c46d42..f3335af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -101,7 +101,7 @@
 import com.android.systemui.EventLogTags;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
-import com.android.systemui.assist.AssistGestureManager;
+import com.android.systemui.assist.AssistManager;
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -323,7 +323,7 @@
     private int mNavigationIconHints = 0;
     private HandlerThread mHandlerThread;
 
-    private AssistGestureManager mAssistGestureManager;
+    private AssistManager mAssistManager;
 
     // ensure quick settings is disabled until the current user makes it through the setup wizard
     private boolean mUserSetup = false;
@@ -645,8 +645,8 @@
                         new NavigationBarView.OnVerticalChangedListener() {
                     @Override
                     public void onVerticalChanged(boolean isVertical) {
-                        if (mAssistGestureManager != null) {
-                            mAssistGestureManager.onConfigurationChanged();
+                        if (mAssistManager != null) {
+                            mAssistManager.onConfigurationChanged();
                         }
                         mNotificationPanel.setQsScrimEnabled(!isVertical);
                     }
@@ -662,6 +662,8 @@
             // no window manager? good luck with that
         }
 
+        mAssistManager = new AssistManager(this, context);
+
         // figure out which pixel-format to use for the status bar.
         mPixelFormat = PixelFormat.OPAQUE;
 
@@ -720,6 +722,7 @@
         mKeyguardBottomArea =
                 (KeyguardBottomAreaView) mStatusBarWindow.findViewById(R.id.keyguard_bottom_area);
         mKeyguardBottomArea.setActivityStarter(this);
+        mKeyguardBottomArea.setAssistManager(mAssistManager);
         mKeyguardIndicationController = new KeyguardIndicationController(mContext,
                 (KeyguardIndicationTextView) mStatusBarWindow.findViewById(
                         R.id.keyguard_indication_text));
@@ -841,7 +844,6 @@
         mBroadcastReceiver.onReceive(mContext,
                 new Intent(pm.isScreenOn() ? Intent.ACTION_SCREEN_ON : Intent.ACTION_SCREEN_OFF));
 
-        mAssistGestureManager = new AssistGestureManager(this, context);
 
         // receive broadcasts
         IntentFilter filter = new IntentFilter();
@@ -964,7 +966,7 @@
 
     public void invokeAssistGesture(boolean vibrate) {
         mHandler.removeCallbacks(mInvokeAssist);
-        mAssistGestureManager.onGestureInvoked(vibrate);
+        mAssistManager.onGestureInvoked(vibrate);
     }
 
     public int getStatusBarHeight() {
@@ -1044,7 +1046,7 @@
         mNavigationBarView.getBackButton().setLongClickable(true);
         mNavigationBarView.getBackButton().setOnLongClickListener(mLongPressBackRecentsListener);
         mNavigationBarView.getHomeButton().setOnTouchListener(mHomeActionListener);
-        mAssistGestureManager.onConfigurationChanged();
+        mAssistManager.onConfigurationChanged();
     }
 
     // For small-screen devices (read: phones) that lack hardware navigation buttons
@@ -1805,6 +1807,11 @@
         startActivityDismissingKeyguard(intent, false, dismissShade);
     }
 
+    @Override
+    public void preventNextAnimation() {
+        overrideActivityPendingAppTransition(true /* keyguardShowing */);
+    }
+
     public void setQsExpanded(boolean expanded) {
         mStatusBarWindowManager.setQsExpanded(expanded);
     }
@@ -1924,6 +1931,10 @@
         return mHeadsUpManager.isSnoozed(sbn.getPackageName());
     }
 
+    public boolean isKeyguardCurrentlySecure() {
+        return !mUnlockMethodCache.isCurrentlyInsecure();
+    }
+
     /**
      * All changes to the status bar and notifications funnel through here and are batched.
      */
@@ -2650,6 +2661,23 @@
         final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
                 mContext, intent, mCurrentUserId);
         final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing();
+        Runnable runnable = new Runnable() {
+            public void run() {
+                intent.setFlags(
+                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                mContext.startActivityAsUser(
+                        intent, new UserHandle(UserHandle.USER_CURRENT));
+                overrideActivityPendingAppTransition(
+                        keyguardShowing && !afterKeyguardGone);
+            }
+        };
+        executeRunnableDismissingKeyguard(runnable, dismissShade, afterKeyguardGone);
+    }
+
+    public void executeRunnableDismissingKeyguard(final Runnable runnable,
+            final boolean dismissShade,
+            final boolean afterKeyguardGone) {
+        final boolean keyguardShowing = mStatusBarKeyguardViewManager.isShowing();
         dismissKeyguardThenExecute(new OnDismissAction() {
             @Override
             public boolean onDismiss() {
@@ -2660,12 +2688,9 @@
                                 ActivityManagerNative.getDefault()
                                         .keyguardWaitingForActivityDrawn();
                             }
-                            intent.setFlags(
-                                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-                            mContext.startActivityAsUser(
-                                    intent, new UserHandle(UserHandle.USER_CURRENT));
-                            overrideActivityPendingAppTransition(
-                                    keyguardShowing && !afterKeyguardGone);
+                            if (runnable != null) {
+                                runnable.run();
+                            }
                         } catch (RemoteException e) {
                         }
                     }
@@ -3019,7 +3044,7 @@
             mHandlerThread = null;
         }
         mContext.unregisterReceiver(mBroadcastReceiver);
-        mAssistGestureManager.destroy();
+        mAssistManager.destroy();
     }
 
     private boolean mDemoModeAllowed;
@@ -3467,6 +3492,10 @@
         mKeyguardIndicationController.showTransientIndication(R.string.camera_hint);
     }
 
+    public void onVoiceAssistHintStarted() {
+        mKeyguardIndicationController.showTransientIndication(R.string.voice_hint);
+    }
+
     public void onPhoneHintStarted() {
         mKeyguardIndicationController.showTransientIndication(R.string.phone_hint);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java
index 5d89e2f..4269c19 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PreviewInflater.java
@@ -21,6 +21,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -50,6 +51,15 @@
 
     public View inflatePreview(Intent intent) {
         WidgetInfo info = getWidgetInfo(intent);
+        return inflatePreview(info);
+    }
+
+    public View inflatePreviewFromService(ComponentName componentName) {
+        WidgetInfo info = getWidgetInfoFromService(componentName);
+        return inflatePreview(info);
+    }
+
+    private KeyguardPreviewContainer inflatePreview(WidgetInfo info) {
         if (info == null) {
             return null;
         }
@@ -77,8 +87,36 @@
         return widgetView;
     }
 
-    private WidgetInfo getWidgetInfo(Intent intent) {
+    private WidgetInfo getWidgetInfoFromService(ComponentName componentName) {
+        PackageManager packageManager = mContext.getPackageManager();
+        // Look for the preview specified in the service meta-data
+        try {
+            Bundle metaData = packageManager.getServiceInfo(
+                    componentName, PackageManager.GET_META_DATA).metaData;
+            return getWidgetInfoFromMetaData(componentName.getPackageName(), metaData);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Failed to load preview; " + componentName.flattenToShortString()
+                    + " not found", e);
+        }
+        return null;
+    }
+
+    private WidgetInfo getWidgetInfoFromMetaData(String contextPackage,
+            Bundle metaData) {
+        if (metaData == null) {
+            return null;
+        }
+        int layoutId = metaData.getInt(META_DATA_KEYGUARD_LAYOUT);
+        if (layoutId == 0) {
+            return null;
+        }
         WidgetInfo info = new WidgetInfo();
+        info.contextPackage = contextPackage;
+        info.layoutId = layoutId;
+        return info;
+    }
+
+    private WidgetInfo getWidgetInfo(Intent intent) {
         PackageManager packageManager = mContext.getPackageManager();
         final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser(
                 intent, PackageManager.MATCH_DEFAULT_ONLY, KeyguardUpdateMonitor.getCurrentUser());
@@ -94,16 +132,8 @@
         if (resolved == null || resolved.activityInfo == null) {
             return null;
         }
-        if (resolved.activityInfo.metaData == null || resolved.activityInfo.metaData.isEmpty()) {
-            return null;
-        }
-        int layoutId = resolved.activityInfo.metaData.getInt(META_DATA_KEYGUARD_LAYOUT);
-        if (layoutId == 0) {
-            return null;
-        }
-        info.contextPackage = resolved.activityInfo.packageName;
-        info.layoutId = layoutId;
-        return info;
+        return getWidgetInfoFromMetaData(resolved.activityInfo.packageName,
+                resolved.activityInfo.metaData);
     }
 
     public static boolean wouldLaunchResolverActivity(Context ctx, Intent intent,
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 56b2fa5..2897c61 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -28,7 +28,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.hardware.soundtrigger.IRecognitionStatusCallback;
@@ -726,6 +725,24 @@
         }
 
         @Override
+        public void launchVoiceAssistFromKeyguard() {
+            enforceCallingPermission(Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE);
+            synchronized (this) {
+                if (mImpl == null) {
+                    Slog.w(TAG, "launchVoiceAssistFromKeyguard without running voice interaction"
+                            + "service");
+                    return;
+                }
+                final long caller = Binder.clearCallingIdentity();
+                try {
+                    mImpl.launchVoiceAssistFromKeyguard();
+                } finally {
+                    Binder.restoreCallingIdentity(caller);
+                }
+            }
+        }
+
+        @Override
         public boolean isSessionRunning() {
             enforceCallingPermission(Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE);
             synchronized (this) {
@@ -742,6 +759,14 @@
         }
 
         @Override
+        public boolean activeServiceSupportsLaunchFromKeyguard() throws RemoteException {
+            enforceCallingPermission(Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE);
+            synchronized (this) {
+                return mImpl != null && mImpl.mInfo.getSupportsLaunchFromKeyguard();
+            }
+        }
+
+        @Override
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
                     != PackageManager.PERMISSION_GRANTED) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index f439915..0a5b668 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -235,6 +235,18 @@
         }
     }
 
+    public void launchVoiceAssistFromKeyguard() {
+        if (mService == null) {
+            Slog.w(TAG, "Not bound to voice interaction service " + mComponent);
+            return;
+        }
+        try {
+            mService.launchVoiceAssistFromKeyguard();
+        } catch (RemoteException e) {
+            Slog.w(TAG, "RemoteException while calling launchVoiceAssistFromKeyguard", e);
+        }
+    }
+
     void shutdownLocked() {
         try {
             if (mService != null) {
@@ -256,6 +268,7 @@
     void notifySoundModelsChangedLocked() {
         if (mService == null) {
             Slog.w(TAG, "Not bound to voice interaction service " + mComponent);
+            return;
         }
         try {
             mService.soundModelsChanged();