• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.notification.template;
17 
18 import static android.app.Notification.EXTRA_IS_GROUP_CONVERSATION;
19 
20 import android.annotation.Nullable;
21 import android.app.Notification;
22 import android.app.Person;
23 import android.graphics.drawable.Drawable;
24 import android.graphics.drawable.Icon;
25 import android.os.Build;
26 import android.os.Bundle;
27 import android.os.Parcelable;
28 import android.service.notification.StatusBarNotification;
29 import android.text.TextUtils;
30 import android.util.Log;
31 import android.view.View;
32 
33 import androidx.core.app.NotificationCompat.MessagingStyle;
34 
35 import com.android.car.notification.AlertEntry;
36 import com.android.car.notification.NotificationClickHandlerFactory;
37 import com.android.car.notification.PreprocessingManager;
38 import com.android.car.notification.R;
39 
40 import java.util.List;
41 
42 /**
43  * Messaging notification template that displays a messaging notification and a voice reply button.
44  */
45 public class MessageNotificationViewHolder extends CarNotificationBaseViewHolder {
46     private static final String TAG = "MessageNotificationViewHolder";
47     private static final boolean DEBUG = Build.IS_DEBUGGABLE;
48     private static final String SENDER_TITLE_SEPARATOR = " • ";
49     private static final String SENDER_BODY_SEPARATOR = ": ";
50     private static final String NEW_LINE = "\n";
51 
52     private final CarNotificationBodyView mBodyView;
53     private final CarNotificationHeaderView mHeaderView;
54     private final CarNotificationActionsView mActionsView;
55     private final PreprocessingManager mPreprocessingManager;
56     private final String mNewMessageText;
57     private final String mSeeMoreText;
58     private final String mEllipsizedSuffix;
59     private final int mMaxMessageCount;
60     private final int mMaxLineCount;
61     private final int mAdditionalCharCountAfterExpansion;
62     private final Drawable mGroupIcon;
63 
64     private final NotificationClickHandlerFactory mClickHandlerFactory;
65 
MessageNotificationViewHolder( View view, NotificationClickHandlerFactory clickHandlerFactory)66     public MessageNotificationViewHolder(
67             View view, NotificationClickHandlerFactory clickHandlerFactory) {
68         super(view, clickHandlerFactory);
69         mHeaderView = view.findViewById(R.id.notification_header);
70         mActionsView = view.findViewById(R.id.notification_actions);
71         mBodyView = view.findViewById(R.id.notification_body);
72 
73         mNewMessageText = getContext().getString(R.string.restricted_hun_message_content);
74         mSeeMoreText = getContext().getString(R.string.see_more_message);
75         mEllipsizedSuffix = getContext().getString(R.string.ellipsized_string);
76         mMaxMessageCount =
77                 getContext().getResources().getInteger(R.integer.config_maxNumberOfMessagesInPanel);
78         mMaxLineCount = getContext().getResources().getInteger(
79                 R.integer.config_maxNumberOfMessageLinesInPanel);
80         mAdditionalCharCountAfterExpansion = getContext().getResources().getInteger(
81                 R.integer.config_additionalCharactersToShowInSingleMessageExpandedNotification);
82         mGroupIcon = getContext().getDrawable(R.drawable.ic_group);
83 
84         mClickHandlerFactory = clickHandlerFactory;
85         mPreprocessingManager = PreprocessingManager.getInstance(getContext());
86     }
87 
88     /**
89      * Binds a {@link AlertEntry} to a messaging car notification template without
90      * UX restriction.
91      */
92     @Override
bind(AlertEntry alertEntry, boolean isInGroup, boolean isHeadsUp)93     public void bind(AlertEntry alertEntry, boolean isInGroup,
94             boolean isHeadsUp) {
95         super.bind(alertEntry, isInGroup, isHeadsUp);
96         bindBody(alertEntry, isInGroup, /* isRestricted= */ false, isHeadsUp);
97         mHeaderView.bind(alertEntry, isInGroup);
98         mActionsView.bind(mClickHandlerFactory, alertEntry);
99     }
100 
101     /**
102      * Binds a {@link AlertEntry} to a messaging car notification template with
103      * UX restriction.
104      */
bindRestricted(AlertEntry alertEntry, boolean isInGroup, boolean isHeadsUp)105     public void bindRestricted(AlertEntry alertEntry, boolean isInGroup, boolean isHeadsUp) {
106         super.bind(alertEntry, isInGroup, isHeadsUp);
107         bindBody(alertEntry, isInGroup, /* isRestricted= */ true, isHeadsUp);
108         mHeaderView.bind(alertEntry, isInGroup);
109 
110         mActionsView.bind(mClickHandlerFactory, alertEntry);
111     }
112 
113     /**
114      * Private method that binds the data to the view.
115      */
bindBody(AlertEntry alertEntry, boolean isInGroup, boolean isRestricted, boolean isHeadsUp)116     private void bindBody(AlertEntry alertEntry, boolean isInGroup, boolean isRestricted,
117             boolean isHeadsUp) {
118         if (DEBUG) {
119             if (isInGroup) {
120                 Log.d(TAG, "Is part of notification group: " + alertEntry);
121             } else {
122                 Log.d(TAG, "Is not part of notification group: " + alertEntry);
123             }
124             if (isRestricted) {
125                 Log.d(TAG, "Has driver restrictions: " + alertEntry);
126             } else {
127                 Log.d(TAG, "Doesn't have driver restrictions: " + alertEntry);
128             }
129             if (isHeadsUp) {
130                 Log.d(TAG, "Is a heads-up notification: " + alertEntry);
131             } else {
132                 Log.d(TAG, "Is not a heads-up notification: " + alertEntry);
133             }
134         }
135 
136         mBodyView.setCountTextColor(getAccentColor());
137         Notification notification = alertEntry.getNotification();
138         StatusBarNotification sbn = alertEntry.getStatusBarNotification();
139         Bundle extras = notification.extras;
140         CharSequence messageText;
141         CharSequence conversationTitle;
142         Icon avatar = null;
143         Integer messageCount;
144         CharSequence senderName = null;
145         Notification.MessagingStyle.Message latestMessage = null;
146 
147         MessagingStyle messagingStyle =
148                 MessagingStyle.extractMessagingStyleFromNotification(notification);
149 
150         boolean isGroupConversation =
151                 ((messagingStyle != null && messagingStyle.isGroupConversation())
152                         || extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION));
153         if (DEBUG) {
154             if (isGroupConversation) {
155                 Log.d(TAG, "Is a group conversation: " + alertEntry);
156             } else {
157                 Log.d(TAG, "Is not a group conversation: " + alertEntry);
158             }
159         }
160 
161         boolean messageStyleFlag = false;
162         List<Notification.MessagingStyle.Message> messages = null;
163         Parcelable[] messagesData = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
164         if (messagesData != null) {
165             messages = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messagesData);
166             if (messages != null && !messages.isEmpty()) {
167                 if (DEBUG) {
168                     Log.d(TAG, "App did use messaging style: " + alertEntry);
169                 }
170                 messageStyleFlag = true;
171 
172                 // Use the latest message
173                 latestMessage = messages.get(messages.size() - 1);
174                 Person sender = latestMessage.getSenderPerson();
175                 if (sender != null) {
176                     avatar = sender.getIcon();
177                 }
178                 senderName = (sender != null ? sender.getName() : latestMessage.getSender());
179             } else {
180                 // App did not use messaging style; fall back to standard fields
181                 if (DEBUG) {
182                     Log.d(TAG, "App did not use messaging style; fall back to standard "
183                             + "fields: " + alertEntry);
184                 }
185             }
186         }
187 
188 
189         messageCount = getMessageCount(messages, notification.number);
190         messageText = getMessageText(latestMessage, isRestricted, isHeadsUp, isGroupConversation,
191                 senderName, messageCount, extras);
192         conversationTitle = getConversationTitle(messagingStyle, isHeadsUp, isGroupConversation,
193                 senderName, extras);
194 
195         if (avatar == null) {
196             avatar = notification.getLargeIcon();
197         }
198 
199         Long when;
200         if (notification.showsTime()) {
201             when = notification.when;
202         } else {
203             when = null;
204         }
205 
206         Drawable groupIcon;
207         if (isGroupConversation) {
208             groupIcon = mGroupIcon;
209         } else {
210             groupIcon = null;
211         }
212 
213         int unshownCount = messageCount - 1;
214         String unshownCountText = null;
215         if (!isRestricted && !isHeadsUp && messageStyleFlag) {
216             if (unshownCount > 0) {
217                 unshownCountText = getContext().getResources().getQuantityString(
218                         R.plurals.restricted_numbered_message_content, unshownCount, unshownCount);
219             } else if (messageText.toString().endsWith(mEllipsizedSuffix)) {
220                 unshownCountText = mSeeMoreText;
221             }
222 
223             View.OnClickListener listener =
224                     getCountViewOnClickListener(unshownCount, messages, isGroupConversation,
225                             sbn, conversationTitle, avatar, groupIcon, when);
226             mBodyView.setCountOnClickListener(listener);
227         }
228         mBodyView.bind(conversationTitle, messageText,
229                 sbn, avatar, groupIcon, unshownCountText, when);
230     }
231 
getMessageText(Notification.MessagingStyle.Message message, boolean isRestricted, boolean isHeadsUp, boolean isGroupConversation, CharSequence senderName, int messageCount, Bundle extras)232     private CharSequence getMessageText(Notification.MessagingStyle.Message message,
233             boolean isRestricted, boolean isHeadsUp, boolean isGroupConversation,
234             CharSequence senderName, int messageCount, Bundle extras) {
235         CharSequence messageText = null;
236 
237         if (message != null) {
238             if (DEBUG) {
239                 Log.d(TAG, "Message style message text used.");
240             }
241 
242             messageText = message.getText();
243 
244             if (!isHeadsUp && isGroupConversation) {
245                 // If conversation is a group conversation and notification is not a HUN,
246                 // then prepend sender's name to title.
247                 messageText = senderName + SENDER_BODY_SEPARATOR + messageText;
248             }
249         } else {
250             if (DEBUG) {
251                 Log.d(TAG, "Standard field message text used.");
252             }
253 
254             messageText = extras.getCharSequence(Notification.EXTRA_TEXT);
255         }
256 
257         if (isRestricted) {
258             if (isHeadsUp || messageCount == 1) {
259                 messageText = mNewMessageText;
260             } else {
261                 messageText = getContext().getResources().getQuantityString(
262                         R.plurals.restricted_numbered_message_content, messageCount, messageCount);
263             }
264         }
265 
266         if (!TextUtils.isEmpty(messageText)) {
267             messageText = mPreprocessingManager.trimText(messageText);
268         }
269 
270         return messageText;
271     }
272 
getConversationTitle(MessagingStyle messagingStyle, boolean isHeadsUp, boolean isGroupConversation, CharSequence senderName, Bundle extras)273     private CharSequence getConversationTitle(MessagingStyle messagingStyle, boolean isHeadsUp,
274             boolean isGroupConversation, CharSequence senderName, Bundle extras) {
275         CharSequence conversationTitle = null;
276 
277         if (messagingStyle != null) {
278             conversationTitle = messagingStyle.getConversationTitle();
279         }
280 
281         if (isGroupConversation && conversationTitle != null && isHeadsUp) {
282             // If conversation title has been set, conversation is a group conversation
283             // and notification is a HUN, then prepend sender's name to title.
284             conversationTitle = senderName + SENDER_TITLE_SEPARATOR + conversationTitle;
285         } else if (conversationTitle == null) {
286             if (DEBUG) {
287                 Log.d(TAG, "Conversation title not set.");
288             }
289             // If conversation title has not been set, set it as sender's name.
290             conversationTitle = senderName;
291         }
292 
293         if (TextUtils.isEmpty(conversationTitle)) {
294             if (DEBUG) {
295                 Log.d(TAG, "Standard field conversation title used.");
296             }
297             conversationTitle = extras.getCharSequence(Notification.EXTRA_TITLE);
298         }
299 
300         return conversationTitle;
301     }
302 
getMessageCount(List<Notification.MessagingStyle.Message> messages, int numEvents)303     private int getMessageCount(List<Notification.MessagingStyle.Message> messages, int numEvents) {
304         Integer messageCount = null;
305 
306         if (messages != null) {
307             messageCount = messages.size();
308         } else {
309             messageCount = numEvents;
310             if (messageCount == 0) {
311                 // A notification should at least represent 1 message
312                 messageCount = 1;
313             }
314         }
315 
316         return messageCount;
317     }
318 
319     @Override
reset()320     void reset() {
321         super.reset();
322         mBodyView.reset();
323         mHeaderView.reset();
324         mActionsView.reset();
325     }
326 
getCountViewOnClickListener(int unshownCount, @Nullable List<Notification.MessagingStyle.Message> messages, boolean isGroupConversation, StatusBarNotification sbn, CharSequence title, @Nullable Icon avatar, @Nullable Drawable groupIcon, @Nullable Long when)327     private View.OnClickListener getCountViewOnClickListener(int unshownCount,
328             @Nullable List<Notification.MessagingStyle.Message> messages,
329             boolean isGroupConversation, StatusBarNotification sbn, CharSequence title,
330             @Nullable Icon avatar, @Nullable Drawable groupIcon, @Nullable Long when) {
331         String finalMessage;
332         if (unshownCount > 0) {
333             StringBuilder builder = new StringBuilder();
334             for (int i = messages.size() - 1; i >= messages.size() - 1 - mMaxMessageCount && i >= 0;
335                     i--) {
336                 if (i != messages.size() - 1) {
337                     builder.append(NEW_LINE);
338                     builder.append(NEW_LINE);
339                 }
340                 unshownCount--;
341                 Notification.MessagingStyle.Message message = messages.get(i);
342                 Person sender = message.getSenderPerson();
343                 CharSequence senderName =
344                         (sender != null ? sender.getName() : message.getSender());
345                 if (isGroupConversation) {
346                     builder.append(senderName + SENDER_BODY_SEPARATOR + message.getText());
347                 } else {
348                     builder.append(message.getText());
349                 }
350                 if (builder.toString().split(NEW_LINE).length >= mMaxLineCount) {
351                     break;
352                 }
353             }
354 
355             finalMessage = builder.toString();
356         } else {
357             StringBuilder builder = new StringBuilder();
358             Notification.MessagingStyle.Message message = messages.get(messages.size() - 1);
359             Person sender = message.getSenderPerson();
360             CharSequence senderName =
361                     (sender != null ? sender.getName() : message.getSender());
362             if (isGroupConversation) {
363                 builder.append(senderName + SENDER_BODY_SEPARATOR + message.getText());
364             } else {
365                 builder.append(message.getText());
366             }
367             String messageStr = builder.toString();
368 
369             int maxCharCountAfterExpansion;
370             if (mPreprocessingManager.getMaximumStringLength() == Integer.MAX_VALUE) {
371                 maxCharCountAfterExpansion = Integer.MAX_VALUE;
372             } else {
373                 // We are exceeding UXRE maximum string length limit only when expanding the long
374                 // message notification. This neither applies for collapsed single message
375                 // notifications nor applies for UXRE updates that are handled by `isRestricted`
376                 // being {@code true}.
377                 maxCharCountAfterExpansion = mPreprocessingManager.getMaximumStringLength()
378                         + mAdditionalCharCountAfterExpansion - mEllipsizedSuffix.length();
379             }
380 
381             if (messageStr.length() > maxCharCountAfterExpansion) {
382                 messageStr = messageStr.substring(0, maxCharCountAfterExpansion - 1)
383                         + mEllipsizedSuffix;
384             }
385             finalMessage = messageStr;
386         }
387 
388         int finalUnshownCount = unshownCount;
389 
390         return view -> {
391             String unshownCountText;
392             if (finalUnshownCount <= 0) {
393                 unshownCountText = null;
394             } else {
395                 unshownCountText = getContext().getResources().getQuantityString(
396                         R.plurals.message_unshown_count, finalUnshownCount, finalUnshownCount);
397             }
398 
399             mBodyView.bind(title, finalMessage, sbn, avatar, groupIcon,
400                     unshownCountText, when);
401             mBodyView.setContentMaxLines(mMaxLineCount);
402             mBodyView.setCountOnClickListener(null);
403         };
404     }
405 }
406