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

Commit ab91392a authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Split auto dismissing functionality from heads up"

parents b4dac160 d4f66a48
Loading
Loading
Loading
Loading
+317 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.statusbar.notification.NotificationData;

import java.util.stream.Stream;

/**
 * A manager which contains notification alerting functionality, providing methods to add and
 * remove notifications that appear on screen for a period of time and dismiss themselves at the
 * appropriate time.  These include heads up notifications and ambient pulses.
 */
public abstract class AlertingNotificationManager {
    private static final String TAG = "AlertNotifManager";
    protected final Clock mClock = new Clock();
    protected final ArrayMap<String, AlertEntry> mAlertEntries = new ArrayMap<>();
    protected int mMinimumDisplayTime;
    protected int mAutoDismissNotificationDecay;
    @VisibleForTesting
    public Handler mHandler = new Handler(Looper.getMainLooper());

    /**
     * Called when posting a new notification that should alert the user and appear on screen.
     * Adds the notification to be managed.
     * @param entry entry to show
     */
    public void showNotification(@NonNull NotificationData.Entry entry) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "showNotification");
        }
        addAlertEntry(entry);
        updateNotification(entry.key, true /* alert */);
        entry.setInterruption();
    }

    /**
     * Try to remove the notification.  May not succeed if the notification has not been shown long
     * enough and needs to be kept around.
     * @param key the key of the notification to remove
     * @param releaseImmediately force a remove regardless of earliest removal time
     * @return true if notification is removed, false otherwise
     */
    public boolean removeNotification(@NonNull String key, boolean releaseImmediately) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "removeNotification");
        }
        AlertEntry alertEntry = mAlertEntries.get(key);
        if (alertEntry == null) {
            return true;
        }
        if (releaseImmediately || alertEntry.wasShownLongEnough()) {
            removeAlertEntry(key);
        } else {
            alertEntry.removeAsSoonAsPossible();
            return false;
        }
        return true;
    }

    /**
     * Called when the notification state has been updated.
     * @param key the key of the entry that was updated
     * @param alert whether the notification should alert again and force reevaluation of
     *              removal time
     */
    public void updateNotification(@NonNull String key, boolean alert) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "updateNotification");
        }

        AlertEntry alertEntry = mAlertEntries.get(key);
        if (alertEntry == null) {
            // the entry was released before this update (i.e by a listener) This can happen
            // with the groupmanager
            return;
        }

        alertEntry.mEntry.row.sendAccessibilityEvent(
                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
        if (alert) {
            alertEntry.updateEntry(true /* updatePostTime */);
        }
    }

    /**
     * Clears all managed notifications.
     */
    public void releaseAllImmediately() {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "releaseAllImmediately");
        }
        // A copy is necessary here as we are changing the underlying map.  This would cause
        // undefined behavior if we iterated over the key set directly.
        ArraySet<String> keysToRemove = new ArraySet<>(mAlertEntries.keySet());
        for (String key : keysToRemove) {
            removeAlertEntry(key);
        }
    }

    /**
     * Returns the entry if it is managed by this manager.
     * @param key key of notification
     * @return the entry
     */
    @Nullable
    public NotificationData.Entry getEntry(@NonNull String key) {
        AlertEntry entry = mAlertEntries.get(key);
        return entry != null ? entry.mEntry : null;
    }

    /**
     * Returns the stream of all current notifications managed by this manager.
     * @return all entries
     */
    @NonNull
    public Stream<NotificationData.Entry> getAllEntries() {
        return mAlertEntries.values().stream().map(headsUpEntry -> headsUpEntry.mEntry);
    }

    /**
     * Whether or not there are any active alerting notifications.
     * @return true if there is an alert, false otherwise
     */
    public boolean hasNotifications() {
        return !mAlertEntries.isEmpty();
    }

    /**
     * Whether or not the given notification is alerting and managed by this manager.
     * @return true if the notification is alerting
     */
    public boolean contains(@NonNull String key) {
        return mAlertEntries.containsKey(key);
    }

    /**
     * Add a new entry and begin managing it.
     * @param entry the entry to add
     */
    protected final void addAlertEntry(@NonNull NotificationData.Entry entry) {
        AlertEntry alertEntry = createAlertEntry();
        alertEntry.setEntry(entry);
        mAlertEntries.put(entry.key, alertEntry);
        onAlertEntryAdded(alertEntry);
        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    }

    /**
     * Manager-specific logic that should occur when an entry is added.
     * @param alertEntry alert entry added
     */
    protected abstract void onAlertEntryAdded(@NonNull AlertEntry alertEntry);

    /**
     * Remove a notification and reset the alert entry.
     * @param key key of notification to remove
     */
    protected final void removeAlertEntry(@NonNull String key) {
        AlertEntry alertEntry = mAlertEntries.get(key);
        if (alertEntry == null) {
            return;
        }
        NotificationData.Entry entry = alertEntry.mEntry;
        mAlertEntries.remove(key);
        onAlertEntryRemoved(alertEntry);
        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
        alertEntry.reset();
    }

    /**
     * Manager-specific logic that should occur when an alert entry is removed.
     * @param alertEntry alert entry removed
     */
    protected abstract void onAlertEntryRemoved(@NonNull AlertEntry alertEntry);

    /**
     * Returns a new alert entry instance.
     * @return a new AlertEntry
     */
    protected AlertEntry createAlertEntry() {
        return new AlertEntry();
    }

    protected class AlertEntry implements Comparable<AlertEntry> {
        @Nullable public NotificationData.Entry mEntry;
        public long mPostTime;
        public long mEarliestRemovaltime;

        @Nullable protected Runnable mRemoveAlertRunnable;

        public void setEntry(@Nullable final NotificationData.Entry entry) {
            setEntry(entry, () -> removeAlertEntry(entry.key));
        }

        public void setEntry(@Nullable final NotificationData.Entry entry,
                @Nullable Runnable removeAlertRunnable) {
            mEntry = entry;
            mRemoveAlertRunnable = removeAlertRunnable;

            mPostTime = calculatePostTime();
            updateEntry(true /* updatePostTime */);
        }

        /**
         * Updates an entry's removal time.
         * @param updatePostTime whether or not to refresh the post time
         */
        public void updateEntry(boolean updatePostTime) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                Log.v(TAG, "updateEntry");
            }

            long currentTime = mClock.currentTimeMillis();
            mEarliestRemovaltime = currentTime + mMinimumDisplayTime;
            if (updatePostTime) {
                mPostTime = Math.max(mPostTime, currentTime);
            }
            removeAutoRemovalCallbacks();

            if (!isSticky()) {
                long finishTime = mPostTime + mAutoDismissNotificationDecay;
                long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
                mHandler.postDelayed(mRemoveAlertRunnable, removeDelay);
            }
        }

        /**
         * Whether or not the notification is "sticky" i.e. should stay on screen regardless
         * of the timer and should be removed externally.
         * @return true if the notification is sticky
         */
        protected boolean isSticky() {
            return false;
        }

        /**
         * Whether the notification has been on screen long enough and can be removed.
         * @return true if the notification has been on screen long enough
         */
        public boolean wasShownLongEnough() {
            return mEarliestRemovaltime < mClock.currentTimeMillis();
        }

        @Override
        public int compareTo(@NonNull AlertEntry alertEntry) {
            return (mPostTime < alertEntry.mPostTime)
                    ? 1 : ((mPostTime == alertEntry.mPostTime)
                            ? mEntry.key.compareTo(alertEntry.mEntry.key) : -1);
        }

        public void reset() {
            mEntry = null;
            removeAutoRemovalCallbacks();
            mRemoveAlertRunnable = null;
        }

        /**
         * Clear any pending removal runnables.
         */
        public void removeAutoRemovalCallbacks() {
            if (mRemoveAlertRunnable != null) {
                mHandler.removeCallbacks(mRemoveAlertRunnable);
            }
        }

        /**
         * Remove the alert at the earliest allowed removal time.
         */
        public void removeAsSoonAsPossible() {
            if (mRemoveAlertRunnable != null) {
                removeAutoRemovalCallbacks();
                mHandler.postDelayed(mRemoveAlertRunnable,
                        mEarliestRemovaltime - mClock.currentTimeMillis());
            }
        }

        /**
         * Calculate what the post time of a notification is at some current time.
         * @return the post time
         */
        protected long calculatePostTime() {
            return mClock.currentTimeMillis();
        }
    }

    protected final static class Clock {
        public long currentTimeMillis() {
            return SystemClock.elapsedRealtime();
        }
    }
}
+3 −3
Original line number Diff line number Diff line
@@ -487,7 +487,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
    public void removeNotification(String key, NotificationListenerService.RankingMap ranking) {
        boolean deferRemoval = false;
        abortExistingInflation(key);
        if (mHeadsUpManager.isHeadsUp(key)) {
        if (mHeadsUpManager.contains(key)) {
            // A cancel() in response to a remote input shouldn't be delayed, as it makes the
            // sending look longer than it takes.
            // Also we should not defer the removal if reordering isn't allowed since otherwise
@@ -1060,7 +1060,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
                // We don't want this to be interrupting anymore, lets remove it
                mHeadsUpManager.removeNotification(key, false /* ignoreEarliestRemovalTime */);
            } else {
                mHeadsUpManager.updateNotification(entry, alertAgain);
                mHeadsUpManager.updateNotification(entry.key, alertAgain);
            }
        } else if (shouldPeek && alertAgain) {
            // This notification was updated to be a heads-up, show it!
@@ -1069,7 +1069,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
    }

    protected boolean isHeadsUp(String key) {
        return mHeadsUpManager.isHeadsUp(key);
        return mHeadsUpManager.contains(key);
    }

    public boolean isNotificationKeptForRemoteInput(String key) {
+30 −46
Original line number Diff line number Diff line
@@ -31,7 +31,6 @@ import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.ScreenDecorations;
@@ -55,7 +54,6 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
       ViewTreeObserver.OnComputeInternalInsetsListener, VisualStabilityManager.Callback,
       OnHeadsUpChangedListener, ConfigurationController.ConfigurationListener {
    private static final String TAG = "HeadsUpManagerPhone";
    private static final boolean DEBUG = false;

    private final View mStatusBarWindowView;
    private final NotificationGroupManager mGroupManager;
@@ -114,7 +112,9 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
        addListener(new OnHeadsUpChangedListener() {
            @Override
            public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) {
                if (DEBUG) Log.w(TAG, "onHeadsUpPinnedModeChanged");
                if (Log.isLoggable(TAG, Log.WARN)) {
                    Log.w(TAG, "onHeadsUpPinnedModeChanged");
                }
                updateTouchableRegionListener();
            }
        });
@@ -153,7 +153,7 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
     */
    public boolean shouldSwallowClick(@NonNull String key) {
        HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
        return entry != null && mClock.currentTimeMillis() < entry.postTime;
        return entry != null && mClock.currentTimeMillis() < entry.mPostTime;
    }

    public void onExpandingFinished() {
@@ -162,9 +162,9 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
            mReleaseOnExpandFinish = false;
        } else {
            for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
                if (isHeadsUp(entry.key)) {
                if (contains(entry.key)) {
                    // Maybe the heads-up was removed already
                    removeHeadsUpEntry(entry);
                    removeAlertEntry(entry.key);
                }
            }
        }
@@ -235,13 +235,6 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
        }
    }

    @VisibleForTesting
    public void removeMinimumDisplayTimeForTesting() {
        mMinimumDisplayTime = 0;
        mHeadsUpNotificationDecay = 0;
        mTouchAcceptanceDelay = 0;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    //  HeadsUpManager public methods overrides:

@@ -250,12 +243,6 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
        return mTrackingHeadsUp;
    }

    @Override
    public void snooze() {
        super.snooze();
        mReleaseOnExpandFinish = true;
    }

    /**
     * React to the removal of the notification in the heads up.
     *
@@ -263,14 +250,15 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
     * for a bit since it wasn't shown long enough
     */
    @Override
    public boolean removeNotification(@NonNull String key, boolean ignoreEarliestRemovalTime) {
        if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
            return super.removeNotification(key, ignoreEarliestRemovalTime);
        } else {
            HeadsUpEntryPhone entry = getHeadsUpEntryPhone(key);
            entry.removeAsSoonAsPossible();
            return false;
    public boolean removeNotification(@NonNull String key, boolean releaseImmediately) {
        return super.removeNotification(key, canRemoveImmediately(key)
                || releaseImmediately);
    }

    @Override
    public void snooze() {
        super.snooze();
        mReleaseOnExpandFinish = true;
    }

    public void addSwipedOutNotification(@NonNull String key) {
@@ -354,9 +342,9 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
    public void onReorderingAllowed() {
        mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false);
        for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
            if (isHeadsUp(entry.key)) {
            if (contains(entry.key)) {
                // Maybe the heads-up was removed already
                removeHeadsUpEntry(entry);
                removeAlertEntry(entry.key);
            }
        }
        mEntriesToRemoveWhenReorderingAllowed.clear();
@@ -367,14 +355,14 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
    //  HeadsUpManager utility (protected) methods overrides:

    @Override
    protected HeadsUpEntry createHeadsUpEntry() {
    protected HeadsUpEntry createAlertEntry() {
        return mEntryPool.acquire();
    }

    @Override
    protected void releaseHeadsUpEntry(HeadsUpEntry entry) {
        entry.reset();
        mEntryPool.release((HeadsUpEntryPhone) entry);
    protected void onAlertEntryRemoved(AlertEntry alertEntry) {
        super.onAlertEntryRemoved(alertEntry);
        mEntryPool.release((HeadsUpEntryPhone) alertEntry);
    }

    @Override
@@ -394,7 +382,7 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,

    @Nullable
    private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
        return (HeadsUpEntryPhone) getHeadsUpEntry(key);
        return (HeadsUpEntryPhone) mAlertEntries.get(key);
    }

    @Nullable
@@ -402,7 +390,7 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
        return (HeadsUpEntryPhone) getTopHeadsUpEntry();
    }

    private boolean wasShownLongEnough(@NonNull String key) {
    private boolean canRemoveImmediately(@NonNull String key) {
        if (mSwipedOutKeys.contains(key)) {
            // We always instantly dismiss views being manually swiped out.
            mSwipedOutKeys.remove(key);
@@ -461,33 +449,29 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
                    mVisualStabilityManager.addReorderingAllowedCallback(
                            HeadsUpManagerPhone.this);
                } else if (!mTrackingHeadsUp) {
                    removeHeadsUpEntry(entry);
                    removeAlertEntry(entry.key);
                } else {
                    mEntriesToRemoveAfterExpand.add(entry);
                }
            };

            super.setEntry(entry, removeHeadsUpRunnable);
        }

        public boolean wasShownLongEnough() {
            return earliestRemovaltime < mClock.currentTimeMillis();
            setEntry(entry, removeHeadsUpRunnable);
        }

        @Override
        public void updateEntry(boolean updatePostTime) {
            super.updateEntry(updatePostTime);

            if (mEntriesToRemoveAfterExpand.contains(entry)) {
                mEntriesToRemoveAfterExpand.remove(entry);
            if (mEntriesToRemoveAfterExpand.contains(mEntry)) {
                mEntriesToRemoveAfterExpand.remove(mEntry);
            }
            if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
                mEntriesToRemoveWhenReorderingAllowed.remove(entry);
            if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
                mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
            }
        }

        @Override
        public void expanded(boolean expanded) {
        public void setExpanded(boolean expanded) {
            if (this.expanded == expanded) {
                return;
            }
+9 −7
Original line number Diff line number Diff line
@@ -171,7 +171,7 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener {
     */
    private void cleanUpHeadsUpStatesOnAdd(NotificationGroup group, boolean addIsPending) {
        if (!addIsPending && group.hunSummaryOnNextAddition) {
            if (!mHeadsUpManager.isHeadsUp(group.summary.key)) {
            if (!mHeadsUpManager.contains(group.summary.key)) {
                mHeadsUpManager.showNotification(group.summary);
            }
            group.hunSummaryOnNextAddition = false;
@@ -208,15 +208,17 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener {
                NotificationData.Entry entry = children.get(i);
                if (onlySummaryAlerts(entry) && entry.row.isHeadsUp()) {
                    releasedChild = true;
                    mHeadsUpManager.releaseImmediately(entry.key);
                    mHeadsUpManager.removeNotification(
                            entry.key, true /* releaseImmediately */);
                }
            }
            if (isolatedChild != null && onlySummaryAlerts(isolatedChild)
                    && isolatedChild.row.isHeadsUp()) {
                releasedChild = true;
                mHeadsUpManager.releaseImmediately(isolatedChild.key);
                mHeadsUpManager.removeNotification(
                        isolatedChild.key, true /* releaseImmediately */);
            }
            if (releasedChild && !mHeadsUpManager.isHeadsUp(group.summary.key)) {
            if (releasedChild && !mHeadsUpManager.contains(group.summary.key)) {
                boolean notifyImmediately = (numChildren - numPendingChildren) > 1;
                if (notifyImmediately) {
                    mHeadsUpManager.showNotification(group.summary);
@@ -546,8 +548,8 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener {
                    // the notification is actually already removed, no need to do heads-up on it.
                    return;
                }
                if (mHeadsUpManager.isHeadsUp(child.key)) {
                    mHeadsUpManager.updateNotification(child, true);
                if (mHeadsUpManager.contains(child.key)) {
                    mHeadsUpManager.updateNotification(child.key, true /* alert */);
                } else {
                    if (onlySummaryAlerts(entry)) {
                        notificationGroup.lastHeadsUpTransfer = SystemClock.elapsedRealtime();
@@ -556,7 +558,7 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener {
                }
            }
        }
        mHeadsUpManager.releaseImmediately(entry.key);
        mHeadsUpManager.removeNotification(entry.key, true /* releaseImmediately */);
    }

    private boolean onlySummaryAlerts(NotificationData.Entry entry) {
+5 −7
Original line number Diff line number Diff line
@@ -56,7 +56,6 @@ import android.app.PendingIntent;
import android.app.StatusBarManager;
import android.app.TaskStackBuilder;
import android.app.UiModeManager;
import android.app.WallpaperColors;
import android.app.WallpaperInfo;
import android.app.WallpaperManager;
import android.app.admin.DevicePolicyManager;
@@ -67,8 +66,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.om.IOverlayManager;
import android.content.om.OverlayInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -1414,7 +1411,7 @@ public class StatusBar extends SystemUI implements DemoMode,
    @Override
    public void onPerformRemoveNotification(StatusBarNotification n) {
        if (mStackScroller.hasPulsingNotifications() &&
                    !mHeadsUpManager.hasHeadsUpNotifications()) {
                    !mHeadsUpManager.hasNotifications()) {
            // We were showing a pulse for a notification, but no notifications are pulsing anymore.
            // Finish the pulse.
            mDozeScrimController.pulseOutNow();
@@ -4835,7 +4832,7 @@ public class StatusBar extends SystemUI implements DemoMode,
                @Override
                public void onPulseStarted() {
                    callback.onPulseStarted();
                    if (mHeadsUpManager.hasHeadsUpNotifications()) {
                    if (mHeadsUpManager.hasNotifications()) {
                        // Only pulse the stack scroller if there's actually something to show.
                        // Otherwise just show the always-on screen.
                        setPulsing(true);
@@ -5108,7 +5105,7 @@ public class StatusBar extends SystemUI implements DemoMode,
        final boolean wasOccluded = mIsOccluded;
        dismissKeyguardThenExecute(() -> {
            // TODO: Some of this code may be able to move to NotificationEntryManager.
            if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUp(notificationKey)) {
            if (mHeadsUpManager != null && mHeadsUpManager.contains(notificationKey)) {
                // Release the HUN notification to the shade.

                if (isPresenterFullyCollapsed()) {
@@ -5117,7 +5114,8 @@ public class StatusBar extends SystemUI implements DemoMode,
                //
                // In most cases, when FLAG_AUTO_CANCEL is set, the notification will
                // become canceled shortly by NoMan, but we can't assume that.
                mHeadsUpManager.releaseImmediately(notificationKey);
                mHeadsUpManager.removeNotification(sbn.getKey(),
                        true /* releaseImmediately */);
            }
            StatusBarNotification parentToCancel = null;
            if (shouldAutoCancel(sbn) && mGroupManager.isOnlyChildInGroup(sbn)) {
Loading