show call log messages in conversation stream

This commit is contained in:
Daniel Gultsch 2020-04-12 17:12:59 +02:00
parent 1dc88f38ca
commit 3439f40411
48 changed files with 1077 additions and 855 deletions

View file

@ -57,6 +57,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public static final int TYPE_STATUS = 3; public static final int TYPE_STATUS = 3;
public static final int TYPE_PRIVATE = 4; public static final int TYPE_PRIVATE = 4;
public static final int TYPE_PRIVATE_FILE = 5; public static final int TYPE_PRIVATE_FILE = 5;
public static final int TYPE_RTP_SESSION = 6;
public static final String CONVERSATION = "conversationUuid"; public static final String CONVERSATION = "conversationUuid";
public static final String COUNTERPART = "counterpart"; public static final String COUNTERPART = "counterpart";
@ -151,6 +152,31 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
null); null);
} }
public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
this(conversation, java.util.UUID.randomUUID().toString(),
conversation.getUuid(),
conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
null,
null,
System.currentTimeMillis(),
Message.ENCRYPTION_NONE,
status,
type,
false,
remoteMsgId,
null,
null,
null,
true,
null,
false,
null,
null,
false,
false,
null);
}
protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart, protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
final Jid trueCounterpart, final String body, final long timeSent, final Jid trueCounterpart, final String body, final long timeSent,
final int encryption, final int status, final int type, final boolean carbon, final int encryption, final int status, final int type, final boolean carbon,

View file

@ -0,0 +1,59 @@
package eu.siacs.conversations.entities;
import android.support.annotation.DrawableRes;
import com.google.common.base.Strings;
import eu.siacs.conversations.R;
public class RtpSessionStatus {
public final boolean successful;
public final long duration;
public RtpSessionStatus(boolean successful, long duration) {
this.successful = successful;
this.duration = duration;
}
@Override
public String toString() {
return successful + ":" + duration;
}
public static RtpSessionStatus of(final String body) {
final String[] parts = Strings.nullToEmpty(body).split(":", 2);
long duration = 0;
if (parts.length == 2) {
try {
duration = Long.parseLong(parts[1]);
} catch (NumberFormatException e) {
//do nothing
}
}
boolean made;
try {
made = Boolean.parseBoolean(parts[0]);
} catch (Exception e) {
made = false;
}
return new RtpSessionStatus(made, duration);
}
public static @DrawableRes int getDrawable(final boolean received, final boolean successful, final boolean darkTheme) {
if (received) {
if (successful) {
return darkTheme ? R.drawable.ic_call_received_white_18dp : R.drawable.ic_call_received_black_18dp;
} else {
return darkTheme ? R.drawable.ic_call_missed_white_18dp : R.drawable.ic_call_missed_black_18dp;
}
} else {
if (successful) {
return darkTheme ? R.drawable.ic_call_made_white_18dp : R.drawable.ic_call_made_black_18dp;
} else {
return darkTheme ? R.drawable.ic_call_missed_outgoing_white_18dp : R.drawable.ic_call_missed_outgoing_black_18dp;
}
}
}
}

View file

@ -49,6 +49,7 @@ import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Message.FileParams; import eu.siacs.conversations.entities.Message.FileParams;
import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.http.P1S3UrlStreamHandler;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
@ -72,6 +73,7 @@ import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.StylingHelper;
import eu.siacs.conversations.utils.TimeframeUtils;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.mam.MamReference;
import rocks.xmpp.addr.Jid; import rocks.xmpp.addr.Jid;
@ -83,6 +85,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
private static final int RECEIVED = 1; private static final int RECEIVED = 1;
private static final int STATUS = 2; private static final int STATUS = 2;
private static final int DATE_SEPARATOR = 3; private static final int DATE_SEPARATOR = 3;
private static final int RTP_SESSION = 4;
private final XmppActivity activity; private final XmppActivity activity;
private final ListSelectionManager listSelectionManager = new ListSelectionManager(); private final ListSelectionManager listSelectionManager = new ListSelectionManager();
private final AudioPlayer audioPlayer; private final AudioPlayer audioPlayer;
@ -92,6 +95,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
private OnContactPictureLongClicked mOnContactPictureLongClickedListener; private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
private boolean mUseGreenBackground = false; private boolean mUseGreenBackground = false;
private OnQuoteListener onQuoteListener; private OnQuoteListener onQuoteListener;
public MessageAdapter(XmppActivity activity, List<Message> messages) { public MessageAdapter(XmppActivity activity, List<Message> messages) {
super(activity, 0, messages); super(activity, 0, messages);
this.audioPlayer = new AudioPlayer(this); this.audioPlayer = new AudioPlayer(this);
@ -101,7 +105,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
} }
private static void resetClickListener(View... views) { private static void resetClickListener(View... views) {
for (View view : views) { for (View view : views) {
view.setOnClickListener(null); view.setOnClickListener(null);
@ -135,7 +138,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
@Override @Override
public int getViewTypeCount() { public int getViewTypeCount() {
return 4; return 5;
} }
private int getItemViewType(Message message) { private int getItemViewType(Message message) {
@ -145,12 +148,14 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
} else { } else {
return STATUS; return STATUS;
} }
} else if (message.getType() == Message.TYPE_RTP_SESSION) {
return RTP_SESSION;
} else if (message.getStatus() <= Message.STATUS_RECEIVED) { } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
return RECEIVED; return RECEIVED;
} } else {
return SENT; return SENT;
} }
}
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
@ -283,7 +288,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
final String bodyLanguage = message.getBodyLanguage(); final String bodyLanguage = message.getBodyLanguage();
final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format(" \u00B7 %s", bodyLanguage.toUpperCase(Locale.US)); final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format(" \u00B7 %s", bodyLanguage.toUpperCase(Locale.US));
if (message.getStatus() <= Message.STATUS_RECEIVED) { ; if (message.getStatus() <= Message.STATUS_RECEIVED) {
if ((filesize != null) && (info != null)) { if ((filesize != null) && (info != null)) {
viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + " \u00B7 " + info + bodyLanguageInfo); viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + " \u00B7 " + info + bodyLanguageInfo);
} else if ((filesize == null) && (info != null)) { } else if ((filesize == null) && (info != null)) {
@ -622,6 +627,13 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false); view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false);
viewHolder.status_message = view.findViewById(R.id.message_body); viewHolder.status_message = view.findViewById(R.id.message_body);
viewHolder.message_box = view.findViewById(R.id.message_box); viewHolder.message_box = view.findViewById(R.id.message_box);
viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
break;
case RTP_SESSION:
view = activity.getLayoutInflater().inflate(R.layout.message_rtp_session, parent, false);
viewHolder.status_message = view.findViewById(R.id.message_body);
viewHolder.message_box = view.findViewById(R.id.message_box);
viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
break; break;
case SENT: case SENT:
view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false); view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false);
@ -684,6 +696,28 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
} }
viewHolder.message_box.setBackgroundResource(activity.isDarkTheme() ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white); viewHolder.message_box.setBackgroundResource(activity.isDarkTheme() ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white);
return view; return view;
} else if (type == RTP_SESSION) {
final boolean isDarkTheme = activity.isDarkTheme();
final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
final long duration = rtpSessionStatus.duration;
if (received) {
if (duration > 0) {
viewHolder.status_message.setText(activity.getString(R.string.incoming_call_duration, TimeframeUtils.resolve(activity,duration)));
} else {
viewHolder.status_message.setText(R.string.incoming_call);
}
} else {
if (duration > 0) {
viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_duration, TimeframeUtils.resolve(activity,duration)));
} else {
viewHolder.status_message.setText(R.string.outgoing_call);
}
}
viewHolder.indicatorReceived.setImageResource(RtpSessionStatus.getDrawable(received,rtpSessionStatus.successful,isDarkTheme));
viewHolder.indicatorReceived.setAlpha(isDarkTheme ? 0.7f : 0.57f);
viewHolder.message_box.setBackgroundResource(isDarkTheme ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white);
return view;
} else if (type == STATUS) { } else if (type == STATUS) {
if ("LOAD_MORE".equals(message.getBody())) { if ("LOAD_MORE".equals(message.getBody())) {
viewHolder.status_message.setVisibility(View.GONE); viewHolder.status_message.setVisibility(View.GONE);

View file

@ -299,6 +299,8 @@ public class UIHelper {
return new Pair<>(context.getString(R.string.omemo_decryption_failed), true); return new Pair<>(context.getString(R.string.omemo_decryption_failed), true);
} else if (message.isFileOrImage()) { } else if (message.isFileOrImage()) {
return new Pair<>(getFileDescriptionString(context, message), true); return new Pair<>(getFileDescriptionString(context, message), true);
} else if (message.getType() == Message.TYPE_RTP_SESSION) {
return new Pair<>(context.getString(message.getStatus() == Message.STATUS_RECEIVED ? R.string.incoming_call : R.string.outgoing_call), true);
} else { } else {
final String body = MessageUtils.filterLtrRtl(message.getBody()); final String body = MessageUtils.filterLtrRtl(message.getBody());
if (body.startsWith(Message.ME_COMMAND)) { if (body.startsWith(Message.ME_COMMAND)) {

View file

@ -1,5 +1,6 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import com.google.common.base.Strings; import com.google.common.base.Strings;
@ -18,6 +19,10 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
@ -94,13 +99,27 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
private final ArrayDeque<IceCandidate> pendingIceCandidates = new ArrayDeque<>(); private final ArrayDeque<IceCandidate> pendingIceCandidates = new ArrayDeque<>();
private final Message message;
private State state = State.NULL; private State state = State.NULL;
private RtpContentMap initiatorRtpContentMap; private RtpContentMap initiatorRtpContentMap;
private RtpContentMap responderRtpContentMap; private RtpContentMap responderRtpContentMap;
private long rtpConnectionStarted = 0; //time of 'connected'
JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
super(jingleConnectionManager, id, initiator); super(jingleConnectionManager, id, initiator);
final Conversation conversation = jingleConnectionManager.getXmppConnectionService().findOrCreateConversation(
id.account,
id.with.asBareJid(),
false,
false
);
this.message = new Message(
conversation,
isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED,
Message.TYPE_RTP_SESSION,
id.sessionId
);
} }
private static State reasonToState(Reason reason) { private static State reasonToState(Reason reason) {
@ -153,7 +172,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return; return;
} }
webRTCWrapper.close(); webRTCWrapper.close();
transitionOrThrow(reasonToState(wrapper.reason)); final State target = reasonToState(wrapper.reason);
transitionOrThrow(target);
writeLogMessage(target);
if (previous == State.PROPOSED || previous == State.SESSION_INITIALIZED) { if (previous == State.PROPOSED || previous == State.SESSION_INITIALIZED) {
xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
} }
@ -455,7 +476,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
if (transition(State.RETRACTED)) { if (transition(State.RETRACTED)) {
xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted");
//TODO create missed call notification/message writeLogMessageMissed();
jingleConnectionManager.finishConnection(this); jingleConnectionManager.finishConnection(this);
} else { } else {
Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state); Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state);
@ -509,6 +530,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void sendSessionTerminate(final Reason reason, final String text) { private void sendSessionTerminate(final Reason reason, final String text) {
final State target = reasonToState(reason); final State target = reasonToState(reason);
transitionOrThrow(target); transitionOrThrow(target);
writeLogMessage(target);
final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
jinglePacket.setReason(reason, text); jinglePacket.setReason(reason, text);
send(jinglePacket); send(jinglePacket);
@ -773,6 +795,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState);
updateEndUserState(); updateEndUserState();
if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) {
this.rtpConnectionStarted = SystemClock.elapsedRealtime();
}
if (newState == PeerConnection.PeerConnectionState.FAILED) { if (newState == PeerConnection.PeerConnectionState.FAILED) {
if (TERMINATED.contains(this.state)) { if (TERMINATED.contains(this.state)) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state);
@ -850,6 +875,37 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
} }
private void writeLogMessage(final State state) {
final long started = this.rtpConnectionStarted;
long duration = started <= 0 ? 0 : SystemClock.elapsedRealtime() - started;
if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) {
writeLogMessageSuccess(duration);
} else {
writeLogMessageMissed();
}
}
private void writeLogMessageSuccess(final long duration) {
this.message.setBody(new RtpSessionStatus(true, duration).toString());
this.writeMessage();
}
private void writeLogMessageMissed() {
this.message.setBody(new RtpSessionStatus(false,0).toString());
this.writeMessage();
}
private void writeMessage() {
final Conversational conversational = message.getConversation();
if (conversational instanceof Conversation) {
((Conversation) conversational).add(this.message);
xmppConnectionService.databaseBackend.createMessage(message);
xmppConnectionService.updateConversationUi();
} else {
throw new IllegalStateException("Somehow the conversation in a message was a stub");
}
}
public State getState() { public State getState() {
return this.state; return this.state;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

View file

@ -1,24 +1,27 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="5dp"
android:paddingLeft="8dp" android:paddingLeft="8dp"
android:paddingTop="5dp"
android:paddingRight="8dp" android:paddingRight="8dp"
android:paddingTop="5dp"> android:paddingBottom="5dp">
<LinearLayout <LinearLayout
android:id="@+id/message_box"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/date_bubble_white" android:layout_centerHorizontal="true"
android:id="@+id/message_box" android:background="@drawable/date_bubble_white">
android:layout_centerHorizontal="true">
<TextView <TextView
android:id="@+id/message_body"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Conversations.Body1.Secondary" android:textAppearance="@style/TextAppearance.Conversations.Body1.Secondary"
android:id="@+id/message_body" /> tools:text="Yesterday" />
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingTop="5dp"
android:paddingRight="8dp"
android:paddingBottom="5dp">
<LinearLayout
android:id="@+id/message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:background="@drawable/date_bubble_white"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/indicator_received"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="4sp"
android:layout_marginLeft="0sp"
tools:alpha="0.57"
tools:src="@drawable/ic_call_received_black_18dp" />
<TextView
android:id="@+id/message_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/incoming_call"
android:textAppearance="@style/TextAppearance.Conversations.Body1.Secondary" />
</LinearLayout>
</RelativeLayout>

View file

@ -903,6 +903,10 @@
<string name="hang_up">Hang up</string> <string name="hang_up">Hang up</string>
<string name="ongoing_call">Ongoing call</string> <string name="ongoing_call">Ongoing call</string>
<string name="disable_tor_to_make_call">Disable Tor to make calls</string> <string name="disable_tor_to_make_call">Disable Tor to make calls</string>
<string name="incoming_call">Incoming call</string>
<string name="incoming_call_duration">Incoming call · %s</string>
<string name="outgoing_call">Outgoing call</string>
<string name="outgoing_call_duration">Outgoing call · %s</string>
<plurals name="view_users"> <plurals name="view_users">
<item quantity="one">View %1$d Participant</item> <item quantity="one">View %1$d Participant</item>
<item quantity="other">View %1$d Participants</item> <item quantity="other">View %1$d Participants</item>