mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-06 13:31:18 +01:00
Disappear 1:1 calls.
This commit is contained in:
+21
@@ -7,11 +7,13 @@ import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.databinding.ConversationSettingsCallPreferenceItemBinding
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Renders a single call preference row when displaying call info.
|
||||
@@ -41,6 +43,25 @@ object CallPreference {
|
||||
binding.callIcon.setImageResource(getCallIcon(model.call))
|
||||
binding.callType.text = getCallType(model.call)
|
||||
binding.callTime.text = getCallTime(model.record)
|
||||
presentTimer(model.record)
|
||||
}
|
||||
|
||||
private fun presentTimer(messageRecord: MessageRecord) {
|
||||
if (messageRecord.expiresIn > 0) {
|
||||
binding.callTimer.visible = true
|
||||
binding.callTimer.setPercentComplete(0f)
|
||||
|
||||
if (messageRecord.expireStarted > 0) {
|
||||
binding.callTimer.setExpirationTime(messageRecord.expireStarted, messageRecord.expiresIn)
|
||||
binding.callTimer.startAnimation()
|
||||
|
||||
if (messageRecord.expireStarted + messageRecord.expiresIn <= System.currentTimeMillis()) {
|
||||
AppDependencies.expiringMessageManager.checkSchedule()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.callTimer.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
|
||||
+5
-2
@@ -19,7 +19,6 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
@@ -104,6 +103,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
private EventListener eventListener;
|
||||
private Button collapsedButton;
|
||||
private float lastYDownRelativeToThis;
|
||||
private int tint;
|
||||
|
||||
private final UpdateObserver updateObserver = new UpdateObserver();
|
||||
|
||||
@@ -221,6 +221,8 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
observeDisplayBody(lifecycleOwner, spannableMessage);
|
||||
observeDisplayBodyWithTimer(lifecycleOwner);
|
||||
|
||||
this.tint = updateDescription.getTint(getContext());
|
||||
|
||||
boolean donationRequest = conversationMessage.getMessageRecord().isReleaseChannelDonationRequest();
|
||||
|
||||
present(conversationMessage, nextMessageRecord, conversationRecipient, isMessageRequestAccepted);
|
||||
@@ -487,8 +489,9 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(displayBody);
|
||||
|
||||
int color = tint != 0 ? tint : ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary);
|
||||
if (latestFrame != 0) {
|
||||
Drawable drawable = DrawableUtil.tint(getContext().getDrawable(latestFrame), ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary));
|
||||
Drawable drawable = DrawableUtil.tint(getContext().getDrawable(latestFrame), color);
|
||||
SpanUtil.appendCenteredImageSpan(builder, drawable, 12, 12);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ class ExpirationTimer(
|
||||
}
|
||||
|
||||
fun calculateProgress(): Float {
|
||||
if (startedAt == 0L) {
|
||||
return 0f
|
||||
}
|
||||
|
||||
val progressed = System.currentTimeMillis() - startedAt
|
||||
val percentComplete = progressed.toFloat() / expiresIn.toFloat()
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.ringrtc.CallId
|
||||
import org.signal.ringrtc.CallManager.RingUpdate
|
||||
import org.thoughtcrime.securesms.database.CallTable.Companion.TIMESTAMP
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.CallLinkUpdateSendJob
|
||||
@@ -109,24 +110,66 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
)
|
||||
}
|
||||
fun markAllCallEventsRead(timestamp: Long = Long.MAX_VALUE) {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val allUnreadMissedCalls = readableDatabase
|
||||
.select(MESSAGE_ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$TIMESTAMP <= ? AND $READ != ? AND $EVENT = ?", timestamp, ReadState.serialize(ReadState.READ), Event.serialize(Event.MISSED))
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
cursor.requireLong(MESSAGE_ID)
|
||||
}
|
||||
|
||||
val updateCount = writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(READ to ReadState.serialize(ReadState.READ))
|
||||
.where("$TIMESTAMP <= ? AND $READ != ?", timestamp, ReadState.serialize(ReadState.READ))
|
||||
.run()
|
||||
|
||||
val expiringCalls = SignalDatabase.messages.getUnstartedExpirations(allUnreadMissedCalls)
|
||||
|
||||
if (expiringCalls.isNotEmpty()) {
|
||||
Log.i(TAG, "Found ${expiringCalls.size} calls that needs expiring.")
|
||||
SignalDatabase.messages.markExpireStarted(expiringCalls.map { it.key to now })
|
||||
for ((messageId, expiresIn) in expiringCalls) {
|
||||
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, now, expiresIn)
|
||||
}
|
||||
}
|
||||
|
||||
if (updateCount > 0) {
|
||||
notifyConversationListListeners()
|
||||
}
|
||||
}
|
||||
|
||||
fun markAllCallEventsWithPeerBeforeTimestampRead(peer: RecipientId, timestamp: Long): Call? {
|
||||
val now = System.currentTimeMillis()
|
||||
val latestCallAsOfTimestamp = writableDatabase.withinTransaction { db ->
|
||||
|
||||
val unreadMissedCalls = db
|
||||
.select(MESSAGE_ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$PEER = ? AND $TIMESTAMP <= ? AND $READ != ? AND $EVENT = ?", peer.toLong(), timestamp, ReadState.serialize(ReadState.READ), Event.serialize(Event.MISSED))
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
cursor.requireLong(MESSAGE_ID)
|
||||
}
|
||||
|
||||
val updated = db.update(TABLE_NAME)
|
||||
.values(READ to ReadState.serialize(ReadState.READ))
|
||||
.where("$PEER = ? AND $TIMESTAMP <= ?", peer.toLong(), timestamp)
|
||||
.run()
|
||||
|
||||
val expiring = SignalDatabase.messages.getUnstartedExpirations(unreadMissedCalls)
|
||||
|
||||
if (expiring.isNotEmpty()) {
|
||||
Log.i(TAG, "Found ${expiring.size} calls that needs expiring.")
|
||||
SignalDatabase.messages.markExpireStarted(expiring.map { it.key to now })
|
||||
for ((messageId, expiresIn) in expiring) {
|
||||
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, true, now, expiresIn)
|
||||
}
|
||||
}
|
||||
|
||||
if (updated == 0) {
|
||||
null
|
||||
} else {
|
||||
@@ -157,7 +200,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
val messageType: Long = Call.getMessageType(type, direction, event)
|
||||
|
||||
writableDatabase.withinTransaction {
|
||||
val result = SignalDatabase.messages.insertCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING)
|
||||
val result = SignalDatabase.messages.insertOneToOneCallLog(peer, messageType, timestamp, direction == Direction.OUTGOING)
|
||||
val values = contentValuesOf(
|
||||
CALL_ID to callId,
|
||||
MESSAGE_ID to result.messageId,
|
||||
@@ -202,7 +245,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
if (call.messageId == null) {
|
||||
Log.w(TAG, "Call does not have an associated message id! No message to update.")
|
||||
} else {
|
||||
SignalDatabase.messages.updateCallLog(call.messageId, call.messageType)
|
||||
SignalDatabase.messages.updateOneToOneCallLog(call.messageId, call.messageType)
|
||||
}
|
||||
|
||||
AppDependencies.messageNotifier.updateNotification(context)
|
||||
|
||||
@@ -655,6 +655,32 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return queryMessages(where, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of ids, will return a list of the ids that have an expiration and what that expiration is.
|
||||
*/
|
||||
fun getUnstartedExpirations(messageIds: List<Long>): Map<Long, Long> {
|
||||
val expirations: MutableMap<Long, Long> = hashMapOf()
|
||||
|
||||
SqlUtil.buildCollectionQuery(
|
||||
column = ID,
|
||||
values = messageIds,
|
||||
prefix = "$EXPIRES_IN != 0 AND $EXPIRE_STARTED = 0 AND"
|
||||
).forEach { query ->
|
||||
readableDatabase
|
||||
.select(ID, EXPIRES_IN)
|
||||
.from(TABLE_NAME)
|
||||
.where(query.where, query.whereArgs)
|
||||
.run()
|
||||
.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
expirations[cursor.requireLong(ID)] = cursor.requireLong(EXPIRES_IN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expirations
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff
|
||||
* - the message will expire within [ChatItemArchiveExporter.EXPIRATION_CUTOFF] once viewed
|
||||
@@ -876,11 +902,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return results
|
||||
}
|
||||
|
||||
fun insertCallLog(recipientId: RecipientId, type: Long, timestamp: Long, outgoing: Boolean): InsertResult {
|
||||
fun insertOneToOneCallLog(recipientId: RecipientId, type: Long, timestamp: Long, outgoing: Boolean): InsertResult {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
val threadIdResult = threads.getOrCreateThreadIdResultFor(recipient.id, recipient.isGroup)
|
||||
val threadId = threadIdResult.threadId
|
||||
val dateReceived = System.currentTimeMillis()
|
||||
val expiresIn = if (RemoteConfig.disappearMore) threads.getExpiresIn(threadId) else 0
|
||||
|
||||
val values = contentValuesOf(
|
||||
FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else recipientId.serialize(),
|
||||
@@ -891,7 +918,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
READ to 1,
|
||||
NOTIFIED to 1,
|
||||
TYPE to type,
|
||||
THREAD_ID to threadId
|
||||
THREAD_ID to threadId,
|
||||
EXPIRES_IN to expiresIn
|
||||
)
|
||||
|
||||
val messageId = writableDatabase.insert(TABLE_NAME, null, values)
|
||||
@@ -910,7 +938,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
)
|
||||
}
|
||||
|
||||
fun updateCallLog(messageId: Long, type: Long) {
|
||||
fun updateOneToOneCallLog(messageId: Long, type: Long) {
|
||||
val message = getMessageRecordOrNull(messageId = messageId)
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
@@ -930,6 +958,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
maybeCollapseMessage(db = writableDatabase, messageId = messageId, threadId = threadId, dateReceived = message.dateReceived, messageExtras = message.messageExtras, messageType = type)
|
||||
}
|
||||
|
||||
// Start disappearing timer when a call is answered or declined (e.g. not missed)
|
||||
if (message?.expiresIn != null && message.expiresIn != 0L && !MessageTypes.isMissedVideoCall(type) && !MessageTypes.isMissedAudioCall(type)) {
|
||||
val now = System.currentTimeMillis()
|
||||
markExpireStarted(messageId, now)
|
||||
AppDependencies.expiringMessageManager.scheduleDeletion(messageId, message.isMms, now, message.expiresIn)
|
||||
}
|
||||
|
||||
notifyConversationListeners(threadId)
|
||||
AppDependencies.databaseObserver.notifyMessageUpdateObservers(MessageId(messageId))
|
||||
}
|
||||
|
||||
@@ -2086,7 +2086,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExpiresIn(threadId: Long): Long {
|
||||
fun getExpiresIn(threadId: Long): Long {
|
||||
return readableDatabase
|
||||
.select(EXPIRES_IN)
|
||||
.from(TABLE_NAME)
|
||||
|
||||
@@ -511,6 +511,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return UpdateDescription.staticDescriptionWithExpiration(string, glyph);
|
||||
}
|
||||
|
||||
protected static @NonNull UpdateDescription staticUpdateDescriptionWithExpiration(@NonNull String string, Glyph glyph, @ColorInt int lightTint, @ColorInt int darkTint) {
|
||||
return UpdateDescription.staticDescriptionWithExpiration(string, glyph, lightTint, darkTint);
|
||||
}
|
||||
|
||||
protected static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string,
|
||||
Glyph glyph,
|
||||
@ColorInt int lightTint,
|
||||
|
||||
@@ -265,13 +265,13 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
|
||||
if (call.getEvent() == CallTable.Event.NOT_ACCEPTED) {
|
||||
int message = isVideoCall ? R.string.MessageRecord_unanswered_video_call : R.string.MessageRecord_unanswered_voice_call;
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(message), callDateString),
|
||||
return staticUpdateDescriptionWithExpiration(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(message), callDateString),
|
||||
icon,
|
||||
ContextCompat.getColor(context, R.color.core_red_shade),
|
||||
ContextCompat.getColor(context, R.color.core_red));
|
||||
} else {
|
||||
int updateString = isVideoCall ? R.string.MessageRecord_outgoing_video_call : R.string.MessageRecord_outgoing_voice_call;
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
|
||||
return staticUpdateDescriptionWithExpiration(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
|
||||
}
|
||||
} else {
|
||||
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
|
||||
@@ -279,7 +279,7 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
if (accepted || !call.isDisplayedAsMissedCallInUi()) {
|
||||
int updateString = isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call;
|
||||
Glyph icon = isVideoCall ? Glyph.VIDEO_CAMERA : Glyph.PHONE;
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
|
||||
return staticUpdateDescriptionWithExpiration(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
|
||||
} else {
|
||||
Glyph icon = isVideoCall ? Glyph.VIDEO_CAMERA : Glyph.PHONE;
|
||||
int message;
|
||||
@@ -291,7 +291,7 @@ public class MmsMessageRecord extends MessageRecord {
|
||||
message = isVideoCall ? R.string.MessageRecord_missed_video_call : R.string.MessageRecord_missed_voice_call;
|
||||
}
|
||||
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date,
|
||||
return staticUpdateDescriptionWithExpiration(context.getString(R.string.MessageRecord_call_message_with_date,
|
||||
context.getString(message),
|
||||
callDateString),
|
||||
icon,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -10,6 +11,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.ui.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.Glyph;
|
||||
import org.signal.core.models.ServiceId;
|
||||
|
||||
@@ -98,7 +100,14 @@ public final class UpdateDescription {
|
||||
* Create an update description that's string value is fixed with a start glyph and has the ability to expire when a disappearing timer is set.
|
||||
*/
|
||||
public static UpdateDescription staticDescriptionWithExpiration(@NonNull String staticString, Glyph glyph) {
|
||||
return new UpdateDescription(Collections.emptyList(), null, new SpannableString(staticString), glyph, true,0, 0);
|
||||
return staticDescriptionWithExpiration(staticString, glyph, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an update description that's string value is fixed with a start glyph and has the ability to expire when a disappearing timer is set.
|
||||
*/
|
||||
public static UpdateDescription staticDescriptionWithExpiration(@NonNull String staticString, Glyph glyph, @ColorInt int lightTint, @ColorInt int darkTint) {
|
||||
return new UpdateDescription(Collections.emptyList(), null, new SpannableString(staticString), glyph, true, lightTint, darkTint);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,6 +170,11 @@ public final class UpdateDescription {
|
||||
return darkTint;
|
||||
}
|
||||
|
||||
public @ColorInt int getTint(Context context) {
|
||||
boolean isDarkTheme = ThemeUtil.isDarkTheme(context);
|
||||
return isDarkTheme ? getDarkTint() : getLightTint();
|
||||
}
|
||||
|
||||
public boolean hasExpiration() {
|
||||
return canExpire;
|
||||
}
|
||||
|
||||
@@ -1467,5 +1467,13 @@ object RemoteConfig {
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@get:JvmName("disappearMore")
|
||||
val disappearMore: Boolean by remoteBoolean(
|
||||
key = "android.disappearMore",
|
||||
defaultValue = false,
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user