Browse Source

sync with android-chat

imndx 3 months ago
parent
commit
7ebf7bd373

+ 3 - 1
uni-Android-SDK/client/build.gradle

@@ -53,7 +53,9 @@ android {
 
 dependencies {
     api project(':mars-core-release')
-    api "androidx.lifecycle:lifecycle-extensions:2.2.0"
+    api "androidx.lifecycle:lifecycle-viewmodel:2.8.7"
+    api "androidx.lifecycle:lifecycle-livedata:2.8.7"
+    api "androidx.lifecycle:lifecycle-process:2.8.7"
     api 'com.squareup.okhttp3:okhttp:4.12.0'
     implementation 'com.squareup.okio:okio:3.10.2'
 }

+ 14 - 6
uni-Android-SDK/client/src/main/AndroidManifest.xml

@@ -20,12 +20,20 @@
     <application
         android:usesCleartextTraffic="true"
         tools:targetApi="m">
-        <service
-            android:name="cn.wildfirechat.client.ClientService"
-            android:process=":marsservice" />
-        <receiver
-            android:name="com.tencent.mars.BaseEvent$ConnectionReceiver"
-            android:process=":marsservice" />
+        <!--        clientService 可以配置运行在单独的进程,或直接运行在主进程-->
+        <!--        当 IM 功能,不是应用的主要功能时,可以考虑让 ClientService 运行在单独的进程中-->
+        <!--        当 IM 功能,是应用的主要功能时,ClientService 运行在主进程,性能会更好-->
+
+        <!--        <service-->
+        <!--            android:name="cn.wildfirechat.client.ClientService"-->
+        <!--            android:process=":marsservice" />-->
+        <!--        <receiver-->
+        <!--            android:name="com.tencent.mars.BaseEvent$ConnectionReceiver"-->
+        <!--            android:process=":marsservice" />-->
+
+        <!--        clientService 默认运行在主进程-->
+        <service android:name="cn.wildfirechat.client.ClientService" />
+        <receiver android:name="com.tencent.mars.BaseEvent$ConnectionReceiver" />
 
         <!--must run in th main process-->
         <receiver android:name="cn.wildfirechat.remote.RecoverReceiver" />

+ 3 - 0
uni-Android-SDK/client/src/main/aidl/cn/wildfirechat/client/IRemoteClient.aidl

@@ -174,6 +174,7 @@ interface IRemoteClient {
     Map getMessageDelivery(in int conversationType, in String target);
     oneway void searchUser(in String keyword, in int searchType, in int page, in ISearchUserCallback callback);
     oneway void searchUserEx(in String domainId, in String keyword, in int searchType, in int page, in ISearchUserCallback callback);
+    oneway void searchUserEx2(in String domainId, in String keyword, in int searchType, in int userType, in int page, in ISearchUserCallback callback);
 
     boolean isMyFriend(in String userId);
     List<String> getMyFriendList(in boolean refresh);
@@ -330,6 +331,7 @@ interface IRemoteClient {
     oneway void noUseFts();
     oneway void checkSignature();
 
+    oneway void setHeartBeatInterval(int second);
     String getProtoRevision();
 
     long getDiskSpaceAvailableSize();
@@ -346,6 +348,7 @@ interface IRemoteClient {
     byte[] decodeSecretChatData(in String targetid, in byte[] mediaData);
 
     oneway void decodeSecretChatDataAsync(in String targetId, in AshmenWrapper ashmenWrapper, in int length, in IGeneralCallbackInt callback);
+    oneway void sendMomentsRequest(in String path, in AshmenWrapper ashmenWrapper, in int length, in IGeneralCallbackInt callback);
 
     oneway void setDefaultPortraitProviderClass(in String clazz);
     oneway void setUrlRedirectorClass(in String clazz);

+ 1 - 1
uni-Android-SDK/client/src/main/java/cn/wildfirechat/ErrorCode.java

@@ -15,7 +15,7 @@ public class ErrorCode {
 
     //0~255 server error
     public static final int SUCCESS = 0;  // //"success");
-    public static final int SECRECT_KEY_MISMATCH = 1;  //"secrect key mismatch");
+    public static final int SECRECT_KEY_MISMATCH = 1;  //"secret key mismatch");
     public static final int INVALID_DATA = 2;  //"invalid data");
     public static final int NODE_NOT_EXIST = 3;  //"node not exist");
     public static final int SERVER_ERROR = 4;  //"server error");

+ 158 - 10
uni-Android-SDK/client/src/main/java/cn/wildfirechat/client/ClientService.java

@@ -17,6 +17,7 @@ import static cn.wildfirechat.remote.UserSettingScope.ConversationTop;
 import android.app.Service;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
@@ -25,6 +26,7 @@ import android.os.LocaleList;
 import android.os.Looper;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.Process;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.preference.PreferenceManager;
@@ -54,7 +56,6 @@ import java.lang.reflect.Constructor;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -98,6 +99,7 @@ import cn.wildfirechat.model.GroupSearchResult;
 import cn.wildfirechat.model.ModifyMyInfoEntry;
 import cn.wildfirechat.model.NullGroupMember;
 import cn.wildfirechat.model.NullUserInfo;
+import cn.wildfirechat.model.ProtoAddressItem;
 import cn.wildfirechat.model.ProtoBurnMessageInfo;
 import cn.wildfirechat.model.ProtoChannelInfo;
 import cn.wildfirechat.model.ProtoChannelMenu;
@@ -164,7 +166,8 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
     ProtoLogic.ISecretMessageBurnStateCallback,
     ProtoLogic.IChannelInfoUpdateCallback,
     ProtoLogic.IDomainInfoUpdateCallback,
-    ProtoLogic.IGroupMembersUpdateCallback {
+    ProtoLogic.IGroupMembersUpdateCallback,
+    ProtoLogic.ISortAddressCallback {
     private Map<Integer, Class<? extends MessageContent>> contentMapper = new HashMap<>();
 
     private int mConnectionStatus;
@@ -227,6 +230,8 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
     private ConcurrentHashMap<Long, Call> uploadingMap;
 
     private DefaultPortraitProvider defaultPortraitProvider;
+    private final Map<String, String> protoHttpHeaderMap = new ConcurrentHashMap<>();
+    private String protoUserAgent;
 
     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
     private class ClientServiceStub extends IRemoteClient.Stub {
@@ -411,11 +416,13 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
         @Override
         public void setProtoUserAgent(String userAgent) throws RemoteException {
             ProtoLogic.setUserAgent(userAgent);
+            protoUserAgent = userAgent;
         }
 
         @Override
         public void addHttpHeader(String header, String value) throws RemoteException {
             ProtoLogic.addHttpHeader(header, value);
+            protoHttpHeaderMap.put(header, value);
         }
 
         @Override
@@ -597,7 +604,7 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
                 remoteUrl = ((RawMessageContent) msg.content).payload.remoteMediaUrl;
             }
 
-            if (!TextUtils.isEmpty(localPath)) {
+            if (!TextUtils.isEmpty(localPath) && TextUtils.isEmpty(remoteUrl)) {
                 file = new File(localPath);
                 if (!file.exists() && TextUtils.isEmpty(remoteUrl)) {
                     android.util.Log.e(TAG, "mediaMessage invalid, file not exist");
@@ -612,7 +619,7 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
                     }
                 }
                 if (isSupportBigFilesUpload() && TextUtils.isEmpty(remoteUrl)) {
-                    if (ProtoLogic.forcePresignedUrlUpload() || file.length() > 100 * 1024 * 1024) {
+                    if (tcpShortLink || ProtoLogic.forcePresignedUrlUpload() || file.length() > 100 * 1024 * 1024) {
                         uploadThenSend = true;
                     }
                 }
@@ -1443,6 +1450,35 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
             });
         }
 
+        @Override
+        public void searchUserEx2(String domainId, String keyword, int searchType, int userType, int page, ISearchUserCallback callback) throws RemoteException {
+            ProtoLogic.searchUserEx2(domainId, keyword, searchType, userType, page, new ProtoLogic.ISearchUserCallback() {
+                @Override
+                public void onSuccess(ProtoUserInfo[] userInfos) {
+                    List<UserInfo> out = new ArrayList<>();
+                    if (userInfos != null) {
+                        for (ProtoUserInfo protoUserInfo : userInfos) {
+                            out.add(convertProtoUserInfo(protoUserInfo));
+                        }
+                    }
+                    try {
+                        callback.onSuccess(out);
+                    } catch (RemoteException e) {
+                        e.printStackTrace();
+                    }
+                }
+
+                @Override
+                public void onFailure(int errorCode) {
+                    try {
+                        callback.onFailure(errorCode);
+                    } catch (RemoteException e) {
+                        e.printStackTrace();
+                    }
+                }
+            });
+        }
+
         @Override
         public boolean isMyFriend(String userId) throws RemoteException {
             return ProtoLogic.isMyFriend(userId);
@@ -2078,8 +2114,8 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
                 return;
             }
             android.util.Log.d(TAG, "uploadMedia " + fileName + " " + data.length + " " + mediaType);
-            if (isSupportBigFilesUpload()) {
-                android.util.Log.d(TAG, "uploadMedia00");
+            if (ProtoLogic.forcePresignedUrlUpload() || isSupportBigFilesUpload()) {
+                android.util.Log.d(TAG, "uploadMedia");
                 String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
                 String contentType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
                 uploadBigFile(-1, fileName, null, data, mediaType, contentType, new UploadMediaCallback() {
@@ -2155,15 +2191,24 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
                     return;
                 }
 
+                boolean largeMedia = false;
                 if (tcpShortLink) {
                     if (!isSupportBigFilesUpload()) {
                         android.util.Log.e(TAG, "TCP短连接不支持内置对象存储,请把对象存储切换到其他类型");
                         callback.onFailure(-1);
                         return;
+                    } else {
+                        largeMedia = true;
+                    }
+                } else if(isSupportBigFilesUpload()) {
+                    if(ProtoLogic.forcePresignedUrlUpload()) {
+                        largeMedia = true;
+                    } else {
+                        largeMedia = file.length() > 100 * 1024 * 1024;
                     }
                 }
 
-                if (tcpShortLink || (file.length() > 100 * 1024 * 1024 && isSupportBigFilesUpload())) {
+                if (largeMedia) {
                     uploadBigFile(-1, file.getName(), mediaPath, null, mediaType, null, new UploadMediaCallback() {
                         @Override
                         public void onSuccess(String result) {
@@ -3671,6 +3716,13 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
             ProtoLogic.checkSignature();
         }
 
+        @Override
+        public void setHeartBeatInterval(int second) throws RemoteException {
+            if (second > 0) {
+                StnLogic.setHeartBeatInterval(second);
+            }
+        }
+
         @Override
         public String getProtoRevision() throws RemoteException {
             return ProtoLogic.getProtoRevision();
@@ -3792,7 +3844,9 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
                 ashmenHolder.readBytes(data, 0, length);
                 data = ProtoLogic.decodeSecretChatData(targetId, data);
                 ashmenHolder.writeBytes(data, 0, data.length);
+                if(!isMainProcess()){
                 ashmenHolder.close();
+                }
                 if (callback != null) {
                     callback.onSuccess(data.length);
                 }
@@ -3804,6 +3858,45 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
             }
         }
 
+        @Override
+        public void sendMomentsRequest(String targetId, AshmenWrapper ashmenHolder, int length, IGeneralCallbackInt callback) throws RemoteException {
+            try {
+                byte[] data = new byte[length];
+                ashmenHolder.readBytes(data, 0, length);
+                ProtoLogic.sendMomentsRequest(targetId, data, new ProtoLogic.IGeneralCallback4() {
+                    @Override
+                    public void onSuccess(byte[] bytes) {
+                        ashmenHolder.writeBytes(bytes, 0, bytes.length);
+                        if(!isMainProcess()){
+                            ashmenHolder.close();
+                        }
+                        if (callback != null) {
+                            try {
+                                callback.onSuccess(bytes.length);
+                            } catch (RemoteException e) {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void onFailure(int i) {
+                        if (callback != null) {
+                            try {
+                                callback.onFailure(i);
+                            } catch (RemoteException e) {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+                });
+            } catch (Exception e) {
+                e.printStackTrace();
+                if (callback != null) {
+                    callback.onFailure(-1);
+                }
+            }
+        }
         public void setDefaultPortraitProviderClass(String clazzName) {
             try {
                 Class cls = Class.forName(clazzName);
@@ -3886,6 +3979,9 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
         channelInfo.name = protoChannelInfo.getName();
         channelInfo.desc = protoChannelInfo.getDesc();
         channelInfo.portrait = protoChannelInfo.getPortrait();
+        if (!TextUtils.isEmpty(channelInfo.portrait)) {
+            channelInfo.portrait = ClientService.urlRedirect(channelInfo.portrait);
+        }
         channelInfo.extra = protoChannelInfo.getExtra();
         channelInfo.owner = protoChannelInfo.getOwner();
         channelInfo.status = protoChannelInfo.getStatus();
@@ -3962,6 +4058,9 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
         groupInfo.deleted = protoGroupInfo.getDeleted();
 
         groupInfo.portrait = protoGroupInfo.getPortrait();
+        if (!TextUtils.isEmpty(groupInfo.portrait)) {
+            groupInfo.portrait = ClientService.urlRedirect(groupInfo.portrait);
+        }
         if (TextUtils.isEmpty(groupInfo.portrait) && defaultPortraitProvider != null) {
             ProtoGroupMember[] protoGroupMembers = ProtoLogic.getGroupMembersByCount(protoGroupInfo.getTarget(), 9);
             String[] memberIds = new String[protoGroupMembers.length];
@@ -4054,6 +4153,9 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
         userInfo.groupAlias = protoUserInfo.getGroupAlias();
 
         userInfo.portrait = protoUserInfo.getPortrait();
+        if (!TextUtils.isEmpty(userInfo.portrait)) {
+            userInfo.portrait = ClientService.urlRedirect(userInfo.portrait);
+        }
         if (TextUtils.isEmpty(userInfo.portrait) && defaultPortraitProvider != null) {
             userInfo.portrait = defaultPortraitProvider.userDefaultPortrait(userInfo);
         }
@@ -4228,6 +4330,7 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
         ProtoLogic.setSecretChatStateCallback(ClientService.this);
         ProtoLogic.setSecretMessageBurnStateCallback(ClientService.this);
         ProtoLogic.setDomainInfoUpdateCallback(ClientService.this);
+        ProtoLogic.setSortAddressCallback(ClientService.this);
         Log.i(TAG, "Proto connect:" + userName);
         ProtoLogic.setAuthInfo(userName, userPwd);
         return ProtoLogic.connect(mHost);
@@ -4807,6 +4910,21 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
             onDomainInfoUpdatedCallbackList.finishBroadcast();
         });
     }
+    @Override
+    public boolean onSortAddress(boolean longlink, ProtoAddressItem[] protoAddressItems) {
+        boolean changed = false;
+// 当有多个可连网络时,这里可以进行排序。只能排序和删除,不能添加。
+//        for (int i = 1; i < protoAddressItems.length; i++) {
+//            if(protoAddressItems[i].getPort() == 1886) {
+//                ProtoAddressItem temp = protoAddressItems[0];
+//                protoAddressItems[0] = protoAddressItems[i];
+//                protoAddressItems[i] = temp;
+//                changed = true;
+//                break;
+//            }
+//        }
+        return changed;
+    }
 //    // 只是大概大小
 //    private int getMessageLength(ProtoMessage message) {
 //        int length = 0;
@@ -4863,6 +4981,11 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
         if (parcelables == null || parcelables.length == 0) {
             return entry;
         }
+        if (isMainProcess()) {
+            entry.entries.addAll(Arrays.asList(parcelables));
+            entry.index = parcelables.length - 1;
+            return entry;
+        }
 
         for (int i = startIndex; i < parcelables.length; i++) {
             T parcelable = parcelables[i];
@@ -4939,7 +5062,18 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
         }
         RequestBody fileBody = new UploadFileRequestBody(requestBody, callback::onProgress);
 
-        Request request = new Request.Builder().url(uploadUrl).put(fileBody).build();
+        Request.Builder builder = new Request.Builder();
+        builder.url(uploadUrl);
+        builder.put(fileBody);
+        if (!protoHttpHeaderMap.isEmpty()) {
+            for (Map.Entry<String, String> entry : protoHttpHeaderMap.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+        if (!TextUtils.isEmpty(protoUserAgent)) {
+            builder.removeHeader("User-Agent").addHeader("User-Agent", protoUserAgent);
+        }
+        Request request = builder.build();
         Call call = okHttpClient.newCall(request);
         call.enqueue(new Callback() {
             @Override
@@ -4996,9 +5130,19 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
         mb.addFormDataPart("file", "fileName", fileBody);
         mb.setType(MediaType.parse("multipart/form-data"));
         RequestBody body = mb.build();
-        Request.Builder requestBuilder = new Request.Builder().url(uploadUrl).post(body);
 
-        Call call = okHttpClient.newCall(requestBuilder.build());
+        Request.Builder builder = new Request.Builder();
+        builder.url(uploadUrl);
+        builder.post(body);
+        if (!protoHttpHeaderMap.isEmpty()) {
+            for (Map.Entry<String, String> entry : protoHttpHeaderMap.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+        if (!TextUtils.isEmpty(protoUserAgent)) {
+            builder.removeHeader("User-Agent").addHeader("User-Agent", protoUserAgent);
+        }
+        Call call = okHttpClient.newCall(builder.build());
         call.enqueue(new Callback() {
             @Override
             public void onFailure(Call call, IOException e) {
@@ -5039,4 +5183,8 @@ public class ClientService extends Service implements SdtLogic.ICallBack,
         }
     }
 
+    // 判断是否是单进程模式,单进程时,一次性回调所有内容
+    public boolean isMainProcess() {
+        return Binder.getCallingPid() == Process.myPid();
+    }
 }

+ 33 - 1
uni-Android-SDK/client/src/main/java/cn/wildfirechat/message/ConferenceInviteMessageContent.java

@@ -29,14 +29,18 @@ public class ConferenceInviteMessageContent extends MessageContent {
     private long startTime;
     private boolean audioOnly;
     private boolean audience;
+    // 会议PIN码,加入会议时使用
     private String pin;
+    // 会议密码,查询会议时使用
+    private String password;
     private boolean advanced;
+    private String callExtra;
 
 
     public ConferenceInviteMessageContent() {
     }
 
-    public ConferenceInviteMessageContent(String callId, String host, String title, String desc, long startTime, boolean audioOnly, boolean audience, boolean advanced, String pin) {
+    public ConferenceInviteMessageContent(String callId, String host, String title, String desc, long startTime, boolean audioOnly, boolean audience, boolean advanced, String pin, String password) {
         this.callId = callId;
         this.host = host;
         this.title = title;
@@ -46,6 +50,7 @@ public class ConferenceInviteMessageContent extends MessageContent {
         this.audience = audience;
         this.advanced = advanced;
         this.pin = pin;
+        this.password = password;
     }
 
     public String getCallId() {
@@ -121,6 +126,21 @@ public class ConferenceInviteMessageContent extends MessageContent {
         this.advanced = advanced;
     }
 
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public String getCallExtra() {
+        return callExtra;
+    }
+
+    public void setCallExtra(String callExtra) {
+        this.callExtra = callExtra;
+    }
     @Override
     public MessagePayload encode() {
         MessagePayload payload = super.encode();
@@ -145,6 +165,12 @@ public class ConferenceInviteMessageContent extends MessageContent {
             if (desc != null) {
                 objWrite.put("d", desc);
             }
+            if (password != null) {
+                objWrite.put("pwd", password);
+            }
+            if (callExtra != null) {
+                objWrite.put("ce", callExtra);
+            }
 
             objWrite.put("audience", audience?1:0);
             objWrite.put("advanced", advanced?1:0);
@@ -174,6 +200,8 @@ public class ConferenceInviteMessageContent extends MessageContent {
                 title = jsonObject.optString("t");
                 desc = jsonObject.optString("d");
                 pin = jsonObject.optString("p");
+                password = jsonObject.optString("pwd");
+                callExtra = jsonObject.optString("ce");
                 startTime = jsonObject.optLong("s");
                 audience = jsonObject.optInt("audience")>0;
                 advanced = jsonObject.optInt("advanced")>0;
@@ -207,6 +235,8 @@ public class ConferenceInviteMessageContent extends MessageContent {
         dest.writeByte(audience ? (byte) 1 : (byte) 0);
         dest.writeByte(advanced ? (byte) 1 : (byte) 0);
         dest.writeString(pin!=null?pin:"");
+        dest.writeString(password != null ? password : "");
+        dest.writeString(callExtra != null ? callExtra : "");
     }
 
     protected ConferenceInviteMessageContent(Parcel in) {
@@ -221,6 +251,8 @@ public class ConferenceInviteMessageContent extends MessageContent {
         audience = in.readByte() != 0;
         advanced = in.readByte() != 0;
         pin = in.readString();
+        password = in.readString();
+        callExtra = in.readString();
     }
 
     public static final Creator<ConferenceInviteMessageContent> CREATOR = new Creator<ConferenceInviteMessageContent>() {

+ 1 - 0
uni-Android-SDK/client/src/main/java/cn/wildfirechat/model/ClientState.java

@@ -9,6 +9,7 @@ import android.os.Parcelable;
 
 public class ClientState implements Parcelable {
     private int platform;
+    //设备的在线状态,0是在线,1是有session但不在线,其它不在线。
     private int state;
     private long lastSeen;
 

+ 12 - 0
uni-Android-SDK/client/src/main/java/cn/wildfirechat/model/UserIdNamePortrait.java

@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2025 WildFireChat. All rights reserved.
+ */
+
+package cn.wildfirechat.model;
+
+public class UserIdNamePortrait {
+    public String userId;
+    public String name;
+    public String portrait;
+
+}

+ 376 - 175
uni-Android-SDK/client/src/main/java/cn/wildfirechat/remote/ChatManager.java

@@ -215,6 +215,7 @@ public class ChatManager {
     private String userId;
     private String token;
     private Handler mainHandler;
+    private Handler internalWorkHandler;
     private Handler workHandler;
     private String deviceToken;
     private String clientId;
@@ -246,6 +247,8 @@ public class ChatManager {
     private boolean checkSignature = false;
     private boolean defaultSilentWhenPCOnline = true;
 
+    //心跳间隔,单位为秒。
+    private int heartBeatInterval = -1;
     private Socks5ProxyInfo proxyInfo;
 
     private boolean isBackground = true;
@@ -286,6 +289,9 @@ public class ChatManager {
 
     private Class<? extends DefaultPortraitProvider> defaultPortraitProviderClazz;
     private Class<? extends UrlRedirector> urlRedirectorClazz;
+    private UrlRedirector urlRedirector;
+
+    private boolean isClientServiceInMainProcess = false;
     public enum SearchUserType {
         //模糊搜索displayName,精确搜索name或电话号码
         General(0),
@@ -334,7 +340,38 @@ public class ChatManager {
             }
             return searchUserType;
         }
+    }
+
+    public enum UserSearchUserType {
+        //搜索所有,包括普通用户和机器人。
+        All(0),
+
+        //只搜索普通用户
+        OnlyUser(1),
+
+        //只搜索机器人
+        OnlyRobot(2);
+
+        UserSearchUserType(int value) {
+        }
 
+        public static UserSearchUserType type(int type) {
+            UserSearchUserType userSearchUserType = null;
+            switch (type) {
+                case 0:
+                    userSearchUserType = All;
+                    break;
+                case 1:
+                    userSearchUserType = OnlyUser;
+                    break;
+                case 2:
+                    userSearchUserType = OnlyRobot;
+                    break;
+                default:
+                    throw new IllegalArgumentException("type " + userSearchUserType + " is invalid");
+            }
+            return userSearchUserType;
+        }
     }
 
     /**
@@ -447,9 +484,12 @@ public class ChatManager {
         INST.userInfoCache = new LruCache<>(1024);
         INST.groupMemberCache = new LruCache<>(1024);
         INST.userOnlineStateMap = new ConcurrentHashMap<>();
-        HandlerThread thread = new HandlerThread("workHandler");
-        thread.start();
-        INST.workHandler = new Handler(thread.getLooper());
+        HandlerThread internalHandlerthread = new HandlerThread("internalWorkHandler");
+        internalHandlerthread.start();
+        HandlerThread workHandlerThread = new HandlerThread("workHandler");
+        workHandlerThread.start();
+        INST.internalWorkHandler = new Handler(internalHandlerthread.getLooper());
+        INST.workHandler = new Handler(workHandlerThread.getLooper());
         ProcessLifecycleOwner.get().getLifecycle().addObserver(new LifecycleObserver() {
             @OnLifecycleEvent(Lifecycle.Event.ON_START)
             public void onForeground() {
@@ -501,7 +541,7 @@ public class ChatManager {
     /**
      * 获取当前的连接状态
      *
-     * @return 连接状态,参考{@link cn.wildfirechat.client.ConnectionStatus}
+     * @return 连接状态,参考{@link ConnectionStatus}
      */
     public int getConnectionStatus() {
         return connectionStatus;
@@ -509,7 +549,6 @@ public class ChatManager {
 
     /**
      * App在后台时,如果需要强制连上服务器并收消息,调用此方法。后台时无法保证长时间连接中。
-     *
      */
     public void forceConnect() {
         if (mClient != null) {
@@ -1271,6 +1310,25 @@ public class ChatManager {
         }
     }
 
+    /**
+     * 设置自定心跳间隔。单位是秒,取值范围为10-2400。一般不建议修改,除非是特殊场景。
+     *
+     * @param second 心跳间隔的秒数
+     */
+    public void setHeartBeatInterval(int second) {
+        heartBeatInterval = second;
+        if (!checkRemoteService()) {
+            return;
+        }
+
+        try {
+            mClient.setHeartBeatInterval(second);
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+    }
+
+
     /**
      * 获取clientId, 野火IM用clientId唯一表示用户设备
      */
@@ -2202,7 +2260,7 @@ public class ChatManager {
     }
 
     /**
-     * 更新消息内容
+     * 更新消息内容。不支持聊天室,聊天室消息本地不存储,所有无法更新。
      *
      * @param messageId     消息id
      * @param newMsgContent 新的消息体,未更新部分,不可置空!
@@ -2534,6 +2592,10 @@ public class ChatManager {
                     msg.messageUid = messageUid;
                     msg.serverTime = timestamp;
                     msg.status = MessageStatus.Sent;
+                    if (msg.content instanceof MediaMessageContent && urlRedirector != null) {
+                        String remoteUrl = ((MediaMessageContent) msg.content).remoteUrl;
+                        ((MediaMessageContent) msg.content).remoteUrl = urlRedirector.urlRedirect(remoteUrl);
+                    }
                     mainHandler.post(new Runnable() {
                         @Override
                         public void run() {
@@ -2736,6 +2798,10 @@ public class ChatManager {
                     msg.messageUid = messageUid;
                     msg.serverTime = timestamp;
                     msg.status = MessageStatus.Sent;
+                    if (msg.content instanceof MediaMessageContent && urlRedirector != null) {
+                        String remoteUrl = ((MediaMessageContent) msg.content).remoteUrl;
+                        ((MediaMessageContent) msg.content).remoteUrl = urlRedirector.urlRedirect(remoteUrl);
+                    }
                     mainHandler.post(new Runnable() {
                         @Override
                         public void run() {
@@ -2861,15 +2927,11 @@ public class ChatManager {
                 @Override
                 public void onSuccess() throws RemoteException {
                     Message recallMsg = mClient.getMessage(msg.messageId);
-                    if (recallMsg == null) {
-                        msg.messageId = 0;
-                        msg.conversation = null;
-                        msg.content = null;
-                    } else {
-                        if (msg.messageId > 0) {
+                    if (recallMsg != null) {
                             msg.content = recallMsg.content;
                             msg.sender = recallMsg.sender;
                             msg.serverTime = recallMsg.serverTime;
+                        msg.messageUid = recallMsg.messageUid;
                         } else {
                             MessagePayload payload = msg.content.encode();
                             RecallMessageContent recallCnt = new RecallMessageContent();
@@ -2886,8 +2948,7 @@ public class ChatManager {
                             msg.sender = userId;
                             msg.serverTime = System.currentTimeMillis();
                         }
-                    }
-                    mainHandler.post(() -> {
+                    	mainHandler.post(() -> {
                         if (callback != null) {
                             callback.onSuccess();
                         }
@@ -3025,28 +3086,30 @@ public class ChatManager {
             inlines[j] = lines.get(j);
         }
 
-        try {
-            List<ConversationInfo> convs = new ArrayList<>();
-            mClient.getConversationListAsync(intypes, inlines, new IGetConversationListCallback.Stub() {
-                @Override
-                public void onSuccess(List<ConversationInfo> infos, boolean hasMore) throws RemoteException {
-                    convs.addAll(infos);
-                    if (!hasMore) {
-                        mainHandler.post(() -> {
-                            callback.onSuccess(convs);
-                        });
+        internalWorkHandler.post(() -> {
+            try {
+                List<ConversationInfo> convs = new ArrayList<>();
+                mClient.getConversationListAsync(intypes, inlines, new IGetConversationListCallback.Stub() {
+                    @Override
+                    public void onSuccess(List<ConversationInfo> infos, boolean hasMore) throws RemoteException {
+                        convs.addAll(infos);
+                        if (!hasMore) {
+                            mainHandler.post(() -> {
+                                callback.onSuccess(convs);
+                            });
+                        }
                     }
-                }
-
-                @Override
-                public void onFailure(int errorCode) throws RemoteException {
-                    mainHandler.post(() -> callback.onFail(errorCode));
-                }
-            });
-        } catch (RemoteException e) {
-            e.printStackTrace();
-            callback.onFail(-1);
+      
+                    @Override
+                    public void onFailure(int errorCode) throws RemoteException {
+                        mainHandler.post(() -> callback.onFail(errorCode));
+                    }
+                });
+            } catch (RemoteException e) {
+                e.printStackTrace();
+                callback.onFail(-1);
         }
+        });
     }
 
     /**
@@ -3100,7 +3163,7 @@ public class ChatManager {
      * @param fromIndex    消息起始id(messageId)
      * @param before       true, 获取fromIndex之前的消息,即更旧的消息;false,获取fromIndex之后的消息,即更新的消息。都不包含fromIndex对应的消息
      * @param count        获取消息条数
-     * @param withUser     只有会话类型为{@link cn.wildfirechat.model.Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
+     * @param withUser     只有会话类型为{@link Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
      * @return 由于ipc大小限制,本接口获取到的消息列表可能不完整,并且超级群里面,可能返回为完全加载的消息,请使用异步获取
      */
     @Deprecated
@@ -3126,7 +3189,7 @@ public class ChatManager {
      * @param fromIndex    消息起始id(messageId)
      * @param before       true, 获取fromIndex之前的消息,即更旧的消息;false,获取fromIndex之后的消息,即更新的消息。都不包含fromIndex对应的消息
      * @param count        获取消息条数
-     * @param withUser     只有会话类型为{@link cn.wildfirechat.model.Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
+     * @param withUser          只有会话类型为{@link Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
      * @return 由于ipc大小限制,本接口获取到的消息列表可能不完整,并且超级群里面,可能返回为完全加载的消息,请使用异步获取
      */
     @Deprecated()
@@ -3165,7 +3228,7 @@ public class ChatManager {
      * @param fromIndex    消息起始id(messageId)
      * @param before       true, 获取fromIndex之前的消息,即更旧的消息;false,获取fromIndex之后的消息,即更新的消息。都不包含fromIndex对应的消息
      * @param count        获取消息条数
-     * @param withUser     只有会话类型为{@link cn.wildfirechat.model.Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
+     * @param withUser          只有会话类型为{@link Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
      * @return 由于ipc大小限制,本接口获取到的消息列表可能不完整,并且超级群里面,可能返回为完全加载的消息,请使用异步获取
      */
     @Deprecated
@@ -3206,7 +3269,7 @@ public class ChatManager {
      * @param fromIndex    消息起始id(messageId)
      * @param before       true, 获取fromIndex之前的消息,即更旧的消息;false,获取fromIndex之后的消息,即更新的消息。都不包含fromIndex对应的消息
      * @param count        获取消息条数
-     * @param withUser     只有会话类型为{@link cn.wildfirechat.model.Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
+     * @param withUser     只有会话类型为{@link Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
      * @param callback     消息回调,当消息比较多,或者消息体比较大时,可能会回调多次
      */
     public void getMessages(Conversation conversation, long fromIndex, boolean before, int count, String withUser, GetMessageCallback callback) {
@@ -3288,7 +3351,7 @@ public class ChatManager {
      * @param fromIndex    消息起始id(messageId)
      * @param before       true, 获取fromIndex之前的消息,即更旧的消息;false,获取fromIndex之后的消息,即更新的消息。都不包含fromIndex对应的消息
      * @param count        获取消息条数
-     * @param withUser     只有会话类型为{@link cn.wildfirechat.model.Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
+     * @param withUser     只有会话类型为{@link Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
      * @param callback     消息回调,当消息比较多,或者消息体比较大时,可能会回调多次
      */
     public void getMessages(Conversation conversation, List<Integer> contentTypes, long fromIndex, boolean before, int count, String withUser, GetMessageCallback callback) {
@@ -3330,7 +3393,7 @@ public class ChatManager {
      * @param fromIndex     消息起始id(messageId)
      * @param before        true, 获取fromIndex之前的消息,即更旧的消息;false,获取fromIndex之后的消息,即更新的消息。都不包含fromIndex对应的消息
      * @param count         获取消息条数
-     * @param withUser      只有会话类型为{@link cn.wildfirechat.model.Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
+     * @param withUser      只有会话类型为{@link Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
      */
     @Deprecated
     public List<Message> getMessagesByMessageStatus(Conversation conversation, List<Integer> messageStatus, long fromIndex, boolean before, int count, String withUser) {
@@ -3350,7 +3413,7 @@ public class ChatManager {
      * @param fromIndex     消息起始id(messageId)
      * @param before        true, 获取fromIndex之前的消息,即更旧的消息;false,获取fromIndex之后的消息,即更新的消息。都不包含fromIndex对应的消息
      * @param count         获取消息条数
-     * @param withUser      只有会话类型为{@link cn.wildfirechat.model.Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
+     * @param withUser      只有会话类型为{@link Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
      * @param callback      消息回调,当消息比较多,或者消息体比较大时,可能会回调多次
      */
     public void getMessagesByMessageStatus(Conversation conversation, List<Integer> messageStatus, long fromIndex, boolean before, int count, String withUser, GetMessageCallback callback) {
@@ -3389,7 +3452,7 @@ public class ChatManager {
      * @param fromIndex         消息起始id(messageId)
      * @param before            true, 获取fromIndex之前的消息,即更旧的消息;false,获取fromIndex之后的消息,即更新的消息。都不包含fromIndex对应的消息
      * @param count             获取消息条数
-     * @param withUser          只有会话类型为{@link cn.wildfirechat.model.Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
+     * @param withUser          只有会话类型为{@link Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
      * @param callback          消息回调,当消息比较多,或者消息体比较大时,可能会回调多次
      */
     public void getMessagesEx(List<Conversation.ConversationType> conversationTypes, List<Integer> lines, List<Integer> contentTypes, long fromIndex, boolean before, int count, String withUser, GetMessageCallback callback) {
@@ -3443,7 +3506,7 @@ public class ChatManager {
      * @param fromIndex         消息起始id(messageId)
      * @param before            true, 获取fromIndex之前的消息,即更旧的消息;false,获取fromIndex之后的消息,即更新的消息。都不包含fromIndex对应的消息
      * @param count             获取消息条数
-     * @param withUser          只有会话类型为{@link cn.wildfirechat.model.Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
+     * @param withUser          只有会话类型为{@link Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
      * @param callback          消息回调,当消息比较多,或者消息体比较大时,可能会回调多次
      */
     public void getMessagesEx2(List<Conversation.ConversationType> conversationTypes, List<Integer> lines, List<MessageStatus> messageStatus, long fromIndex, boolean before, int count, String withUser, GetMessageCallback callback) {
@@ -3500,7 +3563,7 @@ public class ChatManager {
      * @param timestamp    时间戳
      * @param before       true, 获取fromIndex之前的消息,即更旧的消息;false,获取fromIndex之后的消息,即更新的消息。都不包含fromIndex对应的消息
      * @param count        获取消息条数
-     * @param withUser     只有会话类型为{@link cn.wildfirechat.model.Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
+     * @param withUser     只有会话类型为{@link Conversation.ConversationType#Channel}时生效, channel主用来查询和某个用户的所有消息
      * @param callback     消息回调,当消息比较多,或者消息体比较大时,可能会回调多次
      */
     public void getMessagesByTimestamp(Conversation conversation, List<Integer> contentTypes, long timestamp, boolean before, int count, String withUser, GetMessageCallback callback) {
@@ -3963,9 +4026,9 @@ public class ChatManager {
      * @param messageId 消息id
      *                  <p>
      *                  消息uid。消息uid和消息id的区别是,每条消息都有uid,该uid由服务端生成,全局唯一;id是本地生成,
-     *                  切和消息的存储类型{@link cn.wildfirechat.message.core.PersistFlag}相关,只有存储类型为
-     *                  {@link cn.wildfirechat.message.core.PersistFlag#Persist_And_Count}
-     *                  和{@link cn.wildfirechat.message.core.PersistFlag#Persist}的消息,有消息id
+     *                  且和消息的存储类型{@link PersistFlag}相关,只有存储类型为
+     *                  {@link PersistFlag#Persist_And_Count}
+     *                  和{@link PersistFlag#Persist}的消息,有消息id
      * @return
      */
     public Message getMessage(long messageId) {
@@ -3988,9 +4051,9 @@ public class ChatManager {
      *                   <p>
      *                   消息uid和消息id的区别是,每条消息都有uid,该uid由服务端生成,全局唯一;id是本地生成,
      *                   <p>
-     *                   切和消息的存储类型{@link cn.wildfirechat.message.core.PersistFlag}相关,只有存储类型为
-     *                   {@link cn.wildfirechat.message.core.PersistFlag#Persist_And_Count}
-     *                   和{@link cn.wildfirechat.message.core.PersistFlag#Persist}的消息,有消息id
+     *                   切和消息的存储类型{@link PersistFlag}相关,只有存储类型为
+     *                   {@link PersistFlag#Persist_And_Count}
+     *                   和{@link PersistFlag#Persist}的消息,有消息id
      * @return
      */
     public Message getMessageByUid(long messageUid) {
@@ -4650,6 +4713,58 @@ public class ChatManager {
         }
     }
 
+    /**
+     * 搜索用户
+     *
+     * @param domainId
+     * @param keyword
+     * @param searchUserType
+     * @param page
+     * @param callback
+     */
+    public void searchUserEx2(String domainId, String keyword, SearchUserType searchUserType, UserSearchUserType userType, int page, final SearchUserCallback callback) {
+        if (userSource != null) {
+            userSource.searchUser(keyword, callback);
+            return;
+        }
+        if (!checkRemoteService()) {
+            if (callback != null)
+                callback.onFail(ErrorCode.SERVICE_DIED);
+            return;
+        }
+
+        try {
+            mClient.searchUserEx2(domainId, keyword, searchUserType.ordinal(), userType.ordinal(), page, new cn.wildfirechat.client.ISearchUserCallback.Stub() {
+                @Override
+                public void onSuccess(final List<UserInfo> userInfos) throws RemoteException {
+                    if (callback != null) {
+                        mainHandler.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                callback.onSuccess(userInfos);
+                            }
+                        });
+                    }
+                }
+
+                @Override
+                public void onFailure(final int errorCode) throws RemoteException {
+                    if (callback != null) {
+                        mainHandler.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                callback.onFail(errorCode);
+                            }
+                        });
+                    }
+                }
+            });
+        } catch (RemoteException e) {
+            e.printStackTrace();
+            if (callback != null)
+                mainHandler.post(() -> callback.onFail(ErrorCode.SERVICE_EXCEPTION));
+        }
+    }
     /**
      * 判断是否是好友关系
      *
@@ -4817,6 +4932,7 @@ public class ChatManager {
             mClient.setFriendAlias(userId, alias, new IGeneralCallback.Stub() {
                 @Override
                 public void onSuccess() throws RemoteException {
+                    userInfoCache.remove(userId);
                     if (callback != null) {
                         mainHandler.post(callback::onSuccess);
                     }
@@ -4872,7 +4988,7 @@ public class ChatManager {
             List<String> userIds = mClient.getMyFriendList(refresh);
             if (userIds != null && !userIds.isEmpty()) {
                 List<UserInfo> userInfos = new ArrayList<>();
-                int step = 400;
+                int step = isClientServiceInMainProcess ? Integer.MAX_VALUE : 400;
                 int startIndex, endIndex;
                 for (int i = 0; i <= userIds.size() / step; i++) {
                     startIndex = i * step;
@@ -4911,34 +5027,37 @@ public class ChatManager {
             }
             return;
         }
-        try {
-            List<UserInfo> userInfos = new ArrayList<>();
-            mClient.getFriendUserInfoListAsync(refresh, new IGetUserInfoListCallback.Stub() {
-                @Override
-                public void onSuccess(List<UserInfo> infos, boolean hasMore) throws RemoteException {
-                    if (callback == null) {
-                        return;
-                    }
-                    userInfos.addAll(infos);
-                    if (!hasMore) {
-                        mainHandler.post(() -> callback.onSuccess(userInfos));
+        internalWorkHandler.post(() -> {
+            try {
+                List<UserInfo> userInfos = new ArrayList<>();
+                mClient.getFriendUserInfoListAsync(refresh, new IGetUserInfoListCallback.Stub() {
+                    @Override
+                    public void onSuccess(List<UserInfo> infos, boolean hasMore) throws RemoteException {
+                        if (callback == null) {
+                            return;
+                        }
+                        userInfos.addAll(infos);
+                        if (!hasMore) {
+                            mainHandler.post(() -> callback.onSuccess(userInfos));
+                        }
                     }
-                }
 
-                @Override
-                public void onFailure(int errorCode) throws RemoteException {
-                    if (callback != null) {
-                        mainHandler.post(() -> callback.onFail(errorCode));
+                    @Override
+                    public void onFailure(int errorCode) throws RemoteException {
+                        if (callback != null) {
+                            mainHandler.post(() -> callback.onFail(errorCode));
+                        }
                     }
+                });
+            } catch (RemoteException e) {
+                e.printStackTrace();
+                if (callback != null) {
+                    mainHandler.post(() -> callback.onFail(-1));
                 }
-            });
-        } catch (RemoteException e) {
-            e.printStackTrace();
-            if (callback != null) {
-                mainHandler.post(() -> callback.onFail(-1));
             }
-        }
+        });
     }
+     
     /**
      * 从服务端加载好友请求
      */
@@ -5047,7 +5166,7 @@ public class ChatManager {
     /**
      * 删除好友请求
      *
-     * @param direction 方向,0是接收的,1是发出的
+     * @param direction  方向,true 是接收的,false 是发出的
      * @param beforeTime 删除某个时间之前的。如果删除所有用0.
      * @return 是否有数据被删除。
      */
@@ -5712,7 +5831,7 @@ public class ChatManager {
 
         try {
             List<UserInfo> userInfos = new ArrayList<>();
-            int step = 400;
+            int step = isClientServiceInMainProcess ? Integer.MAX_VALUE : 400;
             int startIndex, endIndex;
             for (int i = 0; i <= userIds.size() / step; i++) {
                 startIndex = i * step;
@@ -5763,34 +5882,37 @@ public class ChatManager {
             callback.onFail(-1);
             return;
         }
+ 
+        internalWorkHandler.post(() -> {
+            try {
+                List<UserInfo> userInfos = new ArrayList<>();
+                mClient.getUserInfosAsync(userIds, groupId, new IGetUserInfoListCallback.Stub() {
 
-        try {
-            List<UserInfo> userInfos = new ArrayList<>();
-            mClient.getUserInfosAsync(userIds, groupId, new IGetUserInfoListCallback.Stub() {
-
-                @Override
-                public void onSuccess(List<UserInfo> infos, boolean hasMore) throws RemoteException {
-                    userInfos.addAll(infos);
-                    if (callback != null && !hasMore) {
-                        mainHandler.post(() -> callback.onSuccess(userInfos));
+                    @Override
+                    public void onSuccess(List<UserInfo> infos, boolean hasMore) throws RemoteException {
+                        userInfos.addAll(infos);
+                        if (callback != null && !hasMore) {
+                            mainHandler.post(() -> callback.onSuccess(userInfos));
+                        }
                     }
-                }
 
-                @Override
-                public void onFailure(int errorCode) throws RemoteException {
-                    if (callback != null) {
-                        mainHandler.post(() -> callback.onFail(errorCode));
+                    @Override
+                    public void onFailure(int errorCode) throws RemoteException {
+                        if (callback != null) {
+                            mainHandler.post(() -> callback.onFail(errorCode));
+                        }
                     }
-                }
-            });
-        } catch (RemoteException e) {
-            e.printStackTrace();
-            callback.onFail(-1);
-        }
+                });
+            } catch (RemoteException e) {
+                e.printStackTrace();
+                callback.onFail(-1);
+            }
+        });
     }
 
     /**
      * 异步获取用户信息
+     *
      * @param userId
      * @param refresh
      * @param callback
@@ -5801,6 +5923,7 @@ public class ChatManager {
 
     /**
      * 异步获取用户信息。
+     *
      * @param userId
      * @param groupId
      * @param refresh
@@ -5838,7 +5961,7 @@ public class ChatManager {
      * 上传媒体文件
      *
      * @param mediaPath
-     * @param mediaType 媒体类型,可选值,参考{@link cn.wildfirechat.message.MessageContentMediaType}
+     * @param mediaType 媒体类型,可选值,参考{@link MessageContentMediaType}
      * @param callback
      */
     public void uploadMediaFile(String mediaPath, int mediaType, final UploadMediaCallback callback) {
@@ -5892,7 +6015,7 @@ public class ChatManager {
      * 开始 TCP 短链接之后,该方法不可用,请使用{@link ChatManager#uploadMediaFile}
      *
      * @param data      不能超过1M,为了安全,实际只有900K
-     * @param mediaType 媒体类型,可选值参考{@link cn.wildfirechat.message.MessageContentMediaType}
+     * @param mediaType 媒体类型,可选值参考{@link MessageContentMediaType}
      * @param callback
      */
     public void uploadMedia(String fileName, byte[] data, int mediaType, final GeneralCallback2 callback) {
@@ -5919,7 +6042,7 @@ public class ChatManager {
      * 开始 TCP 短链接之后,该方法不可用,请使用{@link ChatManager#uploadMediaFile}
      *
      * @param data      不能超过1M,为了安全,实际只有900K
-     * @param mediaType 媒体类型,可选值参考{@link cn.wildfirechat.message.MessageContentMediaType}
+     * @param mediaType 媒体类型,可选值参考{@link MessageContentMediaType}
      * @param callback
      */
     public void uploadMedia2(String fileName, byte[] data, int mediaType, final UploadMediaCallback callback) {
@@ -6476,6 +6599,7 @@ public class ChatManager {
 
     /**
      * 获取签名。IM服务可以开启签名保护,只有指定签名的android客户端才可以登录
+     *
      * @return
      * @throws PackageManager.NameNotFoundException
      * @throws CertificateException
@@ -7299,33 +7423,33 @@ public class ChatManager {
             return;
         }
 
-        workHandler.post(() -> {
-        try {
-            List<GroupMember> groupMemberList = new ArrayList<>();
+        internalWorkHandler.post(() -> {
+            try {
+                List<GroupMember> groupMemberList = new ArrayList<>();
                 mClient.getGroupMembersEx(groupId, forceUpdate, new IGetGroupMemberCallback.Stub() {
-                @Override
-                public void onSuccess(List<GroupMember> groupMembers, boolean hasMore) throws RemoteException {
-                    groupMemberList.addAll(groupMembers);
-                    if (!hasMore) {
-                        if (callback != null) {
-                            mainHandler.post(() -> callback.onSuccess(groupMemberList));
+                    @Override
+                    public void onSuccess(List<GroupMember> groupMembers, boolean hasMore) throws RemoteException {
+                        groupMemberList.addAll(groupMembers);
+                        if (!hasMore) {
+                            if (callback != null) {
+                                mainHandler.post(() -> callback.onSuccess(groupMemberList));
+                            }
                         }
                     }
-                }
-
-                @Override
-                public void onFailure(int errorCode) throws RemoteException {
-                    if (callback != null) {
-                        mainHandler.post(() -> callback.onFail(errorCode));
+     
+                    @Override
+                    public void onFailure(int errorCode) throws RemoteException {
+                        if (callback != null) {
+                            mainHandler.post(() -> callback.onFail(errorCode));
+                        }
                     }
+                });
+            } catch (RemoteException e) {
+                e.printStackTrace();
+                if (callback != null) {
+                    callback.onFail(-1);
                 }
-            });
-        } catch (RemoteException e) {
-            e.printStackTrace();
-            if (callback != null) {
-                callback.onFail(-1);
             }
-        }
         });
     }
 
@@ -7344,33 +7468,35 @@ public class ChatManager {
             }
             return;
         }
-        try {
-            List<UserInfo> userInfos = new ArrayList<>();
-            mClient.getGroupMemberUserInfosAsync(groupId, refresh, new IGetUserInfoListCallback.Stub() {
-                @Override
-                public void onSuccess(List<UserInfo> infos, boolean hasMore) throws RemoteException {
-                    if (callback == null) {
-                        return;
-                    }
-                    userInfos.addAll(infos);
-                    if (!hasMore) {
-                        mainHandler.post(() -> callback.onSuccess(userInfos));
+        internalWorkHandler.post(() -> {
+            try {
+                List<UserInfo> userInfos = new ArrayList<>();
+                mClient.getGroupMemberUserInfosAsync(groupId, refresh, new IGetUserInfoListCallback.Stub() {
+                    @Override
+                    public void onSuccess(List<UserInfo> infos, boolean hasMore) throws RemoteException {
+                        if (callback == null) {
+                            return;
+                        }
+                        userInfos.addAll(infos);
+                        if (!hasMore) {
+                            mainHandler.post(() -> callback.onSuccess(userInfos));
+                        }
                     }
-                }
-
-                @Override
-                public void onFailure(int errorCode) throws RemoteException {
-                    if (callback != null) {
-                        mainHandler.post(() -> callback.onFail(errorCode));
+   
+                    @Override
+                    public void onFailure(int errorCode) throws RemoteException {
+                        if (callback != null) {
+                            mainHandler.post(() -> callback.onFail(errorCode));
+                        }
                     }
+                });
+            } catch (RemoteException e) {
+                e.printStackTrace();
+                if (callback != null) {
+                    mainHandler.post(() -> callback.onFail(-1));
                 }
-            });
-        } catch (RemoteException e) {
-            e.printStackTrace();
-            if (callback != null) {
-                mainHandler.post(() -> callback.onFail(-1));
             }
-        }
+        });
     }
 
     private String groupMemberCacheKey(String groupId, String memberId) {
@@ -7800,6 +7926,41 @@ public class ChatManager {
         }
     }
 
+    public void sendMomentsRequest(String path, byte[] mediaData, GeneralCallbackBytes callback) {
+        if (!checkRemoteService()) {
+            return;
+        }
+        internalWorkHandler.post(() -> {
+            try {
+                AshmenWrapper ashmenWrapper = AshmenWrapper.create(path, 1024 * 1024);
+                ashmenWrapper.writeBytes(mediaData, 0, mediaData.length);
+                mClient.sendMomentsRequest(path, ashmenWrapper, mediaData.length, new IGeneralCallbackInt.Stub() {
+                    @Override
+                    public void onSuccess(int length) throws RemoteException {
+                        if (callback != null) {
+                            byte[] data = new byte[length];
+                            try {
+                                ashmenWrapper.readBytes(data, 0, length);
+                                callback.onSuccess(data);
+                            } finally {
+                                ashmenWrapper.close();
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void onFailure(int errorCode) throws RemoteException {
+                        if (callback != null) {
+                            callback.onFail(errorCode);
+                        }
+                        ashmenWrapper.close();
+                    }
+                });
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+        });
+    }
     // connect host
     public String getHost() {
         if (!checkRemoteService()) {
@@ -7894,7 +8055,7 @@ public class ChatManager {
             callback.onFail(ErrorCode.SERVICE_DIED);
             return;
         }
-        workHandler.post(() -> {
+        internalWorkHandler.post(() -> {
             Map<String, String> groupIdMap = getUserSettings(UserSettingScope.FavoriteGroup);
             List<GroupInfo> groups = new ArrayList<>();
             if (groupIdMap != null && !groupIdMap.isEmpty()) {
@@ -7913,6 +8074,7 @@ public class ChatManager {
 
     /**
      * 是否是收藏群组
+     *
      * @param groupId
      * @return
      */
@@ -7930,6 +8092,7 @@ public class ChatManager {
 
     /**
      * 设置收藏群组
+     *
      * @param groupId
      * @param isSet
      * @param callback
@@ -7958,7 +8121,7 @@ public class ChatManager {
             callback.onFail(ErrorCode.SERVICE_DIED);
             return;
         }
-        workHandler.post(() -> {
+        internalWorkHandler.post(() -> {
             Map<String, String> userIdMap = getUserSettings(UserSettingScope.FavoriteUser);
             List<String> userIds = new ArrayList<>();
             if (userIdMap != null && !userIdMap.isEmpty()) {
@@ -7971,7 +8134,7 @@ public class ChatManager {
             mainHandler.post(() -> callback.onSuccess(userIds));
         });
     }
-
+        
     /**
      * 是否是星标用户
      *
@@ -8410,6 +8573,7 @@ public class ChatManager {
 
     /**
      * IM服务群组是否开启了已读报告功能
+     *
      * @return
     */
     public boolean isGroupReceiptEnabled() {
@@ -8432,6 +8596,7 @@ public class ChatManager {
 
     /**
      * IM服务是否支持密聊
+     *
      * @return
      */
     public boolean isEnableSecretChat() {
@@ -8448,6 +8613,7 @@ public class ChatManager {
 
     /**
      * IM服务是否开启了互联功能
+     *
      * @return
      */
     public boolean isEnableMesh() {
@@ -8653,6 +8819,23 @@ public class ChatManager {
                 }
             }
         }
+        String xlogMMapDir = gContext.getFilesDir().getAbsolutePath() + "/xlog";
+        subFile = new File(xlogMMapDir).listFiles();
+        if (subFile != null) {
+            for (File file : subFile) {
+                //wflog为ChatService中定义的,如果修改需要对应修改
+                if (file.isFile() && file.getName().startsWith("wflog_")) {
+                    paths.add(file.getAbsolutePath());
+                }
+            }
+        }
+
+        // sort paths
+        Collections.sort(paths, (f1, f2) -> {
+            String n1 = f1.substring(f1.lastIndexOf("/") + 1);
+            String n2 = f2.substring(f2.lastIndexOf("/") + 1);
+            return n1.compareTo(n2);
+        });
         return paths;
     }
 
@@ -9191,6 +9374,7 @@ public class ChatManager {
 
     /**
      * 是否支持大文件上传。只有专业版支持,当支持大文件上传时,使用getUploadUrl获取到上传链接,然后在应用层上传。
+     *
      * @return
      */
     public boolean isSupportBigFilesUpload() {
@@ -9460,40 +9644,42 @@ public class ChatManager {
         if (!checkRemoteService()) {
             return;
         }
-        try {
-            AshmenWrapper ashmenWrapper = AshmenWrapper.create(targetId, mediaData.length);
-            ashmenWrapper.writeBytes(mediaData, 0, mediaData.length);
-            AshmenWrapper finalAshmenHolder = ashmenWrapper;
-            mClient.decodeSecretChatDataAsync(targetId, ashmenWrapper, mediaData.length, new IGeneralCallbackInt.Stub() {
-                @Override
-                public void onSuccess(int length) throws RemoteException {
-                    if (callback != null) {
-                        // TODO ByteArrayOutputStream
-                        byte[] data = new byte[length];
-                        try {
-                            finalAshmenHolder.readBytes(data, 0, length);
-                            callback.onSuccess(data);
-                        } finally {
-                            finalAshmenHolder.close();
+        internalWorkHandler.post(() -> {
+            try {
+                AshmenWrapper ashmenWrapper = AshmenWrapper.create(targetId, mediaData.length);
+                ashmenWrapper.writeBytes(mediaData, 0, mediaData.length);
+                mClient.decodeSecretChatDataAsync(targetId, ashmenWrapper, mediaData.length, new IGeneralCallbackInt.Stub() {
+                    @Override
+                    public void onSuccess(int length) throws RemoteException {
+                        if (callback != null) {
+                            // TODO ByteArrayOutputStream
+                            byte[] data = new byte[length];
+                            try {
+                                ashmenWrapper.readBytes(data, 0, length);
+                                callback.onSuccess(data);
+                            } finally {
+                                ashmenWrapper.close();
+                            }
                         }
                     }
-                }
-
-                @Override
-                public void onFailure(int errorCode) throws RemoteException {
-                    if (callback != null) {
-                        callback.onFail(errorCode);
+  
+                    @Override
+                    public void onFailure(int errorCode) throws RemoteException {
+                        if (callback != null) {
+                            callback.onFail(errorCode);
+                        }
+                        ashmenWrapper.close();
                     }
-                    finalAshmenHolder.close();
-                }
-            });
-        } catch (RemoteException e) {
-            e.printStackTrace();
-        }
+                });
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+        });
     }
 
     /**
      * 设置密聊消息焚毁时间,单位秒
+     *
      * @param targetId
      * @param burnTime
      */
@@ -9511,6 +9697,7 @@ public class ChatManager {
 
     /**
      * 获取消息焚毁信息
+     *
      * @param messageId
      * @return
      */
@@ -9613,7 +9800,7 @@ public class ChatManager {
             }
             content.extra = payload.extra;
         } catch (Exception e) {
-            android.util.Log.e(TAG, "decode message error, fallback to unknownMessageContent. " + payload.type);
+            Log.e(TAG, "decode message error, fallback to unknownMessageContent. " + payload.type);
             e.printStackTrace();
             if (content == null) {
                 return null;
@@ -9651,6 +9838,11 @@ public class ChatManager {
      */
     public void setUrlRedirectorClazz(Class<? extends UrlRedirector> clazz) {
         this.urlRedirectorClazz = clazz;
+        try {
+            this.urlRedirector = clazz.newInstance();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
         if (mClient != null) {
             try {
                 mClient.setUrlRedirectorClass(clazz.getName());
@@ -9730,8 +9922,14 @@ public class ChatManager {
         @Override
         public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
             Log.d(TAG, "marsClientService connected");
+            if (iBinder instanceof android.os.Binder) {
+                isClientServiceInMainProcess = true;
+            } else {
+                // BinderProxy
+                isClientServiceInMainProcess = false;
+            }
             mClient = IRemoteClient.Stub.asInterface(iBinder);
-            workHandler.post(() -> {
+            internalWorkHandler.post(() -> {
                 try {
                     if (useSM4) {
                         mClient.useSM4();
@@ -9751,6 +9949,9 @@ public class ChatManager {
                     if (checkSignature) {
                         mClient.checkSignature();
                     }
+                    if (heartBeatInterval > 0) {
+                        mClient.setHeartBeatInterval(heartBeatInterval);
+                    }
 
                     mClient.setBackupAddressStrategy(backupAddressStrategy);
                     if (!TextUtils.isEmpty(backupAddressHost))
@@ -10037,7 +10238,7 @@ public class ChatManager {
             try {
                 return cls.newInstance();
             } catch (Exception e) {
-                android.util.Log.e(TAG, "create message content instance failed, fall back to UnknownMessageContent, the message content class must have a default constructor. " + type);
+                Log.e(TAG, "create message content instance failed, fall back to UnknownMessageContent, the message content class must have a default constructor. " + type);
                 e.printStackTrace();
             }
         }

+ 5 - 0
uni-Android-SDK/client/src/main/res/values-en/strings.xml

@@ -0,0 +1,5 @@
+<resources>
+    <string name="app_name">WildFireChatClient</string>
+    <string name="join_group_by_qrcode">%1$s scanned the QR code shared by %2$s and joined the group chat</string>
+    <string name="join_group_by_card">>%1$s joined the group chat via the group card shared by %2$s</string>
+</resources>

+ 2 - 2
uni-Android-SDK/client/src/main/res/values/strings.xml

@@ -1,5 +1,5 @@
 <resources>
     <string name="app_name">WildFireChatClient</string>
-    <string name="join_group_by_qrcode">%s 扫描 %s 分享的二维码加入了群聊</string>
-    <string name="join_group_by_card">%s 通过 %s 分享的群名片加入了群聊</string>
+    <string name="join_group_by_qrcode">%1$s 扫描 %2$s 分享的二维码加入了群聊</string>
+    <string name="join_group_by_card">%1$s 通过 %1$s 分享的群名片加入了群聊</string>
 </resources>

BIN
uni-Android-SDK/mars-core-release/mars-core-release.aar