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.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 notification = row.getEntry().getSbn().getNotification();
195         mIcon.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon());
196 
197         // We need to reset all views that are no longer transforming in case a view was previously
198         // transformed, but now we decided to transform its container instead.
199         ArraySet<View> currentViews = mTransformationHelper.getAllTransformingViews();
200         for (int i = 0; i < previousViews.size(); i++) {
201             View view = previousViews.valueAt(i);
202             if (!currentViews.contains(view)) {
203                 mTransformationHelper.resetTransformedView(view);
204             }
205         }
206     }
207 
208     /**
209      * Adds the remaining TransformTypes to the TransformHelper. This is done to make sure that each
210      * child is faded automatically and doesn't have to be manually added.
211      * The keys used for the views are the ids.
212      */
addRemainingTransformTypes()213     private void addRemainingTransformTypes() {
214         mTransformationHelper.addRemainingTransformTypes(mView);
215     }
216 
217     /**
218      * Since we are deactivating the clipping when transforming the ImageViews don't get clipped
219      * anymore during these transitions. We can avoid that by using
220      * {@link ImageView#setCropToPadding(boolean)} on all ImageViews.
221      */
updateCropToPaddingForImageViews()222     private void updateCropToPaddingForImageViews() {
223         Stack<View> stack = new Stack<>();
224         stack.push(mView);
225         while (!stack.isEmpty()) {
226             View child = stack.pop();
227             if (child instanceof ImageView
228                     // Skip the importance ring for conversations, disabled cropping is needed for
229                     // its animation
230                     && child.getId() != com.android.internal.R.id.conversation_icon_badge_ring) {
231                 ((ImageView) child).setCropToPadding(true);
232             } else if (child instanceof ViewGroup) {
233                 ViewGroup group = (ViewGroup) child;
234                 for (int i = 0; i < group.getChildCount(); i++) {
235                     stack.push(group.getChildAt(i));
236                 }
237             }
238         }
239     }
240 
updateTransformedTypes()241     protected void updateTransformedTypes() {
242         mTransformationHelper.reset();
243         mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ICON, mIcon);
244         mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_EXPANDER,
245                 mExpandButton);
246         if (mIsLowPriority && mHeaderText != null) {
247             mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE,
248                     mHeaderText);
249         }
250         addViewsTransformingToSimilar(mWorkProfileImage, mAudiblyAlertedIcon, mFeedbackIcon);
251     }
252 
253     @Override
updateExpandability( boolean expandable, View.OnClickListener onClickListener, boolean requestLayout)254     public void updateExpandability(
255             boolean expandable,
256             View.OnClickListener onClickListener,
257             boolean requestLayout) {
258         mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE);
259         mExpandButton.setOnClickListener(expandable ? onClickListener : null);
260         if (mAltExpandTarget != null) {
261             mAltExpandTarget.setOnClickListener(expandable ? onClickListener : null);
262         }
263         if (mIconContainer != null) {
264             mIconContainer.setOnClickListener(expandable ? onClickListener : null);
265         }
266         if (mNotificationHeader != null) {
267             mNotificationHeader.setOnClickListener(expandable ? onClickListener : null);
268         }
269         // Unfortunately, the NotificationContentView has to layout its children in order to
270         // determine their heights, and that affects the button visibility.  If that happens
271         // (thankfully it is rare) then we need to request layout of the expand button's parent
272         // in order to ensure it gets laid out correctly.
273         if (requestLayout) {
274             mExpandButton.getParent().requestLayout();
275         }
276     }
277 
278     @Override
setExpanded(boolean expanded)279     public void setExpanded(boolean expanded) {
280         mExpandButton.setExpanded(expanded);
281     }
282 
283     @Override
setRecentlyAudiblyAlerted(boolean audiblyAlerted)284     public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) {
285         if (mAudiblyAlertedIcon != null) {
286             mAudiblyAlertedIcon.setVisibility(audiblyAlerted ? View.VISIBLE : View.GONE);
287         }
288     }
289 
290     @Override
getNotificationHeader()291     public NotificationHeaderView getNotificationHeader() {
292         return mNotificationHeader;
293     }
294 
295     @Override
getExpandButton()296     public View getExpandButton() {
297         return mExpandButton;
298     }
299 
300     @Override
getIcon()301     public CachingIconView getIcon() {
302         return mIcon;
303     }
304 
305     @Override
getOriginalIconColor()306     public int getOriginalIconColor() {
307         return mIcon.getOriginalIconColor();
308     }
309 
310     @Override
getShelfTransformationTarget()311     public View getShelfTransformationTarget() {
312         return mIcon;
313     }
314 
315     @Override
getCurrentState(int fadingView)316     public TransformState getCurrentState(int fadingView) {
317         return mTransformationHelper.getCurrentState(fadingView);
318     }
319 
320     @Override
transformTo(TransformableView notification, Runnable endRunnable)321     public void transformTo(TransformableView notification, Runnable endRunnable) {
322         mTransformationHelper.transformTo(notification, endRunnable);
323     }
324 
325     @Override
transformTo(TransformableView notification, float transformationAmount)326     public void transformTo(TransformableView notification, float transformationAmount) {
327         mTransformationHelper.transformTo(notification, transformationAmount);
328     }
329 
330     @Override
transformFrom(TransformableView notification)331     public void transformFrom(TransformableView notification) {
332         mTransformationHelper.transformFrom(notification);
333     }
334 
335     @Override
transformFrom(TransformableView notification, float transformationAmount)336     public void transformFrom(TransformableView notification, float transformationAmount) {
337         mTransformationHelper.transformFrom(notification, transformationAmount);
338     }
339 
340     @Override
setIsChildInGroup(boolean isChildInGroup)341     public void setIsChildInGroup(boolean isChildInGroup) {
342         super.setIsChildInGroup(isChildInGroup);
343         mTransformLowPriorityTitle = !isChildInGroup;
344     }
345 
346     @Override
setVisible(boolean visible)347     public void setVisible(boolean visible) {
348         super.setVisible(visible);
349         mTransformationHelper.setVisible(visible);
350     }
351 
352     /***
353      * Set Notification when value
354      * @param whenMillis
355      */
setNotificationWhen(long whenMillis)356     public void setNotificationWhen(long whenMillis) {
357         if (mNotificationHeader == null) {
358             return;
359         }
360 
361         final View timeView = mNotificationHeader.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