blob: 53862fb91cf4fa7fee2f0f69c5f0e721e3ae4bc8 [file] [log] [blame]
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.bubbles;
18
19import static android.view.View.GONE;
20import static android.view.View.VISIBLE;
21import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
22
23import static com.android.systemui.bubbles.BubbleMovementHelper.EDGE_OVERLAP;
24
Mady Mellor5549dd22018-11-06 18:07:34 -080025import android.app.Notification;
26import android.app.NotificationManager;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080027import android.content.Context;
28import android.graphics.Point;
Mady Mellor5549dd22018-11-06 18:07:34 -080029import android.service.notification.StatusBarNotification;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080030import android.view.ViewGroup;
31import android.view.WindowManager;
32import android.widget.FrameLayout;
33
34import com.android.systemui.Dependency;
35import com.android.systemui.R;
36import com.android.systemui.statusbar.notification.NotificationData;
37import com.android.systemui.statusbar.phone.StatusBarWindowController;
38
Mady Mellor5549dd22018-11-06 18:07:34 -080039import java.util.ArrayList;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080040import java.util.HashMap;
41import java.util.Map;
42
43/**
44 * Bubbles are a special type of content that can "float" on top of other apps or System UI.
45 * Bubbles can be expanded to show more content.
46 *
47 * The controller manages addition, removal, and visible state of bubbles on screen.
48 */
49public class BubbleController {
50 private static final int MAX_BUBBLES = 5; // TODO: actually enforce this
51
52 private static final String TAG = "BubbleController";
53
Mady Mellor5549dd22018-11-06 18:07:34 -080054 // Enables some subset of notifs to automatically become bubbles
55 public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
56 // When a bubble is dismissed, recreate it as a notification
57 public static final boolean DEBUG_DEMOTE_TO_NOTIF = false;
58
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080059 private Context mContext;
Mady Mellor5549dd22018-11-06 18:07:34 -080060 private BubbleDismissListener mDismissListener;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080061
62 private Map<String, BubbleView> mBubbles = new HashMap<>();
63 private BubbleStackView mStackView;
64 private Point mDisplaySize;
65
66 // Bubbles get added to the status bar view
67 private StatusBarWindowController mStatusBarWindowController;
68
Mady Mellor5549dd22018-11-06 18:07:34 -080069 /**
70 * Listener to find out about bubble / bubble stack dismissal events.
71 */
72 public interface BubbleDismissListener {
73 /**
74 * Called when the entire stack of bubbles is dismissed by the user.
75 */
76 void onStackDismissed();
77
78 /**
79 * Called when a specific bubble is dismissed by the user.
80 */
81 void onBubbleDismissed(String key);
82 }
83
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080084 public BubbleController(Context context) {
85 mContext = context;
86 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
87 mDisplaySize = new Point();
88 wm.getDefaultDisplay().getSize(mDisplaySize);
89 mStatusBarWindowController = Dependency.get(StatusBarWindowController.class);
90 }
91
92 /**
Mady Mellor5549dd22018-11-06 18:07:34 -080093 * Set a listener to be notified of bubble dismissal events.
94 */
95 public void setDismissListener(BubbleDismissListener listener) {
96 mDismissListener = listener;
97 }
98
99 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800100 * Whether or not there are bubbles present, regardless of them being visible on the
101 * screen (e.g. if on AOD).
102 */
103 public boolean hasBubbles() {
104 return mBubbles.size() > 0;
105 }
106
107 /**
108 * Whether the stack of bubbles is expanded or not.
109 */
110 public boolean isStackExpanded() {
111 return mStackView != null && mStackView.isExpanded();
112 }
113
114 /**
115 * Tell the stack of bubbles to collapse.
116 */
117 public void collapseStack() {
118 if (mStackView != null) {
119 mStackView.animateExpansion(false);
120 }
121 }
122
123 /**
124 * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
125 */
126 public void dismissStack() {
127 mStackView.setVisibility(GONE);
128 Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
129 // Reset the position of the stack (TODO - or should we save / respect last user position?)
130 mStackView.setPosition(startPoint.x, startPoint.y);
131 for (String key: mBubbles.keySet()) {
132 removeBubble(key);
133 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800134 if (mDismissListener != null) {
135 mDismissListener.onStackDismissed();
136 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800137 }
138
139 /**
140 * Adds a bubble associated with the provided notification entry or updates it if it exists.
141 */
142 public void addBubble(NotificationData.Entry notif) {
143 if (mBubbles.containsKey(notif.key)) {
144 // It's an update
145 BubbleView bubble = mBubbles.get(notif.key);
146 mStackView.updateBubble(bubble, notif);
147 } else {
148 // It's new
149 BubbleView bubble = new BubbleView(mContext);
150 bubble.setNotif(notif);
151 mBubbles.put(bubble.getKey(), bubble);
152
153 boolean setPosition = false;
154 if (mStackView == null) {
155 setPosition = true;
156 mStackView = new BubbleStackView(mContext);
157 ViewGroup sbv = (ViewGroup) mStatusBarWindowController.getStatusBarView();
158 // XXX: Bug when you expand the shade on top of expanded bubble, there is no scrim
159 // between bubble and the shade
160 int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1;
161 sbv.addView(mStackView, bubblePosition,
162 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
163 }
164 mStackView.setVisibility(VISIBLE);
165 mStackView.addBubble(bubble);
166
167 if (setPosition) {
168 // Need to add the bubble to the stack before we can know the width
169 Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
170 mStackView.setPosition(startPoint.x, startPoint.y);
171 }
172 }
173 }
174
175 /**
176 * Removes the bubble associated with the {@param uri}.
177 */
178 public void removeBubble(String key) {
179 BubbleView bv = mBubbles.get(key);
180 if (bv != null) {
181 mStackView.removeBubble(bv);
182 bv.getEntry().setBubbleDismissed(true);
183 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800184 if (mDismissListener != null) {
185 mDismissListener.onBubbleDismissed(key);
186 }
187 }
188
189 /**
190 * Sets the visibility of the bubbles, doesn't un-bubble them, just changes visibility.
191 */
192 public void updateVisibility(boolean visible) {
193 if (mStackView == null) {
194 return;
195 }
196 ArrayList<BubbleView> viewsToRemove = new ArrayList<>();
197 for (BubbleView bv : mBubbles.values()) {
198 NotificationData.Entry entry = bv.getEntry();
199 if (entry != null) {
200 if (entry.row.isRemoved() || entry.isBubbleDismissed() || entry.row.isDismissed()) {
201 viewsToRemove.add(bv);
202 }
203 }
204 }
205 for (BubbleView view : viewsToRemove) {
206 mBubbles.remove(view.getKey());
207 mStackView.removeBubble(view);
208 if (mBubbles.size() == 0) {
209 ((ViewGroup) mStatusBarWindowController.getStatusBarView()).removeView(mStackView);
210 mStackView = null;
211 }
212 }
213 if (mStackView != null) {
214 mStackView.setVisibility(visible ? VISIBLE : GONE);
215 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800216 }
217
218 // TODO: factor in PIP location / maybe last place user had it
219 /**
220 * Gets an appropriate starting point to position the bubble stack.
221 */
222 public static Point getStartPoint(int size, Point displaySize) {
223 final int x = displaySize.x - size + EDGE_OVERLAP;
224 final int y = displaySize.y / 4;
225 return new Point(x, y);
226 }
227
228 /**
229 * Gets an appropriate position for the bubble when the stack is expanded.
230 */
231 public static Point getExpandPoint(BubbleStackView view, int size, Point displaySize) {
232 // Same place for now..
233 return new Point(EDGE_OVERLAP, size);
234 }
235
Mady Mellor5549dd22018-11-06 18:07:34 -0800236 /**
237 * Whether the notification should bubble or not.
238 */
239 public static boolean shouldAutoBubble(NotificationData.Entry entry, int priority,
240 boolean canAppOverlay) {
241 if (!DEBUG_ENABLE_AUTO_BUBBLE || entry.isBubbleDismissed()) {
242 return false;
243 }
244 StatusBarNotification n = entry.notification;
245 boolean hasRemoteInput = false;
246 if (n.getNotification().actions != null) {
247 for (Notification.Action action : n.getNotification().actions) {
248 if (action.getRemoteInputs() != null) {
249 hasRemoteInput = true;
250 break;
251 }
252 }
253 }
254 Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle();
255 boolean shouldBubble = priority >= NotificationManager.IMPORTANCE_HIGH
256 || Notification.MessagingStyle.class.equals(style)
257 || Notification.CATEGORY_MESSAGE.equals(n.getNotification().category)
258 || hasRemoteInput
259 || canAppOverlay;
260 return shouldBubble && !entry.isBubbleDismissed();
261 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800262}