Assist disclosure

Add an animation that discloses delivery
of contextual data to the assist component.

Also fixes a bug where contextual data was
delivered to legacy assist activities even
though the user explicitly disabled context.

Bug: 21568059
Change-Id: I27dfaa36e2f677b0d73acfa4730f0f4ea3486919
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 9f99f62..0732add 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -64,5 +64,7 @@
      *        bar caused by this app transition in millis
      */
     void appTransitionStarting(long statusBarAnimationsStartTime, long statusBarAnimationsDuration);
+
+    void showAssistDisclosure();
 }
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7af8b80..869b03a 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -578,4 +578,10 @@
 
     <!-- Padding between icon and text for managed profile toast -->
     <dimen name="managed_profile_toast_padding">4dp</dimen>
+
+    <!-- Thickness of the assist disclosure beams -->
+    <dimen name="assist_disclosure_thickness">4dp</dimen>
+
+    <!-- Thickness of the shadows of the assist disclosure beams -->
+    <dimen name="assist_disclosure_shadow_thickness">1.5dp</dimen>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java b/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java
new file mode 100644
index 0000000..234a699
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java
@@ -0,0 +1,251 @@
+/*
+ * 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
+ */
+
+package com.android.systemui.assist;
+
+import com.android.systemui.R;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.os.Handler;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+
+/**
+ * Visually discloses that contextual data was provided to an assistant.
+ */
+public class AssistDisclosure {
+    private final Context mContext;
+    private final WindowManager mWm;
+    private final Handler mHandler;
+
+    private AssistDisclosureView mView;
+    private boolean mViewAdded;
+
+    public AssistDisclosure(Context context, Handler handler) {
+        mContext = context;
+        mHandler = handler;
+        mWm = mContext.getSystemService(WindowManager.class);
+    }
+
+    public void postShow() {
+        mHandler.removeCallbacks(mShowRunnable);
+        mHandler.post(mShowRunnable);
+    }
+
+    private void show() {
+        if (mView == null) {
+            mView = new AssistDisclosureView(mContext);
+        }
+        if (!mViewAdded) {
+            WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                    WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY,
+                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                            | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+                            | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                            | WindowManager.LayoutParams.FLAG_FULLSCREEN
+                            | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED,
+                    PixelFormat.TRANSLUCENT);
+            lp.setTitle("AssistDisclosure");
+
+            mWm.addView(mView, lp);
+            mViewAdded = true;
+        }
+    }
+
+    private void hide() {
+        if (mViewAdded) {
+            mWm.removeView(mView);
+            mViewAdded = false;
+        }
+    }
+
+    private Runnable mShowRunnable = new Runnable() {
+        @Override
+        public void run() {
+            show();
+        }
+    };
+
+    private class AssistDisclosureView extends View
+            implements ValueAnimator.AnimatorUpdateListener {
+
+        public static final int TRACING_ANIMATION_DURATION = 300;
+        public static final int ALPHA_ANIMATION_DURATION = 200;
+
+        private float mThickness;
+        private float mShadowThickness;
+        private final Paint mPaint = new Paint();
+        private final Paint mShadowPaint = new Paint();
+
+        private final ValueAnimator mTracingAnimator;
+        private final ValueAnimator mAlphaAnimator;
+        private final AnimatorSet mAnimator;
+
+        private float mTracingProgress = 0;
+        private int mAlpha = 255;
+
+        public AssistDisclosureView(Context context) {
+            super(context);
+
+            mTracingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(TRACING_ANIMATION_DURATION);
+            mTracingAnimator.addUpdateListener(this);
+            mTracingAnimator.setInterpolator(AnimationUtils.loadInterpolator(mContext,
+                    android.R.interpolator.fast_out_slow_in));
+            mAlphaAnimator = ValueAnimator.ofInt(255, 0).setDuration(ALPHA_ANIMATION_DURATION);
+            mAlphaAnimator.addUpdateListener(this);
+            mAlphaAnimator.setInterpolator(AnimationUtils.loadInterpolator(mContext,
+                    android.R.interpolator.fast_out_linear_in));
+            mAnimator = new AnimatorSet();
+            mAnimator.play(mTracingAnimator).before(mAlphaAnimator);
+            mAnimator.addListener(new AnimatorListenerAdapter() {
+                boolean mCancelled;
+
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    mCancelled = false;
+                }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    mCancelled = true;
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    if (!mCancelled) {
+                        hide();
+                    }
+                }
+            });
+
+            PorterDuffXfermode srcMode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
+            mPaint.setColor(Color.WHITE);
+            mPaint.setXfermode(srcMode);
+            mShadowPaint.setColor(Color.DKGRAY);
+            mShadowPaint.setXfermode(srcMode);
+
+            mThickness = getResources().getDimension(R.dimen.assist_disclosure_thickness);
+            mShadowThickness = getResources().getDimension(
+                    R.dimen.assist_disclosure_shadow_thickness);
+        }
+
+        @Override
+        protected void onAttachedToWindow() {
+            super.onAttachedToWindow();
+
+            startAnimation();
+        }
+
+        @Override
+        protected void onDetachedFromWindow() {
+            super.onDetachedFromWindow();
+
+            mAnimator.cancel();
+
+            mTracingProgress = 0;
+            mAlpha = 255;
+        }
+
+        private void startAnimation() {
+            mAnimator.cancel();
+            mAnimator.start();
+        }
+
+        @Override
+        protected void onDraw(Canvas canvas) {
+            mPaint.setAlpha(mAlpha);
+            mShadowPaint.setAlpha(mAlpha / 4);
+
+            drawGeometry(canvas, mShadowPaint, mShadowThickness);
+            drawGeometry(canvas, mPaint, 0);
+        }
+
+        private void drawGeometry(Canvas canvas, Paint paint, float padding) {
+            final int width = getWidth();
+            final int height = getHeight();
+            float thickness = mThickness;
+            final float pixelProgress = mTracingProgress * (width + height - 2 * thickness);
+
+            float bottomProgress = Math.min(pixelProgress, width / 2f);
+            if (bottomProgress > 0) {
+                drawBeam(canvas,
+                        width / 2f - bottomProgress,
+                        height - thickness,
+                        width / 2f + bottomProgress,
+                        height, paint, padding);
+            }
+
+            float sideProgress = Math.min(pixelProgress - bottomProgress, height - thickness);
+            if (sideProgress > 0) {
+                drawBeam(canvas,
+                        0,
+                        (height - thickness) - sideProgress,
+                        thickness,
+                        height - thickness, paint, padding);
+                drawBeam(canvas,
+                        width - thickness,
+                        (height - thickness) - sideProgress,
+                        width,
+                        height - thickness, paint, padding);
+            }
+
+            float topProgress = Math.min(pixelProgress - bottomProgress - sideProgress,
+                    width / 2 - thickness);
+            if (sideProgress > 0 && topProgress > 0) {
+                drawBeam(canvas,
+                        thickness,
+                        0,
+                        thickness + topProgress,
+                        thickness, paint, padding);
+                drawBeam(canvas,
+                        (width - thickness) - topProgress,
+                        0,
+                        width - thickness,
+                        thickness, paint, padding);
+            }
+        }
+
+        private void drawBeam(Canvas canvas, float left, float top, float right, float bottom,
+                Paint paint, float padding) {
+            canvas.drawRect(left - padding,
+                    top - padding,
+                    right + padding,
+                    bottom + padding,
+                    paint);
+        }
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            if (animation == mAlphaAnimator) {
+                mAlpha = (int) mAlphaAnimator.getAnimatedValue();
+            } else if (animation == mTracingAnimator) {
+                mTracingProgress = (float) mTracingAnimator.getAnimatedValue();
+            }
+            invalidate();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index 445ecb6..674356b 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -55,6 +55,8 @@
 
     private final Context mContext;
     private final WindowManager mWindowManager;
+    private final AssistDisclosure mAssistDisclosure;
+
     private AssistOrbContainer mView;
     private final PhoneStatusBar mBar;
     private final AssistUtils mAssistUtils;
@@ -100,6 +102,7 @@
                 Settings.Secure.getUriFor(Settings.Secure.ASSISTANT), false,
                 mAssistSettingsObserver);
         mAssistSettingsObserver.onChange(false);
+        mAssistDisclosure = new AssistDisclosure(context, new Handler());
     }
 
     public void onConfigurationChanged() {
@@ -187,8 +190,11 @@
         mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL |
                 CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL);
 
+        boolean structureEnabled = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
+
         final Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
-                .getAssistIntent(mContext, true, UserHandle.USER_CURRENT);
+                .getAssistIntent(mContext, structureEnabled, UserHandle.USER_CURRENT);
         if (intent == null) {
             return;
         }
@@ -196,6 +202,10 @@
             intent.setComponent(mAssistComponent);
         }
 
+        if (structureEnabled) {
+            showDisclosure();
+        }
+
         try {
             final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
                     R.anim.search_launch_enter, R.anim.search_launch_exit);
@@ -297,4 +307,8 @@
         pw.println("AssistManager state:");
         pw.print("  mAssistComponent="); pw.println(mAssistComponent);
     }
+
+    public void showDisclosure() {
+        mAssistDisclosure.postShow();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index f8a1385..295fdc8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -2101,4 +2101,11 @@
         }
         return mStatusBarKeyguardViewManager.isSecure();
     }
+
+    @Override
+    public void showAssistDisclosure() {
+        if (mAssistManager != null) {
+            mAssistManager.showDisclosure();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 80fdd28..0deff08 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -61,6 +61,7 @@
     private static final int MSG_APP_TRANSITION_PENDING     = 19 << MSG_SHIFT;
     private static final int MSG_APP_TRANSITION_CANCELLED   = 20 << MSG_SHIFT;
     private static final int MSG_APP_TRANSITION_STARTING    = 21 << MSG_SHIFT;
+    private static final int MSG_ASSIST_DISCLOSURE          = 22 << MSG_SHIFT;
 
     public static final int FLAG_EXCLUDE_NONE = 0;
     public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
@@ -104,6 +105,7 @@
         public void appTransitionPending();
         public void appTransitionCancelled();
         public void appTransitionStarting(long startTime, long duration);
+        public void showAssistDisclosure();
     }
 
     public CommandQueue(Callbacks callbacks, StatusBarIconList list) {
@@ -274,6 +276,13 @@
         }
     }
 
+    public void showAssistDisclosure() {
+        synchronized (mList) {
+            mHandler.removeMessages(MSG_ASSIST_DISCLOSURE);
+            mHandler.obtainMessage(MSG_ASSIST_DISCLOSURE).sendToTarget();
+        }
+    }
+
     private final class H extends Handler {
         public void handleMessage(Message msg) {
             final int what = msg.what & MSG_MASK;
@@ -366,6 +375,9 @@
                     Pair<Long, Long> data = (Pair<Long, Long>) msg.obj;
                     mCallbacks.appTransitionStarting(data.first, data.second);
                     break;
+                case MSG_ASSIST_DISCLOSURE:
+                    mCallbacks.showAssistDisclosure();
+                    break;
             }
         }
     }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 58c3ea1..4692403 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -24,4 +24,5 @@
     void notificationLightPulse(int argb, int onMillis, int offMillis);
     void notificationLightOff();
     void showScreenPinningRequest();
+    void showAssistDisclosure();
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 5669f30..a754379 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -154,6 +154,16 @@
                 }
             }
         }
+
+        @Override
+        public void showAssistDisclosure() {
+            if (mBar != null) {
+                try {
+                    mBar.showAssistDisclosure();
+                } catch (RemoteException e) {
+                }
+            }
+        }
     };
 
     // ================================================================================
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index acd484d..af0ddbe 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -139,7 +139,7 @@
             IVoiceInteractionSessionShowCallback showCallback) {
         if (mActiveSession == null) {
             mActiveSession = new VoiceInteractionSessionConnection(mLock, mSessionComponentName,
-                    mUser, mContext, this, mInfo.getServiceInfo().applicationInfo.uid);
+                    mUser, mContext, this, mInfo.getServiceInfo().applicationInfo.uid, mHandler);
         }
         return mActiveSession.showLocked(args, flags, showCallback);
     }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
index 47a230a..cc6a9c5 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
@@ -32,6 +32,7 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -48,6 +49,8 @@
 import com.android.internal.app.IVoiceInteractionSessionShowCallback;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.os.IResultReceiver;
+import com.android.server.LocalServices;
+import com.android.server.statusbar.StatusBarManagerInternal;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -63,6 +66,7 @@
     final Context mContext;
     final Callback mCallback;
     final int mCallingUid;
+    final Handler mHandler;
     final IActivityManager mAm;
     final IWindowManager mIWindowManager;
     final AppOpsManager mAppOps;
@@ -141,13 +145,14 @@
     };
 
     public VoiceInteractionSessionConnection(Object lock, ComponentName component, int user,
-            Context context, Callback callback, int callingUid) {
+            Context context, Callback callback, int callingUid, Handler handler) {
         mLock = lock;
         mSessionComponentName = component;
         mUser = user;
         mContext = context;
         mCallback = callback;
         mCallingUid = callingUid;
+        mHandler = handler;
         mAm = ActivityManagerNative.getDefault();
         mIWindowManager = IWindowManager.Stub.asInterface(
                 ServiceManager.getService(Context.WINDOW_SERVICE));
@@ -193,11 +198,13 @@
             mShowArgs = args;
             mShowFlags = flags;
             mHaveAssistData = false;
+            boolean needDisclosure = false;
             if ((flags& VoiceInteractionSession.SHOW_WITH_ASSIST) != 0) {
                 if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ASSIST_STRUCTURE, mCallingUid,
                         mSessionComponentName.getPackageName()) == AppOpsManager.MODE_ALLOWED
                         && allDataEnabled) {
                     try {
+                        needDisclosure = true;
                         mAm.requestAssistContextExtras(ActivityManager.ASSIST_CONTEXT_FULL,
                                 mAssistReceiver);
                     } catch (RemoteException e) {
@@ -215,6 +222,7 @@
                         mSessionComponentName.getPackageName()) == AppOpsManager.MODE_ALLOWED
                         && allDataEnabled) {
                     try {
+                        needDisclosure = true;
                         mIWindowManager.requestAssistScreenshot(mScreenshotReceiver);
                     } catch (RemoteException e) {
                     }
@@ -225,6 +233,9 @@
             } else {
                 mScreenshot = null;
             }
+            if (needDisclosure) {
+                mHandler.post(mShowAssistDisclosureRunnable);
+            }
             if (mSession != null) {
                 try {
                     mSession.show(mShowArgs, mShowFlags, showCallback);
@@ -483,4 +494,15 @@
             pw.print(prefix); pw.print("mAssistData="); pw.println(mAssistData);
         }
     }
+
+    private Runnable mShowAssistDisclosureRunnable = new Runnable() {
+        @Override
+        public void run() {
+            StatusBarManagerInternal statusBarInternal = LocalServices.getService(
+                    StatusBarManagerInternal.class);
+            if (statusBarInternal != null) {
+                statusBarInternal.showAssistDisclosure();
+            }
+        }
+    };
 };