Integrate Heads-up notifications into the shade

Change-Id: I4ca0fb4e76e7c974490538c168da0564fe97e0ae
This commit is contained in:
Selim Cinek
2015-03-16 17:09:28 -07:00
parent ba67acff25
commit b8f09cf553
23 changed files with 1337 additions and 1428 deletions

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
>
<translate
android:interpolator="@android:interpolator/overshoot"
android:fromYDelta="-50%" android:toYDelta="0"
android:duration="@android:integer/config_shortAnimTime" />
<alpha
android:interpolator="@android:interpolator/decelerate_quad"
android:fromAlpha="0.0" android:toAlpha="1.0"
android:duration="@android:integer/config_shortAnimTime" />
</set>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
>
<translate
android:interpolator="@android:interpolator/overshoot"
android:fromYDelta="0" android:toYDelta="-50%"
android:duration="@android:integer/config_shortAnimTime" />
<alpha
android:interpolator="@android:interpolator/accelerate_quad"
android:fromAlpha="1.0" android:toAlpha="0.0"
android:duration="@android:integer/config_shortAnimTime" />
</set>

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- extends FrameLayout -->
<com.android.systemui.statusbar.policy.HeadsUpNotificationView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@drawable/heads_up_scrim">
<FrameLayout
android:layout_width="@dimen/notification_panel_width"
android:layout_height="wrap_content"
android:layout_gravity="@integer/notification_panel_layout_gravity"
android:paddingStart="@dimen/notification_side_padding"
android:paddingEnd="@dimen/notification_side_padding"
android:elevation="8dp"
android:id="@+id/content_holder" />
</com.android.systemui.statusbar.policy.HeadsUpNotificationView>

View File

@@ -147,7 +147,7 @@
<integer name="heads_up_default_snooze_length_ms">60000</integer>
<!-- Minimum display time for a heads up notification, in milliseconds. -->
<integer name="heads_up_notification_minimum_time">3000</integer>
<integer name="heads_up_notification_minimum_time">2000</integer>
<!-- milliseconds before the heads up notification accepts touches. -->
<integer name="heads_up_sensitivity_delay">700</integer>

View File

@@ -330,7 +330,6 @@
keyguard_clock_height_fraction_* for the difference between min and max.-->
<dimen name="keyguard_clock_notifications_margin_min">24dp</dimen>
<dimen name="keyguard_clock_notifications_margin_max">36dp</dimen>
<dimen name="heads_up_window_height">250dp</dimen>
<!-- The minimum amount the user needs to swipe to go to the camera / phone. -->
<dimen name="keyguard_min_swipe_amount">110dp</dimen>

View File

@@ -205,11 +205,6 @@
<style name="Animation.StatusBar">
</style>
<style name="Animation.StatusBar.HeadsUp">
<item name="android:windowEnterAnimation">@anim/heads_up_enter</item>
<item name="android:windowExitAnimation">@anim/heads_up_exit</item>
</style>
<style name="systemui_theme" parent="@android:style/Theme.DeviceDefault">
<item name="android:colorPrimary">@color/system_primary_color</item>
<item name="android:colorControlActivated">@color/system_accent_color</item>

View File

@@ -96,7 +96,7 @@ import com.android.systemui.statusbar.NotificationData.Entry;
import com.android.systemui.statusbar.phone.NavigationBarView;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.HeadsUpNotificationView;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.PreviewInflater;
import com.android.systemui.statusbar.policy.RemoteInputView;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
@@ -130,9 +130,6 @@ public abstract class BaseStatusBar extends SystemUI implements
protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023;
protected static final int MSG_SHOW_NEXT_AFFILIATED_TASK = 1024;
protected static final int MSG_SHOW_PREV_AFFILIATED_TASK = 1025;
protected static final int MSG_SHOW_HEADS_UP = 1028;
protected static final int MSG_HIDE_HEADS_UP = 1029;
protected static final int MSG_ESCALATE_HEADS_UP = 1030;
protected static final boolean ENABLE_HEADS_UP = true;
// scores above this threshold should be displayed in heads up mode.
@@ -158,8 +155,7 @@ public abstract class BaseStatusBar extends SystemUI implements
protected NotificationGroupManager mGroupManager = new NotificationGroupManager();
// for heads up notifications
protected HeadsUpNotificationView mHeadsUpNotificationView;
protected int mHeadsUpNotificationDecay;
protected HeadsUpManager mHeadsUpManager;
protected int mCurrentUserId = 0;
final protected SparseArray<UserInfo> mCurrentProfiles = new SparseArray<UserInfo>();
@@ -446,9 +442,8 @@ public abstract class BaseStatusBar extends SystemUI implements
@Override
public void run() {
processForRemoteInput(sbn.getNotification());
Notification n = sbn.getNotification();
boolean isUpdate = mNotificationData.get(sbn.getKey()) != null
|| isHeadsUp(sbn.getKey());
String key = sbn.getKey();
boolean isUpdate = mNotificationData.get(key) != null;
// In case we don't allow child notifications, we ignore children of
// notifications that have a summary, since we're not going to show them
@@ -462,7 +457,7 @@ public abstract class BaseStatusBar extends SystemUI implements
// Remove existing notification to avoid stale data.
if (isUpdate) {
removeNotification(sbn.getKey(), rankingMap);
removeNotification(key, rankingMap);
} else {
mNotificationData.updateRanking(rankingMap);
}
@@ -693,13 +688,11 @@ public abstract class BaseStatusBar extends SystemUI implements
}
private void setHeadsUpUser(int newUserId) {
if (mHeadsUpNotificationView != null) {
mHeadsUpNotificationView.setUser(newUserId);
}
mHeadsUpManager.setUser(newUserId);
}
public boolean isHeadsUp(String key) {
return mHeadsUpNotificationView != null && mHeadsUpNotificationView.isShowing(key);
return mHeadsUpManager.isHeadsUp(key);
}
@Override // NotificationData.Environment
@@ -758,8 +751,8 @@ public abstract class BaseStatusBar extends SystemUI implements
protected View updateNotificationVetoButton(View row, StatusBarNotification n) {
View vetoButton = row.findViewById(R.id.veto);
if (n.isClearable() || (mHeadsUpNotificationView.getEntry() != null
&& mHeadsUpNotificationView.getEntry().row == row)) {
// TODO: make headsup non-clearable if it is in the shade
if (n.isClearable() || (mHeadsUpManager.isHeadsUp(n.getKey()))) {
final String _pkg = n.getPackageName();
final String _tag = n.getTag();
final int _id = n.getId();
@@ -1002,9 +995,6 @@ public abstract class BaseStatusBar extends SystemUI implements
}
}
public void onHeadsUpDismissed() {
}
@Override
public void showRecentApps(boolean triggeredFromAltTab) {
int msg = MSG_SHOW_RECENT_APPS;
@@ -1141,13 +1131,10 @@ public abstract class BaseStatusBar extends SystemUI implements
// Do nothing
}
public abstract void scheduleHeadsUpDecay(long delay);
public abstract void scheduleHeadsUpOpen();
public abstract void scheduleHeadsUpClose();
public abstract void scheduleHeadsUpEscalation();
/**
* if the interrupting notification had a fullscreen intent, fire it now.
*/
public abstract void escalateHeadsUp();
/**
* Save the current "public" (locked and secure) state of the lockscreen.
@@ -1238,15 +1225,8 @@ public abstract class BaseStatusBar extends SystemUI implements
protected void workAroundBadLayerDrawableOpacity(View v) {
}
protected boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) {
return inflateViews(entry, parent, false);
}
protected boolean inflateViewsForHeadsUp(NotificationData.Entry entry, ViewGroup parent) {
return inflateViews(entry, parent, true);
}
private boolean inflateViews(NotificationData.Entry entry, ViewGroup parent, boolean isHeadsUp) {
protected boolean inflateViews(NotificationData.Entry entry, ViewGroup parent,
boolean isHeadsUp) {
PackageManager pmUser = getPackageManagerForUser(
entry.notification.getUser().getIdentifier());
@@ -1284,7 +1264,6 @@ public abstract class BaseStatusBar extends SystemUI implements
hasUserChangedExpansion = row.hasUserChangedExpansion();
userExpanded = row.isUserExpanded();
userLocked = row.isUserLocked();
entry.row.setHeadsUp(isHeadsUp);
entry.reset();
if (hasUserChangedExpansion) {
row.setUserExpanded(userExpanded);
@@ -1597,12 +1576,12 @@ public abstract class BaseStatusBar extends SystemUI implements
mCurrentUserId);
dismissKeyguardThenExecute(new OnDismissAction() {
public boolean onDismiss() {
if (mNotificationKey.equals(mHeadsUpNotificationView.getKey())) {
if (mHeadsUpManager.isHeadsUp(mNotificationKey)) {
// Release the HUN notification to the shade.
//
// In most cases, when FLAG_AUTO_CANCEL is set, the notification will
// become canceled shortly by NoMan, but we can't assume that.
mHeadsUpNotificationView.releaseImmediately();
mHeadsUpManager.releaseImmediately(mNotificationKey);
}
new Thread() {
@Override
@@ -1740,7 +1719,8 @@ public abstract class BaseStatusBar extends SystemUI implements
return entry.notification;
}
protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn) {
protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn,
boolean isHeadsUp) {
if (DEBUG) {
Log.d(TAG, "createNotificationViews(notification=" + sbn);
}
@@ -1751,7 +1731,7 @@ public abstract class BaseStatusBar extends SystemUI implements
// Construct the expanded view.
NotificationData.Entry entry = new NotificationData.Entry(sbn, iconView);
if (!inflateViews(entry, mStackScroller)) {
if (!inflateViews(entry, mStackScroller, isHeadsUp)) {
handleNotificationError(sbn, "Couldn't expand RemoteViews for: " + sbn);
return null;
}
@@ -1889,26 +1869,117 @@ public abstract class BaseStatusBar extends SystemUI implements
if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")");
final String key = notification.getKey();
boolean wasHeadsUp = isHeadsUp(key);
Entry oldEntry;
if (wasHeadsUp) {
oldEntry = mHeadsUpNotificationView.getEntry();
} else {
oldEntry = mNotificationData.get(key);
}
if (oldEntry == null) {
Entry entry = mNotificationData.get(key);
if (entry == null) {
return;
}
final StatusBarNotification oldNotification = oldEntry.notification;
Notification n = notification.getNotification();
if (DEBUG) {
logUpdate(entry, n);
}
boolean applyInPlace = shouldApplyInPlace(entry, n);
final boolean shouldInterrupt = shouldInterrupt(notification);
final boolean alertAgain = shouldInterrupt && alertAgain(entry, n);
boolean isHeadsUp = shouldInterrupt && alertAgain;
entry.notification = notification;
mGroupManager.onEntryUpdated(entry, entry.notification);
boolean updateSuccessful = false;
if (applyInPlace) {
// We can just reapply the notifications in place
if (DEBUG) Log.d(TAG, "reusing notification for key: " + key);
try {
if (entry.icon != null) {
// Update the icon
final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(),
notification.getUser(),
n.icon,
n.iconLevel,
n.number,
n.tickerText);
entry.icon.setNotification(n);
if (!entry.icon.set(ic)) {
handleNotificationError(notification, "Couldn't update icon: " + ic);
return;
}
}
updateNotificationViews(entry, notification, isHeadsUp);
updateSuccessful = true;
}
catch (RuntimeException e) {
// It failed to add cleanly. Log, and remove the view from the panel.
Log.w(TAG, "Couldn't reapply views for package " + n.contentView.getPackage(), e);
}
}
if (!updateSuccessful) {
if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key);
final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(),
notification.getUser(),
n.icon,
n.iconLevel,
n.number,
n.tickerText);
entry.icon.setNotification(n);
entry.icon.set(ic);
inflateViews(entry, mStackScroller, isHeadsUp);
}
mNotificationData.updateRanking(ranking);
updateNotifications();
updateHeadsUp(key, entry, shouldInterrupt, alertAgain);
// Update the veto button accordingly (and as a result, whether this row is
// swipe-dismissable)
updateNotificationVetoButton(entry.row, notification);
// Is this for you?
boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
// Recalculate the position of the sliding windows and the titles.
setAreThereNotifications();
}
private void updateHeadsUp(String key, Entry entry, boolean shouldInterrupt,
boolean alertAgain) {
final boolean wasHeadsUp = isHeadsUp(key);
if (wasHeadsUp) {
mHeadsUpManager.updateNotification(entry, alertAgain);
if (!shouldInterrupt) {
mHeadsUpManager.removeNotification(key);
}
}
}
private void logUpdate(Entry oldEntry, Notification n) {
StatusBarNotification oldNotification = oldEntry.notification;
Log.d(TAG, "old notification: when=" + oldNotification.getNotification().when
+ " ongoing=" + oldNotification.isOngoing()
+ " expanded=" + oldEntry.expanded
+ " contentView=" + oldNotification.getNotification().contentView
+ " bigContentView=" + oldNotification.getNotification().bigContentView
+ " publicView=" + oldNotification.getNotification().publicVersion
+ " rowParent=" + oldEntry.row.getParent());
Log.d(TAG, "new notification: when=" + n.when
+ " ongoing=" + oldNotification.isOngoing()
+ " contentView=" + n.contentView
+ " bigContentView=" + n.bigContentView
+ " publicView=" + n.publicVersion);
}
/**
* @return whether we can just reapply the RemoteViews in place when it is updated
*/
private boolean shouldApplyInPlace(Entry entry, Notification n) {
StatusBarNotification oldNotification = entry.notification;
// XXX: modify when we do something more intelligent with the two content views
final RemoteViews oldContentView = oldNotification.getNotification().contentView;
Notification n = notification.getNotification();
final RemoteViews contentView = n.contentView;
final RemoteViews oldBigContentView = oldNotification.getNotification().bigContentView;
final RemoteViews bigContentView = n.bigContentView;
final RemoteViews oldHeadsUpContentView = oldNotification.getNotification().headsUpContentView;
final RemoteViews oldHeadsUpContentView
= oldNotification.getNotification().headsUpContentView;
final RemoteViews headsUpContentView = n.headsUpContentView;
final Notification oldPublicNotification = oldNotification.getNotification().publicVersion;
final RemoteViews oldPublicContentView = oldPublicNotification != null
@@ -1916,34 +1987,15 @@ public abstract class BaseStatusBar extends SystemUI implements
final Notification publicNotification = n.publicVersion;
final RemoteViews publicContentView = publicNotification != null
? publicNotification.contentView : null;
if (DEBUG) {
Log.d(TAG, "old notification: when=" + oldNotification.getNotification().when
+ " ongoing=" + oldNotification.isOngoing()
+ " expanded=" + oldEntry.expanded
+ " contentView=" + oldContentView
+ " bigContentView=" + oldBigContentView
+ " publicView=" + oldPublicContentView
+ " rowParent=" + oldEntry.row.getParent());
Log.d(TAG, "new notification: when=" + n.when
+ " ongoing=" + oldNotification.isOngoing()
+ " contentView=" + contentView
+ " bigContentView=" + bigContentView
+ " publicView=" + publicContentView);
}
// Can we just reapply the RemoteViews in place?
// 1U is never null
boolean contentsUnchanged = oldEntry.expanded != null
boolean contentsUnchanged = entry.expanded != null
&& contentView.getPackage() != null
&& oldContentView.getPackage() != null
&& oldContentView.getPackage().equals(contentView.getPackage())
&& oldContentView.getLayoutId() == contentView.getLayoutId();
// large view may be null
boolean bigContentsUnchanged =
(oldEntry.getBigContentView() == null && bigContentView == null)
|| ((oldEntry.getBigContentView() != null && bigContentView != null)
(entry.getBigContentView() == null && bigContentView == null)
|| ((entry.getBigContentView() != null && bigContentView != null)
&& bigContentView.getPackage() != null
&& oldBigContentView.getPackage() != null
&& oldBigContentView.getPackage().equals(bigContentView.getPackage())
@@ -1962,123 +2014,8 @@ public abstract class BaseStatusBar extends SystemUI implements
&& oldPublicContentView.getPackage() != null
&& oldPublicContentView.getPackage().equals(publicContentView.getPackage())
&& oldPublicContentView.getLayoutId() == publicContentView.getLayoutId());
final boolean shouldInterrupt = shouldInterrupt(notification);
final boolean alertAgain = shouldInterrupt && alertAgain(oldEntry, n);
boolean updateSuccessful = false;
if (contentsUnchanged && bigContentsUnchanged && headsUpContentsUnchanged
&& publicUnchanged) {
if (DEBUG) Log.d(TAG, "reusing notification for key: " + key);
oldEntry.notification = notification;
mGroupManager.onEntryUpdated(oldEntry, oldNotification);
try {
if (oldEntry.icon != null) {
// Update the icon
final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(),
notification.getUser(),
n.icon,
n.iconLevel,
n.number,
n.tickerText);
oldEntry.icon.setNotification(n);
if (!oldEntry.icon.set(ic)) {
handleNotificationError(notification, "Couldn't update icon: " + ic);
return;
}
}
if (wasHeadsUp) {
// Release may hang on to the views for a bit, so we should always update them.
updateHeadsUpViews(oldEntry, notification);
mHeadsUpNotificationView.updateNotification(oldEntry, alertAgain);
if (!shouldInterrupt) {
// we updated the notification above, so release to build a new shade entry
mHeadsUpNotificationView.release();
return;
}
} else {
if (shouldInterrupt && alertAgain) {
mStackScroller.setRemoveAnimationEnabled(false);
removeNotificationViews(key, ranking);
mStackScroller.setRemoveAnimationEnabled(true);
addNotification(notification, ranking, oldEntry); //this will pop the headsup
} else {
updateNotificationViews(oldEntry, notification);
}
}
mNotificationData.updateRanking(ranking);
updateNotifications();
updateSuccessful = true;
}
catch (RuntimeException e) {
// It failed to add cleanly. Log, and remove the view from the panel.
Log.w(TAG, "Couldn't reapply views for package " + contentView.getPackage(), e);
}
}
if (!updateSuccessful) {
if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key);
if (wasHeadsUp) {
if (DEBUG) Log.d(TAG, "rebuilding heads up for key: " + key);
ViewGroup holder = mHeadsUpNotificationView.getHolder();
if (inflateViewsForHeadsUp(oldEntry, holder)) {
mHeadsUpNotificationView.updateNotification(oldEntry, alertAgain);
} else {
Log.w(TAG, "Couldn't create new updated headsup for package "
+ contentView.getPackage());
}
if (!shouldInterrupt) {
if (DEBUG) Log.d(TAG, "releasing heads up for key: " + key);
oldEntry.notification = notification;
mGroupManager.onEntryUpdated(oldEntry, oldNotification);
mHeadsUpNotificationView.release();
return;
}
} else {
if (shouldInterrupt && alertAgain) {
if (DEBUG) Log.d(TAG, "reposting to invoke heads up for key: " + key);
mStackScroller.setRemoveAnimationEnabled(false);
removeNotificationViews(key, ranking);
mStackScroller.setRemoveAnimationEnabled(true);
addNotification(notification, ranking, oldEntry); //this will pop the headsup
} else {
if (DEBUG) Log.d(TAG, "rebuilding update in place for key: " + key);
oldEntry.notification = notification;
mGroupManager.onEntryUpdated(oldEntry, oldNotification);
final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(),
notification.getUser(),
n.icon,
n.iconLevel,
n.number,
n.tickerText);
oldEntry.icon.setNotification(n);
oldEntry.icon.set(ic);
inflateViews(oldEntry, mStackScroller, wasHeadsUp);
mNotificationData.updateRanking(ranking);
updateNotifications();
}
}
}
// Update the veto button accordingly (and as a result, whether this row is
// swipe-dismissable)
updateNotificationVetoButton(oldEntry.row, notification);
// Is this for you?
boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
// Recalculate the position of the sliding windows and the titles.
setAreThereNotifications();
}
private void updateNotificationViews(NotificationData.Entry entry,
StatusBarNotification notification) {
updateNotificationViews(entry, notification, false);
}
private void updateHeadsUpViews(NotificationData.Entry entry,
StatusBarNotification notification) {
updateNotificationViews(entry, notification, true);
return contentsUnchanged && bigContentsUnchanged && headsUpContentsUnchanged
&& publicUnchanged;
}
private void updateNotificationViews(NotificationData.Entry entry,
@@ -2115,10 +2052,8 @@ public abstract class BaseStatusBar extends SystemUI implements
applyRemoteInput(entry);
}
protected void notifyHeadsUpScreenOn(boolean screenOn) {
if (!screenOn) {
scheduleHeadsUpEscalation();
}
protected void notifyHeadsUpScreenOff() {
escalateHeadsUp();
}
private boolean alertAgain(Entry oldEntry, Notification newNotification) {
@@ -2134,7 +2069,7 @@ public abstract class BaseStatusBar extends SystemUI implements
return false;
}
if (mHeadsUpNotificationView.isSnoozed(sbn.getPackageName())) {
if (mHeadsUpManager.isSnoozed(sbn.getPackageName())) {
return false;
}

View File

@@ -108,6 +108,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
!mChildrenExpanded);
}
};
private boolean mInShade;
public void setIconAnimationRunning(boolean running) {
setIconAnimationRunning(running, mPublicLayout);
@@ -164,6 +165,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
return mStatusBarNotification;
}
public boolean isHeadsUp() {
return mIsHeadsUp;
}
public void setHeadsUp(boolean isHeadsUp) {
mIsHeadsUp = isHeadsUp;
}
@@ -263,6 +268,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
return realActualHeight;
}
public void setInShade(boolean inShade) {
mInShade = inShade;
}
public boolean isInShade() {
return mInShade;
}
public int getHeadsUpHeight() {
// TODO: improve this logic
return getIntrinsicHeight();
}
public interface ExpansionLogger {
public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.systemui.statusbar.phone;
import android.content.Context;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.ExpandableView;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
/**
* A Helper class to handle touches on the heads-up views
*/
public class HeadsUpTouchHelper implements Gefingerpoken {
private HeadsUpManager mHeadsUpManager;
private NotificationStackScrollLayout mStackScroller;
private int mTrackingPointer;
private float mTouchSlop;
private float mInitialTouchX;
private float mInitialTouchY;
private boolean mMotionOnHeadsUpView;
private boolean mTrackingHeadsUp;
private boolean mCollapseSnoozes;
private NotificationPanelView mPanel;
private ExpandableNotificationRow mPickedChild;
public boolean isTrackingHeadsUp() {
return mTrackingHeadsUp;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!mMotionOnHeadsUpView && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
return false;
}
int pointerIndex = event.findPointerIndex(mTrackingPointer);
if (pointerIndex < 0) {
pointerIndex = 0;
mTrackingPointer = event.getPointerId(pointerIndex);
}
final float x = event.getX(pointerIndex);
final float y = event.getY(pointerIndex);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitialTouchY = y;
mInitialTouchX = x;
setTrackingHeadsUp(false);
ExpandableView child = mStackScroller.getChildAtPosition(x, y);
mMotionOnHeadsUpView = false;
if (child instanceof ExpandableNotificationRow) {
mPickedChild = (ExpandableNotificationRow) child;
mMotionOnHeadsUpView = mPickedChild.isHeadsUp() && !mPickedChild.isInShade();
}
break;
case MotionEvent.ACTION_POINTER_UP:
final int upPointer = event.getPointerId(event.getActionIndex());
if (mTrackingPointer == upPointer) {
// gesture is ongoing, find a new pointer to track
final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
mTrackingPointer = event.getPointerId(newIndex);
mInitialTouchX = event.getX(newIndex);
mInitialTouchY = event.getY(newIndex);
}
break;
case MotionEvent.ACTION_MOVE:
final float h = y - mInitialTouchY;
if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)) {
setTrackingHeadsUp(true);
mCollapseSnoozes = h < 0;
mInitialTouchX = x;
mInitialTouchY = y;
mHeadsUpManager.releaseAllToShade();
int expandedHeight = mPickedChild.getActualHeight();
mPanel.startExpandMotion(x, y, true /* startTracking */, expandedHeight);
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (mPickedChild != null && mMotionOnHeadsUpView) {
if (mHeadsUpManager.shouldSwallowClick(
mPickedChild.getStatusBarNotification().getKey())) {
endMotion();
return true;
}
}
endMotion();
break;
}
return false;
}
private void setTrackingHeadsUp(boolean tracking) {
mTrackingHeadsUp = tracking;
mHeadsUpManager.setTrackingHeadsUp(tracking);
mPanel.setTrackingHeadsUp(tracking);
}
public void notifyFling(boolean collapse) {
if (collapse && mCollapseSnoozes) {
mHeadsUpManager.snooze();
}
mCollapseSnoozes = false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mTrackingHeadsUp) {
return false;
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
endMotion();
setTrackingHeadsUp(false);
break;
}
return true;
}
private void endMotion() {
mTrackingPointer = -1;
mPickedChild = null;
mMotionOnHeadsUpView = false;
}
public ExpandableView getPickedChild() {
return mPickedChild;
}
public void bind(HeadsUpManager headsUpManager, NotificationStackScrollLayout stackScroller,
NotificationPanelView notificationPanelView) {
mHeadsUpManager = headsUpManager;
mStackScroller = stackScroller;
mPanel = notificationPanelView;
Context context = stackScroller.getContext();
final ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledTouchSlop();
}
}

View File

@@ -32,6 +32,7 @@ import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
@@ -39,8 +40,8 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.keyguard.KeyguardStatusView;
import com.android.systemui.EventLogTags;
import com.android.systemui.EventLogConstants;
import com.android.systemui.EventLogTags;
import com.android.systemui.R;
import com.android.systemui.qs.QSContainer;
import com.android.systemui.qs.QSPanel;
@@ -48,7 +49,9 @@ import com.android.systemui.statusbar.ExpandableView;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.statusbar.GestureRecorder;
import com.android.systemui.statusbar.KeyguardAffordanceView;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.stack.StackStateAnimator;
@@ -56,7 +59,8 @@ import com.android.systemui.statusbar.stack.StackStateAnimator;
public class NotificationPanelView extends PanelView implements
ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener {
KeyguardAffordanceHelper.Callback, NotificationStackScrollLayout.OnEmptySpaceClickListener,
HeadsUpManager.OnHeadsUpChangedListener {
private static final boolean DEBUG = false;
@@ -81,7 +85,7 @@ public class NotificationPanelView extends PanelView implements
private TextView mClockView;
private View mReserveNotificationSpace;
private View mQsNavbarScrim;
private View mNotificationContainerParent;
private NotificationsQuickSettingsContainer mNotificationContainerParent;
private NotificationStackScrollLayout mNotificationStackScroller;
private int mNotificationTopPadding;
private boolean mAnimateNextTopPaddingChange;
@@ -177,6 +181,16 @@ public class NotificationPanelView extends PanelView implements
private float mKeyguardStatusBarAnimateAlpha = 1f;
private int mOldLayoutDirection;
private HeadsUpTouchHelper mHeadsUpTouchHelper = new HeadsUpTouchHelper();
private boolean mPinnedHeadsUpExist;
private boolean mExpansionIsFromHeadsUp;
private int mBottomBarHeight;
private Runnable mHeadsUpExistenceChangedRunnable = new Runnable() {
@Override
public void run() {
notifyBarPanelExpansionChanged();
}
};
public NotificationPanelView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -201,7 +215,8 @@ public class NotificationPanelView extends PanelView implements
mScrollView.setListener(this);
mScrollView.setFocusable(false);
mReserveNotificationSpace = findViewById(R.id.reserve_notification_space);
mNotificationContainerParent = findViewById(R.id.notification_container_parent);
mNotificationContainerParent = (NotificationsQuickSettingsContainer)
findViewById(R.id.notification_container_parent);
mNotificationStackScroller = (NotificationStackScrollLayout)
findViewById(R.id.notification_stack_scroller);
mNotificationStackScroller.setOnHeightChangedListener(this);
@@ -317,6 +332,7 @@ public class NotificationPanelView extends PanelView implements
if (mQsSizeChangeAnimator == null) {
mQsContainer.setHeightOverride(mQsContainer.getDesiredHeight());
}
updateMaxHeadsUpTranslation();
}
@Override
@@ -492,6 +508,15 @@ public class NotificationPanelView extends PanelView implements
super.fling(vel, expand);
}
@Override
protected void flingToHeight(float vel, boolean expand, float target) {
if (!expand && mHeadsUpManager.hasPinnedHeadsUp()) {
target = mHeadsUpManager.getHighestPinnedHeadsUp();
}
mHeadsUpTouchHelper.notifyFling(!expand);
super.flingToHeight(vel, expand, target);
}
@Override
public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
@@ -499,16 +524,27 @@ public class NotificationPanelView extends PanelView implements
mLastAnnouncementWasQuickSettings = false;
return true;
}
return super.dispatchPopulateAccessibilityEventInternal(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
public boolean
onInterceptTouchEvent(MotionEvent event) {
if (mBlockTouches) {
return false;
}
initDownStates(event);
if (mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
mExpansionIsFromHeadsUp = true;
return true;
}
if (onQsIntercept(event)) {
return true;
}
return super.onInterceptTouchEvent(event);
}
private boolean onQsIntercept(MotionEvent event) {
int pointerIndex = event.findPointerIndex(mTrackingPointer);
if (pointerIndex < 0) {
pointerIndex = 0;
@@ -583,7 +619,7 @@ public class NotificationPanelView extends PanelView implements
mIntercepting = false;
break;
}
return super.onInterceptTouchEvent(event);
return false;
}
@Override
@@ -652,6 +688,15 @@ public class NotificationPanelView extends PanelView implements
if (mOnlyAffordanceInThisMotion) {
return true;
}
mHeadsUpTouchHelper.onTouchEvent(event);
if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQSTouch(event)) {
return true;
}
super.onTouchEvent(event);
return true;
}
private boolean handleQSTouch(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f
&& mStatusBar.getBarState() != StatusBarState.KEYGUARD && !mQsExpanded
&& mQsExpansionEnabled) {
@@ -691,8 +736,7 @@ public class NotificationPanelView extends PanelView implements
// earlier so the state is already up to date when dragging down.
setListening(true);
}
super.onTouchEvent(event);
return true;
return false;
}
private boolean isInQsArea(float x, float y) {
@@ -834,13 +878,13 @@ public class NotificationPanelView extends PanelView implements
setQsExpansion(mQsExpansionHeight);
flingSettings(!mQsExpansionEnabled && open ? 0f : velocity, open && mQsExpansionEnabled,
new Runnable() {
@Override
public void run() {
mStackScrollerOverscrolling = false;
mQsExpansionFromOverscroll = false;
updateQsState();
}
});
@Override
public void run() {
mStackScrollerOverscrolling = false;
mQsExpansionFromOverscroll = false;
updateQsState();
}
});
}
private void onQsExpansionStarted() {
@@ -869,6 +913,7 @@ public class NotificationPanelView extends PanelView implements
mNotificationStackScroller.setInterceptDelegateEnabled(expanded);
mStatusBar.setQsExpanded(expanded);
mQsPanel.setExpanded(expanded);
mNotificationContainerParent.setQsExpanded(expanded);
}
}
@@ -1056,7 +1101,7 @@ public class NotificationPanelView extends PanelView implements
mKeyguardBottomArea.animate()
.alpha(0f)
.setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
.setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
.setDuration(mStatusBar.getKeyguardFadingAwayDuration() / 2)
.setInterpolator(PhoneStatusBar.ALPHA_OUT)
.withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable)
.start();
@@ -1402,6 +1447,8 @@ public class NotificationPanelView extends PanelView implements
updateHeader();
updateUnlockIcon();
updateNotificationTranslucency();
mHeadsUpManager.setIsExpanded(!isShadeCollapsed());
mNotificationStackScroller.setShadeExpanded(!isShadeCollapsed());
if (DEBUG) {
invalidate();
}
@@ -1544,7 +1591,14 @@ public class NotificationPanelView extends PanelView implements
return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight;
}
}
return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR;
float paddingOffset = mNotificationStackScroller.getPaddingOffset();
float translation = paddingOffset / HEADER_RUBBERBAND_FACTOR;
if (mHeadsUpManager.hasPinnedHeadsUp() || mExpansionIsFromHeadsUp) {
translation = mNotificationStackScroller.getTopPadding()
+ mNotificationStackScroller.getPaddingOffset()
- mNotificationTopPadding - mQsMinExpansionHeight;
}
return Math.min(0, translation);
}
/**
@@ -1605,8 +1659,10 @@ public class NotificationPanelView extends PanelView implements
protected void onExpandingFinished() {
super.onExpandingFinished();
mNotificationStackScroller.onExpansionStopped();
mHeadsUpManager.onExpandingFinished();
mIsExpanding = false;
mScrollYOverride = -1;
// TODO: look into whether this is still correct
if (mExpandedHeight == 0f) {
setListening(false);
} else {
@@ -1614,6 +1670,8 @@ public class NotificationPanelView extends PanelView implements
}
mQsExpandImmediate = false;
mTwoFingerQsExpandPossible = false;
mExpansionIsFromHeadsUp = false;
mNotificationStackScroller.setTrackingHeadsUp(mHeadsUpTouchHelper.isTrackingHeadsUp());
}
private void setListening(boolean listening) {
@@ -1708,6 +1766,17 @@ public class NotificationPanelView extends PanelView implements
mAfforanceHelper.onConfigurationChanged();
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
mBottomBarHeight = insets.getSystemWindowInsetBottom();
updateMaxHeadsUpTranslation();
return insets;
}
private void updateMaxHeadsUpTranslation() {
mNotificationStackScroller.setMaxHeadsUpTranslation(getHeight() - mBottomBarHeight);
}
@Override
public void onRtlPropertiesChanged(int layoutDirection) {
if (layoutDirection != mOldLayoutDirection) {
@@ -2066,4 +2135,44 @@ public class NotificationPanelView extends PanelView implements
mNotificationStackScroller.getTopPadding(), p);
}
}
@Override
public void OnPinnedHeadsUpExistChanged(final boolean exist, boolean changeImmediatly) {
if (exist != mPinnedHeadsUpExist) {
mPinnedHeadsUpExist = exist;
if (!exist) {
mNotificationStackScroller.performOnAnimationFinished(
mHeadsUpExistenceChangedRunnable);
} else {
mHeadsUpExistenceChangedRunnable.run();
}
}
}
@Override
public void OnHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
// TODO: figure out the conditions when not to generate an animation
mNotificationStackScroller.generateHeadsUpAnimation(entry.row, isHeadsUp);
if (isShadeCollapsed()) {
setExpandedHeight(mHeadsUpManager.getHighestPinnedHeadsUp());
}
}
private boolean isShadeCollapsed() {
// TODO: handle this cleaner
return mHeader.getTranslationY() + mHeader.getCollapsedHeight() <= 0;
}
@Override
public void setHeadsUpManager(HeadsUpManager headsUpManager) {
super.setHeadsUpManager(headsUpManager);
mHeadsUpTouchHelper.bind(headsUpManager, mNotificationStackScroller, this);
}
public void setTrackingHeadsUp(boolean tracking) {
if (tracking) {
// otherwise we update the state when the expansion is finished
mNotificationStackScroller.setTrackingHeadsUp(true);
}
}
}

View File

@@ -37,6 +37,7 @@ public class NotificationsQuickSettingsContainer extends FrameLayout
private View mStackScroller;
private View mKeyguardStatusBar;
private boolean mInflated;
private boolean mQsExpanded;
public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -64,26 +65,29 @@ public class NotificationsQuickSettingsContainer extends FrameLayout
boolean userSwitcherVisible = mInflated && mUserSwitcher.getVisibility() == View.VISIBLE;
boolean statusBarVisible = mKeyguardStatusBar.getVisibility() == View.VISIBLE;
View stackQsTop = mQsExpanded ? mStackScroller : mScrollView;
View stackQsBottom = !mQsExpanded ? mStackScroller : mScrollView;
// Invert the order of the scroll view and user switcher such that the notifications receive
// touches first but the panel gets drawn above.
if (child == mScrollView) {
return super.drawChild(canvas, mStackScroller, drawingTime);
} else if (child == mStackScroller) {
return super.drawChild(canvas,
userSwitcherVisible && statusBarVisible ? mUserSwitcher
return super.drawChild(canvas, userSwitcherVisible && statusBarVisible ? mUserSwitcher
: statusBarVisible ? mKeyguardStatusBar
: userSwitcherVisible ? mUserSwitcher
: mScrollView,
: stackQsBottom, drawingTime);
} else if (child == mStackScroller) {
return super.drawChild(canvas,
userSwitcherVisible && statusBarVisible ? mKeyguardStatusBar
: statusBarVisible || userSwitcherVisible ? stackQsBottom
: stackQsTop,
drawingTime);
} else if (child == mUserSwitcher) {
return super.drawChild(canvas,
userSwitcherVisible && statusBarVisible ? mKeyguardStatusBar
: mScrollView,
userSwitcherVisible && statusBarVisible ? stackQsBottom
: stackQsTop,
drawingTime);
} else if (child == mKeyguardStatusBar) {
return super.drawChild(canvas,
userSwitcherVisible && statusBarVisible ? mScrollView
: mScrollView,
stackQsTop,
drawingTime);
}else {
return super.drawChild(canvas, child, drawingTime);
@@ -97,4 +101,11 @@ public class NotificationsQuickSettingsContainer extends FrameLayout
mInflated = true;
}
}
public void setQsExpanded(boolean expanded) {
if (mQsExpanded != expanded) {
mQsExpanded = expanded;
invalidate();
}
}
}

View File

@@ -157,8 +157,7 @@ public class PanelBar extends FrameLayout {
if (DEBUG) LOG("panelExpansionChanged: start state=%d panel=%s", mState, panel.getName());
mPanelExpandedFractionSum = 0f;
for (PanelView pv : mPanels) {
boolean visible = pv.getExpandedHeight() > 0;
pv.setVisibility(visible ? View.VISIBLE : View.GONE);
pv.setVisibility(expanded ? View.VISIBLE : View.INVISIBLE);
// adjust any other panels that may be partially visible
if (expanded) {
if (mState == STATE_CLOSED) {
@@ -167,7 +166,7 @@ public class PanelBar extends FrameLayout {
}
fullyClosed = false;
final float thisFrac = pv.getExpandedFraction();
mPanelExpandedFractionSum += (visible ? thisFrac : 0);
mPanelExpandedFractionSum += thisFrac;
if (DEBUG) LOG("panelExpansionChanged: -> %s: f=%.1f", pv.getName(), thisFrac);
if (panel == pv) {
if (thisFrac == 1f) fullyOpenedPanel = panel;
@@ -196,7 +195,6 @@ public class PanelBar extends FrameLayout {
} else {
pv.resetViews();
pv.setExpandedFraction(0); // just in case
pv.setVisibility(View.GONE);
pv.cancelPeek();
}
}

View File

@@ -38,6 +38,7 @@ import com.android.systemui.R;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -45,6 +46,7 @@ import java.io.PrintWriter;
public abstract class PanelView extends FrameLayout {
public static final boolean DEBUG = PanelBar.DEBUG;
public static final String TAG = PanelView.class.getSimpleName();
protected HeadsUpManager mHeadsUpManager;
private final void logf(String fmt, Object... args) {
Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
@@ -238,10 +240,7 @@ public abstract class PanelView extends FrameLayout {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitialTouchY = y;
mInitialTouchX = x;
mInitialOffsetOnTouch = mExpandedHeight;
mTouchSlopExceeded = false;
startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
mJustPeeked = false;
mPanelClosedOnDown = mExpandedHeight == 0.0f;
mHasLayoutedSinceDown = false;
@@ -274,9 +273,7 @@ public abstract class PanelView extends FrameLayout {
final float newY = event.getY(newIndex);
final float newX = event.getX(newIndex);
mTrackingPointer = event.getPointerId(newIndex);
mInitialOffsetOnTouch = mExpandedHeight;
mInitialTouchY = newY;
mInitialTouchX = newX;
startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
@@ -297,9 +294,7 @@ public abstract class PanelView extends FrameLayout {
mTouchSlopExceeded = true;
if (waitForTouchSlop && !mTracking) {
if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
mInitialOffsetOnTouch = mExpandedHeight;
mInitialTouchX = x;
mInitialTouchY = y;
startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
h = 0;
}
cancelHeightAnimator();
@@ -334,6 +329,17 @@ public abstract class PanelView extends FrameLayout {
return !waitForTouchSlop || mTracking;
}
protected void startExpandMotion(float newX, float newY, boolean startTracking,
float expandedHeight) {
mInitialOffsetOnTouch = expandedHeight;
mInitialTouchY = newY;
mInitialTouchX = newX;
if (startTracking) {
mTouchSlopExceeded = true;
onTrackingStarted();
}
}
private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
mTrackingPointer = -1;
if ((mTracking && mTouchSlopExceeded)
@@ -474,12 +480,7 @@ public abstract class PanelView extends FrameLayout {
if (scrolledToBottom || mTouchStartedInEmptyArea) {
if (h < -mTouchSlop && h < -Math.abs(x - mInitialTouchX)) {
cancelHeightAnimator();
mInitialOffsetOnTouch = mExpandedHeight;
mInitialTouchY = y;
mInitialTouchX = x;
mTracking = true;
mTouchSlopExceeded = true;
onTrackingStarted();
startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
return true;
}
}
@@ -564,7 +565,10 @@ public abstract class PanelView extends FrameLayout {
protected void fling(float vel, boolean expand) {
cancelPeek();
float target = expand ? getMaxPanelHeight() : 0.0f;
flingToHeight(vel, expand, target);
}
protected void flingToHeight(float vel, boolean expand, float target) {
// Hack to make the expand transition look nice when clear all button is visible - we make
// the animation only to the last notification, and then jump to the maximum panel height so
// clear all just fades in and the decelerating motion is towards the last notification.
@@ -644,7 +648,7 @@ public abstract class PanelView extends FrameLayout {
mHasLayoutedSinceDown = true;
if (mUpdateFlingOnLayout) {
abortAnimations();
fling(mUpdateFlingVelocity, true);
fling(mUpdateFlingVelocity, true /* expands */);
mUpdateFlingOnLayout = false;
}
}
@@ -805,7 +809,7 @@ public abstract class PanelView extends FrameLayout {
if (mExpanding) {
notifyExpandingFinished();
}
setVisibility(VISIBLE);
notifyBarPanelExpansionChanged();
// Wait for window manager to pickup the change, so we know the maximum height of the panel
// then.
@@ -941,9 +945,9 @@ public abstract class PanelView extends FrameLayout {
return animator;
}
private void notifyBarPanelExpansionChanged() {
protected void notifyBarPanelExpansionChanged() {
mBar.panelExpansionChanged(this, mExpandedFraction, mExpandedFraction > 0f || mPeekPending
|| mPeekAnimator != null);
|| mPeekAnimator != null || mInstantExpanding || mHeadsUpManager.hasPinnedHeadsUp());
}
/**
@@ -1014,4 +1018,8 @@ public abstract class PanelView extends FrameLayout {
* @return the height of the clear all button, in pixels
*/
protected abstract int getClearAllHeight();
public void setHeadsUpManager(HeadsUpManager headsUpManager) {
mHeadsUpManager = headsUpManager;
}
}

View File

@@ -17,19 +17,6 @@
package com.android.systemui.statusbar.phone;
import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
import static android.app.StatusBarManager.windowStateToString;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.NonNull;
@@ -92,7 +79,6 @@ import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewStub;
import android.view.WindowManager;
@@ -137,7 +123,6 @@ import com.android.systemui.statusbar.NotificationOverflowContainer;
import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.SignalClusterView;
import com.android.systemui.statusbar.SpeedBumpView;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener;
import com.android.systemui.statusbar.policy.AccessibilityController;
@@ -147,7 +132,7 @@ import com.android.systemui.statusbar.policy.BluetoothControllerImpl;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.statusbar.policy.CastControllerImpl;
import com.android.systemui.statusbar.policy.FlashlightController;
import com.android.systemui.statusbar.policy.HeadsUpNotificationView;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.HotspotControllerImpl;
import com.android.systemui.statusbar.policy.KeyButtonView;
import com.android.systemui.statusbar.policy.KeyguardMonitor;
@@ -172,11 +157,26 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
import static android.app.StatusBarManager.windowStateToString;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING;
public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener {
DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener, HeadsUpManager.OnHeadsUpChangedListener {
static final String TAG = "PhoneStatusBar";
public static final boolean DEBUG = BaseStatusBar.DEBUG;
public static final boolean SPEW = false;
@@ -360,11 +360,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
if (wasUsing != mUseHeadsUp) {
if (!mUseHeadsUp) {
Log.d(TAG, "dismissing any existing heads up notification on disable event");
setHeadsUpVisibility(false);
mHeadsUpNotificationView.releaseImmediately();
removeHeadsUpView();
} else {
addHeadsUpView();
mHeadsUpManager.releaseAllImmediately();
}
}
}
@@ -528,6 +524,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
};
private HashMap<ExpandableNotificationRow, List<ExpandableNotificationRow>> mTmpChildOrderMap
= new HashMap<>();
private HashSet<Entry> mHeadsUpEntriesToRemoveOnSwitch = new HashSet<>();
private RankingMap mLatestRankingMap;
@Override
public void start() {
@@ -598,7 +596,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
}
}
return mStatusBarWindow.onTouchEvent(event);
}});
}
});
mStatusBarView = (PhoneStatusBarView) mStatusBarWindow.findViewById(R.id.status_bar);
mStatusBarView.setBar(this);
@@ -615,12 +614,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
mNotificationPanel.setBackground(new FastColorDrawable(context.getColor(
R.color.notification_panel_solid_background)));
}
if (ENABLE_HEADS_UP) {
mHeadsUpNotificationView =
(HeadsUpNotificationView) View.inflate(context, R.layout.heads_up, null);
mHeadsUpNotificationView.setVisibility(View.GONE);
mHeadsUpNotificationView.setBar(this);
}
mHeadsUpManager = new HeadsUpManager(context);
mHeadsUpManager.setBar(this);
mHeadsUpManager.addListener(this);
mHeadsUpManager.addListener(mNotificationPanel);
mNotificationPanel.setHeadsUpManager(mHeadsUpManager);
if (MULTIUSER_DEBUG) {
mNotificationPanelDebugText = (TextView) mNotificationPanel.findViewById(
R.id.header_debug_info);
@@ -667,6 +667,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
mStackScroller.setLongPressListener(getNotificationLongClicker());
mStackScroller.setPhoneStatusBar(this);
mStackScroller.setGroupManager(mGroupManager);
mStackScroller.setHeadsUpManager(mHeadsUpManager);
mGroupManager.setOnGroupChangeListener(mStackScroller);
mKeyguardIconOverflowContainer =
@@ -1084,32 +1085,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
return lp;
}
private void addHeadsUpView() {
int headsUpHeight = mContext.getResources()
.getDimensionPixelSize(R.dimen.heads_up_window_height);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
LayoutParams.MATCH_PARENT, headsUpHeight,
WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL, // above the status bar!
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
| WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
PixelFormat.TRANSLUCENT);
lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
lp.gravity = Gravity.TOP;
lp.setTitle("Heads Up");
lp.packageName = mContext.getPackageName();
lp.windowAnimations = R.style.Animation_StatusBar_HeadsUp;
mWindowManager.addView(mHeadsUpNotificationView, lp);
}
private void removeHeadsUpView() {
mWindowManager.removeView(mHeadsUpNotificationView);
}
public void addIcon(String slot, int index, int viewIndex, StatusBarIcon icon) {
mIconController.addSystemIcon(slot, index, viewIndex, icon);
}
@@ -1131,30 +1106,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
public void addNotification(StatusBarNotification notification, RankingMap ranking,
Entry oldEntry) {
if (DEBUG) Log.d(TAG, "addNotification key=" + notification.getKey());
if (mUseHeadsUp && shouldInterrupt(notification)) {
if (DEBUG) Log.d(TAG, "launching notification in heads up mode");
Entry interruptionCandidate = oldEntry;
if (interruptionCandidate == null) {
final StatusBarIconView iconView = createIcon(notification);
if (iconView == null) {
return;
}
interruptionCandidate = new Entry(notification, iconView);
}
ViewGroup holder = mHeadsUpNotificationView.getHolder();
if (inflateViewsForHeadsUp(interruptionCandidate, holder)) {
// 1. Populate mHeadsUpNotificationView
mHeadsUpNotificationView.showNotification(interruptionCandidate);
boolean isHeadsUp = mUseHeadsUp && shouldInterrupt(notification);
// do not show the notification in the shade, yet.
return;
}
}
Entry shadeEntry = createNotificationViews(notification);
Entry shadeEntry = createNotificationViews(notification, isHeadsUp);
if (shadeEntry == null) {
return;
}
if (isHeadsUp) {
mHeadsUpManager.showNotification(shadeEntry);
}
if (notification.getNotification().fullScreenIntent != null) {
// Stop screensaver if the notification has a full-screen intent.
@@ -1175,18 +1135,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
setAreThereNotifications();
}
public void displayNotificationFromHeadsUp(Entry shadeEntry) {
// The notification comes from the headsup, let's inflate the normal layout again
inflateViews(shadeEntry, mStackScroller);
shadeEntry.setInterruption();
shadeEntry.row.setHeadsUp(false);
addNotificationViews(shadeEntry, null);
// Recalculate the position of the sliding windows and the titles.
setAreThereNotifications();
}
@Override
protected void updateNotificationRanking(RankingMap ranking) {
mNotificationData.updateRanking(ranking);
@@ -1195,10 +1143,15 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
@Override
public void removeNotification(String key, RankingMap ranking) {
if (ENABLE_HEADS_UP) {
mHeadsUpNotificationView.removeNotification(key);
boolean defferRemoval = false;
if (mHeadsUpManager.isHeadsUp(key)) {
defferRemoval = !mHeadsUpManager.removeNotification(key);
}
if (defferRemoval) {
mLatestRankingMap = ranking;
mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
return;
}
StatusBarNotification old = removeNotificationViews(key, ranking);
if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old);
@@ -1870,6 +1823,38 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
logStateToEventlog();
}
@Override
public void OnPinnedHeadsUpExistChanged(boolean exist, boolean changeImmediatly) {
if (exist) {
mStatusBarWindowManager.setHeadsUpShowing(true);
} else {
Runnable endRunnable = new Runnable() {
@Override
public void run() {
if (!mHeadsUpManager.hasPinnedHeadsUp()) {
mStatusBarWindowManager.setHeadsUpShowing(false);
}
}
};
if (changeImmediatly) {
endRunnable.run();
} else {
mStackScroller.performOnAnimationFinished(endRunnable);
}
}
}
@Override
public void OnHeadsUpStateChanged(Entry entry, boolean isHeadsUp) {
if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) {
removeNotification(entry.key, mLatestRankingMap);
mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) {
mLatestRankingMap = null;
}
}
}
/**
* All changes to the status bar and notifications funnel through here and are batched.
*/
@@ -1886,15 +1871,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
case MSG_CLOSE_PANELS:
animateCollapsePanels();
break;
case MSG_SHOW_HEADS_UP:
setHeadsUpVisibility(true);
break;
case MSG_ESCALATE_HEADS_UP:
escalateHeadsUp();
case MSG_HIDE_HEADS_UP:
mHeadsUpNotificationView.releaseImmediately();
setHeadsUpVisibility(false);
break;
case MSG_LAUNCH_TRANSITION_TIMEOUT:
onLaunchTransitionTimeout();
break;
@@ -1903,44 +1879,16 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
}
@Override
public void scheduleHeadsUpDecay(long delay) {
mHandler.removeMessages(MSG_HIDE_HEADS_UP);
if (mHeadsUpNotificationView.isClearable()) {
mHandler.sendEmptyMessageDelayed(MSG_HIDE_HEADS_UP, delay);
}
}
@Override
public void scheduleHeadsUpOpen() {
mHandler.removeMessages(MSG_HIDE_HEADS_UP);
mHandler.removeMessages(MSG_SHOW_HEADS_UP);
mHandler.sendEmptyMessage(MSG_SHOW_HEADS_UP);
}
@Override
public void scheduleHeadsUpClose() {
mHandler.removeMessages(MSG_HIDE_HEADS_UP);
if (mHeadsUpNotificationView.getVisibility() != View.GONE) {
mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP);
}
}
@Override
public void scheduleHeadsUpEscalation() {
mHandler.removeMessages(MSG_HIDE_HEADS_UP);
mHandler.removeMessages(MSG_ESCALATE_HEADS_UP);
mHandler.sendEmptyMessage(MSG_ESCALATE_HEADS_UP);
}
/** if the interrupting notification had a fullscreen intent, fire it now. */
private void escalateHeadsUp() {
if (mHeadsUpNotificationView.getEntry() != null) {
final StatusBarNotification sbn = mHeadsUpNotificationView.getEntry().notification;
mHeadsUpNotificationView.releaseImmediately();
public void escalateHeadsUp() {
TreeMap<String, HeadsUpManager.HeadsUpEntry> entries = mHeadsUpManager.getEntries();
for (String key : entries.keySet()) {
Entry entry = entries.get(key).entry;
final StatusBarNotification sbn = entry.notification;
final Notification notification = sbn.getNotification();
if (notification.fullScreenIntent != null) {
if (DEBUG)
if (DEBUG) {
Log.d(TAG, "converting a heads up to fullScreen");
}
try {
EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_ESCALATION,
sbn.getKey());
@@ -1949,6 +1897,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
}
}
}
mHeadsUpManager.releaseAllImmediately();
}
boolean panelsEnabled() {
@@ -2081,10 +2030,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
// Ensure the panel is fully collapsed (just in case; bug 6765842, 7260868)
mStatusBarView.collapseAllPanels(/*animate=*/ false, false /* delayed*/);
// reset things to their proper state
mStackScroller.setVisibility(View.VISIBLE);
mNotificationPanel.setVisibility(View.GONE);
mNotificationPanel.closeQs();
mExpandedVisible = false;
@@ -2478,8 +2423,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
pw.println(Settings.Global.zenModeToString(mZenMode));
pw.print(" mUseHeadsUp=");
pw.println(mUseHeadsUp);
pw.print(" interrupting package: ");
pw.println(hunStateToString(mHeadsUpNotificationView.getEntry()));
dumpBarTransitions(pw, "mStatusBarView", mStatusBarView.getBarTransitions());
if (mNavigationBarView != null) {
pw.print(" mNavigationBarWindowState=");
@@ -2571,10 +2514,10 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
if (mSecurityController != null) {
mSecurityController.dump(fd, pw, args);
}
if (mHeadsUpNotificationView != null) {
mHeadsUpNotificationView.dump(fd, pw, args);
if (mHeadsUpManager != null) {
mHeadsUpManager.dump(fd, pw, args);
} else {
pw.println(" mHeadsUpNotificationView: null");
pw.println(" mHeadsUpManager: null");
}
pw.println("SharedPreferences:");
@@ -2672,7 +2615,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
mScreenOn = false;
notifyNavigationBarScreenOn(false);
notifyHeadsUpScreenOn(false);
notifyHeadsUpScreenOff();
finishBarAnimations();
resetUserExpandedStates();
}
@@ -2764,15 +2707,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
mUserSetupObserver, mCurrentUserId);
}
private void setHeadsUpVisibility(boolean vis) {
if (!ENABLE_HEADS_UP) return;
if (DEBUG) Log.v(TAG, (vis ? "showing" : "hiding") + " heads up window");
EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_STATUS,
vis ? mHeadsUpNotificationView.getKey() : "",
vis ? 1 : 0);
mHeadsUpNotificationView.setVisibility(vis ? View.VISIBLE : View.GONE);
}
/**
* Reload some of our resources when the configuration changes.
*
@@ -2791,9 +2725,6 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
if (mNotificationPanel != null) {
mNotificationPanel.updateResources();
}
if (mHeadsUpNotificationView != null) {
mHeadsUpNotificationView.updateResources();
}
if (mBrightnessMirrorController != null) {
mBrightnessMirrorController.updateResources();
}

View File

@@ -130,7 +130,7 @@ public class StatusBarWindowManager {
private void applyHeight(State state) {
boolean expanded = state.isKeyguardShowingAndNotOccluded() || state.statusBarExpanded
|| state.keyguardFadingAway || state.bouncerShowing;
|| state.keyguardFadingAway || state.bouncerShowing || state.headsUpShowing;
if (expanded) {
mLpChanged.height = ViewGroup.LayoutParams.MATCH_PARENT;
} else {
@@ -218,6 +218,11 @@ public class StatusBarWindowManager {
apply(mCurrentState);
}
public void setHeadsUpShowing(boolean showing) {
mCurrentState.headsUpShowing = showing;
apply(mCurrentState);
}
/**
* @param state The {@link StatusBarState} of the status bar.
*/
@@ -235,6 +240,7 @@ public class StatusBarWindowManager {
boolean bouncerShowing;
boolean keyguardFadingAway;
boolean qsExpanded;
boolean headsUpShowing;
/**
* The {@link BaseStatusBar} state from the status bar.

View File

@@ -0,0 +1,474 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.policy;
import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pools;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import com.android.systemui.R;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Stack;
import java.util.TreeMap;
public class HeadsUpManager {
private static final String TAG = "HeadsUpManager";
private static final boolean DEBUG = false;
private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
private final int mHeadsUpNotificationDecay;
private final int mMinimumDisplayTime;
private final int mTouchSensitivityDelay;
private final ArrayMap<String, Long> mSnoozedPackages;
private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
private final int mDefaultSnoozeLengthMs;
private final Handler mHandler = new Handler();
private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() {
private Stack<HeadsUpEntry> mPoolObjects = new Stack<>();
@Override
public HeadsUpEntry acquire() {
if (!mPoolObjects.isEmpty()) {
return mPoolObjects.pop();
}
return new HeadsUpEntry();
}
@Override
public boolean release(HeadsUpEntry instance) {
instance.removeAutoCancelCallbacks();
mPoolObjects.push(instance);
return true;
}
};
private PhoneStatusBar mBar;
private int mSnoozeLengthMs;
private ContentObserver mSettingsObserver;
private TreeMap<String ,HeadsUpEntry> mHeadsUpEntries = new TreeMap<>();
private HashSet<String> mSwipedOutKeys = new HashSet<>();
private int mUser;
private Clock mClock;
private boolean mReleaseOnExpandFinish;
private boolean mTrackingHeadsUp;
private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>();
private boolean mIsExpanded;
private boolean mHasPinnedHeadsUp;
public HeadsUpManager(final Context context) {
Resources resources = context.getResources();
mTouchSensitivityDelay = resources.getInteger(R.integer.heads_up_sensitivity_delay);
if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay);
mSnoozedPackages = new ArrayMap<>();
mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
mSnoozeLengthMs = mDefaultSnoozeLengthMs;
mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
mHeadsUpNotificationDecay = 2000000;
mClock = new Clock();
// TODO: shadow mSwipeHelper.setMaxSwipeProgress(mMaxAlpha);
mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(),
SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
mSettingsObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
final int packageSnoozeLengthMs = Settings.Global.getInt(
context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
mSnoozeLengthMs = packageSnoozeLengthMs;
if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
}
}
};
context.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
mSettingsObserver);
if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
// TODO: investigate whether this is still needed
// if (!mHeadsUpEntries.isEmpty()) {
// whoops, we're on already!
// showNotification(mHeadsUpEntries);
// }
}
public void setBar(PhoneStatusBar bar) {
mBar = bar;
}
public void addListener(OnHeadsUpChangedListener listener) {
mListeners.add(listener);
}
public PhoneStatusBar getBar() {
return mBar;
}
/**
* Called when posting a new notification to the heads up.
*/
public void showNotification(NotificationData.Entry headsUp) {
if (DEBUG) Log.v(TAG, "showNotification");
addHeadsUpEntry(headsUp);
updateNotification(headsUp, true);
headsUp.setInterruption();
updatePinnedHeadsUpState(false);
}
/**
* Called when updating or posting a notification to the heads up.
*/
public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
if (DEBUG) Log.v(TAG, "updateNotification");
headsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */);
headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
if (alert) {
HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key);
headsUpEntry.updateEntry();
headsUpEntry.entry.row.setInShade(mIsExpanded);
}
}
private void addHeadsUpEntry(NotificationData.Entry entry) {
boolean wasEmpty = mHeadsUpEntries.isEmpty();
HeadsUpEntry headsUpEntry = mEntryPool.acquire();
headsUpEntry.setEntry(entry);
mHeadsUpEntries.put(entry.key, headsUpEntry);
for (OnHeadsUpChangedListener listener : mListeners) {
listener.OnHeadsUpStateChanged(entry, true);
}
entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
entry.row.setHeadsUp(true);
}
private void removeHeadsUpEntry(NotificationData.Entry entry) {
HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key);
mEntryPool.release(remove);
entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
entry.row.setHeadsUp(false);
for (OnHeadsUpChangedListener listener : mListeners) {
listener.OnHeadsUpStateChanged(entry, false);
}
updatePinnedHeadsUpState(false);
}
private void updatePinnedHeadsUpState(boolean forceImmediate) {
boolean hasPinnedHeadsUp = hasPinnedHeadsUpInternal();
if (hasPinnedHeadsUp == mHasPinnedHeadsUp) {
return;
}
mHasPinnedHeadsUp = hasPinnedHeadsUp;
for (OnHeadsUpChangedListener listener :mListeners) {
listener.OnPinnedHeadsUpExistChanged(hasPinnedHeadsUp, forceImmediate);
}
}
/**
* React to the removal of the notification in the heads up.
*
* @return true if the notification was removed and false if it still needs to be kept around
* for a bit since it wasn't shown long enough
*/
public boolean removeNotification(String key) {
if (DEBUG) Log.v(TAG, "remove");
if (wasShownLongEnough(key)) {
releaseImmediately(key);
return true;
} else {
getHeadsUpEntry(key).hideAsSoonAsPossible();
return false;
}
}
private boolean wasShownLongEnough(String key) {
HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
HeadsUpEntry topEntry = getTopEntry();
if (mSwipedOutKeys.contains(key)) {
// We always instantly dismiss views being manually swiped out.
mSwipedOutKeys.remove(key);
return true;
}
if (headsUpEntry != topEntry) {
return true;
}
return headsUpEntry.wasShownLongEnough();
}
public boolean isHeadsUp(String key) {
return mHeadsUpEntries.containsKey(key);
}
/**
* Push any current Heads Up notification down into the shade.
*/
public void releaseAllImmediately() {
if (DEBUG) Log.v(TAG, "releaseAllImmediately");
for (String key: mHeadsUpEntries.keySet()) {
releaseImmediately(key);
}
}
public void releaseImmediately(String key) {
HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
if (headsUpEntry == null) {
return;
}
NotificationData.Entry shadeEntry = headsUpEntry.entry;
removeHeadsUpEntry(shadeEntry);
}
public boolean isSnoozed(String packageName) {
final String key = snoozeKey(packageName, mUser);
Long snoozedUntil = mSnoozedPackages.get(key);
if (snoozedUntil != null) {
if (snoozedUntil > SystemClock.elapsedRealtime()) {
if (DEBUG) Log.v(TAG, key + " snoozed");
return true;
}
mSnoozedPackages.remove(packageName);
}
return false;
}
public void snooze() {
for (String key: mHeadsUpEntries.keySet()) {
HeadsUpEntry entry = mHeadsUpEntries.get(key);
String packageName = entry.entry.notification.getPackageName();
mSnoozedPackages.put(snoozeKey(packageName, mUser),
SystemClock.elapsedRealtime() + mSnoozeLengthMs);
}
mReleaseOnExpandFinish = true;
}
private static String snoozeKey(String packageName, int user) {
return user + "," + packageName;
}
private HeadsUpEntry getHeadsUpEntry(String key) {
return mHeadsUpEntries.get(key);
}
public NotificationData.Entry getEntry(String key) {
return mHeadsUpEntries.get(key).entry;
}
public TreeMap<String, HeadsUpEntry> getEntries() {
return mHeadsUpEntries;
}
public HeadsUpEntry getTopEntry() {
return mHeadsUpEntries.isEmpty() ? null : mHeadsUpEntries.lastEntry().getValue();
}
/**
* @param key the key of the touched notification
* @return whether the touch is valid and should not be discarded
*/
public boolean shouldSwallowClick(String key) {
if (mClock.currentTimeMillis() < mHeadsUpEntries.get(key).postTime) {
return true;
}
return false;
}
public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
// TODO: handle the shadow
//getBackground().setAlpha((int) (255 * swipeProgress));
return false;
}
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
// TODO: Look into touchable region
// mContentHolder.getLocationOnScreen(mTmpTwoArray);
//
// info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
// info.touchableRegion.set(mTmpTwoArray[0], mTmpTwoArray[1],
// mTmpTwoArray[0] + mContentHolder.getWidth(),
// mTmpTwoArray[1] + mContentHolder.getHeight());
}
public void setUser(int user) {
mUser = user;
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("HeadsUpManager state:");
pw.print(" mTouchSensitivityDelay="); pw.println(mTouchSensitivityDelay);
pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
pw.print(" now="); pw.println(SystemClock.elapsedRealtime());
pw.print(" mUser="); pw.println(mUser);
for (String key: mHeadsUpEntries.keySet()) {
pw.print(" HeadsUpEntry="); pw.println(mHeadsUpEntries.get(key));
}
int N = mSnoozedPackages.size();
pw.println(" snoozed packages: " + N);
for (int i = 0; i < N; i++) {
pw.print(" "); pw.print(mSnoozedPackages.valueAt(i));
pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
}
}
public boolean hasPinnedHeadsUp() {
return mHasPinnedHeadsUp;
}
private boolean hasPinnedHeadsUpInternal() {
for (String key: mHeadsUpEntries.keySet()) {
HeadsUpEntry entry = mHeadsUpEntries.get(key);
if (!entry.entry.row.isInShade()) {
return true;
}
}
return false;
}
public void addSwipedOutKey(String key) {
mSwipedOutKeys.add(key);
}
public float getHighestPinnedHeadsUp() {
float max = 0;
for (String key: mHeadsUpEntries.keySet()) {
HeadsUpEntry entry = mHeadsUpEntries.get(key);
if (!entry.entry.row.isInShade()) {
max = Math.max(max, entry.entry.row.getActualHeight());
}
}
return max;
}
public void releaseAllToShade() {
for (String key: mHeadsUpEntries.keySet()) {
HeadsUpEntry entry = mHeadsUpEntries.get(key);
entry.entry.row.setInShade(true);
}
updatePinnedHeadsUpState(true);
}
public void onExpandingFinished() {
if (mReleaseOnExpandFinish) {
releaseAllImmediately();
mReleaseOnExpandFinish = false;
} else {
for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) {
removeHeadsUpEntry(entry);
}
mEntriesToRemoveAfterExpand.clear();
}
}
public void setTrackingHeadsUp(boolean trackingHeadsUp) {
mTrackingHeadsUp = trackingHeadsUp;
}
public void setIsExpanded(boolean isExpanded) {
mIsExpanded = isExpanded;
}
public int getTopHeadsUpHeight() {
HeadsUpEntry topEntry = getTopEntry();
return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0;
}
public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
public NotificationData.Entry entry;
public long postTime;
public long earliestRemovaltime;
private Runnable mRemoveHeadsUpRunnable;
public void setEntry(final NotificationData.Entry entry) {
this.entry = entry;
// The actual post time will be just after the heads-up really slided in
postTime = mClock.currentTimeMillis() + mTouchSensitivityDelay;
mRemoveHeadsUpRunnable = new Runnable() {
@Override
public void run() {
if (!mTrackingHeadsUp) {
removeHeadsUpEntry(entry);
} else {
mEntriesToRemoveAfterExpand.add(entry);
}
}
};
updateEntry();
}
public void updateEntry() {
long currentTime = mClock.currentTimeMillis();
postTime = Math.max(postTime, currentTime);
long finishTime = postTime + mHeadsUpNotificationDecay;
long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime);
earliestRemovaltime = currentTime + mMinimumDisplayTime;
removeAutoCancelCallbacks();
mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay);
}
@Override
public int compareTo(HeadsUpEntry o) {
return postTime < o.postTime ? -1
: postTime == o.postTime ? 0
: 1;
}
public void removeAutoCancelCallbacks() {
mHandler.removeCallbacks(mRemoveHeadsUpRunnable);
}
public boolean wasShownLongEnough() {
return earliestRemovaltime < mClock.currentTimeMillis();
}
public void hideAsSoonAsPossible() {
removeAutoCancelCallbacks();
mHandler.postDelayed(mRemoveHeadsUpRunnable,
earliestRemovaltime - mClock.currentTimeMillis());
}
}
public static class Clock {
public long currentTimeMillis() {
return SystemClock.elapsedRealtime();
}
}
public interface OnHeadsUpChangedListener {
void OnPinnedHeadsUpExistChanged(boolean exist, boolean changeImmediatly);
void OnHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp);
}
}

View File

@@ -1,622 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.policy;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Outline;
import android.graphics.Rect;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.ExpandHelper;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.R;
import com.android.systemui.SwipeHelper;
import com.android.systemui.statusbar.ExpandableView;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import java.io.FileDescriptor;
import java.io.PrintWriter;
public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper.Callback, ExpandHelper.Callback,
ViewTreeObserver.OnComputeInternalInsetsListener {
private static final String TAG = "HeadsUpNotificationView";
private static final boolean DEBUG = false;
private static final boolean SPEW = DEBUG;
private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
Rect mTmpRect = new Rect();
int[] mTmpTwoArray = new int[2];
private final int mHeadsUpNotificationDecay;
private final int mMinimumDisplayTime;
private final int mTouchSensitivityDelay;
private final float mMaxAlpha = 1f;
private final ArrayMap<String, Long> mSnoozedPackages;
private final int mDefaultSnoozeLengthMs;
private SwipeHelper mSwipeHelper;
private EdgeSwipeHelper mEdgeSwipeHelper;
private PhoneStatusBar mBar;
private long mLingerUntilMs;
private long mStartTouchTime;
private ViewGroup mContentHolder;
private int mSnoozeLengthMs;
private ContentObserver mSettingsObserver;
private NotificationData.Entry mHeadsUp;
private int mUser;
private String mMostRecentPackageName;
private boolean mTouched;
private Clock mClock;
public static class Clock {
public long currentTimeMillis() {
return SystemClock.elapsedRealtime();
}
}
public HeadsUpNotificationView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HeadsUpNotificationView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
Resources resources = context.getResources();
mTouchSensitivityDelay = resources.getInteger(R.integer.heads_up_sensitivity_delay);
if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay);
mSnoozedPackages = new ArrayMap<>();
mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms);
mSnoozeLengthMs = mDefaultSnoozeLengthMs;
mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
mClock = new Clock();
}
@VisibleForTesting
public HeadsUpNotificationView(Context context, Clock clock, SwipeHelper swipeHelper,
EdgeSwipeHelper edgeSwipeHelper, int headsUpNotificationDecay, int minimumDisplayTime,
int touchSensitivityDelay, int snoozeLength) {
super(context, null);
mClock = clock;
mSwipeHelper = swipeHelper;
mEdgeSwipeHelper = edgeSwipeHelper;
mMinimumDisplayTime = minimumDisplayTime;
mHeadsUpNotificationDecay = headsUpNotificationDecay;
mTouchSensitivityDelay = touchSensitivityDelay;
mSnoozedPackages = new ArrayMap<>();
mDefaultSnoozeLengthMs = snoozeLength;
}
public void updateResources() {
if (mContentHolder != null) {
final LayoutParams lp = (LayoutParams) mContentHolder.getLayoutParams();
lp.width = getResources().getDimensionPixelSize(R.dimen.notification_panel_width);
lp.gravity = getResources().getInteger(R.integer.notification_panel_layout_gravity);
mContentHolder.setLayoutParams(lp);
}
}
public void setBar(PhoneStatusBar bar) {
mBar = bar;
}
public PhoneStatusBar getBar() {
return mBar;
}
public ViewGroup getHolder() {
return mContentHolder;
}
/**
* Called when posting a new notification to the heads up.
*/
public void showNotification(NotificationData.Entry headsUp) {
if (DEBUG) Log.v(TAG, "showNotification");
if (mHeadsUp != null) {
// bump any previous heads up back to the shade
releaseImmediately();
}
mTouched = false;
updateNotification(headsUp, true);
mLingerUntilMs = mClock.currentTimeMillis() + mMinimumDisplayTime;
}
/**
* Called when updating or posting a notification to the heads up.
*/
public void updateNotification(NotificationData.Entry headsUp, boolean alert) {
if (DEBUG) Log.v(TAG, "updateNotification");
if (mHeadsUp == headsUp) {
resetViewForHeadsup();
// This is an in-place update. Noting more to do.
return;
}
mHeadsUp = headsUp;
if (mContentHolder != null) {
mContentHolder.removeAllViews();
}
if (mHeadsUp != null) {
mMostRecentPackageName = mHeadsUp.notification.getPackageName();
if (mHeadsUp.row != null) {
resetViewForHeadsup();
}
mStartTouchTime = SystemClock.elapsedRealtime() + mTouchSensitivityDelay;
if (mContentHolder != null) { // only null in tests and before we are attached to a window
mContentHolder.setX(0);
mContentHolder.setVisibility(View.VISIBLE);
mContentHolder.setAlpha(mMaxAlpha);
mContentHolder.addView(mHeadsUp.row);
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
mSwipeHelper.snapChild(mContentHolder, 1f);
}
mHeadsUp.setInterruption();
}
if (alert) {
// Make sure the heads up window is open.
mBar.scheduleHeadsUpOpen();
mBar.scheduleHeadsUpDecay(mHeadsUpNotificationDecay);
}
}
private void resetViewForHeadsup() {
if (mHeadsUp.row.areChildrenExpanded()) {
mHeadsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */);
}
mHeadsUp.row.setSystemExpanded(true);
mHeadsUp.row.setSensitive(false);
mHeadsUp.row.setHeadsUp(true);
mHeadsUp.row.setTranslationY(0);
mHeadsUp.row.setTranslationZ(0);
mHeadsUp.row.setHideSensitive(
false, false /* animated */, 0 /* delay */, 0 /* duration */);
}
/**
* Possibly enter the lingering state by delaying the closing of the window.
*
* @return true if the notification has entered the lingering state.
*/
private boolean startLingering(boolean removed) {
final long now = mClock.currentTimeMillis();
if (!mTouched && mHeadsUp != null && now < mLingerUntilMs) {
if (removed) {
mHeadsUp = null;
}
mBar.scheduleHeadsUpDecay(mLingerUntilMs - now);
return true;
}
return false;
}
/**
* React to the removal of the notification in the heads up.
*/
public void removeNotification(String key) {
if (DEBUG) Log.v(TAG, "remove");
if (mHeadsUp == null || !mHeadsUp.key.equals(key)) {
return;
}
if (!startLingering(/* removed */ true)) {
mHeadsUp = null;
releaseImmediately();
}
}
/**
* Ask for any current Heads Up notification to be pushed down into the shade.
*/
public void release() {
if (DEBUG) Log.v(TAG, "release");
if (!startLingering(/* removed */ false)) {
releaseImmediately();
}
}
/**
* Push any current Heads Up notification down into the shade.
*/
public void releaseImmediately() {
if (DEBUG) Log.v(TAG, "releaseImmediately");
if (mHeadsUp != null) {
mContentHolder.removeView(mHeadsUp.row);
mBar.displayNotificationFromHeadsUp(mHeadsUp);
}
mHeadsUp = null;
mBar.scheduleHeadsUpClose();
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (DEBUG) Log.v(TAG, "onVisibilityChanged: " + visibility);
if (changedView.getVisibility() == VISIBLE) {
mStartTouchTime = mClock.currentTimeMillis() + mTouchSensitivityDelay;
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
}
public boolean isSnoozed(String packageName) {
final String key = snoozeKey(packageName, mUser);
Long snoozedUntil = mSnoozedPackages.get(key);
if (snoozedUntil != null) {
if (snoozedUntil > SystemClock.elapsedRealtime()) {
if (DEBUG) Log.v(TAG, key + " snoozed");
return true;
}
mSnoozedPackages.remove(packageName);
}
return false;
}
private void snooze() {
if (mMostRecentPackageName != null) {
mSnoozedPackages.put(snoozeKey(mMostRecentPackageName, mUser),
SystemClock.elapsedRealtime() + mSnoozeLengthMs);
}
releaseImmediately();
}
private static String snoozeKey(String packageName, int user) {
return user + "," + packageName;
}
public boolean isShowing(String key) {
return mHeadsUp != null && mHeadsUp.key.equals(key);
}
public NotificationData.Entry getEntry() {
return mHeadsUp;
}
public boolean isClearable() {
return mHeadsUp == null || mHeadsUp.notification.isClearable();
}
// ViewGroup methods
private static final ViewOutlineProvider CONTENT_HOLDER_OUTLINE_PROVIDER =
new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
int outlineLeft = view.getPaddingLeft();
int outlineTop = view.getPaddingTop();
// Apply padding to shadow.
outline.setRect(outlineLeft, outlineTop,
view.getWidth() - outlineLeft - view.getPaddingRight(),
view.getHeight() - outlineTop - view.getPaddingBottom());
}
};
@Override
public void onAttachedToWindow() {
final ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
float touchSlop = viewConfiguration.getScaledTouchSlop();
mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, getContext());
mSwipeHelper.setMaxSwipeProgress(mMaxAlpha);
mEdgeSwipeHelper = new EdgeSwipeHelper(this, touchSlop);
int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height);
mContentHolder = (ViewGroup) findViewById(R.id.content_holder);
mContentHolder.setOutlineProvider(CONTENT_HOLDER_OUTLINE_PROVIDER);
mSnoozeLengthMs = Settings.Global.getInt(mContext.getContentResolver(),
SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs);
mSettingsObserver = new ContentObserver(getHandler()) {
@Override
public void onChange(boolean selfChange) {
final int packageSnoozeLengthMs = Settings.Global.getInt(
mContext.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
mSnoozeLengthMs = packageSnoozeLengthMs;
if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
}
}
};
mContext.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false,
mSettingsObserver);
if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
if (mHeadsUp != null) {
// whoops, we're on already!
showNotification(mHeadsUp);
}
getViewTreeObserver().addOnComputeInternalInsetsListener(this);
}
@Override
protected void onDetachedFromWindow() {
mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()");
if (mClock.currentTimeMillis() < mStartTouchTime) {
return true;
}
mTouched = true;
return mEdgeSwipeHelper.onInterceptTouchEvent(ev)
|| mSwipeHelper.onInterceptTouchEvent(ev)
|| mHeadsUp == null // lingering
|| super.onInterceptTouchEvent(ev);
}
// View methods
@Override
public void onDraw(android.graphics.Canvas c) {
super.onDraw(c);
if (DEBUG) {
//Log.d(TAG, "onDraw: canvas height: " + c.getHeight() + "px; measured height: "
// + getMeasuredHeight() + "px");
c.save();
c.clipRect(6, 6, c.getWidth() - 6, getMeasuredHeight() - 6,
android.graphics.Region.Op.DIFFERENCE);
c.drawColor(0xFFcc00cc);
c.restore();
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mClock.currentTimeMillis() < mStartTouchTime) {
return false;
}
final boolean wasRemoved = mHeadsUp == null;
if (!wasRemoved) {
mBar.scheduleHeadsUpDecay(mHeadsUpNotificationDecay);
}
return mEdgeSwipeHelper.onTouchEvent(ev)
|| mSwipeHelper.onTouchEvent(ev)
|| wasRemoved
|| super.onTouchEvent(ev);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
float densityScale = getResources().getDisplayMetrics().density;
mSwipeHelper.setDensityScale(densityScale);
float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
}
// ExpandHelper.Callback methods
@Override
public ExpandableView getChildAtRawPosition(float x, float y) {
return getChildAtPosition(x, y);
}
@Override
public ExpandableView getChildAtPosition(float x, float y) {
return mHeadsUp == null ? null : mHeadsUp.row;
}
@Override
public boolean canChildBeExpanded(View v) {
return mHeadsUp != null && mHeadsUp.row == v && mHeadsUp.row.isExpandable();
}
@Override
public void setUserExpandedChild(View v, boolean userExpanded) {
if (mHeadsUp != null && mHeadsUp.row == v) {
mHeadsUp.row.setUserExpanded(userExpanded);
}
}
@Override
public void setUserLockedChild(View v, boolean userLocked) {
if (mHeadsUp != null && mHeadsUp.row == v) {
mHeadsUp.row.setUserLocked(userLocked);
}
}
@Override
public void expansionStateChanged(boolean isExpanding) {
}
// SwipeHelper.Callback methods
@Override
public boolean canChildBeDismissed(View v) {
return true;
}
@Override
public boolean isAntiFalsingNeeded() {
return false;
}
@Override
public float getFalsingThresholdFactor() {
return 1.0f;
}
@Override
public void onChildDismissed(View v) {
Log.v(TAG, "User swiped heads up to dismiss");
if (mHeadsUp != null && mHeadsUp.notification.isClearable()) {
mBar.onNotificationClear(mHeadsUp.notification);
mHeadsUp = null;
}
releaseImmediately();
}
@Override
public void onBeginDrag(View v) {
}
@Override
public void onDragCancelled(View v) {
mContentHolder.setAlpha(mMaxAlpha); // sometimes this isn't quite reset
}
@Override
public void onChildSnappedBack(View animView) {
}
@Override
public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
getBackground().setAlpha((int) (255 * swipeProgress));
return false;
}
@Override
public View getChildAtPosition(MotionEvent ev) {
return mContentHolder;
}
@Override
public View getChildContentView(View v) {
return mContentHolder;
}
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
mContentHolder.getLocationOnScreen(mTmpTwoArray);
info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
info.touchableRegion.set(mTmpTwoArray[0], mTmpTwoArray[1],
mTmpTwoArray[0] + mContentHolder.getWidth(),
mTmpTwoArray[1] + mContentHolder.getHeight());
}
public void escalate() {
mBar.scheduleHeadsUpEscalation();
}
public String getKey() {
return mHeadsUp == null ? null : mHeadsUp.notification.getKey();
}
public void setUser(int user) {
mUser = user;
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("HeadsUpNotificationView state:");
pw.print(" mTouchSensitivityDelay="); pw.println(mTouchSensitivityDelay);
pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
pw.print(" mLingerUntilMs="); pw.println(mLingerUntilMs);
pw.print(" mTouched="); pw.println(mTouched);
pw.print(" mMostRecentPackageName="); pw.println(mMostRecentPackageName);
pw.print(" mStartTouchTime="); pw.println(mStartTouchTime);
pw.print(" now="); pw.println(SystemClock.elapsedRealtime());
pw.print(" mUser="); pw.println(mUser);
if (mHeadsUp == null) {
pw.println(" mHeadsUp=null");
} else {
pw.print(" mHeadsUp="); pw.println(mHeadsUp.notification.getKey());
}
int N = mSnoozedPackages.size();
pw.println(" snoozed packages: " + N);
for (int i = 0; i < N; i++) {
pw.print(" "); pw.print(mSnoozedPackages.valueAt(i));
pw.print(", "); pw.println(mSnoozedPackages.keyAt(i));
}
}
public static class EdgeSwipeHelper implements Gefingerpoken {
private static final boolean DEBUG_EDGE_SWIPE = false;
private final float mTouchSlop;
private final HeadsUpNotificationView mHeadsUpView;
private boolean mConsuming;
private float mFirstY;
private float mFirstX;
public EdgeSwipeHelper(HeadsUpNotificationView headsUpView, float touchSlop) {
mHeadsUpView = headsUpView;
mTouchSlop = touchSlop;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action down " + ev.getY());
mFirstX = ev.getX();
mFirstY = ev.getY();
mConsuming = false;
break;
case MotionEvent.ACTION_MOVE:
if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action move " + ev.getY());
final float dY = ev.getY() - mFirstY;
final float daX = Math.abs(ev.getX() - mFirstX);
final float daY = Math.abs(dY);
if (!mConsuming && daX < daY && daY > mTouchSlop) {
mHeadsUpView.snooze();
if (dY > 0) {
if (DEBUG_EDGE_SWIPE) Log.d(TAG, "found an open");
mHeadsUpView.getBar().animateExpandNotificationsPanel();
}
mConsuming = true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action done");
mConsuming = false;
break;
}
return mConsuming;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return mConsuming;
}
}
}

View File

@@ -18,8 +18,10 @@ package com.android.systemui.statusbar.stack;
import android.view.View;
import com.android.systemui.statusbar.ActivatableNotificationView;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import java.util.ArrayList;
import java.util.TreeMap;
/**
* A global state to track all input states for the algorithm.
@@ -34,6 +36,12 @@ public class AmbientState {
private int mSpeedBumpIndex = -1;
private boolean mDark;
private boolean mHideSensitive;
private HeadsUpManager mHeadsUpManager;
private float mPaddingOffset;
private int mLayoutHeight;
private int mTopPadding;
private boolean mShadeExpanded;
private float mMaxHeadsUpTranslation;
public int getScrollY() {
return mScrollY;
@@ -115,4 +123,56 @@ public class AmbientState {
public void setSpeedBumpIndex(int speedBumpIndex) {
mSpeedBumpIndex = speedBumpIndex;
}
public void setHeadsUpManager(HeadsUpManager headsUpManager) {
mHeadsUpManager = headsUpManager;
}
public TreeMap<String, HeadsUpManager.HeadsUpEntry> getHeadsUpEntries() {
return mHeadsUpManager.getEntries();
}
public float getPaddingOffset() {
return mPaddingOffset;
}
public void setPaddingOffset(float paddingOffset) {
mPaddingOffset = paddingOffset;
}
public int getLayoutHeight() {
return mLayoutHeight;
}
public void setLayoutHeight(int layoutHeight) {
mLayoutHeight = layoutHeight;
}
public int getTopPadding() {
return mTopPadding;
}
public void setTopPadding(int topPadding) {
mTopPadding = topPadding;
}
public int getInnerHeight() {
return mLayoutHeight - mTopPadding;
}
public boolean isShadeExpanded() {
return mShadeExpanded;
}
public void setShadeExpanded(boolean shadeExpanded) {
mShadeExpanded = shadeExpanded;
}
public void setMaxHeadsUpTranslation(float maxHeadsUpTranslation) {
mMaxHeadsUpTranslation = maxHeadsUpTranslation;
}
public float getMaxHeadsUpTranslation() {
return mMaxHeadsUpTranslation;
}
}

View File

@@ -24,6 +24,7 @@ import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
@@ -47,6 +48,7 @@ import com.android.systemui.statusbar.StackScrollerDecorView;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.ScrollAdapter;
import java.util.ArrayList;
@@ -179,19 +181,21 @@ public class NotificationStackScrollLayout extends ViewGroup
private float mMinTopOverScrollToEscape;
private int mIntrinsicPadding;
private int mNotificationTopPadding;
private float mPaddingOffset;
private float mTopPaddingOverflow;
private boolean mDontReportNextOverScroll;
private boolean mRequestViewResizeAnimationOnLayout;
private boolean mNeedViewResizeAnimation;
private View mExpandedGroupView;
private boolean mEverythingNeedsAnimation;
private boolean mEverythingNeedsAnimation;
/**
* The maximum scrollPosition which we are allowed to reach when a notification was expanded.
* This is needed to avoid scrolling too far after the notification was collapsed in the same
* motion.
*/
private int mMaxScrollAfterExpand;
private SwipeHelper.LongPressListener mLongPressListener;
/**
@@ -203,8 +207,8 @@ public class NotificationStackScrollLayout extends ViewGroup
private boolean mInterceptDelegateEnabled;
private boolean mDelegateToScrollView;
private boolean mDisallowScrollingInThisMotion;
private long mGoToFullShadeDelay;
private long mGoToFullShadeDelay;
private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
= new ViewTreeObserver.OnPreDrawListener() {
@Override
@@ -219,6 +223,11 @@ public class NotificationStackScrollLayout extends ViewGroup
private int[] mTempInt2 = new int[2];
private boolean mGenerateChildOrderChangedEvent;
private boolean mRemoveAnimationEnabled;
private HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>();
private HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations
= new HashSet<>();
private HeadsUpManager mHeadsUpManager;
private boolean mTrackingHeadsUp;
public NotificationStackScrollLayout(Context context) {
this(context, null);
@@ -404,8 +413,8 @@ public class NotificationStackScrollLayout extends ViewGroup
}
private void updateAlgorithmHeightAndPadding() {
mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
mStackScrollAlgorithm.setTopPadding(mTopPadding);
mAmbientState.setLayoutHeight(getLayoutHeight());
mAmbientState.setTopPadding(mTopPadding);
}
/**
@@ -478,9 +487,13 @@ public class NotificationStackScrollLayout extends ViewGroup
int newStackHeight = (int) height;
int minStackHeight = getMinStackHeight();
int stackHeight;
if (newStackHeight - mTopPadding - mTopPaddingOverflow >= minStackHeight
float paddingOffset;
boolean trackingHeadsUp = mTrackingHeadsUp;
int normalExpandPositionStart = trackingHeadsUp ? mHeadsUpManager.getTopHeadsUpHeight()
: minStackHeight;
if (newStackHeight - mTopPadding - mTopPaddingOverflow >= normalExpandPositionStart
|| getNotGoneChildCount() == 0) {
setTranslationY(mTopPaddingOverflow);
paddingOffset = mTopPaddingOverflow;
stackHeight = newStackHeight;
} else {
@@ -492,9 +505,13 @@ public class NotificationStackScrollLayout extends ViewGroup
float partiallyThere = (newStackHeight - mTopPadding - mTopPaddingOverflow)
/ minStackHeight;
partiallyThere = Math.max(0, partiallyThere);
translationY += (1 - partiallyThere) * (mBottomStackPeekSize +
mCollapseSecondCardPadding);
setTranslationY(translationY - mTopPadding);
if (!trackingHeadsUp) {
translationY += (1 - partiallyThere) * (mBottomStackPeekSize +
mCollapseSecondCardPadding);
} else {
translationY = (int) (height - mHeadsUpManager.getTopHeadsUpHeight());
}
paddingOffset = translationY - mTopPadding;
stackHeight = (int) (height - (translationY - mTopPadding));
}
if (stackHeight != mCurrentStackHeight) {
@@ -502,6 +519,19 @@ public class NotificationStackScrollLayout extends ViewGroup
updateAlgorithmHeightAndPadding();
requestChildrenUpdate();
}
setPaddingOffset(paddingOffset);
}
public float getPaddingOffset() {
return mPaddingOffset;
}
private void setPaddingOffset(float paddingOffset) {
if (paddingOffset != mPaddingOffset) {
mPaddingOffset = paddingOffset;
mAmbientState.setPaddingOffset(paddingOffset);
requestChildrenUpdate();
}
}
/**
@@ -543,11 +573,6 @@ public class NotificationStackScrollLayout extends ViewGroup
if (mDismissAllInProgress) {
return;
}
if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
final View veto = v.findViewById(R.id.veto);
if (veto != null && veto.getVisibility() != View.GONE) {
veto.performClick();
}
setSwipingInProgress(false);
if (mDragAnimPendingChildren.contains(v)) {
// We start the swipe and finish it in the same frame, we don't want any animation
@@ -556,6 +581,17 @@ public class NotificationStackScrollLayout extends ViewGroup
}
mSwipedOutViews.add(v);
mAmbientState.onDragFinished(v);
if (v instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) v;
if (row.isHeadsUp()) {
mHeadsUpManager.addSwipedOutKey(row.getStatusBarNotification().getKey());
}
}
final View veto = v.findViewById(R.id.veto);
if (veto != null && veto.getVisibility() != View.GONE) {
veto.performClick();
}
if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
}
@Override
@@ -578,11 +614,6 @@ public class NotificationStackScrollLayout extends ViewGroup
return false;
}
@Override
public float getFalsingThresholdFactor() {
return mPhoneStatusBar.isScreenOnComingFromTouch() ? 1.5f : 1.0f;
}
public void onBeginDrag(View v) {
setSwipingInProgress(true);
mAmbientState.onBeginDrag(v);
@@ -597,6 +628,11 @@ public class NotificationStackScrollLayout extends ViewGroup
setSwipingInProgress(false);
}
@Override
public float getFalsingThresholdFactor() {
return mPhoneStatusBar.isScreenOnComingFromTouch() ? 1.5f : 1.0f;
}
public View getChildAtPosition(MotionEvent ev) {
return getChildAtPosition(ev.getX(), ev.getY());
}
@@ -1803,10 +1839,21 @@ public class NotificationStackScrollLayout extends ViewGroup
generateGoToFullShadeEvent();
generateViewResizeEvent();
generateGroupExpansionEvent();
generateHeadsUpAnimationEvents();
generateAnimateEverythingEvent();
mNeedsAnimation = false;
}
private void generateHeadsUpAnimationEvents() {
for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) {
int type = eventPair.second ? AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR
: AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR;
mAnimationEvents.add(new AnimationEvent(eventPair.first,
type));
}
mHeadsUpChangeAnimations.clear();
}
private void generateGroupExpansionEvent() {
// Generate a group expansion/collapsing event if there is such a group at all
if (mExpandedGroupView != null) {
@@ -2182,6 +2229,10 @@ public class NotificationStackScrollLayout extends ViewGroup
public void onChildAnimationFinished() {
requestChildrenUpdate();
for (Runnable runnable : mAnimationFinishedRunnables) {
runnable.run();
}
mAnimationFinishedRunnables.clear();
}
/**
@@ -2579,6 +2630,36 @@ public class NotificationStackScrollLayout extends ViewGroup
}
}
public void performOnAnimationFinished(Runnable runnable) {
mAnimationFinishedRunnables.add(runnable);
}
public void setHeadsUpManager(HeadsUpManager headsUpManager) {
mHeadsUpManager = headsUpManager;
mAmbientState.setHeadsUpManager(headsUpManager);
}
public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) {
if (mAnimationsEnabled) {
mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp));
mNeedsAnimation = true;
requestChildrenUpdate();
}
}
public void setShadeExpanded(boolean shadeExpanded) {
mAmbientState.setShadeExpanded(shadeExpanded);
}
public void setMaxHeadsUpTranslation(int maxTranslation) {
mAmbientState.setMaxHeadsUpTranslation(maxTranslation);
requestChildrenUpdate();
}
public void setTrackingHeadsUp(boolean trackingHeadsUp) {
mTrackingHeadsUp = trackingHeadsUp;
}
/**
* A listener that is notified when some child locations might have changed.
*/
@@ -2723,6 +2804,22 @@ public class NotificationStackScrollLayout extends ViewGroup
.animateY()
.animateZ(),
// ANIMATION_TYPE_HEADS_UP_APPEAR
new AnimationFilter()
.animateAlpha()
.animateHeight()
.animateTopInset()
.animateY()
.animateZ(),
// ANIMATION_TYPE_HEADS_UP_DISAPPEAR
new AnimationFilter()
.animateAlpha()
.animateHeight()
.animateTopInset()
.animateY()
.animateZ(),
// ANIMATION_TYPE_EVERYTHING
new AnimationFilter()
.animateAlpha()
@@ -2780,6 +2877,12 @@ public class NotificationStackScrollLayout extends ViewGroup
// ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
StackStateAnimator.ANIMATION_DURATION_EXPAND_CLICKED,
// ANIMATION_TYPE_HEADS_UP_APPEAR
StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR,
// ANIMATION_TYPE_HEADS_UP_DISAPPEAR
StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
// ANIMATION_TYPE_EVERYTHING
StackStateAnimator.ANIMATION_DURATION_STANDARD,
};
@@ -2798,7 +2901,9 @@ public class NotificationStackScrollLayout extends ViewGroup
static final int ANIMATION_TYPE_HIDE_SENSITIVE = 11;
static final int ANIMATION_TYPE_VIEW_RESIZE = 12;
static final int ANIMATION_TYPE_GROUP_EXPANSION_CHANGED = 13;
static final int ANIMATION_TYPE_EVERYTHING = 14;
static final int ANIMATION_TYPE_HEADS_UP_APPEAR = 14;
static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR = 15;
static final int ANIMATION_TYPE_EVERYTHING = 16;
static final int DARK_ANIMATION_ORIGIN_INDEX_ABOVE = -1;
static final int DARK_ANIMATION_ORIGIN_INDEX_BELOW = -2;

View File

@@ -25,9 +25,11 @@ import android.view.ViewGroup;
import com.android.systemui.R;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.ExpandableView;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
/**
* The Algorithm of the {@link com.android.systemui.statusbar.stack
@@ -54,11 +56,6 @@ public class StackScrollAlgorithm {
private StackIndentationFunctor mTopStackIndentationFunctor;
private StackIndentationFunctor mBottomStackIndentationFunctor;
private int mLayoutHeight;
/** mLayoutHeight - mTopPadding */
private int mInnerHeight;
private int mTopPadding;
private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
private boolean mIsExpansionChanging;
private int mFirstChildMaxHeight;
@@ -157,13 +154,13 @@ public class StackScrollAlgorithm {
scrollY = Math.max(0, scrollY);
algorithmState.scrollY = (int) (scrollY + mCollapsedSize + bottomOverScroll);
updateVisibleChildren(resultState, algorithmState);
updateVisibleChildren(resultState, algorithmState, ambientState);
// Phase 1:
findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState);
findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState, ambientState);
// Phase 2:
updatePositionsForState(resultState, algorithmState);
updatePositionsForState(resultState, algorithmState, ambientState);
// Phase 3:
updateZValuesForState(resultState, algorithmState);
@@ -329,23 +326,30 @@ public class StackScrollAlgorithm {
* Update the visible children on the state.
*/
private void updateVisibleChildren(StackScrollState resultState,
StackScrollAlgorithmState state) {
StackScrollAlgorithmState state, AmbientState ambientState) {
ViewGroup hostView = resultState.getHostView();
int childCount = hostView.getChildCount();
state.visibleChildren.clear();
state.visibleChildren.ensureCapacity(childCount);
int notGoneIndex = 0;
TreeMap<String, HeadsUpManager.HeadsUpEntry> headsUpEntries =
ambientState.getHeadsUpEntries();
for (String key: headsUpEntries.keySet()) {
ExpandableView v = headsUpEntries.get(key).entry.row;
notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
}
for (int i = 0; i < childCount; i++) {
ExpandableView v = (ExpandableView) hostView.getChildAt(i);
if (v.getVisibility() != View.GONE) {
StackViewState viewState = resultState.getViewStateForView(v);
viewState.notGoneIndex = notGoneIndex;
state.visibleChildren.add(v);
notGoneIndex++;
// handle the notgoneIndex for the children as well
if (v instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) v;
if (row.isHeadsUp()) {
continue;
}
notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
// handle the notgoneIndex for the children as well
List<ExpandableNotificationRow> children =
row.getNotificationChildren();
if (row.areChildrenExpanded() && children != null) {
@@ -358,22 +362,35 @@ public class StackScrollAlgorithm {
}
}
}
} else {
notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
}
}
}
}
private int updateNotGoneIndex(StackScrollState resultState,
StackScrollAlgorithmState state, int notGoneIndex,
ExpandableView v) {
StackViewState viewState = resultState.getViewStateForView(v);
viewState.notGoneIndex = notGoneIndex;
state.visibleChildren.add(v);
notGoneIndex++;
return notGoneIndex;
}
/**
* Determine the positions for the views. This is the main part of the algorithm.
*
* @param resultState The result state to update if a change to the properties of a child occurs
* @param resultState The result state to update if a change to the properties of a child occurs
* @param algorithmState The state in which the current pass of the algorithm is currently in
* @param ambientState The current ambient state
*/
private void updatePositionsForState(StackScrollState resultState,
StackScrollAlgorithmState algorithmState) {
StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
// The starting position of the bottom stack peek
float bottomPeekStart = mInnerHeight - mBottomStackPeekSize;
float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize;
// The position where the bottom stack starts.
float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength;
@@ -427,7 +444,8 @@ public class StackScrollAlgorithm {
bottomPeekStart, childViewState.yTranslation, childViewState,
childHeight);
}
clampPositionToBottomStackStart(childViewState, childViewState.height);
clampPositionToBottomStackStart(childViewState, childViewState.height,
ambientState);
} else if (nextYPosition >= bottomStackStart) {
// Case 2:
// We are in the bottom stack.
@@ -435,7 +453,7 @@ public class StackScrollAlgorithm {
// According to the regular scroll view we are fully translated out of the
// bottom of the screen so we are fully in the bottom stack
updateStateForChildFullyInBottomStack(algorithmState,
bottomStackStart, childViewState, childHeight);
bottomStackStart, childViewState, childHeight, ambientState);
} else {
// According to the regular scroll view we are currently translating out of /
// into the bottom of the screen
@@ -447,7 +465,7 @@ public class StackScrollAlgorithm {
// Case 3:
// We are in the regular scroll area.
childViewState.location = StackViewState.LOCATION_MAIN_AREA;
clampYTranslation(childViewState, childHeight);
clampYTranslation(childViewState, childHeight, ambientState);
}
// The first card is always rendered.
@@ -468,7 +486,44 @@ public class StackScrollAlgorithm {
currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements;
yPositionInScrollView = yPositionInScrollViewAfterElement;
childViewState.yTranslation += mTopPadding;
childViewState.yTranslation += ambientState.getTopPadding()
+ ambientState.getPaddingOffset();
updateHeadsUpStates(resultState, algorithmState, ambientState);
}
}
private void updateHeadsUpStates(StackScrollState resultState,
StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
TreeMap<String, HeadsUpManager.HeadsUpEntry> headsUpEntries =
ambientState.getHeadsUpEntries();
boolean hasPinnedHeadsUp = false;
for (String key: headsUpEntries.keySet()) {
ExpandableNotificationRow row = headsUpEntries.get(key).entry.row;
StackViewState childState = resultState.getViewStateForView(row);
if (!row.isInShade()) {
childState.yTranslation = Math.max(childState.yTranslation, 0);
hasPinnedHeadsUp = true;
}
childState.height = Math.max(childState.height, row.getHeadsUpHeight());
childState.yTranslation = Math.min(childState.yTranslation,
ambientState.getMaxHeadsUpTranslation() - childState.height);
}
if (hasPinnedHeadsUp && !ambientState.isShadeExpanded()) {
// Let's hide all normal views
int childCount = algorithmState.visibleChildren.size();
for (int i = 0; i < childCount; i++) {
ExpandableView child = algorithmState.visibleChildren.get(i);
StackViewState state = resultState.getViewStateForView(child);
boolean hideView = true;
if (child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
hideView = !row.isHeadsUp();
}
if (hideView) {
state.alpha = 0.0f;
}
}
}
}
@@ -478,8 +533,9 @@ public class StackScrollAlgorithm {
* @param childViewState the view state of the child
* @param childHeight the height of this child
*/
private void clampYTranslation(StackViewState childViewState, int childHeight) {
clampPositionToBottomStackStart(childViewState, childHeight);
private void clampYTranslation(StackViewState childViewState, int childHeight,
AmbientState ambientState) {
clampPositionToBottomStackStart(childViewState, childHeight, ambientState);
clampPositionToTopStackEnd(childViewState, childHeight);
}
@@ -491,9 +547,10 @@ public class StackScrollAlgorithm {
* @param childHeight the height of this child
*/
private void clampPositionToBottomStackStart(StackViewState childViewState,
int childHeight) {
int childHeight, AmbientState ambientState) {
childViewState.yTranslation = Math.min(childViewState.yTranslation,
mInnerHeight - mBottomStackPeekSize - mCollapseSecondCardPadding - childHeight);
ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding
- childHeight);
}
/**
@@ -548,8 +605,7 @@ public class StackScrollAlgorithm {
private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
float transitioningPositionStart, StackViewState childViewState,
int childHeight) {
int childHeight, AmbientState ambientState) {
float currentYPosition;
algorithmState.itemsInBottomStack += 1.0f;
if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
@@ -567,7 +623,7 @@ public class StackScrollAlgorithm {
childViewState.alpha = 1.0f - algorithmState.partialInBottom;
}
childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN;
currentYPosition = mInnerHeight;
currentYPosition = ambientState.getInnerHeight();
}
childViewState.yTranslation = currentYPosition - childHeight;
clampPositionToTopStackEnd(childViewState, childHeight);
@@ -629,7 +685,7 @@ public class StackScrollAlgorithm {
* @param algorithmState The state in which the current pass of the algorithm is currently in
*/
private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
StackScrollAlgorithmState algorithmState) {
StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
// The y Position if the element would be in a regular scrollView
float yPositionInScrollView = 0.0f;
@@ -647,7 +703,7 @@ public class StackScrollAlgorithm {
if (i == 0 && algorithmState.scrollY <= mCollapsedSize) {
// The starting position of the bottom stack peek
int bottomPeekStart = mInnerHeight - mBottomStackPeekSize -
int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize -
mCollapseSecondCardPadding;
// Collapse and expand the first child while the shade is being expanded
float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding
@@ -744,21 +800,6 @@ public class StackScrollAlgorithm {
}
}
public void setLayoutHeight(int layoutHeight) {
this.mLayoutHeight = layoutHeight;
updateInnerHeight();
}
public void setTopPadding(int topPadding) {
mTopPadding = topPadding;
updateInnerHeight();
}
private void updateInnerHeight() {
mInnerHeight = mLayoutHeight - mTopPadding;
}
/**
* Update whether the device is very small, i.e. Notifications can be in both the top and the
* bottom stack at the same time

View File

@@ -45,6 +45,8 @@ public class StackStateAnimator {
public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
public static final int ANIMATION_DURATION_EXPAND_CLICKED = 360;
public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 280;
public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 220;
public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
public static final int ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN = 54;
public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
@@ -90,6 +92,7 @@ public class StackStateAnimator {
private ValueAnimator mTopOverScrollAnimator;
private ValueAnimator mBottomOverScrollAnimator;
private ExpandableNotificationRow mChildExpandingView;
private StackViewState mTmpState = new StackViewState();
public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
mHostLayout = hostLayout;
@@ -828,6 +831,13 @@ public class StackStateAnimator {
ExpandableNotificationRow row = (ExpandableNotificationRow) event.changingView;
row.prepareExpansionChanged(finalState);
mChildExpandingView = row;
} else if (event.animationType == NotificationStackScrollLayout
.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) {
// This item is added, initialize it's properties.
StackViewState viewState = finalState.getViewStateForView(changingView);
mTmpState.copyFrom(viewState);
mTmpState.yTranslation = -mTmpState.height;
finalState.applyState(changingView, mTmpState);
}
mNewEvents.add(event);
}

View File

@@ -123,19 +123,7 @@ public class TvStatusBar extends BaseStatusBar {
}
@Override
public void scheduleHeadsUpDecay(long delay) {
}
@Override
public void scheduleHeadsUpOpen() {
}
@Override
public void scheduleHeadsUpEscalation() {
}
@Override
public void scheduleHeadsUpClose() {
public void escalateHeadsUp() {
}
@Override

View File

@@ -1,261 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.policy;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import android.app.Notification;
import android.os.*;
import android.service.notification.StatusBarNotification;
import com.android.systemui.SwipeHelper;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/**
* Test the Heads Up Notification.
*
* Specifically the policy that a notificaiton must remain visibile for a minimum period of time.
*/
public class HeadsUpNotificationTest extends SysuiTestCase {
private static final String TAG = "HeadsUpNotificationTest";
private static int TOUCH_SENSITIVITY = 100;
private static int NOTIFICATION_DECAY = 10000;
private static int MINIMUM_DISPLAY_TIME = 3000;
private static int SNOOZE_TIME = 60000;
private static long TOO_SOON = 1000L; // less than MINIMUM_DISPLAY_TIME
private static long LATER = 5000L; // more than MINIMUM_DISPLAY_TIME
private static long REMAINING_VISIBILITY = MINIMUM_DISPLAY_TIME - TOO_SOON;
protected HeadsUpNotificationView mHeadsUp;
@Mock protected PhoneStatusBar mMockStatusBar;
@Mock private HeadsUpNotificationView.Clock mClock;
@Mock private SwipeHelper mMockSwipeHelper;
@Mock private HeadsUpNotificationView.EdgeSwipeHelper mMockEdgeSwipeHelper;
@Override
protected void setUp() throws Exception {
super.setUp();
MockitoAnnotations.initMocks(this);
mHeadsUp = new HeadsUpNotificationView(mContext,
mClock, mMockSwipeHelper, mMockEdgeSwipeHelper,
NOTIFICATION_DECAY, MINIMUM_DISPLAY_TIME, TOUCH_SENSITIVITY, SNOOZE_TIME);
mHeadsUp.setBar(mMockStatusBar);
}
private NotificationData.Entry makeNotification(String key) {
StatusBarNotification sbn = mock(StatusBarNotification.class);
when(sbn.getKey()).thenReturn(key);
return new NotificationData.Entry(sbn, null);
}
public void testPostAndDecay() {
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpOpen();
ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
Mockito.verify(mMockStatusBar).scheduleHeadsUpDecay(decayArg.capture());
// New notification gets a full decay time.
assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue());
}
public void testPostAndDeleteTooSoon() {
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
mHeadsUp.removeNotification(a.key);
ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
Mockito.verify(mMockStatusBar).scheduleHeadsUpDecay(decayArg.capture());
// Leave the window up for the balance of the minumum time.
assertEquals(REMAINING_VISIBILITY, (long) decayArg.getValue());
}
public void testPostAndDeleteLater() {
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(LATER);
mHeadsUp.removeNotification(a.key);
// Delete closes immediately if the minimum time window is satisfied.
Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose();
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt());
}
// This is a bad test. It should not care that there is a call to scheduleHeadsUpClose(),
// but it happens that there will be one, so it is important that it happen before the
// call to scheduleHeadsUpOpen(), so that the final state is open.
// Maybe mMockStatusBar should instead be a fake that tracks the open/closed state.
public void testPostAndReplaceTooSoon() {
InOrder callOrder = inOrder(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
NotificationData.Entry b = makeNotification("b");
mHeadsUp.showNotification(b);
Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose();
ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture());
// New notification gets a full decay time.
assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue());
// Make sure close was called before open, so that the heads up stays open.
callOrder.verify(mMockStatusBar).scheduleHeadsUpClose();
callOrder.verify(mMockStatusBar).scheduleHeadsUpOpen();
}
public void testPostAndUpdateAlertAgain() {
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
mHeadsUp.updateNotification(a, true);
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture());
// Alert again gets a full decay time.
assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue());
}
public void testPostAndUpdateAlertAgainFastFail() {
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
NotificationData.Entry a_prime = makeNotification("a");
mHeadsUp.updateNotification(a_prime, true);
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture());
// Alert again gets a full decay time.
assertEquals(NOTIFICATION_DECAY, (long) decayArg.getValue());
}
public void testPostAndUpdateNoAlertAgain() {
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
mHeadsUp.updateNotification(a, false);
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt());
}
public void testPostAndUpdateNoAlertAgainFastFail() {
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
NotificationData.Entry a_prime = makeNotification("a");
mHeadsUp.updateNotification(a_prime, false);
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt());
}
public void testPostAndUpdateLowPriorityTooSoon() {
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
mHeadsUp.release();
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture());
// Down grade on update leaves the window up for the balance of the minumum time.
assertEquals(REMAINING_VISIBILITY, (long) decayArg.getValue());
}
public void testPostAndUpdateLowPriorityTooSoonFastFail() {
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(TOO_SOON);
NotificationData.Entry a_prime = makeNotification("a");
mHeadsUp.updateNotification(a_prime, false);
mHeadsUp.release();
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpClose();
ArgumentCaptor<Long> decayArg = ArgumentCaptor.forClass(Long.class);
Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpDecay(decayArg.capture());
// Down grade on update leaves the window up for the balance of the minumum time.
assertEquals(REMAINING_VISIBILITY, (long) decayArg.getValue());
}
public void testPostAndUpdateLowPriorityLater() {
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(LATER);
mHeadsUp.release();
// Down grade on update closes immediately if the minimum time window is satisfied.
Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose();
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt());
}
public void testPostAndUpdateLowPriorityLaterFastFail() {
when(mClock.currentTimeMillis()).thenReturn(0L);
NotificationData.Entry a = makeNotification("a");
mHeadsUp.showNotification(a);
reset(mMockStatusBar);
when(mClock.currentTimeMillis()).thenReturn(LATER);
NotificationData.Entry a_prime = makeNotification("a");
mHeadsUp.updateNotification(a_prime, false);
mHeadsUp.release();
// Down grade on update closes immediately if the minimum time window is satisfied.
Mockito.verify(mMockStatusBar, times(1)).scheduleHeadsUpClose();
Mockito.verify(mMockStatusBar, never()).scheduleHeadsUpDecay(anyInt());
}
}