/* * Copyright 2016 OpenMarket Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.matrix.androidsdk.util; import android.content.Context; import android.graphics.Typeface; import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import org.matrix.androidsdk.util.Log; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.matrix.androidsdk.R; import org.matrix.androidsdk.call.MXCallsManager; import org.matrix.androidsdk.crypto.MXCryptoError; import org.matrix.androidsdk.data.RoomState; import org.matrix.androidsdk.rest.model.Event; import org.matrix.androidsdk.rest.model.EventContent; import org.matrix.androidsdk.rest.model.Message; import org.matrix.androidsdk.rest.model.RedactedBecause; import org.matrix.androidsdk.rest.model.RoomMember; import org.matrix.androidsdk.rest.model.RoomThirdPartyInvite; /** * Class helper to stringify an event */ public class EventDisplay { private static final String LOG_TAG = "EventDisplay"; // members private final Event mEvent; private final Context mContext; private final RoomState mRoomState; private boolean mPrependAuthor; // let the application defines if the redacted events must be displayed public static boolean mDisplayRedactedEvents = false; // constructor public EventDisplay(Context context, Event event, RoomState roomState) { mContext = context.getApplicationContext(); mEvent = event; mRoomState = roomState; } /** * <p>Prepend the text with the author's name if they have not been mentioned in the text.</p> * This will prepend text messages with the author's name. This will NOT prepend things like * emote, room topic changes, etc which already mention the author's name in the message. * @param prepend true to prepend the message author. */ public void setPrependMessagesWithAuthor(boolean prepend) { mPrependAuthor = prepend; } /** * Compute an "human readable" name for an user Id. * @param userId the user id * @param roomState the room state * @return the user display name */ private static String getUserDisplayName(String userId, RoomState roomState) { if (null != roomState) { return roomState.getMemberName(userId); } else { return userId; } } /** * Stringify the linked event. * @return The text or null if it isn't possible. */ public CharSequence getTextualDisplay() { return getTextualDisplay(null); } /** * Stringify the linked event. * @param displayNameColor the display name highlighted color. * @return The text or null if it isn't possible. */ public CharSequence getTextualDisplay(Integer displayNameColor) { CharSequence text = null; try { JsonObject jsonEventContent = mEvent.getContentAsJsonObject(); String userDisplayName = getUserDisplayName(mEvent.getSender(), mRoomState); String eventType = mEvent.getType(); if (mEvent.isCallEvent()) { if (Event.EVENT_TYPE_CALL_INVITE.equals(eventType)) { boolean isVideo = false; // detect call type from the sdp try { JsonObject offer = jsonEventContent.get("offer").getAsJsonObject(); JsonElement sdp = offer.get("sdp"); String sdpValue = sdp.getAsString(); isVideo = sdpValue.contains("m=video"); } catch (Exception e) { Log.e(LOG_TAG, "getTextualDisplay : " + e.getLocalizedMessage()); } if (isVideo) { return mContext.getString(R.string.notice_placed_video_call, userDisplayName); } else { return mContext.getString(R.string.notice_placed_voice_call, userDisplayName); } } else if (Event.EVENT_TYPE_CALL_ANSWER.equals(eventType)) { return mContext.getString(R.string.notice_answered_call, userDisplayName); } else if (Event.EVENT_TYPE_CALL_HANGUP.equals(eventType)) { return mContext.getString(R.string.notice_ended_call, userDisplayName); } else { return eventType; } } else if (Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY.equals(eventType)) { CharSequence subpart; String historyVisibility = (null != jsonEventContent.get("history_visibility")) ? jsonEventContent.get("history_visibility").getAsString() : RoomState.HISTORY_VISIBILITY_SHARED; if (TextUtils.equals(historyVisibility, RoomState.HISTORY_VISIBILITY_SHARED)) { subpart = mContext.getString(R.string.notice_room_visibility_shared); } else if (TextUtils.equals(historyVisibility, RoomState.HISTORY_VISIBILITY_INVITED)) { subpart = mContext.getString(R.string.notice_room_visibility_invited); } else if (TextUtils.equals(historyVisibility, RoomState.HISTORY_VISIBILITY_JOINED)) { subpart = mContext.getString(R.string.notice_room_visibility_joined); } else if (TextUtils.equals(historyVisibility, RoomState.HISTORY_VISIBILITY_WORLD_READABLE)) { subpart = mContext.getString(R.string.notice_room_visibility_world_readable); } else { subpart = mContext.getString(R.string.notice_room_visibility_unknown, historyVisibility); } text = mContext.getString(R.string.notice_made_future_room_visibility, userDisplayName, subpart); } else if (Event.EVENT_TYPE_RECEIPT.equals(eventType)) { // the read receipt should not be displayed text = "Read Receipt"; } else if (Event.EVENT_TYPE_MESSAGE.equals(eventType)) { String msgtype = (null != jsonEventContent.get("msgtype")) ? jsonEventContent.get("msgtype").getAsString() : ""; // all m.room.message events should support the 'body' key fallback, so use it. text = jsonEventContent.get("body") == null ? null : jsonEventContent.get("body").getAsString(); // check for html formatting if (jsonEventContent.has("formatted_body") && jsonEventContent.has("format")) { String format = jsonEventContent.getAsJsonPrimitive("format").getAsString(); if (Message.FORMAT_MATRIX_HTML.equals(format)) { String htmlBody = jsonEventContent.getAsJsonPrimitive("formatted_body").getAsString(); // some markers are not supported so fallback on an ascii display until to find the right way to manage them // an issue has been created https://github.com/vector-im/vector-android/issues/38 if (!TextUtils.isEmpty(htmlBody) && !htmlBody.contains("<ol>") && !htmlBody.contains("<li>")) { text = Html.fromHtml(jsonEventContent.getAsJsonPrimitive("formatted_body").getAsString()); } } } // avoid empty image name if (TextUtils.equals(msgtype, Message.MSGTYPE_IMAGE) && TextUtils.isEmpty(text)) { text = mContext.getString(R.string.summary_user_sent_image, userDisplayName); } else if (TextUtils.equals(msgtype, Message.MSGTYPE_EMOTE)) { text = "* " + userDisplayName + " " + text; } else if (TextUtils.isEmpty(text)) { text = ""; } else if (mPrependAuthor) { text = new SpannableStringBuilder(mContext.getString(R.string.summary_message, userDisplayName, text)); if (null != displayNameColor) { ((SpannableStringBuilder)text).setSpan(new ForegroundColorSpan(displayNameColor), 0, userDisplayName.length()+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ((SpannableStringBuilder)text).setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, userDisplayName.length()+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } else if (Event.EVENT_TYPE_MESSAGE_ENCRYPTION.equals(eventType)) { text = mContext.getString(R.string.notice_end_to_end, userDisplayName, mEvent.getWireEventContent().algorithm); } else if (Event.EVENT_TYPE_MESSAGE_ENCRYPTED.equals(eventType)) { // don't display if (mEvent.isRedacted()) { String redactedInfo = EventDisplay.getRedactionMessage(mContext, mEvent, mRoomState); if (TextUtils.isEmpty(redactedInfo)) { return null; } else { return redactedInfo; } } else { String message = null; if (null != mEvent.getCryptoError()) { String errorDescription; MXCryptoError error = mEvent.getCryptoError(); if (TextUtils.equals(error.errcode, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE)) { errorDescription = mContext.getResources().getString(R.string.notice_crypto_error_unkwown_inbound_session_id); } else { errorDescription = error.getLocalizedMessage(); } message = mContext.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription); } if (TextUtils.isEmpty(message)) { message = mContext.getString(R.string.encrypted_message); } SpannableString spannableStr = new SpannableString(message); spannableStr.setSpan(new android.text.style.StyleSpan(Typeface.ITALIC), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); text = spannableStr; } } else if (Event.EVENT_TYPE_STATE_ROOM_TOPIC.equals(eventType)) { String topic = jsonEventContent.getAsJsonPrimitive("topic").getAsString(); if (mEvent.isRedacted()) { String redactedInfo = EventDisplay.getRedactionMessage(mContext, mEvent, mRoomState); if (TextUtils.isEmpty(redactedInfo)) { return null; } topic = redactedInfo; } if (!TextUtils.isEmpty(topic)) { text = mContext.getString(R.string.notice_topic_changed, userDisplayName, topic); } else { text = mContext.getString(R.string.notice_room_topic_removed, userDisplayName); } } else if (Event.EVENT_TYPE_STATE_ROOM_NAME.equals(eventType)) { String roomName = jsonEventContent.getAsJsonPrimitive("name").getAsString(); if (mEvent.isRedacted()) { String redactedInfo = EventDisplay.getRedactionMessage(mContext, mEvent, mRoomState); if (TextUtils.isEmpty(redactedInfo)) { return null; } roomName = redactedInfo; } if (!TextUtils.isEmpty(roomName)) { text = mContext.getString(R.string.notice_room_name_changed, userDisplayName, roomName); } else { text = mContext.getString(R.string.notice_room_name_removed, userDisplayName); } } else if (Event.EVENT_TYPE_STATE_ROOM_THIRD_PARTY_INVITE.equals(eventType)) { RoomThirdPartyInvite invite = JsonUtils.toRoomThirdPartyInvite(mEvent.getContent()); String displayName = invite.display_name; if (mEvent.isRedacted()) { String redactedInfo = EventDisplay.getRedactionMessage(mContext, mEvent, mRoomState); if (TextUtils.isEmpty(redactedInfo)) { return null; } displayName = redactedInfo; } text = mContext.getString(R.string.notice_room_third_party_invite, userDisplayName, displayName); } else if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(eventType)) { text = getMembershipNotice(mContext, mEvent, mRoomState); } } catch (Exception e) { Log.e(LOG_TAG, "getTextualDisplay() " + e.getLocalizedMessage()); } return text; } /** * Compute the redact text for an event. * @param context the context * @param event the event * @param roomState the room state * @return the redacted event text */ public static String getRedactionMessage(Context context, Event event, RoomState roomState) { // test if the redacted event must be displayed. if (!mDisplayRedactedEvents) { return null; } // Check first whether the event has been redacted String redactedInfo = null; if (event.isRedacted() && (null != roomState)) { RedactedBecause redactedBecause = event.unsigned.redacted_because; String redactedBy = redactedBecause.sender; String redactedReason = null; if (null != redactedBecause.content) { redactedReason = redactedBecause.content.reason; } if (!TextUtils.isEmpty(redactedReason)) { if (!TextUtils.isEmpty(redactedBy)) { redactedBy = context.getString(R.string.notice_event_redacted_by, redactedBy) + context.getString(R.string.notice_event_redacted_reason, redactedReason); } else { redactedBy = context.getString(R.string.notice_event_redacted_reason, redactedReason); } } else if (!TextUtils.isEmpty(redactedBy)) { redactedBy = context.getString(R.string.notice_event_redacted_by, redactedBy); } redactedInfo = context.getString(R.string.notice_event_redacted, redactedBy); } return redactedInfo; } /** * Compute the sender display name * @param event the event * @param eventContent the event content * @param roomState the room state * @return the "human readable" display name */ private static String senderDisplayNameForEvent(Event event, EventContent eventContent, EventContent prevEventContent, RoomState roomState) { String senderDisplayName = event.getSender(); if (!event.isRedacted()) { if (null != roomState) { // Consider first the current display name defined in provided room state (Note: this room state is supposed to not take the new event into account) senderDisplayName = roomState.getMemberName(event.getSender()); } // Check whether this sender name is updated by the current event (This happens in case of new joined member) if ((null != eventContent) && TextUtils.equals(RoomMember.MEMBERSHIP_JOIN, eventContent.membership)) { // detect if it is displayname update // a display name update is detected when the previous state was join and there was a displayname if (!TextUtils.isEmpty(eventContent.displayname) || ((null != prevEventContent) && TextUtils.equals(RoomMember.MEMBERSHIP_JOIN, prevEventContent.membership) && !TextUtils.isEmpty(prevEventContent.displayname)) ) { senderDisplayName = eventContent.displayname; } } } return senderDisplayName; } /** * Build a membership notice text from its dedicated event. * @param context the context. * @param event the event. * @param roomState the room state. * @return the membership text. */ public static String getMembershipNotice(Context context, Event event, RoomState roomState) { JsonObject content = event.getContentAsJsonObject(); // don't support redacted membership event if ((null == content) || (content.entrySet().size() == 0)) { return null; } EventContent eventContent = JsonUtils.toEventContent(event.getContentAsJsonObject()); EventContent prevEventContent = event.getPrevContent(); String senderDisplayName = senderDisplayNameForEvent(event, eventContent, prevEventContent, roomState); String prevUserDisplayName = null; String prevMembership = null; if (null != prevEventContent) { prevMembership = prevEventContent.membership; } if ((null != prevEventContent)) { prevUserDisplayName = prevEventContent.displayname; } // use by default the provided display name String targetDisplayName = eventContent.displayname; // if it is not provided, use the stateKey value // and try to retrieve a valid display name if (null == targetDisplayName) { targetDisplayName = event.stateKey; if ((null != targetDisplayName) && (null != roomState) && !event.isRedacted()) { targetDisplayName = roomState.getMemberName(targetDisplayName); } } // Check whether the sender has updated his profile (the membership is then unchanged) if (TextUtils.equals(prevMembership, eventContent.membership)) { String redactedInfo = EventDisplay.getRedactionMessage(context, event, roomState); // Is redacted event? if (event.isRedacted()) { // Here the event is ignored (no display) if (null == redactedInfo) { return null; } return context.getString(R.string.notice_profile_change_redacted, senderDisplayName, redactedInfo); } else { String displayText = ""; if (!TextUtils.equals(senderDisplayName, prevUserDisplayName)) { if (TextUtils.isEmpty(prevUserDisplayName)) { if (!TextUtils.equals(event.getSender(), senderDisplayName)) { displayText = context.getString(R.string.notice_display_name_set, event.getSender(), senderDisplayName); } } else if (TextUtils.isEmpty(senderDisplayName)) { displayText = context.getString(R.string.notice_display_name_removed, event.getSender(), prevUserDisplayName); } else { displayText = context.getString(R.string.notice_display_name_changed_from, event.getSender(), prevUserDisplayName, senderDisplayName); } } // Check whether the avatar has been changed String avatar = eventContent.avatar_url; String prevAvatar = null; if (null != prevEventContent) { prevAvatar = prevEventContent.avatar_url; } if (!TextUtils.equals(prevAvatar, avatar)) { if (!TextUtils.isEmpty(displayText)) { displayText = displayText + " " + context.getString(R.string.notice_avatar_changed_too); } else { displayText = context.getString(R.string.notice_avatar_url_changed, senderDisplayName); } } return displayText; } } else if (RoomMember.MEMBERSHIP_INVITE.equals(eventContent.membership)) { if (null != eventContent.third_party_invite) { return context.getString(R.string.notice_room_third_party_registered_invite, targetDisplayName, eventContent.third_party_invite.display_name); } else { String selfUserId = null; if ((null != roomState) && (null != roomState.getDataHandler())) { selfUserId = roomState.getDataHandler().getUserId(); } if (TextUtils.equals(event.stateKey, selfUserId)) { return context.getString(R.string.notice_room_invite_you, senderDisplayName); } if (null == event.stateKey) { return context.getString(R.string.notice_room_invite_no_invitee, senderDisplayName); } // conference call case if (targetDisplayName.equals(MXCallsManager.getConferenceUserId(event.roomId))) { return context.getString(R.string.notice_requested_voip_conference, senderDisplayName); } return context.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName); } } else if (RoomMember.MEMBERSHIP_JOIN.equals(eventContent.membership)) { // conference call case if (TextUtils.equals(event.sender, MXCallsManager.getConferenceUserId(event.roomId))) { return context.getString(R.string.notice_voip_started); } return context.getString(R.string.notice_room_join, senderDisplayName); } else if (RoomMember.MEMBERSHIP_LEAVE.equals(eventContent.membership)) { // conference call case if (TextUtils.equals(event.sender, MXCallsManager.getConferenceUserId(event.roomId))) { return context.getString(R.string.notice_voip_finished); } // 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked if (TextUtils.equals(event.getSender(), event.stateKey)) { if ((null != prevEventContent) && TextUtils.equals(prevEventContent.membership, RoomMember.MEMBERSHIP_INVITE)) { return context.getString(R.string.notice_room_reject, senderDisplayName); } else { // use the latest known displayname if ((null == eventContent.displayname) && (null != prevUserDisplayName)) { senderDisplayName = prevUserDisplayName; } return context.getString(R.string.notice_room_leave, senderDisplayName); } } else if (null != prevMembership) { if (prevMembership.equals(RoomMember.MEMBERSHIP_INVITE)) { return context.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName); } else if (prevMembership.equals(RoomMember.MEMBERSHIP_JOIN)) { return context.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName); } else if (prevMembership.equals(RoomMember.MEMBERSHIP_BAN)) { return context.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName); } } } else if (RoomMember.MEMBERSHIP_BAN.equals(eventContent.membership)) { return context.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName); } else { // eh? Log.e(LOG_TAG, "Unknown membership: " + eventContent.membership); } return null; } }