| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.systemui.statusbar.notification.row; |
| |
| import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY; |
| import static android.service.notification.NotificationListenerService.REASON_CANCEL; |
| |
| import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; |
| import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ObjectAnimator; |
| import android.animation.ValueAnimator.AnimatorUpdateListener; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.INotificationManager; |
| import android.app.Notification; |
| import android.app.NotificationChannel; |
| import android.app.role.RoleManager; |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Path; |
| import android.graphics.Point; |
| import android.graphics.drawable.AnimatedVectorDrawable; |
| import android.graphics.drawable.AnimationDrawable; |
| import android.graphics.drawable.ColorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.Trace; |
| import android.service.notification.StatusBarNotification; |
| import android.util.ArraySet; |
| import android.util.AttributeSet; |
| import android.util.FloatProperty; |
| import android.util.IndentingPrintWriter; |
| import android.util.Log; |
| import android.util.MathUtils; |
| import android.util.Property; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.NotificationHeaderView; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewStub; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; |
| import android.widget.Chronometer; |
| import android.widget.FrameLayout; |
| import android.widget.ImageView; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.logging.MetricsLogger; |
| import com.android.internal.logging.nano.MetricsProto.MetricsEvent; |
| import com.android.internal.util.ContrastColorUtil; |
| import com.android.internal.widget.CachingIconView; |
| import com.android.internal.widget.CallLayout; |
| import com.android.systemui.R; |
| import com.android.systemui.animation.Interpolators; |
| import com.android.systemui.classifier.FalsingCollector; |
| import com.android.systemui.plugins.FalsingManager; |
| import com.android.systemui.plugins.PluginListener; |
| import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; |
| import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem; |
| import com.android.systemui.plugins.statusbar.StatusBarStateController; |
| import com.android.systemui.statusbar.NotificationMediaManager; |
| import com.android.systemui.statusbar.RemoteInputController; |
| import com.android.systemui.statusbar.SmartReplyController; |
| import com.android.systemui.statusbar.StatusBarIconView; |
| import com.android.systemui.statusbar.notification.AboveShelfChangedListener; |
| import com.android.systemui.statusbar.notification.FeedbackIcon; |
| import com.android.systemui.statusbar.notification.LaunchAnimationParameters; |
| import com.android.systemui.statusbar.notification.NotificationFadeAware; |
| import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController; |
| import com.android.systemui.statusbar.notification.NotificationUtils; |
| import com.android.systemui.statusbar.notification.collection.NotificationEntry; |
| import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager; |
| import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; |
| import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; |
| import com.android.systemui.statusbar.notification.logging.NotificationCounters; |
| import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; |
| import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; |
| import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; |
| import com.android.systemui.statusbar.notification.stack.AmbientState; |
| import com.android.systemui.statusbar.notification.stack.AnimationProperties; |
| import com.android.systemui.statusbar.notification.stack.ExpandableViewState; |
| import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer; |
| import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; |
| import com.android.systemui.statusbar.notification.stack.SwipeableView; |
| import com.android.systemui.statusbar.phone.KeyguardBypassController; |
| import com.android.systemui.statusbar.policy.HeadsUpManager; |
| import com.android.systemui.statusbar.policy.InflatedSmartReplyState; |
| import com.android.systemui.statusbar.policy.SmartReplyConstants; |
| import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent; |
| import com.android.systemui.util.Compile; |
| import com.android.systemui.util.DumpUtilsKt; |
| import com.android.systemui.wmshell.BubblesManager; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.BooleanSupplier; |
| import java.util.function.Consumer; |
| |
| /** |
| * View representing a notification item - this can be either the individual child notification or |
| * the group summary (which contains 1 or more child notifications). |
| */ |
| public class ExpandableNotificationRow extends ActivatableNotificationView |
| implements PluginListener<NotificationMenuRowPlugin>, SwipeableView, |
| NotificationFadeAware.FadeOptimizedNotification { |
| |
| private static final String TAG = "ExpandableNotifRow"; |
| private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG); |
| private static final int DEFAULT_DIVIDER_ALPHA = 0x29; |
| private static final int COLORED_DIVIDER_ALPHA = 0x7B; |
| private static final int MENU_VIEW_INDEX = 0; |
| public static final float DEFAULT_HEADER_VISIBLE_AMOUNT = 1.0f; |
| private static final long RECENTLY_ALERTED_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); |
| |
| // We don't correctly track dark mode until the content views are inflated, so always update |
| // the background on first content update just in case it happens to be during a theme change. |
| private boolean mUpdateSelfBackgroundOnUpdate = true; |
| private boolean mNotificationTranslationFinished = false; |
| private boolean mIsSnoozed; |
| private boolean mIsFaded; |
| |
| /** |
| * Listener for when {@link ExpandableNotificationRow} is laid out. |
| */ |
| public interface LayoutListener { |
| void onLayout(); |
| } |
| |
| /** Listens for changes to the expansion state of this row. */ |
| public interface OnExpansionChangedListener { |
| void onExpansionChanged(boolean isExpanded); |
| } |
| |
| private StatusBarStateController mStatusBarStateController; |
| private KeyguardBypassController mBypassController; |
| private LayoutListener mLayoutListener; |
| private RowContentBindStage mRowContentBindStage; |
| private PeopleNotificationIdentifier mPeopleNotificationIdentifier; |
| private Optional<BubblesManager> mBubblesManagerOptional; |
| private MetricsLogger mMetricsLogger; |
| private int mIconTransformContentShift; |
| private int mMaxHeadsUpHeightBeforeN; |
| private int mMaxHeadsUpHeightBeforeP; |
| private int mMaxHeadsUpHeightBeforeS; |
| private int mMaxHeadsUpHeight; |
| private int mMaxHeadsUpHeightIncreased; |
| private int mMaxSmallHeightBeforeN; |
| private int mMaxSmallHeightBeforeP; |
| private int mMaxSmallHeightBeforeS; |
| private int mMaxSmallHeight; |
| private int mMaxSmallHeightLarge; |
| private int mMaxSmallHeightMedia; |
| private int mMaxExpandedHeight; |
| private int mIncreasedPaddingBetweenElements; |
| private int mNotificationLaunchHeight; |
| private boolean mMustStayOnScreen; |
| |
| /** Does this row contain layouts that can adapt to row expansion */ |
| private boolean mExpandable; |
| /** Has the user actively changed the expansion state of this row */ |
| private boolean mHasUserChangedExpansion; |
| /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */ |
| private boolean mUserExpanded; |
| /** Whether the blocking helper is showing on this notification (even if dismissed) */ |
| private boolean mIsBlockingHelperShowing; |
| |
| /** |
| * Has this notification been expanded while it was pinned |
| */ |
| private boolean mExpandedWhenPinned; |
| /** Is the user touching this row */ |
| private boolean mUserLocked; |
| /** Are we showing the "public" version */ |
| private boolean mShowingPublic; |
| private boolean mSensitive; |
| private boolean mSensitiveHiddenInGeneral; |
| private boolean mShowingPublicInitialized; |
| private boolean mHideSensitiveForIntrinsicHeight; |
| private float mHeaderVisibleAmount = DEFAULT_HEADER_VISIBLE_AMOUNT; |
| |
| /** |
| * Is this notification expanded by the system. The expansion state can be overridden by the |
| * user expansion. |
| */ |
| private boolean mIsSystemExpanded; |
| |
| /** |
| * Whether the notification is on the keyguard and the expansion is disabled. |
| */ |
| private boolean mOnKeyguard; |
| |
| private Animator mTranslateAnim; |
| private ArrayList<View> mTranslateableViews; |
| private NotificationContentView mPublicLayout; |
| private NotificationContentView mPrivateLayout; |
| private NotificationContentView[] mLayouts; |
| private int mNotificationColor; |
| private ExpansionLogger mLogger; |
| private String mLoggingKey; |
| private NotificationGuts mGuts; |
| private NotificationEntry mEntry; |
| private String mAppName; |
| private FalsingManager mFalsingManager; |
| private FalsingCollector mFalsingCollector; |
| |
| /** |
| * Whether or not the notification is using the heads up view and should peek from the top. |
| */ |
| private boolean mIsHeadsUp; |
| |
| /** |
| * Whether or not the notification should be redacted on the lock screen, i.e has sensitive |
| * content which should be redacted on the lock screen. |
| */ |
| private boolean mNeedsRedaction; |
| private boolean mLastChronometerRunning = true; |
| private ViewStub mChildrenContainerStub; |
| private GroupMembershipManager mGroupMembershipManager; |
| private GroupExpansionManager mGroupExpansionManager; |
| private boolean mChildrenExpanded; |
| private boolean mIsSummaryWithChildren; |
| private NotificationChildrenContainer mChildrenContainer; |
| private NotificationMenuRowPlugin mMenuRow; |
| private ViewStub mGutsStub; |
| private boolean mIsSystemChildExpanded; |
| private boolean mIsPinned; |
| private boolean mExpandAnimationRunning; |
| private AboveShelfChangedListener mAboveShelfChangedListener; |
| private HeadsUpManager mHeadsUpManager; |
| private Consumer<Boolean> mHeadsUpAnimatingAwayListener; |
| private boolean mChildIsExpanding; |
| |
| private boolean mJustClicked; |
| private boolean mIconAnimationRunning; |
| private boolean mShowNoBackground; |
| private ExpandableNotificationRow mNotificationParent; |
| private OnExpandClickListener mOnExpandClickListener; |
| private View.OnClickListener mOnAppClickListener; |
| private View.OnClickListener mOnFeedbackClickListener; |
| private Path mExpandingClipPath; |
| |
| // Listener will be called when receiving a long click event. |
| // Use #setLongPressPosition to optionally assign positional data with the long press. |
| private LongPressListener mLongPressListener; |
| |
| private ExpandableNotificationRowDragController mDragController; |
| |
| private boolean mGroupExpansionChanging; |
| |
| /** |
| * A supplier that returns true if keyguard is secure. |
| */ |
| private BooleanSupplier mSecureStateProvider; |
| |
| /** |
| * Whether or not a notification that is not part of a group of notifications can be manually |
| * expanded by the user. |
| */ |
| private boolean mEnableNonGroupedNotificationExpand; |
| |
| /** |
| * Whether or not to update the background of the header of the notification when its expanded. |
| * If {@code true}, the header background will disappear when expanded. |
| */ |
| private boolean mShowGroupBackgroundWhenExpanded; |
| |
| private OnClickListener mExpandClickListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (!shouldShowPublic() && (!mIsLowPriority || isExpanded()) |
| && mGroupMembershipManager.isGroupSummary(mEntry)) { |
| mGroupExpansionChanging = true; |
| final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); |
| boolean nowExpanded = mGroupExpansionManager.toggleGroupExpansion(mEntry); |
| mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded); |
| mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded); |
| onExpansionChanged(true /* userAction */, wasExpanded); |
| } else if (mEnableNonGroupedNotificationExpand) { |
| if (v.isAccessibilityFocused()) { |
| mPrivateLayout.setFocusOnVisibilityChange(); |
| } |
| boolean nowExpanded; |
| if (isPinned()) { |
| nowExpanded = !mExpandedWhenPinned; |
| mExpandedWhenPinned = nowExpanded; |
| // Also notify any expansion changed listeners. This is necessary since the |
| // expansion doesn't actually change (it's already system expanded) but it |
| // changes visually |
| if (mExpansionChangedListener != null) { |
| mExpansionChangedListener.onExpansionChanged(nowExpanded); |
| } |
| } else { |
| nowExpanded = !isExpanded(); |
| setUserExpanded(nowExpanded); |
| } |
| notifyHeightChanged(true); |
| mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded); |
| mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_EXPANDER, nowExpanded); |
| } |
| } |
| }; |
| private boolean mKeepInParent; |
| private boolean mRemoved; |
| private static final Property<ExpandableNotificationRow, Float> TRANSLATE_CONTENT = |
| new FloatProperty<ExpandableNotificationRow>("translate") { |
| @Override |
| public void setValue(ExpandableNotificationRow object, float value) { |
| object.setTranslation(value); |
| } |
| |
| @Override |
| public Float get(ExpandableNotificationRow object) { |
| return object.getTranslation(); |
| } |
| }; |
| private OnClickListener mOnClickListener; |
| private OnDragSuccessListener mOnDragSuccessListener; |
| private boolean mHeadsupDisappearRunning; |
| private View mChildAfterViewWhenDismissed; |
| private View mGroupParentWhenDismissed; |
| private boolean mAboveShelf; |
| private OnUserInteractionCallback mOnUserInteractionCallback; |
| private NotificationGutsManager mNotificationGutsManager; |
| private boolean mIsLowPriority; |
| private boolean mUseIncreasedCollapsedHeight; |
| private boolean mUseIncreasedHeadsUpHeight; |
| private float mTranslationWhenRemoved; |
| private boolean mWasChildInGroupWhenRemoved; |
| private NotificationInlineImageResolver mImageResolver; |
| private NotificationMediaManager mMediaManager; |
| @Nullable private OnExpansionChangedListener mExpansionChangedListener; |
| @Nullable private Runnable mOnIntrinsicHeightReachedRunnable; |
| |
| private float mTopRoundnessDuringLaunchAnimation; |
| private float mBottomRoundnessDuringLaunchAnimation; |
| |
| /** |
| * Returns whether the given {@code statusBarNotification} is a system notification. |
| * <b>Note</b>, this should be run in the background thread if possible as it makes multiple IPC |
| * calls. |
| */ |
| private static Boolean isSystemNotification(Context context, StatusBarNotification sbn) { |
| INotificationManager iNm = INotificationManager.Stub.asInterface( |
| ServiceManager.getService(Context.NOTIFICATION_SERVICE)); |
| |
| boolean isSystem = false; |
| try { |
| isSystem = iNm.isPermissionFixed(sbn.getPackageName(), sbn.getUserId()); |
| } catch (RemoteException e) { |
| Log.e(TAG, "cannot reach NMS"); |
| } |
| RoleManager rm = context.getSystemService(RoleManager.class); |
| List<String> fixedRoleHolders = new ArrayList<>(); |
| fixedRoleHolders.addAll(rm.getRoleHolders(RoleManager.ROLE_DIALER)); |
| fixedRoleHolders.addAll(rm.getRoleHolders(RoleManager.ROLE_EMERGENCY)); |
| if (fixedRoleHolders.contains(sbn.getPackageName())) { |
| isSystem = true; |
| } |
| |
| return isSystem; |
| } |
| |
| public NotificationContentView[] getLayouts() { |
| return Arrays.copyOf(mLayouts, mLayouts.length); |
| } |
| |
| /** |
| * Is this entry pinned and was expanded while doing so |
| */ |
| public boolean isPinnedAndExpanded() { |
| if (!isPinned()) { |
| return false; |
| } |
| return mExpandedWhenPinned; |
| } |
| |
| @Override |
| public boolean isGroupExpansionChanging() { |
| if (isChildInGroup()) { |
| return mNotificationParent.isGroupExpansionChanging(); |
| } |
| return mGroupExpansionChanging; |
| } |
| |
| public void setGroupExpansionChanging(boolean changing) { |
| mGroupExpansionChanging = changing; |
| } |
| |
| @Override |
| public void setActualHeightAnimating(boolean animating) { |
| if (mPrivateLayout != null) { |
| mPrivateLayout.setContentHeightAnimating(animating); |
| } |
| } |
| |
| public NotificationContentView getPrivateLayout() { |
| return mPrivateLayout; |
| } |
| |
| public NotificationContentView getPublicLayout() { |
| return mPublicLayout; |
| } |
| |
| public void setIconAnimationRunning(boolean running) { |
| for (NotificationContentView l : mLayouts) { |
| setIconAnimationRunning(running, l); |
| } |
| if (mIsSummaryWithChildren) { |
| NotificationViewWrapper viewWrapper = mChildrenContainer.getNotificationViewWrapper(); |
| if (viewWrapper != null) { |
| setIconAnimationRunningForChild(running, viewWrapper.getIcon()); |
| } |
| NotificationViewWrapper lowPriWrapper = mChildrenContainer.getLowPriorityViewWrapper(); |
| if (lowPriWrapper != null) { |
| setIconAnimationRunningForChild(running, lowPriWrapper.getIcon()); |
| } |
| List<ExpandableNotificationRow> notificationChildren = |
| mChildrenContainer.getAttachedChildren(); |
| for (int i = 0; i < notificationChildren.size(); i++) { |
| ExpandableNotificationRow child = notificationChildren.get(i); |
| child.setIconAnimationRunning(running); |
| } |
| } |
| mIconAnimationRunning = running; |
| } |
| |
| private void setIconAnimationRunning(boolean running, NotificationContentView layout) { |
| if (layout != null) { |
| View contractedChild = layout.getContractedChild(); |
| View expandedChild = layout.getExpandedChild(); |
| View headsUpChild = layout.getHeadsUpChild(); |
| setIconAnimationRunningForChild(running, contractedChild); |
| setIconAnimationRunningForChild(running, expandedChild); |
| setIconAnimationRunningForChild(running, headsUpChild); |
| } |
| } |
| |
| private void setIconAnimationRunningForChild(boolean running, View child) { |
| if (child != null) { |
| ImageView icon = child.findViewById(com.android.internal.R.id.icon); |
| setIconRunning(icon, running); |
| ImageView rightIcon = child.findViewById(com.android.internal.R.id.right_icon); |
| setIconRunning(rightIcon, running); |
| } |
| } |
| |
| private void setIconRunning(ImageView imageView, boolean running) { |
| if (imageView != null) { |
| Drawable drawable = imageView.getDrawable(); |
| if (drawable instanceof AnimationDrawable) { |
| AnimationDrawable animationDrawable = (AnimationDrawable) drawable; |
| if (running) { |
| animationDrawable.start(); |
| } else { |
| animationDrawable.stop(); |
| } |
| } else if (drawable instanceof AnimatedVectorDrawable) { |
| AnimatedVectorDrawable animationDrawable = (AnimatedVectorDrawable) drawable; |
| if (running) { |
| animationDrawable.start(); |
| } else { |
| animationDrawable.stop(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Marks a content view as freeable, setting it so that future inflations do not reinflate |
| * and ensuring that the view is freed when it is safe to remove. |
| * |
| * @param inflationFlag flag corresponding to the content view to be freed |
| * @deprecated By default, {@link NotificationContentInflater#unbindContent} will tell the |
| * view hierarchy to only free when the view is safe to remove so this method is no longer |
| * needed. Will remove when all uses are gone. |
| */ |
| @Deprecated |
| public void freeContentViewWhenSafe(@InflationFlag int inflationFlag) { |
| RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); |
| params.markContentViewsFreeable(inflationFlag); |
| mRowContentBindStage.requestRebind(mEntry, null /* callback */); |
| } |
| |
| /** |
| * Returns whether this row is considered non-blockable (i.e. it's a non-blockable system notif |
| * or is in an allowList). |
| */ |
| public boolean getIsNonblockable() { |
| if (mEntry == null) { |
| return true; |
| } |
| return !mEntry.isBlockable(); |
| } |
| |
| private boolean isConversation() { |
| return mPeopleNotificationIdentifier.getPeopleNotificationType(mEntry) |
| != PeopleNotificationIdentifier.TYPE_NON_PERSON; |
| } |
| |
| public void onNotificationUpdated() { |
| for (NotificationContentView l : mLayouts) { |
| l.onNotificationUpdated(mEntry); |
| } |
| mShowingPublicInitialized = false; |
| updateNotificationColor(); |
| if (mMenuRow != null) { |
| mMenuRow.onNotificationUpdated(mEntry.getSbn()); |
| mMenuRow.setAppName(mAppName); |
| } |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.recreateNotificationHeader(mExpandClickListener, isConversation()); |
| mChildrenContainer.onNotificationUpdated(); |
| } |
| if (mIconAnimationRunning) { |
| setIconAnimationRunning(true); |
| } |
| if (mLastChronometerRunning) { |
| setChronometerRunning(true); |
| } |
| if (mNotificationParent != null) { |
| mNotificationParent.updateChildrenAppearance(); |
| } |
| onAttachedChildrenCountChanged(); |
| // The public layouts expand button is always visible |
| mPublicLayout.updateExpandButtons(true); |
| updateLimits(); |
| updateShelfIconColor(); |
| updateRippleAllowed(); |
| if (mUpdateSelfBackgroundOnUpdate) { |
| // Because this is triggered by UiMode change which we already propagated to children, |
| // we know that child rows will receive the same event, and will update their own |
| // backgrounds when they finish inflating, so propagating again would be redundant. |
| mUpdateSelfBackgroundOnUpdate = false; |
| updateBackgroundColorsOfSelf(); |
| } |
| } |
| |
| private void updateBackgroundColorsOfSelf() { |
| super.updateBackgroundColors(); |
| } |
| |
| @Override |
| public void updateBackgroundColors() { |
| // Because this call is made by the NSSL only on attached rows at the moment of the |
| // UiMode or Theme change, we have to propagate to our child views. |
| updateBackgroundColorsOfSelf(); |
| if (mIsSummaryWithChildren) { |
| for (ExpandableNotificationRow child : mChildrenContainer.getAttachedChildren()) { |
| child.updateBackgroundColors(); |
| } |
| } |
| } |
| |
| /** Called when the notification's ranking was changed (but nothing else changed). */ |
| public void onNotificationRankingUpdated() { |
| if (mMenuRow != null) { |
| mMenuRow.onNotificationUpdated(mEntry.getSbn()); |
| } |
| } |
| |
| /** Call when bubble state has changed and the button on the notification should be updated. */ |
| public void updateBubbleButton() { |
| for (NotificationContentView l : mLayouts) { |
| l.updateBubbleButton(mEntry); |
| } |
| } |
| |
| @VisibleForTesting |
| void updateShelfIconColor() { |
| StatusBarIconView expandedIcon = mEntry.getIcons().getShelfIcon(); |
| boolean isPreL = Boolean.TRUE.equals(expandedIcon.getTag(R.id.icon_is_pre_L)); |
| boolean colorize = !isPreL || NotificationUtils.isGrayscale(expandedIcon, |
| ContrastColorUtil.getInstance(mContext)); |
| int color = StatusBarIconView.NO_COLOR; |
| if (colorize) { |
| color = getOriginalIconColor(); |
| } |
| expandedIcon.setStaticDrawableColor(color); |
| } |
| |
| public int getOriginalIconColor() { |
| if (mIsSummaryWithChildren && !shouldShowPublic()) { |
| return mChildrenContainer.getVisibleWrapper().getOriginalIconColor(); |
| } |
| int color = getShowingLayout().getOriginalIconColor(); |
| if (color != Notification.COLOR_INVALID) { |
| return color; |
| } else { |
| return mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded(), |
| getBackgroundColorWithoutTint()); |
| } |
| } |
| |
| public void setAboveShelfChangedListener(AboveShelfChangedListener aboveShelfChangedListener) { |
| mAboveShelfChangedListener = aboveShelfChangedListener; |
| } |
| |
| /** |
| * Sets a supplier that can determine whether the keyguard is secure or not. |
| * @param secureStateProvider A function that returns true if keyguard is secure. |
| */ |
| public void setSecureStateProvider(BooleanSupplier secureStateProvider) { |
| mSecureStateProvider = secureStateProvider; |
| } |
| |
| private void updateLimits() { |
| for (NotificationContentView l : mLayouts) { |
| updateLimitsForView(l); |
| } |
| } |
| |
| private void updateLimitsForView(NotificationContentView layout) { |
| View contractedView = layout.getContractedChild(); |
| boolean customView = contractedView != null |
| && contractedView.getId() |
| != com.android.internal.R.id.status_bar_latest_event_content; |
| boolean beforeN = mEntry.targetSdk < Build.VERSION_CODES.N; |
| boolean beforeP = mEntry.targetSdk < Build.VERSION_CODES.P; |
| boolean beforeS = mEntry.targetSdk < Build.VERSION_CODES.S; |
| int smallHeight; |
| |
| boolean isCallLayout = contractedView instanceof CallLayout; |
| |
| if (customView && beforeS && !mIsSummaryWithChildren) { |
| if (beforeN) { |
| smallHeight = mMaxSmallHeightBeforeN; |
| } else if (beforeP) { |
| smallHeight = mMaxSmallHeightBeforeP; |
| } else { |
| smallHeight = mMaxSmallHeightBeforeS; |
| } |
| } else if (isCallLayout) { |
| smallHeight = mMaxExpandedHeight; |
| } else if (mUseIncreasedCollapsedHeight && layout == mPrivateLayout) { |
| smallHeight = mMaxSmallHeightLarge; |
| } else { |
| smallHeight = mMaxSmallHeight; |
| } |
| boolean headsUpCustom = layout.getHeadsUpChild() != null && |
| layout.getHeadsUpChild().getId() |
| != com.android.internal.R.id.status_bar_latest_event_content; |
| int headsUpHeight; |
| if (headsUpCustom && beforeS) { |
| if (beforeN) { |
| headsUpHeight = mMaxHeadsUpHeightBeforeN; |
| } else if (beforeP) { |
| headsUpHeight = mMaxHeadsUpHeightBeforeP; |
| } else { |
| headsUpHeight = mMaxHeadsUpHeightBeforeS; |
| } |
| } else if (mUseIncreasedHeadsUpHeight && layout == mPrivateLayout) { |
| headsUpHeight = mMaxHeadsUpHeightIncreased; |
| } else { |
| headsUpHeight = mMaxHeadsUpHeight; |
| } |
| NotificationViewWrapper headsUpWrapper = layout.getVisibleWrapper( |
| VISIBLE_TYPE_HEADSUP); |
| if (headsUpWrapper != null) { |
| headsUpHeight = Math.max(headsUpHeight, headsUpWrapper.getMinLayoutHeight()); |
| } |
| layout.setHeights(smallHeight, headsUpHeight, mMaxExpandedHeight); |
| } |
| |
| @NonNull |
| public NotificationEntry getEntry() { |
| return mEntry; |
| } |
| |
| @Override |
| public boolean isHeadsUp() { |
| return mIsHeadsUp; |
| } |
| |
| public void setHeadsUp(boolean isHeadsUp) { |
| boolean wasAboveShelf = isAboveShelf(); |
| int intrinsicBefore = getIntrinsicHeight(); |
| mIsHeadsUp = isHeadsUp; |
| mPrivateLayout.setHeadsUp(isHeadsUp); |
| if (mIsSummaryWithChildren) { |
| // The overflow might change since we allow more lines as HUN. |
| mChildrenContainer.updateGroupOverflow(); |
| } |
| if (intrinsicBefore != getIntrinsicHeight()) { |
| notifyHeightChanged(false /* needsAnimation */); |
| } |
| if (isHeadsUp) { |
| mMustStayOnScreen = true; |
| setAboveShelf(true); |
| } else if (isAboveShelf() != wasAboveShelf) { |
| mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); |
| } |
| } |
| |
| @Override |
| public boolean showingPulsing() { |
| return isHeadsUpState() && (isDozing() || (mOnKeyguard && isBypassEnabled())); |
| } |
| |
| /** |
| * @return if the view is in heads up state, i.e either still heads upped or it's disappearing. |
| */ |
| public boolean isHeadsUpState() { |
| return mIsHeadsUp || mHeadsupDisappearRunning; |
| } |
| |
| public void setRemoteInputController(RemoteInputController r) { |
| mPrivateLayout.setRemoteInputController(r); |
| } |
| |
| |
| String getAppName() { |
| return mAppName; |
| } |
| |
| public void addChildNotification(ExpandableNotificationRow row) { |
| addChildNotification(row, -1); |
| } |
| |
| /** |
| * Set the how much the header should be visible. A value of 0 will make the header fully gone |
| * and a value of 1 will make the notification look just like normal. |
| * This is being used for heads up notifications, when they are pinned to the top of the screen |
| * and the header content is extracted to the statusbar. |
| * |
| * @param headerVisibleAmount the amount the header should be visible. |
| */ |
| public void setHeaderVisibleAmount(float headerVisibleAmount) { |
| if (mHeaderVisibleAmount != headerVisibleAmount) { |
| mHeaderVisibleAmount = headerVisibleAmount; |
| for (NotificationContentView l : mLayouts) { |
| l.setHeaderVisibleAmount(headerVisibleAmount); |
| } |
| if (mChildrenContainer != null) { |
| mChildrenContainer.setHeaderVisibleAmount(headerVisibleAmount); |
| } |
| notifyHeightChanged(false /* needsAnimation */); |
| } |
| } |
| |
| @Override |
| public float getHeaderVisibleAmount() { |
| return mHeaderVisibleAmount; |
| } |
| |
| @Override |
| public void setHeadsUpIsVisible() { |
| super.setHeadsUpIsVisible(); |
| mMustStayOnScreen = false; |
| } |
| |
| /** |
| * @see NotificationChildrenContainer#setUntruncatedChildCount(int) |
| */ |
| public void setUntruncatedChildCount(int childCount) { |
| if (mChildrenContainer == null) { |
| mChildrenContainerStub.inflate(); |
| } |
| mChildrenContainer.setUntruncatedChildCount(childCount); |
| } |
| |
| /** Called after children have been attached to set the expansion states */ |
| public void resetChildSystemExpandedStates() { |
| if (isSummaryWithChildren()) { |
| mChildrenContainer.updateExpansionStates(); |
| } |
| } |
| |
| /** |
| * Add a child notification to this view. |
| * |
| * @param row the row to add |
| * @param childIndex the index to add it at, if -1 it will be added at the end |
| */ |
| public void addChildNotification(ExpandableNotificationRow row, int childIndex) { |
| if (mChildrenContainer == null) { |
| mChildrenContainerStub.inflate(); |
| } |
| mChildrenContainer.addNotification(row, childIndex); |
| onAttachedChildrenCountChanged(); |
| row.setIsChildInGroup(true, this); |
| } |
| |
| public void removeChildNotification(ExpandableNotificationRow row) { |
| if (mChildrenContainer != null) { |
| mChildrenContainer.removeNotification(row); |
| } |
| onAttachedChildrenCountChanged(); |
| row.setIsChildInGroup(false, null); |
| row.setBottomRoundness(0.0f, false /* animate */); |
| } |
| |
| /** Returns the child notification at [index], or null if no such child. */ |
| @Nullable |
| public ExpandableNotificationRow getChildNotificationAt(int index) { |
| if (mChildrenContainer == null |
| || mChildrenContainer.getAttachedChildren().size() <= index) { |
| return null; |
| } else { |
| return mChildrenContainer.getAttachedChildren().get(index); |
| } |
| } |
| |
| @Override |
| public boolean isChildInGroup() { |
| return mNotificationParent != null; |
| } |
| |
| public ExpandableNotificationRow getNotificationParent() { |
| return mNotificationParent; |
| } |
| |
| /** |
| * @param isChildInGroup Is this notification now in a group |
| * @param parent the new parent notification |
| */ |
| public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) { |
| if (mExpandAnimationRunning && !isChildInGroup && mNotificationParent != null) { |
| mNotificationParent.setChildIsExpanding(false); |
| mNotificationParent.setExpandingClipPath(null); |
| mNotificationParent.setExtraWidthForClipping(0.0f); |
| mNotificationParent.setMinimumHeightForClipping(0); |
| } |
| mNotificationParent = isChildInGroup ? parent : null; |
| mPrivateLayout.setIsChildInGroup(isChildInGroup); |
| |
| updateBackgroundForGroupState(); |
| updateClickAndFocus(); |
| if (mNotificationParent != null) { |
| setOverrideTintColor(NO_COLOR, 0.0f); |
| mNotificationParent.updateBackgroundForGroupState(); |
| } |
| updateBackgroundClipping(); |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| // Other parts of the system may intercept and handle all the falsing. |
| // Otherwise, if we see motion and follow-on events, try to classify them as a tap. |
| if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { |
| mFalsingManager.isFalseTap(FalsingManager.MODERATE_PENALTY); |
| } |
| return super.onInterceptTouchEvent(ev); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| if (event.getActionMasked() != MotionEvent.ACTION_DOWN |
| || !isChildInGroup() || isGroupExpanded()) { |
| return super.onTouchEvent(event); |
| } else { |
| return false; |
| } |
| } |
| |
| @Override |
| protected boolean handleSlideBack() { |
| if (mMenuRow != null && mMenuRow.isMenuVisible()) { |
| animateResetTranslation(); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean isSummaryWithChildren() { |
| return mIsSummaryWithChildren; |
| } |
| |
| @Override |
| public boolean areChildrenExpanded() { |
| return mChildrenExpanded; |
| } |
| |
| public List<ExpandableNotificationRow> getAttachedChildren() { |
| return mChildrenContainer == null ? null : mChildrenContainer.getAttachedChildren(); |
| } |
| |
| /** |
| * Apply the order given in the list to the children. |
| * |
| * @param childOrder the new list order |
| * @param visualStabilityManager |
| * @param callback the callback to invoked in case it is not allowed |
| * @return whether the list order has changed |
| */ |
| public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder, |
| VisualStabilityManager visualStabilityManager, |
| VisualStabilityManager.Callback callback) { |
| return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder, |
| visualStabilityManager, callback); |
| } |
| |
| /** Updates states of all children. */ |
| public void updateChildrenStates(AmbientState ambientState) { |
| if (mIsSummaryWithChildren) { |
| ExpandableViewState parentState = getViewState(); |
| mChildrenContainer.updateState(parentState, ambientState); |
| } |
| } |
| |
| /** Applies children states. */ |
| public void applyChildrenState() { |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.applyState(); |
| } |
| } |
| |
| /** Prepares expansion changed. */ |
| public void prepareExpansionChanged() { |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.prepareExpansionChanged(); |
| } |
| } |
| |
| /** Starts child animations. */ |
| public void startChildAnimation(AnimationProperties properties) { |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.startAnimationToState(properties); |
| } |
| } |
| |
| public ExpandableNotificationRow getViewAtPosition(float y) { |
| if (!mIsSummaryWithChildren || !mChildrenExpanded) { |
| return this; |
| } else { |
| ExpandableNotificationRow view = mChildrenContainer.getViewAtPosition(y); |
| return view == null ? this : view; |
| } |
| } |
| |
| public NotificationGuts getGuts() { |
| return mGuts; |
| } |
| |
| /** |
| * Set this notification to be pinned to the top if {@link #isHeadsUp()} is true. By doing this |
| * the notification will be rendered on top of the screen. |
| * |
| * @param pinned whether it is pinned |
| */ |
| public void setPinned(boolean pinned) { |
| int intrinsicHeight = getIntrinsicHeight(); |
| boolean wasAboveShelf = isAboveShelf(); |
| mIsPinned = pinned; |
| if (intrinsicHeight != getIntrinsicHeight()) { |
| notifyHeightChanged(false /* needsAnimation */); |
| } |
| if (pinned) { |
| setIconAnimationRunning(true); |
| mExpandedWhenPinned = false; |
| } else if (mExpandedWhenPinned) { |
| setUserExpanded(true); |
| } |
| setChronometerRunning(mLastChronometerRunning); |
| if (isAboveShelf() != wasAboveShelf) { |
| mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); |
| } |
| } |
| |
| @Override |
| public boolean isPinned() { |
| return mIsPinned; |
| } |
| |
| @Override |
| public int getPinnedHeadsUpHeight() { |
| return getPinnedHeadsUpHeight(true /* atLeastMinHeight */); |
| } |
| |
| /** |
| * @param atLeastMinHeight should the value returned be at least the minimum height. |
| * Used to avoid cyclic calls |
| * @return the height of the heads up notification when pinned |
| */ |
| private int getPinnedHeadsUpHeight(boolean atLeastMinHeight) { |
| if (mIsSummaryWithChildren) { |
| return mChildrenContainer.getIntrinsicHeight(); |
| } |
| if(mExpandedWhenPinned) { |
| return Math.max(getMaxExpandHeight(), getHeadsUpHeight()); |
| } else if (atLeastMinHeight) { |
| return Math.max(getCollapsedHeight(), getHeadsUpHeight()); |
| } else { |
| return getHeadsUpHeight(); |
| } |
| } |
| |
| /** |
| * Mark whether this notification was just clicked, i.e. the user has just clicked this |
| * notification in this frame. |
| */ |
| public void setJustClicked(boolean justClicked) { |
| mJustClicked = justClicked; |
| } |
| |
| /** |
| * @return true if this notification has been clicked in this frame, false otherwise |
| */ |
| public boolean wasJustClicked() { |
| return mJustClicked; |
| } |
| |
| public void setChronometerRunning(boolean running) { |
| mLastChronometerRunning = running; |
| setChronometerRunning(running, mPrivateLayout); |
| setChronometerRunning(running, mPublicLayout); |
| if (mChildrenContainer != null) { |
| List<ExpandableNotificationRow> notificationChildren = |
| mChildrenContainer.getAttachedChildren(); |
| for (int i = 0; i < notificationChildren.size(); i++) { |
| ExpandableNotificationRow child = notificationChildren.get(i); |
| child.setChronometerRunning(running); |
| } |
| } |
| } |
| |
| private void setChronometerRunning(boolean running, NotificationContentView layout) { |
| if (layout != null) { |
| running = running || isPinned(); |
| View contractedChild = layout.getContractedChild(); |
| View expandedChild = layout.getExpandedChild(); |
| View headsUpChild = layout.getHeadsUpChild(); |
| setChronometerRunningForChild(running, contractedChild); |
| setChronometerRunningForChild(running, expandedChild); |
| setChronometerRunningForChild(running, headsUpChild); |
| } |
| } |
| |
| private void setChronometerRunningForChild(boolean running, View child) { |
| if (child != null) { |
| View chronometer = child.findViewById(com.android.internal.R.id.chronometer); |
| if (chronometer instanceof Chronometer) { |
| ((Chronometer) chronometer).setStarted(running); |
| } |
| } |
| } |
| |
| /** |
| * @return the main notification view wrapper. |
| */ |
| public NotificationViewWrapper getNotificationViewWrapper() { |
| if (mIsSummaryWithChildren) { |
| return mChildrenContainer.getNotificationViewWrapper(); |
| } |
| return mPrivateLayout.getNotificationViewWrapper(); |
| } |
| |
| /** |
| * @return the currently visible notification view wrapper. This can be different from |
| * {@link #getNotificationViewWrapper()} in case it is a low-priority group. |
| */ |
| public NotificationViewWrapper getVisibleNotificationViewWrapper() { |
| if (mIsSummaryWithChildren && !shouldShowPublic()) { |
| return mChildrenContainer.getVisibleWrapper(); |
| } |
| return getShowingLayout().getVisibleWrapper(); |
| } |
| |
| public void setLongPressListener(LongPressListener longPressListener) { |
| mLongPressListener = longPressListener; |
| } |
| |
| public void setDragController(ExpandableNotificationRowDragController dragController) { |
| mDragController = dragController; |
| } |
| |
| @Override |
| public void setOnClickListener(@Nullable OnClickListener l) { |
| super.setOnClickListener(l); |
| mOnClickListener = l; |
| updateClickAndFocus(); |
| } |
| |
| /** The click listener for the bubble button. */ |
| public View.OnClickListener getBubbleClickListener() { |
| return v -> { |
| if (mBubblesManagerOptional.isPresent()) { |
| mBubblesManagerOptional.get() |
| .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); |
| } |
| mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */); |
| }; |
| } |
| |
| /** The click listener for the snooze button. */ |
| public View.OnClickListener getSnoozeClickListener(MenuItem item) { |
| return v -> { |
| // Dismiss a snoozed notification if one is still left behind |
| mNotificationGutsManager.closeAndSaveGuts(true /* removeLeavebehind */, |
| false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */, |
| false /* resetMenu */); |
| mNotificationGutsManager.openGuts(this, 0, 0, item); |
| mIsSnoozed = true; |
| }; |
| } |
| |
| private void updateClickAndFocus() { |
| boolean normalChild = !isChildInGroup() || isGroupExpanded(); |
| boolean clickable = mOnClickListener != null && normalChild; |
| if (isFocusable() != normalChild) { |
| setFocusable(normalChild); |
| } |
| if (isClickable() != clickable) { |
| setClickable(clickable); |
| } |
| } |
| |
| public HeadsUpManager getHeadsUpManager() { |
| return mHeadsUpManager; |
| } |
| |
| public void setGutsView(MenuItem item) { |
| if (getGuts() != null && item.getGutsView() instanceof NotificationGuts.GutsContent) { |
| getGuts().setGutsContent((NotificationGuts.GutsContent) item.getGutsView()); |
| } |
| } |
| |
| @Override |
| public void onPluginConnected(NotificationMenuRowPlugin plugin, Context pluginContext) { |
| boolean existed = mMenuRow != null && mMenuRow.getMenuView() != null; |
| if (existed) { |
| removeView(mMenuRow.getMenuView()); |
| } |
| if (plugin == null) { |
| return; |
| } |
| mMenuRow = plugin; |
| if (mMenuRow.shouldUseDefaultMenuItems()) { |
| ArrayList<MenuItem> items = new ArrayList<>(); |
| items.add(NotificationMenuRow.createConversationItem(mContext)); |
| items.add(NotificationMenuRow.createPartialConversationItem(mContext)); |
| items.add(NotificationMenuRow.createInfoItem(mContext)); |
| items.add(NotificationMenuRow.createSnoozeItem(mContext)); |
| mMenuRow.setMenuItems(items); |
| } |
| if (existed) { |
| createMenu(); |
| } |
| } |
| |
| @Override |
| public void onPluginDisconnected(NotificationMenuRowPlugin plugin) { |
| boolean existed = mMenuRow.getMenuView() != null; |
| mMenuRow = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); |
| if (existed) { |
| createMenu(); |
| } |
| } |
| |
| @Override |
| public boolean hasFinishedInitialization() { |
| return getEntry().hasFinishedInitialization(); |
| } |
| |
| /** |
| * Get a handle to a NotificationMenuRowPlugin whose menu view has been added to our hierarchy, |
| * or null if there is no menu row |
| * |
| * @return a {@link NotificationMenuRowPlugin}, or null |
| */ |
| @Nullable |
| public NotificationMenuRowPlugin createMenu() { |
| if (mMenuRow == null) { |
| return null; |
| } |
| if (mMenuRow.getMenuView() == null) { |
| mMenuRow.createMenu(this, mEntry.getSbn()); |
| mMenuRow.setAppName(mAppName); |
| FrameLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, |
| LayoutParams.MATCH_PARENT); |
| addView(mMenuRow.getMenuView(), MENU_VIEW_INDEX, lp); |
| } |
| return mMenuRow; |
| } |
| |
| @Nullable |
| public NotificationMenuRowPlugin getProvider() { |
| return mMenuRow; |
| } |
| |
| @Override |
| public void onDensityOrFontScaleChanged() { |
| super.onDensityOrFontScaleChanged(); |
| initDimens(); |
| initBackground(); |
| reInflateViews(); |
| } |
| |
| private void reInflateViews() { |
| Trace.beginSection("ExpandableNotificationRow#reInflateViews"); |
| // Let's update our childrencontainer. This is intentionally not guarded with |
| // mIsSummaryWithChildren since we might have had children but not anymore. |
| if (mChildrenContainer != null) { |
| mChildrenContainer.reInflateViews(mExpandClickListener, mEntry.getSbn()); |
| } |
| if (mGuts != null) { |
| NotificationGuts oldGuts = mGuts; |
| int index = indexOfChild(oldGuts); |
| removeView(oldGuts); |
| mGuts = (NotificationGuts) LayoutInflater.from(mContext).inflate( |
| R.layout.notification_guts, this, false); |
| mGuts.setVisibility(oldGuts.isExposed() ? VISIBLE : GONE); |
| addView(mGuts, index); |
| } |
| View oldMenu = mMenuRow == null ? null : mMenuRow.getMenuView(); |
| if (oldMenu != null) { |
| int menuIndex = indexOfChild(oldMenu); |
| removeView(oldMenu); |
| mMenuRow.createMenu(ExpandableNotificationRow.this, mEntry.getSbn()); |
| mMenuRow.setAppName(mAppName); |
| addView(mMenuRow.getMenuView(), menuIndex); |
| } |
| for (NotificationContentView l : mLayouts) { |
| l.reinflate(); |
| l.reInflateViews(); |
| } |
| mEntry.getSbn().clearPackageContext(); |
| // TODO: Move content inflation logic out of this call |
| RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); |
| params.setNeedsReinflation(true); |
| mRowContentBindStage.requestRebind(mEntry, null /* callback */); |
| Trace.endSection(); |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration newConfig) { |
| super.onConfigurationChanged(newConfig); |
| if (mMenuRow != null && mMenuRow.getMenuView() != null) { |
| mMenuRow.onConfigurationChanged(); |
| } |
| if (mImageResolver != null) { |
| mImageResolver.updateMaxImageSizes(); |
| } |
| } |
| |
| public void onUiModeChanged() { |
| mUpdateSelfBackgroundOnUpdate = true; |
| reInflateViews(); |
| if (mChildrenContainer != null) { |
| for (ExpandableNotificationRow child : mChildrenContainer.getAttachedChildren()) { |
| child.onUiModeChanged(); |
| } |
| } |
| } |
| |
| public void setContentBackground(int customBackgroundColor, boolean animate, |
| NotificationContentView notificationContentView) { |
| if (getShowingLayout() == notificationContentView) { |
| setTintColor(customBackgroundColor, animate); |
| } |
| } |
| |
| @Override |
| protected void setBackgroundTintColor(int color) { |
| super.setBackgroundTintColor(color); |
| NotificationContentView view = getShowingLayout(); |
| if (view != null) { |
| view.setBackgroundTintColor(color); |
| } |
| } |
| |
| public void closeRemoteInput() { |
| for (NotificationContentView l : mLayouts) { |
| l.closeRemoteInput(); |
| } |
| } |
| |
| /** |
| * Set by how much the single line view should be indented. |
| */ |
| public void setSingleLineWidthIndention(int indention) { |
| mPrivateLayout.setSingleLineWidthIndention(indention); |
| } |
| |
| public int getNotificationColor() { |
| return mNotificationColor; |
| } |
| |
| public void updateNotificationColor() { |
| Configuration currentConfig = getResources().getConfiguration(); |
| boolean nightMode = (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) |
| == Configuration.UI_MODE_NIGHT_YES; |
| |
| mNotificationColor = ContrastColorUtil.resolveContrastColor(mContext, |
| mEntry.getSbn().getNotification().color, |
| getBackgroundColorWithoutTint(), nightMode); |
| } |
| |
| public HybridNotificationView getSingleLineView() { |
| return mPrivateLayout.getSingleLineView(); |
| } |
| |
| public boolean isOnKeyguard() { |
| return mOnKeyguard; |
| } |
| |
| public void removeAllChildren() { |
| List<ExpandableNotificationRow> notificationChildren = |
| mChildrenContainer.getAttachedChildren(); |
| ArrayList<ExpandableNotificationRow> clonedList = new ArrayList<>(notificationChildren); |
| for (int i = 0; i < clonedList.size(); i++) { |
| ExpandableNotificationRow row = clonedList.get(i); |
| if (row.keepInParent()) { |
| continue; |
| } |
| mChildrenContainer.removeNotification(row); |
| row.setIsChildInGroup(false, null); |
| } |
| onAttachedChildrenCountChanged(); |
| } |
| |
| @Override |
| public void dismiss(boolean refocusOnDismiss) { |
| super.dismiss(refocusOnDismiss); |
| setLongPressListener(null); |
| setDragController(null); |
| mGroupParentWhenDismissed = mNotificationParent; |
| mChildAfterViewWhenDismissed = null; |
| mEntry.getIcons().getStatusBarIcon().setDismissed(); |
| if (isChildInGroup()) { |
| List<ExpandableNotificationRow> notificationChildren = |
| mNotificationParent.getAttachedChildren(); |
| int i = notificationChildren.indexOf(this); |
| if (i != -1 && i < notificationChildren.size() - 1) { |
| mChildAfterViewWhenDismissed = notificationChildren.get(i + 1); |
| } |
| } |
| } |
| |
| public boolean keepInParent() { |
| return mKeepInParent; |
| } |
| |
| public void setKeepInParent(boolean keepInParent) { |
| mKeepInParent = keepInParent; |
| } |
| |
| @Override |
| public boolean isRemoved() { |
| return mRemoved; |
| } |
| |
| public void setRemoved() { |
| mRemoved = true; |
| mTranslationWhenRemoved = getTranslationY(); |
| mWasChildInGroupWhenRemoved = isChildInGroup(); |
| if (isChildInGroup()) { |
| mTranslationWhenRemoved += getNotificationParent().getTranslationY(); |
| } |
| for (NotificationContentView l : mLayouts) { |
| l.setRemoved(); |
| } |
| } |
| |
| public boolean wasChildInGroupWhenRemoved() { |
| return mWasChildInGroupWhenRemoved; |
| } |
| |
| public float getTranslationWhenRemoved() { |
| return mTranslationWhenRemoved; |
| } |
| |
| public NotificationChildrenContainer getChildrenContainer() { |
| return mChildrenContainer; |
| } |
| |
| public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { |
| boolean wasAboveShelf = isAboveShelf(); |
| boolean changed = headsUpAnimatingAway != mHeadsupDisappearRunning; |
| mHeadsupDisappearRunning = headsUpAnimatingAway; |
| mPrivateLayout.setHeadsUpAnimatingAway(headsUpAnimatingAway); |
| if (changed && mHeadsUpAnimatingAwayListener != null) { |
| mHeadsUpAnimatingAwayListener.accept(headsUpAnimatingAway); |
| } |
| if (isAboveShelf() != wasAboveShelf) { |
| mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); |
| } |
| } |
| |
| public void setHeadsUpAnimatingAwayListener(Consumer<Boolean> listener) { |
| mHeadsUpAnimatingAwayListener = listener; |
| } |
| |
| /** |
| * @return if the view was just heads upped and is now animating away. During such a time the |
| * layout needs to be kept consistent |
| */ |
| @Override |
| public boolean isHeadsUpAnimatingAway() { |
| return mHeadsupDisappearRunning; |
| } |
| |
| public View getChildAfterViewWhenDismissed() { |
| return mChildAfterViewWhenDismissed; |
| } |
| |
| public View getGroupParentWhenDismissed() { |
| return mGroupParentWhenDismissed; |
| } |
| |
| /** |
| * Dismisses the notification. |
| * |
| * @param fromAccessibility whether this dismiss is coming from an accessibility action |
| */ |
| public void performDismiss(boolean fromAccessibility) { |
| mMetricsLogger.count(NotificationCounters.NOTIFICATION_DISMISSED, 1); |
| dismiss(fromAccessibility); |
| if (mEntry.isDismissable()) { |
| if (mOnUserInteractionCallback != null) { |
| mOnUserInteractionCallback.registerFutureDismissal(mEntry, REASON_CANCEL).run(); |
| } |
| } |
| } |
| |
| public void setBlockingHelperShowing(boolean isBlockingHelperShowing) { |
| mIsBlockingHelperShowing = isBlockingHelperShowing; |
| } |
| |
| public boolean isBlockingHelperShowing() { |
| return mIsBlockingHelperShowing; |
| } |
| |
| public boolean isBlockingHelperShowingAndTranslationFinished() { |
| return mIsBlockingHelperShowing && mNotificationTranslationFinished; |
| } |
| |
| @Override |
| public View getShelfTransformationTarget() { |
| if (mIsSummaryWithChildren && !shouldShowPublic()) { |
| return mChildrenContainer.getVisibleWrapper().getShelfTransformationTarget(); |
| } |
| return getShowingLayout().getShelfTransformationTarget(); |
| } |
| |
| /** |
| * @return whether the notification is currently showing a view with an icon. |
| */ |
| public boolean isShowingIcon() { |
| if (areGutsExposed()) { |
| return false; |
| } |
| return getShelfTransformationTarget() != null; |
| } |
| |
| @Override |
| protected void updateContentTransformation() { |
| if (mExpandAnimationRunning) { |
| return; |
| } |
| super.updateContentTransformation(); |
| } |
| |
| @Override |
| protected void applyContentTransformation(float contentAlpha, float translationY) { |
| super.applyContentTransformation(contentAlpha, translationY); |
| if (!mIsLastChild) { |
| // Don't fade views unless we're last |
| contentAlpha = 1.0f; |
| } |
| for (NotificationContentView l : mLayouts) { |
| l.setAlpha(contentAlpha); |
| l.setTranslationY(translationY); |
| } |
| if (mChildrenContainer != null) { |
| mChildrenContainer.setAlpha(contentAlpha); |
| mChildrenContainer.setTranslationY(translationY); |
| // TODO: handle children fade out better |
| } |
| } |
| |
| public void setIsLowPriority(boolean isLowPriority) { |
| mIsLowPriority = isLowPriority; |
| mPrivateLayout.setIsLowPriority(isLowPriority); |
| if (mChildrenContainer != null) { |
| mChildrenContainer.setIsLowPriority(isLowPriority); |
| } |
| } |
| |
| public boolean isLowPriority() { |
| return mIsLowPriority; |
| } |
| |
| public void setUsesIncreasedCollapsedHeight(boolean use) { |
| mUseIncreasedCollapsedHeight = use; |
| } |
| |
| public void setUsesIncreasedHeadsUpHeight(boolean use) { |
| mUseIncreasedHeadsUpHeight = use; |
| } |
| |
| /** @deprecated TODO: Remove this when the old pipeline code is removed. */ |
| @Deprecated |
| public void setNeedsRedaction(boolean needsRedaction) { |
| if (mNeedsRedaction != needsRedaction) { |
| mNeedsRedaction = needsRedaction; |
| if (!isRemoved()) { |
| RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); |
| if (needsRedaction) { |
| params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC); |
| } else { |
| params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC); |
| } |
| mRowContentBindStage.requestRebind(mEntry, null /* callback */); |
| } |
| } |
| } |
| |
| public interface ExpansionLogger { |
| void logNotificationExpansion(String key, boolean userAction, boolean expanded); |
| } |
| |
| public ExpandableNotificationRow(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| mImageResolver = new NotificationInlineImageResolver(context, |
| new NotificationInlineImageCache()); |
| initDimens(); |
| } |
| |
| /** |
| * Initialize row. |
| */ |
| public void initialize( |
| NotificationEntry entry, |
| RemoteInputViewSubcomponent.Factory rivSubcomponentFactory, |
| String appName, |
| String notificationKey, |
| ExpansionLogger logger, |
| KeyguardBypassController bypassController, |
| GroupMembershipManager groupMembershipManager, |
| GroupExpansionManager groupExpansionManager, |
| HeadsUpManager headsUpManager, |
| RowContentBindStage rowContentBindStage, |
| OnExpandClickListener onExpandClickListener, |
| NotificationMediaManager notificationMediaManager, |
| CoordinateOnClickListener onFeedbackClickListener, |
| FalsingManager falsingManager, |
| FalsingCollector falsingCollector, |
| StatusBarStateController statusBarStateController, |
| PeopleNotificationIdentifier peopleNotificationIdentifier, |
| OnUserInteractionCallback onUserInteractionCallback, |
| Optional<BubblesManager> bubblesManagerOptional, |
| NotificationGutsManager gutsManager, |
| MetricsLogger metricsLogger, |
| SmartReplyConstants smartReplyConstants, |
| SmartReplyController smartReplyController) { |
| mEntry = entry; |
| mAppName = appName; |
| if (mMenuRow == null) { |
| mMenuRow = new NotificationMenuRow(mContext, peopleNotificationIdentifier); |
| } |
| if (mMenuRow.getMenuView() != null) { |
| mMenuRow.setAppName(mAppName); |
| } |
| mLogger = logger; |
| mLoggingKey = notificationKey; |
| mBypassController = bypassController; |
| mGroupMembershipManager = groupMembershipManager; |
| mGroupExpansionManager = groupExpansionManager; |
| mPrivateLayout.setGroupMembershipManager(groupMembershipManager); |
| mHeadsUpManager = headsUpManager; |
| mRowContentBindStage = rowContentBindStage; |
| mOnExpandClickListener = onExpandClickListener; |
| mMediaManager = notificationMediaManager; |
| setOnFeedbackClickListener(onFeedbackClickListener); |
| mFalsingManager = falsingManager; |
| mFalsingCollector = falsingCollector; |
| mStatusBarStateController = statusBarStateController; |
| mPeopleNotificationIdentifier = peopleNotificationIdentifier; |
| for (NotificationContentView l : mLayouts) { |
| l.initialize( |
| mPeopleNotificationIdentifier, |
| rivSubcomponentFactory, |
| smartReplyConstants, |
| smartReplyController); |
| } |
| mOnUserInteractionCallback = onUserInteractionCallback; |
| mBubblesManagerOptional = bubblesManagerOptional; |
| mNotificationGutsManager = gutsManager; |
| mMetricsLogger = metricsLogger; |
| } |
| |
| private void initDimens() { |
| mMaxSmallHeightBeforeN = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_min_height_legacy); |
| mMaxSmallHeightBeforeP = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_min_height_before_p); |
| mMaxSmallHeightBeforeS = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_min_height_before_s); |
| mMaxSmallHeight = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_min_height); |
| mMaxSmallHeightLarge = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_min_height_increased); |
| mMaxSmallHeightMedia = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_min_height_media); |
| mMaxExpandedHeight = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_max_height); |
| mMaxHeadsUpHeightBeforeN = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_max_heads_up_height_legacy); |
| mMaxHeadsUpHeightBeforeP = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_max_heads_up_height_before_p); |
| mMaxHeadsUpHeightBeforeS = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_max_heads_up_height_before_s); |
| mMaxHeadsUpHeight = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_max_heads_up_height); |
| mMaxHeadsUpHeightIncreased = NotificationUtils.getFontScaledHeight(mContext, |
| R.dimen.notification_max_heads_up_height_increased); |
| |
| Resources res = getResources(); |
| mEnableNonGroupedNotificationExpand = |
| res.getBoolean(R.bool.config_enableNonGroupedNotificationExpand); |
| mShowGroupBackgroundWhenExpanded = |
| res.getBoolean(R.bool.config_showGroupNotificationBgWhenExpanded); |
| } |
| |
| NotificationInlineImageResolver getImageResolver() { |
| return mImageResolver; |
| } |
| |
| /** |
| * Resets this view so it can be re-used for an updated notification. |
| */ |
| public void reset() { |
| mShowingPublicInitialized = false; |
| unDismiss(); |
| if (mMenuRow == null || !mMenuRow.isMenuVisible()) { |
| resetTranslation(); |
| } |
| onHeightReset(); |
| requestLayout(); |
| |
| setTargetPoint(null); |
| } |
| |
| /** Shows the given feedback icon, or hides the icon if null. */ |
| public void setFeedbackIcon(@Nullable FeedbackIcon icon) { |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.setFeedbackIcon(icon); |
| } |
| mPrivateLayout.setFeedbackIcon(icon); |
| mPublicLayout.setFeedbackIcon(icon); |
| } |
| |
| /** Sets the last time the notification being displayed audibly alerted the user. */ |
| public void setLastAudiblyAlertedMs(long lastAudiblyAlertedMs) { |
| long timeSinceAlertedAudibly = System.currentTimeMillis() - lastAudiblyAlertedMs; |
| boolean alertedRecently = timeSinceAlertedAudibly < RECENTLY_ALERTED_THRESHOLD_MS; |
| |
| applyAudiblyAlertedRecently(alertedRecently); |
| |
| removeCallbacks(mExpireRecentlyAlertedFlag); |
| if (alertedRecently) { |
| long timeUntilNoLongerRecent = RECENTLY_ALERTED_THRESHOLD_MS - timeSinceAlertedAudibly; |
| postDelayed(mExpireRecentlyAlertedFlag, timeUntilNoLongerRecent); |
| } |
| } |
| |
| @VisibleForTesting |
| protected void setEntry(NotificationEntry entry) { |
| mEntry = entry; |
| } |
| |
| private final Runnable mExpireRecentlyAlertedFlag = () -> applyAudiblyAlertedRecently(false); |
| |
| private void applyAudiblyAlertedRecently(boolean audiblyAlertedRecently) { |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.setRecentlyAudiblyAlerted(audiblyAlertedRecently); |
| } |
| mPrivateLayout.setRecentlyAudiblyAlerted(audiblyAlertedRecently); |
| mPublicLayout.setRecentlyAudiblyAlerted(audiblyAlertedRecently); |
| } |
| |
| public View.OnClickListener getFeedbackOnClickListener() { |
| return mOnFeedbackClickListener; |
| } |
| |
| void setOnFeedbackClickListener(CoordinateOnClickListener l) { |
| mOnFeedbackClickListener = v -> { |
| createMenu(); |
| NotificationMenuRowPlugin provider = getProvider(); |
| if (provider == null) { |
| return; |
| } |
| MenuItem menuItem = provider.getFeedbackMenuItem(mContext); |
| if (menuItem != null) { |
| l.onClick(this, v.getWidth() / 2, v.getHeight() / 2, menuItem); |
| } |
| }; |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| Trace.beginSection(appendTraceStyleTag("ExpNotRow#onMeasure")); |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| Trace.endSection(); |
| } |
| |
| /** Generates and appends "(MessagingStyle)" type tag to passed string for tracing. */ |
| @NonNull |
| private String appendTraceStyleTag(@NonNull String traceTag) { |
| if (!Trace.isEnabled()) { |
| return traceTag; |
| } |
| |
| Class<? extends Notification.Style> style = |
| getEntry().getSbn().getNotification().getNotificationStyle(); |
| if (style == null) { |
| return traceTag + "(nostyle)"; |
| } else { |
| return traceTag + "(" + style.getSimpleName() + ")"; |
| } |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mPublicLayout = findViewById(R.id.expandedPublic); |
| mPrivateLayout = findViewById(R.id.expanded); |
| mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout}; |
| |
| for (NotificationContentView l : mLayouts) { |
| l.setExpandClickListener(mExpandClickListener); |
| l.setContainingNotification(this); |
| } |
| mGutsStub = findViewById(R.id.notification_guts_stub); |
| mGutsStub.setOnInflateListener((stub, inflated) -> { |
| mGuts = (NotificationGuts) inflated; |
| mGuts.setClipTopAmount(getClipTopAmount()); |
| mGuts.setActualHeight(getActualHeight()); |
| mGutsStub = null; |
| }); |
| mChildrenContainerStub = findViewById(R.id.child_container_stub); |
| mChildrenContainerStub.setOnInflateListener((stub, inflated) -> { |
| mChildrenContainer = (NotificationChildrenContainer) inflated; |
| mChildrenContainer.setIsLowPriority(mIsLowPriority); |
| mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this); |
| mChildrenContainer.onNotificationUpdated(); |
| |
| mTranslateableViews.add(mChildrenContainer); |
| }); |
| |
| // Add the views that we translate to reveal the menu |
| mTranslateableViews = new ArrayList<>(); |
| for (int i = 0; i < getChildCount(); i++) { |
| mTranslateableViews.add(getChildAt(i)); |
| } |
| // Remove views that don't translate |
| mTranslateableViews.remove(mChildrenContainerStub); |
| mTranslateableViews.remove(mGutsStub); |
| } |
| |
| /** |
| * Called once when starting drag motion after opening notification guts, |
| * in case of notification that has {@link android.app.Notification#contentIntent} |
| * and it is to start an activity. |
| */ |
| public void doDragCallback(float x, float y) { |
| if (mDragController != null) { |
| setTargetPoint(new Point((int) x, (int) y)); |
| mDragController.startDragAndDrop(this); |
| } |
| } |
| |
| public void setOnDragSuccessListener(OnDragSuccessListener listener) { |
| mOnDragSuccessListener = listener; |
| } |
| |
| /** |
| * Called when a notification is dropped on proper target window. |
| */ |
| public void dragAndDropSuccess() { |
| if (mOnDragSuccessListener != null) { |
| mOnDragSuccessListener.onDragSuccess(getEntry()); |
| } |
| } |
| |
| private void doLongClickCallback() { |
| doLongClickCallback(getWidth() / 2, getHeight() / 2); |
| } |
| |
| public void doLongClickCallback(int x, int y) { |
| createMenu(); |
| NotificationMenuRowPlugin provider = getProvider(); |
| MenuItem menuItem = null; |
| if (provider != null) { |
| menuItem = provider.getLongpressMenuItem(mContext); |
| } |
| doLongClickCallback(x, y, menuItem); |
| } |
| |
| /** |
| * Perform a smart action which triggers a longpress (expose guts). |
| * Based on the semanticAction passed, may update the state of the guts view. |
| * @param semanticAction associated with this smart action click |
| */ |
| public void doSmartActionClick(int x, int y, int semanticAction) { |
| createMenu(); |
| NotificationMenuRowPlugin provider = getProvider(); |
| MenuItem menuItem = null; |
| if (provider != null) { |
| menuItem = provider.getLongpressMenuItem(mContext); |
| } |
| if (SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY == semanticAction |
| && menuItem.getGutsView() instanceof NotificationConversationInfo) { |
| NotificationConversationInfo info = |
| (NotificationConversationInfo) menuItem.getGutsView(); |
| info.setSelectedAction(NotificationConversationInfo.ACTION_FAVORITE); |
| } |
| doLongClickCallback(x, y, menuItem); |
| } |
| |
| private void doLongClickCallback(int x, int y, MenuItem menuItem) { |
| if (mLongPressListener != null && menuItem != null) { |
| mLongPressListener.onLongPress(this, x, y, menuItem); |
| } |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| if (KeyEvent.isConfirmKey(keyCode)) { |
| event.startTracking(); |
| return true; |
| } |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| if (KeyEvent.isConfirmKey(keyCode)) { |
| if (!event.isCanceled()) { |
| performClick(); |
| } |
| return true; |
| } |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| @Override |
| public boolean onKeyLongPress(int keyCode, KeyEvent event) { |
| if (KeyEvent.isConfirmKey(keyCode)) { |
| doLongClickCallback(); |
| return true; |
| } |
| return false; |
| } |
| |
| public void resetTranslation() { |
| if (mTranslateAnim != null) { |
| mTranslateAnim.cancel(); |
| } |
| |
| if (mDismissUsingRowTranslationX) { |
| setTranslationX(0); |
| } else if (mTranslateableViews != null) { |
| for (int i = 0; i < mTranslateableViews.size(); i++) { |
| mTranslateableViews.get(i).setTranslationX(0); |
| } |
| invalidateOutline(); |
| getEntry().getIcons().getShelfIcon().setScrollX(0); |
| } |
| |
| if (mMenuRow != null) { |
| mMenuRow.resetMenu(); |
| } |
| } |
| |
| void onGutsOpened() { |
| resetTranslation(); |
| updateContentAccessibilityImportanceForGuts(false /* isEnabled */); |
| } |
| |
| void onGutsClosed() { |
| updateContentAccessibilityImportanceForGuts(true /* isEnabled */); |
| mIsSnoozed = false; |
| } |
| |
| /** |
| * Updates whether all the non-guts content inside this row is important for accessibility. |
| * |
| * @param isEnabled whether the content views should be enabled for accessibility |
| */ |
| private void updateContentAccessibilityImportanceForGuts(boolean isEnabled) { |
| updateAccessibilityImportance(isEnabled); |
| |
| if (mChildrenContainer != null) { |
| updateChildAccessibilityImportance(mChildrenContainer, isEnabled); |
| } |
| if (mLayouts != null) { |
| for (View view : mLayouts) { |
| updateChildAccessibilityImportance(view, isEnabled); |
| } |
| } |
| |
| if (isEnabled) { |
| this.requestAccessibilityFocus(); |
| } |
| } |
| |
| /** |
| * Updates whether this view is important for accessibility based on {@code isEnabled}. |
| */ |
| private void updateAccessibilityImportance(boolean isEnabled) { |
| setImportantForAccessibility(isEnabled |
| ? View.IMPORTANT_FOR_ACCESSIBILITY_AUTO |
| : View.IMPORTANT_FOR_ACCESSIBILITY_NO); |
| } |
| |
| /** |
| * Updates whether the given childView is important for accessibility based on |
| * {@code isEnabled}. |
| */ |
| private void updateChildAccessibilityImportance(View childView, boolean isEnabled) { |
| childView.setImportantForAccessibility(isEnabled |
| ? View.IMPORTANT_FOR_ACCESSIBILITY_AUTO |
| : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); |
| } |
| |
| public CharSequence getActiveRemoteInputText() { |
| return mPrivateLayout.getActiveRemoteInputText(); |
| } |
| |
| /** |
| * Reset the translation with an animation. |
| */ |
| public void animateResetTranslation() { |
| if (mTranslateAnim != null) { |
| mTranslateAnim.cancel(); |
| } |
| mTranslateAnim = getTranslateViewAnimator(0, null /* updateListener */); |
| if (mTranslateAnim != null) { |
| mTranslateAnim.start(); |
| } |
| } |
| |
| /** |
| * Set the dismiss behavior of the view. |
| * @param usingRowTranslationX {@code true} if the view should translate using regular |
| * translationX, otherwise the contents will be |
| * translated. |
| */ |
| @Override |
| public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) { |
| if (usingRowTranslationX != mDismissUsingRowTranslationX) { |
| // In case we were already transitioning, let's switch over! |
| float previousTranslation = getTranslation(); |
| if (previousTranslation != 0) { |
| setTranslation(0); |
| } |
| super.setDismissUsingRowTranslationX(usingRowTranslationX); |
| if (previousTranslation != 0) { |
| setTranslation(previousTranslation); |
| } |
| } |
| } |
| |
| @Override |
| public void setTranslation(float translationX) { |
| invalidate(); |
| if (isBlockingHelperShowingAndTranslationFinished()) { |
| mGuts.setTranslationX(translationX); |
| return; |
| } else if (mDismissUsingRowTranslationX) { |
| setTranslationX(translationX); |
| } else if (mTranslateableViews != null) { |
| // Translate the group of views |
| for (int i = 0; i < mTranslateableViews.size(); i++) { |
| if (mTranslateableViews.get(i) != null) { |
| mTranslateableViews.get(i).setTranslationX(translationX); |
| } |
| } |
| invalidateOutline(); |
| |
| // In order to keep the shelf in sync with this swiping, we're simply translating |
| // it's icon by the same amount. The translation is already being used for the normal |
| // positioning, so we can use the scrollX instead. |
| getEntry().getIcons().getShelfIcon().setScrollX((int) -translationX); |
| } |
| |
| if (mMenuRow != null && mMenuRow.getMenuView() != null) { |
| mMenuRow.onParentTranslationUpdate(translationX); |
| } |
| } |
| |
| @Override |
| public float getTranslation() { |
| if (mDismissUsingRowTranslationX) { |
| return getTranslationX(); |
| } |
| |
| if (isBlockingHelperShowingAndCanTranslate()) { |
| return mGuts.getTranslationX(); |
| } |
| |
| if (mTranslateableViews != null && mTranslateableViews.size() > 0) { |
| // All of the views in the list should have same translation, just use first one. |
| return mTranslateableViews.get(0).getTranslationX(); |
| } |
| |
| return 0; |
| } |
| |
| private boolean isBlockingHelperShowingAndCanTranslate() { |
| return areGutsExposed() && mIsBlockingHelperShowing && mNotificationTranslationFinished; |
| } |
| |
| public Animator getTranslateViewAnimator(final float leftTarget, |
| AnimatorUpdateListener listener) { |
| if (mTranslateAnim != null) { |
| mTranslateAnim.cancel(); |
| } |
| |
| final ObjectAnimator translateAnim = ObjectAnimator.ofFloat(this, TRANSLATE_CONTENT, |
| leftTarget); |
| if (listener != null) { |
| translateAnim.addUpdateListener(listener); |
| } |
| translateAnim.addListener(new AnimatorListenerAdapter() { |
| boolean cancelled = false; |
| |
| @Override |
| public void onAnimationCancel(Animator anim) { |
| cancelled = true; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator anim) { |
| if (mIsBlockingHelperShowing) { |
| mNotificationTranslationFinished = true; |
| } |
| if (!cancelled && leftTarget == 0) { |
| if (mMenuRow != null) { |
| mMenuRow.resetMenu(); |
| } |
| mTranslateAnim = null; |
| } |
| } |
| }); |
| mTranslateAnim = translateAnim; |
| return translateAnim; |
| } |
| |
| void ensureGutsInflated() { |
| if (mGuts == null) { |
| mGutsStub.inflate(); |
| } |
| } |
| |
| private void updateChildrenVisibility() { |
| boolean hideContentWhileLaunching = mExpandAnimationRunning && mGuts != null |
| && mGuts.isExposed(); |
| mPrivateLayout.setVisibility(!mShowingPublic && !mIsSummaryWithChildren |
| && !hideContentWhileLaunching ? VISIBLE : INVISIBLE); |
| if (mChildrenContainer != null) { |
| mChildrenContainer.setVisibility(!mShowingPublic && mIsSummaryWithChildren |
| && !hideContentWhileLaunching ? VISIBLE |
| : INVISIBLE); |
| } |
| // The limits might have changed if the view suddenly became a group or vice versa |
| updateLimits(); |
| } |
| |
| @Override |
| public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) { |
| if (super.onRequestSendAccessibilityEventInternal(child, event)) { |
| // Add a record for the entire layout since its content is somehow small. |
| // The event comes from a leaf view that is interacted with. |
| AccessibilityEvent record = AccessibilityEvent.obtain(); |
| onInitializeAccessibilityEvent(record); |
| dispatchPopulateAccessibilityEvent(record); |
| event.appendRecord(record); |
| return true; |
| } |
| return false; |
| } |
| |
| |
| public void applyLaunchAnimationParams(LaunchAnimationParameters params) { |
| if (params == null) { |
| // `null` params indicates the animation is over, which means we can't access |
| // params.getParentStartClipTopAmount() which has the value we want to restore. |
| // Fortunately, only NotificationShelf actually uses these values for anything other |
| // than this launch animation, so we can restore the value to 0 and it's right for now. |
| if (mNotificationParent != null) { |
| mNotificationParent.setClipTopAmount(0); |
| } |
| return; |
| } |
| |
| if (!params.getVisible()) { |
| if (getVisibility() == View.VISIBLE) { |
| setVisibility(View.INVISIBLE); |
| } |
| return; |
| } |
| |
| float zProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation( |
| params.getProgress(0, 50)); |
| float translationZ = MathUtils.lerp(params.getStartTranslationZ(), |
| mNotificationLaunchHeight, |
| zProgress); |
| setTranslationZ(translationZ); |
| float extraWidthForClipping = params.getWidth() - getWidth(); |
| setExtraWidthForClipping(extraWidthForClipping); |
| int top; |
| if (params.getStartRoundedTopClipping() > 0) { |
| // If we were clipping initially, let's interpolate from the start position to the |
| // top. Otherwise, we just take the top directly. |
| float expandProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation( |
| params.getProgress(0, |
| NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING)); |
| float startTop = params.getStartNotificationTop(); |
| top = (int) Math.min(MathUtils.lerp(startTop, |
| params.getTop(), expandProgress), |
| startTop); |
| } else { |
| top = params.getTop(); |
| } |
| int actualHeight = params.getBottom() - top; |
| setActualHeight(actualHeight); |
| int startClipTopAmount = params.getStartClipTopAmount(); |
| int clipTopAmount = (int) MathUtils.lerp(startClipTopAmount, 0, params.getProgress()); |
| if (mNotificationParent != null) { |
| float parentY = mNotificationParent.getTranslationY(); |
| top -= parentY; |
| mNotificationParent.setTranslationZ(translationZ); |
| |
| // When the expanding notification is below its parent, the parent must be clipped |
| // exactly how it was clipped before the animation. When the expanding notification is |
| // on or above its parent (top <= 0), then the parent must be clipped exactly 'top' |
| // pixels to show the expanding notification, while still taking the decreasing |
| // notification clipTopAmount into consideration, so 'top + clipTopAmount'. |
| int parentStartClipTopAmount = params.getParentStartClipTopAmount(); |
| int parentClipTopAmount = Math.min(parentStartClipTopAmount, |
| top + clipTopAmount); |
| mNotificationParent.setClipTopAmount(parentClipTopAmount); |
| |
| mNotificationParent.setExtraWidthForClipping(extraWidthForClipping); |
| float clipBottom = Math.max(params.getBottom(), |
| parentY + mNotificationParent.getActualHeight() |
| - mNotificationParent.getClipBottomAmount()); |
| float clipTop = Math.min(params.getTop(), parentY); |
| int minimumHeightForClipping = (int) (clipBottom - clipTop); |
| mNotificationParent.setMinimumHeightForClipping(minimumHeightForClipping); |
| } else if (startClipTopAmount != 0) { |
| setClipTopAmount(clipTopAmount); |
| } |
| setTranslationY(top); |
| |
| mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / mOutlineRadius; |
| mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / mOutlineRadius; |
| invalidateOutline(); |
| |
| mBackgroundNormal.setExpandAnimationSize(params.getWidth(), actualHeight); |
| } |
| |
| @Override |
| public float getCurrentTopRoundness() { |
| if (mExpandAnimationRunning) { |
| return mTopRoundnessDuringLaunchAnimation; |
| } |
| |
| return super.getCurrentTopRoundness(); |
| } |
| |
| @Override |
| public float getCurrentBottomRoundness() { |
| if (mExpandAnimationRunning) { |
| return mBottomRoundnessDuringLaunchAnimation; |
| } |
| |
| return super.getCurrentBottomRoundness(); |
| } |
| |
| public void setExpandAnimationRunning(boolean expandAnimationRunning) { |
| if (expandAnimationRunning) { |
| setAboveShelf(true); |
| mExpandAnimationRunning = true; |
| getViewState().cancelAnimations(this); |
| mNotificationLaunchHeight = AmbientState.getNotificationLaunchHeight(getContext()); |
| } else { |
| mExpandAnimationRunning = false; |
| setAboveShelf(isAboveShelf()); |
| setVisibility(View.VISIBLE); |
| if (mGuts != null) { |
| mGuts.setAlpha(1.0f); |
| } |
| resetAllContentAlphas(); |
| setExtraWidthForClipping(0.0f); |
| if (mNotificationParent != null) { |
| mNotificationParent.setExtraWidthForClipping(0.0f); |
| mNotificationParent.setMinimumHeightForClipping(0); |
| } |
| } |
| if (mNotificationParent != null) { |
| mNotificationParent.setChildIsExpanding(mExpandAnimationRunning); |
| } |
| updateChildrenVisibility(); |
| updateClipping(); |
| mBackgroundNormal.setExpandAnimationRunning(expandAnimationRunning); |
| } |
| |
| private void setChildIsExpanding(boolean isExpanding) { |
| mChildIsExpanding = isExpanding; |
| updateClipping(); |
| invalidate(); |
| } |
| |
| @Override |
| public boolean hasExpandingChild() { |
| return mChildIsExpanding; |
| } |
| |
| @Override |
| public @NonNull StatusBarIconView getShelfIcon() { |
| return getEntry().getIcons().getShelfIcon(); |
| } |
| |
| @Override |
| protected boolean shouldClipToActualHeight() { |
| return super.shouldClipToActualHeight() && !mExpandAnimationRunning; |
| } |
| |
| @Override |
| public boolean isExpandAnimationRunning() { |
| return mExpandAnimationRunning; |
| } |
| |
| /** |
| * Tap sounds should not be played when we're unlocking. |
| * Doing so would cause audio collision and the system would feel unpolished. |
| */ |
| @Override |
| public boolean isSoundEffectsEnabled() { |
| final boolean mute = mStatusBarStateController != null |
| && mStatusBarStateController.isDozing() |
| && mSecureStateProvider != null && |
| !mSecureStateProvider.getAsBoolean(); |
| return !mute && super.isSoundEffectsEnabled(); |
| } |
| |
| public boolean isExpandable() { |
| if (mIsSummaryWithChildren && !shouldShowPublic()) { |
| return !mChildrenExpanded; |
| } |
| return mEnableNonGroupedNotificationExpand && mExpandable; |
| } |
| |
| public void setExpandable(boolean expandable) { |
| mExpandable = expandable; |
| mPrivateLayout.updateExpandButtons(isExpandable()); |
| } |
| |
| @Override |
| public void setClipToActualHeight(boolean clipToActualHeight) { |
| super.setClipToActualHeight(clipToActualHeight || isUserLocked()); |
| getShowingLayout().setClipToActualHeight(clipToActualHeight || isUserLocked()); |
| } |
| |
| /** |
| * @return whether the user has changed the expansion state |
| */ |
| public boolean hasUserChangedExpansion() { |
| return mHasUserChangedExpansion; |
| } |
| |
| public boolean isUserExpanded() { |
| return mUserExpanded; |
| } |
| |
| /** |
| * Set this notification to be expanded by the user |
| * |
| * @param userExpanded whether the user wants this notification to be expanded |
| */ |
| public void setUserExpanded(boolean userExpanded) { |
| setUserExpanded(userExpanded, false /* allowChildExpansion */); |
| } |
| |
| /** |
| * Set this notification to be expanded by the user |
| * |
| * @param userExpanded whether the user wants this notification to be expanded |
| * @param allowChildExpansion whether a call to this method allows expanding children |
| */ |
| public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { |
| mFalsingCollector.setNotificationExpanded(); |
| if (mIsSummaryWithChildren && !shouldShowPublic() && allowChildExpansion |
| && !mChildrenContainer.showingAsLowPriority()) { |
| final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); |
| mGroupExpansionManager.setGroupExpanded(mEntry, userExpanded); |
| onExpansionChanged(true /* userAction */, wasExpanded); |
| return; |
| } |
| if (userExpanded && !mExpandable) return; |
| final boolean wasExpanded = isExpanded(); |
| mHasUserChangedExpansion = true; |
| mUserExpanded = userExpanded; |
| onExpansionChanged(true /* userAction */, wasExpanded); |
| if (!wasExpanded && isExpanded() |
| && getActualHeight() != getIntrinsicHeight()) { |
| notifyHeightChanged(true /* needsAnimation */); |
| } |
| } |
| |
| public void resetUserExpansion() { |
| boolean wasExpanded = isExpanded(); |
| mHasUserChangedExpansion = false; |
| mUserExpanded = false; |
| if (wasExpanded != isExpanded()) { |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.onExpansionChanged(); |
| } |
| notifyHeightChanged(false /* needsAnimation */); |
| } |
| updateShelfIconColor(); |
| } |
| |
| public boolean isUserLocked() { |
| return mUserLocked; |
| } |
| |
| public void setUserLocked(boolean userLocked) { |
| mUserLocked = userLocked; |
| mPrivateLayout.setUserExpanding(userLocked); |
| // This is intentionally not guarded with mIsSummaryWithChildren since we might have had |
| // children but not anymore. |
| if (mChildrenContainer != null) { |
| mChildrenContainer.setUserLocked(userLocked); |
| if (mIsSummaryWithChildren && (userLocked || !isGroupExpanded())) { |
| updateBackgroundForGroupState(); |
| } |
| } |
| } |
| |
| /** |
| * @return has the system set this notification to be expanded |
| */ |
| public boolean isSystemExpanded() { |
| return mIsSystemExpanded; |
| } |
| |
| /** |
| * Set this notification to be expanded by the system. |
| * |
| * @param expand whether the system wants this notification to be expanded. |
| */ |
| public void setSystemExpanded(boolean expand) { |
| if (expand != mIsSystemExpanded) { |
| final boolean wasExpanded = isExpanded(); |
| mIsSystemExpanded = expand; |
| notifyHeightChanged(false /* needsAnimation */); |
| onExpansionChanged(false /* userAction */, wasExpanded); |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.updateGroupOverflow(); |
| resetChildSystemExpandedStates(); |
| } |
| } |
| } |
| |
| void setOnKeyguard(boolean onKeyguard) { |
| if (onKeyguard != mOnKeyguard) { |
| boolean wasAboveShelf = isAboveShelf(); |
| final boolean wasExpanded = isExpanded(); |
| mOnKeyguard = onKeyguard; |
| onExpansionChanged(false /* userAction */, wasExpanded); |
| if (wasExpanded != isExpanded()) { |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.updateGroupOverflow(); |
| } |
| notifyHeightChanged(false /* needsAnimation */); |
| } |
| if (isAboveShelf() != wasAboveShelf) { |
| mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); |
| } |
| } |
| updateRippleAllowed(); |
| } |
| |
| private void updateRippleAllowed() { |
| boolean allowed = isOnKeyguard() |
| || mEntry.getSbn().getNotification().contentIntent == null; |
| setRippleAllowed(allowed); |
| } |
| |
| @Override |
| public void onTap() { |
| // This notification will expand and animates into the content activity, so we disable the |
| // ripple. We will restore its value once the tap/click is actually performed. |
| if (mEntry.getSbn().getNotification().contentIntent != null) { |
| setRippleAllowed(false); |
| } |
| } |
| |
| @Override |
| public boolean performClick() { |
| // We force-disabled the ripple in onTap. When this method is called, the code drawing the |
| // ripple will already have been called so we can restore its value now. |
| updateRippleAllowed(); |
| return super.performClick(); |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| if (isUserLocked()) { |
| return getActualHeight(); |
| } |
| if (mGuts != null && mGuts.isExposed()) { |
| return mGuts.getIntrinsicHeight(); |
| } else if ((isChildInGroup() && !isGroupExpanded())) { |
| return mPrivateLayout.getMinHeight(); |
| } else if (mSensitive && mHideSensitiveForIntrinsicHeight) { |
| return getMinHeight(); |
| } else if (mIsSummaryWithChildren) { |
| return mChildrenContainer.getIntrinsicHeight(); |
| } else if (canShowHeadsUp() && isHeadsUpState()) { |
| if (isPinned() || mHeadsupDisappearRunning) { |
| return getPinnedHeadsUpHeight(true /* atLeastMinHeight */); |
| } else if (isExpanded()) { |
| return Math.max(getMaxExpandHeight(), getHeadsUpHeight()); |
| } else { |
| return Math.max(getCollapsedHeight(), getHeadsUpHeight()); |
| } |
| } else if (isExpanded()) { |
| return getMaxExpandHeight(); |
| } else { |
| return getCollapsedHeight(); |
| } |
| } |
| |
| /** |
| * @return {@code true} if the notification can show it's heads up layout. This is mostly true |
| * except for legacy use cases. |
| */ |
| public boolean canShowHeadsUp() { |
| if (mOnKeyguard && !isDozing() && !isBypassEnabled()) { |
| return false; |
| } |
| return true; |
| } |
| |
| private boolean isBypassEnabled() { |
| return mBypassController == null || mBypassController.getBypassEnabled(); |
| } |
| |
| private boolean isDozing() { |
| return mStatusBarStateController != null && mStatusBarStateController.isDozing(); |
| } |
| |
| @Override |
| public boolean isGroupExpanded() { |
| return mGroupExpansionManager.isGroupExpanded(mEntry); |
| } |
| |
| private void onAttachedChildrenCountChanged() { |
| mIsSummaryWithChildren = mChildrenContainer != null |
| && mChildrenContainer.getNotificationChildCount() > 0; |
| if (mIsSummaryWithChildren) { |
| NotificationViewWrapper wrapper = mChildrenContainer.getNotificationViewWrapper(); |
| if (wrapper == null || wrapper.getNotificationHeader() == null) { |
| mChildrenContainer.recreateNotificationHeader(mExpandClickListener, |
| isConversation()); |
| } |
| } |
| getShowingLayout().updateBackgroundColor(false /* animate */); |
| mPrivateLayout.updateExpandButtons(isExpandable()); |
| updateChildrenAppearance(); |
| updateChildrenVisibility(); |
| applyChildrenRoundness(); |
| } |
| |
| protected void expandNotification() { |
| mExpandClickListener.onClick(this); |
| } |
| |
| /** |
| * Returns the number of channels covered by the notification row (including its children if |
| * it's a summary notification). |
| */ |
| public int getNumUniqueChannels() { |
| return getUniqueChannels().size(); |
| } |
| |
| /** |
| * Returns the channels covered by the notification row (including its children if |
| * it's a summary notification). |
| */ |
| public ArraySet<NotificationChannel> getUniqueChannels() { |
| ArraySet<NotificationChannel> channels = new ArraySet<>(); |
| |
| channels.add(mEntry.getChannel()); |
| |
| // If this is a summary, then add in the children notification channels for the |
| // same user and pkg. |
| if (mIsSummaryWithChildren) { |
| final List<ExpandableNotificationRow> childrenRows = getAttachedChildren(); |
| final int numChildren = childrenRows.size(); |
| for (int i = 0; i < numChildren; i++) { |
| final ExpandableNotificationRow childRow = childrenRows.get(i); |
| final NotificationChannel childChannel = childRow.getEntry().getChannel(); |
| final StatusBarNotification childSbn = childRow.getEntry().getSbn(); |
| if (childSbn.getUser().equals(mEntry.getSbn().getUser()) |
| && childSbn.getPackageName().equals(mEntry.getSbn().getPackageName())) { |
| channels.add(childChannel); |
| } |
| } |
| } |
| |
| return channels; |
| } |
| |
| /** |
| * If this is a group, update the appearance of the children. |
| */ |
| public void updateChildrenAppearance() { |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.updateChildrenAppearance(); |
| } |
| } |
| |
| /** |
| * Check whether the view state is currently expanded. This is given by the system in {@link |
| * #setSystemExpanded(boolean)} and can be overridden by user expansion or |
| * collapsing in {@link #setUserExpanded(boolean)}. Note that the visual appearance of this |
| * view can differ from this state, if layout params are modified from outside. |
| * |
| * @return whether the view state is currently expanded. |
| */ |
| public boolean isExpanded() { |
| return isExpanded(false /* allowOnKeyguard */); |
| } |
| |
| public boolean isExpanded(boolean allowOnKeyguard) { |
| return (!mOnKeyguard || allowOnKeyguard) |
| && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded()) |
| || isUserExpanded()); |
| } |
| |
| private boolean isSystemChildExpanded() { |
| return mIsSystemChildExpanded; |
| } |
| |
| public void setSystemChildExpanded(boolean expanded) { |
| mIsSystemChildExpanded = expanded; |
| } |
| |
| public void setLayoutListener(LayoutListener listener) { |
| mLayoutListener = listener; |
| } |
| |
| public void removeListener() { |
| mLayoutListener = null; |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| Trace.beginSection(appendTraceStyleTag("ExpNotRow#onLayout")); |
| int intrinsicBefore = getIntrinsicHeight(); |
| super.onLayout(changed, left, top, right, bottom); |
| if (intrinsicBefore != getIntrinsicHeight() |
| && (intrinsicBefore != 0 || getActualHeight() > 0)) { |
| notifyHeightChanged(true /* needsAnimation */); |
| } |
| if (mMenuRow != null && mMenuRow.getMenuView() != null) { |
| mMenuRow.onParentHeightUpdate(); |
| } |
| updateContentShiftHeight(); |
| if (mLayoutListener != null) { |
| mLayoutListener.onLayout(); |
| } |
| Trace.endSection(); |
| } |
| |
| /** |
| * Updates the content shift height such that the header is completely hidden when coming from |
| * the top. |
| */ |
| private void updateContentShiftHeight() { |
| NotificationViewWrapper wrapper = getVisibleNotificationViewWrapper(); |
| CachingIconView icon = wrapper == null ? null : wrapper.getIcon(); |
| if (icon != null) { |
| mIconTransformContentShift = getRelativeTopPadding(icon) + icon.getHeight(); |
| } else { |
| mIconTransformContentShift = mContentShift; |
| } |
| } |
| |
| @Override |
| protected float getContentTransformationShift() { |
| return mIconTransformContentShift; |
| } |
| |
| @Override |
| public void notifyHeightChanged(boolean needsAnimation) { |
| super.notifyHeightChanged(needsAnimation); |
| getShowingLayout().requestSelectLayout(needsAnimation || isUserLocked()); |
| } |
| |
| public void setSensitive(boolean sensitive, boolean hideSensitive) { |
| int intrinsicBefore = getIntrinsicHeight(); |
| mSensitive = sensitive; |
| mSensitiveHiddenInGeneral = hideSensitive; |
| if (intrinsicBefore != getIntrinsicHeight()) { |
| // The animation has a few flaws and is highly visible, so jump cut instead. |
| notifyHeightChanged(false /* needsAnimation */); |
| } |
| } |
| |
| @Override |
| public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) { |
| mHideSensitiveForIntrinsicHeight = hideSensitive; |
| if (mIsSummaryWithChildren) { |
| List<ExpandableNotificationRow> notificationChildren = |
| mChildrenContainer.getAttachedChildren(); |
| for (int i = 0; i < notificationChildren.size(); i++) { |
| ExpandableNotificationRow child = notificationChildren.get(i); |
| child.setHideSensitiveForIntrinsicHeight(hideSensitive); |
| } |
| } |
| } |
| |
| @Override |
| public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, |
| long duration) { |
| if (getVisibility() == GONE) { |
| // If we are GONE, the hideSensitive parameter will not be calculated and always be |
| // false, which is incorrect, let's wait until a real call comes in later. |
| return; |
| } |
| boolean oldShowingPublic = mShowingPublic; |
| mShowingPublic = mSensitive && hideSensitive; |
| if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) { |
| return; |
| } |
| |
| if (!animated) { |
| mPublicLayout.animate().cancel(); |
| mPrivateLayout.animate().cancel(); |
| if (mChildrenContainer != null) { |
| mChildrenContainer.animate().cancel(); |
| } |
| resetAllContentAlphas(); |
| mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE); |
| updateChildrenVisibility(); |
| } else { |
| animateShowingPublic(delay, duration, mShowingPublic); |
| } |
| NotificationContentView showingLayout = getShowingLayout(); |
| showingLayout.updateBackgroundColor(animated); |
| mPrivateLayout.updateExpandButtons(isExpandable()); |
| updateShelfIconColor(); |
| mShowingPublicInitialized = true; |
| } |
| |
| private void animateShowingPublic(long delay, long duration, boolean showingPublic) { |
| View[] privateViews = mIsSummaryWithChildren |
| ? new View[] {mChildrenContainer} |
| : new View[] {mPrivateLayout}; |
| View[] publicViews = new View[] {mPublicLayout}; |
| View[] hiddenChildren = showingPublic ? privateViews : publicViews; |
| View[] shownChildren = showingPublic ? publicViews : privateViews; |
| for (final View hiddenView : hiddenChildren) { |
| hiddenView.setVisibility(View.VISIBLE); |
| hiddenView.animate().cancel(); |
| hiddenView.animate() |
| .alpha(0f) |
| .setStartDelay(delay) |
| .setDuration(duration) |
| .withEndAction(() -> { |
| hiddenView.setVisibility(View.INVISIBLE); |
| resetAllContentAlphas(); |
| }); |
| } |
| for (View showView : shownChildren) { |
| showView.setVisibility(View.VISIBLE); |
| showView.setAlpha(0f); |
| showView.animate().cancel(); |
| showView.animate() |
| .alpha(1f) |
| .setStartDelay(delay) |
| .setDuration(duration); |
| } |
| } |
| |
| @Override |
| public boolean mustStayOnScreen() { |
| return mIsHeadsUp && mMustStayOnScreen; |
| } |
| |
| /** |
| * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as |
| * otherwise some state might not be updated. To request about the general clearability |
| * see {@link NotificationEntry#isDismissable()}. |
| */ |
| public boolean canViewBeDismissed() { |
| return mEntry.isDismissable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); |
| } |
| |
| /** |
| * @return Whether this view is allowed to be cleared with clear all. Only valid for visible |
| * notifications as otherwise some state might not be updated. To request about the general |
| * clearability see {@link NotificationEntry#isClearable()}. |
| */ |
| public boolean canViewBeCleared() { |
| return mEntry.isClearable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); |
| } |
| |
| private boolean shouldShowPublic() { |
| return mSensitive && mHideSensitiveForIntrinsicHeight; |
| } |
| |
| public void makeActionsVisibile() { |
| setUserExpanded(true, true); |
| if (isChildInGroup()) { |
| mGroupExpansionManager.setGroupExpanded(mEntry, true); |
| } |
| notifyHeightChanged(false /* needsAnimation */); |
| } |
| |
| public void setChildrenExpanded(boolean expanded, boolean animate) { |
| mChildrenExpanded = expanded; |
| if (mChildrenContainer != null) { |
| mChildrenContainer.setChildrenExpanded(expanded); |
| } |
| updateBackgroundForGroupState(); |
| updateClickAndFocus(); |
| } |
| |
| public static void applyTint(View v, int color) { |
| int alpha; |
| if (color != 0) { |
| alpha = COLORED_DIVIDER_ALPHA; |
| } else { |
| color = 0xff000000; |
| alpha = DEFAULT_DIVIDER_ALPHA; |
| } |
| if (v.getBackground() instanceof ColorDrawable) { |
| ColorDrawable background = (ColorDrawable) v.getBackground(); |
| background.mutate(); |
| background.setColor(color); |
| background.setAlpha(alpha); |
| } |
| } |
| |
| public int getMaxExpandHeight() { |
| return mPrivateLayout.getExpandHeight(); |
| } |
| |
| |
| private int getHeadsUpHeight() { |
| return getShowingLayout().getHeadsUpHeight(false /* forceNoHeader */); |
| } |
| |
| public boolean areGutsExposed() { |
| return (mGuts != null && mGuts.isExposed()); |
| } |
| |
| @Override |
| public boolean isContentExpandable() { |
| if (mIsSummaryWithChildren && !shouldShowPublic()) { |
| return true; |
| } |
| NotificationContentView showingLayout = getShowingLayout(); |
| return showingLayout.isContentExpandable(); |
| } |
| |
| @Override |
| protected View getContentView() { |
| if (mIsSummaryWithChildren && !shouldShowPublic()) { |
| return mChildrenContainer; |
| } |
| return getShowingLayout(); |
| } |
| |
| @Override |
| public long performRemoveAnimation(long duration, long delay, float translationDirection, |
| boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable, |
| AnimatorListenerAdapter animationListener) { |
| if (mMenuRow != null && mMenuRow.isMenuVisible()) { |
| Animator anim = getTranslateViewAnimator(0f, null /* listener */); |
| if (anim != null) { |
| anim.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| ExpandableNotificationRow.super.performRemoveAnimation( |
| duration, delay, translationDirection, isHeadsUpAnimation, |
| endLocation, onFinishedRunnable, animationListener); |
| } |
| }); |
| anim.start(); |
| return anim.getDuration(); |
| } |
| } |
| return super.performRemoveAnimation(duration, delay, translationDirection, |
| isHeadsUpAnimation, endLocation, onFinishedRunnable, animationListener); |
| } |
| |
| @Override |
| protected void onAppearAnimationFinished(boolean wasAppearing) { |
| super.onAppearAnimationFinished(wasAppearing); |
| if (wasAppearing) { |
| // During the animation the visible view might have changed, so let's make sure all |
| // alphas are reset |
| resetAllContentAlphas(); |
| if (FADE_LAYER_OPTIMIZATION_ENABLED) { |
| setNotificationFaded(false); |
| } else { |
| setNotificationFadedOnChildren(false); |
| } |
| } else { |
| setHeadsUpAnimatingAway(false); |
| } |
| } |
| |
| @Override |
| protected void resetAllContentAlphas() { |
| mPrivateLayout.setAlpha(1f); |
| mPrivateLayout.setLayerType(LAYER_TYPE_NONE, null); |
| mPublicLayout.setAlpha(1f); |
| mPublicLayout.setLayerType(LAYER_TYPE_NONE, null); |
| if (mChildrenContainer != null) { |
| mChildrenContainer.setAlpha(1f); |
| mChildrenContainer.setLayerType(LAYER_TYPE_NONE, null); |
| } |
| } |
| |
| /** Gets the last value set with {@link #setNotificationFaded(boolean)} */ |
| @Override |
| public boolean isNotificationFaded() { |
| return mIsFaded; |
| } |
| |
| /** |
| * This class needs to delegate the faded state set on it by |
| * {@link com.android.systemui.statusbar.notification.stack.ViewState} to its children. |
| * Having each notification use layerType of HARDWARE anytime it fades in/out can result in |
| * extremely large layers (in the case of groups, it can even exceed the device height). |
| * Because these large renders can cause serious jank when rendering, we instead have |
| * notifications return false from {@link #hasOverlappingRendering()} and delegate the |
| * layerType to child views which really need it in order to render correctly, such as icon |
| * views or the conversation face pile. |
| * |
| * Another compounding factor for notifications is that we change clipping on each frame of the |
| * animation, so the hardware layer isn't able to do any caching at the top level, but the |
| * individual elements we render with hardware layers (e.g. icons) cache wonderfully because we |
| * never invalidate them. |
| */ |
| @Override |
| public void setNotificationFaded(boolean faded) { |
| mIsFaded = faded; |
| if (childrenRequireOverlappingRendering()) { |
| // == Simple Scenario == |
| // If a child (like remote input) needs this to have overlapping rendering, then set |
| // the layerType of this view and reset the children to render as if the notification is |
| // not fading. |
| NotificationFadeAware.setLayerTypeForFaded(this, faded); |
| setNotificationFadedOnChildren(false); |
| } else { |
| // == Delegating Scenario == |
| // This is the new normal for alpha: Explicitly reset this view's layer type to NONE, |
| // and require that all children use their own hardware layer if they have bad |
| // overlapping rendering. |
| NotificationFadeAware.setLayerTypeForFaded(this, false); |
| setNotificationFadedOnChildren(faded); |
| } |
| } |
| |
| /** Private helper for iterating over the layouts and children containers to set faded state */ |
| private void setNotificationFadedOnChildren(boolean faded) { |
| delegateNotificationFaded(mChildrenContainer, faded); |
| for (NotificationContentView layout : mLayouts) { |
| delegateNotificationFaded(layout, faded); |
| } |
| } |
| |
| private static void delegateNotificationFaded(@Nullable View view, boolean faded) { |
| if (FADE_LAYER_OPTIMIZATION_ENABLED && view instanceof NotificationFadeAware) { |
| ((NotificationFadeAware) view).setNotificationFaded(faded); |
| } else { |
| NotificationFadeAware.setLayerTypeForFaded(view, faded); |
| } |
| } |
| |
| /** |
| * Only declare overlapping rendering if independent children of the view require it. |
| */ |
| @Override |
| public boolean hasOverlappingRendering() { |
| return super.hasOverlappingRendering() && childrenRequireOverlappingRendering(); |
| } |
| |
| /** |
| * Because RemoteInputView is designed to be an opaque view that overlaps the Actions row, the |
| * row should require overlapping rendering to ensure that the overlapped view doesn't bleed |
| * through when alpha fading. |
| * |
| * Note that this currently works for top-level notifications which squish their height down |
| * while collapsing the shade, but does not work for children inside groups, because the |
| * accordion affect does not apply to those views, so super.hasOverlappingRendering() will |
| * always return false to avoid the clipping caused when the view's measured height is less than |
| * the 'actual height'. |
| */ |
| private boolean childrenRequireOverlappingRendering() { |
| if (!FADE_LAYER_OPTIMIZATION_ENABLED) { |
| return true; |
| } |
| // The colorized background is another layer with which all other elements overlap |
| if (getEntry().getSbn().getNotification().isColorized()) { |
| return true; |
| } |
| // Check if the showing layout has a need for overlapping rendering. |
| // NOTE: We could check both public and private layouts here, but becuause these states |
| // don't animate well, there are bigger visual artifacts if we start changing the shown |
| // layout during shade expansion. |
| NotificationContentView showingLayout = getShowingLayout(); |
| return showingLayout != null && showingLayout.requireRowToHaveOverlappingRendering(); |
| } |
| |
| @Override |
| public int getExtraBottomPadding() { |
| if (mIsSummaryWithChildren && isGroupExpanded()) { |
| return mIncreasedPaddingBetweenElements; |
| } |
| return 0; |
| } |
| |
| @Override |
| public void setActualHeight(int height, boolean notifyListeners) { |
| boolean changed = height != getActualHeight(); |
| super.setActualHeight(height, notifyListeners); |
| if (changed && isRemoved()) { |
| // TODO: remove this once we found the gfx bug for this. |
| // This is a hack since a removed view sometimes would just stay blank. it occured |
| // when sending yourself a message and then clicking on it. |
| ViewGroup parent = (ViewGroup) getParent(); |
| if (parent != null) { |
| parent.invalidate(); |
| } |
| } |
| if (mGuts != null && mGuts.isExposed()) { |
| mGuts.setActualHeight(height); |
| return; |
| } |
| int contentHeight = Math.max(getMinHeight(), height); |
| for (NotificationContentView l : mLayouts) { |
| l.setContentHeight(contentHeight); |
| } |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.setActualHeight(height); |
| } |
| if (mGuts != null) { |
| mGuts.setActualHeight(height); |
| } |
| if (mMenuRow != null && mMenuRow.getMenuView() != null) { |
| mMenuRow.onParentHeightUpdate(); |
| } |
| handleIntrinsicHeightReached(); |
| } |
| |
| @Override |
| public int getMaxContentHeight() { |
| if (mIsSummaryWithChildren && !shouldShowPublic()) { |
| return mChildrenContainer.getMaxContentHeight(); |
| } |
| NotificationContentView showingLayout = getShowingLayout(); |
| return showingLayout.getMaxHeight(); |
| } |
| |
| @Override |
| public int getMinHeight(boolean ignoreTemporaryStates) { |
| if (!ignoreTemporaryStates && mGuts != null && mGuts.isExposed()) { |
| return mGuts.getIntrinsicHeight(); |
| } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp |
| && mHeadsUpManager.isTrackingHeadsUp()) { |
| return getPinnedHeadsUpHeight(false /* atLeastMinHeight */); |
| } else if (mIsSummaryWithChildren && !isGroupExpanded() && !shouldShowPublic()) { |
| return mChildrenContainer.getMinHeight(); |
| } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp) { |
| return getHeadsUpHeight(); |
| } |
| NotificationContentView showingLayout = getShowingLayout(); |
| return showingLayout.getMinHeight(); |
| } |
| |
| @Override |
| public int getCollapsedHeight() { |
| if (mIsSummaryWithChildren && !shouldShowPublic()) { |
| return mChildrenContainer.getCollapsedHeight(); |
| } |
| return getMinHeight(); |
| } |
| |
| @Override |
| public int getHeadsUpHeightWithoutHeader() { |
| if (!canShowHeadsUp() || !mIsHeadsUp) { |
| return getCollapsedHeight(); |
| } |
| if (mIsSummaryWithChildren && !shouldShowPublic()) { |
| return mChildrenContainer.getCollapsedHeightWithoutHeader(); |
| } |
| return getShowingLayout().getHeadsUpHeight(true /* forceNoHeader */); |
| } |
| |
| @Override |
| public void setClipTopAmount(int clipTopAmount) { |
| super.setClipTopAmount(clipTopAmount); |
| for (NotificationContentView l : mLayouts) { |
| l.setClipTopAmount(clipTopAmount); |
| } |
| if (mGuts != null) { |
| mGuts.setClipTopAmount(clipTopAmount); |
| } |
| } |
| |
| @Override |
| public void setClipBottomAmount(int clipBottomAmount) { |
| if (mExpandAnimationRunning) { |
| return; |
| } |
| if (clipBottomAmount != mClipBottomAmount) { |
| super.setClipBottomAmount(clipBottomAmount); |
| for (NotificationContentView l : mLayouts) { |
| l.setClipBottomAmount(clipBottomAmount); |
| } |
| if (mGuts != null) { |
| mGuts.setClipBottomAmount(clipBottomAmount); |
| } |
| } |
| if (mChildrenContainer != null && !mChildIsExpanding) { |
| // We have to update this even if it hasn't changed, since the children locations can |
| // have changed |
| mChildrenContainer.setClipBottomAmount(clipBottomAmount); |
| } |
| } |
| |
| public NotificationContentView getShowingLayout() { |
| return shouldShowPublic() ? mPublicLayout : mPrivateLayout; |
| } |
| |
| public View getExpandedContentView() { |
| return getPrivateLayout().getExpandedChild(); |
| } |
| |
| public void setLegacy(boolean legacy) { |
| for (NotificationContentView l : mLayouts) { |
| l.setLegacy(legacy); |
| } |
| } |
| |
| @Override |
| protected void updateBackgroundTint() { |
| super.updateBackgroundTint(); |
| updateBackgroundForGroupState(); |
| if (mIsSummaryWithChildren) { |
| List<ExpandableNotificationRow> notificationChildren = |
| mChildrenContainer.getAttachedChildren(); |
| for (int i = 0; i < notificationChildren.size(); i++) { |
| ExpandableNotificationRow child = notificationChildren.get(i); |
| child.updateBackgroundForGroupState(); |
| } |
| } |
| } |
| |
| /** |
| * Called when a group has finished animating from collapsed or expanded state. |
| */ |
| public void onFinishedExpansionChange() { |
| mGroupExpansionChanging = false; |
| updateBackgroundForGroupState(); |
| } |
| |
| /** |
| * Updates the parent and children backgrounds in a group based on the expansion state. |
| */ |
| public void updateBackgroundForGroupState() { |
| if (mIsSummaryWithChildren) { |
| // Only when the group has finished expanding do we hide its background. |
| mShowNoBackground = !mShowGroupBackgroundWhenExpanded && isGroupExpanded() |
| && !isGroupExpansionChanging() && !isUserLocked(); |
| mChildrenContainer.updateHeaderForExpansion(mShowNoBackground); |
| List<ExpandableNotificationRow> children = mChildrenContainer.getAttachedChildren(); |
| for (int i = 0; i < children.size(); i++) { |
| children.get(i).updateBackgroundForGroupState(); |
| } |
| } else if (isChildInGroup()) { |
| final int childColor = getShowingLayout().getBackgroundColorForExpansionState(); |
| // Only show a background if the group is expanded OR if it is expanding / collapsing |
| // and has a custom background color. |
| final boolean showBackground = isGroupExpanded() |
| || ((mNotificationParent.isGroupExpansionChanging() |
| || mNotificationParent.isUserLocked()) && childColor != 0); |
| mShowNoBackground = !showBackground; |
| } else { |
| // Only children or parents ever need no background. |
| mShowNoBackground = false; |
| } |
| updateOutline(); |
| updateBackground(); |
| } |
| |
| @Override |
| protected boolean hideBackground() { |
| return mShowNoBackground || super.hideBackground(); |
| } |
| |
| public int getPositionOfChild(ExpandableNotificationRow childRow) { |
| if (mIsSummaryWithChildren) { |
| return mChildrenContainer.getPositionInLinearLayout(childRow); |
| } |
| return 0; |
| } |
| |
| public void onExpandedByGesture(boolean userExpanded) { |
| int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER; |
| if (mGroupMembershipManager.isGroupSummary(mEntry)) { |
| event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER; |
| } |
| mMetricsLogger.action(event, userExpanded); |
| } |
| |
| @Override |
| protected boolean disallowSingleClick(MotionEvent event) { |
| if (areGutsExposed()) { |
| return false; |
| } |
| float x = event.getX(); |
| float y = event.getY(); |
| NotificationViewWrapper wrapper = getVisibleNotificationViewWrapper(); |
| NotificationHeaderView header = wrapper == null ? null : wrapper.getNotificationHeader(); |
| // the extra translation only needs to be added, if we're translating the notification |
| // contents, otherwise the motionEvent is already at the right place due to the |
| // touch event system. |
| float translation = !mDismissUsingRowTranslationX ? getTranslation() : 0; |
| if (header != null && header.isInTouchRect(x - translation, y)) { |
| return true; |
| } |
| if ((!mIsSummaryWithChildren || shouldShowPublic()) |
| && getShowingLayout().disallowSingleClick(x, y)) { |
| return true; |
| } |
| return super.disallowSingleClick(event); |
| } |
| |
| private void onExpansionChanged(boolean userAction, boolean wasExpanded) { |
| boolean nowExpanded = isExpanded(); |
| if (mIsSummaryWithChildren && (!mIsLowPriority || wasExpanded)) { |
| nowExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); |
| } |
| if (nowExpanded != wasExpanded) { |
| updateShelfIconColor(); |
| if (mLogger != null) { |
| mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded); |
| } |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.onExpansionChanged(); |
| } |
| if (mExpansionChangedListener != null) { |
| mExpansionChangedListener.onExpansionChanged(nowExpanded); |
| } |
| } |
| } |
| |
| public void setOnExpansionChangedListener(@Nullable OnExpansionChangedListener listener) { |
| mExpansionChangedListener = listener; |
| } |
| |
| /** |
| * Perform an action when the notification height has reached its intrinsic height. |
| * |
| * @param runnable the runnable to run |
| */ |
| public void performOnIntrinsicHeightReached(@Nullable Runnable runnable) { |
| mOnIntrinsicHeightReachedRunnable = runnable; |
| handleIntrinsicHeightReached(); |
| } |
| |
| private void handleIntrinsicHeightReached() { |
| if (mOnIntrinsicHeightReachedRunnable != null |
| && getActualHeight() == getIntrinsicHeight()) { |
| mOnIntrinsicHeightReachedRunnable.run(); |
| mOnIntrinsicHeightReachedRunnable = null; |
| } |
| } |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfoInternal(info); |
| info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); |
| if (canViewBeDismissed() && !mIsSnoozed) { |
| info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); |
| } |
| boolean expandable = shouldShowPublic(); |
| boolean isExpanded = false; |
| if (!expandable) { |
| if (mIsSummaryWithChildren) { |
| expandable = true; |
| if (!mIsLowPriority || isExpanded()) { |
| isExpanded = isGroupExpanded(); |
| } |
| } else { |
| expandable = mPrivateLayout.isContentExpandable(); |
| isExpanded = isExpanded(); |
| } |
| } |
| if (expandable && !mIsSnoozed) { |
| if (isExpanded) { |
| info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE); |
| } else { |
| info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); |
| } |
| } |
| NotificationMenuRowPlugin provider = getProvider(); |
| if (provider != null) { |
| MenuItem snoozeMenu = provider.getSnoozeMenuItem(getContext()); |
| if (snoozeMenu != null) { |
| AccessibilityAction action = new AccessibilityAction(R.id.action_snooze, |
| getContext().getResources() |
| .getString(R.string.notification_menu_snooze_action)); |
| info.addAction(action); |
| } |
| } |
| } |
| |
| @Override |
| public boolean performAccessibilityActionInternal(int action, Bundle arguments) { |
| if (super.performAccessibilityActionInternal(action, arguments)) { |
| return true; |
| } |
| switch (action) { |
| case AccessibilityNodeInfo.ACTION_DISMISS: |
| performDismiss(true /* fromAccessibility */); |
| return true; |
| case AccessibilityNodeInfo.ACTION_COLLAPSE: |
| case AccessibilityNodeInfo.ACTION_EXPAND: |
| mExpandClickListener.onClick(this); |
| return true; |
| case AccessibilityNodeInfo.ACTION_LONG_CLICK: |
| doLongClickCallback(); |
| return true; |
| default: |
| if (action == R.id.action_snooze) { |
| NotificationMenuRowPlugin provider = getProvider(); |
| if (provider == null) { |
| return false; |
| } |
| MenuItem snoozeMenu = provider.getSnoozeMenuItem(getContext()); |
| if (snoozeMenu != null) { |
| doLongClickCallback(getWidth() / 2, getHeight() / 2, snoozeMenu); |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public interface OnExpandClickListener { |
| void onExpandClicked(NotificationEntry clickedEntry, View clickedView, boolean nowExpanded); |
| } |
| |
| @Override |
| public ExpandableViewState createExpandableViewState() { |
| return new NotificationViewState(); |
| } |
| |
| @Override |
| public boolean isAboveShelf() { |
| return (canShowHeadsUp() |
| && (mIsPinned || mHeadsupDisappearRunning || (mIsHeadsUp && mAboveShelf) |
| || mExpandAnimationRunning || mChildIsExpanding)); |
| } |
| |
| @Override |
| protected boolean childNeedsClipping(View child) { |
| if (child instanceof NotificationContentView) { |
| NotificationContentView contentView = (NotificationContentView) child; |
| if (isClippingNeeded()) { |
| return true; |
| } else if (!hasNoRounding() |
| && contentView.shouldClipToRounding(getCurrentTopRoundness() != 0.0f, |
| getCurrentBottomRoundness() != 0.0f)) { |
| return true; |
| } |
| } else if (child == mChildrenContainer) { |
| if (isClippingNeeded() || !hasNoRounding()) { |
| return true; |
| } |
| } else if (child instanceof NotificationGuts) { |
| return !hasNoRounding(); |
| } |
| return super.childNeedsClipping(child); |
| } |
| |
| /** |
| * Set a clip path to be set while expanding the notification. This is needed to nicely |
| * clip ourselves during the launch if we were clipped rounded in the beginning |
| */ |
| public void setExpandingClipPath(Path path) { |
| mExpandingClipPath = path; |
| invalidate(); |
| } |
| |
| @Override |
| protected void dispatchDraw(Canvas canvas) { |
| canvas.save(); |
| if (mExpandingClipPath != null && (mExpandAnimationRunning || mChildIsExpanding)) { |
| // If we're launching a notification, let's clip if a clip rounded to the clipPath |
| canvas.clipPath(mExpandingClipPath); |
| } |
| super.dispatchDraw(canvas); |
| canvas.restore(); |
| } |
| |
| @Override |
| protected void applyRoundness() { |
| super.applyRoundness(); |
| applyChildrenRoundness(); |
| } |
| |
| private void applyChildrenRoundness() { |
| if (mIsSummaryWithChildren) { |
| mChildrenContainer.setCurrentBottomRoundness(getCurrentBottomRoundness()); |
| } |
| } |
| |
| @Override |
| public Path getCustomClipPath(View child) { |
| if (child instanceof NotificationGuts) { |
| return getClipPath(true /* ignoreTranslation */); |
| } |
| return super.getCustomClipPath(child); |
| } |
| |
| private boolean hasNoRounding() { |
| return getCurrentBottomRoundness() == 0.0f && getCurrentTopRoundness() == 0.0f; |
| } |
| |
| public boolean isMediaRow() { |
| return mEntry.getSbn().getNotification().isMediaNotification(); |
| } |
| |
| public boolean isTopLevelChild() { |
| return getParent() instanceof NotificationStackScrollLayout; |
| } |
| |
| public boolean isGroupNotFullyVisible() { |
| return getClipTopAmount() > 0 || getTranslationY() < 0; |
| } |
| |
| public void setAboveShelf(boolean aboveShelf) { |
| boolean wasAboveShelf = isAboveShelf(); |
| mAboveShelf = aboveShelf; |
| if (isAboveShelf() != wasAboveShelf) { |
| mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); |
| } |
| } |
| |
| private static class NotificationViewState extends ExpandableViewState { |
| |
| @Override |
| public void applyToView(View view) { |
| if (view instanceof ExpandableNotificationRow) { |
| ExpandableNotificationRow row = (ExpandableNotificationRow) view; |
| if (row.isExpandAnimationRunning()) { |
| return; |
| } |
| handleFixedTranslationZ(row); |
| super.applyToView(view); |
| row.applyChildrenState(); |
| } |
| } |
| |
| private void handleFixedTranslationZ(ExpandableNotificationRow row) { |
| if (row.hasExpandingChild()) { |
| zTranslation = row.getTranslationZ(); |
| clipTopAmount = row.getClipTopAmount(); |
| } |
| } |
| |
| @Override |
| protected void onYTranslationAnimationFinished(View view) { |
| super.onYTranslationAnimationFinished(view); |
| if (view instanceof ExpandableNotificationRow) { |
| ExpandableNotificationRow row = (ExpandableNotificationRow) view; |
| if (row.isHeadsUpAnimatingAway()) { |
| row.setHeadsUpAnimatingAway(false); |
| } |
| } |
| } |
| |
| @Override |
| public void animateTo(View child, AnimationProperties properties) { |
| if (child instanceof ExpandableNotificationRow) { |
| ExpandableNotificationRow row = (ExpandableNotificationRow) child; |
| if (row.isExpandAnimationRunning()) { |
| return; |
| } |
| handleFixedTranslationZ(row); |
| super.animateTo(child, properties); |
| row.startChildAnimation(properties); |
| } |
| } |
| } |
| |
| /** |
| * Returns the Smart Suggestions backing the smart suggestion buttons in the notification. |
| */ |
| public InflatedSmartReplyState getExistingSmartReplyState() { |
| return mPrivateLayout.getCurrentSmartReplyState(); |
| } |
| |
| @VisibleForTesting |
| protected void setChildrenContainer(NotificationChildrenContainer childrenContainer) { |
| mChildrenContainer = childrenContainer; |
| } |
| |
| @VisibleForTesting |
| protected void setPrivateLayout(NotificationContentView privateLayout) { |
| mPrivateLayout = privateLayout; |
| } |
| |
| @VisibleForTesting |
| protected void setPublicLayout(NotificationContentView publicLayout) { |
| mPublicLayout = publicLayout; |
| } |
| |
| /** |
| * Equivalent to View.OnLongClickListener with coordinates |
| */ |
| public interface LongPressListener { |
| /** |
| * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates |
| * @return whether the longpress was handled |
| */ |
| boolean onLongPress(View v, int x, int y, MenuItem item); |
| } |
| |
| /** |
| * Called when notification drag and drop is finished successfully. |
| */ |
| public interface OnDragSuccessListener { |
| /** |
| * @param entry NotificationEntry that succeed to drop on proper target window. |
| */ |
| void onDragSuccess(NotificationEntry entry); |
| } |
| |
| /** |
| * Equivalent to View.OnClickListener with coordinates |
| */ |
| public interface CoordinateOnClickListener { |
| /** |
| * Equivalent to {@link View.OnClickListener#onClick(View)} with coordinates |
| * @return whether the click was handled |
| */ |
| boolean onClick(View v, int x, int y, MenuItem item); |
| } |
| |
| @Override |
| public void dump(PrintWriter pwOriginal, String[] args) { |
| IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); |
| // Skip super call; dump viewState ourselves |
| pw.println("Notification: " + mEntry.getKey()); |
| DumpUtilsKt.withIncreasedIndent(pw, () -> { |
| pw.print("visibility: " + getVisibility()); |
| pw.print(", alpha: " + getAlpha()); |
| pw.print(", translation: " + getTranslation()); |
| pw.print(", removed: " + isRemoved()); |
| pw.print(", expandAnimationRunning: " + mExpandAnimationRunning); |
| NotificationContentView showingLayout = getShowingLayout(); |
| pw.print(", privateShowing: " + (showingLayout == mPrivateLayout)); |
| pw.println(); |
| showingLayout.dump(pw, args); |
| |
| if (getViewState() != null) { |
| getViewState().dump(pw, args); |
| pw.println(); |
| } else { |
| pw.println("no viewState!!!"); |
| } |
| |
| if (mIsSummaryWithChildren) { |
| pw.println(); |
| pw.print("ChildrenContainer"); |
| pw.print(" visibility: " + mChildrenContainer.getVisibility()); |
| pw.print(", alpha: " + mChildrenContainer.getAlpha()); |
| pw.print(", translationY: " + mChildrenContainer.getTranslationY()); |
| pw.println(); |
| List<ExpandableNotificationRow> notificationChildren = getAttachedChildren(); |
| pw.println("Children: " + notificationChildren.size()); |
| pw.print("{"); |
| pw.increaseIndent(); |
| for (ExpandableNotificationRow child : notificationChildren) { |
| pw.println(); |
| child.dump(pw, args); |
| } |
| pw.decreaseIndent(); |
| pw.println("}"); |
| } else if (mPrivateLayout != null) { |
| mPrivateLayout.dumpSmartReplies(pw); |
| } |
| }); |
| } |
| |
| private void setTargetPoint(Point p) { |
| mTargetPoint = p; |
| } |
| public Point getTargetPoint() { |
| return mTargetPoint; |
| } |
| } |