Home | History | Annotate | Download | only in notification
      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