diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java index b1f02f0d69..19a47db279 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.Projection; +import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat; import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat; @@ -64,6 +65,8 @@ public class ConversationItemFooter extends ConstraintLayout { private final Rect speedToggleHitRect = new Rect(); private final int touchTargetSize = ViewUtil.dpToPx(48); + private long previousMessageId; + public ConversationItemFooter(Context context) { super(context); init(null); @@ -368,6 +371,19 @@ public class ConversationItemFooter extends ConstraintLayout { } private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) { + long newMessageId = buildMessageId(messageRecord); + + if (previousMessageId == newMessageId && deliveryStatusView.isPending() && !messageRecord.isPending()) { + if (messageRecord.getRecipient().isGroup()) { + SignalLocalMetrics.GroupMessageSend.onUiUpdated(messageRecord.getId()); + } else { + SignalLocalMetrics.IndividualMessageSend.onUiUpdated(messageRecord.getId()); + } + } + + previousMessageId = newMessageId; + + if (messageRecord.isFailed() || messageRecord.isPendingInsecureSmsFallback()) { deliveryStatusView.setNone(); return; @@ -426,6 +442,10 @@ public class ConversationItemFooter extends ConstraintLayout { playbackSpeedToggleTextView.setVisibility(View.GONE); } + private long buildMessageId(@NonNull MessageRecord record) { + return record.isMms() ? -record.getId() : record.getId(); + } + public interface OnTouchDelegateChangedListener { void onTouchDelegateChanged(@NonNull Rect delegateRect, @NonNull View delegateView); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java b/app/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java index 5e7a2893e6..c430e4988d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java @@ -60,6 +60,10 @@ public class DeliveryStatusView extends FrameLayout { this.setVisibility(View.GONE); } + public boolean isPending() { + return pendingIndicator.getVisibility() == View.VISIBLE; + } + public void setPending() { this.setVisibility(View.VISIBLE); pendingIndicator.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index c571a313a6..6aae8bf8c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.RecipientAccessList; +import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.ContentHint; @@ -147,6 +148,8 @@ public final class PushGroupSendJob extends PushSendJob { public void onPushSend() throws IOException, MmsException, NoSuchMessageException, RetryLaterException { + SignalLocalMetrics.GroupMessageSend.onJobStarted(messageId); + MessageDatabase database = DatabaseFactory.getMmsDatabase(context); OutgoingMediaMessage message = database.getOutgoingMessage(messageId); long threadId = database.getMessageRecord(messageId).getThreadId(); @@ -186,6 +189,7 @@ public final class PushGroupSendJob extends PushSendJob { RecipientAccessList accessList = new RecipientAccessList(target); List results = deliver(message, groupRecipient, target); + SignalLocalMetrics.GroupMessageSend.onNetworkFinished(messageId); Log.i(TAG, JobLogger.format(this, "Finished send.")); List networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(accessList.requireIdByAddress(result.getAddress()))).toList(); @@ -258,6 +262,8 @@ public final class PushGroupSendJob extends PushSendJob { database.markAsSentFailed(messageId); notifyMediaMessageDeliveryFailed(context, messageId); } + + SignalLocalMetrics.GroupMessageSend.onJobFinished(messageId); } @Override @@ -303,6 +309,7 @@ public final class PushGroupSendJob extends PushSendJob { .withExpiration(groupRecipient.getExpiresInSeconds()) .asGroupMessage(group) .build(); + SignalLocalMetrics.GroupMessageSend.onNetworkStarted(messageId); return GroupSendUtil.sendResendableDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.IMPLICIT, new MessageId(messageId, true), groupDataMessage); } else { throw new UndeliverableMessageException("Messages can no longer be sent to V1 groups!"); @@ -328,6 +335,7 @@ public final class PushGroupSendJob extends PushSendJob { Log.i(TAG, JobLogger.format(this, "Beginning message send.")); + SignalLocalMetrics.GroupMessageSend.onNetworkStarted(messageId); return GroupSendUtil.sendResendableDataMessage(context, groupRecipient.getGroupId().transform(GroupId::requireV2).orNull(), destinations, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 3dac31c1c8..41b69857f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; +import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; @@ -74,6 +75,8 @@ public class PushTextSendJob extends PushSendJob { @Override public void onPushSend() throws IOException, NoSuchMessageException, UndeliverableMessageException, RetryLaterException { + SignalLocalMetrics.IndividualMessageSend.onJobStarted(messageId); + ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager(); MessageDatabase database = DatabaseFactory.getSmsDatabase(context); SmsMessageRecord record = database.getSmsMessage(messageId); @@ -136,6 +139,8 @@ public class PushTextSendJob extends PushSendJob { } catch (ProofRequiredException e) { handleProofRequiredException(e, record.getRecipient(), record.getThreadId(), messageId, false); } + + SignalLocalMetrics.IndividualMessageSend.onJobFinished(messageId); } @Override @@ -176,11 +181,17 @@ public class PushTextSendJob extends PushSendJob { Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess); + SignalLocalMetrics.IndividualMessageSend.onNetworkStarted(messageId); SendMessageResult result = messageSender.sendSyncMessage(syncMessage, syncAccess); + SignalLocalMetrics.IndividualMessageSend.onNetworkFinished(messageId); + DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(messageRecipient.getId(), message.getDateSent(), result, ContentHint.RESENDABLE, new MessageId(messageId, false)); return syncAccess.isPresent(); } else { + SignalLocalMetrics.IndividualMessageSend.onNetworkStarted(messageId); SendMessageResult result = messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.RESENDABLE, textSecureMessage); + SignalLocalMetrics.IndividualMessageSend.onNetworkFinished(messageId); + DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(messageRecipient.getId(), message.getDateSent(), result, ContentHint.RESENDABLE, new MessageId(messageId, false)); return result.getSuccess().isUnidentified(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index 30ed094760..b42cf41d75 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.util.ParcelUtil; +import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Preconditions; @@ -114,6 +115,8 @@ public class MessageSender { System.currentTimeMillis(), insertListener); + SignalLocalMetrics.IndividualMessageSend.start(messageId); + sendTextMessage(context, recipient, forceSms, keyExchange, messageId); onMessageSent(); @@ -135,6 +138,10 @@ public class MessageSender { Recipient recipient = message.getRecipient(); long messageId = database.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId), allocatedThreadId, forceSms, insertListener); + if (message.getRecipient().isGroup() && message.getAttachments().isEmpty() && message.getLinkPreviews().isEmpty() && message.getSharedContacts().isEmpty()) { + SignalLocalMetrics.GroupMessageSend.start(messageId); + } + sendMediaMessage(context, recipient, forceSms, messageId, Collections.emptyList()); onMessageSent(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt b/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt index 667dc83d81..6173fd1fef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt @@ -26,8 +26,8 @@ import java.util.concurrent.Executor object LocalMetrics { private val TAG: String = Log.tag(LocalMetrics::class.java) - private val eventsById: MutableMap = mutableMapOf() - private val lastSplitTimeById: MutableMap = mutableMapOf() + private val eventsById: MutableMap = LRUCache(200) + private val lastSplitTimeById: MutableMap = LRUCache(200) private val executor: Executor = SignalExecutors.newCachedSingleThreadExecutor("signal-LocalMetrics") private val db: LocalMetricsDatabase by lazy { LocalMetricsDatabase.getInstance(ApplicationDependencies.getApplication()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java b/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java index 9ecb35c94d..8d3b8201de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java @@ -25,8 +25,8 @@ public final class SignalLocalMetrics { @MainThread public static void start() { - conversationListId = NAME_CONVERSATION_LIST + System.currentTimeMillis(); - otherId = NAME_OTHER + System.currentTimeMillis(); + conversationListId = NAME_CONVERSATION_LIST + "-" + System.currentTimeMillis(); + otherId = NAME_OTHER + "-" + System.currentTimeMillis(); LocalMetrics.getInstance().start(conversationListId, NAME_CONVERSATION_LIST); LocalMetrics.getInstance().start(otherId, NAME_OTHER); @@ -63,4 +63,82 @@ public final class SignalLocalMetrics { } } } + + public static final class IndividualMessageSend { + private static final String NAME = "individual-message-send"; + + private static final String SPLIT_JOB_ENQUEUE = "job-enqueue"; + private static final String SPLIT_JOB_PRE_NETWORK = "job-pre-network"; + private static final String SPLIT_NETWORK = "network"; + private static final String SPLIT_JOB_POST_NETWORK = "job-post-network"; + private static final String SPLIT_UI_UPDATE = "ui-update"; + + public static void start(long messageId) { + LocalMetrics.getInstance().start(buildId(messageId), NAME); + } + + public static void onJobStarted(long messageId) { + LocalMetrics.getInstance().split(buildId(messageId), SPLIT_JOB_ENQUEUE); + } + + public static void onNetworkStarted(long messageId) { + LocalMetrics.getInstance().split(buildId(messageId), SPLIT_JOB_PRE_NETWORK); + } + + public static void onNetworkFinished(long messageId) { + LocalMetrics.getInstance().split(buildId(messageId), SPLIT_NETWORK); + } + + public static void onJobFinished(long messageId) { + LocalMetrics.getInstance().split(buildId(messageId), SPLIT_JOB_POST_NETWORK); + } + + public static void onUiUpdated(long messageId) { + LocalMetrics.getInstance().split(buildId(messageId), SPLIT_UI_UPDATE); + LocalMetrics.getInstance().end(buildId(messageId)); + } + + private static String buildId(long messageId) { + return NAME + "-" + messageId; + } + } + + public static final class GroupMessageSend { + private static final String NAME = "group-message-send"; + + private static final String SPLIT_JOB_ENQUEUE = "job-enqueue"; + private static final String SPLIT_JOB_PRE_NETWORK = "job-pre-network"; + private static final String SPLIT_NETWORK = "network"; + private static final String SPLIT_JOB_POST_NETWORK = "job-post-network"; + private static final String SPLIT_UI_UPDATE = "ui-update"; + + public static void start(long messageId) { + LocalMetrics.getInstance().start(buildId(messageId), NAME); + } + + public static void onJobStarted(long messageId) { + LocalMetrics.getInstance().split(buildId(messageId), SPLIT_JOB_ENQUEUE); + } + + public static void onNetworkStarted(long messageId) { + LocalMetrics.getInstance().split(buildId(messageId), SPLIT_JOB_PRE_NETWORK); + } + + public static void onNetworkFinished(long messageId) { + LocalMetrics.getInstance().split(buildId(messageId), SPLIT_NETWORK); + } + + public static void onJobFinished(long messageId) { + LocalMetrics.getInstance().split(buildId(messageId), SPLIT_JOB_POST_NETWORK); + } + + public static void onUiUpdated(long messageId) { + LocalMetrics.getInstance().split(buildId(messageId), SPLIT_UI_UPDATE); + LocalMetrics.getInstance().end(buildId(messageId)); + } + + private static String buildId(long messageId) { + return NAME + "-" + messageId; + } + } }