Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

View File

@@ -0,0 +1,28 @@
package org.thoughtcrime.securesms.longmessage;
import android.text.TextUtils;
import org.thoughtcrime.securesms.database.model.MessageRecord;
/**
* A wrapper around a {@link MessageRecord} and its extra text attachment expanded into a string
* held in memory.
*/
class LongMessage {
private final MessageRecord messageRecord;
private final String fullBody;
LongMessage(MessageRecord messageRecord, String fullBody) {
this.messageRecord = messageRecord;
this.fullBody = fullBody;
}
MessageRecord getMessageRecord() {
return messageRecord;
}
String getFullBody() {
return !TextUtils.isEmpty(fullBody) ? fullBody : messageRecord.getBody();
}
}

View File

@@ -0,0 +1,178 @@
package org.thoughtcrime.securesms.longmessage;
import androidx.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.util.TypedValue;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.views.Stub;
public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
private static final String KEY_CONVERSATION_RECIPIENT = "recipient_id";
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_IS_MMS = "is_mms";
private static final int MAX_DISPLAY_LENGTH = 64 * 1024;
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private final DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
private Stub<ViewGroup> sentBubble;
private Stub<ViewGroup> receivedBubble;
private LongMessageViewModel viewModel;
public static Intent getIntent(@NonNull Context context, @NonNull RecipientId conversationRecipient, long messageId, boolean isMms) {
Intent intent = new Intent(context, LongMessageActivity.class);
intent.putExtra(KEY_CONVERSATION_RECIPIENT, conversationRecipient);
intent.putExtra(KEY_MESSAGE_ID, messageId);
intent.putExtra(KEY_IS_MMS, isMms);
return intent;
}
@Override
protected void onPreCreate() {
super.onPreCreate();
dynamicLanguage.onCreate(this);
dynamicTheme.onCreate(this);
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready);
setContentView(R.layout.longmessage_activity);
sentBubble = new Stub<>(findViewById(R.id.longmessage_sent_stub));
receivedBubble = new Stub<>(findViewById(R.id.longmessage_received_stub));
initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), getIntent().getBooleanExtra(KEY_IS_MMS, false));
LiveRecipient conversationRecipient = Recipient.live(getIntent().getParcelableExtra(KEY_CONVERSATION_RECIPIENT));
conversationRecipient.observe(this, recipient -> updateActionBarColor(recipient.getColor()));
updateActionBarColor(conversationRecipient.get().getColor());
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
protected void onResume() {
super.onResume();
dynamicLanguage.onResume(this);
dynamicTheme.onResume(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return false;
}
private void updateActionBarColor(@NonNull MaterialColor color) {
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(color.toStatusBarColor(this));
}
}
private void initViewModel(long messageId, boolean isMms) {
viewModel = ViewModelProviders.of(this, new LongMessageViewModel.Factory(getApplication(), new LongMessageRepository(this), messageId, isMms))
.get(LongMessageViewModel.class);
viewModel.getMessage().observe(this, message -> {
if (message == null) return;
if (!message.isPresent()) {
Toast.makeText(this, R.string.LongMessageActivity_unable_to_find_message, Toast.LENGTH_SHORT).show();
finish();
return;
}
if (message.get().getMessageRecord().isOutgoing()) {
getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_your_message));
} else {
Recipient recipient = message.get().getMessageRecord().getRecipient();
String name = recipient.getDisplayName(this);
getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_message_from_s, name));
}
ViewGroup bubble;
if (message.get().getMessageRecord().isOutgoing()) {
bubble = sentBubble.get();
bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.conversation_item_bubble_background), PorterDuff.Mode.MULTIPLY);
} else {
bubble = receivedBubble.get();
bubble.getBackground().setColorFilter(message.get().getMessageRecord().getRecipient().getColor().toConversationColor(this), PorterDuff.Mode.MULTIPLY);
}
TextView text = bubble.findViewById(R.id.longmessage_text);
ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer);
String trimmedBody = getTrimmedBody(message.get().getFullBody());
SpannableString styledBody = linkifyMessageBody(new SpannableString(trimmedBody));
bubble.setVisibility(View.VISIBLE);
text.setText(styledBody);
text.setMovementMethod(LinkMovementMethod.getInstance());
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(this));
footer.setMessageRecord(message.get().getMessageRecord(), dynamicLanguage.getCurrentLocale());
});
}
private String getTrimmedBody(@NonNull String text) {
return text.length() <= MAX_DISPLAY_LENGTH ? text
: text.substring(0, MAX_DISPLAY_LENGTH);
}
private SpannableString linkifyMessageBody(SpannableString messageBody) {
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
boolean hasLinks = Linkify.addLinks(messageBody, linkPattern);
if (hasLinks) {
Stream.of(messageBody.getSpans(0, messageBody.length(), URLSpan.class))
.filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL()))
.forEach(messageBody::removeSpan);
}
return messageBody;
}
}

View File

@@ -0,0 +1,101 @@
package org.thoughtcrime.securesms.longmessage;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.io.InputStream;
class LongMessageRepository {
private final static String TAG = LongMessageRepository.class.getSimpleName();
private final MmsDatabase mmsDatabase;
private final SmsDatabase smsDatabase;
LongMessageRepository(@NonNull Context context) {
this.mmsDatabase = DatabaseFactory.getMmsDatabase(context);
this.smsDatabase = DatabaseFactory.getSmsDatabase(context);
}
void getMessage(@NonNull Context context, long messageId, boolean isMms, @NonNull Callback<Optional<LongMessage>> callback) {
SignalExecutors.BOUNDED.execute(() -> {
if (isMms) {
callback.onComplete(getMmsLongMessage(context, mmsDatabase, messageId));
} else {
callback.onComplete(getSmsLongMessage(smsDatabase, messageId));
}
});
}
@WorkerThread
private Optional<LongMessage> getMmsLongMessage(@NonNull Context context, @NonNull MmsDatabase mmsDatabase, long messageId) {
Optional<MmsMessageRecord> record = getMmsMessage(mmsDatabase, messageId);
if (record.isPresent()) {
TextSlide textSlide = record.get().getSlideDeck().getTextSlide();
if (textSlide != null && textSlide.getUri() != null) {
return Optional.of(new LongMessage(record.get(), readFullBody(context, textSlide.getUri())));
} else {
return Optional.of(new LongMessage(record.get(), ""));
}
} else {
return Optional.absent();
}
}
@WorkerThread
private Optional<LongMessage> getSmsLongMessage(@NonNull SmsDatabase smsDatabase, long messageId) {
Optional<MessageRecord> record = getSmsMessage(smsDatabase, messageId);
if (record.isPresent()) {
return Optional.of(new LongMessage(record.get(), ""));
} else {
return Optional.absent();
}
}
@WorkerThread
private Optional<MmsMessageRecord> getMmsMessage(@NonNull MmsDatabase mmsDatabase, long messageId) {
try (Cursor cursor = mmsDatabase.getMessage(messageId)) {
return Optional.fromNullable((MmsMessageRecord) mmsDatabase.readerFor(cursor).getNext());
}
}
@WorkerThread
private Optional<MessageRecord> getSmsMessage(@NonNull SmsDatabase smsDatabase, long messageId) {
try (Cursor cursor = smsDatabase.getMessageCursor(messageId)) {
return Optional.fromNullable(smsDatabase.readerFor(cursor).getNext());
}
}
private String readFullBody(@NonNull Context context, @NonNull Uri uri) {
try (InputStream stream = PartAuthority.getAttachmentStream(context, uri)) {
return Util.readFullyAsString(stream);
} catch (IOException e) {
Log.w(TAG, "Failed to read full text body.", e);
return "";
}
}
interface Callback<T> {
void onComplete(T result);
}
}

View File

@@ -0,0 +1,84 @@
package org.thoughtcrime.securesms.longmessage;
import android.app.Application;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.whispersystems.libsignal.util.guava.Optional;
class LongMessageViewModel extends ViewModel {
private final Application application;
private final LongMessageRepository repository;
private final long messageId;
private final boolean isMms;
private final MutableLiveData<Optional<LongMessage>> message;
private final MessageObserver messageObserver;
private LongMessageViewModel(@NonNull Application application, @NonNull LongMessageRepository repository, long messageId, boolean isMms) {
this.application = application;
this.repository = repository;
this.messageId = messageId;
this.isMms = isMms;
this.message = new MutableLiveData<>();
this.messageObserver = new MessageObserver(new Handler());
repository.getMessage(application, messageId, isMms, longMessage -> {
if (longMessage.isPresent()) {
Uri uri = DatabaseContentProviders.Conversation.getUriForThread(longMessage.get().getMessageRecord().getThreadId());
application.getContentResolver().registerContentObserver(uri, true, messageObserver);
}
message.postValue(longMessage);
});
}
LiveData<Optional<LongMessage>> getMessage() {
return message;
}
@Override
protected void onCleared() {
application.getContentResolver().unregisterContentObserver(messageObserver);
}
private class MessageObserver extends ContentObserver {
MessageObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
repository.getMessage(application, messageId, isMms, message::postValue);
}
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application context;
private final LongMessageRepository repository;
private final long messageId;
private final boolean isMms;
public Factory(@NonNull Application application, @NonNull LongMessageRepository repository, long messageId, boolean isMms) {
this.context = application;
this.repository = repository;
this.messageId = messageId;
this.isMms = isMms;
}
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return modelClass.cast(new LongMessageViewModel(context, repository, messageId, isMms));
}
}
}