blob: 644723321103deabb503cbca0f3c111c666139ad [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
Mady Mellorebdbbb92018-11-15 14:36:48 -080035import com.android.internal.annotations.VisibleForTesting;
Ned Burns01e38212019-01-03 16:32:52 -050036import com.android.systemui.Dependency;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080037import com.android.systemui.R;
38import com.android.systemui.statusbar.notification.NotificationData;
Ned Burns01e38212019-01-03 16:32:52 -050039import com.android.systemui.statusbar.notification.NotificationEntryListener;
40import com.android.systemui.statusbar.notification.NotificationEntryManager;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080041import com.android.systemui.statusbar.phone.StatusBarWindowController;
42
Mady Mellor5549dd22018-11-06 18:07:34 -080043import java.util.ArrayList;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080044import java.util.HashMap;
45import java.util.Map;
46
Jason Monk27d01a622018-12-10 15:57:09 -050047import javax.inject.Inject;
48import javax.inject.Singleton;
49
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080050/**
51 * Bubbles are a special type of content that can "float" on top of other apps or System UI.
52 * Bubbles can be expanded to show more content.
53 *
54 * The controller manages addition, removal, and visible state of bubbles on screen.
55 */
Jason Monk27d01a622018-12-10 15:57:09 -050056@Singleton
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080057public class BubbleController {
58 private static final int MAX_BUBBLES = 5; // TODO: actually enforce this
59
60 private static final String TAG = "BubbleController";
61
Mady Mellor5549dd22018-11-06 18:07:34 -080062 // Enables some subset of notifs to automatically become bubbles
Ned Burns01e38212019-01-03 16:32:52 -050063 private static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
Mady Mellor5549dd22018-11-06 18:07:34 -080064 // When a bubble is dismissed, recreate it as a notification
Ned Burns01e38212019-01-03 16:32:52 -050065 private static final boolean DEBUG_DEMOTE_TO_NOTIF = false;
Mady Mellor5549dd22018-11-06 18:07:34 -080066
Mady Mellorceced172018-11-27 11:18:39 -080067 // Secure settings
68 private static final String ENABLE_AUTO_BUBBLE_MESSAGES = "experiment_autobubble_messaging";
69 private static final String ENABLE_AUTO_BUBBLE_ONGOING = "experiment_autobubble_ongoing";
70 private static final String ENABLE_AUTO_BUBBLE_ALL = "experiment_autobubble_all";
71
Ned Burns01e38212019-01-03 16:32:52 -050072 private final Context mContext;
73 private final NotificationEntryManager mNotificationEntryManager;
Mady Mellord1c78b262018-11-06 18:04:40 -080074 private BubbleStateChangeListener mStateChangeListener;
Mady Mellorcd9b1302018-11-06 18:08:04 -080075 private BubbleExpandListener mExpandListener;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080076
Ned Burns01e38212019-01-03 16:32:52 -050077 private final Map<String, BubbleView> mBubbles = new HashMap<>();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080078 private BubbleStackView mStackView;
Ned Burns01e38212019-01-03 16:32:52 -050079 private final Point mDisplaySize;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080080
81 // Bubbles get added to the status bar view
Ned Burns01e38212019-01-03 16:32:52 -050082 private final StatusBarWindowController mStatusBarWindowController;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080083
Mady Mellord1c78b262018-11-06 18:04:40 -080084 // Used for determining view rect for touch interaction
85 private Rect mTempRect = new Rect();
86
Mady Mellor5549dd22018-11-06 18:07:34 -080087 /**
Mady Mellord1c78b262018-11-06 18:04:40 -080088 * Listener to be notified when some states of the bubbles change.
89 */
90 public interface BubbleStateChangeListener {
91 /**
92 * Called when the stack has bubbles or no longer has bubbles.
93 */
94 void onHasBubblesChanged(boolean hasBubbles);
95 }
96
Mady Mellorcd9b1302018-11-06 18:08:04 -080097 /**
98 * Listener to find out about stack expansion / collapse events.
99 */
100 public interface BubbleExpandListener {
101 /**
102 * Called when the expansion state of the bubble stack changes.
103 *
104 * @param isExpanding whether it's expanding or collapsing
105 * @param amount fraction of how expanded or collapsed it is, 1 being fully, 0 at the start
106 */
107 void onBubbleExpandChanged(boolean isExpanding, float amount);
108 }
109
Jason Monk27d01a622018-12-10 15:57:09 -0500110 @Inject
Jason Monk92d5c242018-12-21 14:37:34 -0500111 public BubbleController(Context context, StatusBarWindowController statusBarWindowController) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800112 mContext = context;
Ned Burns01e38212019-01-03 16:32:52 -0500113 mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800114 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
115 mDisplaySize = new Point();
116 wm.getDefaultDisplay().getSize(mDisplaySize);
Jason Monk92d5c242018-12-21 14:37:34 -0500117 mStatusBarWindowController = statusBarWindowController;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800118
Ned Burns01e38212019-01-03 16:32:52 -0500119 mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
Mady Mellor5549dd22018-11-06 18:07:34 -0800120 }
121
122 /**
Mady Mellord1c78b262018-11-06 18:04:40 -0800123 * Set a listener to be notified when some states of the bubbles change.
124 */
125 public void setBubbleStateChangeListener(BubbleStateChangeListener listener) {
126 mStateChangeListener = listener;
127 }
128
129 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -0800130 * Set a listener to be notified of bubble expand events.
131 */
132 public void setExpandListener(BubbleExpandListener listener) {
133 mExpandListener = listener;
134 if (mStackView != null) {
135 mStackView.setExpandListener(mExpandListener);
136 }
137 }
138
139 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800140 * Whether or not there are bubbles present, regardless of them being visible on the
141 * screen (e.g. if on AOD).
142 */
143 public boolean hasBubbles() {
144 return mBubbles.size() > 0;
145 }
146
147 /**
148 * Whether the stack of bubbles is expanded or not.
149 */
150 public boolean isStackExpanded() {
151 return mStackView != null && mStackView.isExpanded();
152 }
153
154 /**
155 * Tell the stack of bubbles to collapse.
156 */
157 public void collapseStack() {
158 if (mStackView != null) {
159 mStackView.animateExpansion(false);
160 }
161 }
162
163 /**
164 * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
165 */
Ned Burns01e38212019-01-03 16:32:52 -0500166 void dismissStack() {
Mady Mellord1c78b262018-11-06 18:04:40 -0800167 if (mStackView == null) {
168 return;
169 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800170 Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
171 // Reset the position of the stack (TODO - or should we save / respect last user position?)
172 mStackView.setPosition(startPoint.x, startPoint.y);
173 for (String key: mBubbles.keySet()) {
174 removeBubble(key);
175 }
Ned Burns01e38212019-01-03 16:32:52 -0500176 mNotificationEntryManager.updateNotifications();
Mady Mellord1c78b262018-11-06 18:04:40 -0800177 updateBubblesShowing();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800178 }
179
180 /**
181 * Adds a bubble associated with the provided notification entry or updates it if it exists.
182 */
183 public void addBubble(NotificationData.Entry notif) {
184 if (mBubbles.containsKey(notif.key)) {
185 // It's an update
186 BubbleView bubble = mBubbles.get(notif.key);
187 mStackView.updateBubble(bubble, notif);
188 } else {
189 // It's new
190 BubbleView bubble = new BubbleView(mContext);
191 bubble.setNotif(notif);
192 mBubbles.put(bubble.getKey(), bubble);
193
Mady Mellord1c78b262018-11-06 18:04:40 -0800194 boolean setPosition = mStackView != null && mStackView.getVisibility() != VISIBLE;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800195 if (mStackView == null) {
196 setPosition = true;
197 mStackView = new BubbleStackView(mContext);
Mady Mellord1c78b262018-11-06 18:04:40 -0800198 ViewGroup sbv = mStatusBarWindowController.getStatusBarView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800199 // XXX: Bug when you expand the shade on top of expanded bubble, there is no scrim
200 // between bubble and the shade
201 int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1;
202 sbv.addView(mStackView, bubblePosition,
203 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellorcd9b1302018-11-06 18:08:04 -0800204 if (mExpandListener != null) {
205 mStackView.setExpandListener(mExpandListener);
206 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800207 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800208 mStackView.addBubble(bubble);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800209 if (setPosition) {
210 // Need to add the bubble to the stack before we can know the width
211 Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
212 mStackView.setPosition(startPoint.x, startPoint.y);
Mady Mellord1c78b262018-11-06 18:04:40 -0800213 mStackView.setVisibility(VISIBLE);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800214 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800215 updateBubblesShowing();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800216 }
217 }
218
219 /**
220 * Removes the bubble associated with the {@param uri}.
221 */
Ned Burns01e38212019-01-03 16:32:52 -0500222 void removeBubble(String key) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800223 BubbleView bv = mBubbles.get(key);
Mady Mellord1c78b262018-11-06 18:04:40 -0800224 if (mStackView != null && bv != null) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800225 mStackView.removeBubble(bv);
226 bv.getEntry().setBubbleDismissed(true);
227 }
Ned Burns01e38212019-01-03 16:32:52 -0500228
229 NotificationData.Entry entry = mNotificationEntryManager.getNotificationData().get(key);
230 if (entry != null) {
231 entry.setBubbleDismissed(true);
232 if (!DEBUG_DEMOTE_TO_NOTIF) {
233 mNotificationEntryManager.performRemoveNotification(entry.notification);
234 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800235 }
Ned Burns01e38212019-01-03 16:32:52 -0500236 mNotificationEntryManager.updateNotifications();
237
Mady Mellord1c78b262018-11-06 18:04:40 -0800238 updateBubblesShowing();
239 }
240
Ned Burns01e38212019-01-03 16:32:52 -0500241 @SuppressWarnings("FieldCanBeLocal")
242 private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
243 @Override
244 public void onPendingEntryAdded(NotificationData.Entry entry) {
245 if (shouldAutoBubble(mContext, entry)) {
246 entry.setIsBubble(true);
247 }
248 }
249 };
250
Mady Mellord1c78b262018-11-06 18:04:40 -0800251 private void updateBubblesShowing() {
252 boolean hasBubblesShowing = false;
253 for (BubbleView bv : mBubbles.values()) {
254 if (!bv.getEntry().isBubbleDismissed()) {
255 hasBubblesShowing = true;
256 break;
257 }
258 }
259 boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
260 mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
261 if (mStackView != null && !hasBubblesShowing) {
262 mStackView.setVisibility(INVISIBLE);
263 }
264 if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
265 mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
266 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800267 }
268
269 /**
270 * Sets the visibility of the bubbles, doesn't un-bubble them, just changes visibility.
271 */
272 public void updateVisibility(boolean visible) {
273 if (mStackView == null) {
274 return;
275 }
276 ArrayList<BubbleView> viewsToRemove = new ArrayList<>();
277 for (BubbleView bv : mBubbles.values()) {
278 NotificationData.Entry entry = bv.getEntry();
279 if (entry != null) {
Evan Laird94492852018-10-25 13:43:01 -0400280 if (entry.isRowRemoved() || entry.isBubbleDismissed() || entry.isRowDismissed()) {
Mady Mellor5549dd22018-11-06 18:07:34 -0800281 viewsToRemove.add(bv);
282 }
283 }
284 }
285 for (BubbleView view : viewsToRemove) {
286 mBubbles.remove(view.getKey());
287 mStackView.removeBubble(view);
Mady Mellor5549dd22018-11-06 18:07:34 -0800288 }
289 if (mStackView != null) {
Mady Mellord1c78b262018-11-06 18:04:40 -0800290 mStackView.setVisibility(visible ? VISIBLE : INVISIBLE);
291 if (!visible) {
292 collapseStack();
293 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800294 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800295 updateBubblesShowing();
296 }
297
298 /**
299 * Rect indicating the touchable region for the bubble stack / expanded stack.
300 */
301 public Rect getTouchableRegion() {
302 if (mStackView == null || mStackView.getVisibility() != VISIBLE) {
303 return null;
304 }
305 mStackView.getBoundsOnScreen(mTempRect);
306 return mTempRect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800307 }
308
Mady Mellorebdbbb92018-11-15 14:36:48 -0800309 @VisibleForTesting
Ned Burns01e38212019-01-03 16:32:52 -0500310 BubbleStackView getStackView() {
Mady Mellorebdbbb92018-11-15 14:36:48 -0800311 return mStackView;
312 }
313
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800314 // TODO: factor in PIP location / maybe last place user had it
315 /**
316 * Gets an appropriate starting point to position the bubble stack.
317 */
Ned Burns01e38212019-01-03 16:32:52 -0500318 private static Point getStartPoint(int size, Point displaySize) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800319 final int x = displaySize.x - size + EDGE_OVERLAP;
320 final int y = displaySize.y / 4;
321 return new Point(x, y);
322 }
323
324 /**
325 * Gets an appropriate position for the bubble when the stack is expanded.
326 */
Ned Burns01e38212019-01-03 16:32:52 -0500327 static Point getExpandPoint(BubbleStackView view, int size, Point displaySize) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800328 // Same place for now..
329 return new Point(EDGE_OVERLAP, size);
330 }
331
Mady Mellor5549dd22018-11-06 18:07:34 -0800332 /**
333 * Whether the notification should bubble or not.
334 */
Ned Burns01e38212019-01-03 16:32:52 -0500335 private static boolean shouldAutoBubble(Context context, NotificationData.Entry entry) {
Mady Mellorceced172018-11-27 11:18:39 -0800336 if (entry.isBubbleDismissed()) {
Mady Mellor5549dd22018-11-06 18:07:34 -0800337 return false;
338 }
Mady Mellorceced172018-11-27 11:18:39 -0800339
340 boolean autoBubbleMessages = shouldAutoBubbleMessages(context) || DEBUG_ENABLE_AUTO_BUBBLE;
341 boolean autoBubbleOngoing = shouldAutoBubbleOngoing(context) || DEBUG_ENABLE_AUTO_BUBBLE;
342 boolean autoBubbleAll = shouldAutoBubbleAll(context) || DEBUG_ENABLE_AUTO_BUBBLE;
343
Mady Mellor5549dd22018-11-06 18:07:34 -0800344 StatusBarNotification n = entry.notification;
345 boolean hasRemoteInput = false;
346 if (n.getNotification().actions != null) {
347 for (Notification.Action action : n.getNotification().actions) {
348 if (action.getRemoteInputs() != null) {
349 hasRemoteInput = true;
350 break;
351 }
352 }
353 }
Mady Mellor711f9562018-12-05 14:53:46 -0800354 boolean isCall = Notification.CATEGORY_CALL.equals(n.getNotification().category)
355 && n.isOngoing();
356 boolean isMusic = n.getNotification().hasMediaSession();
357 boolean isImportantOngoing = isMusic || isCall;
Mady Mellorceced172018-11-27 11:18:39 -0800358
Mady Mellor5549dd22018-11-06 18:07:34 -0800359 Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle();
Mady Mellore3175372018-12-04 17:05:11 -0800360 boolean isMessageType = Notification.CATEGORY_MESSAGE.equals(n.getNotification().category);
361 boolean isMessageStyle = Notification.MessagingStyle.class.equals(style);
362 return (((isMessageType && hasRemoteInput) || isMessageStyle) && autoBubbleMessages)
Mady Mellor711f9562018-12-05 14:53:46 -0800363 || (isImportantOngoing && autoBubbleOngoing)
Mady Mellorceced172018-11-27 11:18:39 -0800364 || autoBubbleAll;
365 }
366
367 private static boolean shouldAutoBubbleMessages(Context context) {
368 return Settings.Secure.getInt(context.getContentResolver(),
369 ENABLE_AUTO_BUBBLE_MESSAGES, 0) != 0;
370 }
371
372 private static boolean shouldAutoBubbleOngoing(Context context) {
373 return Settings.Secure.getInt(context.getContentResolver(),
374 ENABLE_AUTO_BUBBLE_ONGOING, 0) != 0;
375 }
376
377 private static boolean shouldAutoBubbleAll(Context context) {
378 return Settings.Secure.getInt(context.getContentResolver(),
379 ENABLE_AUTO_BUBBLE_ALL, 0) != 0;
Mady Mellor5549dd22018-11-06 18:07:34 -0800380 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800381}