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