Merge branch 'main' into adb/sticker-picker

This commit is contained in:
adb
2026-03-09 18:18:24 +01:00
committed by GitHub
19 changed files with 273 additions and 450 deletions
@@ -15,6 +15,7 @@ public class DcContext {
public final static int DC_EVENT_MSG_DELIVERED = 2010;
public final static int DC_EVENT_MSG_FAILED = 2012;
public final static int DC_EVENT_MSG_READ = 2015;
public final static int DC_EVENT_MSG_DELETED = 2016;
public final static int DC_EVENT_CHAT_MODIFIED = 2020;
public final static int DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021;
public final static int DC_EVENT_CHAT_DELETED = 2023;
@@ -803,8 +803,6 @@ public class ConversationItem extends BaseConversationItem
return stickerStub.get().getFooter();
} else if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getText())) {
return mediaThumbnailStub.get().getFooter();
} else if (messageRecord.getType() == DcMsg.DC_MSG_CALL) {
return callViewStub.get().getFooter();
} else {
return footer;
}
@@ -12,6 +12,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.DateUtils;
import chat.delta.rpc.types.CallInfo;
import chat.delta.rpc.types.CallState;
@@ -21,7 +22,7 @@ public class CallItemView extends FrameLayout {
private final @NonNull ImageView icon;
private final @NonNull TextView title;
private final @NonNull ConversationItemFooter footer;
private final @NonNull TextView duration;
private CallInfo callInfo;
private CallClickListener viewListener;
@@ -40,7 +41,7 @@ public class CallItemView extends FrameLayout {
this.icon = findViewById(R.id.call_icon);
this.title = findViewById(R.id.title);
this.footer = findViewById(R.id.footer);
this.duration = findViewById(R.id.duration);
setOnClickListener(v -> {
if (viewListener != null && callInfo != null) {
@@ -55,10 +56,12 @@ public class CallItemView extends FrameLayout {
public void setCallItem(boolean isOutgoing, CallInfo callInfo) {
this.callInfo = callInfo;
if (callInfo.state instanceof CallState.Completed) {
footer.setCallDuration(((CallState.Completed) callInfo.state).duration);
duration.setText(DateUtils.getFormattedCallDuration(getContext(), ((CallState.Completed) callInfo.state).duration));
duration.setVisibility(VISIBLE);
} else {
footer.setCallDuration(0); // reset
duration.setVisibility(GONE);
}
if (callInfo.state instanceof CallState.Missed) {
@@ -79,26 +82,19 @@ public class CallItemView extends FrameLayout {
if (isOutgoing) {
attrs = new int[]{
R.attr.conversation_item_outgoing_text_primary_color,
R.attr.conversation_item_outgoing_text_secondary_color,
};
} else {
attrs = new int[]{
R.attr.conversation_item_incoming_text_primary_color,
R.attr.conversation_item_incoming_text_secondary_color,
};
}
try (TypedArray ta = getContext().obtainStyledAttributes(attrs)) {
icon.setColorFilter(ta.getColor(0, Color.BLACK));
footer.setTextColor(ta.getColor(1, Color.BLACK));
}
}
public ConversationItemFooter getFooter() {
return footer;
}
public String getDescription() {
return title.getText() + "\n" + footer.getDescription();
return title.getText() + (duration.getVisibility()==VISIBLE ? ("\n" + duration.getText()) : "");
}
public interface CallClickListener {
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
@@ -32,7 +31,6 @@ public class ConversationItemFooter extends LinearLayout {
private ImageView locationIndicatorView;
private DeliveryStatusView deliveryStatusView;
private Integer textColor = null;
private int callDuration = 0;
private Context context;
private Rpc rpc;
@@ -72,11 +70,6 @@ public class ConversationItemFooter extends LinearLayout {
}
}
/* Call duration in seconds. Only >0 if this is a call message */
public void setCallDuration(int duration) {
callDuration = duration;
}
public void setMessageRecord(@NonNull DcMsg messageRecord) {
presentDate(messageRecord);
boolean bookmark = messageRecord.getOriginalMsgId() != 0 || messageRecord.getSavedMsgId() != 0;
@@ -112,7 +105,7 @@ public class ConversationItemFooter extends LinearLayout {
presentDeliveryStatus(messageRecord, isOutChannel);
}
public void setTextColor(int color) {
private void setTextColor(int color) {
textColor = color;
dateView.setTextColor(color);
editedView.setTextColor(color);
@@ -126,27 +119,17 @@ public class ConversationItemFooter extends LinearLayout {
private void presentDate(@NonNull DcMsg dcMsg) {
dateView.forceLayout();
Context context = getContext();
String date = dcMsg.getType() == DcMsg.DC_MSG_CALL?
DateUtils.getExtendedTimeSpanString(context, dcMsg.getTimestamp())
: DateUtils.getExtendedRelativeTimeSpanString(context, dcMsg.getTimestamp());
if (callDuration > 0) {
String duration = DateUtils.getFormattedCallDuration(context, callDuration);
dateView.setText(context.getString(R.string.call_date_and_duration, date, duration));
} else {
dateView.setText(date);
}
dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), dcMsg.getTimestamp()));
}
private void presentDeliveryStatus(@NonNull DcMsg messageRecord, boolean isOutChannel) {
// isDownloading is temporary and should be checked first.
boolean isDownloading = messageRecord.getDownloadState() == DcMsg.DC_DOWNLOAD_IN_PROGRESS;
boolean isCall = messageRecord.getType() == DcMsg.DC_MSG_CALL;
if (isDownloading) deliveryStatusView.setDownloading();
else if (messageRecord.isPending()) deliveryStatusView.setPending();
else if (messageRecord.isFailed()) deliveryStatusView.setFailed();
else if (!messageRecord.isOutgoing() || isCall || isOutChannel) deliveryStatusView.setNone();
else if (!messageRecord.isOutgoing() || isOutChannel) deliveryStatusView.setNone();
else if (messageRecord.isRemoteRead()) deliveryStatusView.setRead();
else if (messageRecord.isDelivered()) deliveryStatusView.setSent();
else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing();
@@ -1,209 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.app.Dialog;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.CustomPreferenceValidator;
import org.thoughtcrime.securesms.util.Prefs;
public class CustomDefaultPreference extends DialogPreference {
private static final String TAG = CustomDefaultPreference.class.getSimpleName();
private final int inputType;
private final String customPreference;
private final String customToggle;
private final CustomPreferenceValidator validator;
private String defaultValue;
public CustomDefaultPreference(Context context, AttributeSet attrs) {
super(context, attrs);
int[] attributeNames = new int[]{android.R.attr.inputType, R.attr.custom_pref_toggle};
try (TypedArray attributes = context.obtainStyledAttributes(attrs, attributeNames)) {
this.inputType = attributes.getInt(0, 0);
this.customPreference = getKey();
this.customToggle = attributes.getString(1);
this.validator = new CustomDefaultPreferenceDialogFragmentCompat.NullValidator();
}
setPersistent(false);
setDialogLayoutResource(R.layout.custom_default_preference_dialog);
}
public CustomDefaultPreference setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
this.setSummary(getSummary());
return this;
}
@Override
public String getSummary() {
if (isCustom()) {
return getContext().getString(R.string.pref_using_custom,
getPrettyPrintValue(getCustomValue()));
} else {
return getContext().getString(R.string.pref_using_default,
getPrettyPrintValue(getDefaultValue()));
}
}
private String getPrettyPrintValue(String value) {
if (TextUtils.isEmpty(value)) return getContext().getString(R.string.none);
else return value;
}
private boolean isCustom() {
return Prefs.getBooleanPreference(getContext(), customToggle, false);
}
private void setCustom(boolean custom) {
Prefs.setBooleanPreference(getContext(), customToggle, custom);
}
private String getCustomValue() {
return Prefs.getStringPreference(getContext(), customPreference, "");
}
private void setCustomValue(String value) {
Prefs.setStringPreference(getContext(), customPreference, value);
}
private String getDefaultValue() {
return defaultValue;
}
public static class CustomDefaultPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat {
private static final String INPUT_TYPE = "input_type";
private Spinner spinner;
private EditText customText;
private TextView defaultLabel;
public static CustomDefaultPreferenceDialogFragmentCompat newInstance(String key) {
CustomDefaultPreferenceDialogFragmentCompat fragment = new CustomDefaultPreferenceDialogFragmentCompat();
Bundle b = new Bundle(1);
b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key);
fragment.setArguments(b);
return fragment;
}
@Override
protected void onBindDialogView(@NonNull View view) {
Log.w(TAG, "onBindDialogView");
super.onBindDialogView(view);
CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
this.spinner = (Spinner) view.findViewById(R.id.default_or_custom);
this.defaultLabel = (TextView) view.findViewById(R.id.default_label);
this.customText = (EditText) view.findViewById(R.id.custom_edit);
this.customText.setInputType(preference.inputType);
this.customText.addTextChangedListener(new TextValidator());
this.customText.setText(preference.getCustomValue());
this.spinner.setOnItemSelectedListener(new SelectionLister());
this.defaultLabel.setText(preference.getPrettyPrintValue(preference.defaultValue));
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle instanceState) {
Dialog dialog = super.onCreateDialog(instanceState);
CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
if (preference.isCustom()) spinner.setSelection(1, true);
else spinner.setSelection(0, true);
return dialog;
}
@Override
public void onDialogClosed(boolean positiveResult) {
CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
if (positiveResult) {
if (spinner != null) preference.setCustom(spinner.getSelectedItemPosition() == 1);
if (customText != null) preference.setCustomValue(customText.getText().toString());
preference.setSummary(preference.getSummary());
}
}
interface CustomPreferenceValidator {
public boolean isValid(String value);
}
private static class NullValidator implements CustomPreferenceValidator {
@Override
public boolean isValid(String value) {
return true;
}
}
private class TextValidator implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
if (spinner.getSelectedItemPosition() == 1) {
Button positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
positiveButton.setEnabled(preference.validator.isValid(s.toString()));
}
}
}
private class SelectionLister implements AdapterView.OnItemSelectedListener {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
CustomDefaultPreference preference = (CustomDefaultPreference)getPreference();
Button positiveButton = ((AlertDialog)getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
defaultLabel.setVisibility(position == 0 ? View.VISIBLE : View.GONE);
customText.setVisibility(position == 0 ? View.GONE : View.VISIBLE);
positiveButton.setEnabled(position == 0 || preference.validator.isValid(customText.getText().toString()));
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
defaultLabel.setVisibility(View.VISIBLE);
customText.setVisibility(View.GONE);
}
}
}
}
@@ -213,6 +213,10 @@ public class DcEventCenter {
DcHelper.getNotificationCenter(context).removeNotifications(accountId, event.getData1Int());
break;
case DcContext.DC_EVENT_MSG_DELETED:
DcHelper.getNotificationCenter(context).removeNotification(accountId, event.getData1Int(), event.getData2Int());
break;
case DcContext.DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE:
FetchForegroundService.stop(context);
break;
@@ -146,7 +146,6 @@ public class DcHelper {
dcContext.setStockTranslation(100, context.getString(R.string.download_max_available_until));
dcContext.setStockTranslation(103, context.getString(R.string.incoming_messages));
dcContext.setStockTranslation(104, context.getString(R.string.outgoing_messages));
dcContext.setStockTranslation(105, context.getString(R.string.storage_on_domain));
dcContext.setStockTranslation(107, context.getString(R.string.connectivity_connected));
dcContext.setStockTranslation(108, context.getString(R.string.connectivity_connecting));
dcContext.setStockTranslation(109, context.getString(R.string.connectivity_updating));
@@ -39,6 +39,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.calls.CallActivity;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
@@ -49,16 +50,16 @@ import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.Pair;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.calls.CallActivity;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import chat.delta.rpc.RpcException;
@@ -72,7 +73,7 @@ public class NotificationCenter {
private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2);
// Map<accountId, Map<chatId, lines>, contains the last lines of each chat for each account
private final HashMap<Integer, HashMap<Integer, ArrayList<String>>> inboxes = new HashMap<>();
private final HashMap<Integer, HashMap<Integer, LinkedHashMap<Integer, String>>> inboxes = new HashMap<>();
public NotificationCenter(Context context) {
this.context = ApplicationContext.getInstance(context);
@@ -563,47 +564,91 @@ public class NotificationCenter {
@WorkerThread
private void maybeAddNotification(int accountId, DcChat dcChat, int msgId, String shortLine, String tickerLine, boolean playInChatSound, boolean isMention) {
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accountId);
int chatId = dcChat.getId();
ChatData chatData = new ChatData(accountId, chatId);
isMention = isMention && dcContext.isMentionsEnabled();
DcContext dcContext = context.getDcAccounts().getAccount(accountId);
int chatId = dcChat.getId();
ChatData chatData = new ChatData(accountId, chatId);
isMention = isMention && dcContext.isMentionsEnabled();
if (dcContext.isMuted() || (!isMention && dcChat.isMuted())) {
return;
}
if (dcContext.isMuted() || (!isMention && dcChat.isMuted())) {
return;
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationManager.areNotificationsEnabled()) {
return;
}
if (Util.equals(visibleChat, chatData)) {
if (playInChatSound && Prefs.isInChatNotifications(context)) {
InChatSounds.getInstance(context).playIncomingSound();
}
return;
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationManager.areNotificationsEnabled()) {
return;
}
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
long now = System.currentTimeMillis();
boolean signal = (now - lastAudibleNotification) > MIN_AUDIBLE_PERIOD_MILLIS;
if (signal) {
lastAudibleNotification = now;
}
if (Util.equals(visibleChat, chatData)) {
if (playInChatSound && Prefs.isInChatNotifications(context)) {
InChatSounds.getInstance(context).playIncomingSound();
// create a basic notification
// even without a name or message displayed,
// it makes sense to use separate notification channels and to open the respective chat directly -
// the user may eg. have chosen a different sound
String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat);
LinkedHashMap<Integer, String> messagesForInbox = null;
if (privacy.isDisplayContact() && privacy.isDisplayMessage()) {
synchronized (inboxes) {
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
if (accountInbox == null) {
accountInbox = new HashMap<>();
inboxes.put(accountId, accountInbox);
}
return;
LinkedHashMap<Integer, String> messages = accountInbox.get(chatId);
if (messages == null) {
messages = new LinkedHashMap<>();
accountInbox.put(chatId, messages);
}
messages.put(msgId, shortLine);
messagesForInbox = new LinkedHashMap<>(messages);
}
}
int cnt = dcContext.getFreshMsgCount(chatId);
buildAndShowChatNotification(accountId, chatId, msgId, dcContext, dcChat,
notificationChannel, shortLine, tickerLine, signal, messagesForInbox, cnt, true);
}
@WorkerThread
private void buildAndShowChatNotification(
int accountId,
int chatId,
int msgId,
DcContext dcContext,
DcChat dcChat,
String notificationChannel,
String contentText,
String ticker,
boolean signal,
LinkedHashMap<Integer, String> messagesForInbox,
int messageCount,
boolean includeSummary) {
try {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
long now = System.currentTimeMillis();
boolean signal = (now - lastAudibleNotification) > MIN_AUDIBLE_PERIOD_MILLIS;
if (signal) {
lastAudibleNotification = now;
}
ChatData chatData = new ChatData(accountId, chatId);
// create a basic notification
// even without a name or message displayed,
// it makes sense to use separate notification channels and to open the respective chat directly -
// the user may eg. have chosen a different sound
String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat);
// Create basic notification
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannel)
.setSmallIcon(R.drawable.icon_notification)
.setColor(context.getResources().getColor(R.color.delta_primary))
.setPriority(Prefs.getNotificationPriority(context))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setOnlyAlertOnce(!signal)
.setContentText(shortLine)
.setContentText(contentText)
.setDeleteIntent(getMarkAsReadIntent(chatData, msgId, false))
.setContentIntent(getOpenChatIntent(chatData));
@@ -612,7 +657,7 @@ public class NotificationCenter {
}
String accountTag = dcContext.getConfig(CONFIG_PRIVATE_TAG);
if (accountTag.isEmpty() && context.getDcAccounts().getAll().length > 1) {
if (accountTag.isEmpty() && ApplicationContext.getDcAccounts().getAll().length > 1) {
accountTag = dcContext.getName();
}
@@ -623,9 +668,11 @@ public class NotificationCenter {
}
}
builder.setTicker(tickerLine);
if (ticker != null) {
builder.setTicker(ticker);
}
// set sound, vibrate, led for systems that do not have notification channels
// Set sound, vibrate, led for systems that do not have notification channels
if (!notificationChannelsSupported()) {
if (signal) {
Uri sound = effectiveSound(chatData);
@@ -639,20 +686,20 @@ public class NotificationCenter {
}
String ledColor = Prefs.getNotificationLedColor(context);
if (!ledColor.equals("none")) {
builder.setLights(getLedArgb(ledColor),500, 2000);
builder.setLights(getLedArgb(ledColor), 500, 2000);
}
}
// set avatar
// Set avatar
if (privacy.isDisplayContact()) {
Bitmap bitmap = getAvatar(dcChat);
if (bitmap != null) {
builder.setLargeIcon(bitmap);
}
Bitmap bitmap = getAvatar(dcChat);
if (bitmap != null) {
builder.setLargeIcon(bitmap);
}
}
// add buttons that allow some actions without opening Delta Chat.
// if privacy options are enabled, the buttons are not added.
// Add buttons that allow some actions without opening Delta Chat.
// If privacy options are enabled, the buttons are not added.
if (privacy.isDisplayContact() && privacy.isDisplayMessage()) {
try {
PendingIntent inNotificationReplyIntent = getRemoteReplyIntent(chatData, msgId);
@@ -683,69 +730,90 @@ public class NotificationCenter {
} catch(Exception e) { Log.w(TAG, e); }
}
// create a tiny inbox (gets visible if the notification is expanded)
if (privacy.isDisplayContact() && privacy.isDisplayMessage()) {
// Create inbox style (gets visible if the notification is expanded)
if (privacy.isDisplayContact() && privacy.isDisplayMessage() && messagesForInbox != null) {
try {
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
synchronized (inboxes) {
HashMap<Integer, ArrayList<String>> accountInbox = inboxes.get(accountId);
if (accountInbox == null) {
accountInbox = new HashMap<>();
inboxes.put(accountId, accountInbox);
}
ArrayList<String> lines = accountInbox.get(chatId);
if (lines == null) {
lines = new ArrayList<>();
accountInbox.put(chatId, lines);
}
lines.add(shortLine);
for (int l = 0; l < lines.size(); l++) {
inboxStyle.addLine(lines.get(l));
}
for (String line : messagesForInbox.values()) {
inboxStyle.addLine(line);
}
builder.setStyle(inboxStyle);
} catch(Exception e) { Log.w(TAG, e); }
}
// messages count, some os make some use of that
// - do not use setSubText() as this is displayed together with setContentInfo() eg. on Lollipop
// - setNumber() may overwrite setContentInfo(), should be called last
// weird stuff.
int cnt = dcContext.getFreshMsgCount(chatId);
builder.setContentInfo(String.valueOf(cnt));
builder.setNumber(cnt);
// Messages count
builder.setContentInfo(String.valueOf(messageCount));
builder.setNumber(messageCount);
// add notification, we use one notification per chat,
// esp. older android are not that great at grouping
// Show notification
// try..catch potentially needed for very specific devices
try {
notificationManager.notify(String.valueOf(accountId), ID_MSG_OFFSET + chatId, builder.build());
notificationManager.notify(String.valueOf(accountId), ID_MSG_OFFSET + chatId, builder.build());
} catch (Exception e) {
Log.e(TAG, "cannot add notification", e);
Log.e(TAG, "cannot add notification", e);
}
// group notifications together in a summary, this is possible since SDK 24 (Android 7)
// https://developer.android.com/training/notify-user/group.html
// in theory, this won't be needed due to setGroup(), however, in practise, it is needed up to at least Android 10.
if (Build.VERSION.SDK_INT >= 24) {
// Group notifications in a summary (Android 7+)
if (includeSummary && Build.VERSION.SDK_INT >= 24) {
try {
NotificationCompat.Builder summary = new NotificationCompat.Builder(context, notificationChannel)
.setGroup(GRP_MSG + "." + accountId)
.setGroupSummary(true)
.setSmallIcon(R.drawable.icon_notification)
.setColor(context.getResources().getColor(R.color.delta_primary, null))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setContentTitle("Delta Chat") // content title would only be used on SDK <24
.setContentText("New messages") // content text would only be used on SDK <24
.setContentIntent(getOpenChatlistIntent(accountId));
if (privacy.isDisplayContact() && !TextUtils.isEmpty(accountTag)) {
summary.setSubText(accountTag);
}
notificationManager.notify(String.valueOf(accountId), ID_MSG_SUMMARY, summary.build());
NotificationCompat.Builder summary = new NotificationCompat.Builder(context, notificationChannel)
.setGroup(GRP_MSG + "." + accountId)
.setGroupSummary(true)
.setSmallIcon(R.drawable.icon_notification)
.setColor(context.getResources().getColor(R.color.delta_primary, null))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setContentTitle("Delta Chat")
.setContentText("New messages")
.setContentIntent(getOpenChatlistIntent(accountId));
if (privacy.isDisplayContact() && !TextUtils.isEmpty(accountTag)) {
summary.setSubText(accountTag);
}
notificationManager.notify(String.valueOf(accountId), ID_MSG_SUMMARY, summary.build());
} catch (Exception e) {
Log.e(TAG, "cannot add notification summary", e);
Log.e(TAG, "cannot add notification summary", e);
}
}
} catch (Exception e) {
Log.e(TAG, "cannot show notification", e);
}
}
@WorkerThread
private void rebuildNotification(int accountId, int chatId, LinkedHashMap<Integer, String> messages) {
try {
DcContext dcContext = ApplicationContext.getDcAccounts().getAccount(accountId);
DcChat dcChat = dcContext.getChat(chatId);
if (dcContext.isMuted() || dcChat.isMuted()) {
return;
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationManager.areNotificationsEnabled()) {
return;
}
// Get the latest message ID (last entry in LinkedHashMap)
Integer latestMsgId = null;
String lastLine = null;
for (Map.Entry<Integer, String> entry : messages.entrySet()) {
latestMsgId = entry.getKey();
lastLine = entry.getValue();
}
if (latestMsgId == null || lastLine == null) {
return;
}
ChatData chatData = new ChatData(accountId, chatId);
String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat);
int cnt = dcContext.getFreshMsgCount(chatId);
buildAndShowChatNotification(accountId, chatId, latestMsgId, dcContext, dcChat,
notificationChannel, lastLine, null, false, messages, cnt, false);
} catch (Exception e) {
Log.e(TAG, "cannot rebuild notification", e);
}
}
public Bitmap getAvatar(DcChat dcChat) {
@@ -782,10 +850,48 @@ public class NotificationCenter {
} catch (Exception e) { Log.w(TAG, e); }
}
public void removeNotification(int accountId, int chatId, int msgId) {
boolean shouldCancelNotification = false;
boolean removeSummary = false;
LinkedHashMap<Integer, String> remainingMessages = null;
synchronized (inboxes) {
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
if (accountInbox != null) {
LinkedHashMap<Integer, String> messages = accountInbox.get(chatId);
if (messages != null) {
messages.remove(msgId);
if (messages.isEmpty()) {
// No more messages for this chat
accountInbox.remove(chatId);
shouldCancelNotification = true;
removeSummary = accountInbox.isEmpty();
} else {
// Keep a copy of remaining messages for rebuilding
remainingMessages = new LinkedHashMap<>(messages);
}
}
}
}
if (shouldCancelNotification) {
// Cancel the notification entirely
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
String tag = String.valueOf(accountId);
notificationManager.cancel(tag, ID_MSG_OFFSET + chatId);
if (removeSummary) {
notificationManager.cancel(tag, ID_MSG_SUMMARY);
}
} else if (remainingMessages != null && !remainingMessages.isEmpty()) {
rebuildNotification(accountId, chatId, remainingMessages);
}
}
public void removeNotifications(int accountId, int chatId) {
boolean removeSummary;
synchronized (inboxes) {
HashMap<Integer, ArrayList<String>> accountInbox = inboxes.get(accountId);
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
if (accountInbox == null) {
accountInbox = new HashMap<>();
}
@@ -809,11 +915,11 @@ public class NotificationCenter {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
String tag = String.valueOf(accountId);
synchronized (inboxes) {
HashMap<Integer, ArrayList<String>> accountInbox = inboxes.get(accountId);
HashMap<Integer, LinkedHashMap<Integer, String>> accountInbox = inboxes.get(accountId);
notificationManager.cancel(tag, ID_MSG_SUMMARY);
if (accountInbox != null) {
for (Integer chatId : accountInbox.keySet()) {
notificationManager.cancel(tag, chatId);
notificationManager.cancel(tag, ID_MSG_OFFSET + chatId);
}
accountInbox.clear();
}
@@ -1,18 +1,11 @@
package org.thoughtcrime.securesms.preferences;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import org.thoughtcrime.securesms.components.CustomDefaultPreference;
public abstract class CorrectedPreferenceFragment extends PreferenceFragmentCompat {
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
@@ -25,22 +18,4 @@ public abstract class CorrectedPreferenceFragment extends PreferenceFragmentComp
View lv = getView().findViewById(android.R.id.list);
if (lv != null) lv.setPadding(0, 0, 0, 0);
}
@Override
public void onDisplayPreferenceDialog(@NonNull Preference preference) {
DialogFragment dialogFragment = null;
if (preference instanceof CustomDefaultPreference) {
dialogFragment = CustomDefaultPreference.CustomDefaultPreferenceDialogFragmentCompat.newInstance(preference.getKey());
}
if (dialogFragment != null) {
dialogFragment.setTargetFragment(this, 0);
dialogFragment.show(getFragmentManager(), "android.support.v7.preference.PreferenceFragment.DIALOG");
} else {
super.onDisplayPreferenceDialog(preference);
}
}
}
@@ -121,11 +121,11 @@ public class DateUtils extends android.text.format.DateUtils {
public static String getFormattedCallDuration(Context c, int seconds) {
if (seconds < 60) {
return c.getResources().getQuantityString(R.plurals.n_seconds_ext, seconds, seconds);
return c.getResources().getString(R.string.call_duration_less_than_a_minute);
}
int mins = seconds / 60;
return c.getResources().getQuantityString(R.plurals.n_minutes_ext, mins, mins);
return c.getResources().getQuantityString(R.plurals.call_duration_minutes, mins, mins);
}
public static String getFormattedTimespan(Context c, int timestamp) {