blob: c8595ebb5fca6fc854150d0cb265f6eb74aef8fb [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.Dependency;
37import com.android.systemui.R;
38import com.android.systemui.statusbar.notification.NotificationData;
39import com.android.systemui.statusbar.phone.StatusBarWindowController;
40
Mady Mellor5549dd22018-11-06 18:07:34 -080041import java.util.ArrayList;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080042import java.util.HashMap;
43import java.util.Map;
44
Jason Monk27d01a622018-12-10 15:57:09 -050045import javax.inject.Inject;
46import javax.inject.Singleton;
47
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080048/**
49 * Bubbles are a special type of content that can "float" on top of other apps or System UI.
50 * Bubbles can be expanded to show more content.
51 *
52 * The controller manages addition, removal, and visible state of bubbles on screen.
53 */
Jason Monk27d01a622018-12-10 15:57:09 -050054@Singleton
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080055public class BubbleController {
56 private static final int MAX_BUBBLES = 5; // TODO: actually enforce this
57
58 private static final String TAG = "BubbleController";
59
Mady Mellor5549dd22018-11-06 18:07:34 -080060 // Enables some subset of notifs to automatically become bubbles
61 public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
62 // When a bubble is dismissed, recreate it as a notification
63 public static final boolean DEBUG_DEMOTE_TO_NOTIF = false;
64
Mady Mellorceced172018-11-27 11:18:39 -080065 // Secure settings
66 private static final String ENABLE_AUTO_BUBBLE_MESSAGES = "experiment_autobubble_messaging";
67 private static final String ENABLE_AUTO_BUBBLE_ONGOING = "experiment_autobubble_ongoing";
68 private static final String ENABLE_AUTO_BUBBLE_ALL = "experiment_autobubble_all";
69
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080070 private Context mContext;
Mady Mellor5549dd22018-11-06 18:07:34 -080071 private BubbleDismissListener mDismissListener;
Mady Mellord1c78b262018-11-06 18:04:40 -080072 private BubbleStateChangeListener mStateChangeListener;
Mady Mellorcd9b1302018-11-06 18:08:04 -080073 private BubbleExpandListener mExpandListener;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080074
75 private Map<String, BubbleView> mBubbles = new HashMap<>();
76 private BubbleStackView mStackView;
77 private Point mDisplaySize;
78
79 // Bubbles get added to the status bar view
Mady Mellorebdbbb92018-11-15 14:36:48 -080080 @VisibleForTesting
81 protected StatusBarWindowController mStatusBarWindowController;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080082
Mady Mellord1c78b262018-11-06 18:04:40 -080083 // Used for determining view rect for touch interaction
84 private Rect mTempRect = new Rect();
85
Mady Mellor5549dd22018-11-06 18:07:34 -080086 /**
87 * Listener to find out about bubble / bubble stack dismissal events.
88 */
89 public interface BubbleDismissListener {
90 /**
91 * Called when the entire stack of bubbles is dismissed by the user.
92 */
93 void onStackDismissed();
94
95 /**
96 * Called when a specific bubble is dismissed by the user.
97 */
98 void onBubbleDismissed(String key);
99 }
100
Mady Mellord1c78b262018-11-06 18:04:40 -0800101 /**
102 * Listener to be notified when some states of the bubbles change.
103 */
104 public interface BubbleStateChangeListener {
105 /**
106 * Called when the stack has bubbles or no longer has bubbles.
107 */
108 void onHasBubblesChanged(boolean hasBubbles);
109 }
110
Mady Mellorcd9b1302018-11-06 18:08:04 -0800111 /**
112 * Listener to find out about stack expansion / collapse events.
113 */
114 public interface BubbleExpandListener {
115 /**
116 * Called when the expansion state of the bubble stack changes.
117 *
118 * @param isExpanding whether it's expanding or collapsing
119 * @param amount fraction of how expanded or collapsed it is, 1 being fully, 0 at the start
120 */
121 void onBubbleExpandChanged(boolean isExpanding, float amount);
122 }
123
Jason Monk27d01a622018-12-10 15:57:09 -0500124 @Inject
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800125 public BubbleController(Context context) {
126 mContext = context;
127 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
128 mDisplaySize = new Point();
129 wm.getDefaultDisplay().getSize(mDisplaySize);
130 mStatusBarWindowController = Dependency.get(StatusBarWindowController.class);
131 }
132
133 /**
Mady Mellor5549dd22018-11-06 18:07:34 -0800134 * Set a listener to be notified of bubble dismissal events.
135 */
136 public void setDismissListener(BubbleDismissListener listener) {
137 mDismissListener = listener;
138 }
139
140 /**
Mady Mellord1c78b262018-11-06 18:04:40 -0800141 * Set a listener to be notified when some states of the bubbles change.
142 */
143 public void setBubbleStateChangeListener(BubbleStateChangeListener listener) {
144 mStateChangeListener = listener;
145 }
146
147 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -0800148 * Set a listener to be notified of bubble expand events.
149 */
150 public void setExpandListener(BubbleExpandListener listener) {
151 mExpandListener = listener;
152 if (mStackView != null) {
153 mStackView.setExpandListener(mExpandListener);
154 }
155 }
156
157 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800158 * Whether or not there are bubbles present, regardless of them being visible on the
159 * screen (e.g. if on AOD).
160 */
161 public boolean hasBubbles() {
162 return mBubbles.size() > 0;
163 }
164
165 /**
166 * Whether the stack of bubbles is expanded or not.
167 */
168 public boolean isStackExpanded() {
169 return mStackView != null && mStackView.isExpanded();
170 }
171
172 /**
173 * Tell the stack of bubbles to collapse.
174 */
175 public void collapseStack() {
176 if (mStackView != null) {
177 mStackView.animateExpansion(false);
178 }
179 }
180
181 /**
182 * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
183 */
184 public void dismissStack() {
Mady Mellord1c78b262018-11-06 18:04:40 -0800185 if (mStackView == null) {
186 return;
187 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800188 Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
189 // Reset the position of the stack (TODO - or should we save / respect last user position?)
190 mStackView.setPosition(startPoint.x, startPoint.y);
191 for (String key: mBubbles.keySet()) {
192 removeBubble(key);
193 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800194 if (mDismissListener != null) {
195 mDismissListener.onStackDismissed();
196 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800197 updateBubblesShowing();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800198 }
199
200 /**
201 * Adds a bubble associated with the provided notification entry or updates it if it exists.
202 */
203 public void addBubble(NotificationData.Entry notif) {
204 if (mBubbles.containsKey(notif.key)) {
205 // It's an update
206 BubbleView bubble = mBubbles.get(notif.key);
207 mStackView.updateBubble(bubble, notif);
208 } else {
209 // It's new
210 BubbleView bubble = new BubbleView(mContext);
211 bubble.setNotif(notif);
212 mBubbles.put(bubble.getKey(), bubble);
213
Mady Mellord1c78b262018-11-06 18:04:40 -0800214 boolean setPosition = mStackView != null && mStackView.getVisibility() != VISIBLE;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800215 if (mStackView == null) {
216 setPosition = true;
217 mStackView = new BubbleStackView(mContext);
Mady Mellord1c78b262018-11-06 18:04:40 -0800218 ViewGroup sbv = mStatusBarWindowController.getStatusBarView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800219 // XXX: Bug when you expand the shade on top of expanded bubble, there is no scrim
220 // between bubble and the shade
221 int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1;
222 sbv.addView(mStackView, bubblePosition,
223 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellorcd9b1302018-11-06 18:08:04 -0800224 if (mExpandListener != null) {
225 mStackView.setExpandListener(mExpandListener);
226 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800227 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800228 mStackView.addBubble(bubble);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800229 if (setPosition) {
230 // Need to add the bubble to the stack before we can know the width
231 Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize);
232 mStackView.setPosition(startPoint.x, startPoint.y);
Mady Mellord1c78b262018-11-06 18:04:40 -0800233 mStackView.setVisibility(VISIBLE);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800234 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800235 updateBubblesShowing();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800236 }
237 }
238
239 /**
240 * Removes the bubble associated with the {@param uri}.
241 */
242 public void removeBubble(String key) {
243 BubbleView bv = mBubbles.get(key);
Mady Mellord1c78b262018-11-06 18:04:40 -0800244 if (mStackView != null && bv != null) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800245 mStackView.removeBubble(bv);
246 bv.getEntry().setBubbleDismissed(true);
247 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800248 if (mDismissListener != null) {
249 mDismissListener.onBubbleDismissed(key);
250 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800251 updateBubblesShowing();
252 }
253
254 private void updateBubblesShowing() {
255 boolean hasBubblesShowing = false;
256 for (BubbleView bv : mBubbles.values()) {
257 if (!bv.getEntry().isBubbleDismissed()) {
258 hasBubblesShowing = true;
259 break;
260 }
261 }
262 boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
263 mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
264 if (mStackView != null && !hasBubblesShowing) {
265 mStackView.setVisibility(INVISIBLE);
266 }
267 if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
268 mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
269 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800270 }
271
272 /**
273 * Sets the visibility of the bubbles, doesn't un-bubble them, just changes visibility.
274 */
275 public void updateVisibility(boolean visible) {
276 if (mStackView == null) {
277 return;
278 }
279 ArrayList<BubbleView> viewsToRemove = new ArrayList<>();
280 for (BubbleView bv : mBubbles.values()) {
281 NotificationData.Entry entry = bv.getEntry();
282 if (entry != null) {
Evan Laird94492852018-10-25 13:43:01 -0400283 if (entry.isRowRemoved() || entry.isBubbleDismissed() || entry.isRowDismissed()) {
Mady Mellor5549dd22018-11-06 18:07:34 -0800284 viewsToRemove.add(bv);
285 }
286 }
287 }
288 for (BubbleView view : viewsToRemove) {
289 mBubbles.remove(view.getKey());
290 mStackView.removeBubble(view);
Mady Mellor5549dd22018-11-06 18:07:34 -0800291 }
292 if (mStackView != null) {
Mady Mellord1c78b262018-11-06 18:04:40 -0800293 mStackView.setVisibility(visible ? VISIBLE : INVISIBLE);
294 if (!visible) {
295 collapseStack();
296 }
Mady Mellor5549dd22018-11-06 18:07:34 -0800297 }
Mady Mellord1c78b262018-11-06 18:04:40 -0800298 updateBubblesShowing();
299 }
300
301 /**
302 * Rect indicating the touchable region for the bubble stack / expanded stack.
303 */
304 public Rect getTouchableRegion() {
305 if (mStackView == null || mStackView.getVisibility() != VISIBLE) {
306 return null;
307 }
308 mStackView.getBoundsOnScreen(mTempRect);
309 return mTempRect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800310 }
311
Mady Mellorebdbbb92018-11-15 14:36:48 -0800312 @VisibleForTesting
313 public BubbleStackView getStackView() {
314 return mStackView;
315 }
316
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800317 // TODO: factor in PIP location / maybe last place user had it
318 /**
319 * Gets an appropriate starting point to position the bubble stack.
320 */
321 public static Point getStartPoint(int size, Point displaySize) {
322 final int x = displaySize.x - size + EDGE_OVERLAP;
323 final int y = displaySize.y / 4;
324 return new Point(x, y);
325 }
326
327 /**
328 * Gets an appropriate position for the bubble when the stack is expanded.
329 */
330 public static Point getExpandPoint(BubbleStackView view, int size, Point displaySize) {
331 // Same place for now..
332 return new Point(EDGE_OVERLAP, size);
333 }
334
Mady Mellor5549dd22018-11-06 18:07:34 -0800335 /**
336 * Whether the notification should bubble or not.
337 */
Mady Mellorceced172018-11-27 11:18:39 -0800338 public static boolean shouldAutoBubble(Context context, NotificationData.Entry entry) {
339 if (entry.isBubbleDismissed()) {
Mady Mellor5549dd22018-11-06 18:07:34 -0800340 return false;
341 }
Mady Mellorceced172018-11-27 11:18:39 -0800342
343 boolean autoBubbleMessages = shouldAutoBubbleMessages(context) || DEBUG_ENABLE_AUTO_BUBBLE;
344 boolean autoBubbleOngoing = shouldAutoBubbleOngoing(context) || DEBUG_ENABLE_AUTO_BUBBLE;
345 boolean autoBubbleAll = shouldAutoBubbleAll(context) || DEBUG_ENABLE_AUTO_BUBBLE;
346
Mady Mellor5549dd22018-11-06 18:07:34 -0800347 StatusBarNotification n = entry.notification;
348 boolean hasRemoteInput = false;
349 if (n.getNotification().actions != null) {
350 for (Notification.Action action : n.getNotification().actions) {
351 if (action.getRemoteInputs() != null) {
352 hasRemoteInput = true;
353 break;
354 }
355 }
356 }
Mady Mellor711f9562018-12-05 14:53:46 -0800357 boolean isCall = Notification.CATEGORY_CALL.equals(n.getNotification().category)
358 && n.isOngoing();
359 boolean isMusic = n.getNotification().hasMediaSession();
360 boolean isImportantOngoing = isMusic || isCall;
Mady Mellorceced172018-11-27 11:18:39 -0800361
Mady Mellor5549dd22018-11-06 18:07:34 -0800362 Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle();
Mady Mellore3175372018-12-04 17:05:11 -0800363 boolean isMessageType = Notification.CATEGORY_MESSAGE.equals(n.getNotification().category);
364 boolean isMessageStyle = Notification.MessagingStyle.class.equals(style);
365 return (((isMessageType && hasRemoteInput) || isMessageStyle) && autoBubbleMessages)
Mady Mellor711f9562018-12-05 14:53:46 -0800366 || (isImportantOngoing && autoBubbleOngoing)
Mady Mellorceced172018-11-27 11:18:39 -0800367 || autoBubbleAll;
368 }
369
370 private static boolean shouldAutoBubbleMessages(Context context) {
371 return Settings.Secure.getInt(context.getContentResolver(),
372 ENABLE_AUTO_BUBBLE_MESSAGES, 0) != 0;
373 }
374
375 private static boolean shouldAutoBubbleOngoing(Context context) {
376 return Settings.Secure.getInt(context.getContentResolver(),
377 ENABLE_AUTO_BUBBLE_ONGOING, 0) != 0;
378 }
379
380 private static boolean shouldAutoBubbleAll(Context context) {
381 return Settings.Secure.getInt(context.getContentResolver(),
382 ENABLE_AUTO_BUBBLE_ALL, 0) != 0;
Mady Mellor5549dd22018-11-06 18:07:34 -0800383 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800384}