blob: 5c259d5c40938ef4340480d9ad3940909e1e8599 [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
Mady Mellord1c78b262018-11-06 18:04:40 -080019import static android.view.View.INVISIBLE;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080020import 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;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080026import android.content.Context;
27import android.graphics.Point;
Mady Mellord1c78b262018-11-06 18:04:40 -080028import android.graphics.Rect;
Mady Mellorceced172018-11-27 11:18:39 -080029import android.provider.Settings;
Mady Mellor5549dd22018-11-06 18:07:34 -080030import android.service.notification.StatusBarNotification;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080031import android.view.ViewGroup;
32import android.view.WindowManager;
33import android.widget.FrameLayout;
34
35import com.android.systemui.Dependency;
36import com.android.systemui.R;
37import com.android.systemui.statusbar.notification.NotificationData;
38import com.android.systemui.statusbar.phone.StatusBarWindowController;
39
Mady Mellor5549dd22018-11-06 18:07:34 -080040import java.util.ArrayList;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080041import java.util.HashMap;
42import java.util.Map;
43
44/**
45 * Bubbles are a special type of content that can "float" on top of other apps or System UI.
46 * Bubbles can be expanded to show more content.
47 *
48 * The controller manages addition, removal, and visible state of bubbles on screen.
49 */
50public class BubbleController {
51 private static final int MAX_BUBBLES = 5; // TODO: actually enforce this
52
53 private static final String TAG = "BubbleController";
54
Mady Mellor5549dd22018-11-06 18:07:34 -080055 // Enables some subset of notifs to automatically become bubbles
56 public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
57 // When a bubble is dismissed, recreate it as a notification
58 public static final boolean DEBUG_DEMOTE_TO_NOTIF = false;
59
Mady Mellorceced172018-11-27 11:18:39 -080060 // Secure settings
61 private static final String ENABLE_AUTO_BUBBLE_MESSAGES = "experiment_autobubble_messaging";
62 private static final String ENABLE_AUTO_BUBBLE_ONGOING = "experiment_autobubble_ongoing";
63 private static final String ENABLE_AUTO_BUBBLE_ALL = "experiment_autobubble_all";
64
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080065 private Context mContext;
Mady Mellor5549dd22018-11-06 18:07:34 -080066 private BubbleDismissListener mDismissListener;
Mady Mellord1c78b262018-11-06 18:04:40 -080067 private BubbleStateChangeListener mStateChangeListener;
Mady Mellorcd9b1302018-11-06 18:08:04 -080068 private BubbleExpandListener mExpandListener;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080069
70 private Map<String, BubbleView> mBubbles = new HashMap<>();
71 private BubbleStackView mStackView;
72 private Point mDisplaySize;
73
74 // Bubbles get added to the status bar view
75 private StatusBarWindowController mStatusBarWindowController;
76
Mady Mellord1c78b262018-11-06 18:04:40 -080077 // Used for determining view rect for touch interaction
78 private Rect mTempRect = new Rect();
79
Mady Mellor5549dd22018-11-06 18:07:34 -080080 /**
81 * Listener to find out about bubble / bubble stack dismissal events.
82 */
83 public interface BubbleDismissListener {
84 /**
85 * Called when the entire stack of bubbles is dismissed by the user.
86 */
87 void onStackDismissed();
88
89 /**
90 * Called when a specific bubble is dismissed by the user.
91 */
92 void onBubbleDismissed(String key);
93 }
94
Mady Mellord1c78b262018-11-06 18:04:40 -080095 /**
96 * Listener to be notified when some states of the bubbles change.
97 */
98 public interface BubbleStateChangeListener {
99 /**
100 * Called when the stack has bubbles or no longer has bubbles.
101 */
102 void onHasBubblesChanged(boolean hasBubbles);
103 }
104
Mady Mellorcd9b1302018-11-06 18:08:04 -0800105 /**
106 * Listener to find out about stack expansion / collapse events.
107 */
108 public interface BubbleExpandListener {
109 /**
110 * Called when the expansion state of the bubble stack changes.
111 *
112 * @param isExpanding whether it's expanding or collapsing
113 * @param amount fraction of how expanded or collapsed it is, 1 being fully, 0 at the start
114 */
115 void onBubbleExpandChanged(boolean isExpanding, float amount);
116 }
117
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800118 public BubbleController(Context context) {
119 mContext = context;
120 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
121 mDisplaySize = new Point();
122 wm.getDefaultDisplay().getSize(mDisplaySize);
123 mStatusBarWindowController = Dependency.get(StatusBarWindowController.class);
124 }
125
126 /**
Mady Mellor5549dd22018-11-06 18:07:34 -0800127 * Set a listener to be notified of bubble dismissal events.
128 */
129 public void setDismissListener(BubbleDismissListener listener) {
130 mDismissListener = listener;
131 }
132
133 /**
Mady Mellord1c78b262018-11-06 18:04:40 -0800134 * Set a listener to be notified when some states of the bubbles change.
135 */
136 public void setBubbleStateChangeListener(BubbleStateChangeListener listener) {
137 mStateChangeListener = listener;
138 }
139
140 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -0800141 * Set a listener to be notified of bubble expand events.
142 */
143 public void setExpandListener(BubbleExpandListener listener) {
144 mExpandListener = listener;
145 if (mStackView != null) {
146 mStackView.setExpandListener(mExpandListener);
147 }
148 }
149
150 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800151 * Whether or not there are bubbles present, regardless of them being visible on the
152 * screen (e.g. if on AOD).
153 */
154 public boolean hasBubbles() {
155 return mBubbles.size() > 0;
156 }
157
158 /**
159 * Whether the stack of bubbles is expanded or not.
160 */
161 public boolean isStackExpanded() {
162 return mStackView != null && mStackView.isExpanded();
163 }
164
165 /**
166 * Tell the stack of bubbles to collapse.
167 */
168 public void collapseStack() {
169 if (mStackView != null) {
170 mStackView.animateExpansion(false);
171 }
172 }
173
174 /**
175 * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
176 */
177 public void dismissStack() {
Mady Mellord1c78b262018-11-06 18:04:40 -0800178 if (mStackView == null) {
179 return;
180 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800181 Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
182 // Reset the position of the stack (TODO - or should we save / respect last user position?)
183 mStackView.setPosition(startPoint.x, startPoint.y);
184 for (String key: mBubbles.keySet()) {
185 removeBubble(key);
186 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800187 if (mDismissListener != null) {
188 mDismissListener.onStackDismissed();
189 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800190 updateBubblesShowing();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800191 }
192
193 /**
194 * Adds a bubble associated with the provided notification entry or updates it if it exists.
195 */
196 public void addBubble(NotificationData.Entry notif) {
197 if (mBubbles.containsKey(notif.key)) {
198 // It's an update
199 BubbleView bubble = mBubbles.get(notif.key);
200 mStackView.updateBubble(bubble, notif);
201 } else {
202 // It's new
203 BubbleView bubble = new BubbleView(mContext);
204 bubble.setNotif(notif);
205 mBubbles.put(bubble.getKey(), bubble);
206
Mady Mellord1c78b262018-11-06 18:04:40 -0800207 boolean setPosition = mStackView != null && mStackView.getVisibility() != VISIBLE;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800208 if (mStackView == null) {
209 setPosition = true;
210 mStackView = new BubbleStackView(mContext);
Mady Mellord1c78b262018-11-06 18:04:40 -0800211 ViewGroup sbv = mStatusBarWindowController.getStatusBarView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800212 // XXX: Bug when you expand the shade on top of expanded bubble, there is no scrim
213 // between bubble and the shade
214 int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1;
215 sbv.addView(mStackView, bubblePosition,
216 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellorcd9b1302018-11-06 18:08:04 -0800217 if (mExpandListener != null) {
218 mStackView.setExpandListener(mExpandListener);
219 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800220 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800221 mStackView.addBubble(bubble);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800222 if (setPosition) {
223 // Need to add the bubble to the stack before we can know the width
224 Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
225 mStackView.setPosition(startPoint.x, startPoint.y);
Mady Mellord1c78b262018-11-06 18:04:40 -0800226 mStackView.setVisibility(VISIBLE);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800227 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800228 updateBubblesShowing();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800229 }
230 }
231
232 /**
233 * Removes the bubble associated with the {@param uri}.
234 */
235 public void removeBubble(String key) {
236 BubbleView bv = mBubbles.get(key);
Mady Mellord1c78b262018-11-06 18:04:40 -0800237 if (mStackView != null && bv != null) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800238 mStackView.removeBubble(bv);
239 bv.getEntry().setBubbleDismissed(true);
240 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800241 if (mDismissListener != null) {
242 mDismissListener.onBubbleDismissed(key);
243 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800244 updateBubblesShowing();
245 }
246
247 private void updateBubblesShowing() {
248 boolean hasBubblesShowing = false;
249 for (BubbleView bv : mBubbles.values()) {
250 if (!bv.getEntry().isBubbleDismissed()) {
251 hasBubblesShowing = true;
252 break;
253 }
254 }
255 boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
256 mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
257 if (mStackView != null && !hasBubblesShowing) {
258 mStackView.setVisibility(INVISIBLE);
259 }
260 if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
261 mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
262 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800263 }
264
265 /**
266 * Sets the visibility of the bubbles, doesn't un-bubble them, just changes visibility.
267 */
268 public void updateVisibility(boolean visible) {
269 if (mStackView == null) {
270 return;
271 }
272 ArrayList<BubbleView> viewsToRemove = new ArrayList<>();
273 for (BubbleView bv : mBubbles.values()) {
274 NotificationData.Entry entry = bv.getEntry();
275 if (entry != null) {
Evan Laird94492852018-10-25 13:43:01 -0400276 if (entry.isRowRemoved() || entry.isBubbleDismissed() || entry.isRowDismissed()) {
Mady Mellor5549dd22018-11-06 18:07:34 -0800277 viewsToRemove.add(bv);
278 }
279 }
280 }
281 for (BubbleView view : viewsToRemove) {
282 mBubbles.remove(view.getKey());
283 mStackView.removeBubble(view);
Mady Mellor5549dd22018-11-06 18:07:34 -0800284 }
285 if (mStackView != null) {
Mady Mellord1c78b262018-11-06 18:04:40 -0800286 mStackView.setVisibility(visible ? VISIBLE : INVISIBLE);
287 if (!visible) {
288 collapseStack();
289 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800290 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800291 updateBubblesShowing();
292 }
293
294 /**
295 * Rect indicating the touchable region for the bubble stack / expanded stack.
296 */
297 public Rect getTouchableRegion() {
298 if (mStackView == null || mStackView.getVisibility() != VISIBLE) {
299 return null;
300 }
301 mStackView.getBoundsOnScreen(mTempRect);
302 return mTempRect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800303 }
304
305 // TODO: factor in PIP location / maybe last place user had it
306 /**
307 * Gets an appropriate starting point to position the bubble stack.
308 */
309 public static Point getStartPoint(int size, Point displaySize) {
310 final int x = displaySize.x - size + EDGE_OVERLAP;
311 final int y = displaySize.y / 4;
312 return new Point(x, y);
313 }
314
315 /**
316 * Gets an appropriate position for the bubble when the stack is expanded.
317 */
318 public static Point getExpandPoint(BubbleStackView view, int size, Point displaySize) {
319 // Same place for now..
320 return new Point(EDGE_OVERLAP, size);
321 }
322
Mady Mellor5549dd22018-11-06 18:07:34 -0800323 /**
324 * Whether the notification should bubble or not.
325 */
Mady Mellorceced172018-11-27 11:18:39 -0800326 public static boolean shouldAutoBubble(Context context, NotificationData.Entry entry) {
327 if (entry.isBubbleDismissed()) {
Mady Mellor5549dd22018-11-06 18:07:34 -0800328 return false;
329 }
Mady Mellorceced172018-11-27 11:18:39 -0800330
331 boolean autoBubbleMessages = shouldAutoBubbleMessages(context) || DEBUG_ENABLE_AUTO_BUBBLE;
332 boolean autoBubbleOngoing = shouldAutoBubbleOngoing(context) || DEBUG_ENABLE_AUTO_BUBBLE;
333 boolean autoBubbleAll = shouldAutoBubbleAll(context) || DEBUG_ENABLE_AUTO_BUBBLE;
334
Mady Mellor5549dd22018-11-06 18:07:34 -0800335 StatusBarNotification n = entry.notification;
336 boolean hasRemoteInput = false;
337 if (n.getNotification().actions != null) {
338 for (Notification.Action action : n.getNotification().actions) {
339 if (action.getRemoteInputs() != null) {
340 hasRemoteInput = true;
341 break;
342 }
343 }
344 }
Mady Mellor711f9562018-12-05 14:53:46 -0800345 boolean isCall = Notification.CATEGORY_CALL.equals(n.getNotification().category)
346 && n.isOngoing();
347 boolean isMusic = n.getNotification().hasMediaSession();
348 boolean isImportantOngoing = isMusic || isCall;
Mady Mellorceced172018-11-27 11:18:39 -0800349
Mady Mellor5549dd22018-11-06 18:07:34 -0800350 Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle();
Mady Mellore3175372018-12-04 17:05:11 -0800351 boolean isMessageType = Notification.CATEGORY_MESSAGE.equals(n.getNotification().category);
352 boolean isMessageStyle = Notification.MessagingStyle.class.equals(style);
353 return (((isMessageType && hasRemoteInput) || isMessageStyle) && autoBubbleMessages)
Mady Mellor711f9562018-12-05 14:53:46 -0800354 || (isImportantOngoing && autoBubbleOngoing)
Mady Mellorceced172018-11-27 11:18:39 -0800355 || autoBubbleAll;
356 }
357
358 private static boolean shouldAutoBubbleMessages(Context context) {
359 return Settings.Secure.getInt(context.getContentResolver(),
360 ENABLE_AUTO_BUBBLE_MESSAGES, 0) != 0;
361 }
362
363 private static boolean shouldAutoBubbleOngoing(Context context) {
364 return Settings.Secure.getInt(context.getContentResolver(),
365 ENABLE_AUTO_BUBBLE_ONGOING, 0) != 0;
366 }
367
368 private static boolean shouldAutoBubbleAll(Context context) {
369 return Settings.Secure.getInt(context.getContentResolver(),
370 ENABLE_AUTO_BUBBLE_ALL, 0) != 0;
Mady Mellor5549dd22018-11-06 18:07:34 -0800371 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800372}