Compare commits

..

17 Commits

Author SHA1 Message Date
Greyson Parrelli
643dd0b679 Bump version to 4.20.6 2018-06-06 20:14:05 -07:00
Greyson Parrelli
2b45b3caa2 Fixed export and restore of FTS tables.
First, FTS index contents do not need to be exported. They will be recreated naturally.

Second, we can't export the secret FTS tables, or SQLite will think it's corrupted.
2018-06-06 20:12:17 -07:00
Greyson Parrelli
b7282589de Fixed issue with backup and restore when creating new tables.
Fixes #7863
2018-06-06 09:07:38 -07:00
Greyson Parrelli
6bc7f2a5a4 Fix NPE in FTS when snippet is null. 2018-06-06 08:08:52 -07:00
Greyson Parrelli
3731e2a74a Fix jumbomoji rendering and EmojiTextView resizing.
Fixed an issue where jumbomoji were not properly being rendered
when using system emoji. Also fixed an issue where the text
content wasn't properly being recalculated when the view is
resized.

Fixes #7875
2018-06-06 07:57:03 -07:00
Greyson Parrelli
bb40f38124 Bump version to 4.20.5 2018-05-29 13:42:21 -04:00
Greyson Parrelli
f0e5aa312e Allow EmojiTextView to redraw after changing emoji setting.
Also just fixed some minor formatting issues.
2018-05-28 17:20:28 -04:00
Greyson Parrelli
ceafb0d130 Reduce emoji flickering and other ellipsize woes.
1. Switch to using default text rendering if there's no emoji present in
the string.

2. Reduce redudant redraws by skipping of setText() calls for identical
strings.

Together, these two changes should reduce the vast majority of
flickering we see with EmojiTextView ellipsizing.
2018-05-24 13:32:56 -04:00
Moxie Marlinspike
70c2a863cc Correctly store backup
Fixes #7831
2018-05-24 09:57:16 -07:00
Greyson Parrelli
d813275f42 Increase number of recent conversations shown when sharing.
Intended to reduce the pain of #7202.
2018-05-24 10:25:58 -04:00
Greyson Parrelli
5650a02cfb Sort search results exclusively by date.
I think I was initially lured into searching by rank because it gives
the illusion of providing the "best match". However, in practice, FTS
never gives back "bad" matches with low ranks -- all of the results it
returns will contain your query in some form (most commonly a direct
substring, but they do take some liberties if you have multiple tokens
in your queries). Given that, in general, more recent search results are
in fact more relevant, we can sort by date exclusively and get a better
ordering overall.
2018-05-24 09:39:57 -04:00
Greyson Parrelli
0503c9eea5 Prevent replies on action messages.
Fixes #7829
2018-05-23 15:16:42 -04:00
Greyson Parrelli
abae419853 Flatten multiline text snippets in search results.
If a search result snippet spans two lines, we only show the first line.
For the purpose of display, we first remove all newlines in order to
make the full snippet visible.
2018-05-23 14:32:46 -04:00
Greyson Parrelli
71ccbf2a1b Secondarily sort search results by date.
This should help with getting back search results that more closely
match user intention.
2018-05-23 14:17:43 -04:00
Greyson Parrelli
92a64f59a4 Switch search to use the existing header.
Probably best to not try out any possible new design changes just yet.
Let's stick with what we have.
2018-05-23 11:06:07 -04:00
Greyson Parrelli
6d56447de2 Bump version to 4.20.4 2018-05-23 08:36:25 -04:00
Greyson Parrelli
e189fff856 Fixed some cursor-related bugs in Search. 2018-05-23 08:32:46 -04:00
16 changed files with 150 additions and 96 deletions

View File

@@ -257,8 +257,8 @@ android {
}
defaultConfig {
versionCode 361
versionName "4.20.3"
versionCode 364
versionName "4.20.6"
minSdkVersion 14
targetSdkVersion 25

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle" >
<solid android:color="@color/grey_200"/>
</shape>
</item>
<item android:top="1dp">
<shape android:shape="rectangle" >
<solid android:color="?attr/search_background" />
</shape>
</item>
</layer-list>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Base.TextAppearance.AppCompat.Body2"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@drawable/header_search_background"
tools:text="Conversations">
</TextView>

View File

@@ -266,7 +266,8 @@ public class ConversationFragment extends Fragment
menu.findItem(R.id.menu_context_forward).setVisible(!actionMessage);
menu.findItem(R.id.menu_context_details).setVisible(!actionMessage);
menu.findItem(R.id.menu_context_reply).setVisible(!messageRecord.isPending() &&
menu.findItem(R.id.menu_context_reply).setVisible(!actionMessage &&
!messageRecord.isPending() &&
!messageRecord.isFailed() &&
messageRecord.isSecure());
}

View File

@@ -315,11 +315,17 @@ public class ConversationListItem extends RelativeLayout
unreadIndicator.setVisibility(View.VISIBLE);
}
private Spanned getHighlightedSpan(@NonNull Locale locale,
private Spanned getHighlightedSpan(@NonNull Locale locale,
@Nullable String value,
@Nullable String highlight)
{
if (value == null || highlight == null) {
if (value == null) {
return new SpannableString("");
}
value = value.replaceAll("\n", " ");
if (highlight == null) {
return new SpannableString(value);
}

View File

@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
@@ -76,9 +77,11 @@ public class FullBackupExporter extends FullBackupBase {
count = exportTable(table, input, outputStream, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0, null, count);
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, null, cursor -> exportAttachment(attachmentSecret, cursor, outputStream), count);
} else if (!table.equals(SignedPreKeyDatabase.TABLE_NAME) &&
!table.equals(OneTimePreKeyDatabase.TABLE_NAME) &&
!table.equals(SessionDatabase.TABLE_NAME))
} else if (!table.equals(SignedPreKeyDatabase.TABLE_NAME) &&
!table.equals(OneTimePreKeyDatabase.TABLE_NAME) &&
!table.equals(SessionDatabase.TABLE_NAME) &&
!table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
!table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME))
{
count = exportTable(table, input, outputStream, null, null, count);
}
@@ -111,14 +114,17 @@ public class FullBackupExporter extends FullBackupBase {
String type = cursor.getString(2);
if (sql != null) {
if ("table".equals(type)) {
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement("DROP TABLE IF EXISTS " + name).build());
tables.add(name);
} else if ("index".equals(type)) {
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement("DROP INDEX IF EXISTS " + name).build());
}
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement(cursor.getString(0)).build());
boolean isSmsFtsSecretTable = name != null && !name.equals(SearchDatabase.SMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME);
boolean isMmsFtsSecretTable = name != null && !name.equals(SearchDatabase.MMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME);
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
if ("table".equals(type)) {
tables.add(name);
}
outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement(cursor.getString(0)).build());
}
}
}
}

View File

@@ -5,7 +5,9 @@ import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.support.annotation.NonNull;
import android.util.Log;
import android.util.Pair;
import net.sqlcipher.database.SQLiteDatabase;
@@ -20,6 +22,7 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.Util;
@@ -36,6 +39,7 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
@@ -62,6 +66,8 @@ public class FullBackupImporter extends FullBackupBase {
try {
db.beginTransaction();
dropAllTables(db);
BackupFrame frame;
while (!(frame = inputStream.readFrame()).getEnd()) {
@@ -87,6 +93,14 @@ public class FullBackupImporter extends FullBackupBase {
}
private static void processStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
boolean isForSmsFtsSecretTable = statement.getStatement().contains(SearchDatabase.SMS_FTS_TABLE_NAME + "_");
boolean isForMmsFtsSecretTable = statement.getStatement().contains(SearchDatabase.MMS_FTS_TABLE_NAME + "_");
if (isForSmsFtsSecretTable || isForMmsFtsSecretTable) {
Log.i(TAG, "Ignoring import for statement: " + statement.getStatement());
return;
}
List<Object> parameters = new LinkedList<>();
for (SqlStatement.SqlParameter parameter : statement.getParametersList()) {
@@ -131,6 +145,19 @@ public class FullBackupImporter extends FullBackupBase {
preferences.edit().putString(preference.getKey(), preference.getValue()).commit();
}
private static void dropAllTables(@NonNull SQLiteDatabase db) {
try (Cursor cursor = db.rawQuery("SELECT name, type FROM sqlite_master", null)) {
while (cursor != null && cursor.moveToNext()) {
String name = cursor.getString(0);
String type = cursor.getString(1);
if ("table".equals(type)) {
db.execSQL("DROP TABLE IF EXISTS " + name);
}
}
}
}
private static class BackupRecordInputStream extends BackupStream {
private final InputStream in;

View File

@@ -11,19 +11,23 @@ import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.ViewTreeObserver;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
public class EmojiTextView extends AppCompatTextView {
private final boolean scaleEmojis;
private CharSequence source;
private CharSequence previousText;
private BufferType previousBufferType;
private float originalFontSize;
private boolean useSystemEmoji;
private boolean sizeChangeInProgress;
public EmojiTextView(Context context) {
this(context, null);
@@ -46,67 +50,102 @@ public class EmojiTextView extends AppCompatTextView {
}
@Override public void setText(@Nullable CharSequence text, BufferType type) {
EmojiProvider provider = EmojiProvider.getInstance(getContext());
EmojiProvider provider = EmojiProvider.getInstance(getContext());
EmojiParser.CandidateList candidates = provider.getCandidates(text);
if (scaleEmojis && candidates != null && candidates.allEmojis) {
int emojis = candidates.size();
float scale = 1.0f;
int emojis = candidates.size();
float scale = 1.0f;
if (emojis <= 8) scale += 0.25f;
if (emojis <= 6) scale += 0.25f;
if (emojis <= 4) scale += 0.25f;
if (emojis <= 2) scale += 0.25f;
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize * scale);
} else if (scaleEmojis) {
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize);
}
if (useSystemEmoji()) {
super.setText(text, type);
if (unchanged(text, type)) {
return;
}
source = EmojiProvider.getInstance(getContext()).emojify(candidates, text, this);
super.setText(source, BufferType.SPANNABLE);
previousText = text;
previousBufferType = type;
useSystemEmoji = useSystemEmoji();
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
super.setText(text, BufferType.NORMAL);
return;
}
CharSequence emojified = provider.emojify(candidates, text, this);
super.setText(emojified, BufferType.SPANNABLE);
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
// We ellipsize them ourselves by manually truncating the appropriate section.
if (getEllipsize() == TextUtils.TruncateAt.END) {
post(() -> {
int maxLines = TextViewCompat.getMaxLines(EmojiTextView.this);
if (maxLines <= 0) {
return;
}
int lineCount = getLineCount();
if (lineCount > maxLines) {
int overflowStart = getLayout().getLineStart(maxLines - 1);
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth(), TextUtils.TruncateAt.END);
SpannableStringBuilder newContent = new SpannableStringBuilder();
newContent.append(getText().subSequence(0, overflowStart))
.append(ellipsized);
super.setText(newContent);
}
});
ellipsize();
}
}
private void ellipsize() {
post(() -> {
if (getLayout() == null) {
ellipsize();
return;
}
int maxLines = TextViewCompat.getMaxLines(EmojiTextView.this);
if (maxLines <= 0) {
return;
}
int lineCount = getLineCount();
if (lineCount > maxLines) {
int overflowStart = getLayout().getLineStart(maxLines - 1);
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth(), TextUtils.TruncateAt.END);
SpannableStringBuilder newContent = new SpannableStringBuilder();
newContent.append(getText().subSequence(0, overflowStart))
.append(ellipsized.subSequence(0, ellipsized.length()));
EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);
CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this);
super.setText(emojified, BufferType.SPANNABLE);
}
});
}
private boolean unchanged(CharSequence text, BufferType bufferType) {
return Util.equals(previousText, text) &&
Util.equals(previousBufferType, bufferType) &&
useSystemEmoji == useSystemEmoji() &&
!sizeChangeInProgress;
}
private boolean useSystemEmoji() {
return TextSecurePreferences.isSystemEmojiPreferred(getContext());
}
@Override public void invalidateDrawable(@NonNull Drawable drawable) {
if (drawable instanceof EmojiDrawable) invalidate();
else super.invalidateDrawable(drawable);
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (!sizeChangeInProgress) {
sizeChangeInProgress = true;
setText(previousText, previousBufferType);
sizeChangeInProgress = false;
}
}
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed && !useSystemEmoji()) {
super.setText(source, BufferType.SPANNABLE);
}
super.onLayout(changed, left, top, right, bottom);
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
if (drawable instanceof EmojiDrawable) invalidate();
else super.invalidateDrawable(drawable);
}
@Override

View File

@@ -63,6 +63,7 @@ public class ContactsCursorLoader extends CursorLoader {
ContactsDatabase.LABEL_COLUMN,
ContactsDatabase.CONTACT_TYPE_COLUMN};
private static final int RECENT_CONVERSATION_MAX = 25;
private final String filter;
private final int mode;
@@ -163,8 +164,8 @@ public class ContactsCursorLoader extends CursorLoader {
private Cursor getRecentConversationsCursor() {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(getContext());
MatrixCursor recentConversations = new MatrixCursor(CONTACT_PROJECTION, 5);
try (Cursor rawConversations = threadDatabase.getRecentConversationList(5)) {
MatrixCursor recentConversations = new MatrixCursor(CONTACT_PROJECTION, RECENT_CONVERSATION_MAX);
try (Cursor rawConversations = threadDatabase.getRecentConversationList(RECENT_CONVERSATION_MAX)) {
ThreadDatabase.Reader reader = threadDatabase.readerFor(rawConversations);
ThreadRecord threadRecord;
while ((threadRecord = reader.getNext()) != null) {

View File

@@ -30,8 +30,6 @@ public class CursorList<T> implements List<T>, Closeable {
public CursorList(@NonNull Cursor cursor, @NonNull ModelBuilder<T> modelBuilder) {
this.cursor = cursor;
this.modelBuilder = modelBuilder;
this.cursor.moveToFirst();
}
public static <T> CursorList<T> emptyList() {
@@ -58,6 +56,8 @@ public class CursorList<T> implements List<T>, Closeable {
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
int index = 0;
@Override
public boolean hasNext() {
return cursor.getCount() > 0 && !cursor.isLast();
@@ -65,9 +65,8 @@ public class CursorList<T> implements List<T>, Closeable {
@Override
public T next() {
T model = modelBuilder.build(cursor);
cursor.moveToNext();
return model;
cursor.moveToPosition(index++);
return modelBuilder.build(cursor);
}
};
}
@@ -179,7 +178,7 @@ public class CursorList<T> implements List<T>, Closeable {
@Override
public void close() {
if (cursor != null) {
if (!cursor.isClosed()) {
cursor.close();
}
}

View File

@@ -56,8 +56,7 @@ public class SearchDatabase extends Database {
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + ", " +
"snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " +
MmsSmsColumns.THREAD_ID + ", " +
"bm25(" + SMS_FTS_TABLE_NAME + ") AS " + RANK + " " +
MmsSmsColumns.THREAD_ID + " " +
"FROM " + SmsDatabase.TABLE_NAME + " " +
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + SmsDatabase.TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
@@ -67,13 +66,12 @@ public class SearchDatabase extends Database {
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + ", " +
"snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " +
MmsSmsColumns.THREAD_ID + ", " +
"bm25(" + MMS_FTS_TABLE_NAME + ") AS " + RANK + " " +
MmsSmsColumns.THREAD_ID + " " +
"FROM " + MmsDatabase.TABLE_NAME + " " +
"INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " +
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MmsDatabase.TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " +
"ORDER BY rank " +
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
"LIMIT 500";
public SearchDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) {

View File

@@ -52,7 +52,7 @@ public class LocalBackupJob extends ContextJob {
try {
String backupPassword = TextSecurePreferences.getBackupPassphrase(context);
File backupDirectory = StorageUtil.getBackupCacheDirectory(context);
File backupDirectory = StorageUtil.getBackupDirectory(context);
String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(new Date());
String fileName = String.format("signal-%s.backup", timestamp);
File backupFile = new File(backupDirectory, fileName);
@@ -65,7 +65,7 @@ public class LocalBackupJob extends ContextJob {
throw new IOException("Backup password is null");
}
File tempFile = File.createTempFile("backup", "tmp", context.getExternalCacheDir());
File tempFile = File.createTempFile("backup", "tmp", StorageUtil.getBackupCacheDirectory(context));
FullBackupExporter.export(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),

View File

@@ -97,7 +97,7 @@ class SearchListAdapter extends RecyclerView.Adapter<SearchListAdapter.Search
@Override
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
return new HeaderViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.header_search_result, parent, false));
.inflate(R.layout.contact_selection_list_divider, parent, false));
}
@Override
@@ -199,7 +199,7 @@ class SearchListAdapter extends RecyclerView.Adapter<SearchListAdapter.Search
public HeaderViewHolder(View itemView) {
super(itemView);
titleView = (TextView) itemView;
titleView = itemView.findViewById(R.id.label);
}
public void bind(int headerType) {

View File

@@ -47,6 +47,7 @@ class SearchViewModel extends ViewModel {
@Override
protected void onCleared() {
debouncer.clear();
searchResult.close();
}

View File

@@ -29,4 +29,8 @@ public class Debouncer {
handler.removeCallbacksAndMessages(null);
handler.postDelayed(runnable, threshold);
}
public void clear() {
handler.removeCallbacksAndMessages(null);
}
}

View File

@@ -12,8 +12,7 @@ import org.thoughtcrime.securesms.database.NoExternalStorageException;
import java.io.File;
public class StorageUtil
{
public class StorageUtil {
public static File getBackupDirectory(Context context) throws NoExternalStorageException {
File storage = null;
@@ -43,7 +42,6 @@ public class StorageUtil
}
}
return backups;
}