package org.thoughtcrime.securesms.notifications; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.text.SpannableStringBuilder; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat.Action; import androidx.core.app.RemoteInput; import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.session.libsession.avatars.ContactColors; import org.session.libsession.avatars.ContactPhoto; import org.session.libsession.avatars.GeneratedContactPhoto; import org.session.libsession.messaging.contacts.Contact; import org.session.libsession.utilities.NotificationPrivacyPreference; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.recipients.Recipient; import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.database.SessionContactDatabase; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator; import org.thoughtcrime.securesms.util.BitmapUtil; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; import network.loki.messenger.R; public class SingleRecipientNotificationBuilder extends AbstractNotificationBuilder { private static final String TAG = SingleRecipientNotificationBuilder.class.getSimpleName(); private final List messageBodies = new LinkedList<>(); private SlideDeck slideDeck; private CharSequence contentTitle; private CharSequence contentText; public SingleRecipientNotificationBuilder(@NonNull Context context, @NonNull NotificationPrivacyPreference privacy) { super(context, privacy); setSmallIcon(R.drawable.ic_notification); setColor(context.getResources().getColor(R.color.textsecure_primary)); setCategory(NotificationCompat.CATEGORY_MESSAGE); if (!NotificationChannels.supported()) { setPriority(TextSecurePreferences.getNotificationPriority(context)); } } public void setThread(@NonNull Recipient recipient) { String channelId = recipient.getNotificationChannel(); setChannelId(channelId != null ? channelId : NotificationChannels.getMessagesChannel(context)); if (privacy.isDisplayContact()) { setContentTitle(recipient.toShortString()); if (recipient.getContactUri() != null) { addPerson(recipient.getContactUri().toString()); } ContactPhoto contactPhoto = recipient.getContactPhoto(); if (contactPhoto != null) { try { // AC: For some reason, if not use ".asBitmap()" method, the returned BitmapDrawable // wraps a recycled bitmap and leads to a crash. Bitmap iconBitmap = GlideApp.with(context.getApplicationContext()) .asBitmap() .load(contactPhoto) .diskCacheStrategy(DiskCacheStrategy.ALL) .circleCrop() .submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height)) .get(); setLargeIcon(iconBitmap); } catch (InterruptedException | ExecutionException e) { Log.w(TAG, e); setLargeIcon(getPlaceholderDrawable(context, recipient)); } } else { setLargeIcon(getPlaceholderDrawable(context, recipient)); } } else { setContentTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal)); setLargeIcon(new GeneratedContactPhoto("Unknown", R.drawable.ic_profile_default).asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context))); } } public void setMessageCount(int messageCount) { setContentInfo(String.valueOf(messageCount)); setNumber(messageCount); } public void setPrimaryMessageBody(@NonNull Recipient threadRecipients, @NonNull Recipient individualRecipient, @NonNull CharSequence message, @Nullable SlideDeck slideDeck) { SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); if (privacy.isDisplayContact() && threadRecipients.isOpenGroupRecipient()) { String displayName = getOpenGroupDisplayName(individualRecipient); stringBuilder.append(Util.getBoldedString(displayName + ": ")); } if (privacy.isDisplayMessage()) { setContentText(stringBuilder.append(message)); this.slideDeck = slideDeck; } else { setContentText(stringBuilder.append(context.getString(R.string.SingleRecipientNotificationBuilder_new_message))); } } public void addAndroidAutoAction(@NonNull PendingIntent androidAutoReplyIntent, @NonNull PendingIntent androidAutoHeardIntent, long timestamp) { if (contentTitle == null || contentText == null) return; RemoteInput remoteInput = new RemoteInput.Builder(AndroidAutoReplyReceiver.VOICE_REPLY_KEY) .setLabel(context.getString(R.string.MessageNotifier_reply)) .build(); NotificationCompat.CarExtender.UnreadConversation.Builder unreadConversationBuilder = new NotificationCompat.CarExtender.UnreadConversation.Builder(contentTitle.toString()) .addMessage(contentText.toString()) .setLatestTimestamp(timestamp) .setReadPendingIntent(androidAutoHeardIntent) .setReplyAction(androidAutoReplyIntent, remoteInput); extend(new NotificationCompat.CarExtender().setUnreadConversation(unreadConversationBuilder.build())); } public void addActions(@NonNull PendingIntent markReadIntent, @Nullable PendingIntent quickReplyIntent, @Nullable PendingIntent wearableReplyIntent, @NonNull ReplyMethod replyMethod) { Action markAsReadAction = new Action(R.drawable.check, context.getString(R.string.MessageNotifier_mark_read), markReadIntent); addAction(markAsReadAction); NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender().addAction(markAsReadAction); if (quickReplyIntent != null) { String actionName = context.getString(R.string.MessageNotifier_reply); String label = context.getString(replyMethodLongDescription(replyMethod)); Action replyAction = new Action(R.drawable.ic_reply_white_36dp, actionName, quickReplyIntent); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { replyAction = new Action.Builder(R.drawable.ic_reply_white_36dp, actionName, wearableReplyIntent) .addRemoteInput(new RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build()) .build(); } Action wearableReplyAction = new Action.Builder(R.drawable.ic_reply, actionName, wearableReplyIntent) .addRemoteInput(new RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build()) .build(); addAction(replyAction); wearableExtender.addAction(wearableReplyAction); } extend(wearableExtender); } @StringRes private static int replyMethodLongDescription(@NonNull ReplyMethod replyMethod) { return R.string.MessageNotifier_reply; } public void putStringExtra(String key, String value) { extras.putString(key,value); } public void addMessageBody(@NonNull Recipient threadRecipient, @NonNull Recipient individualRecipient, @Nullable CharSequence messageBody) { SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); if (privacy.isDisplayContact() && threadRecipient.isOpenGroupRecipient()) { String displayName = getOpenGroupDisplayName(individualRecipient); stringBuilder.append(Util.getBoldedString(displayName + ": ")); } if (privacy.isDisplayMessage()) { messageBodies.add(stringBuilder.append(messageBody == null ? "" : messageBody)); } else { messageBodies.add(stringBuilder.append(context.getString(R.string.SingleRecipientNotificationBuilder_new_message))); } } @Override public Notification build() { if (privacy.isDisplayMessage()) { if (messageBodies.size() == 1 && hasBigPictureSlide(slideDeck)) { setStyle(new NotificationCompat.BigPictureStyle() .bigPicture(getBigPicture(slideDeck)) .setSummaryText(getBigText(messageBodies))); } else { setStyle(new NotificationCompat.BigTextStyle().bigText(getBigText(messageBodies))); } } return super.build(); } private void setLargeIcon(@Nullable Drawable drawable) { if (drawable != null) { int largeIconTargetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); Bitmap recipientPhotoBitmap = BitmapUtil.createFromDrawable(drawable, largeIconTargetSize, largeIconTargetSize); if (recipientPhotoBitmap != null) { setLargeIcon(getCircularBitmap(recipientPhotoBitmap)); } } } private Bitmap getCircularBitmap(Bitmap bitmap) { final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(output); final int color = Color.RED; final Paint paint = new Paint(); final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); final RectF rectF = new RectF(rect); paint.setAntiAlias(true); canvas.drawARGB(0, 0, 0, 0); paint.setColor(color); canvas.drawOval(rectF, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(bitmap, rect, rect, paint); bitmap.recycle(); return output; } private boolean hasBigPictureSlide(@Nullable SlideDeck slideDeck) { if (slideDeck == null) { return false; } Slide thumbnailSlide = slideDeck.getThumbnailSlide(); return thumbnailSlide != null && thumbnailSlide.hasImage() && !thumbnailSlide.isInProgress() && thumbnailSlide.getThumbnailUri() != null; } private Bitmap getBigPicture(@NonNull SlideDeck slideDeck) { try { @SuppressWarnings("ConstantConditions") Uri uri = slideDeck.getThumbnailSlide().getThumbnailUri(); return GlideApp.with(context.getApplicationContext()) .asBitmap() .load(new DecryptableStreamUriLoader.DecryptableUri(uri)) .diskCacheStrategy(DiskCacheStrategy.NONE) .submit(500, 500) .get(); } catch (InterruptedException | ExecutionException e) { Log.w(TAG, e); return Bitmap.createBitmap(500, 500, Bitmap.Config.RGB_565); } } @Override public NotificationCompat.Builder setContentTitle(CharSequence contentTitle) { this.contentTitle = contentTitle; return super.setContentTitle(contentTitle); } public NotificationCompat.Builder setContentText(CharSequence contentText) { this.contentText = trimToDisplayLength(contentText); return super.setContentText(this.contentText); } private CharSequence getBigText(List messageBodies) { SpannableStringBuilder content = new SpannableStringBuilder(); for (int i = 0; i < messageBodies.size(); i++) { content.append(trimToDisplayLength(messageBodies.get(i))); if (i < messageBodies.size() - 1) { content.append('\n'); } } return content; } private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) { String publicKey = recipient.getAddress().serialize(); String displayName = recipient.getName(); return AvatarPlaceholderGenerator.generate(context, 128, publicKey, displayName); } /** * @param recipient the * individual * recipient for which to get the open group display name. */ private String getOpenGroupDisplayName(Recipient recipient) { SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase(); String sessionID = recipient.getAddress().serialize(); Contact contact = contactDB.getContactWithSessionID(sessionID); if (contact == null) { return sessionID; } String displayName = contact.displayName(Contact.ContactContext.OPEN_GROUP); if (displayName == null) { return sessionID; } return displayName; } }