1 /*
2  * Copyright (C) 2013 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.statusbar;
18 
19 import android.content.Context;
20 import android.graphics.drawable.AnimatedVectorDrawable;
21 import android.graphics.drawable.AnimationDrawable;
22 import android.graphics.drawable.Drawable;
23 import android.service.notification.StatusBarNotification;
24 import android.util.AttributeSet;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.ViewStub;
28 import android.view.accessibility.AccessibilityEvent;
29 import android.widget.ImageView;
30 import com.android.systemui.R;
31 
32 public class ExpandableNotificationRow extends ActivatableNotificationView {
33     private int mRowMinHeight;
34     private int mRowMaxHeight;
35 
36     /** Does this row contain layouts that can adapt to row expansion */
37     private boolean mExpandable;
38     /** Has the user actively changed the expansion state of this row */
39     private boolean mHasUserChangedExpansion;
40     /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
41     private boolean mUserExpanded;
42     /** Is the user touching this row */
43     private boolean mUserLocked;
44     /** Are we showing the "public" version */
45     private boolean mShowingPublic;
46     private boolean mSensitive;
47     private boolean mShowingPublicInitialized;
48     private boolean mShowingPublicForIntrinsicHeight;
49 
50     /**
51      * Is this notification expanded by the system. The expansion state can be overridden by the
52      * user expansion.
53      */
54     private boolean mIsSystemExpanded;
55 
56     /**
57      * Whether the notification expansion is disabled. This is the case on Keyguard.
58      */
59     private boolean mExpansionDisabled;
60 
61     private NotificationContentView mPublicLayout;
62     private NotificationContentView mPrivateLayout;
63     private int mMaxExpandHeight;
64     private View mVetoButton;
65     private boolean mClearable;
66     private ExpansionLogger mLogger;
67     private String mLoggingKey;
68     private boolean mWasReset;
69     private NotificationGuts mGuts;
70 
71     private StatusBarNotification mStatusBarNotification;
72     private boolean mIsHeadsUp;
73 
setIconAnimationRunning(boolean running)74     public void setIconAnimationRunning(boolean running) {
75         setIconAnimationRunning(running, mPublicLayout);
76         setIconAnimationRunning(running, mPrivateLayout);
77     }
78 
setIconAnimationRunning(boolean running, NotificationContentView layout)79     private void setIconAnimationRunning(boolean running, NotificationContentView layout) {
80         if (layout != null) {
81             View contractedChild = layout.getContractedChild();
82             View expandedChild = layout.getExpandedChild();
83             setIconAnimationRunningForChild(running, contractedChild);
84             setIconAnimationRunningForChild(running, expandedChild);
85         }
86     }
87 
setIconAnimationRunningForChild(boolean running, View child)88     private void setIconAnimationRunningForChild(boolean running, View child) {
89         if (child != null) {
90             ImageView icon = (ImageView) child.findViewById(com.android.internal.R.id.icon);
91             setIconRunning(icon, running);
92             ImageView rightIcon = (ImageView) child.findViewById(
93                     com.android.internal.R.id.right_icon);
94             setIconRunning(rightIcon, running);
95         }
96     }
97 
setIconRunning(ImageView imageView, boolean running)98     private void setIconRunning(ImageView imageView, boolean running) {
99         if (imageView != null) {
100             Drawable drawable = imageView.getDrawable();
101             if (drawable instanceof AnimationDrawable) {
102                 AnimationDrawable animationDrawable = (AnimationDrawable) drawable;
103                 if (running) {
104                     animationDrawable.start();
105                 } else {
106                     animationDrawable.stop();
107                 }
108             } else if (drawable instanceof AnimatedVectorDrawable) {
109                 AnimatedVectorDrawable animationDrawable = (AnimatedVectorDrawable) drawable;
110                 if (running) {
111                     animationDrawable.start();
112                 } else {
113                     animationDrawable.stop();
114                 }
115             }
116         }
117     }
118 
setStatusBarNotification(StatusBarNotification statusBarNotification)119     public void setStatusBarNotification(StatusBarNotification statusBarNotification) {
120         mStatusBarNotification = statusBarNotification;
121         updateVetoButton();
122     }
123 
getStatusBarNotification()124     public StatusBarNotification getStatusBarNotification() {
125         return mStatusBarNotification;
126     }
127 
setHeadsUp(boolean isHeadsUp)128     public void setHeadsUp(boolean isHeadsUp) {
129         mIsHeadsUp = isHeadsUp;
130     }
131 
132     public interface ExpansionLogger {
logNotificationExpansion(String key, boolean userAction, boolean expanded)133         public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
134     }
135 
ExpandableNotificationRow(Context context, AttributeSet attrs)136     public ExpandableNotificationRow(Context context, AttributeSet attrs) {
137         super(context, attrs);
138     }
139 
140     /**
141      * Resets this view so it can be re-used for an updated notification.
142      */
143     @Override
reset()144     public void reset() {
145         super.reset();
146         mRowMinHeight = 0;
147         final boolean wasExpanded = isExpanded();
148         mRowMaxHeight = 0;
149         mExpandable = false;
150         mHasUserChangedExpansion = false;
151         mUserLocked = false;
152         mShowingPublic = false;
153         mSensitive = false;
154         mShowingPublicInitialized = false;
155         mIsSystemExpanded = false;
156         mExpansionDisabled = false;
157         mPublicLayout.reset(mIsHeadsUp);
158         mPrivateLayout.reset(mIsHeadsUp);
159         resetHeight();
160         logExpansionEvent(false, wasExpanded);
161     }
162 
resetHeight()163     public void resetHeight() {
164         if (mIsHeadsUp) {
165             resetActualHeight();
166         }
167         mMaxExpandHeight = 0;
168         mWasReset = true;
169         onHeightReset();
170         requestLayout();
171     }
172 
173     @Override
filterMotionEvent(MotionEvent event)174     protected boolean filterMotionEvent(MotionEvent event) {
175         return mIsHeadsUp || super.filterMotionEvent(event);
176     }
177 
178     @Override
onFinishInflate()179     protected void onFinishInflate() {
180         super.onFinishInflate();
181         mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic);
182         mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
183         ViewStub gutsStub = (ViewStub) findViewById(R.id.notification_guts_stub);
184         gutsStub.setOnInflateListener(new ViewStub.OnInflateListener() {
185             @Override
186             public void onInflate(ViewStub stub, View inflated) {
187                 mGuts = (NotificationGuts) inflated;
188                 mGuts.setClipTopAmount(getClipTopAmount());
189                 mGuts.setActualHeight(getActualHeight());
190             }
191         });
192         mVetoButton = findViewById(R.id.veto);
193     }
194 
195     @Override
onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)196     public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
197         if (super.onRequestSendAccessibilityEvent(child, event)) {
198             // Add a record for the entire layout since its content is somehow small.
199             // The event comes from a leaf view that is interacted with.
200             AccessibilityEvent record = AccessibilityEvent.obtain();
201             onInitializeAccessibilityEvent(record);
202             dispatchPopulateAccessibilityEvent(record);
203             event.appendRecord(record);
204             return true;
205         }
206         return false;
207     }
208 
209     @Override
setDark(boolean dark, boolean fade, long delay)210     public void setDark(boolean dark, boolean fade, long delay) {
211         super.setDark(dark, fade, delay);
212         final NotificationContentView showing = getShowingLayout();
213         if (showing != null) {
214             showing.setDark(dark, fade, delay);
215         }
216     }
217 
setHeightRange(int rowMinHeight, int rowMaxHeight)218     public void setHeightRange(int rowMinHeight, int rowMaxHeight) {
219         mRowMinHeight = rowMinHeight;
220         mRowMaxHeight = rowMaxHeight;
221     }
222 
isExpandable()223     public boolean isExpandable() {
224         return mExpandable;
225     }
226 
setExpandable(boolean expandable)227     public void setExpandable(boolean expandable) {
228         mExpandable = expandable;
229     }
230 
231     /**
232      * @return whether the user has changed the expansion state
233      */
hasUserChangedExpansion()234     public boolean hasUserChangedExpansion() {
235         return mHasUserChangedExpansion;
236     }
237 
isUserExpanded()238     public boolean isUserExpanded() {
239         return mUserExpanded;
240     }
241 
242     /**
243      * Set this notification to be expanded by the user
244      *
245      * @param userExpanded whether the user wants this notification to be expanded
246      */
setUserExpanded(boolean userExpanded)247     public void setUserExpanded(boolean userExpanded) {
248         if (userExpanded && !mExpandable) return;
249         final boolean wasExpanded = isExpanded();
250         mHasUserChangedExpansion = true;
251         mUserExpanded = userExpanded;
252         logExpansionEvent(true, wasExpanded);
253     }
254 
resetUserExpansion()255     public void resetUserExpansion() {
256         mHasUserChangedExpansion = false;
257         mUserExpanded = false;
258     }
259 
isUserLocked()260     public boolean isUserLocked() {
261         return mUserLocked;
262     }
263 
setUserLocked(boolean userLocked)264     public void setUserLocked(boolean userLocked) {
265         mUserLocked = userLocked;
266     }
267 
268     /**
269      * @return has the system set this notification to be expanded
270      */
isSystemExpanded()271     public boolean isSystemExpanded() {
272         return mIsSystemExpanded;
273     }
274 
275     /**
276      * Set this notification to be expanded by the system.
277      *
278      * @param expand whether the system wants this notification to be expanded.
279      */
setSystemExpanded(boolean expand)280     public void setSystemExpanded(boolean expand) {
281         if (expand != mIsSystemExpanded) {
282             final boolean wasExpanded = isExpanded();
283             mIsSystemExpanded = expand;
284             notifyHeightChanged();
285             logExpansionEvent(false, wasExpanded);
286         }
287     }
288 
289     /**
290      * @param expansionDisabled whether to prevent notification expansion
291      */
setExpansionDisabled(boolean expansionDisabled)292     public void setExpansionDisabled(boolean expansionDisabled) {
293         if (expansionDisabled != mExpansionDisabled) {
294             final boolean wasExpanded = isExpanded();
295             mExpansionDisabled = expansionDisabled;
296             logExpansionEvent(false, wasExpanded);
297             if (wasExpanded != isExpanded()) {
298                 notifyHeightChanged();
299             }
300         }
301     }
302 
303     /**
304      * @return Can the underlying notification be cleared?
305      */
isClearable()306     public boolean isClearable() {
307         return mStatusBarNotification != null && mStatusBarNotification.isClearable();
308     }
309 
310     /**
311      * Apply an expansion state to the layout.
312      */
applyExpansionToLayout()313     public void applyExpansionToLayout() {
314         boolean expand = isExpanded();
315         if (expand && mExpandable) {
316             setActualHeight(mMaxExpandHeight);
317         } else {
318             setActualHeight(mRowMinHeight);
319         }
320     }
321 
322     @Override
getIntrinsicHeight()323     public int getIntrinsicHeight() {
324         if (isUserLocked()) {
325             return getActualHeight();
326         }
327         boolean inExpansionState = isExpanded();
328         if (!inExpansionState) {
329             // not expanded, so we return the collapsed size
330             return mRowMinHeight;
331         }
332 
333         return mShowingPublicForIntrinsicHeight ? mRowMinHeight : getMaxExpandHeight();
334     }
335 
336     /**
337      * Check whether the view state is currently expanded. This is given by the system in {@link
338      * #setSystemExpanded(boolean)} and can be overridden by user expansion or
339      * collapsing in {@link #setUserExpanded(boolean)}. Note that the visual appearance of this
340      * view can differ from this state, if layout params are modified from outside.
341      *
342      * @return whether the view state is currently expanded.
343      */
isExpanded()344     private boolean isExpanded() {
345         return !mExpansionDisabled
346                 && (!hasUserChangedExpansion() && isSystemExpanded() || isUserExpanded());
347     }
348 
349     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)350     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
351         super.onLayout(changed, left, top, right, bottom);
352         boolean updateExpandHeight = mMaxExpandHeight == 0 && !mWasReset;
353         updateMaxExpandHeight();
354         if (updateExpandHeight) {
355             applyExpansionToLayout();
356         }
357         mWasReset = false;
358     }
359 
updateMaxExpandHeight()360     private void updateMaxExpandHeight() {
361         int intrinsicBefore = getIntrinsicHeight();
362         mMaxExpandHeight = mPrivateLayout.getMaxHeight();
363         if (intrinsicBefore != getIntrinsicHeight()) {
364             notifyHeightChanged();
365         }
366     }
367 
setSensitive(boolean sensitive)368     public void setSensitive(boolean sensitive) {
369         mSensitive = sensitive;
370     }
371 
setHideSensitiveForIntrinsicHeight(boolean hideSensitive)372     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
373         mShowingPublicForIntrinsicHeight = mSensitive && hideSensitive;
374     }
375 
setHideSensitive(boolean hideSensitive, boolean animated, long delay, long duration)376     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
377             long duration) {
378         boolean oldShowingPublic = mShowingPublic;
379         mShowingPublic = mSensitive && hideSensitive;
380         if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) {
381             return;
382         }
383 
384         // bail out if no public version
385         if (mPublicLayout.getChildCount() == 0) return;
386 
387         if (!animated) {
388             mPublicLayout.animate().cancel();
389             mPrivateLayout.animate().cancel();
390             mPublicLayout.setAlpha(1f);
391             mPrivateLayout.setAlpha(1f);
392             mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE);
393             mPrivateLayout.setVisibility(mShowingPublic ? View.INVISIBLE : View.VISIBLE);
394         } else {
395             animateShowingPublic(delay, duration);
396         }
397 
398         updateVetoButton();
399         mShowingPublicInitialized = true;
400     }
401 
animateShowingPublic(long delay, long duration)402     private void animateShowingPublic(long delay, long duration) {
403         final View source = mShowingPublic ? mPrivateLayout : mPublicLayout;
404         View target = mShowingPublic ? mPublicLayout : mPrivateLayout;
405         source.setVisibility(View.VISIBLE);
406         target.setVisibility(View.VISIBLE);
407         target.setAlpha(0f);
408         source.animate().cancel();
409         target.animate().cancel();
410         source.animate()
411                 .alpha(0f)
412                 .setStartDelay(delay)
413                 .setDuration(duration)
414                 .withEndAction(new Runnable() {
415                     @Override
416                     public void run() {
417                         source.setVisibility(View.INVISIBLE);
418                     }
419                 });
420         target.animate()
421                 .alpha(1f)
422                 .setStartDelay(delay)
423                 .setDuration(duration);
424     }
425 
updateVetoButton()426     private void updateVetoButton() {
427         // public versions cannot be dismissed
428         mVetoButton.setVisibility(isClearable() && !mShowingPublic ? View.VISIBLE : View.GONE);
429     }
430 
getMaxExpandHeight()431     public int getMaxExpandHeight() {
432         return mShowingPublicForIntrinsicHeight ? mRowMinHeight : mMaxExpandHeight;
433     }
434 
435     @Override
isContentExpandable()436     public boolean isContentExpandable() {
437         NotificationContentView showingLayout = getShowingLayout();
438         return showingLayout.isContentExpandable();
439     }
440 
441     @Override
setActualHeight(int height, boolean notifyListeners)442     public void setActualHeight(int height, boolean notifyListeners) {
443         mPrivateLayout.setActualHeight(height);
444         mPublicLayout.setActualHeight(height);
445         if (mGuts != null) {
446             mGuts.setActualHeight(height);
447         }
448         invalidate();
449         super.setActualHeight(height, notifyListeners);
450     }
451 
452     @Override
getMaxHeight()453     public int getMaxHeight() {
454         NotificationContentView showingLayout = getShowingLayout();
455         return showingLayout.getMaxHeight();
456     }
457 
458     @Override
getMinHeight()459     public int getMinHeight() {
460         NotificationContentView showingLayout = getShowingLayout();
461         return showingLayout.getMinHeight();
462     }
463 
464     @Override
setClipTopAmount(int clipTopAmount)465     public void setClipTopAmount(int clipTopAmount) {
466         super.setClipTopAmount(clipTopAmount);
467         mPrivateLayout.setClipTopAmount(clipTopAmount);
468         mPublicLayout.setClipTopAmount(clipTopAmount);
469         if (mGuts != null) {
470             mGuts.setClipTopAmount(clipTopAmount);
471         }
472     }
473 
notifyContentUpdated()474     public void notifyContentUpdated() {
475         mPublicLayout.notifyContentUpdated();
476         mPrivateLayout.notifyContentUpdated();
477     }
478 
isMaxExpandHeightInitialized()479     public boolean isMaxExpandHeightInitialized() {
480         return mMaxExpandHeight != 0;
481     }
482 
getShowingLayout()483     private NotificationContentView getShowingLayout() {
484         return mShowingPublic ? mPublicLayout : mPrivateLayout;
485     }
486 
setExpansionLogger(ExpansionLogger logger, String key)487     public void setExpansionLogger(ExpansionLogger logger, String key) {
488         mLogger = logger;
489         mLoggingKey = key;
490     }
491 
492 
logExpansionEvent(boolean userAction, boolean wasExpanded)493     private void logExpansionEvent(boolean userAction, boolean wasExpanded) {
494         final boolean nowExpanded = isExpanded();
495         if (wasExpanded != nowExpanded && mLogger != null) {
496             mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded) ;
497         }
498     }
499 }
500