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