/* * Copyright (C) 2018 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.car.notification; import static android.view.ViewTreeObserver.InternalInsetsInfo; import static android.view.ViewTreeObserver.OnComputeInternalInsetsListener; import static android.view.ViewTreeObserver.OnGlobalFocusChangeListener; import static android.view.ViewTreeObserver.OnGlobalLayoutListener; import static com.android.car.assist.client.CarAssistUtils.isCarCompatibleMessagingNotification; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.app.ActivityTaskManager; import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.car.drivingstate.CarUxRestrictions; import android.car.drivingstate.CarUxRestrictionsManager; import android.content.Context; import android.os.Build; import android.service.notification.NotificationListenerService; import android.util.Log; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewTreeObserver; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; import com.android.car.notification.headsup.CarHeadsUpNotificationContainer; import com.android.car.notification.headsup.animationhelper.HeadsUpNotificationAnimationHelper; import com.android.car.notification.template.MessageNotificationViewHolder; import java.time.Clock; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledThreadPoolExecutor; /** * Notification Manager for heads-up notifications in car. */ public class CarHeadsUpNotificationManager implements CarUxRestrictionsManager.OnUxRestrictionsChangedListener { /** * Callback that will be issued after a Heads up notification state is changed. */ public interface OnHeadsUpNotificationStateChange { /** * Will be called if a new notification added/updated changes the heads up state for that * notification. */ void onStateChange(AlertEntry alertEntry, HeadsUpState headsUpState); } /** * Captures HUN State with following values: *
Return's true if the notification will be shown as a heads up, false otherwise.
*/
public boolean maybeShowHeadsUp(
AlertEntry alertEntry,
NotificationListenerService.RankingMap rankingMap,
Map
* A notification will never be shown as a heads-up if:
* A notification will be shown as a heads-up if:
* Group alert behavior still follows API documentation.
*
* @return true if a notification should be shown as a heads-up
*/
private boolean shouldShowHeadsUp(
AlertEntry alertEntry,
NotificationListenerService.RankingMap rankingMap) {
if (mKeyguardManager.isKeyguardLocked()) {
if (DEBUG) {
Log.d(TAG, "Unable to show as HUN: Keyguard is locked");
}
return false;
}
Notification notification = alertEntry.getNotification();
// Navigation notification configured by OEM
if (!mEnableNavigationHeadsup && Notification.CATEGORY_NAVIGATION.equals(
notification.category)) {
if (DEBUG) {
Log.d(TAG, "Unable to show as HUN: OEM has disabled navigation HUN");
}
return false;
}
// Group alert behavior
if (notification.suppressAlertingDueToGrouping()) {
if (DEBUG) {
Log.d(TAG, "Unable to show as HUN: Grouping notification");
}
return false;
}
// Messaging notification muted by user.
if (mNotificationDataManager.isMessageNotificationMuted(alertEntry)) {
if (DEBUG) {
Log.d(TAG, "Unable to show as HUN: Messaging notification is muted by user");
}
return false;
}
// Do not show if importance < HIGH
NotificationListenerService.Ranking ranking = getRanking();
if (rankingMap.getRanking(alertEntry.getKey(), ranking)) {
if (ranking.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
if (DEBUG) {
Log.d(TAG, "Unable to show as HUN: importance is not sufficient");
}
return false;
}
}
if (NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext, alertEntry)) {
if (DEBUG) {
Log.d(TAG, "Show as HUN: application is system privileged or signed with "
+ "platform key");
}
return true;
}
// Allow car messaging type.
if (isCarCompatibleMessagingNotification(alertEntry.getStatusBarNotification())) {
if (DEBUG) {
Log.d(TAG, "Show as HUN: car messaging type notification");
}
return true;
}
if (notification.category == null) {
Log.d(TAG, "category not set for: "
+ alertEntry.getStatusBarNotification().getPackageName());
}
if (DEBUG) {
Log.d(TAG, "Notification category: " + notification.category);
}
// Allow for Call, and nav TBT categories.
return Notification.CATEGORY_CALL.equals(notification.category)
|| Notification.CATEGORY_NAVIGATION.equals(notification.category);
}
private boolean isActiveHun(AlertEntry alertEntry) {
return mActiveHeadsUpNotifications.containsKey(alertEntry.getKey());
}
private HeadsUpEntry getActiveHeadsUpEntry(AlertEntry alertEntry) {
return mActiveHeadsUpNotifications.get(alertEntry.getKey());
}
/**
* We tag HUN that was removed by the app and hence not to be shown in the notification panel
* against the normal behaviour (on dismiss add to notification panel).
*/
private void tagCurrentActiveHunToBeRemoved(AlertEntry alertEntry) {
mHeadsUpNotificationsToBeRemoved.add(alertEntry.getKey());
}
@VisibleForTesting
protected NotificationListenerService.Ranking getRanking() {
return new NotificationListenerService.Ranking();
}
@Override
public void onUxRestrictionsChanged(CarUxRestrictions restrictions) {
mCarHeadsUpNotificationQueue.setActiveUxRestriction(
restrictions.isRequiresDistractionOptimization());
mShouldRestrictMessagePreview =
(restrictions.getActiveRestrictions()
& CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0;
}
/**
* Sets the source of {@link View.OnClickListener}
*
* @param clickHandlerFactory used to generate onClickListeners
*/
@VisibleForTesting
public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) {
mClickHandlerFactory = clickHandlerFactory;
}
@VisibleForTesting
CarHeadsUpNotificationQueue.CarHeadsUpNotificationQueueCallback
getCarHeadsUpNotificationQueueCallback() {
return mCarHeadsUpNotificationQueueCallback;
}
@VisibleForTesting
void setCarHeadsUpNotificationQueue(CarHeadsUpNotificationQueue carHeadsUpNotificationQueue) {
mCarHeadsUpNotificationQueue = carHeadsUpNotificationQueue;
}
@VisibleForTesting
void addActiveHeadsUpNotification(HeadsUpEntry headsUpEntry) {
mActiveHeadsUpNotifications.put(headsUpEntry.getKey(), headsUpEntry);
}
}
*
*/
@UiThread
private void showHeadsUp(AlertEntry alertEntry,
NotificationListenerService.RankingMap rankingMap) {
// Show animations only when there is no active HUN and notification is new. This check
// needs to be done here because after this the new notification will be added to the map
// holding ongoing notifications.
boolean shouldShowAnimation = !isUpdate(alertEntry);
HeadsUpEntry currentNotification = addNewHeadsUpEntry(alertEntry);
if (currentNotification.mIsNewHeadsUp) {
playSound(alertEntry, rankingMap);
setAutoDismissViews(currentNotification, alertEntry);
} else if (currentNotification.mIsAlertAgain) {
setAutoDismissViews(currentNotification, alertEntry);
}
CarNotificationTypeItem notificationTypeItem = NotificationUtils.getNotificationViewType(
alertEntry);
currentNotification.setClickHandlerFactory(mClickHandlerFactory);
if (currentNotification.getNotificationView() == null) {
currentNotification.setNotificationView(mInflater.inflate(
notificationTypeItem.getHeadsUpTemplate(),
null));
mHunContainer.displayNotification(currentNotification.getNotificationView(),
notificationTypeItem);
currentNotification.setViewHolder(
notificationTypeItem.getViewHolder(currentNotification.getNotificationView(),
mClickHandlerFactory));
}
currentNotification.getViewHolder().setHideDismissButton(!isHeadsUpDismissible(alertEntry));
if (mShouldRestrictMessagePreview && notificationTypeItem.getNotificationType()
== NotificationViewType.MESSAGE) {
((MessageNotificationViewHolder) currentNotification.getViewHolder()).bindRestricted(
alertEntry, /* isInGroup= */ false, /* isHeadsUp= */ true, /* isSeen= */ false);
} else {
currentNotification.getViewHolder().bind(alertEntry, /* isInGroup= */false,
/* isHeadsUp= */ true, /* isSeen= */ false);
}
resetViewTreeListenersEntry(currentNotification);
ViewTreeObserver viewTreeObserver =
currentNotification.getNotificationView().getViewTreeObserver();
// measure the size of the card and make that area of the screen touchable
OnComputeInternalInsetsListener onComputeInternalInsetsListener =
info -> setInternalInsetsInfo(info, currentNotification,
/* panelExpanded= */ false);
viewTreeObserver.addOnComputeInternalInsetsListener(onComputeInternalInsetsListener);
// Get the height of the notification view after onLayout() in order to animate the
// notification into the screen.
viewTreeObserver.addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
View view = currentNotification.getNotificationView();
if (shouldShowAnimation) {
mAnimationHelper.resetHUNPosition(view);
AnimatorSet animatorSet = mAnimationHelper.getAnimateInAnimator(
mContext, view);
animatorSet.setTarget(view);
animatorSet.start();
}
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
// Reset the auto dismiss timeout for each rotary event.
OnGlobalFocusChangeListener onGlobalFocusChangeListener =
(oldFocus, newFocus) -> setAutoDismissViews(currentNotification, alertEntry);
viewTreeObserver.addOnGlobalFocusChangeListener(onGlobalFocusChangeListener);
mRegisteredViewTreeListeners.put(currentNotification,
new Pair<>(onComputeInternalInsetsListener, onGlobalFocusChangeListener));
attachHunViewListeners(currentNotification.getNotificationView(), alertEntry);
}
private void attachHunViewListeners(View notificationView, AlertEntry alertEntry) {
// Add swipe gesture
View cardView = notificationView.findViewById(R.id.card_view);
cardView.setOnTouchListener(new HeadsUpNotificationOnTouchListener(cardView,
isHeadsUpDismissible(alertEntry),
() -> dismissHun(alertEntry, /* shouldAnimate= */ false)));
// Add dismiss button listener
View dismissButton = notificationView.findViewById(
R.id.dismiss_button);
if (dismissButton != null) {
dismissButton.setOnClickListener(v ->
dismissHun(alertEntry, /* shouldAnimate= */ true));
}
}
private void resetViewTreeListenersEntry(HeadsUpEntry headsUpEntry) {
Pair
*
*
*
*
*
*