1 /* 2 * Copyright (C) 2014 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; 18 19 import android.app.PendingIntent; 20 import android.content.Context; 21 import android.content.res.ColorStateList; 22 import android.graphics.Color; 23 import android.graphics.PorterDuffColorFilter; 24 import android.graphics.Rect; 25 import android.graphics.drawable.Drawable; 26 import android.service.notification.StatusBarNotification; 27 import android.util.ArraySet; 28 import android.view.View; 29 import android.widget.Button; 30 import android.widget.ImageView; 31 import android.widget.ProgressBar; 32 import android.widget.TextView; 33 34 import com.android.internal.util.NotificationColorUtil; 35 import com.android.internal.widget.NotificationActionListLayout; 36 import com.android.systemui.Dependency; 37 import com.android.systemui.R; 38 import com.android.systemui.UiOffloadThread; 39 import com.android.systemui.statusbar.CrossFadeHelper; 40 import com.android.systemui.statusbar.ExpandableNotificationRow; 41 import com.android.systemui.statusbar.TransformableView; 42 import com.android.systemui.statusbar.ViewTransformationHelper; 43 44 /** 45 * Wraps a notification view inflated from a template. 46 */ 47 public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapper { 48 49 protected ImageView mPicture; 50 private ProgressBar mProgressBar; 51 private TextView mTitle; 52 private TextView mText; 53 protected View mActionsContainer; 54 private ImageView mReplyAction; 55 private Rect mTmpRect = new Rect(); 56 57 private int mContentHeight; 58 private int mMinHeightHint; 59 private NotificationActionListLayout mActions; 60 private ArraySet<PendingIntent> mCancelledPendingIntents = new ArraySet<>(); 61 private UiOffloadThread mUiOffloadThread; 62 private View mRemoteInputHistory; 63 64 protected NotificationTemplateViewWrapper(Context ctx, View view, 65 ExpandableNotificationRow row) { 66 super(ctx, view, row); 67 mTransformationHelper.setCustomTransformation( 68 new ViewTransformationHelper.CustomTransformation() { 69 @Override 70 public boolean transformTo(TransformState ownState, 71 TransformableView notification, final float transformationAmount) { 72 if (!(notification instanceof HybridNotificationView)) { 73 return false; 74 } 75 TransformState otherState = notification.getCurrentState( 76 TRANSFORMING_VIEW_TITLE); 77 final View text = ownState.getTransformedView(); 78 CrossFadeHelper.fadeOut(text, transformationAmount); 79 if (otherState != null) { 80 ownState.transformViewVerticalTo(otherState, this, 81 transformationAmount); 82 otherState.recycle(); 83 } 84 return true; 85 } 86 87 @Override 88 public boolean customTransformTarget(TransformState ownState, 89 TransformState otherState) { 90 float endY = getTransformationY(ownState, otherState); 91 ownState.setTransformationEndY(endY); 92 return true; 93 } 94 95 @Override 96 public boolean transformFrom(TransformState ownState, 97 TransformableView notification, float transformationAmount) { 98 if (!(notification instanceof HybridNotificationView)) { 99 return false; 100 } 101 TransformState otherState = notification.getCurrentState( 102 TRANSFORMING_VIEW_TITLE); 103 final View text = ownState.getTransformedView(); 104 CrossFadeHelper.fadeIn(text, transformationAmount); 105 if (otherState != null) { 106 ownState.transformViewVerticalFrom(otherState, this, 107 transformationAmount); 108 otherState.recycle(); 109 } 110 return true; 111 } 112 113 @Override 114 public boolean initTransformation(TransformState ownState, 115 TransformState otherState) { 116 float startY = getTransformationY(ownState, otherState); 117 ownState.setTransformationStartY(startY); 118 return true; 119 } 120 121 private float getTransformationY(TransformState ownState, 122 TransformState otherState) { 123 int[] otherStablePosition = otherState.getLaidOutLocationOnScreen(); 124 int[] ownStablePosition = ownState.getLaidOutLocationOnScreen(); 125 return (otherStablePosition[1] 126 + otherState.getTransformedView().getHeight() 127 - ownStablePosition[1]) * 0.33f; 128 } 129 130 }, TRANSFORMING_VIEW_TEXT); 131 } 132 133 private void resolveTemplateViews(StatusBarNotification notification) { 134 mPicture = (ImageView) mView.findViewById(com.android.internal.R.id.right_icon); 135 if (mPicture != null) { 136 mPicture.setTag(ImageTransformState.ICON_TAG, 137 notification.getNotification().getLargeIcon()); 138 } 139 mTitle = (TextView) mView.findViewById(com.android.internal.R.id.title); 140 mText = (TextView) mView.findViewById(com.android.internal.R.id.text); 141 final View progress = mView.findViewById(com.android.internal.R.id.progress); 142 if (progress instanceof ProgressBar) { 143 mProgressBar = (ProgressBar) progress; 144 } else { 145 // It's still a viewstub 146 mProgressBar = null; 147 } 148 mActionsContainer = mView.findViewById(com.android.internal.R.id.actions_container); 149 mActions = mView.findViewById(com.android.internal.R.id.actions); 150 mReplyAction = mView.findViewById(com.android.internal.R.id.reply_icon_action); 151 mRemoteInputHistory = mView.findViewById( 152 com.android.internal.R.id.notification_material_reply_container); 153 updatePendingIntentCancellations(); 154 } 155 156 private void updatePendingIntentCancellations() { 157 if (mActions != null) { 158 int numActions = mActions.getChildCount(); 159 for (int i = 0; i < numActions; i++) { 160 Button action = (Button) mActions.getChildAt(i); 161 performOnPendingIntentCancellation(action, () -> { 162 if (action.isEnabled()) { 163 action.setEnabled(false); 164 // The visual appearance doesn't look disabled enough yet, let's add the 165 // alpha as well. Since Alpha doesn't play nicely right now with the 166 // transformation, we rather blend it manually with the background color. 167 ColorStateList textColors = action.getTextColors(); 168 int[] colors = textColors.getColors(); 169 int[] newColors = new int[colors.length]; 170 float disabledAlpha = mView.getResources().getFloat( 171 com.android.internal.R.dimen.notification_action_disabled_alpha); 172 for (int j = 0; j < colors.length; j++) { 173 int color = colors[j]; 174 color = blendColorWithBackground(color, disabledAlpha); 175 newColors[j] = color; 176 } 177 ColorStateList newColorStateList = new ColorStateList( 178 textColors.getStates(), newColors); 179 action.setTextColor(newColorStateList); 180 } 181 }); 182 } 183 } 184 if (mReplyAction != null) { 185 // Let's reset the view on update, assuming the new pending intent isn't cancelled 186 // anymore. The color filter automatically resets when it's updated. 187 mReplyAction.setEnabled(true); 188 performOnPendingIntentCancellation(mReplyAction, () -> { 189 if (mReplyAction != null && mReplyAction.isEnabled()) { 190 mReplyAction.setEnabled(false); 191 // The visual appearance doesn't look disabled enough yet, let's add the 192 // alpha as well. Since Alpha doesn't play nicely right now with the 193 // transformation, we rather blend it manually with the background color. 194 Drawable drawable = mReplyAction.getDrawable().mutate(); 195 PorterDuffColorFilter colorFilter = 196 (PorterDuffColorFilter) drawable.getColorFilter(); 197 float disabledAlpha = mView.getResources().getFloat( 198 com.android.internal.R.dimen.notification_action_disabled_alpha); 199 if (colorFilter != null) { 200 int color = colorFilter.getColor(); 201 color = blendColorWithBackground(color, disabledAlpha); 202 drawable.mutate().setColorFilter(color, colorFilter.getMode()); 203 } else { 204 mReplyAction.setAlpha(disabledAlpha); 205 } 206 } 207 }); 208 } 209 } 210 211 private int blendColorWithBackground(int color, float alpha) { 212 // alpha doesn't go well for color filters, so let's blend it manually 213 return NotificationColorUtil.compositeColors(Color.argb((int) (alpha * 255), 214 Color.red(color), Color.green(color), Color.blue(color)), resolveBackgroundColor()); 215 } 216 217 private void performOnPendingIntentCancellation(View view, Runnable cancellationRunnable) { 218 PendingIntent pendingIntent = (PendingIntent) view.getTag( 219 com.android.internal.R.id.pending_intent_tag); 220 if (pendingIntent == null) { 221 return; 222 } 223 if (mCancelledPendingIntents.contains(pendingIntent)) { 224 cancellationRunnable.run(); 225 } else { 226 PendingIntent.CancelListener listener = (PendingIntent intent) -> { 227 mView.post(() -> { 228 mCancelledPendingIntents.add(pendingIntent); 229 cancellationRunnable.run(); 230 }); 231 }; 232 if (mUiOffloadThread == null) { 233 mUiOffloadThread = Dependency.get(UiOffloadThread.class); 234 } 235 if (view.isAttachedToWindow()) { 236 mUiOffloadThread.submit(() -> pendingIntent.registerCancelListener(listener)); 237 } 238 view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 239 @Override 240 public void onViewAttachedToWindow(View v) { 241 mUiOffloadThread.submit(() -> pendingIntent.registerCancelListener(listener)); 242 } 243 244 @Override 245 public void onViewDetachedFromWindow(View v) { 246 mUiOffloadThread.submit(() -> pendingIntent.unregisterCancelListener(listener)); 247 } 248 }); 249 } 250 } 251 252 @Override 253 public boolean disallowSingleClick(float x, float y) { 254 if (mReplyAction != null && mReplyAction.getVisibility() == View.VISIBLE) { 255 if (isOnView(mReplyAction, x, y) || isOnView(mPicture, x, y)) { 256 return true; 257 } 258 } 259 return super.disallowSingleClick(x, y); 260 } 261 262 private boolean isOnView(View view, float x, float y) { 263 View searchView = (View) view.getParent(); 264 while (searchView != null && !(searchView instanceof ExpandableNotificationRow)) { 265 searchView.getHitRect(mTmpRect); 266 x -= mTmpRect.left; 267 y -= mTmpRect.top; 268 searchView = (View) searchView.getParent(); 269 } 270 view.getHitRect(mTmpRect); 271 return mTmpRect.contains((int) x,(int) y); 272 } 273 274 @Override 275 public void onContentUpdated(ExpandableNotificationRow row) { 276 // Reinspect the notification. Before the super call, because the super call also updates 277 // the transformation types and we need to have our values set by then. 278 resolveTemplateViews(row.getStatusBarNotification()); 279 super.onContentUpdated(row); 280 } 281 282 @Override 283 protected void updateTransformedTypes() { 284 // This also clears the existing types 285 super.updateTransformedTypes(); 286 if (mTitle != null) { 287 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE, 288 mTitle); 289 } 290 if (mText != null) { 291 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT, 292 mText); 293 } 294 if (mPicture != null) { 295 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_IMAGE, 296 mPicture); 297 } 298 if (mProgressBar != null) { 299 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_PROGRESS, 300 mProgressBar); 301 } 302 } 303 304 @Override 305 public void setContentHeight(int contentHeight, int minHeightHint) { 306 super.setContentHeight(contentHeight, minHeightHint); 307 308 mContentHeight = contentHeight; 309 mMinHeightHint = minHeightHint; 310 updateActionOffset(); 311 } 312 313 @Override 314 public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) { 315 if (super.shouldClipToRounding(topRounded, bottomRounded)) { 316 return true; 317 } 318 return bottomRounded && mActionsContainer != null 319 && mActionsContainer.getVisibility() != View.GONE; 320 } 321 322 private void updateActionOffset() { 323 if (mActionsContainer != null) { 324 // We should never push the actions higher than they are in the headsup view. 325 int constrainedContentHeight = Math.max(mContentHeight, mMinHeightHint); 326 327 // We also need to compensate for any header translation, since we're always at the end. 328 mActionsContainer.setTranslationY(constrainedContentHeight - mView.getHeight() 329 - getHeaderTranslation()); 330 } 331 } 332 333 @Override 334 public int getExtraMeasureHeight() { 335 int extra = 0; 336 if (mActions != null) { 337 extra = mActions.getExtraMeasureHeight(); 338 } 339 if (mRemoteInputHistory != null && mRemoteInputHistory.getVisibility() != View.GONE) { 340 extra += mRow.getContext().getResources().getDimensionPixelSize( 341 R.dimen.remote_input_history_extra_height); 342 } 343 return extra + super.getExtraMeasureHeight(); 344 } 345 } 346