add stable IDs to conversations

fixes #2856
Closes #4607
// FREEBIE
pull/1/head
Jake McGinty 9 years ago committed by Moxie Marlinspike
parent 945636ac5c
commit 4314a4b42b

@ -22,12 +22,14 @@ import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import org.thoughtcrime.redphone.util.Conversions;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -39,6 +41,8 @@ import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.LRUCache;
import java.lang.ref.SoftReference;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
@ -46,6 +50,7 @@ import java.util.Map;
import java.util.Set;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.VisibleForTesting;
/**
* A cursor adapter for a conversation thread. Ultimately
@ -69,12 +74,13 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private final Set<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>());
private final ItemClickListener clickListener;
private final MasterSecret masterSecret;
private final Locale locale;
private final Recipients recipients;
private final MmsSmsDatabase db;
private final LayoutInflater inflater;
private final @Nullable ItemClickListener clickListener;
private final @NonNull MasterSecret masterSecret;
private final @NonNull Locale locale;
private final @NonNull Recipients recipients;
private final @NonNull MmsSmsDatabase db;
private final @NonNull LayoutInflater inflater;
private final @NonNull MessageDigest digest;
protected static class ViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
@ -92,6 +98,23 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
void onItemLongClick(ConversationItem item);
}
@SuppressWarnings("ConstantConditions")
@VisibleForTesting
ConversationAdapter(Context context, Cursor cursor) {
super(context, cursor);
try {
this.masterSecret = null;
this.locale = null;
this.clickListener = null;
this.recipients = null;
this.inflater = null;
this.db = null;
this.digest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException nsae) {
throw new AssertionError("SHA1 isn't supported!");
}
}
public ConversationAdapter(@NonNull Context context,
@NonNull MasterSecret masterSecret,
@NonNull Locale locale,
@ -100,12 +123,19 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
@NonNull Recipients recipients)
{
super(context, cursor);
this.masterSecret = masterSecret;
this.locale = locale;
this.clickListener = clickListener;
this.recipients = recipients;
this.inflater = LayoutInflater.from(context);
this.db = DatabaseFactory.getMmsSmsDatabase(context);
try {
this.masterSecret = masterSecret;
this.locale = locale;
this.clickListener = clickListener;
this.recipients = recipients;
this.inflater = LayoutInflater.from(context);
this.db = DatabaseFactory.getMmsSmsDatabase(context);
this.digest = MessageDigest.getInstance("SHA1");
setHasStableIds(true);
} catch (NoSuchAlgorithmException nsae) {
throw new AssertionError("SHA1 isn't supported!");
}
}
@Override
@ -114,7 +144,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
super.changeCursor(cursor);
}
@Override public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
@Override
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
MessageRecord messageRecord = getMessageRecord(id, cursor, type);
@ -122,7 +153,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
viewHolder.getView().bind(masterSecret, messageRecord, locale, batchSelected, recipients);
}
@Override public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
@Override
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType));
if (viewType == MESSAGE_TYPE_INCOMING || viewType == MESSAGE_TYPE_OUTGOING) {
itemView.setOnClickListener(new OnClickListener() {
@ -143,7 +175,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
return new ViewHolder(itemView);
}
@Override public void onItemViewRecycled(ViewHolder holder) {
@Override
public void onItemViewRecycled(ViewHolder holder) {
holder.getView().unbind();
}
@ -171,6 +204,13 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
}
}
@Override
public long getItemId(@NonNull Cursor cursor) {
final String unique = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsColumns.UNIQUE_ROW_ID));
final byte[] bytes = digest.digest(unique.getBytes());
return Conversions.byteArrayToLong(bytes);
}
private MessageRecord getMessageRecord(long messageId, Cursor cursor, String type) {
final SoftReference<MessageRecord> reference = messageRecordCache.get(type + messageId);
if (reference != null) {

@ -92,6 +92,7 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter<Conversat
this.locale = locale;
this.inflater = LayoutInflater.from(context);
this.clickListener = clickListener;
setHasStableIds(true);
}
@Override

@ -128,7 +128,8 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
public void onItemViewRecycled(VH holder) {}
@Override public final ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
@Override
public final ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case HEADER_TYPE: return new HeaderFooterViewHolder(header);
case FOOTER_TYPE: return new HeaderFooterViewHolder(footer);
@ -149,7 +150,8 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
public abstract void onBindItemViewHolder(VH viewHolder, @NonNull Cursor cursor);
@Override public int getItemViewType(int position) {
@Override
public final int getItemViewType(int position) {
if (isHeaderPosition(position)) return HEADER_TYPE;
if (isFooterPosition(position)) return FOOTER_TYPE;
moveToPositionOrThrow(getCursorPosition(position));
@ -160,6 +162,16 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
return 0;
}
@Override
public final long getItemId(int position) {
moveToPositionOrThrow(getCursorPosition(position));
return getItemId(cursor);
}
public long getItemId(@NonNull Cursor cursor) {
return cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
}
private void assertActiveCursor() {
if (!isActiveCursor()) {
throw new IllegalStateException("this should only be called when the cursor is valid");

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.database;
@SuppressWarnings("UnnecessaryInterfaceModifier")
public interface MmsSmsColumns {
public static final String ID = "_id";
@ -12,6 +13,7 @@ public interface MmsSmsColumns {
public static final String ADDRESS_DEVICE_ID = "address_device_id";
public static final String RECEIPT_COUNT = "delivery_receipt_count";
public static final String MISMATCHED_IDENTITIES = "mismatched_identities";
public static final String UNIQUE_ROW_ID = "unique_row_id";
public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF;

@ -16,13 +16,11 @@
*/
package org.thoughtcrime.securesms.database;
import android.annotation.TargetApi;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
@ -42,7 +40,8 @@ public class MmsSmsDatabase extends Database {
public static final String MMS_TRANSPORT = "mms";
public static final String SMS_TRANSPORT = "sms";
private static final String[] PROJECTION = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
private static final String[] PROJECTION = {MmsSmsColumns.ID, MmsSmsColumns.UNIQUE_ROW_ID,
SmsDatabase.BODY, SmsDatabase.TYPE,
MmsSmsColumns.THREAD_ID,
SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT,
MmsSmsColumns.NORMALIZED_DATE_SENT,
@ -123,6 +122,9 @@ public class MmsSmsDatabase extends Database {
String[] mmsProjection = {MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " AS " + MmsSmsColumns.ID,
"'MMS::' || " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID
+ " || '::' || " + MmsDatabase.DATE_SENT
+ " AS " + MmsSmsColumns.UNIQUE_ROW_ID,
AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS,
SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE,
@ -143,7 +145,11 @@ public class MmsSmsDatabase extends Database {
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsSmsColumns.ID, "NULL AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS,
MmsSmsColumns.ID,
"'SMS::' || " + MmsSmsColumns.ID
+ " || '::' || " + SmsDatabase.DATE_SENT
+ " AS " + MmsSmsColumns.UNIQUE_ROW_ID,
"NULL AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS,
SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE,
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
@ -222,8 +228,10 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(SmsDatabase.DATE_RECEIVED);
smsColumnsPresent.add(SmsDatabase.STATUS);
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 3, MMS_TRANSPORT, selection, null, null, null);
String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 3, SMS_TRANSPORT, selection, null, null, null);
@SuppressWarnings("deprecation")
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 4, MMS_TRANSPORT, selection, null, null, null);
@SuppressWarnings("deprecation")
String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 4, SMS_TRANSPORT, selection, null, null, null);
SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
String unionQuery = unionQueryBuilder.buildUnionQuery(new String[] {smsSubQuery, mmsSubQuery}, order, limit);
@ -231,6 +239,7 @@ public class MmsSmsDatabase extends Database {
SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
outerQueryBuilder.setTables("(" + unionQuery + ")");
@SuppressWarnings("deprecation")
String query = outerQueryBuilder.buildQuery(projection, null, null, null, null, null, null);
Log.w("MmsSmsDatabase", "Executing query: " + query);

@ -0,0 +1,37 @@
package org.thoughtcrime.securesms;
import android.database.Cursor;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.when;
public class ConversationAdapterTest extends BaseUnitTest {
private Cursor cursor = mock(Cursor.class);
private ConversationAdapter adapter;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
adapter = new ConversationAdapter(context, cursor);
when(cursor.getColumnIndexOrThrow(anyString())).thenReturn(0);
}
@Test
public void testGetItemIdEquals() throws Exception {
when(cursor.getString(anyInt())).thenReturn("SMS::1::1");
long firstId = adapter.getItemId(cursor);
when(cursor.getString(anyInt())).thenReturn("MMS::1::1");
long secondId = adapter.getItemId(cursor);
assertNotEquals(firstId, secondId);
when(cursor.getString(anyInt())).thenReturn("MMS::2::1");
long thirdId = adapter.getItemId(cursor);
assertNotEquals(secondId, thirdId);
}
}
Loading…
Cancel
Save