From 4860adcd86e80baa41f83a1816e0ced52315ef64 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 2 Sep 2024 11:47:07 +1000 Subject: [PATCH] Added a new control message type to handle missed calls due to permissions --- .../MissingMicrophonePermissionDialog.kt | 29 ++++++ .../conversation/v2/ConversationAdapter.kt | 96 +++++++++++++------ .../v2/menus/ConversationMenuHelper.kt | 16 +--- .../securesms/database/MmsSmsColumns.java | 8 +- .../securesms/database/SmsDatabase.java | 2 + .../database/model/DisplayRecord.java | 3 + .../database/model/MessageRecord.java | 2 + .../securesms/webrtc/CallMessageProcessor.kt | 10 +- .../messaging/calls/CallMessageType.kt | 1 + .../utilities/UpdateMessageBuilder.kt | 4 +- 10 files changed, 123 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/MissingMicrophonePermissionDialog.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/MissingMicrophonePermissionDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/MissingMicrophonePermissionDialog.kt new file mode 100644 index 0000000000..7f0c1e61e4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/MissingMicrophonePermissionDialog.kt @@ -0,0 +1,29 @@ +package org.thoughtcrime.securesms + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.squareup.phrase.Phrase +import network.loki.messenger.R +import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY + +class MissingMicrophonePermissionDialog { + companion object { + @JvmStatic + fun show(context: Context) = context.showSessionDialog { + title(R.string.permissionsMicrophone) + text( + Phrase.from(context, R.string.permissionsMicrophoneAccessRequired) + .put(APP_NAME_KEY, context.getString(R.string.app_name)) + .format().toString()) + button(R.string.sessionSettings, R.string.AccessibilityId_sessionSettings) { + val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val uri = Uri.fromParts("package", context.packageName, null) + intent.setData(uri) + context.startActivity(intent) + } + cancelButton() + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index d75d14ea4a..f1ed1177f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Context import android.content.Intent import android.database.Cursor +import android.net.Uri import android.util.SparseArray import android.util.SparseBooleanArray import android.view.MotionEvent @@ -30,6 +31,9 @@ import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import com.bumptech.glide.RequestManager +import com.squareup.phrase.Phrase +import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY +import org.thoughtcrime.securesms.MissingMicrophonePermissionDialog import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.ui.getSubbedCharSequence @@ -121,7 +125,11 @@ class ConversationAdapter( val senderId = message.individualRecipient.address.serialize() val senderIdHash = senderId.hashCode() updateQueue.trySend(senderId) - if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault(senderIdHash, false)) { + if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault( + senderIdHash, + false + ) + ) { getSenderInfo(senderId)?.let { contact -> contactCache[senderIdHash] = contact } @@ -129,49 +137,77 @@ class ConversationAdapter( val contact = contactCache[senderIdHash] visibleMessageView.bind( - message, - messageBefore, - getMessageAfter(position, cursor), - glide, - searchQuery, - contact, - senderId, - lastSeen.get(), - visibleMessageViewDelegate, - onAttachmentNeedsDownload, - lastSentMessageId + message, + messageBefore, + getMessageAfter(position, cursor), + glide, + searchQuery, + contact, + senderId, + lastSeen.get(), + visibleMessageViewDelegate, + onAttachmentNeedsDownload, + lastSentMessageId ) if (!message.isDeleted) { - visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) } - visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) } - visibleMessageView.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition, visibleMessageView) } + visibleMessageView.onPress = { event -> + onItemPress( + message, + viewHolder.adapterPosition, + visibleMessageView, + event + ) + } + visibleMessageView.onSwipeToReply = + { onItemSwipeToReply(message, viewHolder.adapterPosition) } + visibleMessageView.onLongPress = + { onItemLongPress(message, viewHolder.adapterPosition, visibleMessageView) } } else { visibleMessageView.onPress = null visibleMessageView.onSwipeToReply = null visibleMessageView.onLongPress = null } } + is ControlMessageViewHolder -> { viewHolder.view.bind(message, messageBefore) - if (message.isCallLog && message.isFirstMissedCall) { - viewHolder.view.setOnClickListener { - context.showSessionDialog { - val titleTxt = context.getSubbedString(R.string.callsMissedCallFrom, NAME_KEY to message.individualRecipient.name!!) - title(titleTxt) - - val bodyTxt = context.getSubbedCharSequence(R.string.callsYouMissedCallPermissions, NAME_KEY to message.individualRecipient.name!!) - text(bodyTxt) - - button(R.string.sessionSettings) { - Intent(context, PrivacySettingsActivity::class.java) - .let(context::startActivity) + when { + // Click behaviour for first missed call control message + //todo this behaviour is different than iOS where the control message is always clickable when the call toggle is disabled in the privacy page + message.isCallLog && message.isFirstMissedCall -> { + viewHolder.view.setOnClickListener { + context.showSessionDialog { + val titleTxt = context.getSubbedString( + R.string.callsMissedCallFrom, + NAME_KEY to message.individualRecipient.name!! + ) + title(titleTxt) + + val bodyTxt = context.getSubbedCharSequence( + R.string.callsYouMissedCallPermissions, + NAME_KEY to message.individualRecipient.name!! + ) + text(bodyTxt) + + button(R.string.sessionSettings) { + Intent(context, PrivacySettingsActivity::class.java) + .let(context::startActivity) + } + cancelButton() } - cancelButton() } } - } else { - viewHolder.view.setOnClickListener(null) + + // Click behaviour for missed calls due to missing permission + message.isCallLog && message.isMissedPermissionCall -> { + viewHolder.view.setOnClickListener { + MissingMicrophonePermissionDialog.show(context) + } + } + + // non clickable in other cases + else -> viewHolder.view.setOnClickListener(null) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt index db00899436..fd62793422 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -31,6 +31,7 @@ import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.toHexString +import org.thoughtcrime.securesms.MissingMicrophonePermissionDialog import org.thoughtcrime.securesms.media.MediaOverviewActivity import org.thoughtcrime.securesms.ShortcutLauncherActivity import org.thoughtcrime.securesms.calls.WebRtcCallActivity @@ -182,20 +183,7 @@ object ConversationMenuHelper { // or if the user has not granted audio/microphone permissions else if (!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO)) { Log.d("Loki", "Attempted to make a call without audio permissions") - context.showSessionDialog { - title(R.string.permissionsMicrophone) - text(Phrase.from(context, R.string.permissionsMicrophoneAccessRequired) - .put(APP_NAME_KEY, context.getString(R.string.app_name)) - .format().toString()) - button(R.string.sessionSettings, R.string.AccessibilityId_sessionSettings) { - val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - val uri = Uri.fromParts("package", context.packageName, null) - intent.setData(uri) - context.startActivity(intent) - } - cancelButton() - } + MissingMicrophonePermissionDialog.show(context) return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index e6bc04e364..f98a3b6d62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -41,6 +41,7 @@ public interface MmsSmsColumns { protected static final long MISSED_CALL_TYPE = 3; protected static final long JOINED_TYPE = 4; protected static final long FIRST_MISSED_CALL_TYPE = 5; + protected static final long MISSED_PERMISSION_CALL_TYPE = 6; protected static final long BASE_INBOX_TYPE = 20; protected static final long BASE_OUTBOX_TYPE = 21; @@ -234,7 +235,8 @@ public interface MmsSmsColumns { public static boolean isCallLog(long type) { long baseType = type & BASE_TYPE_MASK; - return baseType == INCOMING_CALL_TYPE || baseType == OUTGOING_CALL_TYPE || baseType == MISSED_CALL_TYPE || baseType == FIRST_MISSED_CALL_TYPE; + return baseType == INCOMING_CALL_TYPE || baseType == OUTGOING_CALL_TYPE || + baseType == MISSED_CALL_TYPE || baseType == FIRST_MISSED_CALL_TYPE || baseType == MISSED_PERMISSION_CALL_TYPE; } public static boolean isExpirationTimerUpdate(long type) { @@ -265,6 +267,10 @@ public interface MmsSmsColumns { return (type & BASE_TYPE_MASK) == MISSED_CALL_TYPE; } + public static boolean isMissedPermissionCall(long type) { + return (type & BASE_TYPE_MASK) == MISSED_PERMISSION_CALL_TYPE; + } + public static boolean isFirstMissedCall(long type) { return (type & BASE_TYPE_MASK) == FIRST_MISSED_CALL_TYPE; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index f02498112f..09f0d64340 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -501,6 +501,8 @@ public class SmsDatabase extends MessagingDatabase { return Types.MISSED_CALL_TYPE; case CALL_FIRST_MISSED: return Types.FIRST_MISSED_CALL_TYPE; + case CALL_MISSED_PERMISSION: + return Types.MISSED_PERMISSION_CALL_TYPE; default: return 0; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index 639ea0db09..13fc8c4042 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -142,6 +142,9 @@ public abstract class DisplayRecord { public boolean isFirstMissedCall() { return SmsDatabase.Types.isFirstMissedCall(type); } + public boolean isMissedPermissionCall() { + return SmsDatabase.Types.isMissedPermissionCall(type); + } public boolean isDeleted() { return MmsSmsColumns.Types.isDeletedMessage(type); } public boolean isMessageRequestResponse() { return MmsSmsColumns.Types.isMessageRequestResponse(type); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index a61b78b4b6..c5bdc48675 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -134,6 +134,8 @@ public abstract class MessageRecord extends DisplayRecord { callType = CallMessageType.CALL_OUTGOING; } else if (isMissedCall()) { callType = CallMessageType.CALL_MISSED; + } else if (isMissedPermissionCall()) { + callType = CallMessageType.CALL_MISSED_PERMISSION; } else { callType = CallMessageType.CALL_FIRST_MISSED; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt index d63c33da30..e72a3000a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt @@ -78,8 +78,10 @@ class CallMessageProcessor(private val context: Context, private val textSecureP } // or if the user has not granted audio/microphone permissions else if (!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO)) { + if (nextMessage.type != PRE_OFFER) continue + val sentTimestamp = nextMessage.sentTimestamp ?: continue Log.d("Loki", "Attempted to receive a call without audio permissions") - //TODO display something to let the user know they missed a call due to missing permission + insertMissedPermissionCall(sender, sentTimestamp) continue } @@ -111,6 +113,12 @@ class CallMessageProcessor(private val context: Context, private val textSecureP } } + private fun insertMissedPermissionCall(sender: String, sentTimestamp: Long) { + val currentUserPublicKey = storage.getUserPublicKey() + if (sender == currentUserPublicKey) return // don't insert a "missed" due to call notifications disabled if it's our own sender + storage.insertCallMessage(sender, CallMessageType.CALL_MISSED_PERMISSION, sentTimestamp) + } + private fun incomingHangup(callMessage: CallMessage) { val callId = callMessage.callId ?: return val hangupIntent = WebRtcCallService.remoteHangupIntent(context, callId) diff --git a/libsession/src/main/java/org/session/libsession/messaging/calls/CallMessageType.kt b/libsession/src/main/java/org/session/libsession/messaging/calls/CallMessageType.kt index 7192bade8e..15e019fe7e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/calls/CallMessageType.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/calls/CallMessageType.kt @@ -5,4 +5,5 @@ enum class CallMessageType { CALL_INCOMING, CALL_OUTGOING, CALL_FIRST_MISSED, + CALL_MISSED_PERMISSION, } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt index d63dd347a2..6599cc3546 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt @@ -1,7 +1,6 @@ package org.session.libsession.messaging.utilities import android.content.Context -import android.text.SpannableString import com.squareup.phrase.Phrase import org.session.libsession.R import org.session.libsession.messaging.MessagingModuleConfiguration @@ -9,6 +8,7 @@ import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.calls.CallMessageType.CALL_FIRST_MISSED import org.session.libsession.messaging.calls.CallMessageType.CALL_INCOMING import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED +import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED_PERMISSION import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage @@ -24,7 +24,6 @@ import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_K import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.OTHER_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY -import org.session.libsession.utilities.Util object UpdateMessageBuilder { const val TAG = "libsession" @@ -267,6 +266,7 @@ object UpdateMessageBuilder { CALL_INCOMING -> Phrase.from(context, R.string.callsCalledYou).put(NAME_KEY, senderName).format().toString() CALL_OUTGOING -> Phrase.from(context, R.string.callsYouCalled).put(NAME_KEY, senderName).format().toString() CALL_MISSED, CALL_FIRST_MISSED -> Phrase.from(context, R.string.callsMissedCallFrom).put(NAME_KEY, senderName).format().toString() + CALL_MISSED_PERMISSION -> Phrase.from(context, R.string.callsMissedCallFrom).put(NAME_KEY, senderName).format().toString() + "\n" + context.getString(R.string.permissionsMicrophoneDescription) } } }