diff --git a/.github/workflows/cache-core.yml b/.github/workflows/cache-core.yml index 7c7b20df7..3418fe1d0 100644 --- a/.github/workflows/cache-core.yml +++ b/.github/workflows/cache-core.yml @@ -25,7 +25,7 @@ jobs: - uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 id: setup-ndk with: - ndk-version: r27 + ndk-version: "r29" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: diff --git a/.github/workflows/preview-apk.yml b/.github/workflows/preview-apk.yml index 7129fb5be..e5ba2342c 100644 --- a/.github/workflows/preview-apk.yml +++ b/.github/workflows/preview-apk.yml @@ -45,7 +45,7 @@ jobs: - uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 id: setup-ndk with: - ndk-version: r27 + ndk-version: "r29" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: diff --git a/CHANGELOG-upstream.md b/CHANGELOG-upstream.md index 9099d2b77..33c89333c 100644 --- a/CHANGELOG-upstream.md +++ b/CHANGELOG-upstream.md @@ -1,5 +1,9 @@ # Delta Chat Android Changelog +## Unreleased + +* Use message style notifications for longer message previews + ## v2.52.0 2026-06 @@ -7,7 +11,8 @@ * Fix: Incorrect total time when attaching audio files as draft * Fix: Audio files in draft showing total time from wrong file * Fix: Update the channel title after joining if the QR code had an outdated title -* Voice recording will be automatically saved as draft when interrupted +* Voice recording will be automatically saved as draft when interrupted +* Remove notification after audio playback ends * Update to core 2.52.0 ## v2.51.0 diff --git a/src/main/java/com/b44t/messenger/DcAccounts.java b/src/main/java/com/b44t/messenger/DcAccounts.java index 253df4c7f..132971940 100644 --- a/src/main/java/com/b44t/messenger/DcAccounts.java +++ b/src/main/java/com/b44t/messenger/DcAccounts.java @@ -84,7 +84,7 @@ public class DcAccounts { public boolean isAllChatmail() { for (int accountId : getAll()) { DcContext dcContext = getAccount(accountId); - if (!dcContext.isChatmail()) { + if (dcContext.getConfigInt("is_chatmail") == 0) { return false; } } diff --git a/src/main/java/com/b44t/messenger/DcContext.java b/src/main/java/com/b44t/messenger/DcContext.java index 897928efe..4b43bbea5 100644 --- a/src/main/java/com/b44t/messenger/DcContext.java +++ b/src/main/java/com/b44t/messenger/DcContext.java @@ -362,10 +362,6 @@ public class DcContext { return displayname; } - public boolean isChatmail() { - return getConfigInt("is_chatmail") == 1; - } - public boolean isMuted() { return getConfigInt("is_muted") == 1; } diff --git a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java index 5bcc46013..1309b33d2 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java +++ b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java @@ -264,7 +264,10 @@ public class AudioPlaybackViewModel extends ViewModel { updateCurrentState(false); } else if (player.getPlaybackState() == Player.STATE_ENDED && !player.hasNextMediaItem()) { - mediaController.setPlayWhenReady(false); + mediaController.stop(); + mediaController.clearMediaItems(); + stopUpdateProgress(); + playbackState.setValue(AudioPlaybackState.idle()); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java b/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java index 62d6c1392..afffc8a65 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java @@ -46,7 +46,7 @@ public class DcContactsLoader extends AsyncLoader { if (query == null && addScanQRLink) { additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_QR_INVITE); } - if (addCreateContactLink && !dcContext.isChatmail()) { + if (addCreateContactLink && dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) { additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT); } @@ -54,7 +54,7 @@ public class DcContactsLoader extends AsyncLoader { additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_GROUP); additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_BROADCAST); - if (!dcContext.isChatmail()) { + if (dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0) { additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP); } diff --git a/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java b/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java index 6aaf1a551..b0b3811ac 100644 --- a/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java +++ b/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java @@ -11,6 +11,7 @@ import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.AudioAttributes; import android.media.RingtoneManager; @@ -23,13 +24,18 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; +import androidx.core.app.Person; import androidx.core.app.RemoteInput; import androidx.core.app.TaskStackBuilder; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; +import androidx.core.graphics.drawable.IconCompat; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import java.io.ByteArrayInputStream; import java.math.BigInteger; import java.security.MessageDigest; import java.util.HashMap; @@ -42,6 +48,7 @@ import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ConversationActivity; import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.ShareActivity; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; @@ -61,8 +68,18 @@ public class NotificationCenter { private volatile long lastAudibleNotification = 0; private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2); - // Map, contains the last lines of each chat for each account - private final HashMap>> inboxes = + private static class NotifData { + final Person sender; + final String text; + + NotifData(Person sender, String text) { + this.sender = sender; + this.text = text; + } + } + + // notification history of each chat for each account + private final HashMap>> inboxes = new HashMap<>(); public NotificationCenter(Context context) { @@ -410,31 +427,36 @@ public class NotificationCenter { DcMsg dcMsg = dcContext.getMsg(msgId); NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context); - String shortLine = + DcContact sender = dcContext.getContact(dcMsg.getFromId()); + String senderName = dcMsg.getSenderName(sender); + String text = privacy.isDisplayMessage() ? dcMsg.getSummarytext(2000) : context.getString(R.string.notify_new_message); + String shortLine = text; if (dcChat.isMultiUser() && privacy.isDisplayContact()) { - shortLine = - dcMsg.getSenderName(dcContext.getContact(dcMsg.getFromId())) + ": " + shortLine; + shortLine = senderName + ": " + text; } - String tickerLine = shortLine; - if (!dcChat.isMultiUser() && privacy.isDisplayContact()) { - tickerLine = - dcMsg.getSenderName(dcContext.getContact(dcMsg.getFromId())) + ": " + tickerLine; - if (dcMsg.getOverrideSenderName() != null) { - // There is an "overridden" display name on the message, so, we need to prepend the - // display name to the message, - // i.e. set the shortLine to be the same as the tickerLine. - shortLine = tickerLine; - } + NotifData notifData = + new NotifData( + new Person.Builder() + .setName(senderName) + .setIcon(getAvatarIcon(sender)) + .setBot(sender.isBot()) + .setKey(accountId + "-" + sender.getId()) + .build(), + text); + + String tickerLine = text; + if (privacy.isDisplayContact()) { + tickerLine = senderName + ": " + text; } DcMsg quotedMsg = dcMsg.getQuotedMsg(); boolean isMention = dcChat.isMultiUser() && quotedMsg != null && quotedMsg.isOutgoing(); - maybeAddNotification(accountId, dcChat, msgId, shortLine, tickerLine, true, isMention); + maybeAddNotification(accountId, dcChat, msgId, notifData, tickerLine, true, isMention); }); } @@ -451,15 +473,27 @@ public class NotificationCenter { } DcContact sender = dcContext.getContact(contactId); - String shortLine = + String senderName = dcMsg.getSenderName(sender); + String text = context.getString( R.string.reaction_by_other, sender.getDisplayName(), reaction, dcMsg.getSummarytext(2000)); DcChat dcChat = dcContext.getChat(dcMsg.getChatId()); + + NotifData notifData = + new NotifData( + new Person.Builder() + .setName(senderName) + .setIcon(getAvatarIcon(sender)) + .setBot(sender.isBot()) + .setKey(accountId + "-" + sender.getId()) + .build(), + text); + maybeAddNotification( - accountId, dcChat, msgId, shortLine, shortLine, false, dcChat.isMultiUser()); + accountId, dcChat, msgId, notifData, text, false, dcChat.isMultiUser()); }); } @@ -473,6 +507,7 @@ public class NotificationCenter { DcContext dcContext = context.getDcAccounts().getAccount(accountId); DcMsg dcMsg = dcContext.getMsg(msgId); + DcChat dcChat = dcContext.getChat(dcMsg.getChatId()); DcMsg parentMsg; if (dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) { parentMsg = dcMsg; @@ -486,10 +521,34 @@ public class NotificationCenter { JSONObject info = parentMsg.getWebxdcInfo(); final String name = JsonUtils.optString(info, "name"); - String shortLine = name.isEmpty() ? text : (name + ": " + text); - DcChat dcChat = dcContext.getChat(dcMsg.getChatId()); + String tickerLine = name.isEmpty() ? text : (name + ": " + text); + + NotifData notifData; + if (dcChat.isMultiUser()) { + byte[] blob = parentMsg.getWebxdcBlob(JsonUtils.optString(info, "icon")); + notifData = + new NotifData( + new Person.Builder() + .setName(name) + .setIcon(getAvatarIcon(blob)) + .setKey(accountId + "-webxdc-" + msgId) + .build(), + text); + } else { + DcContact sender = dcContext.getContact(contactId); + notifData = + new NotifData( + new Person.Builder() + .setName(dcMsg.getSenderName(sender)) + .setIcon(getAvatarIcon(sender)) + .setBot(sender.isBot()) + .setKey(dcContext.getAccountId() + "-" + sender.getId()) + .build(), + tickerLine); + } + maybeAddNotification( - accountId, dcChat, msgId, shortLine, shortLine, false, dcChat.isMultiUser()); + accountId, dcChat, msgId, notifData, tickerLine, false, dcChat.isMultiUser()); }); } @@ -498,7 +557,7 @@ public class NotificationCenter { int accountId, DcChat dcChat, int msgId, - String shortLine, + NotifData notifData, String tickerLine, boolean playInChatSound, boolean isMention) { @@ -538,20 +597,23 @@ public class NotificationCenter { // the user may eg. have chosen a different sound String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat); - LinkedHashMap messagesForInbox = null; - if (privacy.isDisplayContact() && privacy.isDisplayMessage()) { + LinkedHashMap messagesForInbox = null; + if (privacy.isDisplayContact()) { synchronized (inboxes) { - HashMap> accountInbox = inboxes.get(accountId); + HashMap> accountInbox = inboxes.get(accountId); if (accountInbox == null) { accountInbox = new HashMap<>(); inboxes.put(accountId, accountInbox); } - LinkedHashMap messages = accountInbox.get(chatId); + LinkedHashMap messages = accountInbox.get(chatId); if (messages == null) { messages = new LinkedHashMap<>(); accountInbox.put(chatId, messages); } - messages.put(msgId, shortLine); + if (!privacy.isDisplayMessage()) { + messages.clear(); + } + messages.put(msgId, notifData); messagesForInbox = new LinkedHashMap<>(messages); } } @@ -564,7 +626,7 @@ public class NotificationCenter { dcContext, dcChat, notificationChannel, - shortLine, + notifData.text, tickerLine, signal, messagesForInbox, @@ -583,7 +645,7 @@ public class NotificationCenter { String contentText, String ticker, boolean signal, - LinkedHashMap messagesForInbox, + LinkedHashMap messagesForInbox, int messageCount, boolean includeSummary) { try { @@ -694,14 +756,42 @@ public class NotificationCenter { } } - // Create inbox style (gets visible if the notification is expanded) - if (privacy.isDisplayContact() && privacy.isDisplayMessage() && messagesForInbox != null) { + // Create messaging style + if (privacy.isDisplayContact() && messagesForInbox != null) { try { - NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - for (String line : messagesForInbox.values()) { - inboxStyle.addLine(line); + Intent viewChatIntent = new Intent(context, ShareActivity.class); + viewChatIntent.setAction(Intent.ACTION_SEND); + viewChatIntent.putExtra(ShareActivity.EXTRA_ACC_ID, dcContext.getAccountId()); + viewChatIntent.putExtra(ShareActivity.EXTRA_CHAT_ID, dcChat.getId()); + + ShortcutInfoCompat shortcut = + new ShortcutInfoCompat.Builder( + context, "chat-" + dcContext.getAccountId() + "-" + dcChat.getId()) + .setShortLabel(dcChat.getName()) + .setLongLived(true) + .setIntent(viewChatIntent) + .build(); + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut); + builder.setShortcutInfo(shortcut); + + DcContact selfContact = dcContext.getContact(DcContact.DC_CONTACT_ID_SELF); + Person self = + new Person.Builder() + .setName(selfContact.getDisplayName()) + .setIcon(getAvatarIcon(selfContact)) + .setKey(accountId + "-" + selfContact.getId()) + .build(); + NotificationCompat.MessagingStyle style = new NotificationCompat.MessagingStyle(self); + if (dcChat.isMultiUser()) { + style.setGroupConversation(true); + style.setConversationTitle(dcChat.getName()); } - builder.setStyle(inboxStyle); + for (Map.Entry msgEntry : messagesForInbox.entrySet()) { + long timestamp_ms = dcContext.getMsg(msgEntry.getKey()).getSortTimestamp() * 1000; + NotifData notifData = msgEntry.getValue(); + style.addMessage(notifData.text, timestamp_ms, notifData.sender); + } + builder.setStyle(style); } catch (Exception e) { Log.w(TAG, e); } @@ -748,7 +838,7 @@ public class NotificationCenter { @WorkerThread private void rebuildNotification( - int accountId, int chatId, LinkedHashMap messages) { + int accountId, int chatId, LinkedHashMap messages) { try { DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accountId); DcChat dcChat = dcContext.getChat(chatId); @@ -766,9 +856,9 @@ public class NotificationCenter { // Get the latest message ID (last entry in LinkedHashMap) Integer latestMsgId = null; String lastLine = null; - for (Map.Entry entry : messages.entrySet()) { + for (Map.Entry entry : messages.entrySet()) { latestMsgId = entry.getKey(); - lastLine = entry.getValue(); + lastLine = entry.getValue().text; } if (latestMsgId == null || lastLine == null) { return; @@ -797,8 +887,35 @@ public class NotificationCenter { } } - public Bitmap getAvatar(DcChat dcChat) { - Recipient recipient = new Recipient(context, dcChat); + private static @Nullable IconCompat getAvatarIcon(byte[] blob) { + if (blob == null) { + return null; + } + ByteArrayInputStream is = new ByteArrayInputStream(blob); + BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon"); + Bitmap bitmap = drawable.getBitmap(); + return IconCompat.createWithBitmap(bitmap); + } + + private @Nullable IconCompat getAvatarIcon(DcChat dcChat) { + Bitmap avatar = getAvatar(dcChat); + return avatar != null ? IconCompat.createWithBitmap(avatar) : null; + } + + private @Nullable IconCompat getAvatarIcon(DcContact dcContact) { + Bitmap avatar = getAvatar(dcContact); + return avatar != null ? IconCompat.createWithBitmap(avatar) : null; + } + + private @Nullable Bitmap getAvatar(DcChat dcChat) { + return getAvatar(new Recipient(context, dcChat)); + } + + private @Nullable Bitmap getAvatar(DcContact dcContact) { + return getAvatar(new Recipient(context, dcContact)); + } + + private @Nullable Bitmap getAvatar(Recipient recipient) { try { Drawable drawable; ContactPhoto contactPhoto = recipient.getContactPhoto(context); @@ -837,12 +954,12 @@ public class NotificationCenter { public void removeNotification(int accountId, int chatId, int msgId) { boolean shouldCancelNotification = false; boolean removeSummary = false; - LinkedHashMap remainingMessages = null; + LinkedHashMap remainingMessages = null; synchronized (inboxes) { - HashMap> accountInbox = inboxes.get(accountId); + HashMap> accountInbox = inboxes.get(accountId); if (accountInbox != null) { - LinkedHashMap messages = accountInbox.get(chatId); + LinkedHashMap messages = accountInbox.get(chatId); if (messages != null) { messages.remove(msgId); @@ -875,7 +992,7 @@ public class NotificationCenter { public void removeNotifications(int accountId, int chatId) { boolean removeSummary; synchronized (inboxes) { - HashMap> accountInbox = inboxes.get(accountId); + HashMap> accountInbox = inboxes.get(accountId); if (accountInbox == null) { accountInbox = new HashMap<>(); } @@ -901,7 +1018,7 @@ public class NotificationCenter { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); String tag = String.valueOf(accountId); synchronized (inboxes) { - HashMap> accountInbox = inboxes.get(accountId); + HashMap> accountInbox = inboxes.get(accountId); notificationManager.cancel(tag, ID_MSG_SUMMARY); if (accountInbox != null) { for (Integer chatId : accountInbox.keySet()) { diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java b/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java index 350c3bfe8..1e8c7e42b 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java @@ -18,6 +18,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; +import com.b44t.messenger.DcContext; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import com.google.zxing.BinaryBitmap; @@ -120,10 +121,12 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis @Override public boolean onPrepareOptionsMenu(Menu menu) { + DcContext dcContext = DcHelper.getContext(this); + menu.clear(); getMenuInflater().inflate(R.menu.qr_show, menu); menu.findItem(R.id.new_classic_contact) - .setVisible(!scanRelay && !DcHelper.getContext(this).isChatmail()); + .setVisible(!scanRelay && dcContext.getConfigInt(DcHelper.CONFIG_FORCE_ENCRYPTION) == 0); Util.redMenuItem(menu, R.id.withdraw); if (tabLayout.getSelectedTabPosition() == TAB_SCAN) {