blob: f854e1071b234a9f0c00ec8d53209393ccbcf7db [file] [log] [blame]
/*
* 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.template;
import static android.app.Notification.EXTRA_IS_GROUP_CONVERSATION;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.Person;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import androidx.core.app.NotificationCompat.MessagingStyle;
import com.android.car.notification.AlertEntry;
import com.android.car.notification.NotificationClickHandlerFactory;
import com.android.car.notification.PreprocessingManager;
import com.android.car.notification.R;
import java.util.List;
/**
* Messaging notification template that displays a messaging notification and a voice reply button.
*/
public class MessageNotificationViewHolder extends CarNotificationBaseViewHolder {
private static final String TAG = "MessageNotificationViewHolder";
private static final boolean DEBUG = Build.IS_DEBUGGABLE;
private static final String SENDER_TITLE_SEPARATOR = " • ";
private static final String SENDER_BODY_SEPARATOR = ": ";
private static final String NEW_LINE = "\n";
private final CarNotificationBodyView mBodyView;
private final CarNotificationHeaderView mHeaderView;
private final CarNotificationActionsView mActionsView;
private final PreprocessingManager mPreprocessingManager;
private final String mNewMessageText;
private final String mSeeMoreText;
private final String mEllipsizedSuffix;
private final int mMaxMessageCount;
private final int mMaxLineCount;
private final int mAdditionalCharCountAfterExpansion;
private final Drawable mGroupIcon;
private final NotificationClickHandlerFactory mClickHandlerFactory;
public MessageNotificationViewHolder(
View view, NotificationClickHandlerFactory clickHandlerFactory) {
super(view, clickHandlerFactory);
mHeaderView = view.findViewById(R.id.notification_header);
mActionsView = view.findViewById(R.id.notification_actions);
mBodyView = view.findViewById(R.id.notification_body);
mNewMessageText = getContext().getString(R.string.restricted_hun_message_content);
mSeeMoreText = getContext().getString(R.string.see_more_message);
mEllipsizedSuffix = getContext().getString(R.string.ellipsized_string);
mMaxMessageCount =
getContext().getResources().getInteger(R.integer.config_maxNumberOfMessagesInPanel);
mMaxLineCount = getContext().getResources().getInteger(
R.integer.config_maxNumberOfMessageLinesInPanel);
mAdditionalCharCountAfterExpansion = getContext().getResources().getInteger(
R.integer.config_additionalCharactersToShowInSingleMessageExpandedNotification);
mGroupIcon = getContext().getDrawable(R.drawable.ic_group);
mClickHandlerFactory = clickHandlerFactory;
mPreprocessingManager = PreprocessingManager.getInstance(getContext());
}
/**
* Binds a {@link AlertEntry} to a messaging car notification template without
* UX restriction.
*/
@Override
public void bind(AlertEntry alertEntry, boolean isInGroup,
boolean isHeadsUp) {
super.bind(alertEntry, isInGroup, isHeadsUp);
bindBody(alertEntry, isInGroup, /* isRestricted= */ false, isHeadsUp);
mHeaderView.bind(alertEntry, isInGroup);
mActionsView.bind(mClickHandlerFactory, alertEntry);
}
/**
* Binds a {@link AlertEntry} to a messaging car notification template with
* UX restriction.
*/
public void bindRestricted(AlertEntry alertEntry, boolean isInGroup, boolean isHeadsUp) {
super.bind(alertEntry, isInGroup, isHeadsUp);
bindBody(alertEntry, isInGroup, /* isRestricted= */ true, isHeadsUp);
mHeaderView.bind(alertEntry, isInGroup);
mActionsView.bind(mClickHandlerFactory, alertEntry);
}
/**
* Private method that binds the data to the view.
*/
private void bindBody(AlertEntry alertEntry, boolean isInGroup, boolean isRestricted,
boolean isHeadsUp) {
if (DEBUG) {
if (isInGroup) {
Log.d(TAG, "Is part of notification group: " + alertEntry);
} else {
Log.d(TAG, "Is not part of notification group: " + alertEntry);
}
if (isRestricted) {
Log.d(TAG, "Has driver restrictions: " + alertEntry);
} else {
Log.d(TAG, "Doesn't have driver restrictions: " + alertEntry);
}
if (isHeadsUp) {
Log.d(TAG, "Is a heads-up notification: " + alertEntry);
} else {
Log.d(TAG, "Is not a heads-up notification: " + alertEntry);
}
}
mBodyView.setCountTextColor(getAccentColor());
Notification notification = alertEntry.getNotification();
StatusBarNotification sbn = alertEntry.getStatusBarNotification();
Bundle extras = notification.extras;
CharSequence messageText;
CharSequence conversationTitle;
Icon avatar = null;
Integer messageCount;
CharSequence senderName = null;
Notification.MessagingStyle.Message latestMessage = null;
MessagingStyle messagingStyle =
MessagingStyle.extractMessagingStyleFromNotification(notification);
boolean isGroupConversation =
((messagingStyle != null && messagingStyle.isGroupConversation())
|| extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION));
if (DEBUG) {
if (isGroupConversation) {
Log.d(TAG, "Is a group conversation: " + alertEntry);
} else {
Log.d(TAG, "Is not a group conversation: " + alertEntry);
}
}
boolean messageStyleFlag = false;
List<Notification.MessagingStyle.Message> messages = null;
Parcelable[] messagesData = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
if (messagesData != null) {
messages = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messagesData);
if (messages != null && !messages.isEmpty()) {
if (DEBUG) {
Log.d(TAG, "App did use messaging style: " + alertEntry);
}
messageStyleFlag = true;
// Use the latest message
latestMessage = messages.get(messages.size() - 1);
Person sender = latestMessage.getSenderPerson();
if (sender != null) {
avatar = sender.getIcon();
}
senderName = (sender != null ? sender.getName() : latestMessage.getSender());
} else {
// App did not use messaging style; fall back to standard fields
if (DEBUG) {
Log.d(TAG, "App did not use messaging style; fall back to standard "
+ "fields: " + alertEntry);
}
}
}
messageCount = getMessageCount(messages, notification.number);
messageText = getMessageText(latestMessage, isRestricted, isHeadsUp, isGroupConversation,
senderName, messageCount, extras);
conversationTitle = getConversationTitle(messagingStyle, isHeadsUp, isGroupConversation,
senderName, extras);
if (avatar == null) {
avatar = notification.getLargeIcon();
}
Long when;
if (notification.showsTime()) {
when = notification.when;
} else {
when = null;
}
Drawable groupIcon;
if (isGroupConversation) {
groupIcon = mGroupIcon;
} else {
groupIcon = null;
}
int unshownCount = messageCount - 1;
String unshownCountText = null;
if (!isRestricted && !isHeadsUp && messageStyleFlag) {
if (unshownCount > 0) {
unshownCountText = getContext().getResources().getQuantityString(
R.plurals.restricted_numbered_message_content, unshownCount, unshownCount);
} else if (messageText.toString().endsWith(mEllipsizedSuffix)) {
unshownCountText = mSeeMoreText;
}
View.OnClickListener listener =
getCountViewOnClickListener(unshownCount, messages, isGroupConversation,
sbn, conversationTitle, avatar, groupIcon, when);
mBodyView.setCountOnClickListener(listener);
}
mBodyView.bind(conversationTitle, messageText,
sbn, avatar, groupIcon, unshownCountText, when);
}
private CharSequence getMessageText(Notification.MessagingStyle.Message message,
boolean isRestricted, boolean isHeadsUp, boolean isGroupConversation,
CharSequence senderName, int messageCount, Bundle extras) {
CharSequence messageText = null;
if (message != null) {
if (DEBUG) {
Log.d(TAG, "Message style message text used.");
}
messageText = message.getText();
if (!isHeadsUp && isGroupConversation) {
// If conversation is a group conversation and notification is not a HUN,
// then prepend sender's name to title.
messageText = senderName + SENDER_BODY_SEPARATOR + messageText;
}
} else {
if (DEBUG) {
Log.d(TAG, "Standard field message text used.");
}
messageText = extras.getCharSequence(Notification.EXTRA_TEXT);
}
if (isRestricted) {
if (isHeadsUp || messageCount == 1) {
messageText = mNewMessageText;
} else {
messageText = getContext().getResources().getQuantityString(
R.plurals.restricted_numbered_message_content, messageCount, messageCount);
}
}
if (!TextUtils.isEmpty(messageText)) {
messageText = mPreprocessingManager.trimText(messageText);
}
return messageText;
}
private CharSequence getConversationTitle(MessagingStyle messagingStyle, boolean isHeadsUp,
boolean isGroupConversation, CharSequence senderName, Bundle extras) {
CharSequence conversationTitle = null;
if (messagingStyle != null) {
conversationTitle = messagingStyle.getConversationTitle();
}
if (isGroupConversation && conversationTitle != null && isHeadsUp) {
// If conversation title has been set, conversation is a group conversation
// and notification is a HUN, then prepend sender's name to title.
conversationTitle = senderName + SENDER_TITLE_SEPARATOR + conversationTitle;
} else if (conversationTitle == null) {
if (DEBUG) {
Log.d(TAG, "Conversation title not set.");
}
// If conversation title has not been set, set it as sender's name.
conversationTitle = senderName;
}
if (TextUtils.isEmpty(conversationTitle)) {
if (DEBUG) {
Log.d(TAG, "Standard field conversation title used.");
}
conversationTitle = extras.getCharSequence(Notification.EXTRA_TITLE);
}
return conversationTitle;
}
private int getMessageCount(List<Notification.MessagingStyle.Message> messages, int numEvents) {
Integer messageCount = null;
if (messages != null) {
messageCount = messages.size();
} else {
messageCount = numEvents;
if (messageCount == 0) {
// A notification should at least represent 1 message
messageCount = 1;
}
}
return messageCount;
}
@Override
void reset() {
super.reset();
mBodyView.reset();
mHeaderView.reset();
mActionsView.reset();
}
private View.OnClickListener getCountViewOnClickListener(int unshownCount,
@Nullable List<Notification.MessagingStyle.Message> messages,
boolean isGroupConversation, StatusBarNotification sbn, CharSequence title,
@Nullable Icon avatar, @Nullable Drawable groupIcon, @Nullable Long when) {
String finalMessage;
if (unshownCount > 0) {
StringBuilder builder = new StringBuilder();
for (int i = messages.size() - 1; i >= messages.size() - 1 - mMaxMessageCount && i >= 0;
i--) {
if (i != messages.size() - 1) {
builder.append(NEW_LINE);
builder.append(NEW_LINE);
}
unshownCount--;
Notification.MessagingStyle.Message message = messages.get(i);
Person sender = message.getSenderPerson();
CharSequence senderName =
(sender != null ? sender.getName() : message.getSender());
if (isGroupConversation) {
builder.append(senderName + SENDER_BODY_SEPARATOR + message.getText());
} else {
builder.append(message.getText());
}
if (builder.toString().split(NEW_LINE).length >= mMaxLineCount) {
break;
}
}
finalMessage = builder.toString();
} else {
StringBuilder builder = new StringBuilder();
Notification.MessagingStyle.Message message = messages.get(messages.size() - 1);
Person sender = message.getSenderPerson();
CharSequence senderName =
(sender != null ? sender.getName() : message.getSender());
if (isGroupConversation) {
builder.append(senderName + SENDER_BODY_SEPARATOR + message.getText());
} else {
builder.append(message.getText());
}
String messageStr = builder.toString();
int maxCharCountAfterExpansion;
if (mPreprocessingManager.getMaximumStringLength() == Integer.MAX_VALUE) {
maxCharCountAfterExpansion = Integer.MAX_VALUE;
} else {
// We are exceeding UXRE maximum string length limit only when expanding the long
// message notification. This neither applies for collapsed single message
// notifications nor applies for UXRE updates that are handled by `isRestricted`
// being {@code true}.
maxCharCountAfterExpansion = mPreprocessingManager.getMaximumStringLength()
+ mAdditionalCharCountAfterExpansion - mEllipsizedSuffix.length();
}
if (messageStr.length() > maxCharCountAfterExpansion) {
messageStr = messageStr.substring(0, maxCharCountAfterExpansion - 1)
+ mEllipsizedSuffix;
}
finalMessage = messageStr;
}
int finalUnshownCount = unshownCount;
return view -> {
String unshownCountText;
if (finalUnshownCount <= 0) {
unshownCountText = null;
} else {
unshownCountText = getContext().getResources().getQuantityString(
R.plurals.message_unshown_count, finalUnshownCount, finalUnshownCount);
}
mBodyView.bind(title, finalMessage, sbn, avatar, groupIcon,
unshownCountText, when);
mBodyView.setContentMaxLines(mMaxLineCount);
mBodyView.setCountOnClickListener(null);
};
}
}