blob: 881aa18285ff88a4aab9a39595ff1595039d2405 [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;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080036import 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
Jason Monk27d01a622018-12-10 15:57:09 -050044import javax.inject.Inject;
45import javax.inject.Singleton;
46
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080047/**
48 * Bubbles are a special type of content that can "float" on top of other apps or System UI.
49 * Bubbles can be expanded to show more content.
50 *
51 * The controller manages addition, removal, and visible state of bubbles on screen.
52 */
Jason Monk27d01a622018-12-10 15:57:09 -050053@Singleton
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080054public class BubbleController {
55 private static final int MAX_BUBBLES = 5; // TODO: actually enforce this
56
57 private static final String TAG = "BubbleController";
58
Mady Mellor5549dd22018-11-06 18:07:34 -080059 // Enables some subset of notifs to automatically become bubbles
60 public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
61 // When a bubble is dismissed, recreate it as a notification
62 public static final boolean DEBUG_DEMOTE_TO_NOTIF = false;
63
Mady Mellorceced172018-11-27 11:18:39 -080064 // Secure settings
65 private static final String ENABLE_AUTO_BUBBLE_MESSAGES = "experiment_autobubble_messaging";
66 private static final String ENABLE_AUTO_BUBBLE_ONGOING = "experiment_autobubble_ongoing";
67 private static final String ENABLE_AUTO_BUBBLE_ALL = "experiment_autobubble_all";
68
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080069 private Context mContext;
Mady Mellor5549dd22018-11-06 18:07:34 -080070 private BubbleDismissListener mDismissListener;
Mady Mellord1c78b262018-11-06 18:04:40 -080071 private BubbleStateChangeListener mStateChangeListener;
Mady Mellorcd9b1302018-11-06 18:08:04 -080072 private BubbleExpandListener mExpandListener;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080073
74 private Map<String, BubbleView> mBubbles = new HashMap<>();
75 private BubbleStackView mStackView;
76 private Point mDisplaySize;
77
78 // Bubbles get added to the status bar view
Mady Mellorebdbbb92018-11-15 14:36:48 -080079 @VisibleForTesting
80 protected StatusBarWindowController mStatusBarWindowController;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080081
Mady Mellord1c78b262018-11-06 18:04:40 -080082 // Used for determining view rect for touch interaction
83 private Rect mTempRect = new Rect();
84
Mady Mellor5549dd22018-11-06 18:07:34 -080085 /**
86 * Listener to find out about bubble / bubble stack dismissal events.
87 */
88 public interface BubbleDismissListener {
89 /**
90 * Called when the entire stack of bubbles is dismissed by the user.
91 */
92 void onStackDismissed();
93
94 /**
95 * Called when a specific bubble is dismissed by the user.
96 */
97 void onBubbleDismissed(String key);
98 }
99
Mady Mellord1c78b262018-11-06 18:04:40 -0800100 /**
101 * Listener to be notified when some states of the bubbles change.
102 */
103 public interface BubbleStateChangeListener {
104 /**
105 * Called when the stack has bubbles or no longer has bubbles.
106 */
107 void onHasBubblesChanged(boolean hasBubbles);
108 }
109
Mady Mellorcd9b1302018-11-06 18:08:04 -0800110 /**
111 * Listener to find out about stack expansion / collapse events.
112 */
113 public interface BubbleExpandListener {
114 /**
115 * Called when the expansion state of the bubble stack changes.
116 *
117 * @param isExpanding whether it's expanding or collapsing
118 * @param amount fraction of how expanded or collapsed it is, 1 being fully, 0 at the start
119 */
120 void onBubbleExpandChanged(boolean isExpanding, float amount);
121 }
122
Jason Monk27d01a622018-12-10 15:57:09 -0500123 @Inject
Jason Monk92d5c242018-12-21 14:37:34 -0500124 public BubbleController(Context context, StatusBarWindowController statusBarWindowController) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800125 mContext = context;
126 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
127 mDisplaySize = new Point();
128 wm.getDefaultDisplay().getSize(mDisplaySize);
Jason Monk92d5c242018-12-21 14:37:34 -0500129 mStatusBarWindowController = statusBarWindowController;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800130 }
131
132 /**
Mady Mellor5549dd22018-11-06 18:07:34 -0800133 * Set a listener to be notified of bubble dismissal events.
134 */
135 public void setDismissListener(BubbleDismissListener listener) {
136 mDismissListener = listener;
137 }
138
139 /**
Mady Mellord1c78b262018-11-06 18:04:40 -0800140 * Set a listener to be notified when some states of the bubbles change.
141 */
142 public void setBubbleStateChangeListener(BubbleStateChangeListener listener) {
143 mStateChangeListener = listener;
144 }
145
146 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -0800147 * Set a listener to be notified of bubble expand events.
148 */
149 public void setExpandListener(BubbleExpandListener listener) {
150 mExpandListener = listener;
151 if (mStackView != null) {
152 mStackView.setExpandListener(mExpandListener);
153 }
154 }
155
156 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800157 * Whether or not there are bubbles present, regardless of them being visible on the
158 * screen (e.g. if on AOD).
159 */
160 public boolean hasBubbles() {
161 return mBubbles.size() > 0;
162 }
163
164 /**
165 * Whether the stack of bubbles is expanded or not.
166 */
167 public boolean isStackExpanded() {
168 return mStackView != null && mStackView.isExpanded();
169 }
170
171 /**
172 * Tell the stack of bubbles to collapse.
173 */
174 public void collapseStack() {
175 if (mStackView != null) {
176 mStackView.animateExpansion(false);
177 }
178 }
179
180 /**
181 * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
182 */
183 public void dismissStack() {
Mady Mellord1c78b262018-11-06 18:04:40 -0800184 if (mStackView == null) {
185 return;
186 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800187 Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
188 // Reset the position of the stack (TODO - or should we save / respect last user position?)
189 mStackView.setPosition(startPoint.x, startPoint.y);
190 for (String key: mBubbles.keySet()) {
191 removeBubble(key);
192 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800193 if (mDismissListener != null) {
194 mDismissListener.onStackDismissed();
195 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800196 updateBubblesShowing();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800197 }
198
199 /**
200 * Adds a bubble associated with the provided notification entry or updates it if it exists.
201 */
202 public void addBubble(NotificationData.Entry notif) {
203 if (mBubbles.containsKey(notif.key)) {
204 // It's an update
205 BubbleView bubble = mBubbles.get(notif.key);
206 mStackView.updateBubble(bubble, notif);
207 } else {
208 // It's new
209 BubbleView bubble = new BubbleView(mContext);
210 bubble.setNotif(notif);
211 mBubbles.put(bubble.getKey(), bubble);
212
Mady Mellord1c78b262018-11-06 18:04:40 -0800213 boolean setPosition = mStackView != null && mStackView.getVisibility() != VISIBLE;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800214 if (mStackView == null) {
215 setPosition = true;
216 mStackView = new BubbleStackView(mContext);
Mady Mellord1c78b262018-11-06 18:04:40 -0800217 ViewGroup sbv = mStatusBarWindowController.getStatusBarView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800218 // XXX: Bug when you expand the shade on top of expanded bubble, there is no scrim
219 // between bubble and the shade
220 int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1;
221 sbv.addView(mStackView, bubblePosition,
222 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellorcd9b1302018-11-06 18:08:04 -0800223 if (mExpandListener != null) {
224 mStackView.setExpandListener(mExpandListener);
225 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800226 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800227 mStackView.addBubble(bubble);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800228 if (setPosition) {
229 // Need to add the bubble to the stack before we can know the width
230 Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
231 mStackView.setPosition(startPoint.x, startPoint.y);
Mady Mellord1c78b262018-11-06 18:04:40 -0800232 mStackView.setVisibility(VISIBLE);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800233 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800234 updateBubblesShowing();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800235 }
236 }
237
238 /**
239 * Removes the bubble associated with the {@param uri}.
240 */
241 public void removeBubble(String key) {
242 BubbleView bv = mBubbles.get(key);
Mady Mellord1c78b262018-11-06 18:04:40 -0800243 if (mStackView != null && bv != null) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800244 mStackView.removeBubble(bv);
245 bv.getEntry().setBubbleDismissed(true);
246 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800247 if (mDismissListener != null) {
248 mDismissListener.onBubbleDismissed(key);
249 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800250 updateBubblesShowing();
251 }
252
253 private void updateBubblesShowing() {
254 boolean hasBubblesShowing = false;
255 for (BubbleView bv : mBubbles.values()) {
256 if (!bv.getEntry().isBubbleDismissed()) {
257 hasBubblesShowing = true;
258 break;
259 }
260 }
261 boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
262 mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
263 if (mStackView != null && !hasBubblesShowing) {
264 mStackView.setVisibility(INVISIBLE);
265 }
266 if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
267 mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
268 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800269 }
270
271 /**
272 * Sets the visibility of the bubbles, doesn't un-bubble them, just changes visibility.
273 */
274 public void updateVisibility(boolean visible) {
275 if (mStackView == null) {
276 return;
277 }
278 ArrayList<BubbleView> viewsToRemove = new ArrayList<>();
279 for (BubbleView bv : mBubbles.values()) {
280 NotificationData.Entry entry = bv.getEntry();
281 if (entry != null) {
Evan Laird94492852018-10-25 13:43:01 -0400282 if (entry.isRowRemoved() || entry.isBubbleDismissed() || entry.isRowDismissed()) {
Mady Mellor5549dd22018-11-06 18:07:34 -0800283 viewsToRemove.add(bv);
284 }
285 }
286 }
287 for (BubbleView view : viewsToRemove) {
288 mBubbles.remove(view.getKey());
289 mStackView.removeBubble(view);
Mady Mellor5549dd22018-11-06 18:07:34 -0800290 }
291 if (mStackView != null) {
Mady Mellord1c78b262018-11-06 18:04:40 -0800292 mStackView.setVisibility(visible ? VISIBLE : INVISIBLE);
293 if (!visible) {
294 collapseStack();
295 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800296 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800297 updateBubblesShowing();
298 }
299
300 /**
301 * Rect indicating the touchable region for the bubble stack / expanded stack.
302 */
303 public Rect getTouchableRegion() {
304 if (mStackView == null || mStackView.getVisibility() != VISIBLE) {
305 return null;
306 }
307 mStackView.getBoundsOnScreen(mTempRect);
308 return mTempRect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800309 }
310
Mady Mellorebdbbb92018-11-15 14:36:48 -0800311 @VisibleForTesting
312 public BubbleStackView getStackView() {
313 return mStackView;
314 }
315
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800316 // TODO: factor in PIP location / maybe last place user had it
317 /**
318 * Gets an appropriate starting point to position the bubble stack.
319 */
320 public static Point getStartPoint(int size, Point displaySize) {
321 final int x = displaySize.x - size + EDGE_OVERLAP;
322 final int y = displaySize.y / 4;
323 return new Point(x, y);
324 }
325
326 /**
327 * Gets an appropriate position for the bubble when the stack is expanded.
328 */
329 public static Point getExpandPoint(BubbleStackView view, int size, Point displaySize) {
330 // Same place for now..
331 return new Point(EDGE_OVERLAP, size);
332 }
333
Mady Mellor5549dd22018-11-06 18:07:34 -0800334 /**
335 * Whether the notification should bubble or not.
336 */
Mady Mellorceced172018-11-27 11:18:39 -0800337 public static boolean shouldAutoBubble(Context context, NotificationData.Entry entry) {
338 if (entry.isBubbleDismissed()) {
Mady Mellor5549dd22018-11-06 18:07:34 -0800339 return false;
340 }
Mady Mellorceced172018-11-27 11:18:39 -0800341
342 boolean autoBubbleMessages = shouldAutoBubbleMessages(context) || DEBUG_ENABLE_AUTO_BUBBLE;
343 boolean autoBubbleOngoing = shouldAutoBubbleOngoing(context) || DEBUG_ENABLE_AUTO_BUBBLE;
344 boolean autoBubbleAll = shouldAutoBubbleAll(context) || DEBUG_ENABLE_AUTO_BUBBLE;
345
Mady Mellor5549dd22018-11-06 18:07:34 -0800346 StatusBarNotification n = entry.notification;
347 boolean hasRemoteInput = false;
348 if (n.getNotification().actions != null) {
349 for (Notification.Action action : n.getNotification().actions) {
350 if (action.getRemoteInputs() != null) {
351 hasRemoteInput = true;
352 break;
353 }
354 }
355 }
Mady Mellor711f9562018-12-05 14:53:46 -0800356 boolean isCall = Notification.CATEGORY_CALL.equals(n.getNotification().category)
357 && n.isOngoing();
358 boolean isMusic = n.getNotification().hasMediaSession();
359 boolean isImportantOngoing = isMusic || isCall;
Mady Mellorceced172018-11-27 11:18:39 -0800360
Mady Mellor5549dd22018-11-06 18:07:34 -0800361 Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle();
Mady Mellore3175372018-12-04 17:05:11 -0800362 boolean isMessageType = Notification.CATEGORY_MESSAGE.equals(n.getNotification().category);
363 boolean isMessageStyle = Notification.MessagingStyle.class.equals(style);
364 return (((isMessageType && hasRemoteInput) || isMessageStyle) && autoBubbleMessages)
Mady Mellor711f9562018-12-05 14:53:46 -0800365 || (isImportantOngoing && autoBubbleOngoing)
Mady Mellorceced172018-11-27 11:18:39 -0800366 || autoBubbleAll;
367 }
368
369 private static boolean shouldAutoBubbleMessages(Context context) {
370 return Settings.Secure.getInt(context.getContentResolver(),
371 ENABLE_AUTO_BUBBLE_MESSAGES, 0) != 0;
372 }
373
374 private static boolean shouldAutoBubbleOngoing(Context context) {
375 return Settings.Secure.getInt(context.getContentResolver(),
376 ENABLE_AUTO_BUBBLE_ONGOING, 0) != 0;
377 }
378
379 private static boolean shouldAutoBubbleAll(Context context) {
380 return Settings.Secure.getInt(context.getContentResolver(),
381 ENABLE_AUTO_BUBBLE_ALL, 0) != 0;
Mady Mellor5549dd22018-11-06 18:07:34 -0800382 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800383}