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

Commit 7f234901 authored by Mady Mellor's avatar Mady Mellor
Browse files

Add flag to allowing the system to make BubbleMetadata for a notification

- Add experiment class to try and keep experimented related code in
- If notification has info to bubble & is allowed, make BubbleMetadata for
  it (but don't actually bubble)
- Options for only allowing messages to bubble
- Alter NotificationEntry to have settable BubbleMetadata
- Add notion of user created bubbles & user promoting / demoting content
  to a bubble; user created bubbles can't be canceled or finished, the
  user must explictly dismiss the notif
- Move all of our Dependency.get into constructor & use inject

Test: it compiles (see manual tests in CL with the menu affordance)
Test: atest BubbleControllerTest
Bug: 138116133
Bug: 143173197
Change-Id: I6100bce7b74146afd4e0c4c02d4ce7731fecd5af
parent 3ad8b97d
Loading
Loading
Loading
Loading
+19 −7
Original line number Diff line number Diff line
@@ -64,7 +64,8 @@ class Bubble {

    private long mLastUpdated;
    private long mLastAccessed;
    private boolean mIsRemoved;

    private boolean mIsUserCreated;

    /**
     * Whether this notification should be shown in the shade when it is also displayed as a bubble.
@@ -74,9 +75,7 @@ class Bubble {
     */
    private boolean mShowInShadeWhenBubble = true;

    /**
     * Whether the bubble should show a dot for the notification indicating updated content.
     */
    /** Whether the bubble should show a dot for the notification indicating updated content. */
    private boolean mShowBubbleUpdateDot = true;

    /** Whether flyout text should be suppressed, regardless of any other flags or state. */
@@ -294,6 +293,20 @@ class Bubble {
        return (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
    }

    /**
     * Whether this bubble was explicitly created by the user via a SysUI affordance.
     */
    boolean isUserCreated() {
        return mIsUserCreated;
    }

    /**
     * Set whether this bubble was explicitly created by the user via a SysUI affordance.
     */
    void setUserCreated(boolean isUserCreated) {
        mIsUserCreated = isUserCreated;
    }

    float getDesiredHeight(Context context) {
        Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
        boolean useRes = data.getDesiredHeightResId() != 0;
@@ -319,9 +332,8 @@ class Bubble {

    @Nullable
    PendingIntent getBubbleIntent(Context context) {
        Notification notif = mEntry.getSbn().getNotification();
        Notification.BubbleMetadata data = notif.getBubbleMetadata();
        if (BubbleController.canLaunchInActivityView(context, mEntry) && data != null) {
        Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
        if (data != null) {
            return data.getIntent();
        }
        return null;
+75 −19
Original line number Diff line number Diff line
@@ -67,7 +67,6 @@ import androidx.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -82,6 +81,7 @@ import com.android.systemui.statusbar.notification.NotificationInterruptionState
import com.android.systemui.statusbar.notification.collection.NotificationData;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.ShadeController;
import com.android.systemui.statusbar.phone.StatusBarWindowController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ZenModeController;
@@ -96,6 +96,8 @@ import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;

import dagger.Lazy;

/**
 * Bubbles are a special type of content that can "float" on top of other apps or System UI.
 * Bubbles can be expanded to show more content.
@@ -132,6 +134,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
    private BubbleExpandListener mExpandListener;
    @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
    private final NotificationGroupManager mNotificationGroupManager;
    private final Lazy<ShadeController> mShadeController;

    private BubbleData mBubbleData;
    @Nullable private BubbleStackView mStackView;
@@ -206,24 +209,34 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
    }

    @Inject
    public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
            BubbleData data, ConfigurationController configurationController,
    public BubbleController(Context context,
            StatusBarWindowController statusBarWindowController,
            StatusBarStateController statusBarStateController,
            Lazy<ShadeController> shadeController,
            BubbleData data,
            ConfigurationController configurationController,
            NotificationInterruptionStateProvider interruptionStateProvider,
            ZenModeController zenModeController,
            NotificationLockscreenUserManager notifUserManager,
            NotificationGroupManager groupManager) {
        this(context, statusBarWindowController, data, null /* synchronizer */,
                configurationController, interruptionStateProvider, zenModeController,
                notifUserManager, groupManager);
    }

    public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
            BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
            NotificationGroupManager groupManager,
            NotificationEntryManager entryManager) {
        this(context, statusBarWindowController, statusBarStateController, shadeController,
                data, null /* synchronizer */, configurationController, interruptionStateProvider,
                zenModeController, notifUserManager, groupManager, entryManager);
    }

    public BubbleController(Context context,
            StatusBarWindowController statusBarWindowController,
            StatusBarStateController statusBarStateController,
            Lazy<ShadeController> shadeController,
            BubbleData data,
            @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
            ConfigurationController configurationController,
            NotificationInterruptionStateProvider interruptionStateProvider,
            ZenModeController zenModeController,
            NotificationLockscreenUserManager notifUserManager,
            NotificationGroupManager groupManager) {
            NotificationGroupManager groupManager,
            NotificationEntryManager entryManager) {
        mContext = context;
        mNotificationInterruptionStateProvider = interruptionStateProvider;
        mNotifUserManager = notifUserManager;
@@ -249,7 +262,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
        mBubbleData = data;
        mBubbleData.setListener(mBubbleDataListener);

        mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
        mNotificationEntryManager = entryManager;
        mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
        mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
        mNotificationGroupManager = groupManager;
@@ -271,9 +284,10 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
                    }
                });

        mShadeController = shadeController;
        mStatusBarWindowController = statusBarWindowController;
        mStatusBarStateListener = new StatusBarStateListener();
        Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener);
        statusBarStateController.addCallback(mStatusBarStateListener);

        mTaskStackListener = new BubbleTaskStackListener();
        ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
@@ -497,15 +511,45 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
     * @param notif the notification associated with this bubble.
     */
    void updateBubble(NotificationEntry notif) {
        updateBubble(notif, /* supressFlyout */ false);
        updateBubble(notif, false /* suppressFlyout */);
    }

    void updateBubble(NotificationEntry notif, boolean suppressFlyout) {
        updateBubble(notif, suppressFlyout, true /* showInShade */);
    }

    void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) {
        // If this is an interruptive notif, mark that it's interrupted
        if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
            notif.setInterruption();
        }
        mBubbleData.notificationEntryUpdated(notif, suppressFlyout);
        mBubbleData.notificationEntryUpdated(notif, suppressFlyout, showInShade);
    }

    /**
     * Called when a user has indicated that an active notification should be shown as a bubble.
     * <p>
     * This method will collapse the shade, create the bubble without a flyout or dot, and suppress
     * the notification from appearing in the shade.
     *
     * @param entry the notification to show as a bubble.
     */
    public void onUserCreatedBubbleFromNotification(NotificationEntry entry) {
        mShadeController.get().collapsePanel(true);
        entry.setFlagBubble(true);
        updateBubble(entry, true /* suppressFlyout */, false /* showInShade */);
        mBubbleData.getBubbleWithKey(entry.getKey()).setUserCreated(true);
    }

    /**
     * Called when a user has indicated that an active notification appearing as a bubble should
     * no longer be shown as a bubble.
     *
     * @param entry the notification to no longer show as a bubble.
     */
    public void onUserDemotedBubbleFromNotification(NotificationEntry entry) {
        entry.setFlagBubble(false);
        removeBubble(entry.getKey(), DISMISS_BLOCKED);
    }

    /**
@@ -571,7 +615,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
                    mNotificationEntryManager.updateNotifications(
                            "BubbleController.onNotificationRemoveRequested");
                    return true;
                } else if (!userRemovedNotif && entry != null) {
                } else if (!userRemovedNotif && entry != null && !bubble.isUserCreated()) {
                    // This wasn't a user removal so we should remove the bubble as well
                    mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
                    return false;
@@ -631,6 +675,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
    private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
        @Override
        public void onPendingEntryAdded(NotificationEntry entry) {
            Bubble b = mBubbleData.getBubbleWithKey(entry.getKey());
            BubbleExperimentConfig.adjustForExperiments(mContext, entry, b);

            if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
                    && canLaunchInActivityView(mContext, entry)) {
                updateBubble(entry);
@@ -639,13 +686,15 @@ public class BubbleController implements ConfigurationController.ConfigurationLi

        @Override
        public void onPreEntryUpdated(NotificationEntry entry) {
            Bubble b = mBubbleData.getBubbleWithKey(entry.getKey());
            BubbleExperimentConfig.adjustForExperiments(mContext, entry, b);

            boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
                    && canLaunchInActivityView(mContext, entry);
            if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.getKey())) {
                // It was previously a bubble but no longer a bubble -- lets remove it
                removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
            } else if (shouldBubble) {
                Bubble b = mBubbleData.getBubbleWithKey(entry.getKey());
                updateBubble(entry);
            }
        }
@@ -949,19 +998,26 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
        PendingIntent intent = entry.getBubbleMetadata() != null
                ? entry.getBubbleMetadata().getIntent()
                : null;
        return canLaunchIntentInActivityView(context, entry, intent);
    }

    static boolean canLaunchIntentInActivityView(Context context, NotificationEntry entry,
            PendingIntent intent) {
        if (intent == null) {
            Log.w(TAG, "Unable to create bubble -- no intent");
            Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey());
            return false;
        }
        ActivityInfo info =
                intent.getIntent().resolveActivityInfo(context.getPackageManager(), 0);
        if (info == null) {
            Log.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: "
            Log.w(TAG, "Unable to send as bubble, "
                    + entry.getKey() + " couldn't find activity info for intent: "
                    + intent);
            return false;
        }
        if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
            Log.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: "
            Log.w(TAG, "Unable to send as bubble, "
                    + entry.getKey() + " activity is not resizable for intent: "
                    + intent);
            return false;
        }
+3 −2
Original line number Diff line number Diff line
@@ -179,7 +179,8 @@ public class BubbleData {
        dispatchPendingChanges();
    }

    void notificationEntryUpdated(NotificationEntry entry, boolean suppressFlyout) {
    void notificationEntryUpdated(NotificationEntry entry, boolean suppressFlyout,
            boolean showInShade) {
        if (DEBUG_BUBBLE_DATA) {
            Log.d(TAG, "notificationEntryUpdated: " + entry);
        }
@@ -208,7 +209,7 @@ public class BubbleData {
            setSelectedBubbleInternal(bubble);
        }
        boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
        bubble.setShowInShadeWhenBubble(!isBubbleExpandedAndSelected);
        bubble.setShowInShadeWhenBubble(!isBubbleExpandedAndSelected && showInShade);
        bubble.setShowBubbleDot(!isBubbleExpandedAndSelected);
        dispatchPendingChanges();
    }
+6 −4
Original line number Diff line number Diff line
@@ -184,12 +184,14 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
                        + " mActivityViewStatus=" + mActivityViewStatus
                        + " bubble=" + getBubbleKey());
            }
            if (mBubble != null && !mBubble.isUserCreated()) {
                if (mBubble != null) {
                    // Must post because this is called from a binder thread.
                    post(() -> mBubbleController.removeBubble(mBubble.getKey(),
                            BubbleController.DISMISS_TASK_FINISHED));
                }
            }
        }
    };

    public BubbleExpandedView(Context context) {
+98 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.bubbles;

import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.drawable.Icon;
import android.provider.Settings;

import com.android.systemui.statusbar.notification.collection.NotificationEntry;

/**
 * Common class for experiments controlled via secure settings.
 */
public class BubbleExperimentConfig {

    private static final String ALLOW_ANY_NOTIF_TO_BUBBLE = "allow_any_notif_to_bubble";
    private static final boolean ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT = false;

    private static final String ALLOW_MESSAGE_NOTIFS_TO_BUBBLE = "allow_message_notifs_to_bubble";
    private static final boolean ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT = false;

    /**
     * When true, if a notification has the information necessary to bubble (i.e. valid
     * contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata}
     * object will be created by the system and added to the notification.
     *
     * This does not produce a bubble, only adds the metadata. It should be used in conjunction
     * with {@see #allowNotifBubbleMenu} which shows an affordance to bubble notification content.
     */
    static boolean allowAnyNotifToBubble(Context context) {
        return Settings.Secure.getInt(context.getContentResolver(),
                ALLOW_ANY_NOTIF_TO_BUBBLE,
                ALLOW_ANY_NOTIF_TO_BUBBLE_DEFAULT ? 1 : 0) != 0;
    }

    /**
     * Same as {@link #allowAnyNotifToBubble(Context)} except it filters for notifications that
     * are using {@link Notification.MessagingStyle} and have remote input.
     */
    static boolean allowMessageNotifsToBubble(Context context) {
        return Settings.Secure.getInt(context.getContentResolver(),
                ALLOW_MESSAGE_NOTIFS_TO_BUBBLE,
                ALLOW_MESSAGE_NOTIFS_TO_BUBBLE_DEFAULT ? 1 : 0) != 0;
    }

    /**
     * If {@link #allowAnyNotifToBubble(Context)} is true, this method creates and adds
     * {@link android.app.Notification.BubbleMetadata} to the notification entry as long as
     * the notification has necessary info for BubbleMetadata.
     */
    static void adjustForExperiments(Context context, NotificationEntry entry,
            Bubble previousBubble) {
        if (entry.getBubbleMetadata() != null) {
            // Has metadata, nothing to do.
            return;
        }

        Notification notification = entry.getSbn().getNotification();
        boolean isMessage = Notification.MessagingStyle.class.equals(
                notification.getNotificationStyle());
        boolean bubbleNotifForExperiment = (isMessage && allowMessageNotifsToBubble(context))
                || allowAnyNotifToBubble(context);

        final PendingIntent intent = notification.contentIntent;
        if (bubbleNotifForExperiment
                && BubbleController.canLaunchIntentInActivityView(context, entry, intent)) {
            final Icon smallIcon = entry.getSbn().getNotification().getSmallIcon();
            Notification.BubbleMetadata.Builder metadata =
                    new Notification.BubbleMetadata.Builder()
                            .setDesiredHeight(10000)
                            .setIcon(smallIcon)
                            .setIntent(intent);
            entry.setBubbleMetadata(metadata.build());
        }

        if (previousBubble != null) {
            // Update to a previously user-created bubble, set its flag now so the update goes
            // to the bubble.
            entry.setFlagBubble(true);
        }
    }
}
Loading