From 8fe5ac086fab679ef342716b44ed64f5d0cb69ed Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 8 May 2024 17:58:40 -0400 Subject: [PATCH 1/4] Scheduled messages --- .../res/drawable/schedule_message.xml | 11 +++++ .../conversations/entities/Conversation.java | 2 +- .../services/XmppConnectionService.java | 43 +++++++++++++++++-- .../ui/ConversationFragment.java | 28 ++++++++++++ src/main/res/menu/fragment_conversation.xml | 4 ++ src/main/res/values/strings.xml | 1 + 6 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 src/cheogram/res/drawable/schedule_message.xml diff --git a/src/cheogram/res/drawable/schedule_message.xml b/src/cheogram/res/drawable/schedule_message.xml new file mode 100644 index 0000000000000000000000000000000000000000..fa9b1939e3fc0893d1a3a776430fbb1f1aacdfc1 --- /dev/null +++ b/src/cheogram/res/drawable/schedule_message.xml @@ -0,0 +1,11 @@ + + + diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index e296b6daf8903c31c52c9c8ece3aa29fadf1bb0e..a148e645f552d9f5727f0adadd428e7d65e5568b 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -357,7 +357,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl final ArrayList results = new ArrayList<>(); synchronized (this.messages) { for (Message message : this.messages) { - if (message.getStatus() == Message.STATUS_WAITING) { + if (message.getStatus() == Message.STATUS_WAITING || message.getTimeSent() > System.currentTimeMillis()) { results.add(message); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 801a2e7442ca373fc4e9bc68192806eec819625b..df05175750f19acca6ab781dbf3f1af14db147f5 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1069,6 +1069,8 @@ public class XmppConnectionService extends Service { } final var extras = intent == null ? null : intent.getExtras(); try { + Log.d(Config.LOGTAG, "looking for and sending scheduled messages"); + sendScheduledMessages(); internalPingExecutor.execute(() -> manageAccountConnectionStates(action, extras)); } catch (final RejectedExecutionException e) { Log.e(Config.LOGTAG, "can not schedule connection states manager"); @@ -1154,6 +1156,26 @@ public class XmppConnectionService extends Service { WakeLockHelper.release(wakeLock); } + private void sendScheduledMessages() { + List conversations = getConversations(); + for (Conversation conversation : conversations) { + final Account account = conversation.getAccount(); + final boolean inProgressJoin; + synchronized (account.inProgressConferenceJoins) { + inProgressJoin = account.inProgressConferenceJoins.contains(conversation); + } + final boolean pendingJoin; + synchronized (account.pendingConferenceJoins) { + pendingJoin = account.pendingConferenceJoins.contains(conversation); + } + if (conversation.getAccount() == account + && !pendingJoin + && !inProgressJoin) { + sendUnsentMessages(conversation); + } + } + } + private void handleOrbotStartedEvent() { for (final Account account : accounts) { if (account.getStatus() == Account.State.TOR_NOT_AVAILABLE) { @@ -1830,9 +1852,19 @@ public class XmppConnectionService extends Service { } } - @TargetApi(Build.VERSION_CODES.M) private void scheduleNextIdlePing() { - final long timeToWake = SystemClock.elapsedRealtime() + (Config.IDLE_PING_INTERVAL * 1000); + scheduleNextIdlePing(0); + } + + @TargetApi(Build.VERSION_CODES.M) + private void scheduleNextIdlePing(long epochMillis) { + final long timeToWake; + if (epochMillis > 0) { + timeToWake = epochMillis; + Log.d(Config.LOGTAG, "scheduling next idle ping for " + timeToWake); + } else { + timeToWake = SystemClock.elapsedRealtime() + (Config.IDLE_PING_INTERVAL * 1000); + } final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); if (alarmManager == null) { return; @@ -2027,7 +2059,8 @@ public class XmppConnectionService extends Service { } } - if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview) { + // TODO: use timer to grab list of currently scheduled messages from in mem representation and sends them + if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview && !(message.getTimeSent() > System.currentTimeMillis())) { switch (message.getEncryption()) { case Message.ENCRYPTION_NONE: if (message.needsUploading()) { @@ -2121,6 +2154,10 @@ public class XmppConnectionService extends Service { message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid()); } + if (message.getTimeSent() > System.currentTimeMillis()) { + scheduleNextIdlePing(message.getTimeSent()); + } + if (resend) { if (packet != null && addToConversation) { if (account.getXmppConnection().getFeatures().sm() || mucMessage) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index b72415217b351ff92aa89b4c97ec88aac775b9af..fbf7c3525691748840d4e8d33328c649eaf5e04b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -12,9 +12,11 @@ import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; +import android.app.DatePickerDialog; import android.app.Fragment; import android.app.FragmentManager; import android.app.PendingIntent; +import android.app.TimePickerDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; @@ -23,6 +25,7 @@ import android.content.IntentSender.SendIntentException; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.icu.util.Calendar; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -928,6 +931,10 @@ public class ConversationFragment extends XmppFragment } private void sendMessage() { + sendMessage((Long) null); + } + + private void sendMessage(Long sendAt) { if (mediaPreviewAdapter.hasAttachments()) { commitAttachments(); return; @@ -998,6 +1005,7 @@ public class ConversationFragment extends XmppFragment message.setServerMsgId(null); message.setUuid(UUID.randomUUID().toString()); } + if (sendAt != null) message.setTime(sendAt); switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_PGP: sendPgpMessage(message); @@ -1934,6 +1942,9 @@ public class ConversationFragment extends XmppFragment case R.id.attach_subject: binding.textinputSubject.setVisibility(binding.textinputSubject.getVisibility() == View.GONE ? View.VISIBLE : View.GONE); break; + case R.id.attach_schedule: + scheduleMessage(); + break; case R.id.action_search: startSearch(); break; @@ -2011,6 +2022,23 @@ public class ConversationFragment extends XmppFragment startActivity(intent); } + private void scheduleMessage() { + // TODO: also gate menu option in UI behind version check + // TODO: upgrade to material you/3 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + final Calendar now = Calendar.getInstance(); + new DatePickerDialog(activity, (view, year, month, day) -> { + new TimePickerDialog(activity, (view1, hour, minute) -> { + final Calendar c = Calendar.getInstance(); + c.set(year, month, day, hour, minute); + final long timestamp = c.getTimeInMillis(); + sendMessage(timestamp); + Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": scheduled message for " + timestamp); + }, now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), true).show(); + }, now.get(Calendar.YEAR), now.get(Calendar.MONTH), now.get(Calendar.DAY_OF_MONTH)).show(); + } + } + private void returnToOngoingCall() { final Optional ongoingRtpSession = activity.xmppConnectionService diff --git a/src/main/res/menu/fragment_conversation.xml b/src/main/res/menu/fragment_conversation.xml index 76011677f7d598d1c093b07ae4d956f1bd108f4f..b99997bafd22bbbd7702bf4bb9c4a601b337aa1d 100644 --- a/src/main/res/menu/fragment_conversation.xml +++ b/src/main/res/menu/fragment_conversation.xml @@ -66,6 +66,10 @@ android:id="@+id/attach_subject" android:icon="@drawable/subject" android:title="Add Subject" /> + Full screen notifications Allow this app to show incoming call notifications that take up the full screen when the device is locked. Unsupported operation + Send later From e561e126eb3bfeace242d0cef4afb62f709f5065 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 8 May 2024 19:19:52 -0500 Subject: [PATCH 2/4] Scheduled messages cleanup --- .../conversations/entities/Conversation.java | 2 +- .../services/XmppConnectionService.java | 51 ++++++++++--------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index a148e645f552d9f5727f0adadd428e7d65e5568b..e296b6daf8903c31c52c9c8ece3aa29fadf1bb0e 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -357,7 +357,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl final ArrayList results = new ArrayList<>(); synchronized (this.messages) { for (Message message : this.messages) { - if (message.getStatus() == Message.STATUS_WAITING || message.getTimeSent() > System.currentTimeMillis()) { + if (message.getStatus() == Message.STATUS_WAITING) { results.add(message); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index df05175750f19acca6ab781dbf3f1af14db147f5..07955d99794d55c1441fb945ee72364df3073e28 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -91,6 +91,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; @@ -269,6 +270,7 @@ public class XmppConnectionService extends Service { private final ReplacingSerialSingleThreadExecutor mStickerScanExecutor = new ReplacingSerialSingleThreadExecutor("StickerScan"); private long mLastActivity = 0; private long mLastMucPing = 0; + private Map mScheduledMessages = new HashMap<>(); private long mLastStickerRescan = 0; private final FileBackend fileBackend = new FileBackend(this); private MemorizingTrustManager mMemorizingTrustManager; @@ -1067,10 +1069,9 @@ public class XmppConnectionService extends Service { } return START_NOT_STICKY; } + sendScheduledMessages(); final var extras = intent == null ? null : intent.getExtras(); try { - Log.d(Config.LOGTAG, "looking for and sending scheduled messages"); - sendScheduledMessages(); internalPingExecutor.execute(() -> manageAccountConnectionStates(action, extras)); } catch (final RejectedExecutionException e) { Log.e(Config.LOGTAG, "can not schedule connection states manager"); @@ -1157,9 +1158,13 @@ public class XmppConnectionService extends Service { } private void sendScheduledMessages() { - List conversations = getConversations(); - for (Conversation conversation : conversations) { - final Account account = conversation.getAccount(); + Log.d(Config.LOGTAG, "looking for and sending scheduled messages"); + + for (final var message : new ArrayList<>(mScheduledMessages.values())) { + if (message.getTimeSent() > System.currentTimeMillis()) continue; + + final var conversation = message.getConversation(); + final var account = conversation.getAccount(); final boolean inProgressJoin; synchronized (account.inProgressConferenceJoins) { inProgressJoin = account.inProgressConferenceJoins.contains(conversation); @@ -1171,7 +1176,7 @@ public class XmppConnectionService extends Service { if (conversation.getAccount() == account && !pendingJoin && !inProgressJoin) { - sendUnsentMessages(conversation); + resendMessage(message, false); } } } @@ -1852,21 +1857,18 @@ public class XmppConnectionService extends Service { } } - private void scheduleNextIdlePing() { - scheduleNextIdlePing(0); - } - @TargetApi(Build.VERSION_CODES.M) - private void scheduleNextIdlePing(long epochMillis) { - final long timeToWake; - if (epochMillis > 0) { - timeToWake = epochMillis; - Log.d(Config.LOGTAG, "scheduling next idle ping for " + timeToWake); - } else { - timeToWake = SystemClock.elapsedRealtime() + (Config.IDLE_PING_INTERVAL * 1000); + private void scheduleNextIdlePing() { + long timeUntilWake = Config.IDLE_PING_INTERVAL * 1000; + final var now = System.currentTimeMillis(); + for (final var message : mScheduledMessages.values()) { + if (message.getTimeSent() <= now) continue; // Just in case + if (message.getTimeSent() - now < timeUntilWake) timeUntilWake = message.getTimeSent() - now; } + final var timeToWake = SystemClock.elapsedRealtime() + timeUntilWake; final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); if (alarmManager == null) { + Log.d(Config.LOGTAG, "no alarm manager?"); return; } final Intent intent = new Intent(this, SystemEventReceiver.class); @@ -2059,8 +2061,7 @@ public class XmppConnectionService extends Service { } } - // TODO: use timer to grab list of currently scheduled messages from in mem representation and sends them - if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview && !(message.getTimeSent() > System.currentTimeMillis())) { + if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview && message.getTimeSent() <= System.currentTimeMillis()) { switch (message.getEncryption()) { case Message.ENCRYPTION_NONE: if (message.needsUploading()) { @@ -2148,16 +2149,20 @@ public class XmppConnectionService extends Service { } } + synchronized (mScheduledMessages) { + if (message.getTimeSent() > System.currentTimeMillis()) { + mScheduledMessages.put(message.getUuid(), message); + scheduleNextIdlePing(); + } else { + mScheduledMessages.remove(message.getUuid()); + } + } boolean mucMessage = conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage(); if (mucMessage) { message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid()); } - if (message.getTimeSent() > System.currentTimeMillis()) { - scheduleNextIdlePing(message.getTimeSent()); - } - if (resend) { if (packet != null && addToConversation) { if (account.getXmppConnection().getFeatures().sm() || mucMessage) { From b5f662b23ca14634d74c2fb149d35825719418f6 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 8 May 2024 19:31:07 -0500 Subject: [PATCH 3/4] Gate schedule menu on version and text to send --- .../java/eu/siacs/conversations/ui/ConversationFragment.java | 5 ++--- .../conversations/ui/util/ConversationMenuConfigurator.java | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index fbf7c3525691748840d4e8d33328c649eaf5e04b..3dc498e2af453a384cf18c0e5348b4e8cf403063 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1355,7 +1355,7 @@ public class ConversationFragment extends XmppFragment } else { menuUnmute.setVisible(false); } - ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu); + ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu, TextUtils.isEmpty(binding.textinput.getText())); ConversationMenuConfigurator.configureEncryptionMenu(conversation, menu); if (conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false)) { menuTogglePinned.setTitle(R.string.remove_from_favorites); @@ -1629,7 +1629,7 @@ public class ConversationFragment extends XmppFragment MenuItem newItem = menu.add(item.getGroupId(), item.getItemId(), item.getOrder(), item.getTitle()); newItem.setIcon(item.getIcon()); } - ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu); + ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu, TextUtils.isEmpty(binding.textinput.getText())); return; } @@ -2023,7 +2023,6 @@ public class ConversationFragment extends XmppFragment } private void scheduleMessage() { - // TODO: also gate menu option in UI behind version check // TODO: upgrade to material you/3 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { final Calendar now = Calendar.getInstance(); diff --git a/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java b/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java index 70235cf62531b409ce598f23cfdff0c98e7993c3..14c35d2c32aae0ab814c0224088a0bc029badf45 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java @@ -51,7 +51,7 @@ public class ConversationMenuConfigurator { microphoneAvailable = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MICROPHONE); } - public static void configureAttachmentMenu(@NonNull Conversation conversation, Menu menu) { + public static void configureAttachmentMenu(@NonNull Conversation conversation, Menu menu, boolean isTextEmpty) { final MenuItem menuAttach = menu.findItem(R.id.action_attach_file); final boolean visible; @@ -63,6 +63,9 @@ public class ConversationMenuConfigurator { if (menuAttach != null) menuAttach.setVisible(visible); if (visible) menu.findItem(R.id.attach_record_voice).setVisible(microphoneAvailable); menu.findItem(R.id.attach_subject).setVisible(conversation.getNextEncryption() == Message.ENCRYPTION_NONE); + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N || isTextEmpty) { + menu.findItem(R.id.attach_schedule).setVisible(false); + } } public static void configureEncryptionMenu(@NonNull Conversation conversation, Menu menu) { From 2932b5afd235c1a7b4544a5c105691de9911f95f Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 8 May 2024 20:04:28 -0500 Subject: [PATCH 4/4] Switch to material components --- .../ui/ConversationFragment.java | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 3dc498e2af453a384cf18c0e5348b4e8cf403063..b70407fa50680af9dcbcbb4fc20ad10827cb4879 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -2023,18 +2023,31 @@ public class ConversationFragment extends XmppFragment } private void scheduleMessage() { - // TODO: upgrade to material you/3 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - final Calendar now = Calendar.getInstance(); - new DatePickerDialog(activity, (view, year, month, day) -> { - new TimePickerDialog(activity, (view1, hour, minute) -> { - final Calendar c = Calendar.getInstance(); - c.set(year, month, day, hour, minute); - final long timestamp = c.getTimeInMillis(); - sendMessage(timestamp); - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": scheduled message for " + timestamp); - }, now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), true).show(); - }, now.get(Calendar.YEAR), now.get(Calendar.MONTH), now.get(Calendar.DAY_OF_MONTH)).show(); + final var datePicker = com.google.android.material.datepicker.MaterialDatePicker.Builder.datePicker() + .setTitleText("Schedule Message") + .setSelection(com.google.android.material.datepicker.MaterialDatePicker.todayInUtcMilliseconds()) + .setCalendarConstraints( + new com.google.android.material.datepicker.CalendarConstraints.Builder() + .setStart(com.google.android.material.datepicker.MaterialDatePicker.todayInUtcMilliseconds()) + .build() + ) + .build(); + datePicker.addOnPositiveButtonClickListener((date) -> { + final Calendar now = Calendar.getInstance(); + final var timePicker = new com.google.android.material.timepicker.MaterialTimePicker.Builder() + .setTitleText("Schedule Message") + .setHour(now.get(Calendar.HOUR)) + .setMinute(now.get(Calendar.MINUTE)) + .build(); + timePicker.addOnPositiveButtonClickListener((v2) -> { + final long timestamp = date + (timePicker.getHour() * 3600000) + (timePicker.getMinute() * 60000); + sendMessage(timestamp); + Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": scheduled message for " + timestamp); + }); + timePicker.show(activity.getSupportFragmentManager(), "schedulMessageTime"); + }); + datePicker.show(activity.getSupportFragmentManager(), "schedulMessageDate"); } }