Added ability to receive long messages.
Send support is in here too. We'll enable it in a future release after enough people have updated.pull/1/head
parent
bf28e109d3
commit
55699e27bc
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/longmessage_sent_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/longmessage_bubble_sent"/>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/longmessage_received_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/longmessage_bubble_received"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</ScrollView>
|
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/message_bubble_background_received_alone"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/longmessage_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginRight="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:textColor="?conversation_item_received_text_primary_color"
|
||||
android:textColorLink="?conversation_item_received_text_primary_color"
|
||||
android:textIsSelectable="true"
|
||||
app:scaleEmojis="true"
|
||||
tools:text="With great power comes great responsibility."/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ConversationItemFooter
|
||||
android:id="@+id/longmessage_footer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginRight="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:alpha="0.7"
|
||||
app:footer_text_color="?attr/conversation_item_received_text_secondary_color"
|
||||
app:footer_icon_color="?attr/conversation_item_received_text_secondary_color"/>
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/message_bubble_background_sent_alone"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/longmessage_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/message_bubble_top_padding"
|
||||
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginRight="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_collapsed_footer_padding"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:textColor="?conversation_item_sent_text_primary_color"
|
||||
android:textColorLink="?conversation_item_sent_text_primary_color"
|
||||
android:textIsSelectable="true"
|
||||
app:scaleEmojis="true"
|
||||
tools:text="With great power comes great responsibility."/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ConversationItemFooter
|
||||
android:id="@+id/longmessage_footer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:layout_marginLeft="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginRight="@dimen/message_bubble_horizontal_padding"
|
||||
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:footer_text_color="?attr/conversation_item_sent_text_secondary_color"
|
||||
app:footer_icon_color="?attr/conversation_item_sent_icon_color"/>
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,26 @@
|
||||
package org.thoughtcrime.securesms.longmessage;
|
||||
|
||||
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 extraBody;
|
||||
|
||||
LongMessage(MessageRecord messageRecord, String extraBody) {
|
||||
this.messageRecord = messageRecord;
|
||||
this.extraBody = extraBody;
|
||||
}
|
||||
|
||||
MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
}
|
||||
|
||||
String getFullBody() {
|
||||
return messageRecord.getBody() + extraBody;
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
package org.thoughtcrime.securesms.longmessage;
|
||||
|
||||
import android.arch.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 android.support.annotation.NonNull;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
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.database.Address;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
public class LongMessageActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener {
|
||||
|
||||
private static final String KEY_ADDRESS = "address";
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_IS_MMS = "is_mms";
|
||||
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
private Stub<ViewGroup> sentBubble;
|
||||
private Stub<ViewGroup> receivedBubble;
|
||||
|
||||
private LongMessageViewModel viewModel;
|
||||
|
||||
public static Intent getIntent(@NonNull Context context, @NonNull Address conversationAddress, long messageId, boolean isMms) {
|
||||
Intent intent = new Intent(context, LongMessageActivity.class);
|
||||
intent.putExtra(KEY_ADDRESS, conversationAddress.serialize());
|
||||
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));
|
||||
|
||||
Recipient conversationRecipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
|
||||
conversationRecipient.addListener(this);
|
||||
updateActionBarColor(conversationRecipient.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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(final Recipient recipient) {
|
||||
Util.runOnMain(() -> updateActionBarColor(recipient.getColor()));
|
||||
}
|
||||
|
||||
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 = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize()) ;
|
||||
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);
|
||||
|
||||
bubble.setVisibility(View.VISIBLE);
|
||||
text.setText(message.get().getFullBody());
|
||||
footer.setMessageRecord(message.get().getMessageRecord(), dynamicLanguage.getCurrentLocale());
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package org.thoughtcrime.securesms.longmessage;
|
||||
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.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.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) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.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);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package org.thoughtcrime.securesms.longmessage;
|
||||
|
||||
import android.app.Application;
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import android.support.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());
|
||||
}
|
||||
|
||||
LiveData<Optional<LongMessage>> getMessage() {
|
||||
repository.getMessage(application, messageId, isMms, longMessage -> {
|
||||
if (longMessage.isPresent()) {
|
||||
application.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(longMessage.get().getMessageRecord().getThreadId()), true, messageObserver);
|
||||
}
|
||||
message.postValue(longMessage);
|
||||
});
|
||||
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) {
|
||||
getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
|
||||
public class TextSlide extends Slide {
|
||||
|
||||
public TextSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
public TextSlide(@NonNull Context context, @NonNull Uri uri, @Nullable String filename, long size) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, false, false));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue