From 526f328e82edb3ec522b43cb3c6670c8c15e5626 Mon Sep 17 00:00:00 2001 From: navneetsingh-dev Date: Fri, 26 Jun 2026 23:01:00 +0530 Subject: [PATCH 1/6] Fix #82: Filter blocked contacts from notification query The getNotificationQuerySql method was previously fetching all unread incoming messages, including those from blocked contacts. This commit injects a ParticipantColumns.BLOCKED = 0 check to ensure blocked messages do not trigger system notifications or audio alerts. --- .../messaging/datamodel/data/ConversationMessageData.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/com/android/messaging/datamodel/data/ConversationMessageData.java b/src/com/android/messaging/datamodel/data/ConversationMessageData.java index 77e821f64..4303c2056 100644 --- a/src/com/android/messaging/datamodel/data/ConversationMessageData.java +++ b/src/com/android/messaging/datamodel/data/ConversationMessageData.java @@ -651,7 +651,9 @@ public static final String getNotificationQuerySql() { + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", " + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")" + " AND " - + DatabaseHelper.MessageColumns.SEEN + " = 0)" + + DatabaseHelper.MessageColumns.SEEN + " = 0" + + " AND " + + DatabaseHelper.PARTICIPANTS_TABLE + "." + ParticipantColumns.BLOCKED + " = 0)" + ")" + NOTIFICATION_QUERY_SQL_GROUP_BY; } From 248d62995d6a7992a7832e1caeeb75bf960b8114 Mon Sep 17 00:00:00 2001 From: navneetsingh-dev Date: Wed, 15 Jul 2026 12:56:24 +0530 Subject: [PATCH 2/6] feat: Gate audio, secondary users, and MMS notifications for blocked senders --- gradle/verification-keyring.gpg | 0 gradle/verification-metadata.xml | 16 +++++++ settings.gradle.kts | 3 ++ .../datamodel/BugleNotifications.java | 46 ++++++++++++++++--- .../action/ProcessDownloadedMmsAction.java | 17 +++---- .../messaging/receiver/SmsReceiver.java | 17 ++++++- 6 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 gradle/verification-keyring.gpg diff --git a/gradle/verification-keyring.gpg b/gradle/verification-keyring.gpg new file mode 100644 index 000000000..e69de29bb diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 38abfa399..1a2acbe77 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -12874,6 +12874,22 @@ + + + + + + + + + + + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 3d8f30d48..004a112c1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { diff --git a/src/com/android/messaging/datamodel/BugleNotifications.java b/src/com/android/messaging/datamodel/BugleNotifications.java index 23c532fce..ea62c1b3c 100644 --- a/src/com/android/messaging/datamodel/BugleNotifications.java +++ b/src/com/android/messaging/datamodel/BugleNotifications.java @@ -134,7 +134,7 @@ public static void update(final String conversationId, final int coverage) { LogUtil.v(TAG, "Update: conversationId = " + conversationId + " coverage = " + coverage); } - Assert.isNotMainThread(); + Assert.isNotMainThread(); if (!PhoneUtils.getDefault().isDefaultSmsApp()) { LogUtil.d(TAG, "Skipping notification: not the default SMS app"); @@ -157,10 +157,42 @@ public static void update(final String conversationId, final int coverage) { private static void createMessageNotification(final String conversationId) { final MessageNotificationState state = MessageNotificationState.getNotificationState(); final boolean softSound = DataModel.get().isNewMessageObservable(conversationId); + if (state == null) { if (softSound && !TextUtils.isEmpty(conversationId)) { - final Uri ringtoneUri = getNotificationRingtoneUriForConversationId(conversationId); - playObservableConversationNotificationSound(ringtoneUri); + + boolean isBlocked = false; + + // 1. Access the database to check the participant's blocked status + final DatabaseWrapper db = DataModel.get().getDatabase(); + + // 2. Query the participant table for this specific conversation ID + final String query = "SELECT " + DatabaseHelper.ParticipantColumns.BLOCKED + + " FROM " + DatabaseHelper.PARTICIPANTS_TABLE + + " INNER JOIN " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE + + " ON " + DatabaseHelper.PARTICIPANTS_TABLE + "." + DatabaseHelper.ParticipantColumns._ID + + " = " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE + "." + DatabaseHelper.ConversationParticipantsColumns.PARTICIPANT_ID + + " WHERE " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE + "." + DatabaseHelper.ConversationParticipantsColumns.CONVERSATION_ID + " = ?"; + + try (android.database.Cursor cursor = db.rawQuery(query, new String[] { conversationId })) { + if (cursor != null) { + while (cursor.moveToNext()) { + // If any participant in this conversation has BLOCKED = 1, flag it + if (cursor.getInt(0) == 1) { + isBlocked = true; + break; + } + } + } + } + + // 3. The Gatekeeper: Only play the sound if the conversation is NOT blocked + if (!isBlocked) { + final Uri ringtoneUri = getNotificationRingtoneUriForConversationId(conversationId); + playObservableConversationNotificationSound(ringtoneUri); + } else { + LogUtil.v(TAG, "Suppressed audio for blocked observable conversation."); + } } return; } @@ -232,7 +264,7 @@ private static Uri getNotificationRingtoneUriForConversationId(final String conv * @param conversationId The conversation id (optional) */ private static String buildNotificationTag(final String name, - final String conversationId) { + final String conversationId) { final Context context = Factory.get().getApplicationContext(); if (conversationId != null) { return context.getPackageName() + name + ":" + conversationId; @@ -489,7 +521,7 @@ private static void playObservableConversationNotificationSound(final Uri ringto final boolean silenced = audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL; if (silenced) { - return; + return; } final NotificationPlayer player = new NotificationPlayer(LogUtil.BUGLE_TAG); @@ -522,12 +554,12 @@ public static void markMessagesAsRead(final String conversationId, final boolean } public static void notifyEmergencySmsFailed(final String emergencyNumber, - final String conversationId) { + final String conversationId) { final Context context = Factory.get().getApplicationContext(); final CharSequence line1 = MessageNotificationState.applyWarningTextColor(context, context.getString(R.string.notification_emergency_send_failure_line1, - emergencyNumber)); + emergencyNumber)); final String line2 = context.getString(R.string.notification_emergency_send_failure_line2, emergencyNumber); final PendingIntent destinationIntent = UIIntents.get() diff --git a/src/com/android/messaging/datamodel/action/ProcessDownloadedMmsAction.java b/src/com/android/messaging/datamodel/action/ProcessDownloadedMmsAction.java index 21fdef56b..2ba2764af 100644 --- a/src/com/android/messaging/datamodel/action/ProcessDownloadedMmsAction.java +++ b/src/com/android/messaging/datamodel/action/ProcessDownloadedMmsAction.java @@ -136,10 +136,10 @@ public static void processMessageDownloaded(final int resultCode, final Bundle e // This is called for fast failing downloading (due to airplane mode or mobile data ) public static void processMessageDownloadFastFailed(final String messageId, - final Uri notificationUri, final String conversationId, final String participantId, - final String contentLocation, final int subId, final String subPhoneNumber, - final int statusIfFailed, final boolean autoDownload, final String transactionId, - final int resultCode) { + final Uri notificationUri, final String conversationId, final String participantId, + final String contentLocation, final int subId, final String subPhoneNumber, + final int statusIfFailed, final boolean autoDownload, final String transactionId, + final int resultCode) { Assert.notNull(messageId); Assert.notNull(notificationUri); Assert.notNull(conversationId); @@ -163,8 +163,8 @@ public static void processMessageDownloadFastFailed(final String messageId, } public static void processDownloadActionFailure(final String messageId, final int status, - final int rawStatus, final String conversationId, final String participantId, - final int statusIfFailed, final int subId, final String transactionId) { + final int rawStatus, final String conversationId, final String participantId, + final int statusIfFailed, final int subId, final String transactionId) { Assert.notNull(messageId); Assert.notNull(conversationId); Assert.notNull(participantId); @@ -184,7 +184,7 @@ public static void processDownloadActionFailure(final String messageId, final in } public static void sendDeferredRespStatus(final String messageId, final String transactionId, - final String contentLocation, final int subId) { + final String contentLocation, final int subId) { final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction(); final Bundle params = action.actionParameters; params.putString(KEY_MESSAGE_ID, messageId); @@ -473,7 +473,8 @@ private MessageData processResult(final int status, final int rawStatus, final U // TODO: Also write these values to the telephony provider mms.mRead = messageInFocusedConversation; - mms.mSeen = messageInObservableConversation; + // Align with standard SMS: seen = read || messageInObservableConversation || blocked + mms.mSeen = mms.mRead || messageInObservableConversation || blockedSender; // Translate to our format message = MmsUtils.createMmsMessage(mms, conversationId, senderParticipantId, diff --git a/src/com/android/messaging/receiver/SmsReceiver.java b/src/com/android/messaging/receiver/SmsReceiver.java index d96407d9b..3ae511b6f 100644 --- a/src/com/android/messaging/receiver/SmsReceiver.java +++ b/src/com/android/messaging/receiver/SmsReceiver.java @@ -153,8 +153,23 @@ public void onReceive(final Context context, final Intent intent) { (Telephony.Sms.Intents.SMS_RECEIVED_ACTION.equals(action) || // TODO: update this with the actual constant from Telephony "android.provider.Telephony.MMS_DOWNLOADED".equals(action))) { - postNewMessageSecondaryUserNotification(); + boolean isSenderBlocked = false; + final android.telephony.SmsMessage[] messages = getMessagesFromIntent(intent); + if (messages != null && messages.length > 0) { + final String senderAddress = messages[0].getOriginatingAddress(); + if (senderAddress != null) { + final com.android.messaging.datamodel.DatabaseWrapper db = com.android.messaging.datamodel.DataModel.get().getDatabase(); + isSenderBlocked = com.android.messaging.datamodel.BugleDatabaseOperations.isBlockedDestination(db, senderAddress); + + } + } + if (isSenderBlocked) { + postNewMessageSecondaryUserNotification(); + }else{ + LogUtil.v(TAG,"SmsReceviver:Suppressed secondary user notification for blocked sender"); + } } + } } From b042f257a653320c60e40e9b0f364dfec3eb8428 Mon Sep 17 00:00:00 2001 From: navneetsingh-dev Date: Sun, 20 Sep 2026 19:07:57 +0530 Subject: [PATCH 3/6] fixed BugleNotifications.java issue --- lib/platform_frameworks_opt_photoviewer | 1 + 1 file changed, 1 insertion(+) create mode 160000 lib/platform_frameworks_opt_photoviewer diff --git a/lib/platform_frameworks_opt_photoviewer b/lib/platform_frameworks_opt_photoviewer new file mode 160000 index 000000000..d48e36941 --- /dev/null +++ b/lib/platform_frameworks_opt_photoviewer @@ -0,0 +1 @@ +Subproject commit d48e36941419ebfd4f6faf77bf3331d810fc06bd From 605504a992900a6ccf67f618972a64db9976ca47 Mon Sep 17 00:00:00 2001 From: navneetsingh-dev Date: Sun, 20 Sep 2026 19:33:36 +0530 Subject: [PATCH 4/6] Implement isConversationBlocked hook for InConversationSound --- .../datamodel/BugleNotifications.java | 394 +++++++++++++----- 1 file changed, 279 insertions(+), 115 deletions(-) diff --git a/src/com/android/messaging/datamodel/BugleNotifications.java b/src/com/android/messaging/datamodel/BugleNotifications.java index bc3666ca6..2c52d460d 100644 --- a/src/com/android/messaging/datamodel/BugleNotifications.java +++ b/src/com/android/messaging/datamodel/BugleNotifications.java @@ -16,15 +16,16 @@ package com.android.messaging.datamodel; -import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.media.AudioManager; +import android.graphics.Matrix; import android.net.Uri; +import android.service.notification.StatusBarNotification; import android.text.TextUtils; +import android.text.format.DateUtils; import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationCompat; @@ -39,7 +40,7 @@ import com.android.messaging.Factory; import com.android.messaging.R; -import com.android.messaging.data.conversationsettings.repository.ConversationNotificationRepository; +import com.android.messaging.data.conversationsettings.repository.ConversationSnoozeQuery; import com.android.messaging.datamodel.MessageNotificationState.Conversation; import com.android.messaging.datamodel.action.MarkAsReadAction; import com.android.messaging.datamodel.action.MarkAsSeenAction; @@ -51,6 +52,7 @@ import com.android.messaging.datamodel.media.ImageResource; import com.android.messaging.datamodel.media.MediaRequest; import com.android.messaging.datamodel.media.MediaResourceManager; +import com.android.messaging.datamodel.media.UriImageRequestDescriptor; import com.android.messaging.sms.MmsSmsUtils; import com.android.messaging.sms.MmsUtils; import com.android.messaging.ui.UIIntents; @@ -58,21 +60,24 @@ import com.android.messaging.util.AvatarUriUtil; import com.android.messaging.util.LogUtil; import com.android.messaging.util.NotificationChannelUtil; -import com.android.messaging.util.NotificationPlayer; +import com.android.messaging.util.OsUtil; import com.android.messaging.util.PendingIntentConstants; import com.android.messaging.util.PhoneUtils; -import com.android.messaging.util.RingtoneUtil; -import com.android.messaging.util.ThreadUtil; import com.android.messaging.util.UriUtil; +import com.android.messaging.util.exif.ExifInterface; -import dagger.hilt.android.EntryPointAccessors; - +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; /** * Handle posting, updating and removing all conversation notifications.

@@ -91,8 +96,10 @@ public class BugleNotifications { // Logging public static final String TAG = LogUtil.BUGLE_NOTIFICATIONS_TAG; + @VisibleForTesting + public static final int REQUEST_CODE_REDOWNLOAD_MMS = 101; + // Constants to use for update. - @SuppressWarnings("unused") public static final int UPDATE_NONE = 0; public static final int UPDATE_MESSAGES = 1; public static final int UPDATE_ERRORS = 2; @@ -100,6 +107,7 @@ public class BugleNotifications { private static final String SMS_NOTIFICATION_TAG = ":sms:"; private static final String SMS_ERROR_NOTIFICATION_TAG = ":error:"; + public static final String SMS_OVERFLOW_NOTIFICATION_TAG = ":sms:overflow:"; /** * Upper bound on the number of {@link Person} entries attached to a conversation notification. @@ -108,11 +116,20 @@ public class BugleNotifications { @VisibleForTesting public static final int MAX_NOTIFICATION_PEOPLE = 25; - /** - * This is the volume at which to play the observable-conversation notification sound, - * expressed as a fraction of the system notification volume. - */ - private static final float OBSERVABLE_CONVERSATION_NOTIFICATION_VOLUME = 0.25f; + @VisibleForTesting + public static final int MAX_CONVERSATION_NOTIFICATIONS = 40; + + private static final int NOTIFICATION_IMAGE_MAX_SIZE = 1024; + private static final int NOTIFICATION_IMAGE_QUALITY = 85; + private static final long NOTIFICATION_IMAGE_SWEEP_GRACE_MILLIS = + 30 * DateUtils.SECOND_IN_MILLIS; + private static final long NOTIFICATION_IMAGE_SWEEP_INTERVAL_MILLIS = + 5 * DateUtils.MINUTE_IN_MILLIS; + + private static final AtomicLong sLastNotificationImageSweep = new AtomicLong(); + + /** Guards a notification pass, so that concurrent callers do not repeat each other's work */ + private static final Object sNotificationLock = new Object(); /** * Entry point for posting notifications. @@ -138,68 +155,141 @@ public static void update(final String conversationId, final int coverage) { } Assert.isNotMainThread(); - if (!PhoneUtils.getDefault().isDefaultSmsApp()) { - LogUtil.d(TAG, "Skipping notification: not the default SMS app"); - cancel(PendingIntentConstants.SMS_NOTIFICATION_ID); - return; - } - if (conversationId != null && isConversationSnoozed(conversationId)) { - LogUtil.d(TAG, "Skipping notification: conversation snoozed, id=" + conversationId); - cancel(PendingIntentConstants.SMS_NOTIFICATION_ID, conversationId); - return; - } - if ((coverage & UPDATE_MESSAGES) != 0) { - createMessageNotification(conversationId); + synchronized (sNotificationLock) { + updateSerialized(conversationId, coverage); } - if ((coverage & UPDATE_ERRORS) != 0) { - MessageNotificationState.checkFailedMessages(); + } + + private static void updateSerialized(final String conversationId, final int coverage) { + final long passStart = System.currentTimeMillis(); + try { + if (!PhoneUtils.getDefault().isDefaultSmsApp()) { + LogUtil.d(TAG, "Skipping notification: not the default SMS app"); + cancel(PendingIntentConstants.SMS_NOTIFICATION_ID); + return; + } + if (conversationId != null + && ConversationSnoozeQuery.isConversationSnoozed(conversationId)) { + LogUtil.d(TAG, "Skipping notification: conversation snoozed, id=" + + conversationId); + cancel(PendingIntentConstants.SMS_NOTIFICATION_ID, conversationId); + return; + } + if ((coverage & UPDATE_MESSAGES) != 0) { + createMessageNotification(conversationId); + } + if ((coverage & UPDATE_ERRORS) != 0) { + MessageNotificationState.checkFailedMessages(); + } + } finally { + if (isNotificationImageSweepDue(passStart)) { + sweepNotificationImages(passStart); + } } } - private static void createMessageNotification(final String conversationId) { - final MessageNotificationState state = MessageNotificationState.getNotificationState(); - final boolean softSound = DataModel.get().isNewMessageObservable(conversationId); + private static boolean isNotificationImageSweepDue(final long passStart) { + final long lastSweep = sLastNotificationImageSweep.get(); + return passStart - lastSweep >= NOTIFICATION_IMAGE_SWEEP_INTERVAL_MILLIS + && sLastNotificationImageSweep.compareAndSet(lastSweep, passStart); + } - if (state == null) { - if (softSound && !TextUtils.isEmpty(conversationId)) { - - boolean isBlocked = false; - - // 1. Access the database to check the participant's blocked status - final DatabaseWrapper db = DataModel.get().getDatabase(); - - // 2. Query the messages table to find the specific sender of the most recent message in this conversation, - // and join with the participants table to check if that exact sender is blocked. - final String query = "SELECT " + DatabaseHelper.PARTICIPANTS_TABLE + "." + DatabaseHelper.ParticipantColumns.BLOCKED + - " FROM " + DatabaseHelper.MESSAGES_TABLE + - " INNER JOIN " + DatabaseHelper.PARTICIPANTS_TABLE + - " ON " + DatabaseHelper.MESSAGES_TABLE + "." + DatabaseHelper.MessageColumns.SENDER_PARTICIPANT_ID + - " = " + DatabaseHelper.PARTICIPANTS_TABLE + "." + DatabaseHelper.ParticipantColumns._ID + - " WHERE " + DatabaseHelper.MESSAGES_TABLE + "." + DatabaseHelper.MessageColumns.CONVERSATION_ID + " = ?" + - " ORDER BY " + DatabaseHelper.MESSAGES_TABLE + "." + DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " DESC LIMIT 1"; - - try (android.database.Cursor cursor = db.rawQuery(query, new String[] { conversationId })) { - if (cursor != null && cursor.moveToFirst()) { - // If the specific sender of the newest message is blocked, flag it - isBlocked = (cursor.getInt(0) == 1); - } + @VisibleForTesting + static void sweepNotificationImages(final long passStart) { + final Set live = new HashSet<>(); + final StatusBarNotification[] activeNotifications = NotificationChannelUtil.INSTANCE + .getNotificationManager() + .getActiveNotifications(); + + for (final StatusBarNotification posted : activeNotifications) { + final MessagingStyle style = MessagingStyle + .extractMessagingStyleFromNotification(posted.getNotification()); + + if (style == null) { + continue; + } + + for (final MessagingStyle.Message message : style.getMessages()) { + final Uri dataUri = message.getDataUri(); + + if (!NotificationImageProvider.isNotificationImageUri(dataUri)) { + continue; } - // 3. The Gatekeeper: Only play the sound if the actual sender is NOT blocked - if (!isBlocked) { - final Uri ringtoneUri = getNotificationRingtoneUriForConversationId(conversationId); - playObservableConversationNotificationSound(ringtoneUri); - } else { - LogUtil.v(TAG, "Suppressed audio for blocked sender in observable conversation."); + final File liveFile = NotificationImageProvider.getFileFromUri(dataUri); + if (liveFile != null) { + live.add(liveFile.getName()); } } + } + + final long cutoff = passStart - NOTIFICATION_IMAGE_SWEEP_GRACE_MILLIS; + for (final File file : NotificationImageProvider.listImageFiles()) { + if (file.lastModified() >= cutoff || live.contains(file.getName())) { + continue; + } + if (!file.delete()) { + LogUtil.w(TAG, "Could not delete the orphaned notification image " + + file.getAbsolutePath()); + } + } + } + + @VisibleForTesting + static void createMessageNotification(final String conversationId) { + if (!TextUtils.isEmpty(conversationId) && isConversationBlocked(conversationId)) { + LogUtil.d(TAG, "Skipping notification: conversation blocked, id=" + conversationId); + return; + } + + final MessageNotificationState state = MessageNotificationState.getNotificationState(); + if (state == null) { + updateOverflowNotification(0); + return; + } + + // Send per-conversation notifications (if there are multiple conversations). The list + // holds every conversation with unseen messages, so it has to be iterated: picking one + // entry notifies whichever conversation holds the newest unseen message and silently + // drops the rest, and there is no summary notification to surface them. + final List notifiable = state.mConversationsList.mConversations.stream() + .filter(conv -> !isConversationBlocked(conv.mConversationId)) + .filter(conv -> !ConversationSnoozeQuery.isConversationSnoozed( + conv.mConversationId)) + .toList(); + + notifiable.stream() + .limit(MAX_CONVERSATION_NOTIFICATIONS) + .forEach(conv -> processAndSend(state, conv)); + + updateOverflowNotification(notifiable.size() - MAX_CONVERSATION_NOTIFICATIONS); + } + + private static void updateOverflowNotification(final int overflowCount) { + final Context context = Factory.get().getApplicationContext(); + final NotificationManagerCompat notificationManager = + NotificationManagerCompat.from(context); + final String tag = buildNotificationTag(SMS_OVERFLOW_NOTIFICATION_TAG, null); + if (overflowCount <= 0) { + notificationManager.cancel(tag, PendingIntentConstants.SMS_NOTIFICATION_ID); return; } - // Send per-conversation notifications (if there are multiple conversations). - Optional conversation = - state.mConversationsList.mConversations.stream().findFirst(); - conversation.ifPresent(conv -> processAndSend(state, conv)); + final Notification notification = new NotificationCompat.Builder(context, + NotificationChannelUtil.INCOMING_MESSAGES) + .setContentTitle(context.getResources().getQuantityString( + R.plurals.notification_more_conversations, overflowCount, overflowCount)) + .setSmallIcon(R.drawable.ic_sms_light) + .setContentIntent( + UIIntents.get().getPendingIntentForConversationListActivity(context)) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setOnlyAlertOnce(true) + .setAutoCancel(true) + .build(); + + postNotification(notificationManager, tag, PendingIntentConstants.SMS_NOTIFICATION_ID, + notification); } /** @@ -207,7 +297,6 @@ private static void createMessageNotification(final String conversationId) { * * @param type Message or error notifications from Constants. */ - @SuppressWarnings("SameParameterValue") private static synchronized void cancel(final int type) { cancel(type, null); } @@ -241,20 +330,18 @@ public static synchronized void cancel(final int type, final String conversation } } - private static boolean isConversationSnoozed(final String conversationId) { - final Context context = Factory.get().getApplicationContext(); - final ConversationNotificationRepository repository = EntryPointAccessors - .fromApplication(context, ConversationNotificationRepository.Provider.class) - .conversationNotificationRepository(); - return repository.isSnoozed(conversationId); - } - - private static Uri getNotificationRingtoneUriForConversationId(final String conversationId) { + public static boolean isConversationBlocked(final String conversationId) { final DatabaseWrapper db = DataModel.get().getDatabase(); final ConversationListItemData convData = ConversationListItemData.getExistingConversation(db, conversationId); - return RingtoneUtil.getNotificationRingtoneUri(conversationId, - convData != null ? convData.getNotificationSoundUri() : null); + + if (convData == null) { + return false; + } + + final String otherDestination = convData.getOtherParticipantNormalizedDestination(); + return otherDestination != null + && BugleDatabaseOperations.isBlockedDestination(db, otherDestination); } /** @@ -264,7 +351,7 @@ private static Uri getNotificationRingtoneUriForConversationId(final String conv * @param conversationId The conversation id (optional) */ private static String buildNotificationTag(final String name, - final String conversationId) { + final String conversationId) { final Context context = Factory.get().getApplicationContext(); if (conversationId != null) { return context.getPackageName() + name + ":" + conversationId; @@ -282,8 +369,12 @@ private static String buildNotificationTag(final String name, static String buildNotificationTag(final int type, final String conversationId) { String tag = null; switch(type) { - case PendingIntentConstants.SMS_NOTIFICATION_ID -> tag = buildNotificationTag(SMS_NOTIFICATION_TAG, conversationId); - case PendingIntentConstants.MSG_SEND_ERROR -> tag = buildNotificationTag(SMS_ERROR_NOTIFICATION_TAG, null); + case PendingIntentConstants.SMS_NOTIFICATION_ID: + tag = buildNotificationTag(SMS_NOTIFICATION_TAG, conversationId); + break; + case PendingIntentConstants.MSG_SEND_ERROR: + tag = buildNotificationTag(SMS_ERROR_NOTIFICATION_TAG, null); + break; } return tag; } @@ -350,8 +441,8 @@ private static void addPeopleToNotification( } } - @SuppressLint("MissingPermission") - private static void processAndSend(final MessageNotificationState state, final Conversation conversation) { + @VisibleForTesting + static void processAndSend(final MessageNotificationState state, final Conversation conversation) { final Context context = Factory.get().getApplicationContext(); final String conversationId = conversation.mConversationId; final NotificationCompat.Builder notifBuilder = @@ -471,9 +562,11 @@ private static void processAndSend(final MessageNotificationState state, final C notifBuilder.addAction(replyActionBuilder.build()); final String messageId = conversation.getLatestMessageId(); - if (conversation.getDoesLatestMessageNeedDownload() && messageId != null) { + if (conversation.getDoesLatestMessageNeedDownload() && messageId != null + && !OsUtil.isSecondaryUser()) { final PendingIntent downloadPendingIntent = - RedownloadMmsAction.getPendingIntentForRedownloadMms(context, messageId); + RedownloadMmsAction.getPendingIntentForRedownloadMms(context, + messageId, REQUEST_CODE_REDOWNLOAD_MMS); final NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action.Builder(R.drawable.ic_file_download_light, @@ -501,33 +594,22 @@ private static void processAndSend(final MessageNotificationState state, final C Notification notification = notifBuilder.build(); notification.flags |= Notification.FLAG_AUTO_CANCEL; - notificationManager.notify(notificationTag, type, notification); - - LogUtil.i(TAG, "Notifying for conversation " + conversationId + "; " - + "tag = " + notificationTag + ", type = " + type); + if (postNotification(notificationManager, notificationTag, type, notification)) { + LogUtil.i(TAG, "Notifying for conversation " + conversationId + "; " + + "tag = " + notificationTag + ", type = " + type); + } } - /** - * Play the observable conversation notification sound (it's the regular notification sound, but - * played at half-volume) - */ - private static void playObservableConversationNotificationSound(final Uri ringtoneUri) { - final Context context = Factory.get().getApplicationContext(); - final AudioManager audioManager = (AudioManager) context - .getSystemService(Context.AUDIO_SERVICE); - final boolean silenced = - audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL; - if (silenced) { - return; + @VisibleForTesting + static boolean postNotification(final NotificationManagerCompat notificationManager, + final String tag, final int type, final Notification notification) { + try { + notificationManager.notify(tag, type, notification); + return true; + } catch (SecurityException e) { + LogUtil.e(TAG, "Dropping notification: cannot grant access to its attachment", e); + return false; } - - final NotificationPlayer player = new NotificationPlayer(LogUtil.BUGLE_TAG); - player.play(ringtoneUri, false, - AudioManager.STREAM_NOTIFICATION, - OBSERVABLE_CONVERSATION_NOTIFICATION_VOLUME); - - // Stop the sound after five seconds to handle continuous ringtones - ThreadUtil.getMainThreadHandler().postDelayed(player::stop, 5000); } /** @@ -545,14 +627,13 @@ public static void markMessagesAsRead(final String conversationId, final boolean MarkAsReadAction.markAsRead(conversationId, cancelNotification); } - @SuppressLint("MissingPermission") public static void notifyEmergencySmsFailed(final String emergencyNumber, - final String conversationId) { + final String conversationId) { final Context context = Factory.get().getApplicationContext(); final CharSequence line1 = MessageNotificationState.applyWarningTextColor(context, context.getString(R.string.notification_emergency_send_failure_line1, - emergencyNumber)); + emergencyNumber)); final String line2 = context.getString(R.string.notification_emergency_send_failure_line2, emergencyNumber); final PendingIntent destinationIntent = UIIntents.get() @@ -610,7 +691,86 @@ public static Bitmap getAvatarBitmap(Context context, Uri avatarUri) { return null; } - @SuppressLint("MissingPermission") + /** + * Transcodes the attachment at {@code imageUri} into a file named after {@code partId}, or + * returns the file an earlier call already wrote for that part. Call it from a notification + * pass only: the deterministic name makes the temporary file it writes through deterministic + * too, so two concurrent callers for one part would trample each other's write. + */ + static Uri getNotificationImageUri(final Context context, final Uri imageUri, + final String partId) { + if (imageUri == null) { + return null; + } + + final Uri notificationImageUri = NotificationImageProvider + .buildNotificationImageUri(partId); + + if (notificationImageUri == null) { + return null; + } + final File imageFile = NotificationImageProvider.getFileFromUri(notificationImageUri); + if (imageFile == null) { + return null; + } + + if (imageFile.length() > 0) { + imageFile.setLastModified(System.currentTimeMillis()); + return notificationImageUri; + } + + final ImageRequestDescriptor descriptor = new UriImageRequestDescriptor( + imageUri, + NOTIFICATION_IMAGE_MAX_SIZE, + NOTIFICATION_IMAGE_MAX_SIZE, + false, + true, + false, + 0, + 0 + ); + final MediaRequest imageRequest = descriptor.buildSyncMediaRequest(context); + final ImageResource image = MediaResourceManager.get().requestMediaResourceSync( + imageRequest); + if (image == null) { + LogUtil.w(TAG, "Could not decode the attachment for its notification"); + imageFile.delete(); + return null; + } + + final File tempFile = new File(imageFile.getPath() + ".tmp"); + boolean written = false; + try (OutputStream out = new FileOutputStream(tempFile)) { + written = orientUpright(image).compress(Bitmap.CompressFormat.JPEG, + NOTIFICATION_IMAGE_QUALITY, out); + } catch (final IOException | RuntimeException | OutOfMemoryError e) { + LogUtil.e(TAG, "Failed to write the notification image", e); + } finally { + image.release(); + } + + if (!written || !tempFile.renameTo(imageFile)) { + tempFile.delete(); + imageFile.delete(); + return null; + } + return notificationImageUri; + } + + private static Bitmap orientUpright(final ImageResource image) { + final Bitmap bitmap = image.getBitmap(); + final ExifInterface.OrientationParams params = + ExifInterface.getOrientationParams(image.getOrientation()); + if (params.rotation == 0 && params.scaleX == 1 && params.scaleY == 1) { + return bitmap; + } + final Matrix matrix = new Matrix(); + matrix.postRotate(params.rotation); + matrix.postScale(params.scaleX, params.scaleY); + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, + false); + } + public static void updateWithInlineReply(final String conversationId, final String message) { Context context = Factory.get().getApplicationContext(); Notification activeNotification = @@ -630,8 +790,8 @@ public static void updateWithInlineReply(final String conversationId, final Stri recoveredBuilder.setOnlyAlertOnce(true); String tag = buildNotificationTag(PendingIntentConstants.SMS_NOTIFICATION_ID, conversationId); - NotificationManagerCompat.from(context) - .notify(tag, PendingIntentConstants.SMS_NOTIFICATION_ID, recoveredBuilder.build()); + postNotification(NotificationManagerCompat.from(context), tag, + PendingIntentConstants.SMS_NOTIFICATION_ID, recoveredBuilder.build()); } } } @@ -650,7 +810,11 @@ private static void setUpShortcuts(Context context, Conversation conversation, IconCompat icon; IconCompat personIcon = person.getIcon(); - icon = java.util.Objects.requireNonNullElseGet(personIcon, () -> IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground)); + if (personIcon != null) { + icon = personIcon; + } else { + icon = IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground); + } shortcutBuilder.setIcon(icon); ShortcutManagerCompat.pushDynamicShortcut(context, shortcutBuilder.build()); From dd0ad8eab5605707c4e4f0039295701ce6a19feb Mon Sep 17 00:00:00 2001 From: navneetsingh-dev Date: Sun, 20 Sep 2026 19:37:50 +0530 Subject: [PATCH 5/6] Remove orphaned photoviewer submodule gitlink --- lib/platform_frameworks_opt_photoviewer | 1 - 1 file changed, 1 deletion(-) delete mode 160000 lib/platform_frameworks_opt_photoviewer diff --git a/lib/platform_frameworks_opt_photoviewer b/lib/platform_frameworks_opt_photoviewer deleted file mode 160000 index d48e36941..000000000 --- a/lib/platform_frameworks_opt_photoviewer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d48e36941419ebfd4f6faf77bf3331d810fc06bd From 13ad8e26dad6e8558e5696da77e9353d4709305d Mon Sep 17 00:00:00 2001 From: navneetsingh-dev Date: Wed, 23 Sep 2026 11:42:09 +0530 Subject: [PATCH 6/6] Resolve verification-metadata conflict --- gradle/verification-metadata.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index b0169c48e..03a86e0ba 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -10406,7 +10406,6 @@ -