1 /*
2  * Copyright (C) 2015 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.notification.row.wrapper;
18 
19 import static com.android.systemui.statusbar.notification.TransformState.TRANSFORM_Y;
20 
21 import android.app.Notification;
22 import android.content.Context;
23 import android.util.ArraySet;
24 import android.view.NotificationHeaderView;
25 import android.view.NotificationTopLineView;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.view.animation.Interpolator;
29 import android.view.animation.PathInterpolator;
30 import android.widget.DateTimeView;
31 import android.widget.ImageButton;
32 import android.widget.ImageView;
33 import android.widget.TextView;
34 
35 import androidx.annotation.Nullable;
36 
37 import com.android.app.animation.Interpolators;
38 import com.android.internal.widget.CachingIconView;
39 import com.android.internal.widget.NotificationExpandButton;
40 import com.android.systemui.res.R;
41 import com.android.systemui.statusbar.TransformableView;
42 import com.android.systemui.statusbar.ViewTransformationHelper;
43 import com.android.systemui.statusbar.notification.CustomInterpolatorTransformation;
44 import com.android.systemui.statusbar.notification.FeedbackIcon;
45 import com.android.systemui.statusbar.notification.ImageTransformState;
46 import com.android.systemui.statusbar.notification.Roundable;
47 import com.android.systemui.statusbar.notification.RoundableState;
48 import com.android.systemui.statusbar.notification.TransformState;
49 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
50 
51 import java.util.Stack;
52 
53 /**
54  * Wraps a notification view which may or may not include a header.
55  */
56 public class NotificationHeaderViewWrapper extends NotificationViewWrapper implements Roundable {
57 
58     private final RoundableState mRoundableState;
59     private static final Interpolator LOW_PRIORITY_HEADER_CLOSE
60             = new PathInterpolator(0.4f, 0f, 0.7f, 1f);
61     protected final ViewTransformationHelper mTransformationHelper;
62     private CachingIconView mIcon;
63     private NotificationExpandButton mExpandButton;
64     private View mAltExpandTarget;
65     private View mIconContainer;
66     protected NotificationHeaderView mNotificationHeader;
67     protected NotificationTopLineView mNotificationTopLine;
68     private TextView mHeaderText;
69     private TextView mAppNameText;
70     private ImageView mWorkProfileImage;
71     private View mAudiblyAlertedIcon;
72     private View mFeedbackIcon;
73     private boolean mIsLowPriority;
74     private boolean mTransformLowPriorityTitle;
75     private RoundnessChangedListener mRoundnessChangedListener;
76 
NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row)77     protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
78         super(ctx, view, row);
79         mRoundableState = new RoundableState(
80                 mView,
81                 this,
82                 ctx.getResources().getDimension(R.dimen.notification_corner_radius)
83         );
84         mTransformationHelper = new ViewTransformationHelper();
85 
86         // we want to avoid that the header clashes with the other text when transforming
87         // low-priority
88         mTransformationHelper.setCustomTransformation(
89                 new CustomInterpolatorTransformation(TRANSFORMING_VIEW_TITLE) {
90 
91                     @Override
92                     public Interpolator getCustomInterpolator(
93                             int interpolationType,
94                             boolean isFrom) {
95                         boolean isLowPriority = mView instanceof NotificationHeaderView;
96                         if (interpolationType == TRANSFORM_Y) {
97                             if (isLowPriority && !isFrom
98                                     || !isLowPriority && isFrom) {
99                                 return Interpolators.LINEAR_OUT_SLOW_IN;
100                             } else {
101                                 return LOW_PRIORITY_HEADER_CLOSE;
102                             }
103                         }
104                         return null;
105                     }
106 
107                     @Override
108                     protected boolean hasCustomTransformation() {
109                         return mIsLowPriority && mTransformLowPriorityTitle;
110                     }
111                 },
112                 TRANSFORMING_VIEW_TITLE);
113         resolveHeaderViews();
114         addFeedbackOnClickListener(row);
115     }
116 
117     @Override
getRoundableState()118     public RoundableState getRoundableState() {
119         return mRoundableState;
120     }
121 
122     @Override
getClipHeight()123     public int getClipHeight() {
124         return mView.getHeight();
125     }
126 
127     @Override
applyRoundnessAndInvalidate()128     public void applyRoundnessAndInvalidate() {
129         if (mRoundnessChangedListener != null) {
130             // We cannot apply the rounded corner to this View, so our parents (in drawChild()) will
131             // clip our canvas. So we should invalidate our parent.
132             mRoundnessChangedListener.applyRoundnessAndInvalidate();
133         }
134         Roundable.super.applyRoundnessAndInvalidate();
135     }
136 
setOnRoundnessChangedListener(RoundnessChangedListener listener)137     public void setOnRoundnessChangedListener(RoundnessChangedListener listener) {
138         mRoundnessChangedListener = listener;
139     }
140 
resolveHeaderViews()141     protected void resolveHeaderViews() {
142         mIcon = mView.findViewById(com.android.internal.R.id.icon);
143         mHeaderText = mView.findViewById(com.android.internal.R.id.header_text);
144         mAppNameText = mView.findViewById(com.android.internal.R.id.app_name_text);
145         mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button);
146         mAltExpandTarget = mView.findViewById(com.android.internal.R.id.alternate_expand_target);
147         mIconContainer = mView.findViewById(com.android.internal.R.id.conversation_icon_container);
148         mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge);
149         mNotificationHeader = mView.findViewById(com.android.internal.R.id.notification_header);
150         mNotificationTopLine = mView.findViewById(com.android.internal.R.id.notification_top_line);
151         mAudiblyAlertedIcon = mView.findViewById(com.android.internal.R.id.alerted_icon);
152         mFeedbackIcon = mView.findViewById(com.android.internal.R.id.feedback);
153     }
154 
addFeedbackOnClickListener(ExpandableNotificationRow row)155     private void addFeedbackOnClickListener(ExpandableNotificationRow row) {
156         View.OnClickListener listener = row.getFeedbackOnClickListener();
157         if (mNotificationTopLine != null) {
158             mNotificationTopLine.setFeedbackOnClickListener(listener);
159         }
160         if (mFeedbackIcon != null) {
161             mFeedbackIcon.setOnClickListener(listener);
162         }
163     }
164 
165     /**
166      * Shows the given feedback icon, or hides the icon if null.
167      */
168     @Override
setFeedbackIcon(@ullable FeedbackIcon icon)169     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
170         if (mFeedbackIcon != null) {
171             mFeedbackIcon.setVisibility(icon != null ? View.VISIBLE : View.GONE);
172             if (icon != null) {
173                 if (mFeedbackIcon instanceof ImageButton) {
174                     ((ImageButton) mFeedbackIcon).setImageResource(icon.getIconRes());
175                 }
176                 mFeedbackIcon.setContentDescription(
177                         mView.getContext().getString(icon.getContentDescRes()));
178             }
179         }
180     }
181 
182     @Override
onContentUpdated(ExpandableNotificationRow row)183     public void onContentUpdated(ExpandableNotificationRow row) {
184         super.onContentUpdated(row);
185         mIsLowPriority = row.getEntry().isAmbient();
186         mTransformLowPriorityTitle = !row.isChildInGroup() && !row.isSummaryWithChildren();
187         ArraySet<View> previousViews = mTransformationHelper.getAllTransformingViews();
188 
189         // Reinspect the notification.
190         resolveHeaderViews();
191         updateTransformedTypes();
192         addRemainingTransformTypes();
193         updateCropToPaddingForImageViews();
194         Notification n = row.getEntry().getSbn().getNotification();
195         if (n.shouldUseAppIcon()) {
196             mIcon.setTag(ImageTransformState.ICON_TAG, n.getAppIcon());
197         } else {
198             mIcon.setTag(ImageTransformState.ICON_TAG, n.getSmallIcon());
199         }
200 
201         // We need to reset all views that are no longer transforming in case a view was previously
202         // transformed, but now we decided to transform its container instead.
203         ArraySet<View> currentViews = mTransformationHelper.getAllTransformingViews();
204         for (int i = 0; i < previousViews.size(); i++) {
205             View view = previousViews.valueAt(i);
206             if (!currentViews.contains(view)) {
207                 mTransformationHelper.resetTransformedView(view);
208             }
209         }
210     }
211 
212     /**
213      * Adds the remaining TransformTypes to the TransformHelper. This is done to make sure that each
214      * child is faded automatically and doesn't have to be manually added.
215      * The keys used for the views are the ids.
216      */
addRemainingTransformTypes()217     private void addRemainingTransformTypes() {
218         mTransformationHelper.addRemainingTransformTypes(mView);
219     }
220 
221     /**
222      * Since we are deactivating the clipping when transforming the ImageViews don't get clipped
223      * anymore during these transitions. We can avoid that by using
224      * {@link ImageView#setCropToPadding(boolean)} on all ImageViews.
225      */
updateCropToPaddingForImageViews()226     private void updateCropToPaddingForImageViews() {
227         Stack<View> stack = new Stack<>();
228         stack.push(mView);
229         while (!stack.isEmpty()) {
230             View child = stack.pop();
231             if (child instanceof ImageView
232                     // Skip the importance ring for conversations, disabled cropping is needed for
233                     // its animation
234                     && child.getId() != com.android.internal.R.id.conversation_icon_badge_ring) {
235                 ((ImageView) child).setCropToPadding(true);
236             } else if (child instanceof ViewGroup) {
237                 ViewGroup group = (ViewGroup) child;
238                 for (int i = 0; i < group.getChildCount(); i++) {
239                     stack.push(group.getChildAt(i));
240                 }
241             }
242         }
243     }
244 
updateTransformedTypes()245     protected void updateTransformedTypes() {
246         mTransformationHelper.reset();
247         mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ICON, mIcon);
248         mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_EXPANDER,
249                 mExpandButton);
250         if (mIsLowPriority && mHeaderText != null) {
251             mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE,
252                     mHeaderText);
253         }
254         addViewsTransformingToSimilar(mWorkProfileImage, mAudiblyAlertedIcon, mFeedbackIcon);
255     }
256 
257     @Override
updateExpandability( boolean expandable, View.OnClickListener onClickListener, boolean requestLayout)258     public void updateExpandability(
259             boolean expandable,
260             View.OnClickListener onClickListener,
261             boolean requestLayout) {
262         mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE);
263         mExpandButton.setOnClickListener(expandable ? onClickListener : null);
264         if (mAltExpandTarget != null) {
265             mAltExpandTarget.setOnClickListener(expandable ? onClickListener : null);
266         }
267         if (mIconContainer != null) {
268             mIconContainer.setOnClickListener(expandable ? onClickListener : null);
269         }
270         if (mNotificationHeader != null) {
271             mNotificationHeader.setOnClickListener(expandable ? onClickListener : null);
272         }
273         // Unfortunately, the NotificationContentView has to layout its children in order to
274         // determine their heights, and that affects the button visibility.  If that happens
275         // (thankfully it is rare) then we need to request layout of the expand button's parent
276         // in order to ensure it gets laid out correctly.
277         if (requestLayout) {
278             mExpandButton.getParent().requestLayout();
279         }
280     }
281 
282     @Override
setExpanded(boolean expanded)283     public void setExpanded(boolean expanded) {
284         mExpandButton.setExpanded(expanded);
285     }
286 
287     @Override
setRecentlyAudiblyAlerted(boolean audiblyAlerted)288     public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) {
289         if (mAudiblyAlertedIcon != null) {
290             mAudiblyAlertedIcon.setVisibility(audiblyAlerted ? View.VISIBLE : View.GONE);
291         }
292     }
293 
294     @Override
getNotificationHeader()295     public NotificationHeaderView getNotificationHeader() {
296         return mNotificationHeader;
297     }
298 
299     @Override
getExpandButton()300     public View getExpandButton() {
301         return mExpandButton;
302     }
303 
304     @Override
getIcon()305     public CachingIconView getIcon() {
306         return mIcon;
307     }
308 
309     @Override
getOriginalIconColor()310     public int getOriginalIconColor() {
311         return mIcon.getOriginalIconColor();
312     }
313 
314     @Override
getShelfTransformationTarget()315     public View getShelfTransformationTarget() {
316         return mIcon;
317     }
318 
319     @Override
getCurrentState(int fadingView)320     public TransformState getCurrentState(int fadingView) {
321         return mTransformationHelper.getCurrentState(fadingView);
322     }
323 
324     @Override
transformTo(TransformableView notification, Runnable endRunnable)325     public void transformTo(TransformableView notification, Runnable endRunnable) {
326         mTransformationHelper.transformTo(notification, endRunnable);
327     }
328 
329     @Override
transformTo(TransformableView notification, float transformationAmount)330     public void transformTo(TransformableView notification, float transformationAmount) {
331         mTransformationHelper.transformTo(notification, transformationAmount);
332     }
333 
334     @Override
transformFrom(TransformableView notification)335     public void transformFrom(TransformableView notification) {
336         mTransformationHelper.transformFrom(notification);
337     }
338 
339     @Override
transformFrom(TransformableView notification, float transformationAmount)340     public void transformFrom(TransformableView notification, float transformationAmount) {
341         mTransformationHelper.transformFrom(notification, transformationAmount);
342     }
343 
344     @Override
setIsChildInGroup(boolean isChildInGroup)345     public void setIsChildInGroup(boolean isChildInGroup) {
346         super.setIsChildInGroup(isChildInGroup);
347         mTransformLowPriorityTitle = !isChildInGroup;
348     }
349 
350     @Override
setVisible(boolean visible)351     public void setVisible(boolean visible) {
352         super.setVisible(visible);
353         mTransformationHelper.setVisible(visible);
354     }
355 
356     /***
357      * Set Notification when value
358      * @param whenMillis
359      */
setNotificationWhen(long whenMillis)360     public void setNotificationWhen(long whenMillis) {
361         final View timeView = mView.findViewById(com.android.internal.R.id.time);
362 
363         if (timeView instanceof DateTimeView) {
364             ((DateTimeView) timeView).setTime(whenMillis);
365         }
366     }
addTransformedViews(View... views)367     protected void addTransformedViews(View... views) {
368         for (View view : views) {
369             if (view != null) {
370                 mTransformationHelper.addTransformedView(view);
371             }
372         }
373     }
374 
addViewsTransformingToSimilar(View... views)375     protected void addViewsTransformingToSimilar(View... views) {
376         for (View view : views) {
377             if (view != null) {
378                 mTransformationHelper.addViewTransformingToSimilar(view);
379             }
380         }
381     }
382 
383     /**
384      * Interface that handle the Roundness changes
385      */
386     public interface RoundnessChangedListener {
387         /**
388          * This method will be called when this class call applyRoundnessAndInvalidate()
389          */
applyRoundnessAndInvalidate()390         void applyRoundnessAndInvalidate();
391     }
392 }
393