/*
* Copyright (c) 2010-2019 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.linphone.chat;
import android.content.ContentValues;
import android.content.Context;
import android.provider.MediaStore;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.linphone.R;
import org.linphone.contacts.ContactsManager;
import org.linphone.contacts.LinphoneContact;
import org.linphone.core.Address;
import org.linphone.core.ChatMessage;
import org.linphone.core.ChatMessageListenerStub;
import org.linphone.core.Content;
import org.linphone.core.EventLog;
import org.linphone.core.tools.Log;
import org.linphone.settings.LinphonePreferences;
import org.linphone.utils.FileUtils;
import org.linphone.utils.LinphoneUtils;
import org.linphone.utils.SelectableAdapter;
import org.linphone.utils.SelectableHelper;
public class ChatMessagesAdapter extends SelectableAdapter
implements ChatMessagesGenericAdapter {
private static final int MAX_TIME_TO_GROUP_MESSAGES = 300; // 5 minutes
private final Context mContext;
private List mHistory;
private List mParticipants;
private final int mItemResource;
private final ChatMessagesFragment mFragment;
private final List mTransientMessages;
private final ChatMessageViewHolderClickListener mClickListener;
private final ChatMessageListenerStub mListener;
public ChatMessagesAdapter(
ChatMessagesFragment fragment,
SelectableHelper helper,
int itemResource,
EventLog[] history,
ArrayList participants,
ChatMessageViewHolderClickListener clickListener) {
super(helper);
mFragment = fragment;
mContext = mFragment.getActivity();
mItemResource = itemResource;
mHistory = new ArrayList<>(Arrays.asList(history));
Collections.reverse(mHistory);
mParticipants = participants;
mClickListener = clickListener;
mTransientMessages = new ArrayList<>();
mListener =
new ChatMessageListenerStub() {
@Override
public void onMsgStateChanged(ChatMessage message, ChatMessage.State state) {
ChatMessageViewHolder holder =
(ChatMessageViewHolder) message.getUserData();
if (holder != null) {
int position = holder.getAdapterPosition();
if (position >= 0) {
notifyItemChanged(position);
} else {
notifyDataSetChanged();
}
} else {
// Just in case, better to refresh the whole view than to miss
// an update
notifyDataSetChanged();
}
if (state == ChatMessage.State.Displayed) {
mTransientMessages.remove(message);
} else if (state == ChatMessage.State.FileTransferDone) {
Log.i("[Chat Message] File transfer done");
// Do not do it for ephemeral messages of if setting is disabled
if (!message.isEphemeral()
&& LinphonePreferences.instance()
.makeDownloadedImagesVisibleInNativeGallery()) {
for (Content content : message.getContents()) {
if (content.isFile() && content.getFilePath() != null) {
addImageToNativeGalery(content.getFilePath());
}
}
}
}
}
};
}
private void addImageToNativeGalery(String filePath) {
if (!FileUtils.isExtensionImage(filePath)) return;
String mime = FileUtils.getMimeFromFile(filePath);
Log.i("[Chat Message] Adding file ", filePath, " to native gallery with MIME ", mime);
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, filePath);
values.put(MediaStore.Images.Media.MIME_TYPE, mime);
mContext.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}
@Override
public ChatMessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(mItemResource, parent, false);
ChatMessageViewHolder VH = new ChatMessageViewHolder(mContext, v, mClickListener);
// Allows onLongClick ContextMenu on bubbles
mFragment.registerForContextMenu(v);
v.setTag(VH);
return VH;
}
@Override
public void onBindViewHolder(@NonNull ChatMessageViewHolder holder, int position) {
if (position < 0) return;
EventLog event = mHistory.get(position);
holder.delete.setVisibility(View.GONE);
holder.eventLayout.setVisibility(View.GONE);
holder.securityEventLayout.setVisibility(View.GONE);
holder.rightAnchor.setVisibility(View.GONE);
holder.bubbleLayout.setVisibility(View.GONE);
holder.sendInProgress.setVisibility(View.GONE);
holder.isEditionEnabled = isEditionEnabled();
if (isEditionEnabled()) {
holder.delete.setVisibility(View.VISIBLE);
holder.delete.setChecked(isSelected(position));
holder.delete.setTag(position);
}
if (event.getType() == EventLog.Type.ConferenceChatMessage) {
ChatMessage message = event.getChatMessage();
if ((message.isOutgoing() && message.getState() != ChatMessage.State.Displayed)
|| (!message.isOutgoing() && message.isFileTransfer())) {
if (!mTransientMessages.contains(message)) {
mTransientMessages.add(message);
}
// This only works if JAVA object is kept, hence the transient list
message.setUserData(holder);
message.addListener(mListener);
}
LinphoneContact contact = null;
Address remoteSender = message.getFromAddress();
if (!message.isOutgoing()) {
for (LinphoneContact c : mParticipants) {
if (c != null && c.hasAddress(remoteSender.asStringUriOnly())) {
contact = c;
break;
}
}
}
holder.bindMessage(message, contact);
changeBackgroundDependingOnPreviousAndNextEvents(message, holder, position);
} else { // Event is not chat message
Address address = event.getParticipantAddress();
if (address == null && event.getType() == EventLog.Type.ConferenceSecurityEvent) {
address = event.getSecurityEventFaultyDeviceAddress();
}
String displayName = "";
if (address != null) {
LinphoneContact contact =
ContactsManager.getInstance().findContactFromAddress(address);
if (contact != null) {
displayName = contact.getFullName();
} else {
displayName = LinphoneUtils.getAddressDisplayName(address);
}
}
switch (event.getType()) {
case ConferenceCreated:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(mContext.getString(R.string.conference_created));
break;
case ConferenceTerminated:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(mContext.getString(R.string.conference_destroyed));
break;
case ConferenceParticipantAdded:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.participant_added)
.replace("%s", displayName));
break;
case ConferenceParticipantRemoved:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.participant_removed)
.replace("%s", displayName));
break;
case ConferenceSubjectChanged:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.subject_changed)
.replace("%s", event.getSubject()));
break;
case ConferenceParticipantSetAdmin:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.admin_set).replace("%s", displayName));
break;
case ConferenceParticipantUnsetAdmin:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.admin_unset).replace("%s", displayName));
break;
case ConferenceParticipantDeviceAdded:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.device_added).replace("%s", displayName));
break;
case ConferenceParticipantDeviceRemoved:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.device_removed).replace("%s", displayName));
break;
case ConferenceEphemeralMessageDisabled:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.chat_event_ephemeral_disabled));
break;
case ConferenceEphemeralMessageEnabled:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.chat_event_ephemeral_enabled)
.replace(
"%s",
formatEphemeralExpiration(
event.getEphemeralMessageLifetime())));
break;
case ConferenceEphemeralMessageLifetimeChanged:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.chat_event_ephemeral_lifetime_changed)
.replace(
"%s",
formatEphemeralExpiration(
event.getEphemeralMessageLifetime())));
break;
case ConferenceSecurityEvent:
holder.securityEventLayout.setVisibility(View.VISIBLE);
switch (event.getSecurityEventType()) {
case EncryptionIdentityKeyChanged:
holder.securityEventMessage.setText(
mContext.getString(R.string.lime_identity_key_changed)
.replace("%s", displayName));
break;
case ManInTheMiddleDetected:
holder.securityEventMessage.setText(
mContext.getString(R.string.man_in_the_middle_detected)
.replace("%s", displayName));
break;
case SecurityLevelDowngraded:
holder.securityEventMessage.setText(
mContext.getString(R.string.security_level_downgraded)
.replace("%s", displayName));
break;
case ParticipantMaxDeviceCountExceeded:
holder.securityEventMessage.setText(
mContext.getString(R.string.participant_max_count_exceeded)
.replace("%s", displayName));
break;
case None:
default:
break;
}
break;
case None:
default:
holder.eventLayout.setVisibility(View.VISIBLE);
holder.eventMessage.setText(
mContext.getString(R.string.unexpected_event)
.replace("%s", displayName)
.replace("%i", String.valueOf(event.getType().toInt())));
break;
}
}
}
private String formatEphemeralExpiration(long duration) {
if (duration == 0) {
return mContext.getString(R.string.chat_room_ephemeral_message_disabled);
} else if (duration == 60) {
return mContext.getString(R.string.chat_room_ephemeral_message_one_minute);
} else if (duration == 3600) {
return mContext.getString(R.string.chat_room_ephemeral_message_one_hour);
} else if (duration == 86400) {
return mContext.getString(R.string.chat_room_ephemeral_message_one_day);
} else if (duration == 259200) {
return mContext.getString(R.string.chat_room_ephemeral_message_three_days);
} else if (duration == 604800) {
return mContext.getString(R.string.chat_room_ephemeral_message_one_week);
} else {
return "Unexpected duration";
}
}
@Override
public int getItemCount() {
return mHistory.size();
}
public void addToHistory(EventLog log) {
mHistory.add(0, log);
notifyItemInserted(0);
notifyItemChanged(1); // Update second to last item just in case for grouping purposes
}
public void addAllToHistory(ArrayList logs) {
int currentSize = mHistory.size() - 1;
Collections.reverse(logs);
mHistory.addAll(logs);
notifyItemRangeInserted(currentSize + 1, logs.size());
}
public void setContacts(ArrayList participants) {
mParticipants = participants;
}
public void refresh(EventLog[] history) {
mHistory = new ArrayList<>(Arrays.asList(history));
Collections.reverse(mHistory);
notifyDataSetChanged();
}
public void clear() {
for (EventLog event : mHistory) {
if (event.getType() == EventLog.Type.ConferenceChatMessage) {
ChatMessage message = event.getChatMessage();
message.removeListener(mListener);
}
}
mTransientMessages.clear();
mHistory.clear();
}
public Object getItem(int i) {
return mHistory.get(i);
}
public void removeItem(int i) {
mHistory.remove(i);
notifyItemRemoved(i);
}
@Override
public boolean removeFromHistory(EventLog eventLog) {
int index = mHistory.indexOf(eventLog);
if (index >= 0) {
removeItem(index);
return true;
}
return false;
}
private void changeBackgroundDependingOnPreviousAndNextEvents(
ChatMessage message, ChatMessageViewHolder holder, int position) {
boolean hasPrevious = false, hasNext = false;
// Do not forget history is reversed, so previous in order is next in list display and
// chronology !
if (position > 0
&& mContext.getResources()
.getBoolean(R.bool.lower_space_between_chat_bubbles_if_same_person)) {
EventLog previousEvent = (EventLog) getItem(position - 1);
if (previousEvent.getType() == EventLog.Type.ConferenceChatMessage) {
ChatMessage previousMessage = previousEvent.getChatMessage();
if (previousMessage.getFromAddress().weakEqual(message.getFromAddress())) {
if (previousMessage.getTime() - message.getTime()
< MAX_TIME_TO_GROUP_MESSAGES) {
hasPrevious = true;
}
}
}
}
if (position >= 0
&& position < mHistory.size() - 1
&& mContext.getResources()
.getBoolean(R.bool.lower_space_between_chat_bubbles_if_same_person)) {
EventLog nextEvent = (EventLog) getItem(position + 1);
if (nextEvent.getType() == EventLog.Type.ConferenceChatMessage) {
ChatMessage nextMessage = nextEvent.getChatMessage();
if (nextMessage.getFromAddress().weakEqual(message.getFromAddress())) {
if (message.getTime() - nextMessage.getTime() < MAX_TIME_TO_GROUP_MESSAGES) {
holder.timeText.setVisibility(View.GONE);
if (!message.isOutgoing()) {
holder.avatarLayout.setVisibility(View.INVISIBLE);
}
hasNext = true;
}
}
}
}
if (message.isOutgoing()) {
if (hasNext && hasPrevious) {
holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_split_2);
} else if (hasNext) {
holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_split_3);
} else if (hasPrevious) {
holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_split_1);
} else {
holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_full);
}
} else {
if (hasNext && hasPrevious) {
holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_split_2);
} else if (hasNext) {
holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_split_3);
} else if (hasPrevious) {
holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_split_1);
} else {
holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_full);
}
}
}
}