/*
* Copyright 2015 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.adapters;
import android.content.Context;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import org.matrix.androidsdk.HomeserverConnectionConfig;
import org.matrix.androidsdk.R;
import org.matrix.androidsdk.data.RoomState;
import org.matrix.androidsdk.db.MXMediasCache;
import org.matrix.androidsdk.rest.model.PowerLevels;
import org.matrix.androidsdk.rest.model.RoomMember;
import org.matrix.androidsdk.rest.model.User;
import org.matrix.androidsdk.util.ContentManager;
import org.matrix.androidsdk.view.PieFractionView;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* An adapter which can display m.room.member content.
*/
public abstract class RoomMembersAdapter extends ArrayAdapter<RoomMember> {
// the context
protected Context mContext;
// the layout
private LayoutInflater mLayoutInflater;
private int mLayoutResourceId;
// the power levels information
private PowerLevels mPowerLevels;
private int mMaxPowerLevel;
// the room state
private RoomState mRoomState;
// membership description by MEMBERSHIP_XXX
private HashMap<String, String> mMembershipStrings = new HashMap<>();
// User by user id
private Map<String, User> mUserMap = new HashMap<>();
// sort method
private boolean mSortByLastActive = true;
// display the membership
private boolean mDisplayMembership = true;
// the used medias cache
private MXMediasCache mMediasCache = null;
// member display name by user id
private HashMap<String, String> mMembersSortMemberNameByUserId = new HashMap<>();
// the home server config
private final HomeserverConnectionConfig mHsConfig;
// abstract methods
public abstract int lastSeenTextColor();
public abstract int presenceOfflineColor();
public abstract int presenceOnlineColor();
public abstract int presenceUnavailableColor();
/**
* Construct an adapter which will display a list of room members.
* @param context Activity context
* @param hsConfig teh home server config
* @param layoutResourceId The resource ID of the layout for each item. Must have TextViews with
* the IDs: roomMembersAdapter_name, roomMembersAdapter_membership, and
* an ImageView with the ID avatar_img.
* @param roomState the roomState.
* @param mediasCache the media cache
* @param membershipStrings the membership strings by RoomMember.MEMBERSHIP_XX value
*/
public RoomMembersAdapter(Context context, HomeserverConnectionConfig hsConfig, int layoutResourceId, RoomState roomState, MXMediasCache mediasCache, HashMap<String, String> membershipStrings) {
super(context, layoutResourceId);
mContext = context;
mLayoutResourceId = layoutResourceId;
mLayoutInflater = LayoutInflater.from(mContext);
mRoomState = roomState;
// left the caller manages the refresh
setNotifyOnChange(false);
mMembershipStrings = membershipStrings;
mMediasCache = mediasCache;
mHsConfig = hsConfig;
}
/**
* Set the sort method.
* @param useLastActive true to sort by last active ts, false to sort by alphabetical order
*/
public void sortByLastActivePresence(boolean useLastActive) {
mSortByLastActive = useLastActive;
}
/**
* Tell if the membership must be displayed in the cells
* @param withMembership true to display the membership in the cell
*/
public void displayMembership(boolean withMembership) {
mDisplayMembership = withMembership;
}
/**
* Return the cached member display name by its user id
* @param userId the user id
* @return the display name
*/
private String getCachedMemberName(String userId) {
// sanity check
if (null == userId) {
return null;
}
if (mMembersSortMemberNameByUserId.containsKey(userId)) {
return mMembersSortMemberNameByUserId.get(userId);
}
String memberName = mRoomState.getMemberName(userId);
mMembersSortMemberNameByUserId.put(userId, memberName);
return memberName;
}
// Comparator to order members alphabetically
private Comparator<RoomMember> alphaComparator = new Comparator<RoomMember>() {
@Override
public int compare(RoomMember member1, RoomMember member2) {
String lhs = getCachedMemberName(member1.getUserId());
String rhs = getCachedMemberName(member2.getUserId());
if (member1.membership.equals(member2.membership)) {
if (lhs == null) {
return -1;
} else if (rhs == null) {
return 1;
}
if (lhs.startsWith("@")) {
lhs = lhs.substring(1);
}
if (rhs.startsWith("@")) {
rhs = rhs.substring(1);
}
return String.CASE_INSENSITIVE_ORDER.compare(lhs, rhs);
} else {
// sort by membership
// display the joined members before the other one
if (member1.membership.equals(RoomMember.MEMBERSHIP_JOIN)) {
return -1;
} else if (member2.membership.equals(RoomMember.MEMBERSHIP_JOIN)) {
return +1;
} else if (member1.membership.equals(RoomMember.MEMBERSHIP_INVITE)) {
return -1;
} else if (member2.membership.equals(RoomMember.MEMBERSHIP_INVITE)) {
return +1;
} else if (member1.membership.equals(RoomMember.MEMBERSHIP_LEAVE)) {
return -1;
} else if (member2.membership.equals(RoomMember.MEMBERSHIP_LEAVE)) {
return +1;
}
return String.CASE_INSENSITIVE_ORDER.compare(lhs, rhs);
}
}
};
// Comparator to order members by last active time
private Comparator<RoomMember> lastActiveComparator = new Comparator<RoomMember>() {
@Override
public int compare(RoomMember member1, RoomMember member2) {
if (member1.membership.equals(member2.membership)) {
User lUser = mUserMap.get(member1.getUserId());
User rUser = mUserMap.get(member2.getUserId());
// Null cases
if ((lUser == null) || (lUser.lastActiveAgo == null)) {
if ((rUser == null) || (rUser.lastActiveAgo == null)) {
// Fall back to alphabetical order
return alphaComparator.compare(member1, member2);
}
return 1;
}
if ((rUser == null) || (rUser.lastActiveAgo == null)) {
return -1;
}
// Non-null cases
long lLastActive = lUser.getAbsoluteLastActiveAgo();
long rLastActive = rUser.getAbsoluteLastActiveAgo();
if (lLastActive < rLastActive) return -1;
if (lLastActive > rLastActive) return 1;
// Fall back to alphabetical order
return alphaComparator.compare(member1, member2);
} else {
// sort by membership
// display the joined members before the other one
if (member1.membership.equals(RoomMember.MEMBERSHIP_JOIN)) {
return -1;
} else if (member2.membership.equals(RoomMember.MEMBERSHIP_JOIN)) {
return +1;
} else if (member1.membership.equals(RoomMember.MEMBERSHIP_INVITE)) {
return -1;
} else if (member2.membership.equals(RoomMember.MEMBERSHIP_INVITE)) {
return +1;
} else if (member1.membership.equals(RoomMember.MEMBERSHIP_LEAVE)) {
return -1;
} else if (member2.membership.equals(RoomMember.MEMBERSHIP_LEAVE)) {
return +1;
}
String lhs = getCachedMemberName(member1.getUserId());
String rhs = getCachedMemberName(member2.getUserId());
return String.CASE_INSENSITIVE_ORDER.compare(lhs, rhs);
}
}
};
/**
* Sort the members list with the dedicated sort methods (last presence or alphabetical).
*/
public void sortMembers() {
// create a dictionnary to avoid computing the member name at each sort step.
// mRoomState.getMemberName(userId) can be complex
mMembersSortMemberNameByUserId = new HashMap<>();
if (mSortByLastActive) {
sort(lastActiveComparator);
} else {
sort(alphaComparator);
}
}
/**
* Update the power levels information (to display each a member is administrator...).
* @param powerLevels the new power levels.
*/
public void setPowerLevels(PowerLevels powerLevels) {
mPowerLevels = powerLevels;
if (powerLevels != null) {
// Process power levels to find the max. The display will show power levels as a fraction of this
mMaxPowerLevel = powerLevels.users_default;
Iterator it = powerLevels.users.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> pair = (Map.Entry<String, Integer>) it.next();
if (pair.getValue() > mMaxPowerLevel) mMaxPowerLevel = pair.getValue();
}
}
notifyDataSetChanged();
}
/**
* Store the user in the known users dictionnary.
* @param user the user to add.
* @return true if the user is added.
*/
public boolean saveUser(User user) {
if (user != null) {
if(!mUserMap.containsKey(user.user_id)) {
mUserMap.put(user.user_id, user);
return true;
}
}
return false;
}
/**
* Delete an user from the known users list.
* @param user the user to remove
*/
public void deleteUser(User user) {
if (user != null) {
if(mUserMap.containsKey(user.user_id)) {
mUserMap.remove(user.user_id);
}
}
}
/**
* Update the user information of a known user.
* @param userId the user id
* @param member its new room memeber definition.
*/
public void updateMember(String userId, RoomMember member) {
for (int i = 0; i < getCount(); i++) {
RoomMember m = getItem(i);
if (userId.equals(m.getUserId())) {
// Copy members
m.displayname = member.displayname;
m.avatarUrl = member.avatarUrl;
m.membership = member.membership;
notifyDataSetChanged();
break;
}
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mLayoutInflater.inflate(mLayoutResourceId, parent, false);
}
RoomMember member = getItem(position);
User user = mUserMap.get(member.getUserId());
// Member name and last seen time
TextView textView = (TextView) convertView.findViewById(R.id.roomMembersAdapter_name);
if ((user == null) || (user.lastActiveAgo == null)) {
textView.setText(member.getName());
}
else {
String memberName = member.getName();
String lastActiveDisplay = "(" + buildLastActiveDisplay(mContext, user.getAbsoluteLastActiveAgo()) + ")";
SpannableStringBuilder ssb = new SpannableStringBuilder(memberName + " " + lastActiveDisplay);
int lastSeenTextColor = lastSeenTextColor();
ssb.setSpan(new ForegroundColorSpan(lastSeenTextColor), memberName.length(), ssb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(ssb);
}
textView = (TextView) convertView.findViewById(R.id.roomMembersAdapter_membership);
if ((user != null) && User.PRESENCE_OFFLINE.equals(user.presence) && !RoomMember.MEMBERSHIP_LEAVE.equals(member.membership) && !RoomMember.MEMBERSHIP_BAN.equals(member.membership)) {
textView.setText(User.PRESENCE_OFFLINE);
textView.setTextColor(presenceOfflineColor());
textView.setVisibility(View.VISIBLE);
} else {
textView.setText(mMembershipStrings.get(member.membership));
textView.setTextColor(Color.BLACK);
textView.setVisibility((mDisplayMembership || RoomMember.MEMBERSHIP_INVITE.equals(member.membership)) ? View.VISIBLE : View.GONE);
}
textView = (TextView) convertView.findViewById(R.id.roomMembersAdapter_userId);
textView.setText(member.getUserId());
ImageView imageView = (ImageView) convertView.findViewById(R.id.avatar_img);
imageView.setTag(null);
imageView.setImageResource(R.drawable.ic_contact_picture_holo_light);
String url = member.avatarUrl;
if (TextUtils.isEmpty(url)) {
url = ContentManager.getIdenticonURL(member.getUserId());
}
if (!TextUtils.isEmpty(url)) {
int size = getContext().getResources().getDimensionPixelSize(R.dimen.member_list_avatar_size);
mMediasCache.loadAvatarThumbnail(mHsConfig, imageView, url, size);
}
// The presence ring
ImageView presenceRing = (ImageView) convertView.findViewById(R.id.imageView_presenceRing);
presenceRing.setColorFilter(mContext.getResources().getColor(android.R.color.transparent));
if (user != null) {
if (User.PRESENCE_ONLINE.equals(user.presence)) {
presenceRing.setColorFilter(presenceOnlineColor());
} else if (User.PRESENCE_UNAVAILABLE.equals(user.presence)) {
presenceRing.setColorFilter(presenceUnavailableColor());
} else if (User.PRESENCE_OFFLINE.equals(user.presence)) {
presenceRing.setColorFilter(presenceUnavailableColor());
}
}
// The power level disc
PieFractionView pieFractionView = (PieFractionView) convertView.findViewById(R.id.powerDisc);
if ((mPowerLevels == null) || (0 == mMaxPowerLevel)) {
pieFractionView.setVisibility(View.GONE);
}
else {
int powerLevel = mPowerLevels.getUserPowerLevel(member.getUserId());
pieFractionView.setVisibility((powerLevel == 0) ? View.GONE : View.VISIBLE);
pieFractionView.setFraction(powerLevel * 100 / mMaxPowerLevel);
}
// the invited members are displayed with alpha 0.5
if (member.membership.equals(RoomMember.MEMBERSHIP_INVITE)) {
convertView.setAlpha(0.3f);
} else {
convertView.setAlpha(1.0f);
}
if (member.membership.equals(RoomMember.MEMBERSHIP_LEAVE) || member.membership.equals(RoomMember.MEMBERSHIP_BAN) ) {
convertView.setBackgroundResource(android.R.color.darker_gray);
} else {
convertView.setBackgroundResource(android.R.color.transparent);
}
return convertView;
}
/**
* Stringify the member last active ago.
* @param context the context
* @param lastActiveAgo the last active ago.
* @return the stringify value.
*/
public static String buildLastActiveDisplay(Context context, long lastActiveAgo) {
lastActiveAgo /= 1000; // In seconds
if (lastActiveAgo < 60) {
return context.getString(R.string.last_seen_secs, lastActiveAgo);
}
lastActiveAgo /= 60; // In minutes
if (lastActiveAgo < 60) {
return context.getString(R.string.last_seen_mins, lastActiveAgo);
}
lastActiveAgo /= 60; // In hours
if (lastActiveAgo < 24) {
return context.getString(R.string.last_seen_hours, lastActiveAgo);
}
lastActiveAgo /= 24; // In days
return context.getString(R.string.last_seen_days, lastActiveAgo);
}
}