blob: 0aad4389ceec52f659c265284440dd10de797655 [file] [log] [blame]
yoshiki iguchi4e30e762018-02-06 12:09:23 +09001/*
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.statusbar.phone;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
Adrian Roos22af6502018-02-22 16:57:08 +010022import android.content.res.Configuration;
yoshiki iguchi4e30e762018-02-06 12:09:23 +090023import android.content.res.Resources;
Jorim Jaggi0d4a9952018-06-06 17:22:56 +020024import android.graphics.Rect;
25import android.graphics.Region;
26import android.graphics.Region.Op;
yoshiki iguchi4e30e762018-02-06 12:09:23 +090027import android.support.v4.util.ArraySet;
28import android.util.Log;
29import android.util.Pools;
Jorim Jaggi0d4a9952018-06-06 17:22:56 +020030import android.view.DisplayCutout;
31import android.view.Gravity;
yoshiki iguchi4e30e762018-02-06 12:09:23 +090032import android.view.View;
33import android.view.ViewTreeObserver;
Jorim Jaggi0d4a9952018-06-06 17:22:56 +020034import android.view.ViewTreeObserver.InternalInsetsInfo;
yoshiki iguchi4e30e762018-02-06 12:09:23 +090035
36import com.android.internal.annotations.VisibleForTesting;
37import com.android.systemui.Dumpable;
Selim Cinekaa9db1f2018-02-27 17:35:47 -080038import com.android.systemui.R;
Jorim Jaggi0d4a9952018-06-06 17:22:56 +020039import com.android.systemui.ScreenDecorations;
yoshiki iguchi4e30e762018-02-06 12:09:23 +090040import com.android.systemui.statusbar.ExpandableNotificationRow;
41import com.android.systemui.statusbar.NotificationData;
42import com.android.systemui.statusbar.StatusBarState;
43import com.android.systemui.statusbar.notification.VisualStabilityManager;
Adrian Roos22af6502018-02-22 16:57:08 +010044import com.android.systemui.statusbar.policy.ConfigurationController;
yoshiki iguchi4e30e762018-02-06 12:09:23 +090045import com.android.systemui.statusbar.policy.HeadsUpManager;
46import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
47
48import java.io.FileDescriptor;
49import java.io.PrintWriter;
50import java.util.HashSet;
Jorim Jaggi0d4a9952018-06-06 17:22:56 +020051import java.util.List;
yoshiki iguchi4e30e762018-02-06 12:09:23 +090052import java.util.Stack;
53
54/**
55 * A implementation of HeadsUpManager for phone and car.
56 */
57public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable,
58 ViewTreeObserver.OnComputeInternalInsetsListener, VisualStabilityManager.Callback,
Adrian Roos22af6502018-02-22 16:57:08 +010059 OnHeadsUpChangedListener, ConfigurationController.ConfigurationListener {
yoshiki iguchi4e30e762018-02-06 12:09:23 +090060 private static final String TAG = "HeadsUpManagerPhone";
61 private static final boolean DEBUG = false;
62
63 private final View mStatusBarWindowView;
yoshiki iguchi4e30e762018-02-06 12:09:23 +090064 private final NotificationGroupManager mGroupManager;
65 private final StatusBar mBar;
66 private final VisualStabilityManager mVisualStabilityManager;
yoshiki iguchi4e30e762018-02-06 12:09:23 +090067 private boolean mReleaseOnExpandFinish;
Selim Cinekaa9db1f2018-02-27 17:35:47 -080068
69 private int mStatusBarHeight;
70 private int mHeadsUpInset;
Jorim Jaggi0d4a9952018-06-06 17:22:56 +020071 private int mDisplayCutoutTouchableRegionSize;
yoshiki iguchi4e30e762018-02-06 12:09:23 +090072 private boolean mTrackingHeadsUp;
73 private HashSet<String> mSwipedOutKeys = new HashSet<>();
74 private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
75 private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed
76 = new ArraySet<>();
77 private boolean mIsExpanded;
78 private int[] mTmpTwoArray = new int[2];
79 private boolean mHeadsUpGoingAway;
80 private boolean mWaitingOnCollapseWhenGoingAway;
81 private boolean mIsObserving;
82 private int mStatusBarState;
83
84 private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() {
85 private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>();
86
87 @Override
88 public HeadsUpEntryPhone acquire() {
89 if (!mPoolObjects.isEmpty()) {
90 return mPoolObjects.pop();
91 }
92 return new HeadsUpEntryPhone();
93 }
94
95 @Override
96 public boolean release(@NonNull HeadsUpEntryPhone instance) {
97 mPoolObjects.push(instance);
98 return true;
99 }
100 };
101
102 ///////////////////////////////////////////////////////////////////////////////////////////////
103 // Constructor:
104
105 public HeadsUpManagerPhone(@NonNull final Context context, @NonNull View statusBarWindowView,
106 @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar,
107 @NonNull VisualStabilityManager visualStabilityManager) {
108 super(context);
109
110 mStatusBarWindowView = statusBarWindowView;
111 mGroupManager = groupManager;
112 mBar = bar;
113 mVisualStabilityManager = visualStabilityManager;
114
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800115 initResources();
yoshiki iguchi4e30e762018-02-06 12:09:23 +0900116
117 addListener(new OnHeadsUpChangedListener() {
118 @Override
119 public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) {
120 if (DEBUG) Log.w(TAG, "onHeadsUpPinnedModeChanged");
121 updateTouchableRegionListener();
122 }
123 });
124 }
125
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800126 private void initResources() {
127 Resources resources = mContext.getResources();
128 mStatusBarHeight = resources.getDimensionPixelSize(
129 com.android.internal.R.dimen.status_bar_height);
130 mHeadsUpInset = mStatusBarHeight + resources.getDimensionPixelSize(
131 R.dimen.heads_up_status_bar_padding);
Jorim Jaggi0d4a9952018-06-06 17:22:56 +0200132 mDisplayCutoutTouchableRegionSize = resources.getDimensionPixelSize(
133 R.dimen.display_cutout_touchable_region_size);
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800134 }
135
136 @Override
137 public void onDensityOrFontScaleChanged() {
138 super.onDensityOrFontScaleChanged();
139 initResources();
140 }
141
Jorim Jaggi0d4a9952018-06-06 17:22:56 +0200142 @Override
143 public void onOverlayChanged() {
144 initResources();
145 }
146
yoshiki iguchi4e30e762018-02-06 12:09:23 +0900147 ///////////////////////////////////////////////////////////////////////////////////////////////
148 // Public methods:
149
150 /**
151 * Decides whether a click is invalid for a notification, i.e it has not been shown long enough
152 * that a user might have consciously clicked on it.
153 *
154 * @param key the key of the touched notification
155 * @return whether the touch is invalid and should be discarded
156 */
157 public boolean shouldSwallowClick(@NonNull String key) {
158 HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key);
159 return entry != null && mClock.currentTimeMillis() < entry.postTime;
160 }
161
162 public void onExpandingFinished() {
163 if (mReleaseOnExpandFinish) {
164 releaseAllImmediately();
165 mReleaseOnExpandFinish = false;
166 } else {
167 for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
168 if (isHeadsUp(entry.key)) {
169 // Maybe the heads-up was removed already
170 removeHeadsUpEntry(entry);
171 }
172 }
173 }
174 mEntriesToRemoveAfterExpand.clear();
175 }
176
177 /**
178 * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry
179 * from the list even after a Heads Up Notification is gone.
180 */
181 public void setTrackingHeadsUp(boolean trackingHeadsUp) {
182 mTrackingHeadsUp = trackingHeadsUp;
183 }
184
185 /**
186 * Notify that the status bar panel gets expanded or collapsed.
187 *
188 * @param isExpanded True to notify expanded, false to notify collapsed.
189 */
190 public void setIsPanelExpanded(boolean isExpanded) {
191 if (isExpanded != mIsExpanded) {
192 mIsExpanded = isExpanded;
193 if (isExpanded) {
194 // make sure our state is sane
195 mWaitingOnCollapseWhenGoingAway = false;
196 mHeadsUpGoingAway = false;
197 updateTouchableRegionListener();
198 }
199 }
200 }
201
202 /**
203 * Set the current state of the statusbar.
204 */
205 public void setStatusBarState(int statusBarState) {
206 mStatusBarState = statusBarState;
207 }
208
209 /**
210 * Set that we are exiting the headsUp pinned mode, but some notifications might still be
211 * animating out. This is used to keep the touchable regions in a sane state.
212 */
213 public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
214 if (headsUpGoingAway != mHeadsUpGoingAway) {
215 mHeadsUpGoingAway = headsUpGoingAway;
216 if (!headsUpGoingAway) {
217 waitForStatusBarLayout();
218 }
219 updateTouchableRegionListener();
220 }
221 }
222
223 /**
224 * Notifies that a remote input textbox in notification gets active or inactive.
225 * @param entry The entry of the target notification.
226 * @param remoteInputActive True to notify active, False to notify inactive.
227 */
228 public void setRemoteInputActive(
229 @NonNull NotificationData.Entry entry, boolean remoteInputActive) {
230 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.key);
231 if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) {
232 headsUpEntry.remoteInputActive = remoteInputActive;
233 if (remoteInputActive) {
234 headsUpEntry.removeAutoRemovalCallbacks();
235 } else {
236 headsUpEntry.updateEntry(false /* updatePostTime */);
237 }
238 }
239 }
240
241 @VisibleForTesting
242 public void removeMinimumDisplayTimeForTesting() {
243 mMinimumDisplayTime = 0;
244 mHeadsUpNotificationDecay = 0;
245 mTouchAcceptanceDelay = 0;
246 }
247
248 ///////////////////////////////////////////////////////////////////////////////////////////////
249 // HeadsUpManager public methods overrides:
250
251 @Override
252 public boolean isTrackingHeadsUp() {
253 return mTrackingHeadsUp;
254 }
255
256 @Override
257 public void snooze() {
258 super.snooze();
259 mReleaseOnExpandFinish = true;
260 }
261
262 /**
263 * React to the removal of the notification in the heads up.
264 *
265 * @return true if the notification was removed and false if it still needs to be kept around
266 * for a bit since it wasn't shown long enough
267 */
268 @Override
269 public boolean removeNotification(@NonNull String key, boolean ignoreEarliestRemovalTime) {
270 if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) {
271 return super.removeNotification(key, ignoreEarliestRemovalTime);
272 } else {
273 HeadsUpEntryPhone entry = getHeadsUpEntryPhone(key);
274 entry.removeAsSoonAsPossible();
275 return false;
276 }
277 }
278
279 public void addSwipedOutNotification(@NonNull String key) {
280 mSwipedOutKeys.add(key);
281 }
282
283 ///////////////////////////////////////////////////////////////////////////////////////////////
284 // Dumpable overrides:
285
286 @Override
287 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
288 pw.println("HeadsUpManagerPhone state:");
289 dumpInternal(fd, pw, args);
290 }
291
292 ///////////////////////////////////////////////////////////////////////////////////////////////
293 // ViewTreeObserver.OnComputeInternalInsetsListener overrides:
294
295 /**
296 * Overridden from TreeObserver.
297 */
298 @Override
299 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
300 if (mIsExpanded || mBar.isBouncerShowing()) {
301 // The touchable region is always the full area when expanded
302 return;
303 }
304 if (hasPinnedHeadsUp()) {
305 ExpandableNotificationRow topEntry = getTopEntry().row;
306 if (topEntry.isChildInGroup()) {
307 final ExpandableNotificationRow groupSummary
308 = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification());
309 if (groupSummary != null) {
310 topEntry = groupSummary;
311 }
312 }
313 topEntry.getLocationOnScreen(mTmpTwoArray);
314 int minX = mTmpTwoArray[0];
315 int maxX = mTmpTwoArray[0] + topEntry.getWidth();
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800316 int height = topEntry.getIntrinsicHeight();
yoshiki iguchi4e30e762018-02-06 12:09:23 +0900317
318 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
Selim Cinekaa9db1f2018-02-27 17:35:47 -0800319 info.touchableRegion.set(minX, 0, maxX, mHeadsUpInset + height);
Jorim Jaggi0d4a9952018-06-06 17:22:56 +0200320 } else {
321 setCollapsedTouchableInsets(info);
yoshiki iguchi4e30e762018-02-06 12:09:23 +0900322 }
323 }
324
Jorim Jaggi0d4a9952018-06-06 17:22:56 +0200325 private void setCollapsedTouchableInsets(ViewTreeObserver.InternalInsetsInfo info) {
326 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
327 info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight);
328 updateRegionForNotch(info.touchableRegion);
329 }
330
331 private void updateRegionForNotch(Region region) {
332 DisplayCutout cutout = mStatusBarWindowView.getRootWindowInsets().getDisplayCutout();
333 if (cutout == null) {
334 return;
335 }
336
337 // Expand touchable region such that we also catch touches that just start below the notch
338 // area.
339 Region bounds = ScreenDecorations.DisplayCutoutView.boundsFromDirection(
340 cutout, Gravity.TOP);
341 bounds.translate(0, mDisplayCutoutTouchableRegionSize);
342 region.op(bounds, Op.UNION);
343 bounds.recycle();
344 }
345
Adrian Roos22af6502018-02-22 16:57:08 +0100346 @Override
347 public void onConfigChanged(Configuration newConfig) {
348 Resources resources = mContext.getResources();
349 mStatusBarHeight = resources.getDimensionPixelSize(
350 com.android.internal.R.dimen.status_bar_height);
351 }
352
yoshiki iguchi4e30e762018-02-06 12:09:23 +0900353 ///////////////////////////////////////////////////////////////////////////////////////////////
354 // VisualStabilityManager.Callback overrides:
355
356 @Override
357 public void onReorderingAllowed() {
358 mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false);
359 for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
360 if (isHeadsUp(entry.key)) {
361 // Maybe the heads-up was removed already
362 removeHeadsUpEntry(entry);
363 }
364 }
365 mEntriesToRemoveWhenReorderingAllowed.clear();
366 mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true);
367 }
368
369 ///////////////////////////////////////////////////////////////////////////////////////////////
370 // HeadsUpManager utility (protected) methods overrides:
371
372 @Override
373 protected HeadsUpEntry createHeadsUpEntry() {
374 return mEntryPool.acquire();
375 }
376
377 @Override
378 protected void releaseHeadsUpEntry(HeadsUpEntry entry) {
379 entry.reset();
380 mEntryPool.release((HeadsUpEntryPhone) entry);
381 }
382
383 @Override
384 protected boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) {
385 return mStatusBarState != StatusBarState.KEYGUARD && !mIsExpanded
386 || super.shouldHeadsUpBecomePinned(entry);
387 }
388
389 @Override
390 protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) {
391 super.dumpInternal(fd, pw, args);
392 pw.print(" mStatusBarState="); pw.println(mStatusBarState);
393 }
394
395 ///////////////////////////////////////////////////////////////////////////////////////////////
396 // Private utility methods:
397
398 @Nullable
399 private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
400 return (HeadsUpEntryPhone) getHeadsUpEntry(key);
401 }
402
403 @Nullable
404 private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
405 return (HeadsUpEntryPhone) getTopHeadsUpEntry();
406 }
407
408 private boolean wasShownLongEnough(@NonNull String key) {
409 if (mSwipedOutKeys.contains(key)) {
410 // We always instantly dismiss views being manually swiped out.
411 mSwipedOutKeys.remove(key);
412 return true;
413 }
414
415 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key);
416 HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
417 return headsUpEntry != topEntry || headsUpEntry.wasShownLongEnough();
418 }
419
420 /**
421 * We need to wait on the whole panel to collapse, before we can remove the touchable region
422 * listener.
423 */
424 private void waitForStatusBarLayout() {
425 mWaitingOnCollapseWhenGoingAway = true;
426 mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
427 @Override
428 public void onLayoutChange(View v, int left, int top, int right, int bottom,
429 int oldLeft,
430 int oldTop, int oldRight, int oldBottom) {
431 if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) {
432 mStatusBarWindowView.removeOnLayoutChangeListener(this);
433 mWaitingOnCollapseWhenGoingAway = false;
434 updateTouchableRegionListener();
435 }
436 }
437 });
438 }
439
440 private void updateTouchableRegionListener() {
441 boolean shouldObserve = hasPinnedHeadsUp() || mHeadsUpGoingAway
Jorim Jaggi0d4a9952018-06-06 17:22:56 +0200442 || mWaitingOnCollapseWhenGoingAway
443 || mStatusBarWindowView.getRootWindowInsets().getDisplayCutout() != null;
yoshiki iguchi4e30e762018-02-06 12:09:23 +0900444 if (shouldObserve == mIsObserving) {
445 return;
446 }
447 if (shouldObserve) {
448 mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
449 mStatusBarWindowView.requestLayout();
450 } else {
451 mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
452 }
453 mIsObserving = shouldObserve;
454 }
455
456 ///////////////////////////////////////////////////////////////////////////////////////////////
457 // HeadsUpEntryPhone:
458
459 protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry {
460 public void setEntry(@NonNull final NotificationData.Entry entry) {
461 Runnable removeHeadsUpRunnable = () -> {
462 if (!mVisualStabilityManager.isReorderingAllowed()) {
463 mEntriesToRemoveWhenReorderingAllowed.add(entry);
464 mVisualStabilityManager.addReorderingAllowedCallback(
465 HeadsUpManagerPhone.this);
466 } else if (!mTrackingHeadsUp) {
467 removeHeadsUpEntry(entry);
468 } else {
469 mEntriesToRemoveAfterExpand.add(entry);
470 }
471 };
472
473 super.setEntry(entry, removeHeadsUpRunnable);
474 }
475
476 public boolean wasShownLongEnough() {
477 return earliestRemovaltime < mClock.currentTimeMillis();
478 }
479
480 @Override
481 public void updateEntry(boolean updatePostTime) {
482 super.updateEntry(updatePostTime);
483
484 if (mEntriesToRemoveAfterExpand.contains(entry)) {
485 mEntriesToRemoveAfterExpand.remove(entry);
486 }
487 if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) {
488 mEntriesToRemoveWhenReorderingAllowed.remove(entry);
489 }
490 }
491
492 @Override
493 public void expanded(boolean expanded) {
494 if (this.expanded == expanded) {
495 return;
496 }
497
498 this.expanded = expanded;
499 if (expanded) {
500 removeAutoRemovalCallbacks();
501 } else {
502 updateEntry(false /* updatePostTime */);
503 }
504 }
505 }
506}