Save replies in drafts.

Previously, quotes were not saved to drafts, meaning they would be lost
when leaving the conversation or app. Now, a QuoteId (which represents
the necessary data to restore the QuoteModel) is serialized and stored
in the DraftDatabase.

Fixes #7716
Closes #7729
This commit is contained in:
Greyson Parrelli 2018-04-24 11:09:54 -07:00 committed by Moxie Marlinspike
parent 7100030c22
commit 43622e603d
6 changed files with 130 additions and 21 deletions

View File

@ -53,6 +53,7 @@
<string name="DraftDatabase_Draft_audio_snippet">(audio)</string>
<string name="DraftDatabase_Draft_video_snippet">(video)</string>
<string name="DraftDatabase_Draft_location_snippet">(location)</string>
<string name="DraftDatabase_Draft_quote_snippet">(reply)</string>
<!-- AttchmentManager -->
<string name="AttachmentManager_cant_open_media_selection">Can\'t find an app to select media.</string>

View File

@ -128,6 +128,8 @@ import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.QuoteId;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
@ -1035,6 +1037,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case Draft.VIDEO:
setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO);
break;
case Draft.QUOTE:
new QuoteRestorationTask(draft.getValue()).execute();
break;
}
} catch (IOException e) {
Log.w(TAG, e);
@ -1431,6 +1436,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
else if (slide.hasImage() && slide.getUri() != null) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString()));
}
Optional<QuoteModel> quote = inputPanel.getQuote();
if (quote.isPresent()) {
drafts.add(new Draft(Draft.QUOTE, new QuoteId(quote.get().getId(), quote.get().getAuthor()).serialize()));
}
return drafts;
}
@ -2134,4 +2145,33 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
}
private class QuoteRestorationTask extends AsyncTask<Void, Void, MessageRecord> {
private final String serialized;
QuoteRestorationTask(@NonNull String serialized) {
this.serialized = serialized;
}
@Override
protected MessageRecord doInBackground(Void... voids) {
QuoteId quoteId = QuoteId.deserialize(serialized);
if (quoteId != null) {
return DatabaseFactory.getMmsSmsDatabase(getApplicationContext()).getMessageFor(quoteId.getId(), quoteId.getAuthor());
}
return null;
}
@Override
protected void onPostExecute(MessageRecord messageRecord) {
if (messageRecord != null) {
handleReplyMessage(messageRecord);
} else {
Log.e(TAG, "Failed to restore a quote from a draft. No matching message record.");
}
}
}
}

View File

@ -101,6 +101,7 @@ public class DraftDatabase extends Database {
public static final String VIDEO = "video";
public static final String AUDIO = "audio";
public static final String LOCATION = "location";
public static final String QUOTE = "quote";
private final String type;
private final String value;
@ -125,6 +126,7 @@ public class DraftDatabase extends Database {
case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet);
case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet);
case LOCATION: return context.getString(R.string.DraftDatabase_Draft_location_snippet);
case QUOTE: return context.getString(R.string.DraftDatabase_Draft_quote_snippet);
default: return null;
}
}

View File

@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.database.Cursor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import net.sqlcipher.database.SQLiteDatabase;
@ -71,8 +72,24 @@ public class MmsSmsDatabase extends Database {
super(context, databaseHelper);
}
public Cursor getMessagesFor(long timestamp) {
return queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null);
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) {
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
MmsSmsDatabase.Reader reader = db.readerFor(cursor);
MessageRecord messageRecord;
while ((messageRecord = reader.getNext()) != null) {
if ((Util.isOwnNumber(context, author) && messageRecord.isOutgoing()) ||
(!Util.isOwnNumber(context, author) && messageRecord.getIndividualRecipient().getAddress().equals(author)))
{
return messageRecord;
}
}
}
return null;
}
public Cursor getConversation(long threadId, long limit) {

View File

@ -865,32 +865,22 @@ public class PushDecryptJob extends ContextJob {
return Optional.absent();
}
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
Address author = Address.fromExternal(context, quote.get().getAuthor().getNumber());
Address author = Address.fromExternal(context, quote.get().getAuthor().getNumber());
MessageRecord message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quote.get().getId(), author);
try (Cursor cursor = db.getMessagesFor(quote.get().getId())) {
MmsSmsDatabase.Reader reader = db.readerFor(cursor);
if (message != null) {
Log.w(TAG, "Found matching message record...");
MessageRecord messageRecord;
List<Attachment> attachments = new LinkedList<>();
while ((messageRecord = reader.getNext()) != null) {
if ((Util.isOwnNumber(context, author) && messageRecord.isOutgoing()) ||
(!Util.isOwnNumber(context, author) && messageRecord.getIndividualRecipient().getAddress().equals(author)))
{
Log.w(TAG, "Found matching message record...");
List<Attachment> attachments = new LinkedList<>();
if (messageRecord.isMms()) {
attachments = ((MmsMessageRecord)messageRecord).getSlideDeck().asAttachments();
}
return Optional.of(new QuoteModel(quote.get().getId(), author, messageRecord.getBody(), attachments));
}
if (message.isMms()) {
attachments = ((MmsMessageRecord) message).getSlideDeck().asAttachments();
}
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), attachments));
}
Log.w(TAG, "Didn't find matching message record...");
return Optional.of(new QuoteModel(quote.get().getId(),
author,
quote.get().getText(),

View File

@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.mms;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.model.MessageRecord;
/**
* Represents the information required to find the {@link MessageRecord} pointed to by a quote.
*/
public class QuoteId {
private static final String TAG = QuoteId.class.getSimpleName();
private static final String ID = "id";
private static final String AUTHOR = "author";
private final long id;
private final Address author;
public QuoteId(long id, @NonNull Address author) {
this.id = id;
this.author = author;
}
public long getId() {
return id;
}
public @NonNull Address getAuthor() {
return author;
}
public @NonNull String serialize() {
try {
JSONObject object = new JSONObject();
object.put(ID, id);
object.put(AUTHOR, author.serialize());
return object.toString();
} catch (JSONException e) {
Log.e(TAG, "Failed to serialize to json", e);
return "";
}
}
public static @Nullable QuoteId deserialize(@NonNull String serialized) {
try {
JSONObject json = new JSONObject(serialized);
return new QuoteId(json.getLong(ID), Address.fromSerialized(json.getString(AUTHOR)));
} catch (JSONException e) {
Log.e(TAG, "Failed to deserialize from json", e);
return null;
}
}
}