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