Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f3bbd98b authored by Jon Miranda's avatar Jon Miranda Committed by Jonathan Miranda
Browse files

Allow users to dismiss notifications in popup view.

Bug: 193014051
Test: - dismiss notifications (left/right)
      - open popup, dismiss notification from shade
        and ensure popup gets updates
      - open popup, trigger new notification
        and ensure popup gets updated

Change-Id: Iea4d458218cbf5cb22f5f89aa0a4cc1bee18cc73
parent 6e72c8bb
Loading
Loading
Loading
Loading
+6 −6
Original line number Diff line number Diff line
@@ -14,10 +14,11 @@
     limitations under the License.
-->

<merge
<com.android.launcher3.notification.NotificationMainView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <!-- header -->
    <FrameLayout
@@ -49,7 +50,7 @@
    </FrameLayout>

    <!-- Main view -->
    <com.android.launcher3.notification.NotificationMainView
    <FrameLayout
        android:id="@+id/main_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
@@ -59,7 +60,6 @@
            android:id="@+id/text_and_background"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="?attr/popupColorPrimary"
            android:gravity="center_vertical"
            android:orientation="vertical"
            android:paddingTop="@dimen/notification_padding"
@@ -95,5 +95,5 @@
            android:layout_marginTop="@dimen/notification_padding"
            android:layout_marginStart="@dimen/notification_icon_padding" />

    </FrameLayout>
</com.android.launcher3.notification.NotificationMainView>
 No newline at end of file
</merge>
 No newline at end of file
+2 −5
Original line number Diff line number Diff line
@@ -31,12 +31,9 @@
        android:elevation="@dimen/deep_shortcuts_elevation"
        android:orientation="vertical"/>

    <LinearLayout
    <com.android.launcher3.notification.NotificationContainer
        android:id="@+id/notification_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:background="?attr/popupColorPrimary"
        android:elevation="@dimen/deep_shortcuts_elevation"
        android:orientation="vertical"/>
        android:visibility="gone"/>
</com.android.launcher3.popup.PopupContainerWithArrow>
 No newline at end of file
+2 −0
Original line number Diff line number Diff line
@@ -271,6 +271,8 @@

<!-- Notifications -->
    <dimen name="bg_round_rect_radius">8dp</dimen>
    <dimen name="notification_max_trans">8dp</dimen>
    <dimen name="notification_space">8dp</dimen>
    <dimen name="notification_padding">16dp</dimen>
    <dimen name="notification_padding_top">18dp</dimen>
    <dimen name="notification_header_text_size">14sp</dimen>
+283 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.launcher3.notification;

import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

import com.android.launcher3.R;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.touch.BaseSwipeDetector;
import com.android.launcher3.touch.OverScroll;
import com.android.launcher3.touch.SingleAxisSwipeDetector;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Class to manage the notification UI in a {@link PopupContainerWithArrow}.
 *
 * - Has two {@link NotificationMainView} that represent the top two notifications
 * - Handles dismissing a notification
 */
public class NotificationContainer extends FrameLayout implements SingleAxisSwipeDetector.Listener {

    private static final FloatProperty<NotificationContainer> DRAG_TRANSLATION_X =
            new FloatProperty<NotificationContainer>("notificationProgress") {
                @Override
                public void setValue(NotificationContainer view, float transX) {
                    view.setDragTranslationX(transX);
                }

                @Override
                public Float get(NotificationContainer view) {
                    return view.mDragTranslationX;
                }
            };

    private static final Rect sTempRect = new Rect();

    private final SingleAxisSwipeDetector mSwipeDetector;
    private final List<NotificationInfo> mNotificationInfos = new ArrayList<>();
    private boolean mIgnoreTouch = false;

    private final ObjectAnimator mContentTranslateAnimator;
    private float mDragTranslationX = 0;

    private final NotificationMainView mPrimaryView;
    private final NotificationMainView mSecondaryView;
    private PopupContainerWithArrow mPopupContainer;

    public NotificationContainer(Context context) {
        this(context, null, 0);
    }

    public NotificationContainer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NotificationContainer(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mSwipeDetector = new SingleAxisSwipeDetector(getContext(), this, HORIZONTAL);
        mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false);
        mContentTranslateAnimator = ObjectAnimator.ofFloat(this, DRAG_TRANSLATION_X, 0);

        mPrimaryView = (NotificationMainView) View.inflate(getContext(),
                R.layout.notification_content, null);
        mSecondaryView = (NotificationMainView) View.inflate(getContext(),
                R.layout.notification_content, null);
        mSecondaryView.setAlpha(0);

        addView(mSecondaryView);
        addView(mPrimaryView);

    }

    public void setPopupView(PopupContainerWithArrow popupView) {
        mPopupContainer = popupView;
    }

    /**
     * Returns true if we should intercept the swipe.
     */
    public boolean onInterceptSwipeEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            sTempRect.set(getLeft(), getTop(), getRight(), getBottom());
            mIgnoreTouch = !sTempRect.contains((int) ev.getX(), (int) ev.getY());
            if (!mIgnoreTouch) {
                mPopupContainer.getParent().requestDisallowInterceptTouchEvent(true);
            }
        }
        if (mIgnoreTouch) {
            return false;
        }
        if (mPrimaryView.getNotificationInfo() == null) {
            // The notification hasn't been populated yet.
            return false;
        }

        mSwipeDetector.onTouchEvent(ev);
        return mSwipeDetector.isDraggingOrSettling();
    }

    /**
     * Returns true when we should handle the swipe.
     */
    public boolean onSwipeEvent(MotionEvent ev) {
        if (mIgnoreTouch) {
            return false;
        }
        if (mPrimaryView.getNotificationInfo() == null) {
            // The notification hasn't been populated yet.
            return false;
        }
        return mSwipeDetector.onTouchEvent(ev);
    }

    /**
     * Applies the list of @param notificationInfos to this container.
     */
    public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) {
        mNotificationInfos.clear();
        if (notificationInfos.isEmpty()) {
            mPrimaryView.applyNotificationInfo(null);
            mSecondaryView.applyNotificationInfo(null);
            return;
        }
        mNotificationInfos.addAll(notificationInfos);

        NotificationInfo mainNotification = notificationInfos.get(0);
        mPrimaryView.applyNotificationInfo(mainNotification);
        mSecondaryView.applyNotificationInfo(notificationInfos.size() > 1
                ? notificationInfos.get(1)
                : null);
    }

    /**
     * Trims the notifications.
     * @param notificationKeys List of all valid notification keys.
     */
    public void trimNotifications(final List<String> notificationKeys) {
        Iterator<NotificationInfo> iterator = mNotificationInfos.iterator();
        while (iterator.hasNext()) {
            if (!notificationKeys.contains(iterator.next().notificationKey)) {
                iterator.remove();
            }
        }

        NotificationInfo primaryInfo = mNotificationInfos.size() > 0
                ? mNotificationInfos.get(0)
                : null;
        NotificationInfo secondaryInfo = mNotificationInfos.size() > 1
                ? mNotificationInfos.get(1)
                : null;

        mPrimaryView.applyNotificationInfo(primaryInfo);
        mSecondaryView.applyNotificationInfo(secondaryInfo);

        mPrimaryView.onPrimaryDrag(0);
        mSecondaryView.onSecondaryDrag(0);
    }

    private void setDragTranslationX(float translationX) {
        mDragTranslationX = translationX;

        float progress = translationX / getWidth();
        mPrimaryView.onPrimaryDrag(progress);
        if (mSecondaryView.getNotificationInfo() == null) {
            mSecondaryView.setAlpha(0f);
        } else {
            mSecondaryView.onSecondaryDrag(progress);
        }
    }

    // SingleAxisSwipeDetector.Listener's
    @Override
    public void onDragStart(boolean start, float startDisplacement) {
        mPopupContainer.showArrow(false);
    }

    @Override
    public boolean onDrag(float displacement) {
        if (!mPrimaryView.canChildBeDismissed()) {
            displacement = OverScroll.dampedScroll(displacement, getWidth());
        }

        float progress = displacement / getWidth();
        mPrimaryView.onPrimaryDrag(progress);
        if (mSecondaryView.getNotificationInfo() == null) {
            mSecondaryView.setAlpha(0f);
        } else {
            mSecondaryView.onSecondaryDrag(progress);
        }
        mContentTranslateAnimator.cancel();
        return true;
    }

    @Override
    public void onDragEnd(float velocity) {
        final boolean willExit;
        final float endTranslation;
        final float startTranslation = mPrimaryView.getTranslationX();
        final float width = getWidth();

        if (!mPrimaryView.canChildBeDismissed()) {
            willExit = false;
            endTranslation = 0;
        } else if (mSwipeDetector.isFling(velocity)) {
            willExit = true;
            endTranslation = velocity < 0 ? -width : width;
        } else if (Math.abs(startTranslation) > width / 2f) {
            willExit = true;
            endTranslation = (startTranslation < 0 ? -width : width);
        } else {
            willExit = false;
            endTranslation = 0;
        }

        long duration = BaseSwipeDetector.calculateDuration(velocity,
                (endTranslation - startTranslation) / width);

        mContentTranslateAnimator.removeAllListeners();
        mContentTranslateAnimator.setDuration(duration)
                .setInterpolator(scrollInterpolatorForVelocity(velocity));
        mContentTranslateAnimator.setFloatValues(startTranslation, endTranslation);

        NotificationMainView current = mPrimaryView;
        mContentTranslateAnimator.addListener(new AnimationSuccessListener() {
            @Override
            public void onAnimationSuccess(Animator animator) {
                mSwipeDetector.finishedScrolling();
                if (willExit) {
                    current.onChildDismissed();
                }
                mPopupContainer.showArrow(true);
            }
        });
        mContentTranslateAnimator.start();
    }

    /**
     * Animates the background color to a new color.
     * @param color The color to change to.
     * @param animatorSetOut The AnimatorSet where we add the color animator to.
     */
    public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
        mPrimaryView.updateBackgroundColor(color, animatorSetOut);
        mSecondaryView.updateBackgroundColor(color, animatorSetOut);
    }

    /**
     * Updates the header with a new @param notificationCount.
     */
    public void updateHeader(int notificationCount) {
        mPrimaryView.updateHeader(notificationCount);
        mSecondaryView.updateHeader(notificationCount - 1);
    }
}
+0 −179
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.launcher3.notification;

import android.animation.AnimatorSet;
import android.content.Context;
import android.graphics.Outline;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewOutlineProvider;
import android.widget.TextView;

import com.android.launcher3.R;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.util.Themes;

import java.util.ArrayList;
import java.util.List;

/**
 * Utility class to manage notification UI
 */
public class NotificationItemView {

    private static final Rect sTempRect = new Rect();

    private final Context mContext;
    private final PopupContainerWithArrow mPopupContainer;
    private final ViewGroup mRootView;

    private final TextView mHeaderCount;
    private final NotificationMainView mMainView;

    private final View mHeader;

    private View mGutter;

    private boolean mIgnoreTouch = false;
    private List<NotificationInfo> mNotificationInfos = new ArrayList<>();

    public NotificationItemView(PopupContainerWithArrow container, ViewGroup rootView) {
        mPopupContainer = container;
        mRootView = rootView;
        mContext = container.getContext();

        mHeaderCount = container.findViewById(R.id.notification_count);
        mMainView = container.findViewById(R.id.main_view);

        mHeader = container.findViewById(R.id.header);

        float radius = Themes.getDialogCornerRadius(mContext);
        rootView.setClipToOutline(true);
        rootView.setOutlineProvider(new ViewOutlineProvider() {
            @Override
            public void getOutline(View view, Outline outline) {
                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
            }
        });
    }

    /**
     * Animates the background color to a new color.
     * @param color The color to change to.
     * @param animatorSetOut The AnimatorSet where we add the color animator to.
     */
    public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
        mMainView.updateBackgroundColor(color, animatorSetOut);
    }

    public void addGutter() {
        if (mGutter == null) {
            mGutter = mPopupContainer.inflateAndAdd(R.layout.notification_gutter, mRootView);
        }
    }

    public void inverseGutterMargin() {
        MarginLayoutParams lp = (MarginLayoutParams) mGutter.getLayoutParams();
        int top = lp.topMargin;
        lp.topMargin = lp.bottomMargin;
        lp.bottomMargin = top;
    }

    public void removeAllViews() {
        mRootView.removeView(mMainView);
        mRootView.removeView(mHeader);
        if (mGutter != null) {
            mRootView.removeView(mGutter);
        }
    }

    /**
     * Updates the header text.
     * @param notificationCount The number of notifications.
     */
    public void updateHeader(int notificationCount) {
        final String text;
        final int visibility;
        if (notificationCount <= 1) {
            text = "";
            visibility = View.INVISIBLE;
        } else {
            text = String.valueOf(notificationCount);
            visibility = View.VISIBLE;

        }
        mHeaderCount.setText(text);
        mHeaderCount.setVisibility(visibility);
    }

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            sTempRect.set(mRootView.getLeft(), mRootView.getTop(),
                    mRootView.getRight(), mRootView.getBottom());
            mIgnoreTouch = !sTempRect.contains((int) ev.getX(), (int) ev.getY());
            if (!mIgnoreTouch) {
                mPopupContainer.getParent().requestDisallowInterceptTouchEvent(true);
            }
        }
        if (mIgnoreTouch) {
            return false;
        }
        if (mMainView.getNotificationInfo() == null) {
            // The notification hasn't been populated yet.
            return false;
        }

        return false;
    }

    public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) {
        mNotificationInfos.clear();
        if (notificationInfos.isEmpty()) {
            return;
        }
        mNotificationInfos.addAll(notificationInfos);

        NotificationInfo mainNotification = notificationInfos.get(0);
        mMainView.applyNotificationInfo(mainNotification, false);
    }

    public void trimNotifications(final List<String> notificationKeys) {
        NotificationInfo currentMainNotificationInfo = mMainView.getNotificationInfo();
        boolean shouldUpdateMainNotification = !notificationKeys.contains(
                currentMainNotificationInfo.notificationKey);

        if (shouldUpdateMainNotification) {
            int size = notificationKeys.size();
            NotificationInfo nextNotification = null;
            // We get the latest notification by finding the notification after the one that was
            // just dismissed.
            for (int i = 0; i < size; ++i) {
                if (currentMainNotificationInfo == mNotificationInfos.get(i) && i + 1 < size) {
                    nextNotification = mNotificationInfos.get(i + 1);
                    break;
                }
            }
            if (nextNotification != null) {
                mMainView.applyNotificationInfo(nextNotification, true);
            }
        }
    }
}
Loading