1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.assist.ui;
18 
19 import static com.android.systemui.assist.AssistManager.DISMISS_REASON_INVOCATION_CANCELLED;
20 import static com.android.systemui.assist.AssistManager.INVOCATION_TYPE_GESTURE;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.ValueAnimator;
25 import android.content.Context;
26 import android.graphics.PixelFormat;
27 import android.metrics.LogMaker;
28 import android.os.Build;
29 import android.util.Log;
30 import android.view.Gravity;
31 import android.view.LayoutInflater;
32 import android.view.WindowManager;
33 import android.view.animation.PathInterpolator;
34 import android.widget.FrameLayout;
35 
36 import com.android.internal.logging.MetricsLogger;
37 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
38 import com.android.systemui.R;
39 import com.android.systemui.assist.AssistLogger;
40 import com.android.systemui.assist.AssistManager;
41 import com.android.systemui.assist.AssistantSessionEvent;
42 import com.android.systemui.dagger.SysUISingleton;
43 
44 import java.util.Locale;
45 
46 import javax.inject.Inject;
47 
48 import dagger.Lazy;
49 
50 /**
51  * Default UiController implementation. Shows white edge lights along the bottom of the phone,
52  * expanding from the corners to meet in the center.
53  */
54 @SysUISingleton
55 public class DefaultUiController implements AssistManager.UiController {
56 
57     private static final String TAG = "DefaultUiController";
58 
59     private static final long ANIM_DURATION_MS = 200;
60 
61     private static final boolean VERBOSE = Build.TYPE.toLowerCase(Locale.ROOT).contains("debug")
62             || Build.TYPE.toLowerCase(Locale.ROOT).equals("eng");
63 
64     protected final FrameLayout mRoot;
65     protected InvocationLightsView mInvocationLightsView;
66     protected final AssistLogger mAssistLogger;
67 
68     private final WindowManager mWindowManager;
69     private final MetricsLogger mMetricsLogger;
70     private final Lazy<AssistManager> mAssistManagerLazy;
71     private final WindowManager.LayoutParams mLayoutParams;
72     private final PathInterpolator mProgressInterpolator = new PathInterpolator(.83f, 0, .84f, 1);
73 
74     private boolean mAttached = false;
75     private boolean mInvocationInProgress = false;
76     private float mLastInvocationProgress = 0;
77 
78     private ValueAnimator mInvocationAnimator = new ValueAnimator();
79 
80     @Inject
DefaultUiController(Context context, AssistLogger assistLogger, WindowManager windowManager, MetricsLogger metricsLogger, Lazy<AssistManager> assistManagerLazy)81     public DefaultUiController(Context context, AssistLogger assistLogger,
82             WindowManager windowManager, MetricsLogger metricsLogger,
83             Lazy<AssistManager> assistManagerLazy) {
84         mAssistLogger = assistLogger;
85         mRoot = new FrameLayout(context);
86         mWindowManager = windowManager;
87         mMetricsLogger = metricsLogger;
88         mAssistManagerLazy = assistManagerLazy;
89 
90         mLayoutParams = new WindowManager.LayoutParams(
91                 WindowManager.LayoutParams.MATCH_PARENT,
92                 WindowManager.LayoutParams.WRAP_CONTENT, 0, 0,
93                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
94                 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
95                         | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
96                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
97                         | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
98                 PixelFormat.TRANSLUCENT);
99         mLayoutParams.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
100         mLayoutParams.gravity = Gravity.BOTTOM;
101         mLayoutParams.setFitInsetsTypes(0 /* types */);
102         mLayoutParams.setTitle("Assist");
103 
104         mInvocationLightsView = (InvocationLightsView)
105                 LayoutInflater.from(context).inflate(R.layout.invocation_lights, mRoot, false);
106         mRoot.addView(mInvocationLightsView);
107     }
108 
109     @Override // AssistManager.UiController
onInvocationProgress(int type, float progress)110     public void onInvocationProgress(int type, float progress) {
111         boolean invocationWasInProgress = mInvocationInProgress;
112 
113         if (progress == 1) {
114             animateInvocationCompletion(type, 0);
115         } else if (progress == 0) {
116             hide();
117         } else {
118             if (!mInvocationInProgress) {
119                 attach();
120                 mInvocationInProgress = true;
121             }
122             setProgressInternal(type, progress);
123         }
124         mLastInvocationProgress = progress;
125 
126         logInvocationProgressMetrics(type, progress, invocationWasInProgress);
127     }
128 
129     @Override // AssistManager.UiController
onGestureCompletion(float velocity)130     public void onGestureCompletion(float velocity) {
131         animateInvocationCompletion(AssistManager.INVOCATION_TYPE_GESTURE, velocity);
132         logInvocationProgressMetrics(INVOCATION_TYPE_GESTURE, 1, mInvocationInProgress);
133     }
134 
135     @Override // AssistManager.UiController
hide()136     public void hide() {
137         detach();
138         if (mInvocationAnimator.isRunning()) {
139             mInvocationAnimator.cancel();
140         }
141         mInvocationLightsView.hide();
142         mInvocationInProgress = false;
143     }
144 
logInvocationProgressMetrics( int type, float progress, boolean invocationWasInProgress)145     protected void logInvocationProgressMetrics(
146             int type, float progress, boolean invocationWasInProgress) {
147         // Logs assistant invocation start.
148         if (progress == 1f) {
149             if (VERBOSE) {
150                 Log.v(TAG, "Invocation complete: type=" + type);
151             }
152         }
153         if (!invocationWasInProgress && progress > 0.f) {
154             if (VERBOSE) {
155                 Log.v(TAG, "Invocation started: type=" + type);
156             }
157             mAssistLogger.reportAssistantInvocationEventFromLegacy(
158                     type,
159                     /* isInvocationComplete = */ false,
160                     /* assistantComponent = */ null,
161                     /* legacyDeviceState = */ null);
162             mMetricsLogger.write(new LogMaker(MetricsEvent.ASSISTANT)
163                     .setType(MetricsEvent.TYPE_ACTION)
164                     .setSubtype(mAssistManagerLazy.get().toLoggingSubType(type)));
165         }
166         // Logs assistant invocation cancelled.
167         if ((mInvocationAnimator == null || !mInvocationAnimator.isRunning())
168                 && invocationWasInProgress && progress == 0f) {
169             if (VERBOSE) {
170                 Log.v(TAG, "Invocation cancelled: type=" + type);
171             }
172             mAssistLogger.reportAssistantSessionEvent(
173                     AssistantSessionEvent.ASSISTANT_SESSION_INVOCATION_CANCELLED);
174             MetricsLogger.action(new LogMaker(MetricsEvent.ASSISTANT)
175                     .setType(MetricsEvent.TYPE_DISMISS)
176                     .setSubtype(DISMISS_REASON_INVOCATION_CANCELLED));
177         }
178     }
179 
attach()180     private void attach() {
181         if (!mAttached) {
182             mWindowManager.addView(mRoot, mLayoutParams);
183             mAttached = true;
184         }
185     }
186 
detach()187     private void detach() {
188         if (mAttached) {
189             mWindowManager.removeViewImmediate(mRoot);
190             mAttached = false;
191         }
192     }
193 
setProgressInternal(int type, float progress)194     private void setProgressInternal(int type, float progress) {
195         mInvocationLightsView.onInvocationProgress(
196                 mProgressInterpolator.getInterpolation(progress));
197     }
198 
animateInvocationCompletion(int type, float velocity)199     private void animateInvocationCompletion(int type, float velocity) {
200         mInvocationAnimator = ValueAnimator.ofFloat(mLastInvocationProgress, 1);
201         mInvocationAnimator.setStartDelay(1);
202         mInvocationAnimator.setDuration(ANIM_DURATION_MS);
203         mInvocationAnimator.addUpdateListener(
204                 animation -> setProgressInternal(type, (float) animation.getAnimatedValue()));
205         mInvocationAnimator.addListener(new AnimatorListenerAdapter() {
206             @Override
207             public void onAnimationEnd(Animator animation) {
208                 super.onAnimationEnd(animation);
209                 mInvocationInProgress = false;
210                 mLastInvocationProgress = 0;
211                 hide();
212             }
213         });
214         mInvocationAnimator.start();
215     }
216 }
217