MessagingLayout.java revision 1397ea3b910d0981dc4bf91016a78af8fcec08bf
1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package com.android.internal.widget; 18 19import android.annotation.AttrRes; 20import android.annotation.NonNull; 21import android.annotation.Nullable; 22import android.annotation.StyleRes; 23import android.app.Notification; 24import android.content.Context; 25import android.graphics.Bitmap; 26import android.graphics.Canvas; 27import android.graphics.Color; 28import android.graphics.Paint; 29import android.graphics.Rect; 30import android.graphics.drawable.Icon; 31import android.os.Bundle; 32import android.os.Parcelable; 33import android.text.TextUtils; 34import android.util.ArrayMap; 35import android.util.AttributeSet; 36import android.util.DisplayMetrics; 37import android.view.RemotableViewMethod; 38import android.view.ViewTreeObserver; 39import android.view.animation.Interpolator; 40import android.view.animation.PathInterpolator; 41import android.widget.FrameLayout; 42import android.widget.RemoteViews; 43import android.widget.TextView; 44 45import com.android.internal.R; 46import com.android.internal.graphics.ColorUtils; 47import com.android.internal.util.NotificationColorUtil; 48 49import java.util.ArrayList; 50import java.util.List; 51import java.util.function.Consumer; 52 53/** 54 * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal 55 * messages and adapts the layout accordingly. 56 */ 57@RemoteViews.RemoteView 58public class MessagingLayout extends FrameLayout { 59 60 private static final float COLOR_SHIFT_AMOUNT = 60; 61 private static final Consumer<MessagingMessage> REMOVE_MESSAGE 62 = MessagingMessage::removeMessage; 63 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); 64 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); 65 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); 66 public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR 67 = new MessagingPropertyAnimator(); 68 private List<MessagingMessage> mMessages = new ArrayList<>(); 69 private List<MessagingMessage> mHistoricMessages = new ArrayList<>(); 70 private MessagingLinearLayout mMessagingLinearLayout; 71 private boolean mShowHistoricMessages; 72 private ArrayList<MessagingGroup> mGroups = new ArrayList<>(); 73 private TextView mTitleView; 74 private int mLayoutColor; 75 private int mAvatarSize; 76 private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 77 private Paint mTextPaint = new Paint(); 78 private CharSequence mConversationTitle; 79 private Icon mLargeIcon; 80 private boolean mIsOneToOne; 81 private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>(); 82 private Notification.Person mUser; 83 84 public MessagingLayout(@NonNull Context context) { 85 super(context); 86 } 87 88 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 89 super(context, attrs); 90 } 91 92 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, 93 @AttrRes int defStyleAttr) { 94 super(context, attrs, defStyleAttr); 95 } 96 97 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, 98 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { 99 super(context, attrs, defStyleAttr, defStyleRes); 100 } 101 102 @Override 103 protected void onFinishInflate() { 104 super.onFinishInflate(); 105 mMessagingLinearLayout = findViewById(R.id.notification_messaging); 106 mMessagingLinearLayout.setMessagingLayout(this); 107 // We still want to clip, but only on the top, since views can temporarily out of bounds 108 // during transitions. 109 DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 110 Rect rect = new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels); 111 mMessagingLinearLayout.setClipBounds(rect); 112 mTitleView = findViewById(R.id.title); 113 mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size); 114 mTextPaint.setTextAlign(Paint.Align.CENTER); 115 mTextPaint.setAntiAlias(true); 116 } 117 118 @RemotableViewMethod 119 public void setLargeIcon(Icon icon) { 120 mLargeIcon = icon; 121 } 122 123 @RemotableViewMethod 124 public void setData(Bundle extras) { 125 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); 126 List<Notification.MessagingStyle.Message> newMessages 127 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); 128 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); 129 List<Notification.MessagingStyle.Message> newHistoricMessages 130 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); 131 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON)); 132 mConversationTitle = null; 133 TextView headerText = findViewById(R.id.header_text); 134 if (headerText != null) { 135 mConversationTitle = headerText.getText(); 136 } 137 addRemoteInputHistoryToMessages(newMessages, 138 extras.getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY)); 139 bind(newMessages, newHistoricMessages); 140 } 141 142 private void addRemoteInputHistoryToMessages( 143 List<Notification.MessagingStyle.Message> newMessages, 144 CharSequence[] remoteInputHistory) { 145 if (remoteInputHistory == null || remoteInputHistory.length == 0) { 146 return; 147 } 148 for (int i = remoteInputHistory.length - 1; i >= 0; i--) { 149 CharSequence message = remoteInputHistory[i]; 150 newMessages.add(new Notification.MessagingStyle.Message( 151 message, 0, (Notification.Person) null)); 152 } 153 } 154 155 private void bind(List<Notification.MessagingStyle.Message> newMessages, 156 List<Notification.MessagingStyle.Message> newHistoricMessages) { 157 158 List<MessagingMessage> historicMessages = createMessages(newHistoricMessages, 159 true /* isHistoric */); 160 List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */); 161 addMessagesToGroups(historicMessages, messages); 162 163 // Let's remove the remaining messages 164 mMessages.forEach(REMOVE_MESSAGE); 165 mHistoricMessages.forEach(REMOVE_MESSAGE); 166 167 mMessages = messages; 168 mHistoricMessages = historicMessages; 169 170 updateHistoricMessageVisibility(); 171 updateTitleAndNamesDisplay(); 172 } 173 174 private void updateTitleAndNamesDisplay() { 175 ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>(); 176 ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>(); 177 for (int i = 0; i < mGroups.size(); i++) { 178 MessagingGroup group = mGroups.get(i); 179 CharSequence senderName = group.getSenderName(); 180 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { 181 continue; 182 } 183 if (!uniqueNames.containsKey(senderName)) { 184 char c = senderName.charAt(0); 185 if (uniqueCharacters.containsKey(c)) { 186 // this character was already used, lets make it more unique. We first need to 187 // resolve the existing character if it exists 188 CharSequence existingName = uniqueCharacters.get(c); 189 if (existingName != null) { 190 uniqueNames.put(existingName, findNameSplit((String) existingName)); 191 uniqueCharacters.put(c, null); 192 } 193 uniqueNames.put(senderName, findNameSplit((String) senderName)); 194 } else { 195 uniqueNames.put(senderName, Character.toString(c)); 196 uniqueCharacters.put(c, senderName); 197 } 198 } 199 } 200 201 // Now that we have the correct symbols, let's look what we have cached 202 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>(); 203 for (int i = 0; i < mGroups.size(); i++) { 204 // Let's now set the avatars 205 MessagingGroup group = mGroups.get(i); 206 boolean isOwnMessage = group.getSender() == mUser; 207 CharSequence senderName = group.getSenderName(); 208 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName) 209 || (mIsOneToOne && mLargeIcon != null && !isOwnMessage)) { 210 continue; 211 } 212 String symbol = uniqueNames.get(senderName); 213 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName, 214 symbol, mLayoutColor); 215 if (cachedIcon != null) { 216 cachedAvatars.put(senderName, cachedIcon); 217 } 218 } 219 220 for (int i = 0; i < mGroups.size(); i++) { 221 // Let's now set the avatars 222 MessagingGroup group = mGroups.get(i); 223 CharSequence senderName = group.getSenderName(); 224 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { 225 continue; 226 } 227 if (mIsOneToOne && mLargeIcon != null && group.getSender() != mUser) { 228 group.setAvatar(mLargeIcon); 229 } else { 230 Icon cachedIcon = cachedAvatars.get(senderName); 231 if (cachedIcon == null) { 232 cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName), 233 mLayoutColor); 234 cachedAvatars.put(senderName, cachedIcon); 235 } 236 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName), 237 mLayoutColor); 238 } 239 } 240 } 241 242 public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) { 243 Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888); 244 Canvas canvas = new Canvas(bitmap); 245 float radius = mAvatarSize / 2.0f; 246 int color = findColor(senderName, layoutColor); 247 mPaint.setColor(color); 248 canvas.drawCircle(radius, radius, radius, mPaint); 249 boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f; 250 mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE); 251 mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f); 252 int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)) ; 253 canvas.drawText(symbol, radius, yPos, mTextPaint); 254 return Icon.createWithBitmap(bitmap); 255 } 256 257 private int findColor(CharSequence senderName, int layoutColor) { 258 double luminance = NotificationColorUtil.calculateLuminance(layoutColor); 259 float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f; 260 261 // we need to offset the range if the luminance is too close to the borders 262 shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0); 263 shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0); 264 return NotificationColorUtil.getShiftedColor(layoutColor, 265 (int) (shift * COLOR_SHIFT_AMOUNT)); 266 } 267 268 private String findNameSplit(String existingName) { 269 String[] split = existingName.split(" "); 270 if (split.length > 1) { 271 return Character.toString(split[0].charAt(0)) 272 + Character.toString(split[1].charAt(0)); 273 } 274 return existingName.substring(0, 1); 275 } 276 277 @RemotableViewMethod 278 public void setLayoutColor(int color) { 279 mLayoutColor = color; 280 } 281 282 @RemotableViewMethod 283 public void setIsOneToOne(boolean oneToOne) { 284 mIsOneToOne = oneToOne; 285 } 286 287 public void setUser(Notification.Person user) { 288 mUser = user; 289 if (mUser.getIcon() == null) { 290 Icon userIcon = Icon.createWithResource(getContext(), 291 com.android.internal.R.drawable.messaging_user); 292 userIcon.setTint(mLayoutColor); 293 mUser.setIcon(userIcon); 294 } 295 } 296 297 private void addMessagesToGroups(List<MessagingMessage> historicMessages, 298 List<MessagingMessage> messages) { 299 // Let's first find our groups! 300 List<List<MessagingMessage>> groups = new ArrayList<>(); 301 List<Notification.Person> senders = new ArrayList<>(); 302 303 // Lets first find the groups 304 findGroups(historicMessages, messages, groups, senders); 305 306 // Let's now create the views and reorder them accordingly 307 createGroupViews(groups, senders); 308 } 309 310 private void createGroupViews(List<List<MessagingMessage>> groups, 311 List<Notification.Person> senders) { 312 mGroups.clear(); 313 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) { 314 List<MessagingMessage> group = groups.get(groupIndex); 315 MessagingGroup newGroup = null; 316 // we'll just take the first group that exists or create one there is none 317 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) { 318 MessagingMessage message = group.get(messageIndex); 319 newGroup = message.getGroup(); 320 if (newGroup != null) { 321 break; 322 } 323 } 324 if (newGroup == null) { 325 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout); 326 mAddedGroups.add(newGroup); 327 } 328 newGroup.setLayoutColor(mLayoutColor); 329 newGroup.setSender(senders.get(groupIndex)); 330 mGroups.add(newGroup); 331 332 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) { 333 mMessagingLinearLayout.removeView(newGroup); 334 mMessagingLinearLayout.addView(newGroup, groupIndex); 335 } 336 newGroup.setMessages(group); 337 } 338 } 339 340 private void findGroups(List<MessagingMessage> historicMessages, 341 List<MessagingMessage> messages, List<List<MessagingMessage>> groups, 342 List<Notification.Person> senders) { 343 CharSequence currentSenderKey = null; 344 List<MessagingMessage> currentGroup = null; 345 int histSize = historicMessages.size(); 346 for (int i = 0; i < histSize + messages.size(); i++) { 347 MessagingMessage message; 348 if (i < histSize) { 349 message = historicMessages.get(i); 350 } else { 351 message = messages.get(i - histSize); 352 } 353 boolean isNewGroup = currentGroup == null; 354 Notification.Person sender = message.getMessage().getSenderPerson(); 355 CharSequence key = sender == null ? null 356 : sender.getKey() == null ? sender.getName() : sender.getKey(); 357 isNewGroup |= !TextUtils.equals(key, currentSenderKey); 358 if (isNewGroup) { 359 currentGroup = new ArrayList<>(); 360 groups.add(currentGroup); 361 if (sender == null) { 362 sender = mUser; 363 } 364 senders.add(sender); 365 currentSenderKey = key; 366 } 367 currentGroup.add(message); 368 } 369 } 370 371 /** 372 * Creates new messages, reusing existing ones if they are available. 373 * 374 * @param newMessages the messages to parse. 375 */ 376 private List<MessagingMessage> createMessages( 377 List<Notification.MessagingStyle.Message> newMessages, boolean historic) { 378 List<MessagingMessage> result = new ArrayList<>();; 379 for (int i = 0; i < newMessages.size(); i++) { 380 Notification.MessagingStyle.Message m = newMessages.get(i); 381 MessagingMessage message = findAndRemoveMatchingMessage(m); 382 if (message == null) { 383 message = MessagingMessage.createMessage(this, m); 384 message.addOnLayoutChangeListener(MESSAGING_PROPERTY_ANIMATOR); 385 } 386 message.setIsHistoric(historic); 387 result.add(message); 388 } 389 return result; 390 } 391 392 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) { 393 for (int i = 0; i < mMessages.size(); i++) { 394 MessagingMessage existing = mMessages.get(i); 395 if (existing.sameAs(m)) { 396 mMessages.remove(i); 397 return existing; 398 } 399 } 400 for (int i = 0; i < mHistoricMessages.size(); i++) { 401 MessagingMessage existing = mHistoricMessages.get(i); 402 if (existing.sameAs(m)) { 403 mHistoricMessages.remove(i); 404 return existing; 405 } 406 } 407 return null; 408 } 409 410 public void showHistoricMessages(boolean show) { 411 mShowHistoricMessages = show; 412 updateHistoricMessageVisibility(); 413 } 414 415 private void updateHistoricMessageVisibility() { 416 for (int i = 0; i < mHistoricMessages.size(); i++) { 417 MessagingMessage existing = mHistoricMessages.get(i); 418 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE); 419 } 420 } 421 422 @Override 423 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 424 super.onLayout(changed, left, top, right, bottom); 425 if (!mAddedGroups.isEmpty()) { 426 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 427 @Override 428 public boolean onPreDraw() { 429 for (MessagingGroup group : mAddedGroups) { 430 if (!group.isShown()) { 431 continue; 432 } 433 MessagingPropertyAnimator.fadeIn(group.getAvatar()); 434 MessagingPropertyAnimator.fadeIn(group.getSenderView()); 435 MessagingPropertyAnimator.startLocalTranslationFrom(group, 436 group.getHeight(), LINEAR_OUT_SLOW_IN); 437 } 438 mAddedGroups.clear(); 439 getViewTreeObserver().removeOnPreDrawListener(this); 440 return true; 441 } 442 }); 443 } 444 } 445 446 public MessagingLinearLayout getMessagingLinearLayout() { 447 return mMessagingLinearLayout; 448 } 449 450 public ArrayList<MessagingGroup> getMessagingGroups() { 451 return mGroups; 452 } 453} 454