callback) {
+ return NimUIKitImpl.login(loginInfo, callback);
+ }
+
+ /**
+ * 手动登陆成功
+ *
+ * @param account 登陆成功账号
+ */
+ public static void loginSuccess(String account) {
+ NimUIKitImpl.loginSuccess(account);
+ }
+
+ /**
+ * 释放缓存,一般在注销时调用
+ */
+ public static void logout() {
+ NimUIKitImpl.logout();
+ }
+
+ /**
+ * 独立模式进入聊天室成功之后,调用此方法
+ *
+ * @param data EnterChatRoomResultData
+ * @param independent 是否独立模式
+ */
+ public static void enterChatRoomSuccess(EnterChatRoomResultData data, boolean independent) {
+ NimUIKitImpl.enterChatRoomSuccess(data, independent);
+ }
+
+ /**
+ * 退出聊天室之后,调用此方法
+ *
+ * @param roomId 聊天室id
+ */
+ public static void exitedChatRoom(String roomId) {
+ NimUIKitImpl.exitedChatRoom(roomId);
+ }
+
+ /**
+ * 获取上下文
+ *
+ * @return 必须初始化后才有值
+ */
+ public static Context getContext() {
+ return NimUIKitImpl.getContext();
+ }
+
+
+ /**
+ * 设置当前登录用户的帐号
+ *
+ * @param account 帐号
+ */
+ public static void setAccount(String account) {
+ NimUIKitImpl.setAccount(account);
+ }
+
+ /**
+ * 获取当前登录的账号
+ *
+ * @return 必须登录成功后才有值
+ */
+ public static String getAccount() {
+ return NimUIKitImpl.getAccount();
+ }
+
+ /**
+ * 打开单聊界面,若开发者未设置 {@link NimUIKitImpl#setCommonP2PSessionCustomization(SessionCustomization)},
+ * 则定制化信息 SessionCustomization 为{@link DefaultP2PSessionCustomization}
+ *
+ * 若需要为目标会话提供单独定义的SessionCustomization,请使用{@link NimUIKitImpl#startChatting(Context, String, SessionTypeEnum, SessionCustomization, IMMessage)}
+ *
+ * @param context 上下文
+ * @param account 目标账号
+ */
+ public static void startP2PSession(Context context, String account) {
+ NimUIKitImpl.startP2PSession(context, account);
+ }
+
+ /**
+ * 同 {@link NimUIKitImpl#startP2PSession(Context, String)},同时聊天界面打开后,列表跳转至anchor位置
+ *
+ * @param context 上下文
+ * @param account 目标账号
+ * @param anchor 跳转到指定消息的位置,不需要跳转填null
+ */
+ public static void startP2PSession(Context context, String account, IMMessage anchor) {
+ NimUIKitImpl.startP2PSession(context, account, anchor);
+ }
+
+ /**
+ * 打开群聊界面,若开发者未设置 {@link NimUIKitImpl#setCommonTeamSessionCustomization(SessionCustomization)},
+ * 则定制化信息 SessionCustomization 为{@link DefaultTeamSessionCustomization}
+ *
+ * 若需要为目标会话提供单独定义的SessionCustomization,请使用{@link NimUIKitImpl#startChatting(Context, String, SessionTypeEnum, SessionCustomization, IMMessage)}
+ *
+ * @param context 上下文
+ * @param tid 群id
+ */
+ public static void startTeamSession(Context context, String tid) {
+ NimUIKitImpl.startTeamSession(context, tid);
+ }
+
+ /**
+ * 同 {@link NimUIKitImpl#startTeamSession(Context, String)},同时聊天界面打开后,列表跳转至anchor位置
+ *
+ * @param context 上下文
+ * @param tid 群id
+ * @param anchor 跳转到指定消息的位置,不需要跳转填null
+ */
+ public static void startTeamSession(Context context, String tid, IMMessage anchor) {
+ NimUIKitImpl.startTeamSession(context, tid, anchor);
+ }
+
+ /**
+ * 打开一个聊天窗口,开始聊天
+ *
+ * @param context 上下文
+ * @param id 聊天对象ID(用户帐号account或者群组ID)
+ * @param sessionType 会话类型
+ * @param customization 定制化信息。针对不同的聊天对象,可提供不同的定制化。
+ * @param anchor 跳转到指定消息的位置,不需要跳转填null
+ */
+ public static void startChatting(Context context, String id, SessionTypeEnum sessionType, SessionCustomization
+ customization, IMMessage anchor) {
+ NimUIKitImpl.startChatting(context, id, sessionType, customization, anchor);
+ }
+
+ /**
+ * 打开一个聊天窗口(用于从聊天信息中创建群聊时,打开群聊)
+ *
+ * @param context 上下文
+ * @param id 聊天对象ID(用户帐号account或者群组ID)
+ * @param sessionType 会话类型
+ * @param customization 定制化信息。针对不同的聊天对象,可提供不同的定制化。
+ * @param backToClass 返回的指定页面
+ * @param anchor 跳转到指定消息的位置,不需要跳转填null
+ */
+ public static void startChatting(Context context, String id, SessionTypeEnum sessionType, SessionCustomization customization,
+ Class extends Activity> backToClass, IMMessage anchor) {
+ NimUIKitImpl.startChatting(context, id, sessionType, customization, backToClass, anchor);
+ }
+
+ /**
+ * 打开联系人选择器
+ *
+ * @param context 上下文(Activity)
+ * @param option 联系人选择器可选配置项
+ * @param requestCode startActivityForResult使用的请求码
+ */
+ public static void startContactSelector(Context context, ContactSelectActivity.Option option, int requestCode) {
+ NimUIKitImpl.startContactSelector(context, option, requestCode);
+ }
+
+ /**
+ * 打开讨论组或高级群资料页
+ *
+ * @param context 上下文
+ * @param teamId 群id
+ */
+ public static void startTeamInfo(Context context, String teamId) {
+ NimUIKitImpl.startTeamInfo(context, teamId);
+ }
+
+ public static boolean isInitComplete() {
+ return NimUIKitImpl.isInitComplete();
+ }
+
+ /**
+ * 获取 “用户资料” 提供者
+ *
+ * @return 必须在初始化后获取
+ */
+ public static IUserInfoProvider getUserInfoProvider() {
+ return NimUIKitImpl.getUserInfoProvider();
+ }
+
+ /**
+ * 获取 “用户资料” 变更监听管理者
+ * UIKit 与 app 之间 userInfo 数据更新通知接口
+ *
+ * @return UserInfoObservable
+ */
+ public static UserInfoObservable getUserInfoObservable() {
+ return NimUIKitImpl.getUserInfoObservable();
+ }
+
+ /**
+ * 获取 “用户关系” 提供者
+ *
+ * @return 必须在初始化后获取
+ */
+ public static ContactProvider getContactProvider() {
+ return NimUIKitImpl.getContactProvider();
+ }
+
+ /**
+ * 获取 “用户关系” 变更监听管理者
+ * UIKit 与 app 之间 “用户关系” 数据更新通知接口
+ *
+ * @return ContactChangedObservable
+ */
+ public static ContactChangedObservable getContactChangedObservable() {
+ return NimUIKitImpl.getContactChangedObservable();
+ }
+
+ /**
+ * 获取群、群成员信息提供者
+ *
+ * @return TeamProvider
+ */
+ public static TeamProvider getTeamProvider() {
+ return NimUIKitImpl.getTeamProvider();
+ }
+
+ /**
+ * 设置群、群成员信息提供者
+ * 不设置则采用 UIKit 内部默认 teamProvider
+ */
+ public static void setTeamProvider(TeamProvider teamProvider) {
+ NimUIKitImpl.setTeamProvider(teamProvider);
+ }
+
+ /**
+ * 获取群成员变化通知
+ *
+ * @return TeamChangedObservable
+ */
+ public static TeamChangedObservable getTeamChangedObservable() {
+ return NimUIKitImpl.getTeamChangedObservable();
+ }
+
+ /**
+ * 获取机器人信息提供者
+ *
+ * @return RobotInfoProvider
+ */
+ public static RobotInfoProvider getRobotInfoProvider() {
+ return NimUIKitImpl.getRobotInfoProvider();
+ }
+
+ /**
+ * 设置机器人信息提供者
+ * 不设置将使用 uikit 内置默认
+ *
+ * @param provider RobotInfoProvider
+ */
+ public static void setRobotInfoProvider(RobotInfoProvider provider) {
+ NimUIKitImpl.setRobotInfoProvider(provider);
+ }
+
+ /**
+ * 设置聊天室信息提供者
+ * 不设置将使用 uikit 内置默认
+ *
+ * @param provider ChatRoomProvider
+ */
+ public static void setChatRoomProvider(ChatRoomProvider provider) {
+ NimUIKitImpl.setChatRoomProvider(provider);
+ }
+
+ /**
+ * 获取聊天室信息提供者
+ *
+ * @return ChatRoomProvider
+ */
+ public static ChatRoomProvider getChatRoomProvider() {
+ return NimUIKitImpl.getChatRoomProvider();
+ }
+
+ /**
+ * 获取聊天室成员变更监听接口
+ *
+ * @return ChatRoomMemberChangedObservable
+ */
+ public static ChatRoomMemberChangedObservable getChatRoomMemberChangedObservable() {
+ return NimUIKitImpl.getChatRoomMemberChangedObservable();
+ }
+
+ /**
+ * 获取图片缓存
+ *
+ * @return Glide图片缓存
+ */
+ public static ImageLoaderKit getImageLoaderKit() {
+ return NimUIKitImpl.getImageLoaderKit();
+ }
+
+ /**
+ * 设置在线状态文案提供者
+ *
+ * @param onlineStateContentProvider 文案内容提供者
+ */
+ public static void setOnlineStateContentProvider(OnlineStateContentProvider onlineStateContentProvider) {
+ NimUIKitImpl.setOnlineStateContentProvider(onlineStateContentProvider);
+ }
+
+ /**
+ * 获取配置的用户在线状态文案提供者
+ *
+ * @return 文案提供者
+ */
+ public static OnlineStateContentProvider getOnlineStateContentProvider() {
+ return NimUIKitImpl.getOnlineStateContentProvider();
+ }
+
+ /**
+ * 设置了 onlineStateContentProvider 则表示UIKit需要展示在线状态
+ *
+ * @return 在线状态开关
+ */
+ public static boolean enableOnlineState() {
+ return NimUIKitImpl.enableOnlineState();
+ }
+
+ /**
+ * 通知在线状态发生变化
+ *
+ * @param accounts 状态变化的账号
+ */
+ @Deprecated
+ public static void notifyOnlineStateChange(Set accounts) {
+ getOnlineStateChangeObservable().notifyOnlineStateChange(accounts);
+ }
+
+ /**
+ * 获取在线状态变更通知接口
+ * @return
+ */
+ public static OnlineStateChangeObservable getOnlineStateChangeObservable() {
+ return NimUIKitImpl.getOnlineStateChangeObservable();
+ }
+
+ /**
+ * 设置是否听筒模式
+ *
+ * @param enable
+ */
+ public static void setEarPhoneModeEnable(boolean enable) {
+ NimUIKitImpl.setEarPhoneModeEnable(enable);
+ }
+
+ /**
+ * 获取是否听筒模式
+ */
+ public static boolean isEarPhoneModeEnable() {
+ return NimUIKitImpl.getEarPhoneModeEnable();
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/UIKitInitStateListener.java b/nim_uikit/src/com/netease/nim/uikit/api/UIKitInitStateListener.java
new file mode 100644
index 0000000..c970713
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/UIKitInitStateListener.java
@@ -0,0 +1,9 @@
+package com.netease.nim.uikit.api;
+
+/**
+ * Created by hzchenkang on 2017/11/6.
+ */
+
+public interface UIKitInitStateListener {
+ void onFinish();
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/UIKitOptions.java b/nim_uikit/src/com/netease/nim/uikit/api/UIKitOptions.java
new file mode 100644
index 0000000..ecfbbe8
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/UIKitOptions.java
@@ -0,0 +1,165 @@
+package com.netease.nim.uikit.api;
+
+import com.netease.nim.uikit.R;
+import com.netease.nimlib.sdk.media.record.RecordType;
+
+/**
+ * Created by hzchenkang on 2017/10/19.
+ */
+
+public class UIKitOptions {
+
+ /**
+ * 配置 APP 保存图片/语音/文件/log等数据缓存的目录(一般配置在SD卡目录)
+ *
+ * 默认为 /sdcard/{packageName}/
+ */
+ public String appCacheDir;
+
+ /**
+ * 独立聊天室模式,没有 IM 业务
+ */
+ public boolean independentChatRoom = false;
+
+ /**
+ * 是否加载表情贴图
+ */
+
+ public boolean loadSticker = true;
+
+ /**
+ * 开启@功能
+ */
+ public boolean aitEnable = true;
+
+ /**
+ * 支持@群成员
+ */
+ public boolean aitTeamMember = true;
+
+ /**
+ * 支持在 IM 聊天中@机器人
+ */
+ public boolean aitIMRobot = true;
+
+ /**
+ * 支持在 Chat Room 中@机器人
+ */
+ public boolean aitChatRoomRobot = true;
+
+ /**
+ * UIKit 异步初始化
+ * 使用异步方式构建能缩短初始化时间,但同时必须查看初始化状态或者监听初始化成功通知
+ */
+ public boolean initAsync = false;
+
+ /**
+ * 使用云信托管账号体系,构建缓存
+ */
+ public boolean buildNimUserCache = true;
+
+ /**
+ * 构建群缓存
+ */
+ public boolean buildTeamCache = true;
+
+ /**
+ * 构建群好友关系缓存
+ */
+ public boolean buildFriendCache = true;
+
+ /**
+ * 构建智能机器人缓存
+ */
+ public boolean buildRobotInfoCache = true;
+
+ /**
+ * 构建聊天室成员缓存
+ */
+ public boolean buildChatRoomMemberCache = true;
+
+ /**
+ * 消息列表每隔多久显示一条消息时间信息,默认五分钟
+ */
+ public long displayMsgTimeWithInterval = 5 * 60 * 1000;
+
+ /**
+ * 单次抓取消息条数配置
+ */
+ public int messageCountLoadOnce = 20;
+
+ /**
+ * IM 接收到的消息时,内容区域背景的drawable id
+ */
+// public int messageLeftBackground = R.drawable.nim_message_item_left_selector;
+ public int messageLeftBackground = R.drawable.bg_nim_water_drop_other;
+
+ /**
+ * IM 发送出去的消息时,内容区域背景的drawable id
+ */
+// public int messageRightBackground = R.drawable.nim_message_item_right_selector;
+ public int messageRightBackground = R.drawable.bg_nim_water_drop_self;
+
+ /**
+ * chat room 接收到的消息时,内容区域背景的drawable id
+ */
+ public int chatRoomMsgLeftBackground = 0;
+
+ /**
+ * chat room 发送出去的消息时,内容区域背景的drawable id
+ */
+ public int chatRoomMsgRightBackground = 0;
+
+ /**
+ * 全局是否使用消息已读,如果设置为false,UIKit 组件将不会发送、接收已读回执
+ */
+ public boolean shouldHandleReceipt = true;
+
+ /**
+ * 文本框最大输入字符数目
+ */
+ public int maxInputTextLength = 5000;
+
+ /**
+ * 录音类型,默认aac
+ */
+ public RecordType audioRecordType = RecordType.AAC;
+
+ /**
+ * 录音时长限制,单位秒,默认最长120s
+ */
+ public int audioRecordMaxTime = 120;
+
+ /**
+ * 不显示语音消息未读红点
+ */
+ public boolean disableAudioPlayedStatusIcon = false;
+
+ /**
+ * 禁止音频轮播
+ */
+ public boolean disableAutoPlayNextAudio = false;
+
+
+ /**
+ * 是否开启允许群管理员撤回他人消息
+ */
+ public boolean enableTeamManagerRevokeMsg = true;
+
+
+ /**
+ * 返回默认的针对独立模式聊天室的 UIKitOptions
+ * @return
+ */
+
+ public static UIKitOptions buildForIndependentChatRoom() {
+ UIKitOptions options = new UIKitOptions();
+ options.buildFriendCache = false;
+ options.buildNimUserCache = false;
+ options.buildTeamCache = false;
+ options.buildRobotInfoCache = false;
+ options.loadSticker = false;
+ options.independentChatRoom = true;
+ return options;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/NimException.kt b/nim_uikit/src/com/netease/nim/uikit/api/model/NimException.kt
new file mode 100644
index 0000000..bb2e5c2
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/NimException.kt
@@ -0,0 +1,15 @@
+package com.netease.nim.uikit.api.model
+
+import java.lang.Exception
+
+class NimException : Exception {
+ var code: Int = 0
+
+ constructor(code: Int) : super() {
+ this.code = code
+ }
+
+ constructor(message: String?) : super(message)
+ constructor(message: String?, cause: Throwable?) : super(message, cause)
+ constructor(cause: Throwable?) : super(cause)
+}
\ No newline at end of file
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/SimpleCallback.java b/nim_uikit/src/com/netease/nim/uikit/api/model/SimpleCallback.java
new file mode 100644
index 0000000..92772dc
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/SimpleCallback.java
@@ -0,0 +1,16 @@
+package com.netease.nim.uikit.api.model;
+
+/**
+ * 简单的回调接口
+ */
+public interface SimpleCallback {
+
+ /**
+ * 回调函数返回结果
+ *
+ * @param success 是否成功,结果是否有效
+ * @param result 结果
+ * @param code 失败时错误码
+ */
+ void onResult(boolean success, T result, int code);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/ChatRoomMemberChangedObservable.java b/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/ChatRoomMemberChangedObservable.java
new file mode 100644
index 0000000..99c65fc
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/ChatRoomMemberChangedObservable.java
@@ -0,0 +1,51 @@
+package com.netease.nim.uikit.api.model.chatroom;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMember;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * UIKit 与 app 聊天室成员变化监听接口
+ */
+
+public class ChatRoomMemberChangedObservable {
+
+ private List observers = new ArrayList<>();
+ private Handler uiHandler;
+
+ public ChatRoomMemberChangedObservable(Context context) {
+ uiHandler = new Handler(context.getMainLooper());
+ }
+
+ public synchronized void registerObserver(RoomMemberChangedObserver observer, boolean register) {
+ if (observer == null) {
+ return;
+ }
+ if (register) {
+ observers.add(observer);
+ } else {
+ observers.remove(observer);
+ }
+ }
+
+ public synchronized void notifyMemberChange(final ChatRoomMember member, final boolean in) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (in) {
+ for (RoomMemberChangedObserver o : observers) {
+ o.onRoomMemberIn(member);
+ }
+ } else {
+ for (RoomMemberChangedObserver o : observers) {
+ o.onRoomMemberExit(member);
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/ChatRoomProvider.java b/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/ChatRoomProvider.java
new file mode 100644
index 0000000..a2c1d2f
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/ChatRoomProvider.java
@@ -0,0 +1,44 @@
+package com.netease.nim.uikit.api.model.chatroom;
+
+import com.netease.nim.uikit.api.model.SimpleCallback;
+import com.netease.nimlib.sdk.chatroom.constant.MemberQueryType;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMember;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
+
+import java.util.List;
+
+/**
+ * 聊天室成员提供者
+ */
+
+public interface ChatRoomProvider {
+
+ /**
+ * 获取聊天室成员
+ *
+ * @param roomId 聊天室
+ * @param account 账号
+ * @return ChatRoomMember
+ */
+ ChatRoomMember getChatRoomMember(String roomId, String account);
+
+ /**
+ * 异步获取聊天室成员
+ *
+ * @param roomId 聊天室
+ * @param account 账号
+ * @param callback 回调
+ */
+ void fetchMember(String roomId, String account, SimpleCallback callback);
+
+ /**
+ * 异步获取聊天室成员列表
+ *
+ * @param roomId 聊天室
+ * @param memberQueryType 请求类型
+ * @param time 时间
+ * @param limit 条数限制
+ * @param callback 回调
+ */
+ void fetchRoomMembers(String roomId, MemberQueryType memberQueryType, long time, int limit, SimpleCallback> callback);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/ChatRoomSessionCustomization.java b/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/ChatRoomSessionCustomization.java
new file mode 100644
index 0000000..08ab295
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/ChatRoomSessionCustomization.java
@@ -0,0 +1,19 @@
+package com.netease.nim.uikit.api.model.chatroom;
+
+import com.netease.nim.uikit.business.session.actions.BaseAction;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+/**
+ * 聊天室聊天界面定制化参数。 可定制:
+ * 1. 加号展开后的按钮和动作
+ */
+public class ChatRoomSessionCustomization implements Serializable {
+
+ /**
+ * 加号展开后的action list。
+ * 默认已包含图片,视频和地理位置
+ */
+ public ArrayList actions;
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/RoomMemberChangedObserver.java b/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/RoomMemberChangedObserver.java
new file mode 100644
index 0000000..4885ae1
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/chatroom/RoomMemberChangedObserver.java
@@ -0,0 +1,24 @@
+package com.netease.nim.uikit.api.model.chatroom;
+
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMember;
+
+/**
+ * UIKit 与 app 聊天室成员数据变更监听接口
+ */
+
+public interface RoomMemberChangedObserver {
+
+ /**
+ * 聊天室新增成员
+ *
+ * @param member 成员
+ */
+ void onRoomMemberIn(ChatRoomMember member);
+
+ /**
+ * 聊天室退出成员
+ *
+ * @param member 成员
+ */
+ void onRoomMemberExit(ChatRoomMember member);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactChangedObservable.java b/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactChangedObservable.java
new file mode 100644
index 0000000..eef28c8
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactChangedObservable.java
@@ -0,0 +1,76 @@
+package com.netease.nim.uikit.api.model.contact;
+
+import android.content.Context;
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 好友关系变动观察者管理
+ */
+
+public class ContactChangedObservable {
+
+ private List observers = new ArrayList<>();
+ private Handler uiHandler;
+
+ public ContactChangedObservable(Context context) {
+ uiHandler = new Handler(context.getMainLooper());
+ }
+
+ public synchronized void registerObserver(ContactChangedObserver observer, boolean register) {
+ if (observer == null) {
+ return;
+ }
+ if (register) {
+ observers.add(observer);
+ } else {
+ observers.remove(observer);
+ }
+ }
+
+ public synchronized void notifyAddedOrUpdated(final List accounts) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (ContactChangedObserver observer : observers) {
+ observer.onAddedOrUpdatedFriends(accounts);
+ }
+ }
+ });
+ }
+
+ public synchronized void notifyDelete(final List accounts) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (ContactChangedObserver observer : observers) {
+ observer.onDeletedFriends(accounts);
+ }
+ }
+ });
+ }
+
+ public synchronized void notifyAddToBlackList(final List accounts) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (ContactChangedObserver observer : observers) {
+ observer.onAddUserToBlackList(accounts);
+ }
+ }
+ });
+ }
+
+ public synchronized void notifyRemoveFromBlackList(final List accounts) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (ContactChangedObserver observer : observers) {
+ observer.onRemoveUserFromBlackList(accounts);
+ }
+ }
+ });
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactChangedObserver.java b/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactChangedObserver.java
new file mode 100644
index 0000000..05deb60
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactChangedObserver.java
@@ -0,0 +1,38 @@
+package com.netease.nim.uikit.api.model.contact;
+
+import java.util.List;
+
+/**
+ * UIKit 与 app 好友关系变化监听接口
+ */
+
+public interface ContactChangedObserver {
+
+ /**
+ * 增加或者更新好友
+ *
+ * @param accounts 账号列表
+ */
+ void onAddedOrUpdatedFriends(List accounts);
+
+ /**
+ * 删除好友
+ *
+ * @param accounts 账号列表
+ */
+ void onDeletedFriends(List accounts);
+
+ /**
+ * 增加到黑名单
+ *
+ * @param accounts 账号列表
+ */
+ void onAddUserToBlackList(List accounts);
+
+ /**
+ * 从黑名单移除
+ *
+ * @param accounts 账号列表
+ */
+ void onRemoveUserFromBlackList(List accounts);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactEventListener.java b/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactEventListener.java
new file mode 100644
index 0000000..a946ef9
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactEventListener.java
@@ -0,0 +1,29 @@
+package com.netease.nim.uikit.api.model.contact;
+
+import android.content.Context;
+
+/**
+ * 通讯录联系人列表一些点击事件的响应处理函数
+ */
+public interface ContactEventListener {
+ /**
+ * 通讯录联系人项点击事件处理,一般打开会话窗口
+ *
+ * @param account 点击的联系人帐号
+ */
+ void onItemClick(Context context, String account);
+
+ /**
+ * 通讯录联系人项长按事件处理,一般弹出菜单:移除好友、添加到星标好友等
+ *
+ * @param account 点击的联系人帐号
+ */
+ void onItemLongClick(Context context, String account);
+
+ /**
+ * 联系人头像点击相应,一般跳转到用户资料页面
+ *
+ * @param account 点击的联系人帐号
+ */
+ void onAvatarClick(Context context, String account);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactProvider.java b/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactProvider.java
new file mode 100644
index 0000000..784795f
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactProvider.java
@@ -0,0 +1,38 @@
+package com.netease.nim.uikit.api.model.contact;
+
+import java.util.List;
+
+/**
+ * 通讯录(联系人)数据源提供者
+ */
+public interface ContactProvider {
+ /**
+ * 返回本地所有好友用户信息(通讯录一般列出所有的好友)
+ *
+ * @return 用户信息集合
+ */
+ List getUserInfoOfMyFriends();
+
+ /**
+ * 返回我的好友数量,提供给通讯录显示所有联系人数量使用
+ *
+ * @return 好友个数
+ */
+ int getMyFriendsCount();
+
+ /**
+ * 获取备注
+ *
+ * @param account 账号
+ * @return 备注
+ */
+ String getAlias(String account);
+
+ /**
+ * 是否是自己的好友
+ *
+ * @param account 账号
+ * @return 结果
+ */
+ boolean isMyFriend(String account);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactsCustomization.java b/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactsCustomization.java
new file mode 100644
index 0000000..8752112
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/contact/ContactsCustomization.java
@@ -0,0 +1,34 @@
+package com.netease.nim.uikit.api.model.contact;
+
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.viewholder.AbsContactViewHolder;
+
+import java.util.List;
+
+/**
+ * 通讯录列表定制,目前支持:
+ * 1.在联系人列表上方加入功能项,并处理点击事件
+ */
+public interface ContactsCustomization {
+
+ /**
+ * 获取功能项ViewHolder的Class(第三方APP提供)
+ *
+ * @return 实现AbsContactViewHolder且Item数据实现AbsContactItem的自定义类
+ */
+ Class extends AbsContactViewHolder extends AbsContactItem>> onGetFuncViewHolderClass();
+
+ /**
+ * 获取所有功能项(第三方APP提供)
+ *
+ * @return 功能项集合
+ */
+ List onGetFuncItems();
+
+ /**
+ * 用户自定义的功能项(折叠群、黑名单、我的电脑等)点击事件处理,一般跳转到相应功能模块
+ *
+ * @param item 自定义的功能项的基类,一般可以通过"=="、"instance of"来判断对应的具体功能
+ */
+ void onFuncItemClick(AbsContactItem item);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/location/LocationProvider.java b/nim_uikit/src/com/netease/nim/uikit/api/model/location/LocationProvider.java
new file mode 100644
index 0000000..89074bb
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/location/LocationProvider.java
@@ -0,0 +1,22 @@
+package com.netease.nim.uikit.api.model.location;
+
+import android.content.Context;
+
+/**
+ * 定位信息提供者类。用于提供地理位置消息,以及根据地理位置打开对应的地图。
+ * 第三方app可自由选择定位SDK
+ */
+public interface LocationProvider {
+
+ // 请求定位信息,由callback返回当前地理位置信息
+ // 定位成功后,请调用callback.onSuccess,如果取消定位或定位失败,无需调用
+ void requestLocation(Context context, Callback callback);
+
+ // 根据当前地理位置打开地图
+ void openMap(Context context, double longitude, double latitude, String address);
+
+ // 定位请求的回调函数。如果定位不成功,或者用户取消,不回调即可。
+ interface Callback {
+ void onSuccess(double longitude, double latitude, String address);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/main/CustomPushContentProvider.java b/nim_uikit/src/com/netease/nim/uikit/api/model/main/CustomPushContentProvider.java
new file mode 100644
index 0000000..5b18276
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/main/CustomPushContentProvider.java
@@ -0,0 +1,27 @@
+package com.netease.nim.uikit.api.model.main;
+
+import com.netease.nimlib.sdk.msg.model.IMMessage;
+
+import java.util.Map;
+
+/**
+ * 用户自定义推送 content 以及 payload 的接口
+ */
+
+public interface CustomPushContentProvider {
+
+ /**
+ * 在消息发出去之前,回调此方法,用户需实现自定义的推送文案
+ *
+ * @param message
+ */
+ String getPushContent(IMMessage message);
+
+ /**
+ * 在消息发出去之前,回调此方法,用户需实现自定义的推送payload,它可以被消息接受者在通知栏点击之后得到
+ *
+ * @param message
+ */
+ Map getPushPayload(IMMessage message);
+
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/main/LoginSyncDataStatusObserver.java b/nim_uikit/src/com/netease/nim/uikit/api/model/main/LoginSyncDataStatusObserver.java
new file mode 100644
index 0000000..8cb8de4
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/main/LoginSyncDataStatusObserver.java
@@ -0,0 +1,144 @@
+package com.netease.nim.uikit.api.model.main;
+
+import android.os.Handler;
+
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.common.util.log.LogUtil;
+import com.netease.nimlib.sdk.NIMClient;
+import com.netease.nimlib.sdk.Observer;
+import com.netease.nimlib.sdk.auth.AuthServiceObserver;
+import com.netease.nimlib.sdk.auth.constant.LoginSyncStatus;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 登录
+ * Created by huangjun on 2015/10/9.
+ */
+public class LoginSyncDataStatusObserver {
+
+ private static final String TAG = LoginSyncDataStatusObserver.class.getSimpleName();
+
+ private static final int TIME_OUT_SECONDS = 10;
+
+ private Handler uiHandler;
+
+ private Runnable timeoutRunnable;
+
+ /**
+ * 状态
+ */
+ private LoginSyncStatus syncStatus = LoginSyncStatus.NO_BEGIN;
+
+ /**
+ * 监听
+ */
+ private List> observers = new ArrayList<>();
+
+ /**
+ * 注销时清除状态&监听
+ */
+ public void reset() {
+ syncStatus = LoginSyncStatus.NO_BEGIN;
+ observers.clear();
+ }
+
+ /**
+ * 在App启动时向SDK注册登录后同步数据过程状态的通知
+ * 调用时机:主进程Application onCreate中
+ */
+ public void registerLoginSyncDataStatus(boolean register) {
+ LogUtil.i(TAG, "observe login sync data completed event on Application create");
+ NIMClient.getService(AuthServiceObserver.class).observeLoginSyncDataStatus(loginSyncStatusObserver, register);
+ }
+
+ Observer loginSyncStatusObserver = new Observer() {
+ @Override
+ public void onEvent(LoginSyncStatus status) {
+ syncStatus = status;
+ if (status == LoginSyncStatus.BEGIN_SYNC) {
+ LogUtil.i(TAG, "login sync data begin");
+ } else if (status == LoginSyncStatus.SYNC_COMPLETED) {
+ LogUtil.i(TAG, "login sync data completed");
+ onLoginSyncDataCompleted(false);
+ }
+ }
+ };
+
+ /**
+ * 监听登录后同步数据完成事件,缓存构建完成后自动取消监听
+ * 调用时机:登录成功后
+ *
+ * @param observer 观察者
+ * @return 返回true表示数据同步已经完成或者不进行同步,返回false表示正在同步数据
+ */
+ public boolean observeSyncDataCompletedEvent(Observer observer) {
+ if (syncStatus == LoginSyncStatus.NO_BEGIN || syncStatus == LoginSyncStatus.SYNC_COMPLETED) {
+ /*
+ * NO_BEGIN 如果登录后未开始同步数据,那么可能是自动登录的情况:
+ * PUSH进程已经登录同步数据完成了,此时UI进程启动后并不知道,这里直接视为同步完成
+ */
+ return true;
+ }
+
+ // 正在同步
+ if (!observers.contains(observer)) {
+ observers.add(observer);
+ }
+
+ // 超时定时器
+ if (uiHandler == null) {
+ uiHandler = new Handler(NimUIKit.getContext().getMainLooper());
+ }
+
+ if (timeoutRunnable == null) {
+ timeoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ // 如果超时还处于开始同步的状态,模拟结束
+ if (syncStatus == LoginSyncStatus.BEGIN_SYNC) {
+ onLoginSyncDataCompleted(true);
+ }
+ }
+ };
+ }
+
+ uiHandler.removeCallbacks(timeoutRunnable);
+ uiHandler.postDelayed(timeoutRunnable, TIME_OUT_SECONDS * 1000);
+
+ return false;
+ }
+
+ /**
+ * 登录同步数据完成处理
+ */
+ private void onLoginSyncDataCompleted(boolean timeout) {
+ LogUtil.i(TAG, "onLoginSyncDataCompleted, timeout=" + timeout);
+
+ // 移除超时任务(有可能完成包到来的时候,超时任务都还没创建)
+ if (timeoutRunnable != null) {
+ uiHandler.removeCallbacks(timeoutRunnable);
+ }
+
+ // 通知上层
+ for (Observer o : observers) {
+ o.onEvent(null);
+ }
+
+ // 重置状态
+ reset();
+ }
+
+
+ /**
+ * 单例
+ */
+ public static LoginSyncDataStatusObserver getInstance() {
+ return InstanceHolder.instance;
+ }
+
+ static class InstanceHolder {
+ final static LoginSyncDataStatusObserver instance = new LoginSyncDataStatusObserver();
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/main/OnlineStateChangeObservable.java b/nim_uikit/src/com/netease/nim/uikit/api/model/main/OnlineStateChangeObservable.java
new file mode 100644
index 0000000..1f858f8
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/main/OnlineStateChangeObservable.java
@@ -0,0 +1,45 @@
+package com.netease.nim.uikit.api.model.main;
+
+import android.content.Context;
+import android.os.Handler;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 在线状态事件变更通知接口
+ */
+
+public class OnlineStateChangeObservable {
+
+ // 在线状态变化监听
+ private List onlineStateChangeObservers;
+ private Handler uiHandler;
+
+ public OnlineStateChangeObservable(Context context) {
+ onlineStateChangeObservers = new LinkedList<>();
+ uiHandler = new Handler(context.getMainLooper());
+ }
+
+ public synchronized void registerOnlineStateChangeListeners(OnlineStateChangeObserver onlineStateChangeObserver, boolean register) {
+ if (register) {
+ onlineStateChangeObservers.add(onlineStateChangeObserver);
+ }else {
+ onlineStateChangeObservers.remove(onlineStateChangeObserver);
+ }
+ }
+
+ public synchronized void notifyOnlineStateChange(final Set accounts) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (onlineStateChangeObservers != null) {
+ for (OnlineStateChangeObserver listener : onlineStateChangeObservers) {
+ listener.onlineStateChange(accounts);
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/main/OnlineStateChangeObserver.java b/nim_uikit/src/com/netease/nim/uikit/api/model/main/OnlineStateChangeObserver.java
new file mode 100644
index 0000000..50a4951
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/main/OnlineStateChangeObserver.java
@@ -0,0 +1,17 @@
+package com.netease.nim.uikit.api.model.main;
+
+import java.util.Set;
+
+/**
+ * Created by hzchenkang on 2017/4/5.
+ */
+
+public interface OnlineStateChangeObserver {
+
+ /**
+ * 通知在线状态事件变化
+ *
+ * @param account 在线状态事件发生变化的账号
+ */
+ void onlineStateChange(Set account);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/main/OnlineStateContentProvider.java b/nim_uikit/src/com/netease/nim/uikit/api/model/main/OnlineStateContentProvider.java
new file mode 100644
index 0000000..ea7cfb2
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/main/OnlineStateContentProvider.java
@@ -0,0 +1,14 @@
+package com.netease.nim.uikit.api.model.main;
+
+/**
+ * Created by hzchenkang on 2017/3/31.
+ */
+
+public interface OnlineStateContentProvider {
+
+ // 用于展示最近联系人界面的在线状态
+ String getSimpleDisplay(String account);
+
+ // 用于展示聊天界面的在线状态
+ String getDetailDisplay(String account);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/recent/RecentCustomization.java b/nim_uikit/src/com/netease/nim/uikit/api/model/recent/RecentCustomization.java
new file mode 100644
index 0000000..17d6b90
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/recent/RecentCustomization.java
@@ -0,0 +1,17 @@
+package com.netease.nim.uikit.api.model.recent;
+
+import com.netease.nimlib.sdk.msg.model.RecentContact;
+
+import java.io.Serializable;
+
+/**
+ * 会话界面定制
+ * 1. 会话项默认文案定制
+ * Created by huangjun on 2017/9/29.
+ */
+
+public class RecentCustomization implements Serializable {
+ public String getDefaultDigest(RecentContact recent) {
+ return null;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/robot/RobotInfoProvider.java b/nim_uikit/src/com/netease/nim/uikit/api/model/robot/RobotInfoProvider.java
new file mode 100644
index 0000000..ce81321
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/robot/RobotInfoProvider.java
@@ -0,0 +1,40 @@
+package com.netease.nim.uikit.api.model.robot;
+
+import com.netease.nim.uikit.api.model.SimpleCallback;
+import com.netease.nimlib.sdk.robot.model.NimRobotInfo;
+
+import java.util.List;
+
+/**
+ * 智能机器人信息提供者
+ */
+
+public interface RobotInfoProvider {
+
+ /**
+ * 根据 id 获取智能机器人
+ *
+ * @param account 智能机器人id
+ * @return NimRobotInfo
+ */
+ NimRobotInfo getRobotByAccount(String account);
+
+ /**
+ * 获取所有的智能机器人
+ *
+ * @return 智能机器人列表
+ */
+ List getAllRobotAccounts();
+
+ /**
+ * IM 模式下,获取(异步)智能机器人
+ */
+ void fetchRobotList(SimpleCallback> callback);
+
+ /**
+ * 独立聊天室模式下,获取(异步)智能机器人
+ *
+ * @param roomId 聊天室id
+ */
+ void fetchRobotListIndependent(String roomId, SimpleCallback> callback);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/session/SessionCustomization.java b/nim_uikit/src/com/netease/nim/uikit/api/model/session/SessionCustomization.java
new file mode 100644
index 0000000..0ad27aa
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/session/SessionCustomization.java
@@ -0,0 +1,73 @@
+package com.netease.nim.uikit.api.model.session;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.view.View;
+
+import com.netease.nim.uikit.business.session.actions.BaseAction;
+import com.netease.nimlib.sdk.msg.attachment.MsgAttachment;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+/**
+ * 聊天界面定制化参数。 可定制:
+ * 1. 聊天背景
+ * 2. 加号展开后的按钮和动作
+ * 3. ActionBar右侧按钮。
+ */
+public class SessionCustomization implements Serializable {
+
+ /**
+ * 聊天背景。优先使用uri,如果没有提供uri,使用color。如果没有color,使用默认。uri暂时支持以下格式:
+ * drawable: android.resource://包名/drawable/资源名
+ * assets: file:///android_asset/{asset文件路径}
+ * file: file:///文件绝对路径
+ */
+ public String backgroundUri;
+ public int backgroundColor;
+
+ // UIKit
+ public boolean withSticker;
+
+ /**
+ * 加号展开后的action list。
+ * 默认已包含图片,视频和地理位置
+ */
+ public ArrayList actions;
+
+ /**
+ * ActionBar右侧可定制按钮。默认为空。
+ */
+ public ArrayList buttons;
+
+ /**
+ * 如果OptionsButton的点击响应中需要startActivityForResult,可在此函数中处理结果。
+ * 需要注意的是,由于加号中的Action的限制,RequestCode只能使用int的最低8位。
+ *
+ * @param activity 当前的聊天Activity
+ * @param requestCode 请求码
+ * @param resultCode 结果码
+ * @param data 返回的结果数据
+ */
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
+ }
+
+ // uikit内建了对贴图消息的输入和管理展示,并和emoji表情整合在了一起,但贴图消息的附件定义开发者需要根据自己的扩展
+ public MsgAttachment createStickerAttachment(String category, String item) {
+ return null;
+ }
+
+ /**
+ * ActionBar 右侧按钮,可定制icon和点击事件
+ */
+ public static abstract class OptionsButton implements Serializable {
+
+ // 图标drawable id
+ public int iconId;
+
+ // 响应事件
+ public abstract void onClick(Context context, View view, String sessionId);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/session/SessionEventListener.java b/nim_uikit/src/com/netease/nim/uikit/api/model/session/SessionEventListener.java
new file mode 100644
index 0000000..6816226
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/session/SessionEventListener.java
@@ -0,0 +1,17 @@
+package com.netease.nim.uikit.api.model.session;
+
+import android.content.Context;
+
+import com.netease.nimlib.sdk.msg.model.IMMessage;
+
+/**
+ * 会话窗口消息列表一些点击事件的响应处理函数
+ */
+public interface SessionEventListener {
+
+ // 头像点击事件处理,一般用于打开用户资料页面
+ void onAvatarClicked(Context context, IMMessage message);
+
+ // 头像长按事件处理,一般用于群组@功能,或者弹出菜单,做拉黑,加好友等功能
+ void onAvatarLongClicked(Context context, IMMessage message);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/team/AudioPartyOpenListener.java b/nim_uikit/src/com/netease/nim/uikit/api/model/team/AudioPartyOpenListener.java
new file mode 100644
index 0000000..4d1da26
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/team/AudioPartyOpenListener.java
@@ -0,0 +1,5 @@
+package com.netease.nim.uikit.api.model.team;
+
+public interface AudioPartyOpenListener {
+ void openAudioParty();
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/team/AvatarClickListener.java b/nim_uikit/src/com/netease/nim/uikit/api/model/team/AvatarClickListener.java
new file mode 100644
index 0000000..84206e4
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/team/AvatarClickListener.java
@@ -0,0 +1,5 @@
+package com.netease.nim.uikit.api.model.team;
+
+public interface AvatarClickListener {
+ void avatarClick(String uid);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamChangedObservable.java b/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamChangedObservable.java
new file mode 100644
index 0000000..719806c
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamChangedObservable.java
@@ -0,0 +1,93 @@
+package com.netease.nim.uikit.api.model.team;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.netease.nimlib.sdk.team.model.Team;
+import com.netease.nimlib.sdk.team.model.TeamMember;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 群、群成员变更通知接口
+ */
+
+public class TeamChangedObservable {
+
+ private List teamObservers = new ArrayList<>();
+ private List memberObservers = new ArrayList<>();
+
+ private Handler uiHandler;
+
+ public TeamChangedObservable(Context context) {
+ uiHandler = new Handler(context.getMainLooper());
+ }
+
+ public synchronized void registerTeamDataChangedObserver(TeamDataChangedObserver o, boolean register) {
+ if (register) {
+ if (teamObservers.contains(o)) {
+ return;
+ }
+ teamObservers.add(o);
+ } else {
+ teamObservers.remove(o);
+ }
+ }
+
+ public synchronized void registerTeamMemberDataChangedObserver(TeamMemberDataChangedObserver o, boolean register) {
+ if (register) {
+ if (memberObservers.contains(o)) {
+ return;
+ }
+ memberObservers.add(o);
+ } else {
+ memberObservers.remove(o);
+ }
+ }
+
+ public synchronized void notifyTeamDataUpdate(final List teams) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (TeamDataChangedObserver o : teamObservers) {
+ o.onUpdateTeams(teams);
+ }
+ }
+ });
+ }
+
+ public synchronized void notifyTeamDataRemove(final Team team) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (TeamDataChangedObserver o : teamObservers) {
+ o.onRemoveTeam(team);
+ }
+ }
+ });
+
+ }
+
+ public synchronized void notifyTeamMemberDataUpdate(final List members) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (TeamMemberDataChangedObserver o : memberObservers) {
+ o.onUpdateTeamMember(members);
+ }
+ }
+ });
+ }
+
+ public synchronized void notifyTeamMemberRemove(final List members) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (TeamMemberDataChangedObserver o : memberObservers) {
+ o.onRemoveTeamMember(members);
+ }
+ }
+ });
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamDataChangedObserver.java b/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamDataChangedObserver.java
new file mode 100644
index 0000000..7fc9089
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamDataChangedObserver.java
@@ -0,0 +1,26 @@
+package com.netease.nim.uikit.api.model.team;
+
+import com.netease.nimlib.sdk.team.model.Team;
+
+import java.util.List;
+
+/**
+ * UIKit 与 app 群数据变更监听接口
+ */
+
+public interface TeamDataChangedObserver {
+
+ /**
+ * 群更新
+ *
+ * @param teams 群列表
+ */
+ void onUpdateTeams(List teams);
+
+ /**
+ * 群删除
+ *
+ * @param team) 群
+ */
+ void onRemoveTeam(Team team);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamMemberDataChangedObserver.java b/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamMemberDataChangedObserver.java
new file mode 100644
index 0000000..15d6871
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamMemberDataChangedObserver.java
@@ -0,0 +1,26 @@
+package com.netease.nim.uikit.api.model.team;
+
+import com.netease.nimlib.sdk.team.model.TeamMember;
+
+import java.util.List;
+
+/**
+ * UIKit 与 app 群成员数据变更监听接口
+ */
+
+public interface TeamMemberDataChangedObserver {
+
+ /**
+ * 成员更新
+ *
+ * @param members 成员列表
+ */
+ void onUpdateTeamMember(List members);
+
+ /**
+ * 成员删除
+ *
+ * @param members 成员列表
+ */
+ void onRemoveTeamMember(List members);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamProvider.java b/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamProvider.java
new file mode 100644
index 0000000..fc76abe
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/team/TeamProvider.java
@@ -0,0 +1,74 @@
+package com.netease.nim.uikit.api.model.team;
+
+import com.netease.nim.uikit.api.model.SimpleCallback;
+import com.netease.nimlib.sdk.team.constant.TeamTypeEnum;
+import com.netease.nimlib.sdk.team.model.Team;
+import com.netease.nimlib.sdk.team.model.TeamMember;
+
+import java.util.List;
+
+/**
+ * 群、群成员信息提供者
+ */
+
+public interface TeamProvider {
+ /**
+ * 根据teamId 异步获取群信息
+ *
+ * @param teamId 群id
+ * @param callback 回调
+ */
+ void fetchTeamById(String teamId, SimpleCallback callback);
+
+ /**
+ * 根据teamId 同步获取群信息
+ *
+ * @param teamId 群id
+ * @return 群信息Team
+ */
+ Team getTeamById(String teamId);
+
+ /**
+ * 获取当前账号所有的群
+ *
+ * @return 群列表
+ */
+ List getAllTeams();
+
+ /**
+ * 获取当前账号所有的指定类型的群
+ *
+ * @return 群列表
+ */
+ List getAllTeamsByType(TeamTypeEnum teamTypeEnum);
+
+ /**
+ * 根据群id异步获取当前群所有的群成员信息
+ *
+ * @param teamId 群id
+ * @param callback 回调
+ */
+ void fetchTeamMemberList(String teamId, SimpleCallback> callback);
+
+ /**
+ * 根据群id、账号(异步)查询群成员资料
+ */
+ void fetchTeamMember(String teamId, String account, SimpleCallback callback);
+
+ /**
+ * 根据群id 同步获取当前群所有的群成员信息
+ *
+ * @param teamId 群id
+ * @return 群成员信息列表
+ */
+ List getTeamMemberList(String teamId);
+
+ /**
+ * 获取群成员资料
+ *
+ * @param teamId 群id
+ * @param account 成员账号
+ * @return TeamMember
+ */
+ TeamMember getTeamMember(String teamId, String account);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/user/IUserInfoProvider.java b/nim_uikit/src/com/netease/nim/uikit/api/model/user/IUserInfoProvider.java
new file mode 100644
index 0000000..7597d80
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/user/IUserInfoProvider.java
@@ -0,0 +1,53 @@
+package com.netease.nim.uikit.api.model.user;
+
+import com.netease.nim.uikit.api.model.SimpleCallback;
+import com.netease.nimlib.sdk.uinfo.model.UserInfo;
+
+import java.util.List;
+
+/**
+ * Created by hzchenkang on 2017/10/24.
+ */
+
+public interface IUserInfoProvider {
+
+ /**
+ * 获取用户头像
+ *
+ * @param account
+ * @return
+ */
+ String getUserAvatar(String account);
+
+ /**
+ * 同步获取userInfo
+ *
+ * @param account 账号
+ * @return userInfo
+ */
+ T getUserInfo(String account);
+
+ /**
+ * 同步获取userInfo列表
+ *
+ * @param accounts 账号
+ * @return userInfo
+ */
+ List getUserInfo(List accounts);
+
+ /**
+ * 异步获取userInfo
+ *
+ * @param account 账号id
+ * @param callback 回调
+ */
+ void getUserInfoAsync(String account, SimpleCallback callback);
+
+ /**
+ * 异步获取userInfo列表
+ *
+ * @param accounts 账号id 集合
+ * @param callback 回调
+ */
+ void getUserInfoAsync(List accounts, final SimpleCallback> callback);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/user/UserInfoObservable.java b/nim_uikit/src/com/netease/nim/uikit/api/model/user/UserInfoObservable.java
new file mode 100644
index 0000000..d0b14bb
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/user/UserInfoObservable.java
@@ -0,0 +1,42 @@
+package com.netease.nim.uikit.api.model.user;
+
+import android.content.Context;
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 用户资料变动观察者管理
+ */
+public class UserInfoObservable {
+
+ private List observers = new ArrayList<>();
+ private Handler uiHandler;
+
+ public UserInfoObservable(Context context) {
+ uiHandler = new Handler(context.getMainLooper());
+ }
+
+ synchronized public void registerObserver(UserInfoObserver observer, boolean register) {
+ if (observer == null) {
+ return;
+ }
+ if (register) {
+ observers.add(observer);
+ } else {
+ observers.remove(observer);
+ }
+ }
+
+ synchronized public void notifyUserInfoChanged(final List accounts) {
+ uiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (UserInfoObserver observer : observers) {
+ observer.onUserInfoChanged(accounts);
+ }
+ }
+ });
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/model/user/UserInfoObserver.java b/nim_uikit/src/com/netease/nim/uikit/api/model/user/UserInfoObserver.java
new file mode 100644
index 0000000..3ddd495
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/model/user/UserInfoObserver.java
@@ -0,0 +1,17 @@
+package com.netease.nim.uikit.api.model.user;
+
+import java.util.List;
+
+/**
+ * UIKit 与 app 好友关系变化监听接口
+ */
+
+public interface UserInfoObserver {
+
+ /**
+ * 用户信息变更
+ *
+ * @param accounts 账号列表
+ */
+ void onUserInfoChanged(List accounts);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/wrapper/MessageRevokeTip.java b/nim_uikit/src/com/netease/nim/uikit/api/wrapper/MessageRevokeTip.java
new file mode 100644
index 0000000..6fbf93c
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/wrapper/MessageRevokeTip.java
@@ -0,0 +1,69 @@
+package com.netease.nim.uikit.api.wrapper;
+
+import android.text.TextUtils;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.business.team.helper.TeamHelper;
+import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
+import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
+import com.netease.nimlib.sdk.msg.model.IMMessage;
+import com.netease.nimlib.sdk.robot.model.RobotAttachment;
+import com.netease.nimlib.sdk.team.constant.TeamMemberType;
+import com.netease.nimlib.sdk.team.model.TeamMember;
+import com.chwl.library.utils.ResUtil;
+
+/**
+ * 消息撤回通知文案
+ */
+
+public class MessageRevokeTip {
+
+ public static String getRevokeTipContent(IMMessage item, String revokeAccount) {
+
+ String fromAccount = item.getFromAccount();
+ if (item.getMsgType() == MsgTypeEnum.robot) {
+ RobotAttachment robotAttachment = (RobotAttachment) item.getAttachment();
+ if (robotAttachment.isRobotSend()) {
+ fromAccount = robotAttachment.getFromRobotAccount();
+ }
+ }
+
+ if (!TextUtils.isEmpty(
+ revokeAccount) && !revokeAccount.equals(fromAccount)) {
+ return getRevokeTipOfOther(item.getSessionId(), item.getSessionType(), revokeAccount);
+ } else {
+ String revokeNick = ""; // 撤回者
+ if (item.getSessionType() == SessionTypeEnum.Team) {
+ revokeNick = TeamHelper.getTeamMemberDisplayNameYou(item.getSessionId(), item.getFromAccount());
+ } else if (item.getSessionType() == SessionTypeEnum.P2P) {
+ revokeNick = item.getFromAccount().equals(NimUIKit.getAccount()) ? ResUtil.getString(R.string.api_wrapper_messagerevoketip_01) : ResUtil.getString(R.string.api_wrapper_messagerevoketip_02);
+ }
+ return revokeNick + ResUtil.getString(R.string.api_wrapper_messagerevoketip_03);
+ }
+ }
+
+ // 撤回其他人的消息时,获取tip
+ public static String getRevokeTipOfOther(String sessionID, SessionTypeEnum sessionType, String revokeAccount) {
+ if (sessionType == SessionTypeEnum.Team) {
+ String revokeNick = ""; // 撤回者
+
+ if (NimUIKit.getAccount().equals(revokeAccount)) {
+ revokeNick = ResUtil.getString(R.string.api_wrapper_messagerevoketip_04);
+ } else {
+ TeamMember member = NimUIKit.getTeamProvider().getTeamMember(sessionID, revokeAccount);
+
+ String revoker = TeamHelper.getDisplayNameWithoutMe(sessionID, revokeAccount);
+
+ if (member == null || member.getType() == TeamMemberType.Manager) {
+ revokeNick = ResUtil.getString(R.string.api_wrapper_messagerevoketip_05) + revoker + " ";
+ } else if (member.getType() == TeamMemberType.Owner) {
+ revokeNick = ResUtil.getString(R.string.api_wrapper_messagerevoketip_06) + revoker + " ";
+ }
+ }
+ return revokeNick + ResUtil.getString(R.string.api_wrapper_messagerevoketip_07);
+ } else {
+ return ResUtil.getString(R.string.api_wrapper_messagerevoketip_08);
+ }
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/wrapper/NimMessageRevokeObserver.java b/nim_uikit/src/com/netease/nim/uikit/api/wrapper/NimMessageRevokeObserver.java
new file mode 100644
index 0000000..8ccc102
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/wrapper/NimMessageRevokeObserver.java
@@ -0,0 +1,21 @@
+package com.netease.nim.uikit.api.wrapper;
+
+import com.netease.nim.uikit.business.session.helper.MessageHelper;
+import com.netease.nimlib.sdk.Observer;
+import com.netease.nimlib.sdk.msg.model.RevokeMsgNotification;
+
+/**
+ * 云信消息撤回观察者
+ */
+
+public class NimMessageRevokeObserver implements Observer {
+
+ @Override
+ public void onEvent(RevokeMsgNotification notification) {
+ if (notification == null || notification.getMessage() == null) {
+ return;
+ }
+
+ MessageHelper.getInstance().onRevokeMessage(notification.getMessage(), notification.getRevokeAccount());
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/wrapper/NimToolBarOptions.java b/nim_uikit/src/com/netease/nim/uikit/api/wrapper/NimToolBarOptions.java
new file mode 100644
index 0000000..1efe9f8
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/wrapper/NimToolBarOptions.java
@@ -0,0 +1,17 @@
+package com.netease.nim.uikit.api.wrapper;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.common.activity.ToolBarOptions;
+
+/**
+ * Created by hzxuwen on 2016/6/16.
+ */
+public class NimToolBarOptions extends ToolBarOptions {
+
+ public NimToolBarOptions() {
+ //logoId = R.drawable.nim_actionbar_nest_dark_logo;
+ navigateId = R.drawable.arrow_left;
+ titleString = "";
+ isNeedNavigate = true;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/api/wrapper/NimUserInfoProvider.java b/nim_uikit/src/com/netease/nim/uikit/api/wrapper/NimUserInfoProvider.java
new file mode 100644
index 0000000..504efc1
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/api/wrapper/NimUserInfoProvider.java
@@ -0,0 +1,82 @@
+package com.netease.nim.uikit.api.wrapper;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.business.team.helper.TeamHelper;
+import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
+import com.netease.nimlib.sdk.team.model.Team;
+import com.netease.nimlib.sdk.uinfo.UserInfoProvider;
+import com.netease.nimlib.sdk.uinfo.model.UserInfo;
+
+/**
+ * 初始化sdk 需要的用户信息提供者,现主要用于内置通知提醒获取昵称和头像
+ *
+ * 注意不要与 IUserInfoProvider 混淆,后者是 UIKit 与 demo 之间的数据共享接口
+ *
+ */
+
+public class NimUserInfoProvider implements UserInfoProvider {
+
+ private Context context;
+
+ public NimUserInfoProvider(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public UserInfo getUserInfo(String account) {
+ return NimUIKit.getUserInfoProvider().getUserInfo(account);
+ }
+
+ @Override
+ public Bitmap getAvatarForMessageNotifier(SessionTypeEnum sessionType, String sessionId) {
+ /*
+ * 注意:这里最好从缓存里拿,如果加载时间过长会导致通知栏延迟弹出!该函数在后台线程执行!
+ */
+ Bitmap bm = null;
+ int defResId = R.drawable.nim_avatar_default;
+
+ if (SessionTypeEnum.P2P == sessionType) {
+ UserInfo user = getUserInfo(sessionId);
+ bm = (user != null) ? NimUIKit.getImageLoaderKit().getNotificationBitmapFromCache(user.getAvatar()) : null;
+ } else if (SessionTypeEnum.Team == sessionType) {
+ Team team = NimUIKit.getTeamProvider().getTeamById(sessionId);
+ bm = (team != null) ? NimUIKit.getImageLoaderKit().getNotificationBitmapFromCache(team.getIcon()) : null;
+ defResId = R.drawable.nim_avatar_group;
+ }
+
+ if (bm == null) {
+ Drawable drawable = context.getResources().getDrawable(defResId);
+ if (drawable instanceof BitmapDrawable) {
+ bm = ((BitmapDrawable) drawable).getBitmap();
+ }
+ }
+
+ return bm;
+ }
+
+ @Override
+ public String getDisplayNameForMessageNotifier(String account, String sessionId, SessionTypeEnum sessionType) {
+ String nick = null;
+ if (sessionType == SessionTypeEnum.P2P) {
+ nick = NimUIKit.getContactProvider().getAlias(account);
+ } else if (sessionType == SessionTypeEnum.Team) {
+ nick = NimUIKit.getContactProvider().getAlias(account);
+ if (TextUtils.isEmpty(nick)) {
+ nick = TeamHelper.getTeamNick(sessionId, account);
+ }
+ }
+
+ if (TextUtils.isEmpty(nick)) {
+ return null; // 返回null,交给sdk处理。如果对方有设置nick,sdk会显示nick
+ }
+
+ return nick;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/AitBlock.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/AitBlock.java
new file mode 100644
index 0000000..23e031f
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/AitBlock.java
@@ -0,0 +1,148 @@
+package com.netease.nim.uikit.business.ait;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Created by hzchenkang on 2017/7/7.
+ * 表示一个@块
+ */
+
+public class AitBlock {
+
+ /**
+ * text = "@" + name
+ */
+ public String text;
+
+ /**
+ * 类型,群成员/机器人
+ */
+ public int aitType;
+
+ /**
+ * 在文本中的位置
+ */
+ public List segments = new ArrayList<>();
+
+ public AitBlock(String name, int aitType) {
+ this.text = "@" + name;
+ this.aitType = aitType;
+ }
+
+ // 新增 segment
+ public AitSegment addSegment(int start) {
+ int end = start + text.length() - 1;
+ AitSegment segment = new AitSegment(start, end);
+ segments.add(segment);
+ return segment;
+ }
+
+ /**
+ * @param start 起始光标位置
+ * @param changeText 插入文本
+ */
+ public void moveRight(int start, String changeText) {
+ if (changeText == null) {
+ return;
+ }
+ int length = changeText.length();
+ Iterator iterator = segments.iterator();
+ while (iterator.hasNext()) {
+ AitSegment segment = iterator.next();
+ // 从已有的一个@块中插入
+ if (start > segment.start && start <= segment.end) {
+ segment.end += length;
+ segment.broken = true;
+ } else if (start <= segment.start) {
+ segment.start += length;
+ segment.end += length;
+ }
+ }
+ }
+
+ /**
+ * @param start 删除前光标位置
+ * @param length 删除块的长度
+ */
+ public void moveLeft(int start, int length) {
+ int after = start - length;
+ Iterator iterator = segments.iterator();
+
+ while (iterator.hasNext()) {
+ AitSegment segment = iterator.next();
+ // 从已有@块中删除
+ if (start > segment.start) {
+ // @被删除掉
+ if (after <= segment.start) {
+ iterator.remove();
+ } else if (after <= segment.end) {
+ segment.broken = true;
+ segment.end -= length;
+ }
+ } else if (start <= segment.start) {
+ segment.start -= length;
+ segment.end -= length;
+ }
+ }
+ }
+
+ // 获取该账号所有有效的@块最靠前的start
+ public int getFirstSegmentStart() {
+ int start = -1;
+ for (AitSegment segment : segments) {
+ if (segment.broken) {
+ continue;
+ }
+ if (start == -1 || segment.start < start) {
+ start = segment.start;
+ }
+ }
+ return start;
+ }
+
+ public AitSegment findLastSegmentByEnd(int end) {
+ int pos = end - 1;
+ for (AitSegment segment : segments) {
+ if (!segment.broken && segment.end == pos) {
+ return segment;
+ }
+ }
+ return null;
+ }
+
+ public boolean valid() {
+ if (segments.size() == 0) {
+ return false;
+ }
+ for (AitSegment segment : segments) {
+ if (!segment.broken) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static class AitSegment {
+ /**
+ * 位于文本起始位置(include)
+ */
+ public int start;
+
+ /**
+ * 位于文本结束位置(include)
+ */
+ public int end;
+
+ /**
+ * 是否坏掉
+ */
+ public boolean broken = false;
+
+ public AitSegment(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/AitContactType.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/AitContactType.java
new file mode 100644
index 0000000..af5d4d2
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/AitContactType.java
@@ -0,0 +1,10 @@
+package com.netease.nim.uikit.business.ait;
+
+/**
+ * Created by hzchenkang on 2017/6/21.
+ */
+
+public interface AitContactType {
+ int ROBOT = 1;
+ int TEAM_MEMBER = 2;
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/AitContactsModel.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/AitContactsModel.java
new file mode 100644
index 0000000..c1cf9c9
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/AitContactsModel.java
@@ -0,0 +1,132 @@
+package com.netease.nim.uikit.business.ait;
+
+import com.netease.nim.uikit.business.ait.event.AitContactAddEvent;
+import com.netease.nim.uikit.business.ait.event.AitContactDeleteEvent;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by hzchenkang on 2017/7/7.
+ *
+ * @ 联系人数据
+ */
+
+public class AitContactsModel {
+
+ // 已@ 的成员
+ private final Map aitBlocks = new HashMap<>();
+
+ // 清除所有的@块
+ public void reset() {
+ aitBlocks.clear();
+ }
+
+ public Map getAitBlocks() {
+ return aitBlocks;
+ }
+
+ public void addAitMember(String account, String name, int type, int start) {
+ AitBlock aitBlock = aitBlocks.get(account);
+ if (aitBlock == null) {
+ aitBlock = new AitBlock(name, type);
+ aitBlocks.put(account, aitBlock);
+ EventBus.getDefault().post(new AitContactAddEvent()
+ .setAccount(account)
+ .setName(name));
+ }
+ aitBlock.addSegment(start);
+ }
+
+ // 查所有被@的群成员
+ public List getAitTeamMember() {
+ List teamMembers = new ArrayList<>();
+ Iterator iterator = aitBlocks.keySet().iterator();
+ while (iterator.hasNext()) {
+ String account = iterator.next();
+ AitBlock block = aitBlocks.get(account);
+ if (block.aitType == AitContactType.TEAM_MEMBER && block.valid()) {
+ teamMembers.add(account);
+ }
+ }
+ return teamMembers;
+ }
+
+ public AitBlock getAitBlock(String account) {
+ return aitBlocks.get(account);
+ }
+
+ // 查第一个被@ 的机器人
+ public String getFirstAitRobot() {
+ int start = -1;
+ String robotAccount = null;
+
+ Iterator iterator = aitBlocks.keySet().iterator();
+ while (iterator.hasNext()) {
+ String account = iterator.next();
+ AitBlock block = aitBlocks.get(account);
+ if (block.valid() && block.aitType == AitContactType.ROBOT) {
+ int blockStart = block.getFirstSegmentStart();
+ if (blockStart == -1) {
+ continue;
+ }
+ if (start == -1 || blockStart < start) {
+ start = blockStart;
+ robotAccount = account;
+ }
+ }
+ }
+ return robotAccount;
+ }
+
+ // 找到 curPos 恰好命中 end 的segment
+ public AitBlock.AitSegment findAitSegmentByEndPos(int start) {
+ Iterator iterator = aitBlocks.keySet().iterator();
+ while (iterator.hasNext()) {
+ String account = iterator.next();
+ AitBlock block = aitBlocks.get(account);
+ AitBlock.AitSegment segment = block.findLastSegmentByEnd(start);
+ if (segment != null) {
+ return segment;
+ }
+ }
+ return null;
+ }
+
+ // 文本插入后更新@块的起止位置
+ public void onInsertText(int start, String changeText) {
+ Iterator iterator = aitBlocks.keySet().iterator();
+ while (iterator.hasNext()) {
+ String account = iterator.next();
+ AitBlock block = aitBlocks.get(account);
+ block.moveRight(start, changeText);
+ if (!block.valid()) {
+ iterator.remove();
+ EventBus.getDefault().post(new AitContactDeleteEvent()
+ .setAccount(account)
+ .setAitBlock(block));
+ }
+ }
+ }
+
+ // 文本删除后更新@块的起止位置
+ public void onDeleteText(int start, int length) {
+ Iterator iterator = aitBlocks.keySet().iterator();
+ while (iterator.hasNext()) {
+ String account = iterator.next();
+ AitBlock block = aitBlocks.get(account);
+ block.moveLeft(start, length);
+ if (!block.valid()) {
+ iterator.remove();
+ EventBus.getDefault().post(new AitContactDeleteEvent()
+ .setAccount(account)
+ .setAitBlock(block));
+ }
+ }
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/AitManager.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/AitManager.java
new file mode 100644
index 0000000..9bf7df8
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/AitManager.java
@@ -0,0 +1,226 @@
+package com.netease.nim.uikit.business.ait;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+
+import androidx.annotation.NonNull;
+
+import com.netease.nim.uikit.business.ait.event.AitContactActionEvent;
+import com.netease.nim.uikit.business.ait.selector.AitContactSelectorActivity;
+import com.netease.nim.uikit.business.uinfo.UserInfoHelper;
+import com.netease.nimlib.sdk.robot.model.NimRobotInfo;
+import com.netease.nimlib.sdk.team.model.TeamMember;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by hzchenkang on 2017/7/10.
+ */
+
+public class AitManager implements TextWatcher {
+
+ private Context context;
+
+ private String tid;
+
+ private boolean robot;
+
+ private AitContactsModel aitContactsModel;
+
+ private int curPos;
+
+ private boolean ignoreTextChange = false;
+
+ private AitTextChangeListener listener;
+
+ public AitManager(Context context, String tid, boolean robot) {
+ this.context = context;
+ this.tid = tid;
+ this.robot = robot;
+ aitContactsModel = new AitContactsModel();
+ }
+
+ public void setTextChangeListener(AitTextChangeListener listener) {
+ this.listener = listener;
+ }
+
+ public List getAitTeamMember() {
+ return aitContactsModel.getAitTeamMember();
+ }
+
+ @NonNull
+ public Map getAitBlocks() {
+ return aitContactsModel.getAitBlocks();
+ }
+
+ public String getAitRobot() {
+ return aitContactsModel.getFirstAitRobot();
+ }
+
+ public String removeRobotAitString(String text, String robotAccount) {
+ AitBlock block = aitContactsModel.getAitBlock(robotAccount);
+ if (block != null) {
+ return text.replaceAll(block.text, "");
+ } else {
+ return text;
+ }
+ }
+
+ public void reset() {
+ aitContactsModel.reset();
+ ignoreTextChange = false;
+ curPos = 0;
+ }
+
+ /**
+ * ------------------------------ 增加@成员 --------------------------------------
+ */
+
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == AitContactSelectorActivity.REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+ int type = data.getIntExtra(AitContactSelectorActivity.RESULT_TYPE, -1);
+ String account = "";
+ String name = "";
+ if (type == AitContactType.TEAM_MEMBER) {
+ TeamMember member = (TeamMember) data.getSerializableExtra(AitContactSelectorActivity.RESULT_DATA);
+ account = member.getAccount();
+ name = getAitTeamMemberName(member);
+ } else if (type == AitContactType.ROBOT) {
+ NimRobotInfo robotInfo = (NimRobotInfo) data.getSerializableExtra(AitContactSelectorActivity.RESULT_DATA);
+ account = robotInfo.getAccount();
+ name = robotInfo.getName();
+ }
+ insertAitMemberInner(account, name, type, curPos, false);
+ }
+ }
+
+ // 群昵称 > 用户昵称 > 账号
+ private static String getAitTeamMemberName(TeamMember member) {
+ if (member == null) {
+ return "";
+ }
+ String memberNick = member.getTeamNick();
+ if (!TextUtils.isEmpty(memberNick)) {
+ return memberNick;
+ }
+ return UserInfoHelper.getUserName(member.getAccount());
+ }
+
+ public void insertAitRobot(String account, String name, int start) {
+ insertAitMemberInner(account, name, AitContactType.ROBOT, start, true);
+ }
+
+ public void insertAitMember(String account, String name) {
+ insertAitMemberInner(account, name, AitContactType.TEAM_MEMBER, curPos, true);
+ }
+
+ private void insertAitMemberInner(String account, String name, int type, int start, boolean needInsertAitInText) {
+ name = name + " ";
+ String content = needInsertAitInText ? "@" + name : name;
+ if (listener != null) {
+ // 关闭监听
+ ignoreTextChange = true;
+ // insert 文本到editText
+ listener.onTextAdd(content, start, content.length());
+ // 开启监听
+ ignoreTextChange = false;
+ }
+
+ // update 已有的 aitBlock
+ aitContactsModel.onInsertText(start, content);
+
+ int index = needInsertAitInText ? start : start - 1;
+ // 添加当前到 aitBlock
+ aitContactsModel.addAitMember(account, name, type, index);
+ }
+
+ /**
+ * ------------------------------ editText 监听 --------------------------------------
+ */
+
+ // 当删除尾部空格时,删除一整个segment,包含界面上也删除
+ private boolean deleteSegment(int start, int count) {
+ if (count != 1) {
+ return false;
+ }
+ boolean result = false;
+ AitBlock.AitSegment segment = aitContactsModel.findAitSegmentByEndPos(start);
+ if (segment != null) {
+ int length = start - segment.start;
+ if (listener != null) {
+ ignoreTextChange = true;
+ listener.onTextDelete(segment.start, length);
+ ignoreTextChange = false;
+ }
+ aitContactsModel.onDeleteText(start, length);
+ result = true;
+ }
+ return result;
+ }
+
+ /**
+ * @param editable 变化后的Editable
+ * @param start text 变化区块的起始index
+ * @param count text 变化区块的大小
+ * @param delete 是否是删除
+ */
+ private void afterTextChanged(Editable editable, int start, int count, boolean delete) {
+ curPos = delete ? start : count + start;
+ if (ignoreTextChange) {
+ return;
+ }
+ if (delete) {
+ int before = start + count;
+ if (deleteSegment(before, count)) {
+ return;
+ }
+ aitContactsModel.onDeleteText(before, count);
+
+ } else {
+ if (count <= 0 || editable.length() < start + count) {
+ return;
+ }
+ CharSequence s = editable.subSequence(start, start + count);
+ if (s == null) {
+ return;
+ }
+ if (s.toString().endsWith("@")) {
+ // 启动@联系人界面
+ if (!TextUtils.isEmpty(tid) || robot) {
+// AitContactSelectorActivity.start(context, tid, robot);
+ EventBus.getDefault().post(new AitContactActionEvent().setContent(s.toString()));
+ }
+ }
+ aitContactsModel.onInsertText(start, s.toString());
+ }
+ }
+
+ private int editTextStart;
+ private int editTextCount;
+ private int editTextBefore;
+ private boolean delete;
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ delete = count > after;
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ this.editTextStart = start;
+ this.editTextCount = count;
+ this.editTextBefore = before;
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ afterTextChanged(s, editTextStart, delete ? editTextBefore : editTextCount, delete);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/AitTextChangeListener.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/AitTextChangeListener.java
new file mode 100644
index 0000000..5e9b67b
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/AitTextChangeListener.java
@@ -0,0 +1,12 @@
+package com.netease.nim.uikit.business.ait;
+
+/**
+ * Created by hzchenkang on 2017/7/10.
+ */
+
+public interface AitTextChangeListener {
+
+ void onTextAdd(String content, int start, int length);
+
+ void onTextDelete(int start, int length);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/event/AitContactActionEvent.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/event/AitContactActionEvent.java
new file mode 100644
index 0000000..37b1ae8
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/event/AitContactActionEvent.java
@@ -0,0 +1,33 @@
+package com.netease.nim.uikit.business.ait.event;
+
+public class AitContactActionEvent {
+
+ private String account;
+ private String content;
+
+ public String getAccount() {
+ return account;
+ }
+
+ public AitContactActionEvent setAccount(String account) {
+ this.account = account;
+ return this;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public AitContactActionEvent setContent(String content) {
+ this.content = content;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "AitContactActionEvent{" +
+ "account='" + account + '\'' +
+ ", content='" + content + '\'' +
+ '}';
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/event/AitContactAddEvent.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/event/AitContactAddEvent.java
new file mode 100644
index 0000000..9851d11
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/event/AitContactAddEvent.java
@@ -0,0 +1,33 @@
+package com.netease.nim.uikit.business.ait.event;
+
+public class AitContactAddEvent {
+
+ private String account;
+ private String name;
+
+ public String getAccount() {
+ return account;
+ }
+
+ public AitContactAddEvent setAccount(String account) {
+ this.account = account;
+ return this;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public AitContactAddEvent setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "AitContactAddEvent{" +
+ "account='" + account + '\'' +
+ ", name='" + name + '\'' +
+ '}';
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/event/AitContactDeleteEvent.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/event/AitContactDeleteEvent.java
new file mode 100644
index 0000000..56889c2
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/event/AitContactDeleteEvent.java
@@ -0,0 +1,35 @@
+package com.netease.nim.uikit.business.ait.event;
+
+import com.netease.nim.uikit.business.ait.AitBlock;
+
+public class AitContactDeleteEvent {
+
+ private String account;
+ private AitBlock aitBlock;
+
+ public String getAccount() {
+ return account;
+ }
+
+ public AitContactDeleteEvent setAccount(String account) {
+ this.account = account;
+ return this;
+ }
+
+ public AitBlock getAitBlock() {
+ return aitBlock;
+ }
+
+ public AitContactDeleteEvent setAitBlock(AitBlock aitBlock) {
+ this.aitBlock = aitBlock;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "AitContactDeleteEvent{" +
+ "account='" + account + '\'' +
+ ", aitBlock=" + aitBlock +
+ '}';
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/AitContactItemDecorationNIM.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/AitContactItemDecorationNIM.java
new file mode 100644
index 0000000..cbe04ca
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/AitContactItemDecorationNIM.java
@@ -0,0 +1,39 @@
+package com.netease.nim.uikit.business.ait.selector;
+
+import android.content.Context;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.netease.nim.uikit.common.ui.recyclerview.decoration.NIMDividerItemDecoration;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Created by hzchenkang on 2017/6/22.
+ */
+
+public class AitContactItemDecorationNIM extends NIMDividerItemDecoration {
+
+ // 不需要分割线
+ private Set ignoreDecorations;
+
+ public AitContactItemDecorationNIM(Context context, int orientation, List ignoreDecorations) {
+ super(context, orientation);
+ if (ignoreDecorations != null) {
+ this.ignoreDecorations = new HashSet<>(ignoreDecorations);
+ }
+ }
+
+ @Override
+ protected boolean needDrawDecoration(RecyclerView parent, int position) {
+ if (ignoreDecorations != null) {
+ int viewType = parent.getAdapter().getItemViewType(position);
+ if (ignoreDecorations.contains(viewType)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/AitContactSelectorActivity.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/AitContactSelectorActivity.java
new file mode 100644
index 0000000..baba66f
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/AitContactSelectorActivity.java
@@ -0,0 +1,182 @@
+package com.netease.nim.uikit.business.ait.selector;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.api.model.SimpleCallback;
+import com.netease.nim.uikit.api.wrapper.NimToolBarOptions;
+import com.netease.nim.uikit.business.ait.selector.adapter.AitContactAdapter;
+import com.netease.nim.uikit.business.ait.selector.model.AitContactItem;
+import com.netease.nim.uikit.business.ait.selector.model.ItemType;
+import com.netease.nim.uikit.common.activity.ToolBarOptions;
+import com.netease.nim.uikit.common.activity.UI;
+import com.netease.nim.uikit.common.ui.recyclerview.listener.OnItemClickListener;
+import com.netease.nimlib.sdk.robot.model.NimRobotInfo;
+import com.netease.nimlib.sdk.team.model.Team;
+import com.netease.nimlib.sdk.team.model.TeamMember;
+import com.chwl.library.utils.ResUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by hzchenkang on 2017/6/21.
+ */
+
+public class AitContactSelectorActivity extends UI {
+ private static final String EXTRA_ID = "EXTRA_ID";
+ private static final String EXTRA_ROBOT = "EXTRA_ROBOT";
+
+ public static final int REQUEST_CODE = 0x10;
+ public static final String RESULT_TYPE = "type";
+ public static final String RESULT_DATA = "data";
+
+ private AitContactAdapter adapter;
+
+ private String teamId;
+
+ private boolean addRobot;
+
+ private List items;
+
+ public static void start(Context context, String tid, boolean addRobot) {
+ Intent intent = new Intent();
+ if (tid != null) {
+ intent.putExtra(EXTRA_ID, tid);
+ }
+ if (addRobot) {
+ intent.putExtra(EXTRA_ROBOT, true);
+ }
+ intent.setClass(context, AitContactSelectorActivity.class);
+
+ ((Activity) context).startActivityForResult(intent, REQUEST_CODE);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.nim_team_member_list_layout);
+ parseIntent();
+ initViews();
+ initData();
+ }
+
+ private void initViews() {
+ RecyclerView recyclerView = (RecyclerView) findViewById(R.id.member_list);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ initAdapter(recyclerView);
+ ToolBarOptions options = new NimToolBarOptions();
+ options.titleString = ResUtil.getString(R.string.ait_selector_aitcontactselectoractivity_01);
+ setToolBar(R.id.toolbar, options);
+ }
+
+ private void initAdapter(RecyclerView recyclerView) {
+ items = new ArrayList<>();
+ adapter = new AitContactAdapter(recyclerView, items);
+ recyclerView.setAdapter(adapter);
+
+ List noDividerViewTypes = new ArrayList<>(1);
+ noDividerViewTypes.add(ItemType.SIMPLE_LABEL);
+ recyclerView.addItemDecoration(new AitContactItemDecorationNIM(this, LinearLayoutManager.VERTICAL, noDividerViewTypes));
+
+ recyclerView.addOnItemTouchListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AitContactAdapter adapter, View view, int position) {
+ AitContactItem item = adapter.getItem(position);
+ Intent intent = new Intent();
+ intent.putExtra(RESULT_TYPE, item.getViewType());
+ if (item.getViewType() == ItemType.TEAM_MEMBER) {
+ intent.putExtra(RESULT_DATA, (TeamMember) item.getModel());
+ } else if (item.getViewType() == ItemType.ROBOT) {
+ intent.putExtra(RESULT_DATA, (NimRobotInfo) item.getModel());
+ }
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+ });
+ }
+
+ private void parseIntent() {
+ Intent intent = getIntent();
+ teamId = intent.getStringExtra(EXTRA_ID);
+ addRobot = intent.getBooleanExtra(EXTRA_ROBOT, false);
+ }
+
+ private void initData() {
+ items = new ArrayList();
+ if (addRobot) {
+ initRobot();
+ }
+ if (teamId != null) {
+ initTeamMemberAsync();
+ } else {
+ //data 加载结束,通知更新
+ adapter.setNewData(items);
+ }
+ }
+
+ private void initRobot() {
+ List robots = NimUIKit.getRobotInfoProvider().getAllRobotAccounts();
+ if (robots != null && !robots.isEmpty()) {
+ items.add(0, new AitContactItem(ItemType.SIMPLE_LABEL, ResUtil.getString(R.string.ait_selector_aitcontactselectoractivity_02)));
+ for (NimRobotInfo robot : robots) {
+ items.add(new AitContactItem(ItemType.ROBOT, robot));
+ }
+ }
+ }
+
+ private void initTeamMemberAsync() {
+ Team t = NimUIKit.getTeamProvider().getTeamById(teamId);
+ if (t != null) {
+ updateTeamMember(t);
+ } else {
+ NimUIKit.getTeamProvider().fetchTeamById(teamId, new SimpleCallback() {
+ @Override
+ public void onResult(boolean success, Team result, int code) {
+ if (success && result != null) {
+ // 继续加载群成员
+ updateTeamMember(result);
+ } else {
+ //data 加载结束,通知更新
+ adapter.setNewData(items);
+ }
+ }
+ });
+ }
+ }
+
+ private void updateTeamMember(Team team) {
+ NimUIKit.getTeamProvider().fetchTeamMemberList(teamId, new SimpleCallback>() {
+ @Override
+ public void onResult(boolean success, List members, int code) {
+ if (success && members != null && !members.isEmpty()) {
+ // filter self
+ for (TeamMember member : members) {
+ if (member.getAccount().equals(NimUIKit.getAccount())) {
+ members.remove(member);
+ break;
+ }
+ }
+
+ if (!members.isEmpty()) {
+ items.add(new AitContactItem(ItemType.SIMPLE_LABEL, ResUtil.getString(R.string.ait_selector_aitcontactselectoractivity_03)));
+ for (TeamMember member : members) {
+ items.add(new AitContactItem(ItemType.TEAM_MEMBER, member));
+ }
+ }
+ }
+ //data 加载结束,通知更新
+ adapter.setNewData(items);
+ }
+ });
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/adapter/AitContactAdapter.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/adapter/AitContactAdapter.java
new file mode 100644
index 0000000..a1b6aec
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/adapter/AitContactAdapter.java
@@ -0,0 +1,38 @@
+package com.netease.nim.uikit.business.ait.selector.adapter;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.ait.selector.holder.RobotViewHolder;
+import com.netease.nim.uikit.business.ait.selector.holder.SimpleLabelViewHolder;
+import com.netease.nim.uikit.business.ait.selector.holder.TeamMemberViewHolder;
+import com.netease.nim.uikit.business.ait.selector.model.AitContactItem;
+import com.netease.nim.uikit.business.ait.selector.model.ItemType;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemQuickAdapter;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.NIMBaseViewHolder;
+
+import java.util.List;
+
+/**
+ * Created by hzchenkang on 2017/6/21.
+ */
+
+public class AitContactAdapter extends BaseMultiItemQuickAdapter {
+
+ public AitContactAdapter(RecyclerView recyclerView, List data) {
+ super(recyclerView, data);
+ addItemType(ItemType.SIMPLE_LABEL, R.layout.nim_ait_contact_label_item, SimpleLabelViewHolder.class);
+ addItemType(ItemType.ROBOT, R.layout.nim_ait_contact_robot_item, RobotViewHolder.class);
+ addItemType(ItemType.TEAM_MEMBER, R.layout.nim_ait_contact_team_member_item, TeamMemberViewHolder.class);
+ }
+
+ @Override
+ protected int getViewType(AitContactItem item) {
+ return item.getViewType();
+ }
+
+ @Override
+ protected String getItemKey(AitContactItem item) {
+ return "" + item.getViewType() + item.hashCode();
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/holder/RobotViewHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/holder/RobotViewHolder.java
new file mode 100644
index 0000000..92a502f
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/holder/RobotViewHolder.java
@@ -0,0 +1,42 @@
+package com.netease.nim.uikit.business.ait.selector.holder;
+
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.ait.selector.model.AitContactItem;
+import com.netease.nim.uikit.common.ui.imageview.HeadImageView;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseQuickAdapter;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.NIMBaseViewHolder;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.RecyclerViewHolder;
+import com.netease.nimlib.sdk.robot.model.NimRobotInfo;
+
+/**
+ * Created by hzchenkang on 2017/6/21.
+ */
+
+public class RobotViewHolder extends RecyclerViewHolder> {
+
+ private HeadImageView headImageView;
+ private TextView nameTextView;
+
+ public RobotViewHolder(BaseQuickAdapter adapter) {
+ super(adapter);
+ }
+
+ @Override
+ public void convert(NIMBaseViewHolder holder, AitContactItem data, int position, boolean isScrolling) {
+ inflate(holder);
+ refresh(data.getModel());
+ }
+
+ public void inflate(NIMBaseViewHolder holder) {
+ headImageView = holder.getView(R.id.imageViewHeader);
+ nameTextView = holder.getView(R.id.textViewName);
+ }
+
+ public void refresh(NimRobotInfo robot) {
+ headImageView.resetImageView();
+ nameTextView.setText(robot.getName());
+ headImageView.loadAvatar(robot.getAvatar());
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/holder/SimpleLabelViewHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/holder/SimpleLabelViewHolder.java
new file mode 100644
index 0000000..fb68492
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/holder/SimpleLabelViewHolder.java
@@ -0,0 +1,36 @@
+package com.netease.nim.uikit.business.ait.selector.holder;
+
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.ait.selector.model.AitContactItem;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseQuickAdapter;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.NIMBaseViewHolder;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.RecyclerViewHolder;
+
+/**
+ * Created by hzchenkang on 2017/6/21.
+ */
+
+public class SimpleLabelViewHolder extends RecyclerViewHolder> {
+
+ private TextView textView;
+
+ public SimpleLabelViewHolder(BaseQuickAdapter adapter) {
+ super(adapter);
+ }
+
+ @Override
+ public void convert(NIMBaseViewHolder holder, AitContactItem data, int position, boolean isScrolling) {
+ inflate(holder);
+ refresh(data.getModel());
+ }
+
+ public void inflate(NIMBaseViewHolder holder) {
+ textView = holder.getView(R.id.tv_label);
+ }
+
+ public void refresh(String label) {
+ textView.setText(label);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/holder/TeamMemberViewHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/holder/TeamMemberViewHolder.java
new file mode 100644
index 0000000..3878eb1
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/holder/TeamMemberViewHolder.java
@@ -0,0 +1,44 @@
+package com.netease.nim.uikit.business.ait.selector.holder;
+
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.ait.selector.model.AitContactItem;
+import com.netease.nim.uikit.business.team.helper.TeamHelper;
+import com.netease.nim.uikit.common.ui.imageview.HeadImageView;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseQuickAdapter;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.NIMBaseViewHolder;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.RecyclerViewHolder;
+import com.netease.nimlib.sdk.team.model.TeamMember;
+
+/**
+ * Created by hzchenkang on 2017/6/21.
+ */
+
+public class TeamMemberViewHolder extends RecyclerViewHolder> {
+
+ private HeadImageView headImageView;
+
+ private TextView nameTextView;
+
+ public TeamMemberViewHolder(BaseQuickAdapter adapter) {
+ super(adapter);
+ }
+
+ @Override
+ public void convert(NIMBaseViewHolder holder, AitContactItem data, int position, boolean isScrolling) {
+ inflate(holder);
+ refresh(data.getModel());
+ }
+
+ public void inflate(NIMBaseViewHolder holder) {
+ headImageView = holder.getView(R.id.imageViewHeader);
+ nameTextView = holder.getView(R.id.textViewName);
+ }
+
+ public void refresh(TeamMember member) {
+ headImageView.resetImageView();
+ nameTextView.setText(TeamHelper.getTeamMemberDisplayName(member.getTid(), member.getAccount()));
+ headImageView.loadBuddyAvatar(member.getAccount());
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/model/AitContactItem.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/model/AitContactItem.java
new file mode 100644
index 0000000..49acfd2
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/model/AitContactItem.java
@@ -0,0 +1,27 @@
+package com.netease.nim.uikit.business.ait.selector.model;
+
+/**
+ * Created by hzchenkang on 2017/6/21.
+ */
+
+public class AitContactItem {
+
+ // view type
+ private int viewType;
+
+ // data
+ private T model;
+
+ public AitContactItem(int viewType, T model) {
+ this.viewType = viewType;
+ this.model = model;
+ }
+
+ public T getModel() {
+ return model;
+ }
+
+ public int getViewType() {
+ return viewType;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/model/ItemType.java b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/model/ItemType.java
new file mode 100644
index 0000000..990adff
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/ait/selector/model/ItemType.java
@@ -0,0 +1,11 @@
+package com.netease.nim.uikit.business.ait.selector.model;
+
+import com.netease.nim.uikit.business.ait.AitContactType;
+
+/**
+ * Created by hzchenkang on 2017/7/13.
+ */
+
+public interface ItemType extends AitContactType {
+ int SIMPLE_LABEL = 0;
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/adapter/ChatRoomMsgAdapter.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/adapter/ChatRoomMsgAdapter.java
new file mode 100644
index 0000000..8d28830
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/adapter/ChatRoomMsgAdapter.java
@@ -0,0 +1,246 @@
+package com.netease.nim.uikit.business.chatroom.adapter;
+
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.chatroom.viewholder.ChatRoomMsgViewHolderBase;
+import com.netease.nim.uikit.business.chatroom.viewholder.ChatRoomMsgViewHolderFactory;
+import com.netease.nim.uikit.business.session.module.Container;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.NIMBaseViewHolder;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
+import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
+import com.netease.nimlib.sdk.msg.model.IMMessage;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Created by huangjun on 2016/12/21.
+ */
+public class ChatRoomMsgAdapter extends BaseMultiItemFetchLoadAdapter {
+
+ private Map, Integer> holder2ViewType;
+
+ private ViewHolderEventListener eventListener;
+ private Map progresses; // 有文件传输,需要显示进度条的消息ID map
+ private String messageId;
+ private Container container;
+
+ public ChatRoomMsgAdapter(RecyclerView recyclerView, List data, Container container) {
+ super(recyclerView, data);
+
+ timedItems = new HashSet<>();
+ progresses = new HashMap<>();
+
+ // view type, view holder
+ holder2ViewType = new HashMap<>();
+ List> holders = ChatRoomMsgViewHolderFactory.getAllViewHolders();
+ int viewType = 0;
+ for (Class extends ChatRoomMsgViewHolderBase> holder : holders) {
+ viewType++;
+ addItemType(viewType, R.layout.nim_message_item, holder);
+ holder2ViewType.put(holder, viewType);
+ }
+
+ this.container = container;
+ }
+
+ @Override
+ protected int getViewType(ChatRoomMessage message) {
+ return holder2ViewType.get(ChatRoomMsgViewHolderFactory.getViewHolderByType(message));
+ }
+
+ @Override
+ protected String getItemKey(ChatRoomMessage item) {
+ return item.getUuid();
+ }
+
+ public void setEventListener(ViewHolderEventListener eventListener) {
+ this.eventListener = eventListener;
+ }
+
+ public ViewHolderEventListener getEventListener() {
+ return eventListener;
+ }
+
+ public void deleteItem(IMMessage message, boolean isRelocateTime) {
+ if (message == null) {
+ return;
+ }
+
+ int index = 0;
+ for (IMMessage item : getData()) {
+ if (item.isTheSame(message)) {
+ break;
+ }
+ ++index;
+ }
+
+ if (index < getDataSize()) {
+ remove(index);
+ if (isRelocateTime) {
+ relocateShowTimeItemAfterDelete(message, index);
+ }
+ notifyDataSetChanged(); // 可以不要!!!
+ }
+ }
+
+ public float getProgress(IMMessage message) {
+ Float progress = progresses.get(message.getUuid());
+ return progress == null ? 0 : progress;
+ }
+
+ public void putProgress(IMMessage message, float progress) {
+ progresses.put(message.getUuid(), progress);
+ }
+
+ /**
+ * *********************** 时间显示处理 ***********************
+ */
+
+ private Set timedItems; // 需要显示消息时间的消息ID
+ private IMMessage lastShowTimeItem; // 用于消息时间显示,判断和上条消息间的时间间隔
+
+ public boolean needShowTime(IMMessage message) {
+ return timedItems.contains(message.getUuid());
+ }
+
+ /**
+ * 列表加入新消息时,更新时间显示
+ */
+ public void updateShowTimeItem(List items, boolean fromStart, boolean update) {
+ IMMessage anchor = fromStart ? null : lastShowTimeItem;
+ for (IMMessage message : items) {
+ if (setShowTimeFlag(message, anchor)) {
+ anchor = message;
+ }
+ }
+
+ if (update) {
+ lastShowTimeItem = anchor;
+ }
+ }
+
+ /**
+ * 是否显示时间item
+ */
+ private boolean setShowTimeFlag(IMMessage message, IMMessage anchor) {
+ boolean update = false;
+
+ if (hideTimeAlways(message)) {
+ setShowTime(message, false);
+ } else {
+ if (anchor == null) {
+ setShowTime(message, true);
+ update = true;
+ } else {
+ long time = anchor.getTime();
+ long now = message.getTime();
+
+ if (now - time == 0) {
+ // 消息撤回时使用
+ setShowTime(message, true);
+ lastShowTimeItem = message;
+ update = true;
+ } else if (now - time < (long) (5 * 60 * 1000)) {
+ setShowTime(message, false);
+ } else {
+ setShowTime(message, true);
+ update = true;
+ }
+ }
+ }
+
+ return update;
+ }
+
+ private void setShowTime(IMMessage message, boolean show) {
+ if (show) {
+ timedItems.add(message.getUuid());
+ } else {
+ timedItems.remove(message.getUuid());
+ }
+ }
+
+ private void relocateShowTimeItemAfterDelete(IMMessage messageItem, int index) {
+ // 如果被删的项显示了时间,需要继承
+ if (needShowTime(messageItem)) {
+ setShowTime(messageItem, false);
+ if (getDataSize() > 0) {
+ IMMessage nextItem;
+ if (index == getDataSize()) {
+ //删除的是最后一项
+ nextItem = getItem(index - 1);
+ } else {
+ //删除的不是最后一项
+ nextItem = getItem(index);
+ }
+
+ // 增加其他不需要显示时间的消息类型判断
+ if (hideTimeAlways(nextItem)) {
+ setShowTime(nextItem, false);
+ if (lastShowTimeItem != null && lastShowTimeItem != null
+ && lastShowTimeItem.isTheSame(messageItem)) {
+ lastShowTimeItem = null;
+ for (int i = getDataSize() - 1; i >= 0; i--) {
+ IMMessage item = getItem(i);
+ if (needShowTime(item)) {
+ lastShowTimeItem = item;
+ break;
+ }
+ }
+ }
+ } else {
+ setShowTime(nextItem, true);
+ if (lastShowTimeItem == null
+ || (lastShowTimeItem != null && lastShowTimeItem.isTheSame(messageItem))) {
+ lastShowTimeItem = nextItem;
+ }
+ }
+ } else {
+ lastShowTimeItem = null;
+ }
+ }
+ }
+
+ private boolean hideTimeAlways(IMMessage message) {
+ if (message.getSessionType() == SessionTypeEnum.ChatRoom) {
+ return true;
+ }
+ switch (message.getMsgType()) {
+ case notification:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public interface ViewHolderEventListener {
+ // 长按事件响应处理
+ boolean onViewHolderLongClick(View clickView, View viewHolderView, IMMessage item);
+
+ // 发送失败或者多媒体文件下载失败指示按钮点击响应处理
+ void onFailedBtnClick(IMMessage resendMessage);
+
+ // viewholder footer按钮点击,如机器人继续会话
+ void onFooterClick(ChatRoomMsgViewHolderBase holderBase, IMMessage message);
+ }
+
+ public void setUuid(String messageId) {
+ this.messageId = messageId;
+ }
+
+ public String getUuid() {
+ return messageId;
+ }
+
+ public Container getContainer() {
+ return container;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/fragment/ChatRoomMessageFragment.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/fragment/ChatRoomMessageFragment.java
new file mode 100644
index 0000000..fabe6ca
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/fragment/ChatRoomMessageFragment.java
@@ -0,0 +1,276 @@
+package com.netease.nim.uikit.business.chatroom.fragment;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.api.model.chatroom.ChatRoomSessionCustomization;
+import com.netease.nim.uikit.business.ait.AitManager;
+import com.netease.nim.uikit.business.chatroom.helper.ChatRoomHelper;
+import com.netease.nim.uikit.business.chatroom.module.ChatRoomInputPanel;
+import com.netease.nim.uikit.business.chatroom.module.ChatRoomMsgListPanel;
+import com.netease.nim.uikit.business.session.actions.BaseAction;
+import com.netease.nim.uikit.business.session.module.Container;
+import com.netease.nim.uikit.business.session.module.ModuleProxy;
+import com.netease.nim.uikit.common.fragment.TFragment;
+import com.netease.nim.uikit.impl.NimUIKitImpl;
+import com.netease.nimlib.sdk.NIMClient;
+import com.netease.nimlib.sdk.Observer;
+import com.netease.nimlib.sdk.RequestCallback;
+import com.netease.nimlib.sdk.ResponseCode;
+import com.netease.nimlib.sdk.chatroom.ChatRoomMessageBuilder;
+import com.netease.nimlib.sdk.chatroom.ChatRoomService;
+import com.netease.nimlib.sdk.chatroom.ChatRoomServiceObserver;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
+import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
+import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
+import com.netease.nimlib.sdk.msg.model.IMMessage;
+import com.netease.nimlib.sdk.robot.model.NimRobotInfo;
+import com.netease.nimlib.sdk.robot.model.RobotAttachment;
+import com.netease.nimlib.sdk.robot.model.RobotMsgType;
+import com.chwl.library.utils.ResUtil;
+import com.chwl.library.utils.SingleToastUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 聊天室直播互动fragment
+ * 可以直接集成到应用中
+ */
+public class ChatRoomMessageFragment extends TFragment implements ModuleProxy {
+ private String roomId;
+ protected View rootView;
+ private static ChatRoomSessionCustomization customization;
+
+ // modules
+ protected ChatRoomInputPanel inputPanel;
+ protected ChatRoomMsgListPanel messageListPanel;
+ protected AitManager aitManager;
+
+ public static void setChatRoomSessionCustomization(ChatRoomSessionCustomization roomSessionCustomization) {
+ customization = roomSessionCustomization;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ rootView = inflater.inflate(R.layout.nim_chat_room_message_fragment, container, false);
+ return rootView;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (inputPanel != null) {
+ inputPanel.onPause();
+ }
+ if (messageListPanel != null) {
+ messageListPanel.onPause();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (messageListPanel != null) {
+ messageListPanel.onResume();
+ }
+ }
+
+ public boolean onBackPressed() {
+ if (inputPanel != null && inputPanel.collapse(true)) {
+ return true;
+ }
+
+ if (messageListPanel != null && messageListPanel.onBackPressed()) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ registerObservers(false);
+
+ if (messageListPanel != null) {
+ messageListPanel.onDestroy();
+ }
+ if (aitManager != null) {
+ aitManager.reset();
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (aitManager != null) {
+ aitManager.onActivityResult(requestCode, resultCode, data);
+ }
+
+ inputPanel.onActivityResult(requestCode, resultCode, data);
+ }
+
+ public void onLeave() {
+ if (inputPanel != null) {
+ inputPanel.collapse(false);
+ }
+ }
+
+ public void init(String roomId) {
+ this.roomId = roomId;
+ registerObservers(true);
+ findViews();
+ }
+
+ private void findViews() {
+ Container container = new Container(getActivity(), roomId, SessionTypeEnum.ChatRoom, this);
+ if (messageListPanel == null) {
+ messageListPanel = new ChatRoomMsgListPanel(container, rootView);
+ } else {
+ messageListPanel.reload(container);
+ }
+
+ if (inputPanel == null) {
+ inputPanel = new ChatRoomInputPanel(container, rootView, getActionList(), false);
+ } else {
+ inputPanel.reload(container, null);
+ }
+
+ if (NimUIKitImpl.getOptions().aitEnable && NimUIKitImpl.getOptions().aitChatRoomRobot) {
+ if (aitManager == null) {
+ aitManager = new AitManager(getContext(), null, true);
+ }
+ inputPanel.addAitTextWatcher(aitManager);
+ aitManager.setTextChangeListener(inputPanel);
+ }
+ }
+
+ private void registerObservers(boolean register) {
+ NIMClient.getService(ChatRoomServiceObserver.class).observeReceiveMessage(incomingChatRoomMsg, register);
+ }
+
+ Observer> incomingChatRoomMsg = new Observer>() {
+ @Override
+ public void onEvent(List messages) {
+ if (messages == null || messages.isEmpty()) {
+ return;
+ }
+
+ messageListPanel.onIncomingMessage(messages);
+ }
+ };
+
+ /************************** Module proxy ***************************/
+
+ @Override
+ public boolean sendMessage(IMMessage msg) {
+ ChatRoomMessage message = (ChatRoomMessage) msg;
+
+ // 检查是否转换成机器人消息
+ message = changeToRobotMsg(message);
+
+ ChatRoomHelper.buildMemberTypeInRemoteExt(message, roomId);
+
+ ChatRoomMessage finalMessage = message;
+ NIMClient.getService(ChatRoomService.class).sendMessage(message, false)
+ .setCallback(new RequestCallback() {
+ @Override
+ public void onSuccess(Void param) {
+ }
+
+ @Override
+ public void onFailed(int code) {
+ if (code == ResponseCode.RES_CHATROOM_MUTED) {
+// Toast.makeText(NimUIKit.getContext(), ResUtil.getString(R.string.chatroom_fragment_chatroommessagefragment_01), Toast.LENGTH_SHORT).show();
+ SingleToastUtil.showToastShort(ResUtil.getString(R.string.chatroom_fragment_chatroommessagefragment_02));
+ } else if (code == ResponseCode.RES_CHATROOM_ROOM_MUTED) {
+// Toast.makeText(NimUIKit.getContext(), ResUtil.getString(R.string.chatroom_fragment_chatroommessagefragment_03), Toast.LENGTH_SHORT).show();
+ SingleToastUtil.showToastShort(ResUtil.getString(R.string.chatroom_fragment_chatroommessagefragment_04));
+ } else {
+// Toast.makeText(NimUIKit.getContext(), ResUtil.getString(R.string.chatroom_fragment_chatroommessagefragment_05) + code, Toast.LENGTH_SHORT).show();
+ SingleToastUtil.showToastShort(ResUtil.getString(R.string.chatroom_fragment_chatroommessagefragment_06) + code);
+ }
+ }
+
+ @Override
+ public void onException(Throwable exception) {
+// Toast.makeText(NimUIKit.getContext(), ResUtil.getString(R.string.chatroom_fragment_chatroommessagefragment_07), Toast.LENGTH_SHORT).show();
+ SingleToastUtil.showToastShort(ResUtil.getString(R.string.chatroom_fragment_chatroommessagefragment_08));
+ }
+ });
+ messageListPanel.onMsgSend(message);
+ if (aitManager != null) {
+ aitManager.reset();
+ }
+ return true;
+ }
+
+ private ChatRoomMessage changeToRobotMsg(ChatRoomMessage message) {
+ if (aitManager == null) {
+ return message;
+ }
+ if (message.getMsgType() == MsgTypeEnum.robot) {
+ return message;
+ }
+ String robotAccount = aitManager.getAitRobot();
+ if (TextUtils.isEmpty(robotAccount)) {
+ return message;
+ }
+ String text = message.getContent();
+ String content = aitManager.removeRobotAitString(text, robotAccount);
+ content = content.equals("") ? " " : content;
+ message = ChatRoomMessageBuilder.createRobotMessage(roomId, robotAccount, text, RobotMsgType.TEXT, content, null, null);
+
+ return message;
+ }
+
+ @Override
+ public void onInputPanelExpand() {
+ messageListPanel.scrollToBottom();
+ }
+
+ @Override
+ public void shouldCollapseInputPanel() {
+ inputPanel.collapse(false);
+ }
+
+ @Override
+ public void onItemFooterClick(IMMessage message) {
+ if (aitManager != null) {
+ RobotAttachment attachment = (RobotAttachment) message.getAttachment();
+ NimRobotInfo robot = NimUIKit.getRobotInfoProvider().getRobotByAccount(attachment.getFromRobotAccount());
+ aitManager.insertAitRobot(robot.getAccount(), robot.getName(), inputPanel.getEditSelectionStart());
+ }
+ }
+
+ @Override
+ public void onAudioClick(Runnable runnable) {
+
+ }
+
+ @Override
+ public boolean isLongClickEnabled() {
+ return !inputPanel.isRecording();
+ }
+
+ // 操作面板集合
+ protected List getActionList() {
+ List actions = new ArrayList<>();
+ if (customization != null) {
+ actions.addAll(customization.actions);
+ }
+
+ return actions;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/helper/ChatRoomHelper.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/helper/ChatRoomHelper.java
new file mode 100644
index 0000000..44f6435
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/helper/ChatRoomHelper.java
@@ -0,0 +1,34 @@
+package com.netease.nim.uikit.business.chatroom.helper;
+
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nimlib.sdk.chatroom.constant.MemberType;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMember;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by huangjun on 2017/9/18.
+ */
+public class ChatRoomHelper {
+ public static void buildMemberTypeInRemoteExt(ChatRoomMessage message, String roomId) {
+ Map ext = new HashMap<>();
+ ChatRoomMember chatRoomMember = NimUIKit.getChatRoomProvider().getChatRoomMember(roomId, NimUIKit.getAccount());
+ if (chatRoomMember != null && chatRoomMember.getMemberType() != null) {
+ ext.put("type", chatRoomMember.getMemberType().getValue());
+ message.setRemoteExtension(ext);
+ }
+ }
+
+ public static MemberType getMemberTypeByRemoteExt(ChatRoomMessage message) {
+ final String KEY = "type";
+ Map ext = message.getRemoteExtension();
+
+ if (ext == null || !ext.containsKey(KEY)) {
+ return MemberType.UNKNOWN;
+ }
+
+ return MemberType.typeOfValue((Integer) ext.get(KEY));
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/helper/ChatRoomNotificationHelper.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/helper/ChatRoomNotificationHelper.java
new file mode 100644
index 0000000..7114f1e
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/helper/ChatRoomNotificationHelper.java
@@ -0,0 +1,131 @@
+package com.netease.nim.uikit.business.chatroom.helper;
+
+import android.text.TextUtils;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomNotificationAttachment;
+import com.chwl.library.utils.ResUtil;
+
+import java.util.List;
+
+/**
+ * Created by huangjun on 2016/1/13.
+ */
+public class ChatRoomNotificationHelper {
+ public static String getNotificationText(ChatRoomNotificationAttachment attachment) {
+ if (attachment == null) {
+ return "";
+ }
+
+ String targets = getTargetNicks(attachment);
+ String text;
+ switch (attachment.getType()) {
+ case ChatRoomMemberIn:
+ text = buildText(ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_01), targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_02));
+ break;
+ case ChatRoomMemberExit:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_03));
+ break;
+ case ChatRoomMemberBlackAdd:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_04));
+ break;
+ case ChatRoomMemberBlackRemove:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_05));
+ break;
+ case ChatRoomMemberMuteAdd:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_06));
+ break;
+ case ChatRoomMemberMuteRemove:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_07));
+ break;
+ case ChatRoomManagerAdd:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_08));
+ break;
+ case ChatRoomManagerRemove:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_09));
+ break;
+ case ChatRoomCommonAdd:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_010));
+ break;
+ case ChatRoomCommonRemove:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_011));
+ break;
+ case ChatRoomClose:
+ text = buildText(ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_012));
+ break;
+ case ChatRoomInfoUpdated:
+ text = buildText(ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_013));
+ break;
+ case ChatRoomMemberKicked:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_014));
+ break;
+ case ChatRoomMemberTempMuteAdd:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_015));
+ break;
+ case ChatRoomMemberTempMuteRemove:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_016));
+ break;
+ case ChatRoomMyRoomRoleUpdated:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_017));
+ break;
+ case ChatRoomQueueChange:
+ text = buildText(targets, ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_018));
+ break;
+ case ChatRoomRoomMuted:
+ text = buildText(ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_019));
+ break;
+ case ChatRoomRoomDeMuted:
+ text = buildText(ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_020));
+ break;
+ case ChatRoomQueueBatchChange:
+ text = buildText(ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_021));
+ break;
+ default:
+ text = attachment.toString();
+ break;
+ }
+
+ return text;
+ }
+
+ private static String getTargetNicks(final ChatRoomNotificationAttachment attachment) {
+ StringBuilder sb = new StringBuilder();
+ List accounts = attachment.getTargets();
+ List targets = attachment.getTargetNicks();
+ if (attachment.getTargetNicks() != null) {
+ for (int i = 0; i < targets.size(); i++) {
+ sb.append(NimUIKit.getAccount().equals(accounts.get(i)) ? ResUtil.getString(R.string.chatroom_helper_chatroomnotificationhelper_022) : targets.get(i));
+ sb.append(",");
+ }
+ sb.deleteCharAt(sb.length() - 1);
+ }
+
+ return sb.toString();
+ }
+
+ private static String buildText(String pre, String targets, String operate) {
+ StringBuilder sb = new StringBuilder();
+ if (!TextUtils.isEmpty(pre)) {
+ sb.append(pre);
+ }
+
+ if (!TextUtils.isEmpty(targets)) {
+ sb.append(targets);
+ }
+
+ if (!TextUtils.isEmpty(operate)) {
+ sb.append(operate);
+ }
+
+ return sb.toString();
+ }
+
+ private static String buildText(String targets, String operate) {
+ return buildText(null, targets, operate);
+ }
+
+ private static String buildText(String operate) {
+ return buildText(null, operate);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/module/ChatRoomInputPanel.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/module/ChatRoomInputPanel.java
new file mode 100644
index 0000000..5a4bdf5
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/module/ChatRoomInputPanel.java
@@ -0,0 +1,30 @@
+package com.netease.nim.uikit.business.chatroom.module;
+
+import android.view.View;
+
+import com.netease.nim.uikit.business.session.actions.BaseAction;
+import com.netease.nim.uikit.business.session.module.Container;
+import com.netease.nim.uikit.business.session.module.input.InputPanel;
+import com.netease.nimlib.sdk.chatroom.ChatRoomMessageBuilder;
+import com.netease.nimlib.sdk.msg.model.IMMessage;
+
+import java.util.List;
+
+/**
+ * Created by huangjun on 2017/9/18.
+ */
+public class ChatRoomInputPanel extends InputPanel {
+
+ public ChatRoomInputPanel(Container container, View view, List actions, boolean isTextAudioSwitchShow) {
+ super(container, view, actions, isTextAudioSwitchShow);
+ }
+
+ public ChatRoomInputPanel(Container container, View view, List actions) {
+ super(container, view, actions);
+ }
+
+ @Override
+ protected IMMessage createTextMessage(String text) {
+ return ChatRoomMessageBuilder.createChatRoomTextMessage(container.account, text);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/module/ChatRoomMsgListPanel.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/module/ChatRoomMsgListPanel.java
new file mode 100644
index 0000000..da6799e
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/module/ChatRoomMsgListPanel.java
@@ -0,0 +1,503 @@
+package com.netease.nim.uikit.business.chatroom.module;
+
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.View;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.business.chatroom.adapter.ChatRoomMsgAdapter;
+import com.netease.nim.uikit.business.chatroom.viewholder.ChatRoomMsgViewHolderBase;
+import com.netease.nim.uikit.business.preference.UserPreferences;
+import com.netease.nim.uikit.business.robot.parser.elements.group.LinkElement;
+import com.netease.nim.uikit.business.session.audio.MessageAudioControl;
+import com.netease.nim.uikit.business.session.module.Container;
+import com.netease.nim.uikit.business.session.viewholder.robot.RobotLinkView;
+import com.netease.nim.uikit.common.ui.dialog.EasyAlertDialog;
+import com.netease.nim.uikit.common.ui.dialog.EasyAlertDialogHelper;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseFetchLoadAdapter;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.IRecyclerView;
+import com.netease.nim.uikit.common.ui.recyclerview.listener.OnItemClickListener;
+import com.netease.nim.uikit.common.ui.recyclerview.loadmore.MsgListFetchLoadMoreView;
+import com.netease.nim.uikit.common.util.AntiSpamUtil;
+import com.netease.nimlib.sdk.NIMClient;
+import com.netease.nimlib.sdk.Observer;
+import com.netease.nimlib.sdk.RequestCallback;
+import com.netease.nimlib.sdk.RequestCallbackWrapper;
+import com.netease.nimlib.sdk.ResponseCode;
+import com.netease.nimlib.sdk.chatroom.ChatRoomMessageBuilder;
+import com.netease.nimlib.sdk.chatroom.ChatRoomService;
+import com.netease.nimlib.sdk.chatroom.ChatRoomServiceObserver;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
+import com.netease.nimlib.sdk.msg.attachment.FileAttachment;
+import com.netease.nimlib.sdk.msg.constant.MsgDirectionEnum;
+import com.netease.nimlib.sdk.msg.constant.MsgStatusEnum;
+import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
+import com.netease.nimlib.sdk.msg.model.AttachmentProgress;
+import com.netease.nimlib.sdk.msg.model.IMMessage;
+import com.netease.nimlib.sdk.msg.model.QueryDirectionEnum;
+import com.netease.nimlib.sdk.robot.model.RobotAttachment;
+import com.netease.nimlib.sdk.robot.model.RobotMsgType;
+import com.chwl.library.utils.ResUtil;
+import com.chwl.library.utils.SingleToastUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * 聊天室消息收发模块
+ * Created by huangjun on 2016/1/27.
+ */
+public class ChatRoomMsgListPanel {
+ private static final int MESSAGE_CAPACITY = 500;
+
+ // container
+ private Container container;
+ private View rootView;
+ private Handler uiHandler;
+
+ // message list view
+ private RecyclerView messageListView;
+ private LinkedList items;
+ private ChatRoomMsgAdapter adapter;
+
+ public ChatRoomMsgListPanel(Container container, View rootView) {
+ this.container = container;
+ this.rootView = rootView;
+
+ init();
+ }
+
+ public void onResume() {
+ setEarPhoneMode(UserPreferences.isEarPhoneModeEnable(), false);
+ }
+
+ public void onPause() {
+ MessageAudioControl.getInstance(container.activity).stopAudio();
+ }
+
+ public void onDestroy() {
+ registerObservers(false);
+ }
+
+ public boolean onBackPressed() {
+ uiHandler.removeCallbacks(null);
+ MessageAudioControl.getInstance(container.activity).stopAudio(); // 界面返回,停止语音播放
+ return false;
+ }
+
+ public void reload(Container container) {
+ this.container = container;
+ if (adapter != null) {
+ adapter.clearData();
+ }
+ }
+
+ private void init() {
+ initListView();
+ this.uiHandler = new Handler(NimUIKit.getContext().getMainLooper());
+ registerObservers(true);
+ }
+
+ private void initListView() {
+ // RecyclerView
+ messageListView = (RecyclerView) rootView.findViewById(R.id.messageListView);
+ messageListView.setLayoutManager(new LinearLayoutManager(container.activity));
+ messageListView.requestDisallowInterceptTouchEvent(true);
+ messageListView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ super.onScrollStateChanged(recyclerView, newState);
+ if (newState != RecyclerView.SCROLL_STATE_IDLE) {
+ container.proxy.shouldCollapseInputPanel();
+ }
+ }
+ });
+ messageListView.setOverScrollMode(View.OVER_SCROLL_NEVER);
+
+ // adapter
+ items = new LinkedList<>();
+ adapter = new ChatRoomMsgAdapter(messageListView, items, container);
+ adapter.closeLoadAnimation();
+ adapter.setFetchMoreView(new MsgListFetchLoadMoreView());
+ adapter.setLoadMoreView(new MsgListFetchLoadMoreView());
+ adapter.setEventListener(new MsgItemEventListener());
+ adapter.setOnFetchMoreListener(new MessageLoader()); // load from start
+ messageListView.setAdapter(adapter);
+
+ messageListView.addOnItemTouchListener(listener);
+ }
+
+ public void onIncomingMessage(List messages) {
+ boolean needScrollToBottom = isLastMessageVisible();
+ boolean needRefresh = false;
+ List addedListItems = new ArrayList<>(messages.size());
+ for (ChatRoomMessage message : messages) {
+ // 保证显示到界面上的消息,来自同一个聊天室
+ if (isMyMessage(message)) {
+ saveMessage(message);
+ addedListItems.add(message);
+ needRefresh = true;
+ }
+ }
+ if (needRefresh) {
+ adapter.notifyDataSetChanged();
+ }
+
+ // incoming messages tip
+ ChatRoomMessage lastMsg = messages.get(messages.size() - 1);
+ if (isMyMessage(lastMsg) && needScrollToBottom) {
+ doScrollToBottom();
+ }
+ }
+
+ private boolean isLastMessageVisible() {
+ LinearLayoutManager layoutManager = (LinearLayoutManager) messageListView.getLayoutManager();
+ int lastVisiblePosition = layoutManager.findLastCompletelyVisibleItemPosition();
+ return lastVisiblePosition >= adapter.getBottomDataPosition();
+ }
+
+
+ // 发送消息后,更新本地消息列表
+ public void onMsgSend(ChatRoomMessage message) {
+ saveMessage(message);
+
+ adapter.notifyDataSetChanged();
+ doScrollToBottom();
+ }
+
+ public void saveMessage(final ChatRoomMessage message) {
+ if (message == null) {
+ return;
+ }
+
+ if (items.size() >= MESSAGE_CAPACITY) {
+ items.poll();
+ }
+
+ items.add(message);
+ }
+
+ /**
+ * *************** MessageLoader ***************
+ */
+ private class MessageLoader implements BaseFetchLoadAdapter.RequestLoadMoreListener, BaseFetchLoadAdapter.RequestFetchMoreListener {
+
+ private static final int LOAD_MESSAGE_COUNT = 10;
+
+ private IMMessage anchor;
+
+ private boolean firstLoad = true;
+
+ private boolean fetching = false;
+
+ public MessageLoader() {
+ anchor = null;
+ loadFromLocal();
+ }
+
+ private RequestCallback> callback = new RequestCallbackWrapper>() {
+ @Override
+ public void onResult(int code, List messages, Throwable exception) {
+ if (code == ResponseCode.RES_SUCCESS && messages != null) {
+ onMessageLoaded(messages);
+ }
+
+ fetching = false;
+ }
+ };
+
+ private void loadFromLocal() {
+ if (fetching) {
+ return;
+ }
+
+ fetching = true;
+ NIMClient.getService(ChatRoomService.class).pullMessageHistoryEx(container.account, anchor().getTime(), LOAD_MESSAGE_COUNT, QueryDirectionEnum.QUERY_OLD)
+ .setCallback(callback);
+ }
+
+ private IMMessage anchor() {
+ if (items.size() == 0) {
+ return (anchor == null ? ChatRoomMessageBuilder.createEmptyChatRoomMessage(container.account, 0) : anchor);
+ } else {
+ return items.get(0);
+ }
+ }
+
+ /**
+ * 历史消息加载处理
+ */
+ private void onMessageLoaded(List messages) {
+ int count = messages.size();
+
+ // 逆序
+ Collections.reverse(messages);
+ // 加入到列表中
+ if (count < LOAD_MESSAGE_COUNT) {
+ adapter.fetchMoreEnd(messages, true);
+ } else {
+ adapter.fetchMoreComplete(messages);
+ }
+
+ // 如果是第一次加载,updateShowTimeItem返回的就是lastShowTimeItem
+ if (firstLoad) {
+ doScrollToBottom();
+ }
+
+ firstLoad = false;
+ }
+
+ @Override
+ public void onFetchMoreRequested() {
+ loadFromLocal();
+ }
+
+ @Override
+ public void onLoadMoreRequested() {
+
+ }
+ }
+
+ /**
+ * ************************* 观察者 ********************************
+ */
+
+ private void registerObservers(boolean register) {
+ ChatRoomServiceObserver service = NIMClient.getService(ChatRoomServiceObserver.class);
+ service.observeMsgStatus(messageStatusObserver, register);
+ service.observeAttachmentProgress(attachmentProgressObserver, register);
+ }
+
+ /**
+ * 消息状态变化观察者
+ */
+ private Observer messageStatusObserver = new Observer() {
+ @Override
+ public void onEvent(ChatRoomMessage message) {
+ if (isMyMessage(message)) {
+ onMessageStatusChange(message);
+ }
+ }
+ };
+
+ /**
+ * 消息附件上传/下载进度观察者
+ */
+ private Observer attachmentProgressObserver = new Observer() {
+ @Override
+ public void onEvent(AttachmentProgress progress) {
+ onAttachmentProgressChange(progress);
+ }
+ };
+
+ private void onMessageStatusChange(IMMessage message) {
+ int index = getItemIndex(message.getUuid());
+ if (index >= 0 && index < items.size()) {
+ IMMessage item = items.get(index);
+ item.setStatus(message.getStatus());
+ item.setAttachStatus(message.getAttachStatus());
+ // 处理语音、音视频通话
+ if (item.getMsgType() == MsgTypeEnum.audio || item.getMsgType() == MsgTypeEnum.avchat) {
+ item.setAttachment(message.getAttachment()); // 附件可能更新了
+ }
+
+ refreshViewHolderByIndex(index);
+ }
+ }
+
+ private void onAttachmentProgressChange(AttachmentProgress progress) {
+ int index = getItemIndex(progress.getUuid());
+ if (index >= 0 && index < items.size()) {
+ IMMessage item = items.get(index);
+ float value = (float) progress.getTransferred() / (float) progress.getTotal();
+ adapter.putProgress(item, value);
+ refreshViewHolderByIndex(index);
+ }
+ }
+
+ public boolean isMyMessage(ChatRoomMessage message) {
+ return message.getSessionType() == container.sessionType
+ && message.getSessionId() != null
+ && message.getSessionId().equals(container.account);
+ }
+
+ /**
+ * 刷新单条消息
+ */
+ private void refreshViewHolderByIndex(final int index) {
+ container.activity.runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (index < 0) {
+ return;
+ }
+
+ adapter.notifyDataItemChanged(index);
+ }
+ });
+ }
+
+ private int getItemIndex(String uuid) {
+ for (int i = 0; i < items.size(); i++) {
+ IMMessage message = items.get(i);
+ if (TextUtils.equals(message.getUuid(), uuid)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private OnItemClickListener listener = new OnItemClickListener() {
+ @Override
+ public void onItemClick(IRecyclerView adapter, View view, int position) {
+
+ }
+
+ @Override
+ public void onItemLongClick(IRecyclerView adapter, View view, int position) {
+ }
+
+ @Override
+ public void onItemChildClick(IRecyclerView adapter2, View view, int position) {
+ if (view != null && view instanceof RobotLinkView) {
+ RobotLinkView robotLinkView = (RobotLinkView) view;
+ LinkElement element = robotLinkView.getElement();
+ if (element != null) {
+ element.getTarget();
+ if (LinkElement.TYPE_URL.equals(element.getType())) {
+ Intent intent = new Intent();
+ intent.setAction("android.intent.action.VIEW");
+ Uri content_url = Uri.parse(element.getTarget());
+ intent.setData(content_url);
+ try {
+ container.activity.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+// Toast.makeText(container.activity, ResUtil.getString(R.string.chatroom_module_chatroommsglistpanel_01), Toast.LENGTH_SHORT).show();
+ SingleToastUtil.showToastShort(ResUtil.getString(R.string.chatroom_module_chatroommsglistpanel_02));
+ }
+
+ } else if (LinkElement.TYPE_BLOCK.equals(element.getType())) {
+ // 发送点击的block
+ ChatRoomMessage message = adapter.getItem(position);
+ if (message != null) {
+ String robotAccount = ((RobotAttachment) message.getAttachment()).getFromRobotAccount();
+ ChatRoomMessage robotMsg = ChatRoomMessageBuilder.createRobotMessage(container.account, robotAccount,
+ robotLinkView.getShowContent(), RobotMsgType.LINK, "", element.getTarget(), element.getParams());
+ container.proxy.sendMessage(robotMsg);
+ }
+ }
+ }
+ }
+ }
+ };
+
+ private class MsgItemEventListener implements ChatRoomMsgAdapter.ViewHolderEventListener {
+
+ @Override
+ public void onFailedBtnClick(IMMessage message) {
+ if (message.getDirect() == MsgDirectionEnum.Out) {
+ // 发出的消息,如果是发送失败,直接重发,否则有可能是漫游到的多媒体消息,但文件下载
+ if (message.getStatus() == MsgStatusEnum.fail) {
+ if (AntiSpamUtil.isAntiSpam(message)) {
+ return;
+ }
+ resendMessage(message); // 重发
+ } else {
+ if (message.getAttachment() instanceof FileAttachment) {
+ FileAttachment attachment = (FileAttachment) message.getAttachment();
+ if (TextUtils.isEmpty(attachment.getPath())
+ && TextUtils.isEmpty(attachment.getThumbPath())) {
+ showReDownloadConfirmDlg(message);
+ }
+ } else {
+ resendMessage(message);
+ }
+ }
+ } else {
+ showReDownloadConfirmDlg(message);
+ }
+ }
+
+ @Override
+ public boolean onViewHolderLongClick(View clickView, View viewHolderView, IMMessage item) {
+ return true;
+ }
+
+ @Override
+ public void onFooterClick(ChatRoomMsgViewHolderBase viewHolderBase, IMMessage message) {
+ // 与 robot 对话
+ container.proxy.onItemFooterClick(message);
+ }
+
+ // 重新下载(对话框提示)
+ private void showReDownloadConfirmDlg(final IMMessage message) {
+ EasyAlertDialogHelper.OnDialogActionListener listener = new EasyAlertDialogHelper.OnDialogActionListener() {
+
+ @Override
+ public void doCancelAction() {
+ }
+
+ @Override
+ public void doOkAction() {
+ // 正常情况收到消息后附件会自动下载。如果下载失败,可调用该接口重新下载
+ if (message.getAttachment() != null && message.getAttachment() instanceof FileAttachment)
+ NIMClient.getService(ChatRoomService.class).downloadAttachment((ChatRoomMessage) message, true);
+ }
+ };
+
+ final EasyAlertDialog dialog = EasyAlertDialogHelper.createOkCancelDiolag(container.activity, null,
+ container.activity.getString(R.string.repeat_download_message), true, listener);
+ dialog.show();
+ }
+
+ // 重发消息到服务器
+ private void resendMessage(IMMessage message) {
+ // 重置状态为unsent
+ int index = getItemIndex(message.getUuid());
+ if (index >= 0 && index < items.size()) {
+ IMMessage item = items.get(index);
+ item.setStatus(MsgStatusEnum.sending);
+ refreshViewHolderByIndex(index);
+ }
+
+ NIMClient.getService(ChatRoomService.class)
+ .sendMessage((ChatRoomMessage) message, true)
+ .setCallback(new RequestCallbackWrapper() {
+ @Override
+ public void onResult(int code, Void result, Throwable exception) {
+ }
+ });
+ }
+ }
+
+ private void setEarPhoneMode(boolean earPhoneMode, boolean update) {
+ if (update) {
+ UserPreferences.setEarPhoneModeEnable(earPhoneMode);
+ }
+ MessageAudioControl.getInstance(container.activity).setEarPhoneModeEnable(earPhoneMode);
+ }
+
+ public void scrollToBottom() {
+ uiHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ doScrollToBottom();
+ }
+ }, 200);
+ }
+
+ private void doScrollToBottom() {
+ messageListView.scrollToPosition(adapter.getBottomDataPosition());
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderBase.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderBase.java
new file mode 100644
index 0000000..6287567
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderBase.java
@@ -0,0 +1,388 @@
+package com.netease.nim.uikit.business.chatroom.viewholder;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.chatroom.adapter.ChatRoomMsgAdapter;
+import com.netease.nim.uikit.common.ui.imageview.HeadImageView;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.NIMBaseViewHolder;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.RecyclerViewHolder;
+import com.netease.nim.uikit.common.util.sys.ScreenUtil;
+import com.netease.nim.uikit.common.util.sys.TimeUtil;
+import com.netease.nim.uikit.impl.NimUIKitImpl;
+import com.netease.nimlib.sdk.NIMClient;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
+import com.netease.nimlib.sdk.msg.MsgService;
+import com.netease.nimlib.sdk.msg.attachment.FileAttachment;
+import com.netease.nimlib.sdk.msg.constant.MsgDirectionEnum;
+import com.netease.nimlib.sdk.msg.constant.MsgStatusEnum;
+
+/**
+ * 会话窗口消息列表项的ViewHolder基类,负责每个消息项的外层框架,包括头像,昵称,发送/接收进度条,重发按钮等。
+ * 具体的消息展示项可继承该基类,然后完成具体消息内容展示即可。
+ */
+public abstract class ChatRoomMsgViewHolderBase extends RecyclerViewHolder {
+
+ public ChatRoomMsgViewHolderBase(BaseMultiItemFetchLoadAdapter adapter) {
+ super(adapter);
+ this.adapter = adapter;
+ }
+
+ // basic
+ protected View view;
+ protected Context context;
+ protected BaseMultiItemFetchLoadAdapter adapter;
+
+ // data
+ protected ChatRoomMessage message;
+
+ // view
+ protected View alertButton;
+ protected TextView timeTextView;
+ protected ProgressBar progressBar;
+ protected TextView nameTextView;
+ protected FrameLayout contentContainer;
+ protected LinearLayout nameContainer;
+ protected TextView readReceiptTextView;
+
+ private HeadImageView avatarLeft;
+ private HeadImageView avatarRight;
+
+ public ImageView nameIconView;
+
+ // contentContainerView的默认长按事件。如果子类需要不同的处理,可覆盖onItemLongClick方法
+ // 但如果某些子控件会拦截触摸消息,导致contentContainer收不到长按事件,子控件也可在inflate时重新设置
+ protected View.OnLongClickListener longClickListener;
+
+ /// -- 以下接口可由子类覆盖或实现
+ // 返回具体消息类型内容展示区域的layout res id
+ abstract protected int getContentResId();
+
+ // 在该接口中根据layout对各控件成员变量赋值
+ abstract protected void inflateContentView();
+
+ // 将消息数据项与内容的view进行绑定
+ abstract protected void bindContentView();
+
+ // 在该接口操作BaseViewHolder中的数据,进行事件绑定,可选
+ protected void bindHolder(NIMBaseViewHolder holder) {
+
+ }
+
+ // 内容区域点击事件响应处理。
+ protected void onItemClick() {
+ }
+
+ // 内容区域长按事件响应处理。该接口的优先级比adapter中有长按事件的处理监听高,当该接口返回为true时,adapter的长按事件监听不会被调用到。
+ protected boolean onItemLongClick() {
+ return false;
+ }
+
+ // 当是接收到的消息时,内容区域背景的drawable id
+ protected int leftBackground() {
+ return NimUIKitImpl.getOptions().chatRoomMsgLeftBackground;
+ }
+
+ // 当是发送出去的消息时,内容区域背景的drawable id
+ protected int rightBackground() {
+ return NimUIKitImpl.getOptions().chatRoomMsgRightBackground;
+ }
+
+ // 返回该消息是不是居中显示
+ protected boolean isMiddleItem() {
+ return false;
+ }
+
+ // 是否显示头像,默认为不显示
+ protected boolean isShowHeadImage() {
+ return false;
+ }
+
+ // 是否显示气泡背景,默认为不显示
+ protected boolean isShowBubble() {
+ return false;
+ }
+
+ // 是否显示昵称
+ protected boolean shouldDisplayNick() {
+ return !isMiddleItem();
+ }
+
+ /// -- 以下接口可由子类调用
+ protected final ChatRoomMsgAdapter getMsgAdapter() {
+ return (ChatRoomMsgAdapter) adapter;
+ }
+
+ /**
+ * 下载附件/缩略图
+ */
+ protected void downloadAttachment() {
+ if (message.getAttachment() != null && message.getAttachment() instanceof FileAttachment)
+ NIMClient.getService(MsgService.class).downloadAttachment(message, true);
+ }
+
+ // 设置FrameLayout子控件的gravity参数
+ protected final void setGravity(View view, int gravity) {
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) view.getLayoutParams();
+ params.gravity = gravity;
+ }
+
+ // 设置控件的长宽
+ protected void setLayoutParams(int width, int height, View... views) {
+ for (View view : views) {
+ ViewGroup.LayoutParams maskParams = view.getLayoutParams();
+ maskParams.width = width;
+ maskParams.height = height;
+ view.setLayoutParams(maskParams);
+ }
+ }
+
+ // 根据layout id查找对应的控件
+ protected T findViewById(int id) {
+ return (T) view.findViewById(id);
+ }
+
+ // 判断消息方向,是否是接收到的消息
+ protected boolean isReceivedMessage() {
+ return message.getDirect() == MsgDirectionEnum.In;
+ }
+
+ /// -- 以下是基类实现代码
+ @Override
+ public void convert(NIMBaseViewHolder holder, ChatRoomMessage data, int position, boolean isScrolling) {
+ view = holder.getConvertView();
+ context = holder.getContext();
+ message = data;
+
+ inflate();
+ refresh();
+ bindHolder(holder);
+ }
+
+ protected final void inflate() {
+ timeTextView = findViewById(R.id.message_item_time);
+ avatarLeft = findViewById(R.id.message_item_portrait_left);
+ avatarRight = findViewById(R.id.message_item_portrait_right);
+ alertButton = findViewById(R.id.message_item_alert);
+ progressBar = findViewById(R.id.message_item_progress);
+ nameTextView = findViewById(R.id.message_item_nickname);
+ contentContainer = findViewById(R.id.message_item_content);
+ nameIconView = findViewById(R.id.message_item_name_icon);
+ nameContainer = findViewById(R.id.message_item_name_layout);
+ readReceiptTextView = findViewById(R.id.textViewAlreadyRead);
+
+ // 这里只要inflate出来后加入一次即可
+ if (contentContainer.getChildCount() == 0) {
+ View.inflate(view.getContext(), getContentResId(), contentContainer);
+ }
+ inflateContentView();
+ }
+
+ protected final void refresh() {
+ setHeadImageView();
+ setNameTextView();
+ setTimeTextView();
+ setStatus();
+ setOnClickListener();
+ setLongClickListener();
+ setContent();
+
+ bindContentView();
+ }
+
+ public void refreshCurrentItem() {
+ if (message != null) {
+ refresh();
+ }
+ }
+
+ /**
+ * 设置时间显示
+ */
+ private void setTimeTextView() {
+ if (getMsgAdapter().needShowTime(message)) {
+ timeTextView.setVisibility(View.VISIBLE);
+ } else {
+ timeTextView.setVisibility(View.GONE);
+ return;
+ }
+
+ String text = TimeUtil.getTimeShowString(message.getTime(), false);
+ timeTextView.setText(text);
+ }
+
+ /**
+ * 设置消息发送状态
+ */
+ private void setStatus() {
+ MsgStatusEnum status = message.getStatus();
+ switch (status) {
+ case fail:
+ progressBar.setVisibility(View.GONE);
+ alertButton.setVisibility(View.VISIBLE);
+ break;
+ case sending:
+ progressBar.setVisibility(View.VISIBLE);
+ alertButton.setVisibility(View.GONE);
+ break;
+ default:
+ progressBar.setVisibility(View.GONE);
+ alertButton.setVisibility(View.GONE);
+ break;
+ }
+ }
+
+ private void setHeadImageView() {
+ HeadImageView show = isReceivedMessage() ? avatarLeft : avatarRight;
+ HeadImageView hide = isReceivedMessage() ? avatarRight : avatarLeft;
+ hide.setVisibility(View.GONE);
+ if (!isShowHeadImage()) {
+ show.setVisibility(View.GONE);
+ return;
+ }
+ if (isMiddleItem()) {
+ show.setVisibility(View.GONE);
+ } else {
+ show.setVisibility(View.VISIBLE);
+ show.loadBuddyAvatar(message.getFromAccount());
+ }
+
+ }
+
+ private void setOnClickListener() {
+ // 重发/重收按钮响应事件
+ if (getMsgAdapter().getEventListener() != null) {
+ alertButton.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ getMsgAdapter().getEventListener().onFailedBtnClick(message);
+ }
+ });
+ }
+
+ // 内容区域点击事件响应, 相当于点击了整项
+ contentContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onItemClick();
+ }
+ });
+
+ // 头像点击事件响应
+ if (NimUIKitImpl.getSessionListener() != null) {
+ View.OnClickListener portraitListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (NimUIKitImpl.getSessionListener() != null) {
+ NimUIKitImpl.getSessionListener().onAvatarClicked(context, message);
+ }
+ }
+ };
+ avatarLeft.setOnClickListener(portraitListener);
+ avatarRight.setOnClickListener(portraitListener);
+ }
+ }
+
+ /**
+ * item长按事件监听
+ */
+ private void setLongClickListener() {
+ longClickListener = new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ // 优先派发给自己处理,
+ if (!onItemLongClick()) {
+ if (getMsgAdapter().getEventListener() != null) {
+ getMsgAdapter().getEventListener().onViewHolderLongClick(contentContainer, view, message);
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ // 消息长按事件响应处理
+ contentContainer.setOnLongClickListener(longClickListener);
+
+ // 头像长按事件响应处理
+ if (NimUIKitImpl.getSessionListener() != null) {
+ View.OnLongClickListener longClickListener = new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ if (NimUIKitImpl.getSessionListener() != null) {
+ NimUIKitImpl.getSessionListener().onAvatarLongClicked(context, message);
+ }
+ return true;
+ }
+ };
+ avatarLeft.setOnLongClickListener(longClickListener);
+ avatarRight.setOnLongClickListener(longClickListener);
+ }
+ }
+
+ private void setNameTextView() {
+ if (!shouldDisplayNick()) {
+ nameTextView.setVisibility(View.GONE);
+ return;
+ }
+
+ nameContainer.setPadding(ScreenUtil.dip2px(6), 0, 0, 0);
+ nameTextView.setVisibility(View.VISIBLE);
+ nameTextView.setText(getNameText());
+ setStyleOfNameTextView(nameTextView, nameIconView);
+ }
+
+ protected String getNameText() {
+ return ChatRoomViewHolderHelper.getNameText(message);
+ }
+
+ protected void setStyleOfNameTextView(TextView nameTextView, ImageView nameIconView) {
+ ChatRoomViewHolderHelper.setStyleOfNameTextView(message, nameTextView, nameIconView);
+ }
+
+ private void setContent() {
+ if (!isShowBubble() && !isMiddleItem()) {
+ return;
+ }
+
+ LinearLayout bodyContainer = view.findViewById(R.id.message_item_body);
+
+ // 调整container的位置
+ int index = isReceivedMessage() ? 0 : 3;
+ if (bodyContainer.getChildAt(index) != contentContainer) {
+ bodyContainer.removeView(contentContainer);
+ bodyContainer.addView(contentContainer, index);
+ }
+
+ if (isMiddleItem()) {
+ setGravity(bodyContainer, Gravity.CENTER);
+ } else {
+ if (isReceivedMessage()) {
+ setGravity(bodyContainer, Gravity.LEFT);
+ contentContainer.setBackgroundResource(leftBackground());
+ } else {
+ setGravity(bodyContainer, Gravity.RIGHT);
+ contentContainer.setBackgroundResource(rightBackground());
+ }
+ }
+ }
+
+ private void setReadReceipt() {
+ if (!TextUtils.isEmpty(getMsgAdapter().getUuid()) && message.getUuid().equals(getMsgAdapter().getUuid())) {
+ readReceiptTextView.setVisibility(View.VISIBLE);
+ } else {
+ readReceiptTextView.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderFactory.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderFactory.java
new file mode 100644
index 0000000..d07808a
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderFactory.java
@@ -0,0 +1,74 @@
+package com.netease.nim.uikit.business.chatroom.viewholder;
+
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomNotificationAttachment;
+import com.netease.nimlib.sdk.msg.attachment.ImageAttachment;
+import com.netease.nimlib.sdk.msg.attachment.MsgAttachment;
+import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
+import com.netease.nimlib.sdk.robot.model.RobotAttachment;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * 聊天室消息项展示ViewHolder工厂类。
+ */
+public class ChatRoomMsgViewHolderFactory {
+
+ private static HashMap, Class extends ChatRoomMsgViewHolderBase>> viewHolders =
+ new HashMap<>();
+
+ static {
+ // built in
+ register(ChatRoomNotificationAttachment.class, ChatRoomMsgViewHolderNotification.class);
+ register(RobotAttachment.class, ChatRoomMsgViewHolderRobot.class);
+ register(ImageAttachment.class, ChatRoomMsgViewHolderPicture.class);
+ }
+
+ public static void register(Class extends MsgAttachment> attach, Class extends ChatRoomMsgViewHolderBase> viewHolder) {
+ viewHolders.put(attach, viewHolder);
+ }
+
+ public static Class extends ChatRoomMsgViewHolderBase> getViewHolderByType(ChatRoomMessage message) {
+ if (message.getMsgType() == MsgTypeEnum.text) {
+ return ChatRoomMsgViewHolderText.class;
+ } else {
+ Class extends ChatRoomMsgViewHolderBase> viewHolder = null;
+ if (message.getAttachment() != null) {
+ Class extends MsgAttachment> clazz = message.getAttachment().getClass();
+ while (viewHolder == null && clazz != null) {
+ viewHolder = viewHolders.get(clazz);
+ if (viewHolder == null) {
+ clazz = getSuperClass(clazz);
+ }
+ }
+ }
+ return viewHolder == null ? ChatRoomMsgViewHolderUnknown.class : viewHolder;
+ }
+ }
+
+ private static Class extends MsgAttachment> getSuperClass(Class extends MsgAttachment> derived) {
+ Class sup = derived.getSuperclass();
+ if (sup != null && MsgAttachment.class.isAssignableFrom(sup)) {
+ return sup;
+ } else {
+ for (Class itf : derived.getInterfaces()) {
+ if (MsgAttachment.class.isAssignableFrom(itf)) {
+ return itf;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static List> getAllViewHolders() {
+ List> list = new ArrayList<>();
+ list.addAll(viewHolders.values());
+ list.add(ChatRoomMsgViewHolderUnknown.class);
+ list.add(ChatRoomMsgViewHolderText.class);
+ list.add(ChatRoomMsgViewHolderPicture.class);
+
+ return list;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderNotification.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderNotification.java
new file mode 100644
index 0000000..2532973
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderNotification.java
@@ -0,0 +1,43 @@
+package com.netease.nim.uikit.business.chatroom.viewholder;
+
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.chatroom.helper.ChatRoomNotificationHelper;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomNotificationAttachment;
+
+public class ChatRoomMsgViewHolderNotification extends ChatRoomMsgViewHolderBase {
+
+ protected TextView notificationTextView;
+
+ public ChatRoomMsgViewHolderNotification(BaseMultiItemFetchLoadAdapter adapter) {
+ super(adapter);
+ }
+
+ @Override
+ protected int getContentResId() {
+ return R.layout.nim_message_item_notification;
+ }
+
+ @Override
+ protected boolean shouldDisplayNick() {
+ return false;
+ }
+
+ @Override
+ protected void inflateContentView() {
+ notificationTextView = (TextView) view.findViewById(R.id.message_item_notification_label);
+ }
+
+ @Override
+ protected void bindContentView() {
+ notificationTextView.setText(ChatRoomNotificationHelper.getNotificationText((ChatRoomNotificationAttachment) message.getAttachment()));
+ }
+
+ @Override
+ protected boolean isMiddleItem() {
+ return true;
+ }
+}
+
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderPicture.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderPicture.java
new file mode 100644
index 0000000..d5e52d6
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderPicture.java
@@ -0,0 +1,27 @@
+package com.netease.nim.uikit.business.chatroom.viewholder;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.session.activity.WatchMessagePictureActivity;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
+
+public class ChatRoomMsgViewHolderPicture extends ChatRoomMsgViewHolderThumbBase {
+
+ public ChatRoomMsgViewHolderPicture(BaseMultiItemFetchLoadAdapter adapter) {
+ super(adapter);
+ }
+
+ @Override
+ protected int getContentResId() {
+ return R.layout.nim_message_item_picture;
+ }
+
+ @Override
+ protected void onItemClick() {
+ WatchMessagePictureActivity.start(context, message);
+ }
+
+ @Override
+ protected String thumbFromSourceFile(String path) {
+ return path;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderRobot.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderRobot.java
new file mode 100644
index 0000000..36947bc
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderRobot.java
@@ -0,0 +1,105 @@
+package com.netease.nim.uikit.business.chatroom.viewholder;
+
+import android.view.View;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.robot.model.RobotResponseContent;
+import com.netease.nim.uikit.business.session.viewholder.robot.RobotContentLinearLayout;
+import com.netease.nim.uikit.business.session.viewholder.robot.RobotLinkViewStyle;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
+import com.netease.nim.uikit.common.ui.recyclerview.holder.NIMBaseViewHolder;
+import com.netease.nim.uikit.common.util.sys.ScreenUtil;
+import com.netease.nim.uikit.impl.NimUIKitImpl;
+import com.netease.nimlib.sdk.robot.model.NimRobotInfo;
+import com.netease.nimlib.sdk.robot.model.RobotAttachment;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Created by hzchenkang on 2017/8/24.
+ */
+
+public class ChatRoomMsgViewHolderRobot extends ChatRoomMsgViewHolderText implements RobotContentLinearLayout.ClickableChildView {
+
+ private android.widget.LinearLayout containerIn;
+ private RobotContentLinearLayout robotContent;
+ private TextView holderFooterButton;
+ private Set onClickIds;
+
+
+ public ChatRoomMsgViewHolderRobot(BaseMultiItemFetchLoadAdapter adapter) {
+ super(adapter);
+ }
+
+ @Override
+ protected int getContentResId() {
+ return com.netease.nim.uikit.R.layout.nim_message_item_robot;
+ }
+
+ @Override
+ protected void inflateContentView() {
+ containerIn = findViewById(com.netease.nim.uikit.R.id.robot_in);
+ bodyTextView = containerIn.findViewById(R.id.nim_message_item_text_body);
+ robotContent = findViewById(com.netease.nim.uikit.R.id.robot_out);
+ int dp6 = ScreenUtil.dip2px(6);
+ robotContent.setPadding(dp6, 0, 0, 0);
+ RobotLinkViewStyle linkStyle = new RobotLinkViewStyle();
+ linkStyle.setRobotTextColor(R.color.black);
+ linkStyle.setBackground(R.drawable.nim_chatroom_robot_link_view_selector);
+ robotContent.setLinkStyle(linkStyle);
+ holderFooterButton = findViewById(com.netease.nim.uikit.R.id.tv_robot_session_continue);
+ holderFooterButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (getMsgAdapter().getEventListener() != null) {
+ getMsgAdapter().getEventListener().onFooterClick(ChatRoomMsgViewHolderRobot.this, ChatRoomMsgViewHolderRobot.this.message);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void bindContentView() {
+ onClickIds = new HashSet<>(); // for child to add
+ RobotAttachment attachment = (RobotAttachment) message.getAttachment();
+
+ if (attachment.isRobotSend()) {
+ // 下行
+ containerIn.setVisibility(View.GONE);
+ robotContent.setVisibility(View.VISIBLE);
+ holderFooterButton.setVisibility(View.VISIBLE);
+ nameIconView.setVisibility(View.GONE);
+ NimRobotInfo robotInfo = NimUIKitImpl.getRobotInfoProvider().getRobotByAccount(attachment.getFromRobotAccount());
+ if (robotInfo != null) {
+ nameTextView.setText(robotInfo.getName());
+ } else {
+ nameTextView.setText(attachment.getFromRobotAccount());
+ }
+
+ robotContent.bindContentView(this, new RobotResponseContent(attachment.getResponse()));
+ } else {
+ // 上行
+ containerIn.setVisibility(View.VISIBLE);
+ robotContent.setVisibility(View.GONE);
+ holderFooterButton.setVisibility(View.GONE);
+ super.bindContentView();
+ }
+ }
+
+ @Override
+ protected void bindHolder(NIMBaseViewHolder holder) {
+ holder.getChildClickViewIds().clear();
+ for (int id : onClickIds) {
+ holder.addOnClickListener(id);
+ }
+
+ onClickIds.clear();
+ }
+
+ @Override
+ public void addClickableChildView(Class extends View> clazz, int id) {
+ onClickIds.add(id);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderText.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderText.java
new file mode 100644
index 0000000..8adbe4d
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderText.java
@@ -0,0 +1,58 @@
+package com.netease.nim.uikit.business.chatroom.viewholder;
+
+import android.graphics.Color;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ImageSpan;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.business.session.emoji.MoonUtil;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
+import com.netease.nim.uikit.common.util.sys.ScreenUtil;
+
+/**
+ * Created by hzxuwen on 2016/1/18.
+ */
+public class ChatRoomMsgViewHolderText extends ChatRoomMsgViewHolderBase {
+
+ protected TextView bodyTextView;
+
+
+ public ChatRoomMsgViewHolderText(BaseMultiItemFetchLoadAdapter adapter) {
+ super(adapter);
+ }
+
+ @Override
+ protected int getContentResId() {
+ return R.layout.nim_message_item_text;
+ }
+
+ @Override
+ protected void inflateContentView() {
+ bodyTextView = findViewById(R.id.nim_message_item_text_body);
+ }
+
+ protected String getDisplayText() {
+ return message.getContent();
+ }
+
+ @Override
+ protected boolean isShowBubble() {
+ return false;
+ }
+
+ @Override
+ protected boolean isShowHeadImage() {
+ return false;
+ }
+
+ @Override
+ protected void bindContentView() {
+ bodyTextView.setTextColor(Color.BLACK);
+ bodyTextView.setPadding(ScreenUtil.dip2px(6), 0, 0, 0);
+ MoonUtil.identifyFaceExpression(NimUIKit.getContext(), bodyTextView, getDisplayText(), ImageSpan.ALIGN_BOTTOM);
+ bodyTextView.setMovementMethod(LinkMovementMethod.getInstance());
+ bodyTextView.setOnLongClickListener(longClickListener);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderThumbBase.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderThumbBase.java
new file mode 100644
index 0000000..207b03b
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderThumbBase.java
@@ -0,0 +1,138 @@
+package com.netease.nim.uikit.business.chatroom.viewholder;
+
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.common.ui.imageview.MsgThumbImageView;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
+import com.netease.nim.uikit.common.util.media.BitmapDecoder;
+import com.netease.nim.uikit.common.util.media.ImageUtil;
+import com.netease.nim.uikit.common.util.string.StringUtil;
+import com.netease.nim.uikit.common.util.sys.ScreenUtil;
+import com.netease.nimlib.sdk.msg.attachment.FileAttachment;
+import com.netease.nimlib.sdk.msg.attachment.ImageAttachment;
+import com.netease.nimlib.sdk.msg.attachment.VideoAttachment;
+import com.netease.nimlib.sdk.msg.constant.AttachStatusEnum;
+import com.netease.nimlib.sdk.msg.constant.MsgStatusEnum;
+import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
+
+import java.io.File;
+
+public abstract class ChatRoomMsgViewHolderThumbBase extends ChatRoomMsgViewHolderBase {
+
+ public ChatRoomMsgViewHolderThumbBase(BaseMultiItemFetchLoadAdapter adapter) {
+ super(adapter);
+ }
+
+ protected MsgThumbImageView thumbnail;
+ protected View progressCover;
+ protected TextView progressLabel;
+
+ @Override
+ protected boolean isShowBubble() {
+ return false;
+ }
+
+ @Override
+ protected boolean isShowHeadImage() {
+ return false;
+ }
+
+ @Override
+ protected void inflateContentView() {
+ thumbnail = findViewById(R.id.message_item_thumb_thumbnail);
+ progressBar = findViewById(R.id.message_item_thumb_progress_bar); // 覆盖掉
+ progressCover = findViewById(R.id.message_item_thumb_progress_cover);
+ progressLabel = findViewById(R.id.message_item_thumb_progress_text);
+ }
+
+ @Override
+ protected void bindContentView() {
+ FileAttachment msgAttachment = (FileAttachment) message.getAttachment();
+ String path = msgAttachment.getPath();
+ String thumbPath = msgAttachment.getThumbPath();
+ if (!TextUtils.isEmpty(thumbPath)) {
+ loadThumbnailImage(thumbPath, false, msgAttachment.getExtension());
+ } else if (!TextUtils.isEmpty(path)) {
+ loadThumbnailImage(thumbFromSourceFile(path), true, msgAttachment.getExtension());
+ } else {
+ loadThumbnailImage(null, false, msgAttachment.getExtension());
+ if (message.getAttachStatus() == AttachStatusEnum.transferred
+ || message.getAttachStatus() == AttachStatusEnum.def) {
+ downloadAttachment();
+ }
+ }
+
+ refreshStatus();
+ }
+
+ private void refreshStatus() {
+ FileAttachment attachment = (FileAttachment) message.getAttachment();
+ if (TextUtils.isEmpty(attachment.getPath()) && TextUtils.isEmpty(attachment.getThumbPath())) {
+ if (message.getAttachStatus() == AttachStatusEnum.fail || message.getStatus() == MsgStatusEnum.fail) {
+ alertButton.setVisibility(View.VISIBLE);
+ } else {
+ alertButton.setVisibility(View.GONE);
+ }
+ }
+
+ if (message.getStatus() == MsgStatusEnum.sending
+ || (isReceivedMessage() && message.getAttachStatus() == AttachStatusEnum.transferring)) {
+ progressCover.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(View.VISIBLE);
+ progressLabel.setVisibility(View.VISIBLE);
+ progressLabel.setText(StringUtil.getPercentString(getMsgAdapter().getProgress(message)));
+ } else {
+ progressCover.setVisibility(View.GONE);
+ progressBar.setVisibility(View.GONE);
+ progressLabel.setVisibility(View.GONE);
+ }
+ }
+
+ private void loadThumbnailImage(String path, boolean isOriginal, String ext) {
+ setImageSize(path);
+ if (path != null) {
+ //thumbnail.loadAsPath(thumbPath, getImageMaxEdge(), getImageMaxEdge(), maskBg());
+ thumbnail.loadAsPath(path, getImageMaxEdge(), getImageMaxEdge(), maskBg(), ext);
+ } else {
+ thumbnail.loadAsResource(R.drawable.nim_image_default, maskBg());
+ }
+ }
+
+ private void setImageSize(String thumbPath) {
+ int[] bounds = null;
+ if (thumbPath != null) {
+ bounds = BitmapDecoder.decodeBound(new File(thumbPath));
+ }
+ if (bounds == null) {
+ if (message.getMsgType() == MsgTypeEnum.image) {
+ ImageAttachment attachment = (ImageAttachment) message.getAttachment();
+ bounds = new int[]{attachment.getWidth(), attachment.getHeight()};
+ } else if (message.getMsgType() == MsgTypeEnum.video) {
+ VideoAttachment attachment = (VideoAttachment) message.getAttachment();
+ bounds = new int[]{attachment.getWidth(), attachment.getHeight()};
+ }
+ }
+
+ if (bounds != null) {
+ ImageUtil.ImageSize imageSize = ImageUtil.getThumbnailDisplaySize(bounds[0], bounds[1], getImageMaxEdge(), getImageMinEdge());
+ setLayoutParams(imageSize.width, imageSize.height, thumbnail);
+ }
+ }
+
+ private int maskBg() {
+ return R.drawable.nim_message_item_round_bg;
+ }
+
+ public static int getImageMaxEdge() {
+ return (int) (165.0 / 320.0 * ScreenUtil.screenWidth);
+ }
+
+ public static int getImageMinEdge() {
+ return (int) (76.0 / 320.0 * ScreenUtil.screenWidth);
+ }
+
+ protected abstract String thumbFromSourceFile(String path);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderUnknown.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderUnknown.java
new file mode 100644
index 0000000..41a6cea
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomMsgViewHolderUnknown.java
@@ -0,0 +1,36 @@
+package com.netease.nim.uikit.business.chatroom.viewholder;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
+import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
+
+/**
+ * Created by huangjun on 2016/12/27.
+ */
+public class ChatRoomMsgViewHolderUnknown extends ChatRoomMsgViewHolderBase {
+
+ public ChatRoomMsgViewHolderUnknown(BaseMultiItemFetchLoadAdapter adapter) {
+ super(adapter);
+ }
+
+ @Override
+ protected int getContentResId() {
+ return R.layout.nim_message_item_unknown;
+ }
+
+ @Override
+ protected boolean isShowHeadImage() {
+ if (message.getSessionType() == SessionTypeEnum.ChatRoom) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void inflateContentView() {
+ }
+
+ @Override
+ protected void bindContentView() {
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomViewHolderHelper.java b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomViewHolderHelper.java
new file mode 100644
index 0000000..2fe6351
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/chatroom/viewholder/ChatRoomViewHolderHelper.java
@@ -0,0 +1,45 @@
+package com.netease.nim.uikit.business.chatroom.viewholder;
+
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.chatroom.helper.ChatRoomHelper;
+import com.netease.nim.uikit.business.uinfo.UserInfoHelper;
+import com.netease.nim.uikit.impl.NimUIKitImpl;
+import com.netease.nimlib.sdk.chatroom.constant.MemberType;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMember;
+import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
+
+
+/**
+ * 聊天室成员姓名
+ * Created by hzxuwen on 2016/1/20.
+ */
+public class ChatRoomViewHolderHelper {
+
+ public static String getNameText(ChatRoomMessage message) {
+ // 聊天室中显示姓名
+ if (message.getChatRoomMessageExtension() != null) {
+ return message.getChatRoomMessageExtension().getSenderNick();
+ } else {
+ ChatRoomMember member = NimUIKitImpl.getChatRoomProvider().getChatRoomMember(message.getSessionId(), message.getFromAccount());
+ return member == null ? UserInfoHelper.getUserName(message.getFromAccount()) : member.getNick();
+ }
+ }
+
+ public static void setStyleOfNameTextView(ChatRoomMessage message, TextView nameTextView, ImageView nameIconView) {
+ nameTextView.setTextColor(NimUIKitImpl.getContext().getResources().getColor(R.color.color_black_ff999999));
+ MemberType type = ChatRoomHelper.getMemberTypeByRemoteExt(message);
+ if (type == MemberType.ADMIN) {
+ nameIconView.setImageResource(R.drawable.nim_admin_icon);
+ nameIconView.setVisibility(View.VISIBLE);
+ } else if (type == MemberType.CREATOR) {
+ nameIconView.setImageResource(R.drawable.nim_master_icon);
+ nameIconView.setVisibility(View.VISIBLE);
+ } else {
+ nameIconView.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/ContactsFragment.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/ContactsFragment.java
new file mode 100644
index 0000000..f492b78
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/ContactsFragment.java
@@ -0,0 +1,445 @@
+package com.netease.nim.uikit.business.contact;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.api.model.contact.ContactChangedObserver;
+import com.netease.nim.uikit.api.model.contact.ContactsCustomization;
+import com.netease.nim.uikit.api.model.main.LoginSyncDataStatusObserver;
+import com.netease.nim.uikit.api.model.main.OnlineStateChangeObserver;
+import com.netease.nim.uikit.api.model.user.UserInfoObserver;
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ItemTypes;
+import com.netease.nim.uikit.business.contact.core.model.ContactDataAdapter;
+import com.netease.nim.uikit.business.contact.core.model.ContactGroupStrategy;
+import com.netease.nim.uikit.business.contact.core.provider.ContactDataProvider;
+import com.netease.nim.uikit.business.contact.core.query.IContactDataProvider;
+import com.netease.nim.uikit.business.contact.core.viewholder.LabelHolder;
+import com.netease.nim.uikit.business.contact.core.viewholder.OnlineStateContactHolder;
+import com.netease.nim.uikit.common.fragment.TFragment;
+import com.netease.nim.uikit.common.ui.liv.LetterIndexView;
+import com.netease.nim.uikit.common.ui.liv.LivIndex;
+import com.netease.nim.uikit.common.util.log.LogUtil;
+import com.netease.nim.uikit.impl.NimUIKitImpl;
+import com.netease.nim.uikit.impl.cache.UIKitLogTag;
+import com.netease.nimlib.sdk.Observer;
+import com.chwl.library.utils.ResUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import me.everything.android.ui.overscroll.OverScrollDecoratorHelper;
+
+
+/**
+ * 通讯录Fragment
+ *
+ * Created by huangjun on 2015/9/7.
+ */
+public class ContactsFragment extends TFragment {
+
+ private ContactDataAdapter adapter;
+
+ private ListView listView;
+
+ private TextView countText;
+
+ private LivIndex litterIdx;
+
+ private View loadingFrame;
+
+ private ContactsCustomization customization;
+
+ private ReloadFrequencyControl reloadControl = new ReloadFrequencyControl();
+
+ public void setContactsCustomization(ContactsCustomization customization) {
+ this.customization = customization;
+ }
+
+ private static final class ContactsGroupStrategy extends ContactGroupStrategy {
+ public ContactsGroupStrategy() {
+ add(ContactGroupStrategy.GROUP_NULL, -1, "");
+ addABC(0);
+ }
+ }
+
+ /**
+ * ***************************************** 生命周期 *****************************************
+ */
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.nim_contacts, container, false);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ // 界面初始化
+ initAdapter();
+ findViews();
+ buildLitterIdx(getView());
+
+ // 注册观察者
+ registerObserver(true);
+ registerOnlineStateChangeListener(true);
+ // 加载本地数据
+ reload(false);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ registerObserver(false);
+ registerOnlineStateChangeListener(false);
+ }
+
+ private void initAdapter() {
+ IContactDataProvider dataProvider = new ContactDataProvider(ItemTypes.FRIEND);
+
+ adapter = new ContactDataAdapter(getActivity(), new ContactsGroupStrategy(), dataProvider) {
+ @Override
+ protected List onNonDataItems() {
+ if (customization != null) {
+ return customization.onGetFuncItems();
+ }
+
+ return new ArrayList<>();
+ }
+
+ @Override
+ protected void onPreReady() {
+ loadingFrame.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ protected void onPostLoad(boolean empty, String queryText, boolean all) {
+ loadingFrame.setVisibility(View.GONE);
+ int userCount = NimUIKit.getContactProvider().getMyFriendsCount();
+ countText.setText(ResUtil.getString(R.string.business_contact_contactsfragment_01) + userCount + ResUtil.getString(R.string.business_contact_contactsfragment_02));
+
+ onReloadCompleted();
+ }
+ };
+
+ adapter.addViewHolder(ItemTypes.LABEL, LabelHolder.class);
+ if (customization != null) {
+ adapter.addViewHolder(ItemTypes.FUNC, customization.onGetFuncViewHolderClass());
+ }
+ adapter.addViewHolder(ItemTypes.FRIEND, OnlineStateContactHolder.class);
+ }
+
+ private void findViews() {
+ // loading
+ loadingFrame = findView(R.id.contact_loading_frame);
+
+ // count
+ View countLayout = View.inflate(getView().getContext(), R.layout.nim_contacts_count_item, null);
+ countLayout.setClickable(false);
+ countText = (TextView) countLayout.findViewById(R.id.contactCountText);
+
+ // ListView
+ listView = findView(R.id.contact_list_view);
+ listView.addFooterView(countLayout); // 注意:addFooter要放在setAdapter之前,否则旧版本手机可能会add不上
+ listView.setAdapter(adapter);
+ listView.setOnScrollListener(new AbsListView.OnScrollListener() {
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+
+ }
+ });
+
+ ContactItemClickListener listener = new ContactItemClickListener();
+ listView.setOnItemClickListener(listener);
+ listView.setOnItemLongClickListener(listener);
+
+ // ios style
+ OverScrollDecoratorHelper.setUpOverScroll(listView);
+ }
+
+ private void buildLitterIdx(View view) {
+ LetterIndexView livIndex = (LetterIndexView) view.findViewById(R.id.liv_index);
+ livIndex.setNormalColor(getResources().getColor(R.color.contacts_letters_color));
+ ImageView imgBackLetter = (ImageView) view.findViewById(R.id.img_hit_letter);
+ TextView litterHit = (TextView) view.findViewById(R.id.tv_hit_letter);
+ litterIdx = adapter.createLivIndex(listView, livIndex, litterHit, imgBackLetter);
+
+ litterIdx.show();
+ }
+
+ private final class ContactItemClickListener implements OnItemClickListener, OnItemLongClickListener {
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position,
+ long id) {
+ AbsContactItem item = (AbsContactItem) adapter.getItem(position);
+ if (item == null) {
+ return;
+ }
+
+ int type = item.getItemType();
+
+ if (type == ItemTypes.FUNC && customization != null) {
+ customization.onFuncItemClick(item);
+ return;
+ }
+
+ if (type == ItemTypes.FRIEND && item instanceof ContactItem && NimUIKitImpl.getContactEventListener() != null) {
+ NimUIKitImpl.getContactEventListener().onItemClick(getActivity(), (((ContactItem) item).getContact()).getContactId());
+ }
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView> parent, View view,
+ int position, long id) {
+ AbsContactItem item = (AbsContactItem) adapter.getItem(position);
+ if (item == null) {
+ return false;
+ }
+
+ if (item instanceof ContactItem && NimUIKitImpl.getContactEventListener() != null) {
+ NimUIKitImpl.getContactEventListener().onItemLongClick(getActivity(), (((ContactItem) item).getContact()).getContactId());
+ }
+
+ return true;
+ }
+ }
+
+ public void scrollToTop() {
+ if (listView != null) {
+ int top = listView.getFirstVisiblePosition();
+ int bottom = listView.getLastVisiblePosition();
+ if (top >= (bottom - top)) {
+ listView.setSelection(bottom - top);
+ listView.smoothScrollToPosition(0);
+ } else {
+ listView.smoothScrollToPosition(0);
+ }
+ }
+ }
+
+ /**
+ * *********************************** 通讯录加载控制 *******************************
+ */
+
+ /**
+ * 加载通讯录数据并刷新
+ *
+ * @param reload true则重新加载数据;false则判断当前数据源是否空,若空则重新加载,不空则不加载
+ */
+ private void reload(boolean reload) {
+ if (!reloadControl.canDoReload(reload)) {
+ return;
+ }
+
+ if (adapter == null) {
+ if (getActivity() == null) {
+ return;
+ }
+
+ initAdapter();
+ }
+
+ // 开始加载
+ if (!adapter.load(reload)) {
+ // 如果不需要加载,则直接当完成处理
+ onReloadCompleted();
+ }
+ }
+
+ private void onReloadCompleted() {
+ if (reloadControl.continueDoReloadWhenCompleted()) {
+ // 计划下次加载,稍有延迟
+ getHandler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ boolean reloadParam = reloadControl.getReloadParam();
+ Log.i(UIKitLogTag.CONTACT, "continue reload " + reloadParam);
+ reloadControl.resetStatus();
+ reload(reloadParam);
+ }
+ }, 50);
+ } else {
+ // 本次加载完成
+ reloadControl.resetStatus();
+ }
+
+ LogUtil.i(UIKitLogTag.CONTACT, "contact load completed");
+ }
+
+ /**
+ * 通讯录加载频率控制
+ */
+ class ReloadFrequencyControl {
+ boolean isReloading = false;
+ boolean needReload = false;
+ boolean reloadParam = false;
+
+ boolean canDoReload(boolean param) {
+ if (isReloading) {
+ // 正在加载,那么计划加载完后重载
+ needReload = true;
+ if (param) {
+ // 如果加载过程中又有多次reload请求,多次参数只要有true,那么下次加载就是reload(true);
+ reloadParam = true;
+ }
+ LogUtil.i(UIKitLogTag.CONTACT, "pending reload task");
+
+ return false;
+ } else {
+ // 如果当前空闲,那么立即开始加载
+ isReloading = true;
+ return true;
+ }
+ }
+
+ boolean continueDoReloadWhenCompleted() {
+ return needReload;
+ }
+
+ void resetStatus() {
+ isReloading = false;
+ needReload = false;
+ reloadParam = false;
+ }
+
+ boolean getReloadParam() {
+ return reloadParam;
+ }
+ }
+
+ /**
+ * *********************************** 用户资料、好友关系变更、登录数据同步完成观察者 *******************************
+ */
+
+ private void registerObserver(boolean register) {
+ NimUIKit.getUserInfoObservable().registerObserver(userInfoObserver, register);
+ NimUIKit.getContactChangedObservable().registerObserver(friendDataChangedObserver, register);
+ LoginSyncDataStatusObserver.getInstance().observeSyncDataCompletedEvent(loginSyncCompletedObserver);
+ }
+
+ ContactChangedObserver friendDataChangedObserver = new ContactChangedObserver() {
+ @Override
+ public void onAddedOrUpdatedFriends(List accounts) {
+ reloadWhenDataChanged(accounts, "onAddedOrUpdatedFriends", true);
+ }
+
+ @Override
+ public void onDeletedFriends(List accounts) {
+ reloadWhenDataChanged(accounts, "onDeletedFriends", true);
+ }
+
+ @Override
+ public void onAddUserToBlackList(List accounts) {
+ reloadWhenDataChanged(accounts, "onAddUserToBlackList", true);
+ }
+
+ @Override
+ public void onRemoveUserFromBlackList(List accounts) {
+ reloadWhenDataChanged(accounts, "onRemoveUserFromBlackList", true);
+ }
+ };
+
+ private UserInfoObserver userInfoObserver = new UserInfoObserver() {
+ @Override
+ public void onUserInfoChanged(List accounts) {
+ reloadWhenDataChanged(accounts, "onUserInfoChanged", true, false); // 非好友资料变更,不用刷新界面
+ }
+ };
+
+ private Observer loginSyncCompletedObserver = new Observer() {
+ @Override
+ public void onEvent(Void aVoid) {
+ getHandler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ reloadWhenDataChanged(null, "onLoginSyncCompleted", false);
+ }
+ }, 50);
+ }
+ };
+
+ private void reloadWhenDataChanged(List accounts, String reason, boolean reload) {
+ reloadWhenDataChanged(accounts, reason, reload, true);
+ }
+
+ private void reloadWhenDataChanged(List accounts, String reason, boolean reload, boolean force) {
+ if (accounts == null || accounts.isEmpty()) {
+ return;
+ }
+
+ boolean needReload = false;
+ if (!force) {
+ // 非force:与通讯录无关的(非好友)变更通知,去掉
+ for (String account : accounts) {
+ if (NimUIKit.getContactProvider().isMyFriend(account)) {
+ needReload = true;
+ break;
+ }
+ }
+ } else {
+ needReload = true;
+ }
+
+ if (!needReload) {
+ Log.d(UIKitLogTag.CONTACT, "no need to reload contact");
+ return;
+ }
+
+ // log
+ StringBuilder sb = new StringBuilder();
+ sb.append("ContactFragment received data changed as [" + reason + "] : ");
+ if (accounts != null && !accounts.isEmpty()) {
+ for (String account : accounts) {
+ sb.append(account);
+ sb.append(" ");
+ }
+ sb.append(", changed size=" + accounts.size());
+ }
+ Log.i(UIKitLogTag.CONTACT, sb.toString());
+
+ // reload
+ reload(reload);
+ }
+
+ /**
+ * *********************************** 在线状态 *******************************
+ */
+
+ OnlineStateChangeObserver onlineStateChangeObserver = new OnlineStateChangeObserver() {
+ @Override
+ public void onlineStateChange(Set accounts) {
+ // 更新
+ adapter.notifyDataSetChanged();
+ }
+ };
+
+ private void registerOnlineStateChangeListener(boolean register) {
+ if (!NimUIKitImpl.enableOnlineState()) {
+ return;
+ }
+ NimUIKitImpl.getOnlineStateChangeObservable().registerOnlineStateChangeListeners(onlineStateChangeObserver, register);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/AbsContactItem.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/AbsContactItem.java
new file mode 100644
index 0000000..24687b3
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/AbsContactItem.java
@@ -0,0 +1,27 @@
+package com.netease.nim.uikit.business.contact.core.item;
+
+/**
+ * 通讯录数据项抽象类
+ * Created by huangjun on 2015/2/10.
+ */
+public abstract class AbsContactItem {
+ /**
+ * 所属的类型
+ *
+ * @see ItemTypes
+ */
+ public abstract int getItemType();
+
+ /**
+ * 所属的分组
+ */
+ public abstract String belongsGroup();
+
+ protected final int compareType(AbsContactItem item) {
+ return compareType(getItemType(), item.getItemType());
+ }
+
+ public static int compareType(int lhs, int rhs) {
+ return lhs - rhs;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ContactIdFilter.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ContactIdFilter.java
new file mode 100644
index 0000000..b8f8167
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ContactIdFilter.java
@@ -0,0 +1,33 @@
+package com.netease.nim.uikit.business.contact.core.item;
+
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+
+import java.util.Collection;
+
+public class ContactIdFilter implements ContactItemFilter {
+ private static final long serialVersionUID = -6813849507791265300L;
+
+ private final Collection ids;
+
+ private boolean exclude = true; // false means include
+
+ public ContactIdFilter(Collection ids) {
+ this.ids = ids;
+ }
+
+ public ContactIdFilter(Collection ids, boolean exclude) {
+ this.ids = ids;
+ this.exclude = exclude;
+ }
+
+ @Override
+ public boolean filter(AbsContactItem item) {
+ if (item instanceof ContactItem) {
+ IContact contact = ((ContactItem) item).getContact();
+ boolean contains = ids.contains(contact.getContactId());
+ return exclude ? contains : !contains;
+ }
+
+ return false;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ContactItem.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ContactItem.java
new file mode 100644
index 0000000..33d0b4d
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ContactItem.java
@@ -0,0 +1,54 @@
+package com.netease.nim.uikit.business.contact.core.item;
+
+import android.text.TextUtils;
+
+import com.netease.nim.uikit.business.contact.core.model.ContactGroupStrategy;
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.business.contact.core.query.TextComparator;
+
+public class ContactItem extends AbsContactItem implements Comparable {
+ private final IContact contact;
+
+ private final int dataItemType;
+
+ public ContactItem(IContact contact, int type) {
+ this.contact = contact;
+ this.dataItemType = type;
+ }
+
+ public IContact getContact() {
+ return contact;
+ }
+
+ @Override
+ public int getItemType() {
+ return dataItemType;
+ }
+
+ @Override
+ public int compareTo(ContactItem item) {
+ // TYPE
+ int compare = compareType(item);
+ if (compare != 0) {
+ return compare;
+ } else {
+ return TextComparator.compareIgnoreCase(getCompare(), item.getCompare());
+ }
+ }
+
+ @Override
+ public String belongsGroup() {
+ IContact contact = getContact();
+ if (contact == null) {
+ return ContactGroupStrategy.GROUP_NULL;
+ }
+
+ String group = TextComparator.getLeadingUp(getCompare());
+ return !TextUtils.isEmpty(group) ? group : ContactGroupStrategy.GROUP_SHARP;
+ }
+
+ private String getCompare() {
+ IContact contact = getContact();
+ return contact != null ? contact.getDisplayName() : null;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ContactItemFilter.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ContactItemFilter.java
new file mode 100644
index 0000000..f9173bd
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ContactItemFilter.java
@@ -0,0 +1,7 @@
+package com.netease.nim.uikit.business.contact.core.item;
+
+import java.io.Serializable;
+
+public interface ContactItemFilter extends Serializable {
+ boolean filter(AbsContactItem item);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ItemTypes.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ItemTypes.java
new file mode 100644
index 0000000..5e6d0c1
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/ItemTypes.java
@@ -0,0 +1,39 @@
+package com.netease.nim.uikit.business.contact.core.item;
+
+/**
+ * 通讯录列表项类型
+ * Created by huangjun on 2015/2/10.
+ */
+public interface ItemTypes {
+
+ /**
+ * 基础类型
+ */
+ int TEXT = -2;
+
+ int LABEL = -1;
+
+ /**
+ * 扩展类型
+ */
+ int FUNC = 0; // 功能项
+
+ int FRIEND = 1; // 好友项
+
+ int TEAM = 2; // 群组项
+
+ int TEAM_MEMBER = 3; // 群成员
+
+ int MSG = 4; // 消息
+
+ /**
+ * 子类型
+ */
+ interface TEAMS {
+ int BASE = ItemTypes.TEAM << 16;
+
+ int NORMAL_TEAM = BASE + 1; // 普通群
+
+ int ADVANCED_TEAM = BASE + 2; // 高级群
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/LabelItem.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/LabelItem.java
new file mode 100644
index 0000000..061633c
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/LabelItem.java
@@ -0,0 +1,23 @@
+package com.netease.nim.uikit.business.contact.core.item;
+
+public class LabelItem extends AbsContactItem {
+ private final String text;
+
+ public LabelItem(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public int getItemType() {
+ return ItemTypes.LABEL;
+ }
+
+ @Override
+ public String belongsGroup() {
+ return null;
+ }
+
+ public final String getText() {
+ return text;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/MsgItem.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/MsgItem.java
new file mode 100644
index 0000000..93c8848
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/MsgItem.java
@@ -0,0 +1,40 @@
+package com.netease.nim.uikit.business.contact.core.item;
+
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nimlib.sdk.search.model.MsgIndexRecord;
+
+public class MsgItem extends AbsContactItem {
+ private final IContact contact;
+
+ private final MsgIndexRecord record;
+
+ private final boolean querySession;
+
+ public MsgItem(IContact contact, MsgIndexRecord record, boolean querySession) {
+ this.contact = contact;
+ this.record = record;
+ this.querySession = querySession;
+ }
+
+ public IContact getContact() {
+ return contact;
+ }
+
+ public MsgIndexRecord getRecord() {
+ return record;
+ }
+
+ public boolean isQuerySession() {
+ return querySession;
+ }
+
+ @Override
+ public int getItemType() {
+ return ItemTypes.MSG;
+ }
+
+ @Override
+ public String belongsGroup() {
+ return null;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/TextItem.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/TextItem.java
new file mode 100644
index 0000000..cb2d3d6
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/item/TextItem.java
@@ -0,0 +1,35 @@
+package com.netease.nim.uikit.business.contact.core.item;
+
+import android.text.TextUtils;
+
+import com.netease.nim.uikit.business.contact.core.model.ContactGroupStrategy;
+import com.netease.nim.uikit.business.contact.core.query.TextComparator;
+
+public class TextItem extends AbsContactItem implements Comparable {
+ private final String text;
+
+ public TextItem(String text) {
+ this.text = text != null ? text : "";
+ }
+
+ public final String getText() {
+ return text;
+ }
+
+ @Override
+ public int getItemType() {
+ return ItemTypes.TEXT;
+ }
+
+ @Override
+ public String belongsGroup() {
+ String group = TextComparator.getLeadingUp(text);
+
+ return !TextUtils.isEmpty(group) ? group : ContactGroupStrategy.GROUP_SHARP;
+ }
+
+ @Override
+ public int compareTo(TextItem item) {
+ return TextComparator.compareIgnoreCase(text, item.text);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/AbsContact.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/AbsContact.java
new file mode 100644
index 0000000..90331fd
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/AbsContact.java
@@ -0,0 +1,5 @@
+package com.netease.nim.uikit.business.contact.core.model;
+
+public abstract class AbsContact implements IContact {
+
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/AbsContactDataList.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/AbsContactDataList.java
new file mode 100644
index 0000000..5f2eea8
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/AbsContactDataList.java
@@ -0,0 +1,192 @@
+package com.netease.nim.uikit.business.contact.core.model;
+
+import android.text.TextUtils;
+
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.LabelItem;
+import com.netease.nim.uikit.business.contact.core.query.TextQuery;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 通讯录列表数据抽象类
+ * Group定义
+ *
+ * Created by huangjun on 2015/2/10.
+ */
+public abstract class AbsContactDataList {
+ protected final ContactGroupStrategy groupStrategy;
+
+ protected final Map groupMap = new HashMap<>();
+
+ protected final Group groupNull = new Group(null, null);
+
+ private TextQuery query;
+
+ private static final class NoneGroupStrategy extends ContactGroupStrategy {
+ @Override
+ public String belongs(AbsContactItem item) {
+ return null;
+ }
+
+ @Override
+ public int compare(String lhs, String rhs) {
+ return 0;
+ }
+ }
+
+ public AbsContactDataList(ContactGroupStrategy groupStrategy) {
+ if (groupStrategy == null) {
+ groupStrategy = new NoneGroupStrategy();
+ }
+
+ this.groupStrategy = groupStrategy;
+ }
+
+ //
+ // ACCESS
+ //
+
+ public abstract int getCount();
+
+ public abstract boolean isEmpty();
+
+ public abstract AbsContactItem getItem(int index);
+
+ public abstract List getItems();
+
+ public abstract Map getIndexes();
+
+ public final TextQuery getQuery() {
+ return query;
+ }
+
+ public final String getQueryText() {
+ return query != null ? query.text : null;
+ }
+
+ public final void setQuery(TextQuery query) {
+ this.query = query;
+ }
+
+ //
+ // BUILD
+ //
+
+ public abstract void build();
+
+ public final void add(AbsContactItem item) {
+ if (item == null) {
+ return;
+ }
+
+ Group group;
+
+ String id = groupStrategy.belongs(item);
+ if (id == null) {
+ group = groupNull;
+ } else {
+ group = groupMap.get(id);
+ if (group == null) {
+ group = new Group(id, groupStrategy.getName(id));
+ groupMap.put(id, group);
+ }
+ }
+
+ group.add(item);
+ }
+
+ protected final void sortGroups(List groups) {
+ Collections.sort(groups, new Comparator() {
+ @Override
+ public int compare(Group lhs, Group rhs) {
+ return groupStrategy.compare(lhs.id, rhs.id);
+ }
+ });
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ protected static final class Group {
+ final String id;
+
+ final String title;
+
+ final boolean hasHead;
+
+ final List items = new ArrayList();
+
+ Group(String id, String title) {
+ this.id = id;
+ this.title = title;
+ this.hasHead = !TextUtils.isEmpty(title);
+ }
+
+ int getCount() {
+ return items.size() + (hasHead ? 1 : 0);
+ }
+
+ AbsContactItem getItem(int index) {
+ if (hasHead) {
+ if (index == 0) {
+ return getHead();
+ } else {
+ index--;
+ return (AbsContactItem) (index >= 0 && index < items.size() ? items.get(index) : null);
+ }
+ } else {
+ return (AbsContactItem) (index >= 0 && index < items.size() ? items.get(index) : null);
+ }
+ }
+
+ AbsContactItem getHead() {
+ return hasHead ? new LabelItem(title) : null;
+ }
+
+ List getItems() {
+ return items;
+ }
+
+ void add(AbsContactItem add) {
+ if (add instanceof Comparable) {
+ addComparable((Comparable) add);
+ } else {
+ items.add(add);
+ }
+ }
+
+ void merge(Group group) {
+ for (Object item : group.items) {
+ add((AbsContactItem) item);
+ }
+ }
+
+ void addComparable(Comparable add) {
+ if (items.size() < 8) {
+ for (int index = 0; index < items.size(); index++) {
+ Comparable item = (Comparable) items.get(index);
+ if ((item.compareTo((AbsContactItem) add)) > 0) {
+ items.add(index, add);
+ return;
+ }
+ }
+ items.add(add);
+ } else {
+ int index = Collections.binarySearch(items, add);
+ if (index < 0) {
+ index = -index;
+ --index;
+ }
+ if (index >= items.size()) {
+ items.add(add);
+ } else {
+ items.add(index, add);
+ }
+ }
+ }
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactDataAdapter.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactDataAdapter.java
new file mode 100644
index 0000000..0c730c1
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactDataAdapter.java
@@ -0,0 +1,350 @@
+package com.netease.nim.uikit.business.contact.core.model;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ContactItemFilter;
+import com.netease.nim.uikit.business.contact.core.model.ContactDataTask.Host;
+import com.netease.nim.uikit.business.contact.core.query.IContactDataProvider;
+import com.netease.nim.uikit.business.contact.core.query.TextQuery;
+import com.netease.nim.uikit.business.contact.core.viewholder.AbsContactViewHolder;
+import com.netease.nim.uikit.common.ui.liv.LetterIndexView;
+import com.netease.nim.uikit.common.ui.liv.LivIndex;
+import com.netease.nim.uikit.common.util.log.LogUtil;
+import com.netease.nim.uikit.impl.cache.UIKitLogTag;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * 通讯录数据适配器
+ *
+ * Created by huangjun on 2015/2/10.
+ */
+public class ContactDataAdapter extends BaseAdapter {
+
+ //
+ // COMPONENTS
+ //
+
+ private final Context context;
+
+ private final Map>> viewHolderMap;
+
+ private final ContactGroupStrategy groupStrategy;
+
+ private final IContactDataProvider dataProvider;
+
+ //
+ // DATAS
+ //
+
+ private AbsContactDataList datas;
+
+ protected final HashMap indexes = new HashMap<>();
+
+ //
+ // OPTIONS
+ //
+
+ private ContactItemFilter filter;
+
+ private ContactItemFilter disableFilter;
+
+ public ContactDataAdapter(Context context, ContactGroupStrategy groupStrategy, IContactDataProvider dataProvider) {
+ this.context = context;
+ this.groupStrategy = groupStrategy;
+ this.dataProvider = dataProvider;
+ this.viewHolderMap = new HashMap<>(6);
+ }
+
+ public void addViewHolder(int itemDataType, Class extends AbsContactViewHolder extends AbsContactItem>> viewHolder) {
+ this.viewHolderMap.put(itemDataType, viewHolder);
+ }
+
+ public final void setFilter(ContactItemFilter filter) {
+ this.filter = filter;
+ }
+
+ public final void setDisableFilter(ContactItemFilter disableFilter) {
+ this.disableFilter = disableFilter;
+ }
+
+ public final LivIndex createLivIndex(ListView lv, LetterIndexView liv, TextView tvHit, ImageView ivBk) {
+ return new LivIndex(lv, liv, tvHit, ivBk, getIndexes());
+ }
+
+ //
+ // BaseAdapter
+ //
+
+ @Override
+ public int getCount() {
+ return datas != null ? datas.getCount() : 0;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return datas != null ? datas.getItem(position) : null;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return datas != null ? datas.isEmpty() : true;
+ }
+
+ public final TextQuery getQuery() {
+ return datas != null ? datas.getQuery() : null;
+ }
+
+ private void updateData(AbsContactDataList datas) {
+ this.datas = datas;
+
+ updateIndexes(datas.getIndexes());
+
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ Object obj = getItem(position);
+ if (obj == null) {
+ return -1;
+ }
+ AbsContactItem item = (AbsContactItem) obj;
+ int type = item.getItemType();
+ Integer[] types = viewHolderMap.keySet().toArray(new Integer[viewHolderMap.size()]);
+
+ for (int i = 0; i < types.length; i++) {
+ int itemType = types[i];
+ if (itemType == type) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return viewHolderMap.size();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ AbsContactItem item = (AbsContactItem) getItem(position);
+ if (item == null) {
+ return null;
+ }
+ AbsContactViewHolder holder = null;
+ try {
+ if (convertView == null || (holder = (AbsContactViewHolder) convertView.getTag()) == null) {
+ holder = (AbsContactViewHolder) viewHolderMap.get(item.getItemType()).newInstance();
+ if (holder != null) {
+ holder.create(context);
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ if (holder == null) {
+ return null;
+ }
+
+ holder.refresh(this, position, item);
+ convertView = holder.getView();
+ if (convertView != null) {
+ convertView.setTag(holder);
+ }
+
+ return convertView;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ if (disableFilter != null) {
+ return !disableFilter.filter((AbsContactItem) getItem(position));
+ }
+
+ return true;
+ }
+
+ //
+ // LOAD
+ //
+
+ public final void query(String query) {
+ startTask(new TextQuery(query), true);
+ }
+
+ public final boolean load(boolean reload) {
+ if (!reload && !isEmpty()) {
+ return false;
+ }
+
+ LogUtil.i(UIKitLogTag.CONTACT, "contact load data");
+
+ startTask(null, false);
+
+ return true;
+ }
+
+ public final void query(TextQuery query) {
+ startTask(query, true);
+ }
+
+ private final List tasks = new ArrayList<>();
+
+ /**
+ * 启动搜索任务
+ *
+ * @param query 要搜索的信息,填null表示查询所有数据
+ * @param abort 是否终止:例如搜索的时候,第一个搜索词还未搜索完成,第二个搜索词已生成,那么取消之前的搜索任务
+ */
+ private void startTask(TextQuery query, boolean abort) {
+ if (abort) {
+ for (Task task : tasks) {
+ task.cancel(false); // 设为true有风险!
+ }
+ }
+
+ Task task = new Task(new ContactDataTask(query, dataProvider, filter) {
+ @Override
+ protected void onPreProvide(AbsContactDataList datas) {
+ List extends AbsContactItem> itemsND = onNonDataItems();
+
+ if (itemsND != null) {
+ for (AbsContactItem item : itemsND) {
+ datas.add(item);
+ }
+ }
+ }
+ });
+
+ tasks.add(task);
+
+ task.execute();
+ }
+
+ private void onTaskFinish(Task task) {
+ tasks.remove(task);
+ }
+
+ /**
+ * 搜索/查询数据异步任务
+ */
+
+ private class Task extends AsyncTask implements Host {
+ final ContactDataTask task;
+
+ Task(ContactDataTask task) {
+ task.setHost(this);
+
+ this.task = task;
+ }
+
+ //
+ // HOST
+ //
+
+ @Override
+ public void onData(ContactDataTask task, AbsContactDataList datas, boolean all) {
+ publishProgress(datas, all);
+ }
+
+ @Override
+ public boolean isCancelled(ContactDataTask task) {
+ return isCancelled();
+ }
+
+ //
+ // AsyncTask
+ //
+
+ @Override
+ protected void onPreExecute() {
+ onPreReady();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ task.run(new ContactDataList(groupStrategy));
+
+ return null;
+ }
+
+ @Override
+ protected void onProgressUpdate(Object... values) {
+ AbsContactDataList datas = (AbsContactDataList) values[0];
+ boolean all = (Boolean) values[1];
+
+ onPostLoad(datas.isEmpty(), datas.getQueryText(), all);
+
+ updateData(datas);
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ onTaskFinish(this);
+ }
+
+ @Override
+ protected void onCancelled() {
+ onTaskFinish(this);
+ }
+ }
+
+ //
+ // Overrides
+ //
+
+ /**
+ * 数据未准备
+ */
+ protected void onPreReady() {
+ }
+
+ /**
+ * 数据加载完成
+ */
+ protected void onPostLoad(boolean empty, String query, boolean all) {
+ }
+
+ /**
+ * 加载完成后,加入非数据项
+ *
+ * @return
+ */
+ protected List extends AbsContactItem> onNonDataItems() {
+ return null;
+ }
+
+ //
+ // INDEX
+ //
+
+ private Map getIndexes() {
+ return this.indexes;
+ }
+
+ private void updateIndexes(Map indexes) {
+ // CLEAR
+ this.indexes.clear();
+ // SET
+ this.indexes.putAll(indexes);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactDataList.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactDataList.java
new file mode 100644
index 0000000..3291281
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactDataList.java
@@ -0,0 +1,110 @@
+package com.netease.nim.uikit.business.contact.core.model;
+
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * 通讯录列表数据(分组、索引)
+ * Created by huangjun on 2015/2/10.
+ */
+public class ContactDataList extends AbsContactDataList {
+ //
+ // RESULT DATA
+ //
+
+ private List groups;
+
+ private Map indexes;
+
+ public ContactDataList(ContactGroupStrategy groupStrategy) {
+ super(groupStrategy);
+ }
+
+ @Override
+ public int getCount() {
+ int count = 0;
+ for (Group group : groups) {
+ count += group.getCount();
+ }
+ return count;
+ }
+
+ @Override
+ public AbsContactItem getItem(int index) {
+ int count = 0;
+ for (Group group : groups) {
+ int gIndex = index - count;
+ int gCount = group.getCount();
+
+ if (gIndex >= 0 && gIndex < gCount) {
+ return group.getItem(gIndex);
+ }
+
+ count += gCount;
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return groups.isEmpty() || indexes.isEmpty();
+ }
+
+ @Override
+ public List getItems() {
+ List items = new ArrayList();
+ for (Group group : groups) {
+ AbsContactItem head = group.getHead();
+ if (head != null) {
+ items.add(head);
+ }
+ items.addAll(group.getItems());
+ }
+
+ return items;
+ }
+
+ @Override
+ public Map getIndexes() {
+ return indexes;
+ }
+
+ @Override
+ public void build() {
+ //
+ // GROUPS
+ //
+
+ List groups = new ArrayList();
+ groups.add(groupNull);
+ groups.addAll(groupMap.values());
+ sortGroups(groups);
+
+ //
+ // INDEXES
+ //
+
+ Map indexes = new HashMap();
+ int count = 0;
+ for (Group group : groups) {
+ if (group.id != null) {
+ indexes.put(group.id, count);
+ }
+
+ count += group.getCount();
+ }
+
+ //
+ // RESULT
+ //
+
+ this.groups = groups;
+ this.indexes = indexes;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactDataTask.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactDataTask.java
new file mode 100644
index 0000000..3e9f193
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactDataTask.java
@@ -0,0 +1,90 @@
+package com.netease.nim.uikit.business.contact.core.model;
+
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ContactItemFilter;
+import com.netease.nim.uikit.business.contact.core.query.IContactDataProvider;
+import com.netease.nim.uikit.business.contact.core.query.TextQuery;
+
+import java.util.List;
+
+/**
+ * 通讯录获取数据任务
+ * Created by huangjun on 2015/2/10.
+ */
+public class ContactDataTask {
+
+ public interface Host {
+ public void onData(ContactDataTask task, AbsContactDataList datas, boolean all); // 搜索完成,返回数据给调用方
+
+ public boolean isCancelled(ContactDataTask task); // 判断调用放是否已经取消
+ }
+
+ private final IContactDataProvider dataProvider; // 数据源提供者
+
+ private final ContactItemFilter filter; // 项过滤器
+
+ private final TextQuery query; // 要搜索的信息,null为查询所有
+
+ private Host host;
+
+ public ContactDataTask(TextQuery query, IContactDataProvider dataProvider, ContactItemFilter filter) {
+ this.query = query;
+ this.dataProvider = dataProvider;
+ this.filter = filter;
+ }
+
+ public final void setHost(Host host) {
+ this.host = host;
+ }
+
+ protected void onPreProvide(AbsContactDataList datas) {
+
+ }
+
+ public final void run(AbsContactDataList datas) {
+ // CANCELLED
+ if (isCancelled()) {
+ return;
+ }
+
+ // PRE PROVIDE
+ onPreProvide(datas);
+
+ // CANCELLED
+ if (isCancelled()) {
+ return;
+ }
+
+ // PROVIDE
+ List items = dataProvider.provide(query);
+
+ // ADD
+ add(datas, items, filter);
+
+ // BUILD
+ datas.build();
+
+ // PUBLISH ALL
+ publish(datas, true);
+ }
+
+ private void publish(AbsContactDataList datas, boolean all) {
+ if (host != null) {
+ datas.setQuery(query);
+
+ host.onData(this, datas, all);
+ }
+ }
+
+ private boolean isCancelled() {
+ return host != null && host.isCancelled(this);
+ }
+
+ private static void add(AbsContactDataList datas, List items, ContactItemFilter filter) {
+ for (AbsContactItem item : items) {
+ if (filter == null || !filter.filter(item)) {
+ datas.add(item);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactGroupStrategy.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactGroupStrategy.java
new file mode 100644
index 0000000..4d56f37
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/ContactGroupStrategy.java
@@ -0,0 +1,94 @@
+package com.netease.nim.uikit.business.contact.core.model;
+
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 通讯录分组策略
+ * Created by huangjun on 2015/2/10.
+ */
+
+public class ContactGroupStrategy implements Comparator {
+ public static final String GROUP_SHARP = "#";
+
+ public static final String GROUP_TEAM = "@";
+
+ public static final String GROUP_NULL = "?";
+
+ private static final class Group {
+ private final int order;
+
+ private final String name;
+
+ public Group(int order, String name) {
+ this.order = order;
+ this.name = name;
+ }
+ }
+
+ private final Map groups = new HashMap();
+
+ public String belongs(AbsContactItem item) {
+ return item.belongsGroup();
+ }
+
+ protected final void add(String id, int order, String name) {
+ groups.put(id, new Group(order, name));
+ }
+
+ protected final int addABC(int order) {
+ String id = ContactGroupStrategy.GROUP_SHARP;
+
+ add(id, order++, id);
+
+ for (char i = 0; i < 26; i++) {
+ id = Character.toString((char) ('A' + i));
+
+ add(id, order++, id);
+ }
+
+ return order;
+ }
+
+ public final String getName(String id) {
+ Group group = groups.get(id);
+ String name = group != null ? group.name : null;
+ return name != null ? name : "";
+ }
+
+ private Integer toOrder(String id) {
+ Group group = groups.get(id);
+ return group != null ? group.order : null;
+ }
+
+ @Override
+ public int compare(String lhs, String rhs) {
+ if (lhs == null) {
+ lhs = ContactGroupStrategy.GROUP_NULL;
+ }
+
+ if (rhs == null) {
+ rhs = ContactGroupStrategy.GROUP_NULL;
+ }
+
+ Integer lhsO = toOrder(lhs);
+ Integer rhsO = toOrder(rhs);
+
+ if (lhsO == rhsO) {
+ return 0;
+ }
+
+ if (lhsO == null) {
+ return -1;
+ }
+
+ if (rhsO == null) {
+ return 1;
+ }
+
+ return lhsO - rhsO;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/IContact.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/IContact.java
new file mode 100644
index 0000000..e01b484
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/IContact.java
@@ -0,0 +1,48 @@
+package com.netease.nim.uikit.business.contact.core.model;
+
+public interface IContact {
+
+ interface Type {
+
+ /**
+ * TYPE USER
+ */
+ int Friend = 0x1;
+
+ /**
+ * TYPE TEAM
+ */
+ int Team = 0x2;
+
+ /**
+ * TYPE TEAM MEMBER
+ */
+ int TeamMember = 0x03;
+
+ /**
+ * TYPE_MSG
+ */
+ int Msg = 0x04;
+ }
+
+ /**
+ * get contact id
+ *
+ * @return
+ */
+ String getContactId();
+
+ /**
+ * get contact type {@link Type}
+ *
+ * @return
+ */
+ int getContactType();
+
+ /**
+ * get contact's display name to show to user
+ *
+ * @return
+ */
+ String getDisplayName();
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/TeamContact.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/TeamContact.java
new file mode 100644
index 0000000..6281aa4
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/TeamContact.java
@@ -0,0 +1,35 @@
+package com.netease.nim.uikit.business.contact.core.model;
+
+import android.text.TextUtils;
+
+import com.netease.nimlib.sdk.team.model.Team;
+
+public class TeamContact extends AbsContact {
+
+ private Team team;
+
+ public TeamContact(Team team) {
+ this.team = team;
+ }
+
+ @Override
+ public String getContactId() {
+ return team == null ? "" : team.getId();
+ }
+
+ @Override
+ public int getContactType() {
+ return IContact.Type.Team;
+ }
+
+ @Override
+ public String getDisplayName() {
+ String name = team.getName();
+
+ return TextUtils.isEmpty(name) ? team.getId() : name;
+ }
+
+ public Team getTeam() {
+ return team;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/TeamMemberContact.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/TeamMemberContact.java
new file mode 100644
index 0000000..6b8242d
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/model/TeamMemberContact.java
@@ -0,0 +1,31 @@
+package com.netease.nim.uikit.business.contact.core.model;
+
+import com.netease.nim.uikit.business.team.helper.TeamHelper;
+import com.netease.nimlib.sdk.team.model.TeamMember;
+
+/**
+ * Created by huangjun on 2015/5/5.
+ */
+public class TeamMemberContact extends AbsContact {
+
+ private TeamMember teamMember;
+
+ public TeamMemberContact(TeamMember teamMember) {
+ this.teamMember = teamMember;
+ }
+
+ @Override
+ public String getContactId() {
+ return teamMember.getAccount();
+ }
+
+ @Override
+ public int getContactType() {
+ return Type.TeamMember;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return TeamHelper.getTeamMemberDisplayName(teamMember.getTid(), teamMember.getAccount());
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/ContactDataProvider.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/ContactDataProvider.java
new file mode 100644
index 0000000..b2bb237
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/ContactDataProvider.java
@@ -0,0 +1,44 @@
+package com.netease.nim.uikit.business.contact.core.provider;
+
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ItemTypes;
+import com.netease.nim.uikit.business.contact.core.query.IContactDataProvider;
+import com.netease.nim.uikit.business.contact.core.query.TextQuery;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ContactDataProvider implements IContactDataProvider {
+
+ private int[] itemTypes;
+
+ public ContactDataProvider(int... itemTypes) {
+ this.itemTypes = itemTypes;
+ }
+
+ @Override
+ public List provide(TextQuery query) {
+ List data = new ArrayList<>();
+
+ for (int itemType : itemTypes) {
+ data.addAll(provide(itemType, query));
+ }
+
+ return data;
+ }
+
+ private final List provide(int itemType, TextQuery query) {
+ switch (itemType) {
+ case ItemTypes.FRIEND:
+ return UserDataProvider.provide(query);
+ case ItemTypes.TEAM:
+ case ItemTypes.TEAMS.ADVANCED_TEAM:
+ case ItemTypes.TEAMS.NORMAL_TEAM:
+ return TeamDataProvider.provide(query, itemType);
+ case ItemTypes.MSG:
+ return MsgDataProvider.provide(query);
+ default:
+ return new ArrayList<>();
+ }
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/ContactSearch.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/ContactSearch.java
new file mode 100644
index 0000000..76bd892
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/ContactSearch.java
@@ -0,0 +1,120 @@
+package com.netease.nim.uikit.business.contact.core.provider;
+
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.business.contact.core.provider.ContactSearch.HitInfo.Type;
+import com.netease.nim.uikit.business.contact.core.query.TextQuery;
+import com.netease.nim.uikit.business.contact.core.query.TextSearcher;
+import com.netease.nim.uikit.business.team.helper.TeamHelper;
+import com.netease.nim.uikit.business.uinfo.UserInfoHelper;
+import com.netease.nimlib.sdk.team.model.Team;
+import com.netease.nimlib.sdk.team.model.TeamMember;
+import com.netease.nimlib.sdk.uinfo.model.UserInfo;
+
+public class ContactSearch {
+ public static final class HitInfo {
+ public enum Type {
+ Account, Name,
+ }
+
+ public final Type type;
+
+ public final String text;
+
+ public final int[] range;
+
+ public HitInfo(Type type, String text, int[] range) {
+ this.type = type;
+ this.text = text;
+ this.range = range;
+ }
+ }
+
+ /**
+ * 判断是否击中
+ */
+
+ static boolean hitUser(UserInfo contact, TextQuery query) {
+ String account = contact.getAccount();
+ String name = UserInfoHelper.getUserName(account);
+
+ return TextSearcher.contains(query.t9, name, query.text) || TextSearcher.contains(query.t9, account, query.text);
+ }
+
+ static boolean hitFriend(UserInfo contact, TextQuery query) {
+ String account = contact.getAccount();
+ String alias = NimUIKit.getContactProvider().getAlias(account);
+
+ return TextSearcher.contains(query.t9, account, query.text) || TextSearcher.contains(query.t9, alias, query.text);
+ }
+
+ static boolean hitTeam(Team contact, TextQuery query) {
+ String name = contact.getName();
+ String teamId = contact.getId();
+
+ return TextSearcher.contains(query.t9, name, query.text) || TextSearcher.contains(query.t9, teamId, query.text);
+ }
+
+ public static final boolean hitTeamMember(TeamMember teamMember, TextQuery query) {
+ String name = TeamHelper.getTeamMemberDisplayName(teamMember.getTid(), teamMember.getAccount());
+
+ return TextSearcher.contains(query.t9, name, query.text);
+ }
+
+ /**
+ * 返回击中信息(可进行击中文本高亮显示)
+ */
+
+ public static final HitInfo hitInfo(IContact contact, TextQuery query) {
+ if (contact.getContactType() == IContact.Type.Friend) {
+ return hitInfoFriend(contact, query);
+ } else if (contact.getContactType() == IContact.Type.Team) {
+ return hitInfoTeamContact(contact, query);
+ }
+
+ return hitInfoContact(contact, query);
+ }
+
+ public static final HitInfo hitInfoFriend(IContact contact, TextQuery query) {
+ String name = contact.getDisplayName();
+ String account = contact.getContactId();
+
+ int[] range = TextSearcher.indexOf(query.t9, name, query.text);
+
+ if (range != null) {
+ return new HitInfo(Type.Name, name, range);
+ }
+
+ range = TextSearcher.indexOf(query.t9, account, query.text);
+
+ if (range != null) {
+ return new HitInfo(Type.Account, account, range);
+ }
+
+ return null;
+ }
+
+ public static final HitInfo hitInfoTeamContact(IContact contact, TextQuery query) {
+ String name = contact.getDisplayName();
+
+ int[] range = TextSearcher.indexOf(query.t9, name, query.text);
+
+ if (range != null) {
+ return new HitInfo(Type.Name, name, range);
+ }
+
+ return null;
+ }
+
+ public static final HitInfo hitInfoContact(IContact contact, TextQuery query) {
+ String name = contact.getDisplayName();
+
+ int[] range = TextSearcher.indexOf(query.t9, name, query.text);
+
+ if (range != null) {
+ return new HitInfo(Type.Name, name, range);
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/MsgDataProvider.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/MsgDataProvider.java
new file mode 100644
index 0000000..dd33055
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/MsgDataProvider.java
@@ -0,0 +1,86 @@
+package com.netease.nim.uikit.business.contact.core.provider;
+
+import android.text.TextUtils;
+
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.MsgItem;
+import com.netease.nim.uikit.business.contact.core.query.TextQuery;
+import com.netease.nim.uikit.business.contact.core.util.ContactHelper;
+import com.netease.nim.uikit.common.util.log.LogUtil;
+import com.netease.nimlib.sdk.NIMClient;
+import com.netease.nimlib.sdk.lucene.LuceneService;
+import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
+import com.netease.nimlib.sdk.search.model.MsgIndexRecord;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 消息全文检索数据提供者
+ */
+public final class MsgDataProvider {
+
+ private static final String TAG = "MsgDataProvider";
+
+ public static final List provide(TextQuery query) {
+ if (TextUtils.isEmpty(query.text) || TextUtils.isEmpty(query.text.trim())) {
+ return new ArrayList<>(0);
+ }
+
+ // fetch result
+ List sources;
+ boolean querySession;
+ if (query.extra != null) {
+ SessionTypeEnum sessionType = (SessionTypeEnum) query.extra[0];
+ String sessionId = (String) query.extra[1];
+ MsgIndexRecord anchor = null;
+ if (query.extra.length >= 3) {
+ anchor = (MsgIndexRecord) query.extra[2];
+ }
+ sources = searchSession(query.text, sessionType, sessionId, anchor);
+ querySession = true;
+ } else {
+ sources = searchAllSession(query.text);
+ querySession = false;
+ }
+
+ // build AbsContactItem
+ if (sources == null) {
+ return new ArrayList<>(0);
+ }
+
+ List items = new ArrayList<>(sources.size());
+ for (MsgIndexRecord r : sources) {
+ items.add(new MsgItem(ContactHelper.makeContactFromMsgIndexRecord(r), r, querySession));
+ }
+
+ return items;
+ }
+
+ private static List searchSession(String query, SessionTypeEnum sessionType, String sessionId, MsgIndexRecord anchor) {
+ long startTime = System.currentTimeMillis();
+
+ List result;
+ if (anchor != null) {
+ result = NIMClient.getService(LuceneService.class).searchSessionNextPageBlock(query, sessionType, sessionId, anchor, 50);
+ } else {
+ result = NIMClient.getService(LuceneService.class).searchSessionBlock(query, sessionType, sessionId);
+ }
+
+ log(true, result, System.currentTimeMillis() - startTime);
+
+ return result;
+ }
+
+ private static List searchAllSession(String query) {
+ long startTime = System.currentTimeMillis();
+ List result = NIMClient.getService(LuceneService.class).searchAllSessionBlock(query, -1);
+ log(false, result, System.currentTimeMillis() - startTime);
+
+ return result;
+ }
+
+ private static void log(boolean searchSession, List result, long cost) {
+ LogUtil.d(TAG, (searchSession ? "search session" : "search all session") + ", result size=" + (result == null ? 0 : result.size()) + ", cost=" + cost);
+ }
+}
\ No newline at end of file
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/TeamDataProvider.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/TeamDataProvider.java
new file mode 100644
index 0000000..e091c48
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/TeamDataProvider.java
@@ -0,0 +1,75 @@
+package com.netease.nim.uikit.business.contact.core.provider;
+
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.api.model.team.TeamProvider;
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ItemTypes;
+import com.netease.nim.uikit.business.contact.core.model.ContactGroupStrategy;
+import com.netease.nim.uikit.business.contact.core.model.TeamContact;
+import com.netease.nim.uikit.business.contact.core.query.TextComparator;
+import com.netease.nim.uikit.business.contact.core.query.TextQuery;
+import com.netease.nimlib.sdk.team.constant.TeamTypeEnum;
+import com.netease.nimlib.sdk.team.model.Team;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 群数据源提供者
+ *
+ * Created by huangjun on 2015/3/1.
+ */
+public class TeamDataProvider {
+ public static final List provide(TextQuery query, int itemType) {
+ List sources = query(query, itemType);
+ List items = new ArrayList<>(sources.size());
+ for (TeamContact t : sources) {
+ items.add(createTeamItem(t));
+ }
+
+ return items;
+ }
+
+ private static AbsContactItem createTeamItem(TeamContact team) {
+ return new ContactItem(team, ItemTypes.TEAM) {
+ @Override
+ public int compareTo(ContactItem item) {
+ return compareTeam((TeamContact) getContact(), (TeamContact) (item.getContact()));
+ }
+
+ @Override
+ public String belongsGroup() {
+ return ContactGroupStrategy.GROUP_TEAM;
+ }
+ };
+ }
+
+ private static int compareTeam(TeamContact lhs, TeamContact rhs) {
+ return TextComparator.compareIgnoreCase(lhs.getDisplayName(), rhs.getDisplayName());
+ }
+
+ /**
+ * * 数据查询
+ */
+ private static final List query(TextQuery query, int itemType) {
+ List teams;
+ TeamProvider provider = NimUIKit.getTeamProvider();
+ if (itemType == ItemTypes.TEAMS.ADVANCED_TEAM) {
+ teams = provider.getAllTeamsByType(TeamTypeEnum.Advanced);
+ } else if (itemType == ItemTypes.TEAMS.NORMAL_TEAM) {
+ teams = provider.getAllTeamsByType(TeamTypeEnum.Normal);
+ } else {
+ teams = provider.getAllTeams();
+ }
+
+ List contacts = new ArrayList<>();
+ for (Team t : teams) {
+ if (query == null || ContactSearch.hitTeam(t, query)) {
+ contacts.add(new TeamContact(t));
+ }
+ }
+
+ return contacts;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/TeamMemberDataProvider.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/TeamMemberDataProvider.java
new file mode 100644
index 0000000..3e2fbd9
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/TeamMemberDataProvider.java
@@ -0,0 +1,96 @@
+package com.netease.nim.uikit.business.contact.core.provider;
+
+import android.text.TextUtils;
+
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.api.model.SimpleCallback;
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ItemTypes;
+import com.netease.nim.uikit.business.contact.core.model.ContactGroupStrategy;
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.business.contact.core.model.TeamMemberContact;
+import com.netease.nim.uikit.business.contact.core.query.TextComparator;
+import com.netease.nim.uikit.business.contact.core.query.TextQuery;
+import com.netease.nimlib.sdk.team.model.TeamMember;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 群成员数据源提供者
+ *
+ * Created by huangjun on 2015/5/4.
+ */
+public class TeamMemberDataProvider {
+ public static final List provide(TextQuery query, String tid) {
+ List sources = query(query, tid);
+ List items = new ArrayList<>(sources.size());
+ for (TeamMemberContact t : sources) {
+ items.add(createTeamMemberItem(t));
+ }
+
+ return items;
+ }
+
+ private static AbsContactItem createTeamMemberItem(TeamMemberContact teamMember) {
+ return new ContactItem(teamMember, ItemTypes.TEAM_MEMBER) {
+ @Override
+ public int compareTo(ContactItem item) {
+ return compareTeamMember((TeamMemberContact) getContact(), (TeamMemberContact) (item.getContact()));
+ }
+
+ @Override
+ public String belongsGroup() {
+ String group = TextComparator.getLeadingUp(getCompare());
+ return !TextUtils.isEmpty(group) ? group : ContactGroupStrategy.GROUP_TEAM;
+ }
+
+ private String getCompare() {
+ IContact contact = getContact();
+ return contact != null ? contact.getDisplayName() : null;
+ }
+ };
+ }
+
+ private static int compareTeamMember(TeamMemberContact lhs, TeamMemberContact rhs) {
+ return TextComparator.compareIgnoreCase(lhs.getDisplayName(), rhs.getDisplayName());
+ }
+
+ /**
+ * * 数据查询
+ */
+ private static final List query(TextQuery query, String tid) {
+ List teamMembers = NimUIKit.getTeamProvider().getTeamMemberList(tid);
+
+ List contacts = new ArrayList<>();
+ for (TeamMember t : teamMembers) {
+ if (t != null && (query == null || ContactSearch.hitTeamMember(t, query))) {
+ contacts.add(new TeamMemberContact(t));
+ }
+ }
+
+ return contacts;
+ }
+
+ /**
+ * 发起异步任务load群成员进入缓存
+ *
+ * @param tid
+ * @param callback
+ */
+ public static void loadTeamMemberDataAsync(String tid, final LoadTeamMemberCallback callback) {
+ NimUIKit.getTeamProvider().fetchTeamMemberList(tid, new SimpleCallback>() {
+ @Override
+ public void onResult(boolean success, List result, int code) {
+ if (callback != null) {
+ callback.onResult(success);
+ }
+ }
+ });
+ }
+
+ public interface LoadTeamMemberCallback {
+ void onResult(boolean success);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/UserDataProvider.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/UserDataProvider.java
new file mode 100644
index 0000000..3a0c03f
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/provider/UserDataProvider.java
@@ -0,0 +1,47 @@
+package com.netease.nim.uikit.business.contact.core.provider;
+
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ItemTypes;
+import com.netease.nim.uikit.business.contact.core.query.TextQuery;
+import com.netease.nim.uikit.business.contact.core.util.ContactHelper;
+import com.netease.nim.uikit.common.util.log.LogUtil;
+import com.netease.nim.uikit.impl.cache.UIKitLogTag;
+import com.netease.nimlib.sdk.uinfo.model.UserInfo;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+final class UserDataProvider {
+ public static List provide(TextQuery query) {
+ List sources = query(query);
+ List items = new ArrayList<>(sources.size());
+ for (UserInfo u : sources) {
+ items.add(new ContactItem(ContactHelper.makeContactFromUserInfo(u), ItemTypes.FRIEND));
+ }
+
+ LogUtil.i(UIKitLogTag.CONTACT, "contact provide data size =" + items.size());
+ return items;
+ }
+
+ private static final List query(TextQuery query) {
+
+ List friends = NimUIKit.getContactProvider().getUserInfoOfMyFriends();
+ List users = NimUIKit.getUserInfoProvider().getUserInfo(friends);
+ if (query == null) {
+ return users;
+ }
+
+ UserInfo user;
+ for (Iterator iter = users.iterator(); iter.hasNext(); ) {
+ user = iter.next();
+ boolean hit = ContactSearch.hitUser(user, query) || (ContactSearch.hitFriend(user, query));
+ if (!hit) {
+ iter.remove();
+ }
+ }
+ return users;
+ }
+}
\ No newline at end of file
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/IContactDataProvider.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/IContactDataProvider.java
new file mode 100644
index 0000000..81c85e2
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/IContactDataProvider.java
@@ -0,0 +1,13 @@
+package com.netease.nim.uikit.business.contact.core.query;
+
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+
+import java.util.List;
+
+/**
+ * 通讯录数据源提供者接口
+ * Created by huangjun on 2015/4/2.
+ */
+public interface IContactDataProvider {
+ public List provide(TextQuery query);
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/PinYin.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/PinYin.java
new file mode 100644
index 0000000..0fcf33f
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/PinYin.java
@@ -0,0 +1,532 @@
+package com.netease.nim.uikit.business.contact.core.query;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PinYin {
+ private static final String ASSET = "pinyin/index.dat";
+
+ private static final char START = 0x4e00;
+
+ private static final char END = 0x9fa5;
+
+ private static final String[] pinyin = {"a", "ai", "an", "ang", "ao",
+ "ba", "bai", "ban", "bang", "bao", "bei", "ben", "beng", "bi",
+ "bian", "biao", "bie", "bin", "bing", "bo", "bu", "ca", "cai",
+ "can", "cang", "cao", "ce", "ceng", "cha", "chai", "chan", "chang",
+ "chao", "che", "chen", "cheng", "chi", "chong", "chou", "chu",
+ "chuai", "chuan", "chuang", "chui", "chun", "chuo", "ci", "cong",
+ "cou", "cu", "cuan", "cui", "cun", "cuo", "da", "dai", "dan",
+ "dang", "dao", "de", "deng", "di", "dian", "diao", "die", "ding",
+ "diu", "dong", "dou", "du", "duan", "dui", "dun", "duo", "e", "en",
+ "er", "fa", "fan", "fang", "fei", "fen", "feng", "fo", "fou", "fu",
+ "ga", "gai", "gan", "gang", "gao", "ge", "gei", "gen", "geng",
+ "gong", "gou", "gu", "gua", "guai", "guan", "guang", "gui", "gun",
+ "guo", "ha", "hai", "han", "hang", "hao", "he", "hei", "hen",
+ "heng", "hong", "hou", "hu", "hua", "huai", "huan", "huang", "hui",
+ "hun", "huo", "ji", "jia", "jian", "jiang", "jiao", "jie", "jin",
+ "jing", "jiong", "jiu", "ju", "juan", "jue", "jun", "ka", "kai",
+ "kan", "kang", "kao", "ke", "ken", "keng", "kong", "kou", "ku",
+ "kua", "kuai", "kuan", "kuang", "kui", "kun", "kuo", "la", "lai",
+ "lan", "lang", "lao", "le", "lei", "leng", "li", "lia", "lian",
+ "liang", "liao", "lie", "lin", "ling", "liu", "long", "lou", "lu",
+ "lv", "luan", "lue", "lun", "luo", "ma", "mai", "man", "mang",
+ "mao", "me", "mei", "men", "meng", "mi", "mian", "miao", "mie",
+ "min", "ming", "miu", "mo", "mou", "mu", "na", "nai", "nan",
+ "nang", "nao", "ne", "nei", "nen", "neng", "ni", "nian", "niang",
+ "niao", "nie", "nin", "ning", "niu", "nong", "nu", "nv", "nuan",
+ "nue", "nuo", "o", "ou", "pa", "pai", "pan", "pang", "pao", "pei",
+ "pen", "peng", "pi", "pian", "piao", "pie", "pin", "ping", "po",
+ "pu", "qi", "qia", "qian", "qiang", "qiao", "qie", "qin", "qing",
+ "qiong", "qiu", "qu", "quan", "que", "qun", "ran", "rang", "rao",
+ "re", "ren", "reng", "ri", "rong", "rou", "ru", "ruan", "rui",
+ "run", "ruo", "sa", "sai", "san", "sang", "sao", "se", "sen",
+ "seng", "sha", "shai", "shan", "shang", "shao", "she", "shen",
+ "sheng", "shi", "shou", "shu", "shua", "shuai", "shuan", "shuang",
+ "shui", "shun", "shuo", "si", "song", "sou", "su", "suan", "sui",
+ "sun", "suo", "ta", "tai", "tan", "tang", "tao", "te", "teng",
+ "ti", "tian", "tiao", "tie", "ting", "tong", "tou", "tu", "tuan",
+ "tui", "tun", "tuo", "wa", "wai", "wan", "wang", "wei", "wen",
+ "weng", "wo", "wu", "xi", "xia", "xian", "xiang", "xiao", "xie",
+ "xin", "xing", "xiong", "xiu", "xu", "xuan", "xue", "xun", "ya",
+ "yan", "yang", "yao", "ye", "yi", "yin", "ying", "yo", "yong",
+ "you", "yu", "yuan", "yue", "yun", "za", "zai", "zan", "zang",
+ "zao", "ze", "zei", "zen", "zeng", "zha", "zhai", "zhan", "zhang",
+ "zhao", "zhe", "zhen", "zheng", "zhi", "zhong", "zhou", "zhu",
+ "zhua", "zhuai", "zhuan", "zhuang", "zhui", "zhun", "zhuo", "zi",
+ "zong", "zou", "zu", "zuan", "zui", "zun", "zuo"};
+
+ private static final String[] pinyinT9 = {"2", "24", "26", "264", "26",
+ "22", "224", "226", "2264", "226", "234", "236", "2364", "24",
+ "2426", "2426", "243", "246", "2464", "26", "28", "22", "224",
+ "226", "2264", "226", "23", "2364", "242", "2424", "2426", "24264",
+ "2426", "243", "2436", "24364", "244", "24664", "2468", "248",
+ "24824", "24826", "248264", "2484", "2486", "2486", "24", "2664",
+ "268", "28", "2826", "284", "286", "286", "32", "324", "326",
+ "3264", "326", "33", "3364", "34", "3426", "3426", "343", "3464",
+ "348", "3664", "368", "38", "3826", "384", "386", "386", "3", "36",
+ "37", "32", "326", "3264", "334", "336", "3364", "36", "368", "38",
+ "42", "424", "426", "4264", "426", "43", "434", "436", "4364",
+ "4664", "468", "48", "482", "4824", "4826", "48264", "484", "486",
+ "486", "42", "424", "426", "4264", "426", "43", "434", "436",
+ "4364", "4664", "468", "48", "482", "4824", "4826", "48264", "484",
+ "486", "486", "54", "542", "5426", "54264", "5426", "543", "546",
+ "5464", "54664", "548", "58", "5826", "583", "586", "52", "524",
+ "526", "5264", "526", "53", "536", "5364", "5664", "568", "58",
+ "582", "5824", "5826", "58264", "584", "586", "586", "52", "524",
+ "526", "5264", "526", "53", "534", "5364", "54", "542", "5426",
+ "54264", "5426", "543", "546", "5464", "548", "5664", "568", "58",
+ "58", "5826", "583", "586", "586", "62", "624", "626", "6264",
+ "626", "63", "634", "636", "6364", "64", "6426", "6426", "643",
+ "646", "6464", "648", "66", "668", "68", "62", "624", "626",
+ "6264", "626", "63", "634", "636", "6364", "64", "6426", "64264",
+ "6426", "643", "646", "6464", "648", "6664", "68", "68", "6826",
+ "683", "686", "6", "68", "72", "724", "726", "7264", "726", "734",
+ "736", "7364", "74", "7426", "7426", "743", "746", "7464", "76",
+ "78", "74", "742", "7426", "74264", "7426", "743", "746", "7464",
+ "74664", "748", "78", "7826", "783", "786", "726", "7264", "726",
+ "73", "736", "7364", "74", "7664", "768", "78", "7826", "784",
+ "786", "786", "72", "724", "726", "7264", "726", "73", "736",
+ "7364", "742", "7424", "7426", "74264", "7426", "743", "7436",
+ "74364", "744", "7468", "748", "7482", "74824", "74826", "748264",
+ "7484", "7486", "7486", "74", "7664", "768", "78", "7826", "784",
+ "786", "786", "82", "824", "826", "8264", "826", "83", "8364",
+ "84", "8426", "8426", "843", "8464", "8664", "868", "88", "8826",
+ "884", "886", "886", "92", "924", "926", "9264", "934", "936",
+ "9364", "96", "98", "94", "942", "9426", "94264", "9426", "943",
+ "946", "9464", "94664", "948", "98", "9826", "983", "986", "92",
+ "926", "9264", "926", "93", "94", "946", "9464", "96", "9664",
+ "968", "98", "9826", "983", "986", "92", "924", "926", "9264",
+ "926", "93", "934", "936", "9364", "942", "9424", "9426", "94264",
+ "9426", "943", "9436", "94364", "944", "94664", "9468", "948",
+ "9482", "94824", "94826", "948264", "9484", "9486", "9486", "94",
+ "9664", "968", "98", "9826", "984", "986", "986"};
+
+ private static final char[] leadingCUp = new char[]{'A', 'A', 'A', 'A', 'A',
+ 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B',
+ 'B', 'B', 'B', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C',
+ 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C',
+ 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'D', 'D', 'D',
+ 'D', 'D', 'D', 'D', 'D', 'D', 'D', 'D', 'D', 'D', 'D', 'D', 'D',
+ 'D', 'D', 'D', 'D', 'E', 'E', 'E', 'F', 'F', 'F', 'F', 'F', 'F',
+ 'F', 'F', 'F', 'G', 'G', 'G', 'G', 'G', 'G', 'G', 'G', 'G', 'G',
+ 'G', 'G', 'G', 'G', 'G', 'G', 'G', 'G', 'G', 'H', 'H', 'H', 'H',
+ 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H',
+ 'H', 'H', 'J', 'J', 'J', 'J', 'J', 'J', 'J', 'J', 'J', 'J', 'J',
+ 'J', 'J', 'J', 'K', 'K', 'K', 'K', 'K', 'K', 'K', 'K', 'K', 'K',
+ 'K', 'K', 'K', 'K', 'K', 'K', 'K', 'K', 'L', 'L', 'L', 'L', 'L',
+ 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L',
+ 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'M', 'M', 'M', 'M', 'M', 'M',
+ 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M', 'M',
+ 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N',
+ 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'O', 'O', 'P',
+ 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P',
+ 'P', 'P', 'Q', 'Q', 'Q', 'Q', 'Q', 'Q', 'Q', 'Q', 'Q', 'Q', 'Q',
+ 'Q', 'Q', 'Q', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R',
+ 'R', 'R', 'R', 'R', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S',
+ 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S',
+ 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'S', 'T',
+ 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T',
+ 'T', 'T', 'T', 'T', 'T', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',
+ 'W', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X',
+ 'X', 'X', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y',
+ 'Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z',
+ 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z',
+ 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z', 'Z',
+ 'Z'};
+
+ private static final char[] leadingCLo = new char[]{'a', 'a', 'a', 'a',
+ 'a', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b',
+ 'b', 'b', 'b', 'b', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c',
+ 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c',
+ 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'd', 'd',
+ 'd', 'd', 'd', 'd', 'd', 'd', 'd', 'd', 'd', 'd', 'd', 'd', 'd',
+ 'd', 'd', 'd', 'd', 'd', 'e', 'e', 'e', 'f', 'f', 'f', 'f', 'f',
+ 'f', 'f', 'f', 'f', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g',
+ 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g', 'h', 'h', 'h',
+ 'h', 'h', 'h', 'h', 'h', 'h', 'h', 'h', 'h', 'h', 'h', 'h', 'h',
+ 'h', 'h', 'h', 'j', 'j', 'j', 'j', 'j', 'j', 'j', 'j', 'j', 'j',
+ 'j', 'j', 'j', 'j', 'k', 'k', 'k', 'k', 'k', 'k', 'k', 'k', 'k',
+ 'k', 'k', 'k', 'k', 'k', 'k', 'k', 'k', 'k', 'l', 'l', 'l', 'l',
+ 'l', 'l', 'l', 'l', 'l', 'l', 'l', 'l', 'l', 'l', 'l', 'l', 'l',
+ 'l', 'l', 'l', 'l', 'l', 'l', 'l', 'l', 'm', 'm', 'm', 'm', 'm',
+ 'm', 'm', 'm', 'm', 'm', 'm', 'm', 'm', 'm', 'm', 'm', 'm', 'm',
+ 'm', 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'n',
+ 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'n', 'o', 'o',
+ 'p', 'p', 'p', 'p', 'p', 'p', 'p', 'p', 'p', 'p', 'p', 'p', 'p',
+ 'p', 'p', 'p', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q',
+ 'q', 'q', 'q', 'q', 'r', 'r', 'r', 'r', 'r', 'r', 'r', 'r', 'r',
+ 'r', 'r', 'r', 'r', 'r', 's', 's', 's', 's', 's', 's', 's', 's',
+ 's', 's', 's', 's', 's', 's', 's', 's', 's', 's', 's', 's', 's',
+ 's', 's', 's', 's', 's', 's', 's', 's', 's', 's', 's', 's', 's',
+ 't', 't', 't', 't', 't', 't', 't', 't', 't', 't', 't', 't', 't',
+ 't', 't', 't', 't', 't', 't', 'w', 'w', 'w', 'w', 'w', 'w', 'w',
+ 'w', 'w', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x',
+ 'x', 'x', 'x', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y',
+ 'y', 'y', 'y', 'y', 'y', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z',
+ 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z',
+ 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z', 'z',
+ 'z', 'z',};
+
+ private static final String[] leadingSUp = new String[]{"A", "A", "A",
+ "A", "A", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B",
+ "B", "B", "B", "B", "B", "C", "C", "C", "C", "C", "C", "C", "C",
+ "C", "C", "C", "C", "C", "C", "C", "C", "C", "C", "C", "C", "C",
+ "C", "C", "C", "C", "C", "C", "C", "C", "C", "C", "C", "C", "D",
+ "D", "D", "D", "D", "D", "D", "D", "D", "D", "D", "D", "D", "D",
+ "D", "D", "D", "D", "D", "D", "E", "E", "E", "F", "F", "F", "F",
+ "F", "F", "F", "F", "F", "G", "G", "G", "G", "G", "G", "G", "G",
+ "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "G", "H", "H",
+ "H", "H", "H", "H", "H", "H", "H", "H", "H", "H", "H", "H", "H",
+ "H", "H", "H", "H", "J", "J", "J", "J", "J", "J", "J", "J", "J",
+ "J", "J", "J", "J", "J", "K", "K", "K", "K", "K", "K", "K", "K",
+ "K", "K", "K", "K", "K", "K", "K", "K", "K", "K", "L", "L", "L",
+ "L", "L", "L", "L", "L", "L", "L", "L", "L", "L", "L", "L", "L",
+ "L", "L", "L", "L", "L", "L", "L", "L", "L", "M", "M", "M", "M",
+ "M", "M", "M", "M", "M", "M", "M", "M", "M", "M", "M", "M", "M",
+ "M", "M", "N", "N", "N", "N", "N", "N", "N", "N", "N", "N", "N",
+ "N", "N", "N", "N", "N", "N", "N", "N", "N", "N", "N", "N", "O",
+ "O", "P", "P", "P", "P", "P", "P", "P", "P", "P", "P", "P", "P",
+ "P", "P", "P", "P", "Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q",
+ "Q", "Q", "Q", "Q", "Q", "R", "R", "R", "R", "R", "R", "R", "R",
+ "R", "R", "R", "R", "R", "R", "S", "S", "S", "S", "S", "S", "S",
+ "S", "S", "S", "S", "S", "S", "S", "S", "S", "S", "S", "S", "S",
+ "S", "S", "S", "S", "S", "S", "S", "S", "S", "S", "S", "S", "S",
+ "S", "T", "T", "T", "T", "T", "T", "T", "T", "T", "T", "T", "T",
+ "T", "T", "T", "T", "T", "T", "T", "W", "W", "W", "W", "W", "W",
+ "W", "W", "W", "X", "X", "X", "X", "X", "X", "X", "X", "X", "X",
+ "X", "X", "X", "X", "Y", "Y", "Y", "Y", "Y", "Y", "Y", "Y", "Y",
+ "Y", "Y", "Y", "Y", "Y", "Y", "Z", "Z", "Z", "Z", "Z", "Z", "Z",
+ "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z",
+ "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z", "Z",
+ "Z", "Z", "Z"};
+
+ private static final String[] leadingSLo = new String[]{"a", "a", "a",
+ "a", "a", "b", "b", "b", "b", "b", "b", "b", "b", "b", "b", "b",
+ "b", "b", "b", "b", "b", "c", "c", "c", "c", "c", "c", "c", "c",
+ "c", "c", "c", "c", "c", "c", "c", "c", "c", "c", "c", "c", "c",
+ "c", "c", "c", "c", "c", "c", "c", "c", "c", "c", "c", "c", "d",
+ "d", "d", "d", "d", "d", "d", "d", "d", "d", "d", "d", "d", "d",
+ "d", "d", "d", "d", "d", "d", "e", "e", "e", "f", "f", "f", "f",
+ "f", "f", "f", "f", "f", "g", "g", "g", "g", "g", "g", "g", "g",
+ "g", "g", "g", "g", "g", "g", "g", "g", "g", "g", "g", "h", "h",
+ "h", "h", "h", "h", "h", "h", "h", "h", "h", "h", "h", "h", "h",
+ "h", "h", "h", "h", "j", "j", "j", "j", "j", "j", "j", "j", "j",
+ "j", "j", "j", "j", "j", "k", "k", "k", "k", "k", "k", "k", "k",
+ "k", "k", "k", "k", "k", "k", "k", "k", "k", "k", "l", "l", "l",
+ "l", "l", "l", "l", "l", "l", "l", "l", "l", "l", "l", "l", "l",
+ "l", "l", "l", "l", "l", "l", "l", "l", "l", "m", "m", "m", "m",
+ "m", "m", "m", "m", "m", "m", "m", "m", "m", "m", "m", "m", "m",
+ "m", "m", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n",
+ "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "n", "o",
+ "o", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p",
+ "p", "p", "p", "p", "q", "q", "q", "q", "q", "q", "q", "q", "q",
+ "q", "q", "q", "q", "q", "r", "r", "r", "r", "r", "r", "r", "r",
+ "r", "r", "r", "r", "r", "r", "s", "s", "s", "s", "s", "s", "s",
+ "s", "s", "s", "s", "s", "s", "s", "s", "s", "s", "s", "s", "s",
+ "s", "s", "s", "s", "s", "s", "s", "s", "s", "s", "s", "s", "s",
+ "s", "t", "t", "t", "t", "t", "t", "t", "t", "t", "t", "t", "t",
+ "t", "t", "t", "t", "t", "t", "t", "w", "w", "w", "w", "w", "w",
+ "w", "w", "w", "x", "x", "x", "x", "x", "x", "x", "x", "x", "x",
+ "x", "x", "x", "x", "y", "y", "y", "y", "y", "y", "y", "y", "y",
+ "y", "y", "y", "y", "y", "y", "z", "z", "z", "z", "z", "z", "z",
+ "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z",
+ "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z",
+ "z", "z", "z",};
+
+ private static Context context;
+
+ private static byte[] indexes;
+
+ private static final Object lock = new Object();
+
+ private static byte[] loadIndexes(Context context) {
+ byte[] indexes = null;
+
+ InputStream is = null;
+ try {
+ is = context.getAssets().open(ASSET);
+
+ indexes = new byte[(END - START + 1) * 2];
+
+ is.read(indexes);
+ } catch (Throwable tr) {
+ tr.printStackTrace();
+
+ indexes = null;
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ return indexes;
+ }
+
+ private static void ensureIndexes() {
+ if (indexes != null) {
+ return;
+ }
+
+ //
+ // usually not reach
+ //
+
+ byte[] tIndexes = null;
+
+ // protect
+ synchronized (lock) {
+ if (context != null) {
+ tIndexes = loadIndexes(context);
+ if (tIndexes != null) {
+ context = null;
+ }
+ }
+ }
+
+ // may race
+ if (tIndexes != null) {
+ indexes = tIndexes;
+ }
+ }
+
+ public static final void init(Context ctx) {
+ context = ctx.getApplicationContext();
+ }
+
+ public static final String validate() {
+ ensureIndexes();
+
+ StringBuilder invalid = new StringBuilder();
+ StringBuilder miss = new StringBuilder();
+
+ for (char c = START; c <= END; c++) {
+ // offset
+ int offset = (c - START) * 2;
+
+ // raw
+ int raw = indexes[offset] << 8 | 0xff & indexes[offset + 1];
+
+ if (raw < 0 || raw > pinyin.length) {
+ invalid.append(c);
+ }
+
+ if (raw == 0) {
+ miss.append(c);
+ }
+ }
+
+ boolean t9 = validatePinYinT9();
+
+ String result = "pinyin(" + pinyin.length + ") " +
+ "pinyinT9(" + t9 + ") " +
+ "leadingCUp(" + leadingCUp.length + ") " +
+ "leadingCLo(" + leadingCLo.length + ") " +
+ "leadingSUp(" + leadingSUp.length + ") " +
+ "leadingSLo(" + leadingSLo.length + ") " +
+ "invalid(" + invalid.toString() + ") " +
+ "miss(" + miss.toString() + ") ";
+
+ return result;
+ }
+
+ private static final char[] T9 = {'2', '2', '2', '3', '3', '3', '4', '4',
+ '4', '5', '5', '5', '6', '6', '6', '7', '7', '7', '7', '8', '8',
+ '8', '9', '9', '9', '9'};
+
+ private static final boolean validatePinYinT9() {
+ if (pinyin.length != pinyinT9.length) {
+ return false;
+ }
+
+ for (int i = 0; i < pinyin.length; i++) {
+ String p = pinyin[i];
+ String t = pinyinT9[i];
+
+ if (p.length() != t.length()) {
+ return false;
+ }
+
+ for (int j = 0; j < p.length(); j++) {
+ if (T9[p.charAt(j) - 'a'] != t.charAt(j)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public static final int getIndex(char c) {
+ ensureIndexes();
+
+ if (indexes == null) {
+ return -1;
+ }
+
+ // out of bound
+ if (c < START || c > END) {
+ return -1;
+ }
+
+ // offset
+ int offset = (c - START) * 2;
+
+ // raw
+ int raw = indexes[offset] << 8 | 0xff & indexes[offset + 1];
+
+ return raw - 1;
+ }
+
+ public static final char getLeadingUp(char c, char d) {
+ int index = getIndex(c);
+
+ return index != -1 ? leadingCUp[index] : d;
+ }
+
+ public static final char getLeadingLo(char c, char d) {
+ int index = getIndex(c);
+
+ return index != -1 ? leadingCLo[index] : d;
+ }
+
+ public static final String getLeadingUp(char c) {
+ int index = getIndex(c);
+
+ return index != -1 ? leadingSUp[index] : null;
+ }
+
+ public static final String getLeadingLo(char c) {
+ int index = getIndex(c);
+
+ return index != -1 ? leadingSLo[index] : null;
+ }
+
+ public static final String getLeadingUp(String text) {
+ if (TextUtils.isEmpty(text)) {
+ return null;
+ }
+
+ StringBuilder leadings = new StringBuilder();
+
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+
+ int index = getIndex(c);
+
+ if (index != -1) {
+ leadings.append(leadingCUp[index]);
+ } else {
+ leadings.append(c);
+ }
+ }
+
+ return leadings.toString();
+ }
+
+ public static final String getLeadingLo(String text) {
+ if (TextUtils.isEmpty(text)) {
+ return null;
+ }
+
+ StringBuilder leadings = new StringBuilder();
+
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+
+ int index = getIndex(c);
+
+ if (index != -1) {
+ leadings.append(leadingCLo[index]);
+ } else {
+ leadings.append(c);
+ }
+ }
+
+ return leadings.toString();
+ }
+
+ public static final String getPinYin(char c) {
+ int index = getIndex(c);
+
+ return index != -1 ? pinyin[index] : null;
+ }
+
+ public static final String getPinYinT9(char c) {
+ int index = getIndex(c);
+
+ return index != -1 ? pinyinT9[index] : null;
+ }
+
+ public static final String getPinYin(String text) {
+ if (TextUtils.isEmpty(text)) {
+ return null;
+ }
+
+ StringBuilder pinyins = new StringBuilder();
+
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+
+ int index = getIndex(c);
+
+ if (index != -1) {
+ pinyins.append(pinyin[index]);
+ } else {
+ pinyins.append(c);
+ }
+ }
+
+ return pinyins.toString();
+ }
+
+ public static final String[] getPinYins(String text) {
+ if (TextUtils.isEmpty(text)) {
+ return null;
+ }
+
+ StringBuilder pinyins = new StringBuilder();
+ StringBuilder leadings = new StringBuilder();
+
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+
+ int index = getIndex(c);
+
+ if (index != -1) {
+ pinyins.append(pinyin[index]);
+ leadings.append(leadingCLo[index]);
+ } else {
+ pinyins.append(c);
+ leadings.append(c);
+ }
+ }
+
+ String[] result = new String[]{pinyins.toString(), leadings.toString()};
+
+ return result;
+ }
+
+ public static final List getPinYins(List texts) {
+ if (texts == null) {
+ return null;
+ }
+
+ List results = new ArrayList(texts.size());
+
+ for (String text : texts) {
+ results.add(getPinYins(text));
+ }
+
+ return results;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/SimpleT9Matcher.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/SimpleT9Matcher.java
new file mode 100644
index 0000000..5534060
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/SimpleT9Matcher.java
@@ -0,0 +1,52 @@
+package com.netease.nim.uikit.business.contact.core.query;
+
+public class SimpleT9Matcher {
+
+ private static final char[] LATIN_LETTERS_TO_DIGITS = {
+ '2', '2', '2', // A,B,C -> 2
+ '3', '3', '3', // D,E,F -> 3
+ '4', '4', '4', // G,H,I -> 4
+ '5', '5', '5', // J,K,L -> 5
+ '6', '6', '6', // M,N,O -> 6
+ '7', '7', '7', '7', // P,Q,R,S -> 7
+ '8', '8', '8', // T,U,V -> 8
+ '9', '9', '9', '9' // W,X,Y,Z -> 9
+ };
+
+ private static boolean isAlphaNumberString(String name) {
+ return name.matches("[0-9a-zA-Z]+");
+ }
+
+ private static char getDialpadNumericCharacter(char ch) {
+ if (ch >= 'A' && ch <= 'Z') {
+ ch = (char) (ch + ('a' - 'A'));
+ }
+ if (ch >= 'a' && ch <= 'z') {
+ return LATIN_LETTERS_TO_DIGITS[ch - 'a'];
+ }
+ return ch;
+ }
+
+ /**
+ * 将仅包含数字和字母的字符串转化为数字串(对应T9)
+ *
+ * @param name
+ * @return
+ */
+ private static String alphaNumberStringToNumbericString(String name) {
+ if (isAlphaNumberString(name)) {
+ final int length = name.length();
+ StringBuilder builder = new StringBuilder();
+ for (int index = 0; index < length; index++) {
+ builder.append(getDialpadNumericCharacter(name.charAt(index)));
+ }
+ return builder.toString();
+ }
+ return "";
+ }
+
+ public static boolean hit(String text, String query) {
+ return alphaNumberStringToNumbericString(text).contains(query);
+ }
+
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/TextComparator.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/TextComparator.java
new file mode 100644
index 0000000..ee29c75
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/TextComparator.java
@@ -0,0 +1,221 @@
+package com.netease.nim.uikit.business.contact.core.query;
+
+import android.text.TextUtils;
+
+public class TextComparator {
+ public static final int compare(String a, String b) {
+ if (a == b) {
+ return 0;
+ }
+
+ if (a == null) {
+ return -1;
+ }
+
+ if (b == null) {
+ return 1;
+ }
+
+ for (int index = 0; index < a.length() && index < b.length(); index++) {
+ int compare = compare(a.charAt(index), b.charAt(index));
+ if (compare != 0) {
+ return compare;
+ }
+ }
+
+ return a.length() - b.length();
+ }
+
+ public static final int compareIgnoreCase(String a, String b) {
+ if (a == b) {
+ return 0;
+ }
+
+ if (a == null) {
+ return -1;
+ }
+
+ if (b == null) {
+ return 1;
+ }
+
+ for (int index = 0; index < a.length() && index < b.length(); index++) {
+ int compare = compareIgnoreCase(a.charAt(index), b.charAt(index));
+ if (compare != 0) {
+ return compare;
+ }
+ }
+
+ return a.length() - b.length();
+ }
+
+ public static final int compare(char a, char b) {
+ if (a == b) {
+ return 0;
+ }
+
+ {
+ int ai = getAsciiIndex(a, false);
+ int bi = getAsciiIndex(b, false);
+
+ if (ai != bi) {
+ if (ai == -1) {
+ return 1;
+ } else if (bi == -1) {
+ return -1;
+ } else {
+ return ai - bi;
+ }
+ } else {
+ if (ai != -1) {
+ return 0;
+ }
+ }
+ }
+
+ {
+ int ai = PinYin.getIndex(a);
+ int bi = PinYin.getIndex(b);
+
+ if (ai != bi) {
+ if (ai == -1) {
+ return 1;
+ } else if (bi == -1) {
+ return -1;
+ } else {
+ return ai - bi;
+ }
+ }
+ }
+
+ return a - b;
+ }
+
+ public static final int compareIgnoreCase(char a, char b) {
+ if (a == b) {
+ return 0;
+ }
+
+ {
+ int ai = getAsciiIndex(a, true);
+ int bi = getAsciiIndex(b, true);
+
+ if (ai != bi) {
+ if (ai == -1) {
+ return 1;
+ } else if (bi == -1) {
+ return -1;
+ } else {
+ return ai - bi;
+ }
+ } else {
+ if (ai != -1) {
+ return 0;
+ }
+ }
+ }
+
+ {
+ int ai = PinYin.getIndex(a);
+ int bi = PinYin.getIndex(b);
+
+ if (ai != bi) {
+ if (ai == -1) {
+ return 1;
+ } else if (bi == -1) {
+ return -1;
+ } else {
+ return ai - bi;
+ }
+ }
+ }
+
+ return a - b;
+ }
+
+ public static final String getLeadingUp(String s) {
+ if (TextUtils.isEmpty(s)) {
+ return null;
+ }
+
+ char c = s.charAt(0);
+
+ String leading = getAsciiLeadingUp(c);
+ if (leading == null) {
+ leading = PinYin.getLeadingUp(c);
+ }
+
+ return leading;
+ }
+
+ public static final String getLeadingLo(String s) {
+ if (TextUtils.isEmpty(s)) {
+ return null;
+ }
+
+ char c = s.charAt(0);
+
+ String leading = getAsciiLeadingLo(c);
+ if (leading == null) {
+ leading = PinYin.getLeadingLo(c);
+ }
+
+ return leading;
+ }
+
+ private static int getAsciiIndex(char c, boolean ignoreCase) {
+ // 0-9 0x0
+ if (c >= 0x30 && c <= 0x39) {
+ return c - 0x30 + 0;
+ }
+
+ // a-z 0xA
+ if (c >= 0x61 && c <= 0x7A) {
+ return c - 0x61 + 0xA;
+ }
+
+ // A-Z 0x24
+ if (c >= 0x41 && c <= 0x5A) {
+ return c - 0x41 + (ignoreCase ? 0xA : 0x24);
+ }
+
+ return -1;
+ }
+
+ private static final String[] leadingUp = new String[]{"A", "B", "C", "D", "E",
+ "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
+ "S", "T", "U", "V", "W", "X", "Y", "Z"
+ };
+
+ private static final String[] leadingLo = new String[]{"a", "b", "c",
+ "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p",
+ "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"};
+
+ private static String getAsciiLeadingUp(char c) {
+ // a-z 0xA
+ if (c >= 0x61 && c <= 0x7A) {
+ return leadingUp[c - 0x61];
+ }
+
+ // A-Z 0x24
+ if (c >= 0x41 && c <= 0x5A) {
+ return leadingUp[c - 0x41];
+ }
+
+ return null;
+ }
+
+ private static String getAsciiLeadingLo(char c) {
+ // a-z 0xA
+ if (c >= 0x61 && c <= 0x7A) {
+ return leadingLo[c - 0x61];
+ }
+
+ // A-Z 0x24
+ if (c >= 0x41 && c <= 0x5A) {
+ return leadingLo[c - 0x41];
+ }
+
+ return null;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/TextQuery.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/TextQuery.java
new file mode 100644
index 0000000..27e0a7b
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/TextQuery.java
@@ -0,0 +1,56 @@
+package com.netease.nim.uikit.business.contact.core.query;
+
+import android.annotation.SuppressLint;
+import android.text.TextUtils;
+
+@SuppressLint("DefaultLocale")
+public final class TextQuery {
+ public final String text;
+
+ public final boolean t9;
+
+ public boolean digit;
+
+ public boolean letter;
+
+ public boolean pinyin;
+
+ public Object[] extra;
+
+ public TextQuery(String text) {
+ this(text, false);
+ }
+
+ public TextQuery(String text, boolean t9) {
+ this.text = !TextUtils.isEmpty(text) ? text.toLowerCase() : text;
+ this.t9 = t9;
+
+ init();
+ }
+
+ private void init() {
+ if (TextUtils.isEmpty(text)) {
+ return;
+ }
+
+ int digits = 0;
+ int letters = 0;
+ int pinyins = 0;
+
+ for (int i = 0; i < text.length(); i++) {
+ char chr = text.charAt(i);
+
+ if ('0' <= chr && chr <= '9') {
+ digits++;
+ } else if ('a' <= chr && chr <= 'z') {
+ letters++;
+ } else if (PinYin.getIndex(chr) != -1) {
+ pinyins++;
+ }
+ }
+
+ digit = digits == text.length();
+ letter = letters == text.length();
+ pinyin = pinyins == text.length();
+ }
+}
\ No newline at end of file
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/TextSearcher.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/TextSearcher.java
new file mode 100644
index 0000000..ede4549
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/query/TextSearcher.java
@@ -0,0 +1,300 @@
+package com.netease.nim.uikit.business.contact.core.query;
+
+import android.text.TextUtils;
+
+public final class TextSearcher {
+ /**
+ * T9
+ */
+ private boolean mT9;
+
+ /**
+ * string
+ */
+ private String mStr;
+
+ //
+ // string state
+ //
+
+ /**
+ * string index
+ */
+ private int mIndex;
+
+ /**
+ * eaten state
+ */
+ private boolean mEaten;
+
+ //
+ // PinYin state
+ //
+
+ /**
+ * PinYin
+ */
+ private String mPinyin;
+
+ /**
+ * string index after PinYin
+ */
+ private int mIndexP;
+
+ /**
+ * PinYin sub index
+ */
+ private int mIndexSub;
+
+ /**
+ * T9 characters
+ */
+ private static final char[] T9 = {'2', '2', '2', '3', '3', '3', '4', '4',
+ '4', '5', '5', '5', '6', '6', '6', '7', '7', '7', '7', '8', '8',
+ '8', '9', '9', '9', '9'};
+
+ /**
+ * searcher
+ */
+ private static final ThreadLocal sSearcher = new ThreadLocal() {
+ protected TextSearcher initialValue() {
+ return new TextSearcher();
+ }
+ };
+
+ /**
+ * @param t9
+ * @return TextSearcher
+ */
+ public static final TextSearcher obtain(boolean t9) {
+ TextSearcher searcher = sSearcher.get();
+
+ searcher.mT9 = t9;
+
+ return searcher;
+ }
+
+ /**
+ * @param s
+ * @param i
+ */
+ public final void initialize(String s, int i) {
+ mStr = s;
+
+ mIndex = i;
+ mEaten = true;
+
+ mPinyin = null;
+ mIndexP = -1;
+ mIndexSub = -1;
+ }
+
+ /**
+ * @return last index
+ */
+ public final int index() {
+ return mIndex;
+ }
+
+ /**
+ * @param eat assuming in lower case or [0-9]
+ * @return eaten
+ */
+ public final boolean eat(char eat) {
+ //
+ // PinYin
+ //
+
+ boolean pEaten = false;
+ boolean pEnd = false;
+
+ // on
+ if (mPinyin != null) {
+ // compare then move
+ pEaten = mPinyin.charAt(mIndexSub++) == eat;
+ pEnd = mIndexSub == mPinyin.length();
+
+ // not eaten or reach the end
+ if (!pEaten || pEnd) {
+ // close
+ mPinyin = null;
+ }
+ }
+
+ //
+ // string
+ //
+
+ String pinyin = null;
+ boolean eaten = false;
+
+ // on && in bound
+ if (mEaten && mIndex < mStr.length()) {
+ char chr = mStr.charAt(mIndex);
+
+ if (mT9) {
+ if ('a' <= chr && chr <= 'z') {
+ eaten = T9[chr - 'a'] == eat;
+ } else if ('A' <= chr && chr <= 'Z') {
+ eaten = T9[chr - 'A'] == eat;
+ } else {
+ eaten = chr == eat;
+ }
+ } else {
+ if ('A' <= chr && chr <= 'Z') {
+ eaten = chr + 'a' - 'A' == eat;
+ } else {
+ eaten = chr == eat;
+ }
+ }
+
+ // PinYin
+ if (!eaten) {
+ pinyin = mT9 ? PinYin.getPinYinT9(chr) : PinYin.getPinYin(chr);
+
+ // has
+ if (pinyin != null) {
+ // equals
+ if (pinyin.charAt(0) == eat) {
+ eaten = true;
+ } else {
+ // clear
+ pinyin = null;
+ }
+ }
+ }
+ }
+
+ // fail
+ if (!pEaten && !eaten) {
+ return false;
+ }
+
+ // string
+ if (eaten) {
+ // next
+ mIndex++;
+
+ // PinYin fail
+ if (!pEaten) {
+ // clear
+ mPinyin = null;
+
+ // setup
+ if (pinyin != null && pinyin.length() > 1) {
+ mPinyin = pinyin;
+ // first has done
+ mIndexSub = 1;
+
+ // at next
+ mIndexP = mIndex;
+ }
+ }
+ } else {
+ //
+ // PinYin here
+ //
+
+ // reach the end
+ if (pEnd) {
+ // previous index done
+ eaten = true;
+ mIndex = mIndexP;
+ }
+ }
+
+ // save last
+ mEaten = eaten;
+
+ return true;
+ }
+
+ /**
+ * @param t9
+ * @param str
+ * @param query assuming in lower case or [0-9]
+ * @return range array or NULL
+ */
+ public static final int[] indexOf(boolean t9, String str, String query) {
+ if (TextUtils.isEmpty(str) || TextUtils.isEmpty(query)) {
+ return null;
+ }
+
+ TextSearcher searcher = TextSearcher.obtain(t9);
+
+ // move
+ EAT:
+ for (int index = 0; index < str.length(); index++) {
+ searcher.initialize(str, index);
+
+ for (int subIndex = 0; subIndex < query.length(); subIndex++) {
+ if (!searcher.eat(query.charAt(subIndex))) {
+ // next
+ continue EAT;
+ }
+ }
+
+ // eaten
+ return new int[]{index, searcher.index()};
+ }
+
+ return null;
+ }
+
+ /**
+ * @param t9
+ * @param str
+ * @param query assuming in lower case or [0-9]
+ * @return contains
+ */
+ public static final boolean contains(boolean t9, String str, String query) {
+ if (TextUtils.isEmpty(str) || TextUtils.isEmpty(query)) {
+ return false;
+ }
+
+ TextSearcher searcher = TextSearcher.obtain(t9);
+
+ // move
+ EAT:
+ for (int index = 0; index < str.length(); index++) {
+ searcher.initialize(str, index);
+
+ for (int subIndex = 0; subIndex < query.length(); subIndex++) {
+ if (!searcher.eat(query.charAt(subIndex))) {
+ // next
+ continue EAT;
+ }
+ }
+
+ // eaten
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param t9
+ * @param str
+ * @param query assuming in lower case or [0-9]
+ * @return last index or -1
+ */
+ public static final int startsWith(boolean t9, String str, String query) {
+ if (TextUtils.isEmpty(str) || TextUtils.isEmpty(query)) {
+ return -1;
+ }
+
+ TextSearcher searcher = TextSearcher.obtain(t9);
+
+ searcher.initialize(str, 0);
+
+ for (int subIndex = 0; subIndex < query.length(); subIndex++) {
+ if (!searcher.eat(query.charAt(subIndex))) {
+ // fail
+ return -1;
+ }
+ }
+
+ return searcher.index();
+ }
+}
\ No newline at end of file
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/util/ContactHelper.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/util/ContactHelper.java
new file mode 100644
index 0000000..e589761
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/util/ContactHelper.java
@@ -0,0 +1,60 @@
+package com.netease.nim.uikit.business.contact.core.util;
+
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.business.team.helper.TeamHelper;
+import com.netease.nim.uikit.business.uinfo.UserInfoHelper;
+import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
+import com.netease.nimlib.sdk.search.model.MsgIndexRecord;
+import com.netease.nimlib.sdk.uinfo.model.UserInfo;
+
+/**
+ * Created by huangjun on 2015/9/8.
+ */
+public class ContactHelper {
+ public static IContact makeContactFromUserInfo(final UserInfo userInfo) {
+ return new IContact() {
+ @Override
+ public String getContactId() {
+ return userInfo.getAccount();
+ }
+
+ @Override
+ public int getContactType() {
+ return Type.Friend;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return UserInfoHelper.getUserDisplayName(userInfo.getAccount());
+ }
+ };
+ }
+
+ public static IContact makeContactFromMsgIndexRecord(final MsgIndexRecord record) {
+ return new IContact() {
+ @Override
+ public String getContactId() {
+ return record.getSessionId();
+ }
+
+ @Override
+ public int getContactType() {
+ return Type.Msg;
+ }
+
+ @Override
+ public String getDisplayName() {
+ String sessionId = record.getSessionId();
+ SessionTypeEnum sessionType = record.getSessionType();
+
+ if (sessionType == SessionTypeEnum.P2P) {
+ return UserInfoHelper.getUserDisplayName(sessionId);
+ } else if (sessionType == SessionTypeEnum.Team) {
+ return TeamHelper.getTeamName(sessionId);
+ }
+
+ return "";
+ }
+ };
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/AbsContactViewHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/AbsContactViewHolder.java
new file mode 100644
index 0000000..833ed4b
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/AbsContactViewHolder.java
@@ -0,0 +1,31 @@
+package com.netease.nim.uikit.business.contact.core.viewholder;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.model.ContactDataAdapter;
+
+public abstract class AbsContactViewHolder {
+ protected View view;
+
+ protected Context context;
+
+ public AbsContactViewHolder() {
+
+ }
+
+ public abstract void refresh(ContactDataAdapter adapter, int position, T item);
+
+ public abstract View inflate(LayoutInflater inflater);
+
+ public final View getView() {
+ return view;
+ }
+
+ public void create(Context context) {
+ this.context = context;
+ this.view = inflate(LayoutInflater.from(context));
+ }
+}
\ No newline at end of file
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/ContactHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/ContactHolder.java
new file mode 100644
index 0000000..eb4713c
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/ContactHolder.java
@@ -0,0 +1,73 @@
+package com.netease.nim.uikit.business.contact.core.viewholder;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.business.contact.core.item.ContactItem;
+import com.netease.nim.uikit.business.contact.core.model.ContactDataAdapter;
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.common.ui.imageview.HeadImageView;
+import com.netease.nim.uikit.impl.NimUIKitImpl;
+import com.netease.nimlib.sdk.team.model.Team;
+
+public class ContactHolder extends AbsContactViewHolder {
+
+ protected HeadImageView head;
+
+ protected TextView name;
+
+ protected TextView desc;
+
+ protected RelativeLayout headLayout;
+
+ @Override
+ public void refresh(ContactDataAdapter adapter, int position, final ContactItem item) {
+ // contact info
+ final IContact contact = item.getContact();
+ if (contact.getContactType() == IContact.Type.Friend) {
+ head.loadBuddyAvatar(contact.getContactId());
+ } else {
+ Team team = NimUIKit.getTeamProvider().getTeamById(contact.getContactId());
+ head.loadTeamIconByTeam(team);
+ }
+ name.setText(contact.getDisplayName());
+ headLayout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (contact.getContactType() == IContact.Type.Friend) {
+ if (NimUIKitImpl.getContactEventListener() != null) {
+ NimUIKitImpl.getContactEventListener().onAvatarClick(context, item.getContact().getContactId());
+ }
+ }
+ }
+ });
+
+ // query result
+ desc.setVisibility(View.GONE);
+ /*
+ TextQuery query = adapter.getQuery();
+ HitInfo hitInfo = query != null ? ContactSearch.hitInfo(contact, query) : null;
+ if (hitInfo != null && !hitInfo.text.equals(contact.getDisplayName())) {
+ desc.setVisibility(View.VISIBLE);
+ } else {
+ desc.setVisibility(View.GONE);
+ }
+ */
+ }
+
+ @Override
+ public View inflate(LayoutInflater inflater) {
+ View view = inflater.inflate(R.layout.nim_contacts_item, null);
+
+ headLayout = (RelativeLayout) view.findViewById(R.id.head_layout);
+ head = (HeadImageView) view.findViewById(R.id.contacts_item_head);
+ name = (TextView) view.findViewById(R.id.contacts_item_name);
+ desc = (TextView) view.findViewById(R.id.contacts_item_desc);
+
+ return view;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/LabelHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/LabelHolder.java
new file mode 100644
index 0000000..aa1025b
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/LabelHolder.java
@@ -0,0 +1,27 @@
+package com.netease.nim.uikit.business.contact.core.viewholder;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.contact.core.item.LabelItem;
+import com.netease.nim.uikit.business.contact.core.model.ContactDataAdapter;
+
+public class LabelHolder extends AbsContactViewHolder {
+
+ private TextView name;
+
+ @Override
+ public void refresh(ContactDataAdapter contactAdapter, int position, LabelItem item) {
+ this.name.setText(item.getText());
+ }
+
+ @Override
+ public View inflate(LayoutInflater inflater) {
+ View view = inflater.inflate(R.layout.nim_contacts_abc_item, null);
+ this.name = (TextView) view.findViewById(R.id.tv_nickname);
+ return view;
+ }
+
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/MsgHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/MsgHolder.java
new file mode 100644
index 0000000..ff1f925
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/MsgHolder.java
@@ -0,0 +1,181 @@
+package com.netease.nim.uikit.business.contact.core.viewholder;
+
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.business.contact.core.item.MsgItem;
+import com.netease.nim.uikit.business.contact.core.model.ContactDataAdapter;
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.common.ui.imageview.HeadImageView;
+import com.netease.nim.uikit.common.util.sys.ScreenUtil;
+import com.netease.nim.uikit.common.util.sys.TimeUtil;
+import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
+import com.netease.nimlib.sdk.search.model.MsgIndexRecord;
+import com.netease.nimlib.sdk.search.model.RecordHitInfo;
+import com.netease.nimlib.sdk.team.model.Team;
+import com.chwl.library.utils.ResUtil;
+
+import java.util.List;
+
+public class MsgHolder extends AbsContactViewHolder {
+
+ private static final String PREFIX = "...";
+
+ protected HeadImageView head;
+
+ protected TextView name;
+
+ protected TextView time;
+
+ protected TextView desc;
+
+ protected int descTextViewWidth; // 当前ViewHolder desc TextView 最大的宽度px
+
+ @Override
+ public View inflate(LayoutInflater inflater) {
+ View view = inflater.inflate(R.layout.nim_contacts_item, null);
+
+ head = (HeadImageView) view.findViewById(R.id.contacts_item_head);
+ name = (TextView) view.findViewById(R.id.contacts_item_name);
+ time = (TextView) view.findViewById(R.id.contacts_item_time);
+ desc = (TextView) view.findViewById(R.id.contacts_item_desc);
+
+ // calculate
+ View parent = (View) desc.getParent();
+ if (parent.getMeasuredWidth() == 0) {
+ // xml中:50dp,包括头像的长度还有左右间距大小
+ parent.measure(View.MeasureSpec.makeMeasureSpec(ScreenUtil.getDisplayWidth() - ScreenUtil.dip2px(50.0f), View.MeasureSpec.EXACTLY), 0);
+ }
+ descTextViewWidth = (int) (parent.getMeasuredWidth() - desc.getPaint().measureText(PREFIX));
+
+ return view;
+ }
+
+ @Override
+ public void refresh(ContactDataAdapter adapter, int position, final MsgItem item) {
+ // contact info
+ final IContact contact = item.getContact();
+ final MsgIndexRecord record = item.getRecord();
+
+ if (record.getSessionType() == SessionTypeEnum.P2P) {
+ head.loadBuddyAvatar(contact.getContactId());
+ } else {
+ Team team = NimUIKit.getTeamProvider().getTeamById(contact.getContactId());
+ head.loadTeamIconByTeam(team);
+ }
+ name.setText(contact.getDisplayName());
+
+ if (item.isQuerySession()) {
+ time.setVisibility(View.VISIBLE);
+ time.setText(TimeUtil.getTimeShowString(record.getTime(), false));
+ } else {
+ time.setVisibility(View.GONE);
+ }
+
+ // query result
+ if (record.getCount() > 1) {
+ desc.setText(String.format(ResUtil.getString(R.string.core_viewholder_msgholder_01), record.getCount()));
+ } else {
+ String text = record.getText(); // 原串
+ List clone = record.cloneHitInfo(); // 计算高亮区域并clone
+
+ // 异常情况,没有高亮击中的区间,直接设置原串
+ if (clone == null || clone.isEmpty()) {
+ desc.setText(text);
+ return;
+ }
+
+ int firstIndex = clone.get(0).start; // 首个需要高亮的关键字的起始位置
+ int firstHitInfoLength = clone.get(0).end - clone.get(0).start + 1; // 首个需要高亮的关键字的长度
+
+ // 判断是否需要截取
+ Object[] result = needCutText(record.getText(), firstIndex, firstHitInfoLength);
+ Boolean needCut = (Boolean) result[0];
+ int extractPreStrNum = (Integer) result[1];
+ if (needCut) {
+ // 文本截取
+ int newStartIndex = firstIndex - extractPreStrNum;
+ text = PREFIX + text.substring(newStartIndex);
+
+ // 矫正hitInfo
+ int delta = newStartIndex - PREFIX.length();
+ for (RecordHitInfo rh : clone) {
+ rh.start -= delta;
+ rh.end -= delta;
+ }
+ }
+
+ display(desc, text, clone);
+ }
+ }
+
+ /**
+ * 假如第一个索引之前的文字宽度>layout宽度,就需要把text截断
+ *
+ * @param text 原文本
+ * @param firstIndex 首个需要高亮的关键字的起始位置
+ * @param firstHitInfoLength 首个需要高亮的关键字的长度
+ * @return [0]是否需要截取字符串;[1]如果要截取,那么需要提取前面几个字符
+ */
+ private Object[] needCutText(String text, int firstIndex, int firstHitInfoLength) {
+ Boolean r0;
+ Integer r1;
+ float descLength = desc.getPaint().measureText(text.substring(0, firstIndex + firstHitInfoLength));
+ float avg = descLength / (firstIndex + firstHitInfoLength); // 前firstIndex+firstHitInfoLength个字符,平均每个字符的长度
+
+ if (descLength >= descTextViewWidth) {
+ r0 = true;
+ int extractPreStrNum = (int) (1.0f * descTextViewWidth / avg); // 当前viewHolder desc TextView能容纳多少个字符(前firstIndex区间的字符)
+ extractPreStrNum = extractPreStrNum - PREFIX.length() - firstHitInfoLength; // 可以提取前面几个字符
+ if (extractPreStrNum < 0) {
+ extractPreStrNum = 0; // 有可能为负数,例如firstHitInfo特别长
+ }
+
+ // 新的文本
+ int newStartIndex = firstIndex - extractPreStrNum;
+ text = PREFIX + text.substring(newStartIndex, firstIndex + firstHitInfoLength);
+
+ // extractPreStrNum 校准
+ if (extractPreStrNum > 0) {
+ descLength = desc.getPaint().measureText(text);
+ if (descLength > descTextViewWidth) {
+ int delta = (int) ((descLength - descTextViewWidth) / (descLength / text.length())) + 1;
+ extractPreStrNum -= delta; // 減少提取的字符数
+ }
+
+ if (extractPreStrNum < 0) {
+ extractPreStrNum = 0; // 修正
+ }
+ }
+
+ r1 = extractPreStrNum;
+ } else {
+ r0 = false;
+ r1 = 0;
+ }
+ return new Object[]{r0, r1};
+ }
+
+ public static final void display(TextView tv, String text, List hitInfos) {
+ if (hitInfos == null || hitInfos.isEmpty()) {
+ tv.setText(text);
+ return;
+ }
+
+ SpannableStringBuilder sb = new SpannableStringBuilder();
+ SpannableString ss = new SpannableString(text);
+ for (RecordHitInfo r : hitInfos) {
+ ss.setSpan(new ForegroundColorSpan(tv.getResources().getColor(R.color.contact_search_hit)), r.start, r.end + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+
+ sb.append(ss);
+ tv.setText(sb);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/OnlineStateContactHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/OnlineStateContactHolder.java
new file mode 100644
index 0000000..d5a03cc
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/OnlineStateContactHolder.java
@@ -0,0 +1,34 @@
+package com.netease.nim.uikit.business.contact.core.viewholder;
+
+import android.text.TextUtils;
+import android.view.View;
+
+import com.netease.nim.uikit.business.contact.core.item.ContactItem;
+import com.netease.nim.uikit.business.contact.core.model.ContactDataAdapter;
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.impl.NimUIKitImpl;
+
+/**
+ * Created by hzchenkang on 2017/4/6.
+ */
+
+public class OnlineStateContactHolder extends ContactHolder {
+
+ @Override
+ public void refresh(ContactDataAdapter adapter, int position, ContactItem item) {
+ super.refresh(adapter, position, item);
+ IContact contact = item.getContact();
+ // 在线状态
+ if (contact.getContactType() != IContact.Type.Friend || !NimUIKitImpl.enableOnlineState()) {
+ desc.setVisibility(View.GONE);
+ } else {
+ String onlineStateContent = NimUIKitImpl.getOnlineStateContentProvider().getSimpleDisplay(contact.getContactId());
+ if (TextUtils.isEmpty(onlineStateContent)) {
+ desc.setVisibility(View.GONE);
+ } else {
+ desc.setVisibility(View.VISIBLE);
+ desc.setText(onlineStateContent);
+ }
+ }
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/TextHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/TextHolder.java
new file mode 100644
index 0000000..8c10ff8
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/core/viewholder/TextHolder.java
@@ -0,0 +1,24 @@
+package com.netease.nim.uikit.business.contact.core.viewholder;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.contact.core.item.TextItem;
+import com.netease.nim.uikit.business.contact.core.model.ContactDataAdapter;
+
+public class TextHolder extends AbsContactViewHolder {
+ private TextView textView;
+
+ public void refresh(ContactDataAdapter contactAdapter, int position, TextItem item) {
+ textView.setText(item.getText());
+ }
+
+ @Override
+ public View inflate(LayoutInflater inflater) {
+ View view = inflater.inflate(R.layout.nim_contact_text_item, null);
+ textView = (TextView) view.findViewById(R.id.text);
+ return view;
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/activity/ContactSelectActivity.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/activity/ContactSelectActivity.java
new file mode 100644
index 0000000..1aed6df
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/activity/ContactSelectActivity.java
@@ -0,0 +1,626 @@
+package com.netease.nim.uikit.business.contact.selector.activity;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.GridView;
+import android.widget.HorizontalScrollView;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import androidx.appcompat.widget.SearchView;
+import androidx.core.view.MenuItemCompat;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.wrapper.NimToolBarOptions;
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ContactItemFilter;
+import com.netease.nim.uikit.business.contact.core.item.ItemTypes;
+import com.netease.nim.uikit.business.contact.core.model.ContactGroupStrategy;
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.business.contact.core.provider.ContactDataProvider;
+import com.netease.nim.uikit.business.contact.core.provider.TeamMemberDataProvider;
+import com.netease.nim.uikit.business.contact.core.query.IContactDataProvider;
+import com.netease.nim.uikit.business.contact.core.query.TextQuery;
+import com.netease.nim.uikit.business.contact.core.viewholder.LabelHolder;
+import com.netease.nim.uikit.business.contact.selector.adapter.ContactSelectAdapter;
+import com.netease.nim.uikit.business.contact.selector.adapter.ContactSelectAvatarAdapter;
+import com.netease.nim.uikit.business.contact.selector.viewholder.ContactsMultiSelectHolder;
+import com.netease.nim.uikit.business.contact.selector.viewholder.ContactsSelectHolder;
+import com.netease.nim.uikit.common.activity.ToolBarOptions;
+import com.netease.nim.uikit.common.activity.UI;
+import com.netease.nim.uikit.common.ui.liv.LetterIndexView;
+import com.netease.nim.uikit.common.ui.liv.LivIndex;
+import com.chwl.library.utils.ResUtil;
+import com.chwl.library.utils.SingleToastUtil;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 联系人选择器
+ *
+ * Created by huangjun on 2015/3/3.
+ */
+public class ContactSelectActivity extends UI implements View.OnClickListener, androidx.appcompat.widget.SearchView.OnQueryTextListener {
+
+ public static final String EXTRA_DATA = "EXTRA_DATA"; // 请求数据:Option
+ public static final String RESULT_DATA = "RESULT_DATA"; // 返回结果
+
+ // adapter
+
+ private ContactSelectAdapter contactAdapter;
+
+ private ContactSelectAvatarAdapter contactSelectedAdapter;
+
+ // view
+
+ private ListView listView;
+
+ private LivIndex livIndex;
+
+ private RelativeLayout bottomPanel;
+
+ private HorizontalScrollView scrollViewSelected;
+
+ private GridView imageSelectedGridView;
+
+ private Button btnSelect;
+
+ private SearchView searchView;
+
+ // other
+
+ private String queryText;
+
+ private Option option;
+
+ // class
+
+ private static class ContactsSelectGroupStrategy extends ContactGroupStrategy {
+ public ContactsSelectGroupStrategy() {
+ add(ContactGroupStrategy.GROUP_NULL, -1, "");
+ addABC(0);
+ }
+ }
+
+ /**
+ * 联系人选择器配置可选项
+ */
+ public enum ContactSelectType {
+ BUDDY,
+ TEAM_MEMBER,
+ TEAM
+ }
+
+ public static class Option implements Serializable {
+
+ /**
+ * 联系人选择器中数据源类型:好友(默认)、群、群成员(需要设置teamId)
+ */
+ public ContactSelectType type = ContactSelectType.BUDDY;
+
+ /**
+ * 联系人选择器数据源类型为群成员时,需要设置群号
+ */
+ public String teamId = null;
+
+ /**
+ * 联系人选择器标题
+ */
+ public String title = ResUtil.getString(R.string.selector_activity_contactselectactivity_01);
+
+ /**
+ * 联系人单选/多选(默认)
+ */
+ public boolean multi = true;
+
+ /**
+ * 至少选择人数
+ */
+ public int minSelectNum = 1;
+
+ /**
+ * 低于最少选择人数的提示
+ */
+ public String minSelectedTip = null;
+
+ /**
+ * 最大可选人数
+ */
+ public int maxSelectNum = 2000;
+
+ /**
+ * 超过最大可选人数的提示
+ */
+ public String maxSelectedTip = null;
+
+ /**
+ * 是否显示已选头像区域
+ */
+ public boolean showContactSelectArea = true;
+
+ /**
+ * 默认勾选(且可操作)的联系人项
+ */
+ public ArrayList alreadySelectedAccounts = null;
+
+ /**
+ * 需要过滤(不显示)的联系人项
+ */
+ public ContactItemFilter itemFilter = null;
+
+ /**
+ * 需要disable(可见但不可操作)的联系人项
+ */
+ public ContactItemFilter itemDisableFilter = null;
+
+ /**
+ * 是否支持搜索
+ */
+ public boolean searchVisible = true;
+
+ /**
+ * 允许不选任何人点击确定
+ */
+ public boolean allowSelectEmpty = false;
+
+ /**
+ * 是否显示最大数目,结合maxSelectNum,与搜索位置相同
+ */
+ public boolean maxSelectNumVisible = false;
+ }
+
+ public static void startActivityForResult(Context context, Option option, int requestCode) {
+ Intent intent = new Intent();
+ intent.putExtra(EXTRA_DATA, option);
+ intent.setClass(context, ContactSelectActivity.class);
+
+ ((Activity) context).startActivityForResult(intent, requestCode);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (searchView != null) {
+ searchView.setQuery("", true);
+ searchView.setIconified(true);
+ }
+ showKeyboard(false);
+ finish();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(android.view.Menu menu) {
+ // search view
+ getMenuInflater().inflate(R.menu.nim_contacts_search_menu, menu);
+ MenuItem item = menu.findItem(R.id.action_search);
+ if (!option.searchVisible) {
+ item.setVisible(false);
+ return true;
+ }
+
+ MenuItemCompat.setOnActionExpandListener(item, new MenuItemCompat.OnActionExpandListener() {
+
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem menuItem) {
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem menuItem) {
+ finish();
+ return false;
+ }
+ });
+ SearchView searchView = (SearchView) MenuItemCompat.getActionView(item);
+ this.searchView = searchView;
+ this.searchView.setVisibility(option.searchVisible ? View.VISIBLE : View.GONE);
+ searchView.setOnQueryTextListener(this);
+ return true;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.nim_contacts_select);
+
+ ToolBarOptions options = new NimToolBarOptions();
+ setToolBar(R.id.toolbar, options);
+
+ parseIntentData();
+ initAdapter();
+ initListView();
+ initContactSelectArea();
+
+ loadData();
+ }
+
+ private void parseIntentData() {
+ this.option = (Option) getIntent().getSerializableExtra(EXTRA_DATA);
+ if (TextUtils.isEmpty(option.maxSelectedTip)) {
+ option.maxSelectedTip = ResUtil.getString(R.string.selector_activity_contactselectactivity_02) + option.maxSelectNum + ResUtil.getString(R.string.selector_activity_contactselectactivity_03);
+ }
+ if (TextUtils.isEmpty(option.minSelectedTip)) {
+ option.minSelectedTip = ResUtil.getString(R.string.selector_activity_contactselectactivity_04) + option.minSelectNum + ResUtil.getString(R.string.selector_activity_contactselectactivity_05);
+ }
+ setTitle(option.title);
+ }
+
+ private class ContactDataProviderEx extends ContactDataProvider {
+ private String teamId;
+
+ private boolean loadedTeamMember = false;
+
+ public ContactDataProviderEx(String teamId, int... itemTypes) {
+ super(itemTypes);
+ this.teamId = teamId;
+ }
+
+ @Override
+ public List provide(TextQuery query) {
+ List data = new ArrayList<>();
+ // 异步加载
+ if (!loadedTeamMember) {
+ TeamMemberDataProvider.loadTeamMemberDataAsync(teamId, new TeamMemberDataProvider.LoadTeamMemberCallback() {
+ @Override
+ public void onResult(boolean success) {
+ if (success) {
+ loadedTeamMember = true;
+ // 列表重新加载数据
+ loadData();
+ }
+ }
+ });
+ } else {
+ data = TeamMemberDataProvider.provide(query, teamId);
+ }
+ return data;
+ }
+ }
+
+ private void initAdapter() {
+ IContactDataProvider dataProvider;
+ if (option.type == ContactSelectType.TEAM_MEMBER && !TextUtils.isEmpty(this.option.teamId)) {
+ dataProvider = new ContactDataProviderEx(this.option.teamId, ItemTypes.TEAM_MEMBER);
+ } else if (option.type == ContactSelectType.TEAM) {
+ option.showContactSelectArea = false;
+ dataProvider = new ContactDataProvider(ItemTypes.TEAM);
+ } else {
+ dataProvider = new ContactDataProvider(ItemTypes.FRIEND);
+ }
+
+ // contact adapter
+ contactAdapter = new ContactSelectAdapter(ContactSelectActivity.this, new ContactsSelectGroupStrategy(),
+ dataProvider) {
+ boolean isEmptyContacts = false;
+
+ @Override
+ protected List onNonDataItems() {
+ return null;
+ }
+
+ @Override
+ protected void onPostLoad(boolean empty, String queryText, boolean all) {
+ if (empty) {
+ if (TextUtils.isEmpty(queryText)) {
+ isEmptyContacts = true;
+ }
+ updateEmptyView(queryText);
+ } else {
+ setSearchViewVisible(true);
+ }
+ }
+
+ private void updateEmptyView(String queryText) {
+ if (!isEmptyContacts && !TextUtils.isEmpty(queryText)) {
+ setSearchViewVisible(true);
+ } else {
+ setSearchViewVisible(false);
+ }
+ }
+
+ private void setSearchViewVisible(boolean visible) {
+ option.searchVisible = visible;
+ if (searchView != null) {
+ searchView.setVisibility(option.searchVisible ? View.VISIBLE : View.GONE);
+ }
+ }
+ };
+
+ Class c = option.multi ? ContactsMultiSelectHolder.class : ContactsSelectHolder.class;
+ contactAdapter.addViewHolder(ItemTypes.LABEL, LabelHolder.class);
+ contactAdapter.addViewHolder(ItemTypes.FRIEND, c);
+ contactAdapter.addViewHolder(ItemTypes.TEAM_MEMBER, c);
+ contactAdapter.addViewHolder(ItemTypes.TEAM, c);
+
+ contactAdapter.setFilter(option.itemFilter);
+ contactAdapter.setDisableFilter(option.itemDisableFilter);
+
+ // contact select adapter
+ contactSelectedAdapter = new ContactSelectAvatarAdapter(this);
+ }
+
+ private void initListView() {
+ listView = findView(R.id.contact_list_view);
+ listView.setAdapter(contactAdapter);
+ listView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ showKeyboard(false);
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+
+ }
+ });
+ listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ position = position - listView.getHeaderViewsCount();
+ AbsContactItem item = (AbsContactItem) contactAdapter.getItem(position);
+
+ if (item == null) {
+ return;
+ }
+
+ if (option.multi) {
+ if (!contactAdapter.isEnabled(position)) {
+ return;
+ }
+ IContact contact = null;
+ if (item instanceof ContactItem) {
+ contact = ((ContactItem) item).getContact();
+ }
+ if (contactAdapter.isSelected(position)) {
+ contactAdapter.cancelItem(position);
+ if (contact != null) {
+ contactSelectedAdapter.removeContact(contact);
+ }
+ } else {
+ if (contactSelectedAdapter.getCount() <= option.maxSelectNum) {
+ contactAdapter.selectItem(position);
+ if (contact != null) {
+ contactSelectedAdapter.addContact(contact);
+ }
+ } else {
+// Toast.makeText(ContactSelectActivity.this, option.maxSelectedTip, Toast.LENGTH_SHORT).show();
+ SingleToastUtil.showToastShort(option.maxSelectedTip);
+ }
+
+ if (!TextUtils.isEmpty(queryText) && searchView != null) {
+ searchView.setQuery("", true);
+ searchView.setIconified(true);
+ showKeyboard(false);
+ }
+ }
+ arrangeSelected();
+ } else {
+ if (item instanceof ContactItem) {
+ final IContact contact = ((ContactItem) item).getContact();
+ ArrayList selectedIds = new ArrayList<>();
+ selectedIds.add(contact.getContactId());
+ onSelected(selectedIds);
+ }
+
+ arrangeSelected();
+ }
+ }
+ });
+
+ // 字母导航
+ TextView letterHit = (TextView) findViewById(R.id.tv_hit_letter);
+ LetterIndexView idxView = (LetterIndexView) findViewById(R.id.liv_index);
+ idxView.setLetters(getResources().getStringArray(R.array.letter_list2));
+ ImageView imgBackLetter = (ImageView) findViewById(R.id.img_hit_letter);
+ if (option.type != ContactSelectType.TEAM) {
+ livIndex = contactAdapter.createLivIndex(listView, idxView, letterHit, imgBackLetter);
+ livIndex.show();
+ } else {
+ idxView.setVisibility(View.GONE);
+ }
+ }
+
+ private void initContactSelectArea() {
+ btnSelect = (Button) findViewById(R.id.btnSelect);
+ if (!option.allowSelectEmpty) {
+ btnSelect.setEnabled(false);
+ } else {
+ btnSelect.setEnabled(true);
+ }
+ btnSelect.setOnClickListener(this);
+ bottomPanel = (RelativeLayout) findViewById(R.id.rlCtrl);
+ scrollViewSelected = (HorizontalScrollView) findViewById(R.id.contact_select_area);
+ if (option.multi) {
+ bottomPanel.setVisibility(View.VISIBLE);
+ if (option.showContactSelectArea) {
+ scrollViewSelected.setVisibility(View.VISIBLE);
+ btnSelect.setVisibility(View.VISIBLE);
+ } else {
+ scrollViewSelected.setVisibility(View.GONE);
+ btnSelect.setVisibility(View.GONE);
+ }
+ btnSelect.setText(getOKBtnText(0));
+ } else {
+ bottomPanel.setVisibility(View.GONE);
+ }
+
+ // selected contact image banner
+ imageSelectedGridView = (GridView) findViewById(R.id.contact_select_area_grid);
+ imageSelectedGridView.setAdapter(contactSelectedAdapter);
+ notifySelectAreaDataSetChanged();
+ imageSelectedGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ try {
+ if (contactSelectedAdapter.getItem(position) == null) {
+ return;
+ }
+
+ IContact iContact = contactSelectedAdapter.remove(position);
+ if (iContact != null) {
+ contactAdapter.cancelItem(iContact);
+ }
+ arrangeSelected();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ // init already selected items
+ List selectedUids = option.alreadySelectedAccounts;
+ if (selectedUids != null && !selectedUids.isEmpty()) {
+ contactAdapter.setAlreadySelectedAccounts(selectedUids);
+ List selectedItems = contactAdapter.getSelectedItem();
+ for (ContactItem item : selectedItems) {
+ contactSelectedAdapter.addContact(item.getContact());
+ }
+ arrangeSelected();
+ }
+ }
+
+ private void loadData() {
+ contactAdapter.load(true);
+ }
+
+ private void arrangeSelected() {
+ this.contactAdapter.notifyDataSetChanged();
+ if (option.multi) {
+ int count = contactSelectedAdapter.getCount();
+ if (!option.allowSelectEmpty) {
+ btnSelect.setEnabled(count > 1);
+ } else {
+ btnSelect.setEnabled(true);
+ }
+ btnSelect.setText(getOKBtnText(count));
+ notifySelectAreaDataSetChanged();
+ }
+ }
+
+ private void notifySelectAreaDataSetChanged() {
+ int converViewWidth = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 46, this.getResources()
+ .getDisplayMetrics()));
+ ViewGroup.LayoutParams layoutParams = imageSelectedGridView.getLayoutParams();
+ layoutParams.width = converViewWidth * contactSelectedAdapter.getCount();
+ layoutParams.height = converViewWidth;
+ imageSelectedGridView.setLayoutParams(layoutParams);
+ imageSelectedGridView.setNumColumns(contactSelectedAdapter.getCount());
+
+ try {
+ final int x = layoutParams.width;
+ final int y = layoutParams.height;
+ new Handler().post(new Runnable() {
+ @Override
+ public void run() {
+ scrollViewSelected.scrollTo(x, y);
+ }
+ });
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ contactSelectedAdapter.notifyDataSetChanged();
+ }
+
+ private String getOKBtnText(int count) {
+ String caption = getString(R.string.ok);
+ int showCount = (count < 1 ? 0 : (count - 1));
+ StringBuilder sb = new StringBuilder(caption);
+ sb.append(" (");
+ sb.append(showCount);
+ if (option.maxSelectNumVisible) {
+ sb.append("/");
+ sb.append(option.maxSelectNum);
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+
+ /**
+ * ************************** select ************************
+ */
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.btnSelect) {
+ List contacts = contactSelectedAdapter
+ .getSelectedContacts();
+ if (option.allowSelectEmpty || checkMinMaxSelection(contacts.size())) {
+ ArrayList selectedAccounts = new ArrayList<>();
+ for (IContact c : contacts) {
+ selectedAccounts.add(c.getContactId());
+ }
+ onSelected(selectedAccounts);
+ }
+
+ }
+ }
+
+ private boolean checkMinMaxSelection(int selected) {
+ if (option.minSelectNum > selected) {
+ return showMaxMinSelectTip(true);
+ } else if (option.maxSelectNum < selected) {
+ return showMaxMinSelectTip(false);
+ }
+ return true;
+ }
+
+ private boolean showMaxMinSelectTip(boolean min) {
+ if (min) {
+// Toast.makeText(this, option.minSelectedTip, Toast.LENGTH_SHORT).show();
+ SingleToastUtil.showToastShort(option.minSelectedTip);
+ } else {
+// Toast.makeText(this, option.maxSelectedTip, Toast.LENGTH_SHORT).show();
+ SingleToastUtil.showToastShort(option.maxSelectedTip);
+ }
+ return false;
+ }
+
+ public void onSelected(ArrayList selects) {
+ Intent intent = new Intent();
+ intent.putStringArrayListExtra(RESULT_DATA, selects);
+ setResult(Activity.RESULT_OK, intent);
+ this.finish();
+ }
+
+ /**
+ * ************************* search ******************************
+ */
+
+ @Override
+ public boolean onQueryTextChange(String query) {
+ queryText = query;
+ if (TextUtils.isEmpty(query)) {
+ this.contactAdapter.load(true);
+ } else {
+ this.contactAdapter.query(query);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String arg0) {
+ return false;
+ }
+
+ @Override
+ public void finish() {
+ showKeyboard(false);
+ super.finish();
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/adapter/ContactSelectAdapter.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/adapter/ContactSelectAdapter.java
new file mode 100644
index 0000000..7a48c2b
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/adapter/ContactSelectAdapter.java
@@ -0,0 +1,75 @@
+package com.netease.nim.uikit.business.contact.selector.adapter;
+
+import android.content.Context;
+
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.business.contact.core.item.AbsContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ContactItem;
+import com.netease.nim.uikit.business.contact.core.item.ItemTypes;
+import com.netease.nim.uikit.business.contact.core.model.ContactDataAdapter;
+import com.netease.nim.uikit.business.contact.core.model.ContactGroupStrategy;
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.business.contact.core.query.IContactDataProvider;
+import com.netease.nim.uikit.business.contact.core.util.ContactHelper;
+import com.netease.nimlib.sdk.uinfo.model.UserInfo;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+public class ContactSelectAdapter extends ContactDataAdapter {
+ private HashSet selects = new HashSet();
+
+ public ContactSelectAdapter(Context context,
+ ContactGroupStrategy groupStrategy, IContactDataProvider dataProvider) {
+ super(context, groupStrategy, dataProvider);
+ }
+
+ public final void setAlreadySelectedAccounts(List accounts) {
+ selects.addAll(accounts);
+ }
+
+ public final List getSelectedItem() {
+ if (selects.isEmpty()) {
+ return null;
+ }
+
+ List res = new ArrayList<>();
+ for (String account : selects) {
+ final UserInfo user = NimUIKit.getUserInfoProvider().getUserInfo(account);
+ if (user != null) {
+ res.add(new ContactItem(ContactHelper.makeContactFromUserInfo(user), ItemTypes.FRIEND));
+ }
+ }
+
+ return res;
+ }
+
+ public final void selectItem(int position) {
+ AbsContactItem item = (AbsContactItem) getItem(position);
+ if (item != null && item instanceof ContactItem) {
+ selects.add(((ContactItem) item).getContact().getContactId());
+ }
+ notifyDataSetChanged();
+ }
+
+ public final boolean isSelected(int position) {
+ AbsContactItem item = (AbsContactItem) getItem(position);
+ if (item != null && item instanceof ContactItem) {
+ return selects.contains(((ContactItem) item).getContact().getContactId());
+ }
+ return false;
+ }
+
+ public final void cancelItem(int position) {
+ AbsContactItem item = (AbsContactItem) getItem(position);
+ if (item != null && item instanceof ContactItem) {
+ selects.remove(((ContactItem) item).getContact().getContactId());
+ }
+ notifyDataSetChanged();
+ }
+
+ public final void cancelItem(IContact iContact) {
+ selects.remove(iContact.getContactId());
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/adapter/ContactSelectAvatarAdapter.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/adapter/ContactSelectAvatarAdapter.java
new file mode 100644
index 0000000..c39ca30
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/adapter/ContactSelectAvatarAdapter.java
@@ -0,0 +1,111 @@
+package com.netease.nim.uikit.business.contact.selector.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.common.ui.imageview.HeadImageView;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+class GalleryItemViewHolder {
+ HeadImageView imageView;
+}
+
+public class ContactSelectAvatarAdapter extends BaseAdapter {
+ private Context context;
+
+ private List selectedContactItems;
+
+ public ContactSelectAvatarAdapter(Context context) {
+ this.context = context;
+ this.selectedContactItems = new ArrayList();
+ selectedContactItems.add(null);
+ }
+
+ @Override
+ public int getCount() {
+ return selectedContactItems.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return selectedContactItems.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ HeadImageView imageView;
+
+ if (convertView == null) {
+ convertView = LayoutInflater.from(context).inflate(R.layout.nim_contact_select_area_item, null);
+ imageView = (HeadImageView) convertView.findViewById(R.id.contact_select_area_image);
+
+ GalleryItemViewHolder holder = new GalleryItemViewHolder();
+ holder.imageView = imageView;
+ convertView.setTag(holder);
+ } else {
+ GalleryItemViewHolder holder = (GalleryItemViewHolder) convertView.getTag();
+ imageView = holder.imageView;
+ }
+
+ try {
+ IContact item = selectedContactItems.get(position);
+ if (item == null) {
+ imageView.setBackgroundResource(R.drawable.nim_contact_select_dot_avatar);
+ imageView.setImageDrawable(null);
+ } else {
+ imageView.loadBuddyAvatar(item.getContactId());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return convertView;
+ }
+
+ public void addContact(IContact contact) {
+ if (selectedContactItems.size() > 0) {
+ IContact iContact = selectedContactItems.get(selectedContactItems.size() - 1);
+ if (iContact == null) {
+ selectedContactItems.remove(selectedContactItems.size() - 1);
+ }
+ }
+ this.selectedContactItems.add(contact);
+ selectedContactItems.add(null);
+ }
+
+ public void removeContact(IContact contact) {
+ if (contact == null) {
+ return;
+ }
+ for (Iterator iterator = selectedContactItems.iterator(); iterator.hasNext(); ) {
+ IContact iContact = iterator.next();
+ if (iContact == null) {
+ continue;
+ }
+ if (iContact.getContactId().equals(contact.getContactId())) {
+ iterator.remove();
+ }
+ }
+ }
+
+ public IContact remove(int pos) {
+ return this.selectedContactItems.remove(pos);
+ }
+
+ public List getSelectedContacts() {
+ return this.selectedContactItems.subList(0, selectedContactItems.size() - 1);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/viewholder/ContactsMultiSelectHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/viewholder/ContactsMultiSelectHolder.java
new file mode 100644
index 0000000..324de61
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/viewholder/ContactsMultiSelectHolder.java
@@ -0,0 +1,7 @@
+package com.netease.nim.uikit.business.contact.selector.viewholder;
+
+public class ContactsMultiSelectHolder extends ContactsSelectHolder {
+ public ContactsMultiSelectHolder() {
+ super(true);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/viewholder/ContactsSelectHolder.java b/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/viewholder/ContactsSelectHolder.java
new file mode 100644
index 0000000..3b60c7e
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/contact/selector/viewholder/ContactsSelectHolder.java
@@ -0,0 +1,88 @@
+package com.netease.nim.uikit.business.contact.selector.viewholder;
+
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nim.uikit.business.contact.core.item.ContactItem;
+import com.netease.nim.uikit.business.contact.core.model.ContactDataAdapter;
+import com.netease.nim.uikit.business.contact.core.model.IContact;
+import com.netease.nim.uikit.business.contact.core.viewholder.AbsContactViewHolder;
+import com.netease.nim.uikit.business.contact.selector.adapter.ContactSelectAdapter;
+import com.netease.nim.uikit.common.ui.imageview.HeadImageView;
+
+public class ContactsSelectHolder extends AbsContactViewHolder {
+ private final boolean multi;
+
+ private HeadImageView image;
+
+ private TextView nickname;
+
+ private ImageView select;
+
+ private Drawable defaultBackground;
+
+ public ContactsSelectHolder() {
+ this(false);
+ }
+
+ ContactsSelectHolder(boolean multi) {
+ this.multi = multi;
+ }
+
+ @Override
+ public void refresh(ContactDataAdapter adapter, int position, ContactItem item) {
+ if (multi) {
+ boolean disabled = !adapter.isEnabled(position);
+ boolean selected = adapter instanceof ContactSelectAdapter ? ((ContactSelectAdapter) adapter).isSelected(position) : false;
+ this.select.setVisibility(View.VISIBLE);
+ if (disabled) {
+ this.select.setBackgroundResource(R.drawable.nim_contact_checkbox_checked_grey);
+ getView().setBackgroundColor(context.getResources().getColor(R.color.transparent));
+ } else if (selected) {
+ setBackground(getView(), defaultBackground);
+ this.select.setBackgroundResource(R.drawable.nim_contact_checkbox_checked_green);
+ } else {
+ setBackground(getView(), defaultBackground);
+ this.select.setBackgroundResource(R.drawable.nim_contact_checkbox_unchecked);
+ }
+ } else {
+ this.select.setVisibility(View.GONE);
+ }
+
+ IContact contact = item.getContact();
+ this.nickname.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+ this.nickname.setText(contact.getDisplayName());
+ if (contact.getContactType() == IContact.Type.Friend || contact.getContactType() == IContact.Type.TeamMember) {
+ this.nickname.setText(contact.getDisplayName());
+ this.image.loadBuddyAvatar(contact.getContactId());
+ } else if (contact.getContactType() == IContact.Type.Team) {
+ this.image.loadTeamIconByTeam(NimUIKit.getTeamProvider().getTeamById(contact.getContactId()));
+ }
+
+ this.image.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public View inflate(LayoutInflater inflater) {
+ View view = inflater.inflate(R.layout.nim_contacts_select_item, null);
+ defaultBackground = view.getBackground();
+ this.image = view.findViewById(R.id.img_head);
+ this.nickname = view.findViewById(R.id.tv_nickname);
+ this.select = view.findViewById(R.id.imgSelect);
+ return view;
+ }
+
+ private void setBackground(View view, Drawable drawable) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ view.setBackground(drawable);
+ } else {
+ view.setBackgroundDrawable(drawable);
+ }
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/preference/UserPreferences.java b/nim_uikit/src/com/netease/nim/uikit/business/preference/UserPreferences.java
new file mode 100644
index 0000000..8d1780f
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/preference/UserPreferences.java
@@ -0,0 +1,36 @@
+package com.netease.nim.uikit.business.preference;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.netease.nim.uikit.api.NimUIKit;
+
+/**
+ * Created by hzxuwen on 2015/10/21.
+ */
+public class UserPreferences {
+
+ private final static String KEY_EARPHONE_MODE = "KEY_EARPHONE_MODE";
+
+ public static void setEarPhoneModeEnable(boolean on) {
+ saveBoolean(KEY_EARPHONE_MODE, on);
+ }
+
+ public static boolean isEarPhoneModeEnable() {
+ return getBoolean(KEY_EARPHONE_MODE, true);
+ }
+
+ private static boolean getBoolean(String key, boolean value) {
+ return getSharedPreferences().getBoolean(key, value);
+ }
+
+ private static void saveBoolean(String key, boolean value) {
+ SharedPreferences.Editor editor = getSharedPreferences().edit();
+ editor.putBoolean(key, value);
+ editor.apply();
+ }
+
+ private static SharedPreferences getSharedPreferences() {
+ return NimUIKit.getContext().getSharedPreferences("UIKit." + NimUIKit.getAccount(), Context.MODE_PRIVATE);
+ }
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/recent/RecentContactsCallback.java b/nim_uikit/src/com/netease/nim/uikit/business/recent/RecentContactsCallback.java
new file mode 100644
index 0000000..56133d8
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/recent/RecentContactsCallback.java
@@ -0,0 +1,29 @@
+package com.netease.nim.uikit.business.recent;
+
+import com.netease.nimlib.sdk.msg.model.RecentContact;
+
+/**
+ * 最近联系人列表自定义事件回调函数.
+ */
+public interface RecentContactsCallback {
+
+ /**
+ * 最近联系人列表数据加载完成的回调函数
+ */
+ void onRecentContactsLoaded();
+
+ /**
+ * 有未读数更新时的回调函数,供更新除最近联系人列表外的其他界面和未读指示
+ *
+ * @param unreadCount 当前总的未读数
+ */
+ void onUnreadCountChange(int unreadCount);
+
+ /**
+ * 最近联系人点击响应回调函数,以供打开会话窗口时传入定制化参数,或者做其他动作
+ *
+ * @param recent 最近联系人
+ */
+ void onItemClick(RecentContact recent);
+
+}
diff --git a/nim_uikit/src/com/netease/nim/uikit/business/recent/TeamMemberAitHelper.java b/nim_uikit/src/com/netease/nim/uikit/business/recent/TeamMemberAitHelper.java
new file mode 100644
index 0000000..326c32b
--- /dev/null
+++ b/nim_uikit/src/com/netease/nim/uikit/business/recent/TeamMemberAitHelper.java
@@ -0,0 +1,130 @@
+package com.netease.nim.uikit.business.recent;
+
+import android.graphics.Color;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+
+import com.netease.nim.uikit.R;
+import com.netease.nim.uikit.api.NimUIKit;
+import com.netease.nimlib.sdk.NIMClient;
+import com.netease.nimlib.sdk.msg.MsgService;
+import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
+import com.netease.nimlib.sdk.msg.model.IMMessage;
+import com.netease.nimlib.sdk.msg.model.MemberPushOption;
+import com.netease.nimlib.sdk.msg.model.RecentContact;
+import com.chwl.library.utils.ResUtil;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Created by hzchenkang on 2016/12/5.
+ */
+
+public class TeamMemberAitHelper {
+
+ private static final String KEY_AIT = "ait";
+
+ public static String getAitAlertString(String content) {
+ return ResUtil.getString(R.string.business_recent_teammemberaithelper_01) + content;
+ }
+
+ public static void replaceAitForeground(String value, SpannableString mSpannableString) {
+ if (TextUtils.isEmpty(value) || TextUtils.isEmpty(mSpannableString)) {
+ return;
+ }
+ Pattern pattern = Pattern.compile(ResUtil.getString(R.string.business_recent_teammemberaithelper_02));
+ Matcher matcher = pattern.matcher(value);
+ while (matcher.find()) {
+ int start = matcher.start();
+ if (start != 0) {
+ continue;
+ }
+ int end = matcher.end();
+ mSpannableString.setSpan(new ForegroundColorSpan(Color.RED), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
+ }
+ }
+
+ public static boolean isAitMessage(IMMessage message) {
+ if (message == null || message.getSessionType() != SessionTypeEnum.Team) {
+ return false;
+ }
+ MemberPushOption option = message.getMemberPushOption();
+ boolean isForce = option != null && option.isForcePush() &&
+ (option.getForcePushList() == null || option.getForcePushList().contains(NimUIKit.getAccount()));
+
+ return isForce;
+ }
+
+ public static boolean hasAitExtension(RecentContact recentContact) {
+ if (recentContact == null || recentContact.getSessionType() != SessionTypeEnum.Team) {
+ return false;
+ }
+ Map ext = recentContact.getExtension();
+ if (ext == null) {
+ return false;
+ }
+ List mid = (List) ext.get(KEY_AIT);
+
+ return mid != null && !mid.isEmpty();
+ }
+
+ public static void clearRecentContactAited(RecentContact recentContact) {
+ if (recentContact == null || recentContact.getSessionType() != SessionTypeEnum.Team) {
+ return;
+ }
+ Map exts = recentContact.getExtension();
+ if (exts != null) {
+ exts.put(KEY_AIT, null);
+ }
+ recentContact.setExtension(exts);
+ NIMClient.getService(MsgService.class).updateRecent(recentContact);
+ }
+
+
+ public static void buildAitExtensionByMessage(Map extention, IMMessage message) {
+
+ if (extention == null || message == null || message.getSessionType() != SessionTypeEnum.Team) {
+ return;
+ }
+ List