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