2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.systemui.statusbar.notification;
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;
34 import com.android.internal.util.ContrastColorUtil;
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;
45 * Wraps a notification view inflated from a template.
47 public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapper {
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();
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;
64 protected NotificationTemplateViewWrapper(Context ctx, View view,
65 ExpandableNotificationRow row) {
66 super(ctx, view, row);
67 mTransformationHelper.setCustomTransformation(
68 new ViewTransformationHelper.CustomTransformation() {
70 public boolean transformTo(TransformState ownState,
71 TransformableView notification, final float transformationAmount) {
72 if (!(notification instanceof HybridNotificationView)) {
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);
88 public boolean customTransformTarget(TransformState ownState,
89 TransformState otherState) {
90 float endY = getTransformationY(ownState, otherState);
91 ownState.setTransformationEndY(endY);
96 public boolean transformFrom(TransformState ownState,
97 TransformableView notification, float transformationAmount) {
98 if (!(notification instanceof HybridNotificationView)) {
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();
114 public boolean initTransformation(TransformState ownState,
115 TransformState otherState) {
116 float startY = getTransformationY(ownState, otherState);
117 ownState.setTransformationStartY(startY);
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;
130 }, TRANSFORMING_VIEW_TEXT);
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());
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;
145 // It's still a viewstub
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();
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;
177 ColorStateList newColorStateList = new ColorStateList(
178 textColors.getStates(), newColors);
179 action.setTextColor(newColorStateList);
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());
204 mReplyAction.setAlpha(disabledAlpha);
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 ContrastColorUtil.compositeColors(Color.argb((int) (alpha * 255),
214 Color.red(color), Color.green(color), Color.blue(color)), resolveBackgroundColor());
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) {
223 if (mCancelledPendingIntents.contains(pendingIntent)) {
224 cancellationRunnable.run();
226 PendingIntent.CancelListener listener = (PendingIntent intent) -> {
228 mCancelledPendingIntents.add(pendingIntent);
229 cancellationRunnable.run();
232 if (mUiOffloadThread == null) {
233 mUiOffloadThread = Dependency.get(UiOffloadThread.class);
235 if (view.isAttachedToWindow()) {
236 mUiOffloadThread.submit(() -> pendingIntent.registerCancelListener(listener));
238 view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
240 public void onViewAttachedToWindow(View v) {
241 mUiOffloadThread.submit(() -> pendingIntent.registerCancelListener(listener));
245 public void onViewDetachedFromWindow(View v) {
246 mUiOffloadThread.submit(() -> pendingIntent.unregisterCancelListener(listener));
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)) {
259 return super.disallowSingleClick(x, y);
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);
268 searchView = (View) searchView.getParent();
270 view.getHitRect(mTmpRect);
271 return mTmpRect.contains((int) x,(int) y);
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);
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,
291 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TEXT,
294 if (mPicture != null) {
295 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_IMAGE,
298 if (mProgressBar != null) {
299 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_PROGRESS,
305 public void setContentHeight(int contentHeight, int minHeightHint) {
306 super.setContentHeight(contentHeight, minHeightHint);
308 mContentHeight = contentHeight;
309 mMinHeightHint = minHeightHint;
310 updateActionOffset();
314 public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
315 if (super.shouldClipToRounding(topRounded, bottomRounded)) {
318 return bottomRounded && mActionsContainer != null
319 && mActionsContainer.getVisibility() != View.GONE;
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);
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());
334 public int getExtraMeasureHeight() {
336 if (mActions != null) {
337 extra = mActions.getExtraMeasureHeight();
339 if (mRemoteInputHistory != null && mRemoteInputHistory.getVisibility() != View.GONE) {
340 extra += mRow.getContext().getResources().getDimensionPixelSize(
341 R.dimen.remote_input_history_extra_height);
343 return extra + super.getExtraMeasureHeight();