mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Merge branch 'main' into adb/sticker-picker
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user