From b5642c7b31ecfe91f5628d56e58ca1a35442de74 Mon Sep 17 00:00:00 2001
From: xuchengpu mac <1550540124@qq.com>
Date: Fri, 17 Nov 2023 19:15:56 +0800
Subject: [PATCH 01/11] update version to 1.2.2
---
app/build.gradle | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 184fe457..56cd18de 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -16,8 +16,8 @@ android {
applicationId "io.agora.chatdemo"
minSdkVersion 21
targetSdkVersion 34
- versionCode 11
- versionName "1.1.0"
+ versionCode 122
+ versionName "1.2.2"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -139,8 +139,8 @@ dependencies {
implementation 'com.google.code.gson:gson:2.6.2'
// Agora Chat Uikit
- implementation 'io.agora.rtc:chat-uikit:1.1.0'
- implementation 'io.agora.rtc:chat-callkit:1.1.0'
+ implementation 'io.agora.rtc:chat-uikit:1.2.2'
+ implementation 'io.agora.rtc:chat-callkit:1.2.2'
// implementation project(path: ':chat-uikit')
// implementation project(path: ':chat-callkit')
// url preview
From 8eac31a67bb8335e7321cf937113c1d5386efde1 Mon Sep 17 00:00:00 2001
From: xuchengpu mac <1550540124@qq.com>
Date: Fri, 17 Nov 2023 20:53:41 +0800
Subject: [PATCH 02/11] update androidTestImplementation version
---
app/build.gradle | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 56cd18de..99d72c78 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -111,8 +111,8 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
testImplementation 'junit:junit:4.+'
- androidTestImplementation 'androidx.test.ext:junit:1.1.1'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
//ViewModel and LiveData
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// Google firebase cloud messaging
From aebeabd145f7a8b69d54355eff7cdd87160e5681 Mon Sep 17 00:00:00 2001
From: xuchengpu mac <1550540124@qq.com>
Date: Thu, 30 Nov 2023 19:57:05 +0800
Subject: [PATCH 03/11] Fixed an issue where audio and video activities were
recovered when the suspended window returned to the desktop
---
app/src/main/AndroidManifest.xml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f5bb1895..ac971a8a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -175,6 +175,7 @@
android:exported="false"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/demo_activity_label_video_call"
+ android:taskAffinity=".SingleCallTask"
android:launchMode="singleInstance"
android:screenOrientation="portrait" />
From a7fa42cd780bbc22826bc38281267bff87275533 Mon Sep 17 00:00:00 2001
From: xuchengpu mac <1550540124@qq.com>
Date: Thu, 30 Nov 2023 19:58:37 +0800
Subject: [PATCH 04/11] Fixed a bug where the initiator's personal information
could not be displayed
---
.../main/java/io/agora/chatdemo/av/DemoCallKitListener.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/src/main/java/io/agora/chatdemo/av/DemoCallKitListener.java b/app/src/main/java/io/agora/chatdemo/av/DemoCallKitListener.java
index cd7fa8bf..da119b35 100644
--- a/app/src/main/java/io/agora/chatdemo/av/DemoCallKitListener.java
+++ b/app/src/main/java/io/agora/chatdemo/av/DemoCallKitListener.java
@@ -247,9 +247,9 @@ private void getUserIdByAgoraUid(int uId, String url, EaseCallGetUserAccountCall
String uIdStr = it.next().toString();
int uid = Integer.valueOf(uIdStr).intValue();
String username = resToken.optString(uIdStr);
- if (uid == uId) {
+ if (uid == uId||uid==0) {
//Obtain information such as userName, profile picture, and nickname of the current user
- userAccount=new EaseUserAccount(uid, username);
+ userAccount=new EaseUserAccount(uId, username);
}
}
callback.onUserAccount(userAccount);
From 5fa2ef057586cd19da94d4363e9b33cfd624a4dc Mon Sep 17 00:00:00 2001
From: xuchengpu mac <1550540124@qq.com>
Date: Fri, 1 Dec 2023 16:26:03 +0800
Subject: [PATCH 05/11] The audio and video stack is not displayed
---
app/src/main/AndroidManifest.xml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ac971a8a..61da85f7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -177,6 +177,7 @@
android:label="@string/demo_activity_label_video_call"
android:taskAffinity=".SingleCallTask"
android:launchMode="singleInstance"
+ android:excludeFromRecents="true"
android:screenOrientation="portrait" />
Date: Wed, 13 Mar 2024 15:56:08 +0800
Subject: [PATCH 06/11] remove fpa
---
app/src/main/java/io/agora/chatdemo/DemoHelper.java | 2 --
1 file changed, 2 deletions(-)
diff --git a/app/src/main/java/io/agora/chatdemo/DemoHelper.java b/app/src/main/java/io/agora/chatdemo/DemoHelper.java
index 824d895d..c03d14ae 100644
--- a/app/src/main/java/io/agora/chatdemo/DemoHelper.java
+++ b/app/src/main/java/io/agora/chatdemo/DemoHelper.java
@@ -170,8 +170,6 @@ private boolean initSDK(Context context) {
// options.setIMServer("106.75.100.247");
// options.setImPort(6717);
options.setUsingHttpsOnly(true);
- // Use fpa by default
- options.setFpaEnable(true);
boolean hasAppkey = checkAgoraChatAppKey(context, options);
// You can set your AppKey by options.setAppKey(appkey)
if (!hasAppkey) {
From d331e087a354f64c1041bb7ee5ff3fc2ae799c1c Mon Sep 17 00:00:00 2001
From: xuchengpu mac <1550540124@qq.com>
Date: Thu, 14 Mar 2024 14:25:13 +0800
Subject: [PATCH 07/11] add ENABLE_AGORA for pck
---
app/jni/Android.mk | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/jni/Android.mk b/app/jni/Android.mk
index 129fd8b7..f094fe4e 100644
--- a/app/jni/Android.mk
+++ b/app/jni/Android.mk
@@ -19,5 +19,6 @@ include $(CLEAR_VARS)
PB_LITE=1
ENABLE_CALL=0
USE_SQLCIPHER=1
+ENABLE_AGORA=1
#libhyphenate.so
include $(LOCAL_PATH)/../../../emclient-linux/Android.mk
From 0a3d8552f2dd260b0b47ee853525707b288bd377 Mon Sep 17 00:00:00 2001
From: xuchengpu mac <1550540124@qq.com>
Date: Sun, 7 Apr 2024 16:45:51 +0800
Subject: [PATCH 08/11] add pin message feature in Agora-Chat demo
---
.../java/io/agora/chatdemo/DemoHelper.java | 5 -
.../chatdemo/chat/ChatReportActivity.java | 2 +-
.../chatdemo/chat/CustomChatFragment.java | 356 ++++++++++++++----
.../chat/PinListItemSpaceDecoration.java | 29 ++
.../chat/adapter/PinMessageListAdapter.java | 44 +++
.../pinmessage/PinDefaultViewHolder.java | 68 ++++
.../pinmessage/PinImageMessageViewHolder.java | 65 ++++
.../pinmessage/PinTextMessageViewHolder.java | 68 ++++
.../chat/viewmodel/ChatViewModel.java | 29 +-
.../ConversationListFragment.java | 1 -
.../chatdemo/general/dialog/SimpleDialog.java | 6 +-
.../general/manager/UsersManager.java | 1 +
.../repositories/EMChatManagerRepository.java | 52 +++
.../repositories/EMClientRepository.java | 4 +-
.../chatdemo/general/widget/PinInfoView.java | 118 ++++++
.../widget/PinMessageListViewGroup.java | 164 ++++++++
.../drawable-xxhdpi/chat_item_menu_pin.png | Bin 0 -> 760 bytes
.../res/drawable-xxhdpi/chat_pin_onlight.png | Bin 0 -> 1022 bytes
.../drawable-xxhdpi/chat_pin_rectangle.png | Bin 0 -> 373 bytes
.../res/drawable-xxhdpi/chat_pininfo_icon.png | Bin 0 -> 554 bytes
.../drawable/shape_gray_ebebeb_corner_8.xml | 7 +
.../drawable/shape_gray_f5f5f5_corner_8.xml | 7 +
app/src/main/res/layout/pin_info_view.xml | 40 ++
.../layout/pin_message_list_view_group.xml | 46 +++
app/src/main/res/layout/pinlist_default.xml | 52 +++
app/src/main/res/layout/pinlist_image.xml | 53 +++
app/src/main/res/layout/pinlist_text.xml | 53 +++
app/src/main/res/values/colors.xml | 2 +
app/src/main/res/values/ids.xml | 1 +
app/src/main/res/values/strings.xml | 5 +
30 files changed, 1186 insertions(+), 92 deletions(-)
create mode 100644 app/src/main/java/io/agora/chatdemo/chat/PinListItemSpaceDecoration.java
create mode 100644 app/src/main/java/io/agora/chatdemo/chat/adapter/PinMessageListAdapter.java
create mode 100644 app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinDefaultViewHolder.java
create mode 100644 app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinImageMessageViewHolder.java
create mode 100644 app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinTextMessageViewHolder.java
create mode 100644 app/src/main/java/io/agora/chatdemo/general/widget/PinInfoView.java
create mode 100644 app/src/main/java/io/agora/chatdemo/general/widget/PinMessageListViewGroup.java
create mode 100644 app/src/main/res/drawable-xxhdpi/chat_item_menu_pin.png
create mode 100644 app/src/main/res/drawable-xxhdpi/chat_pin_onlight.png
create mode 100644 app/src/main/res/drawable-xxhdpi/chat_pin_rectangle.png
create mode 100644 app/src/main/res/drawable-xxhdpi/chat_pininfo_icon.png
create mode 100644 app/src/main/res/drawable/shape_gray_ebebeb_corner_8.xml
create mode 100644 app/src/main/res/drawable/shape_gray_f5f5f5_corner_8.xml
create mode 100644 app/src/main/res/layout/pin_info_view.xml
create mode 100644 app/src/main/res/layout/pin_message_list_view_group.xml
create mode 100644 app/src/main/res/layout/pinlist_default.xml
create mode 100644 app/src/main/res/layout/pinlist_image.xml
create mode 100644 app/src/main/res/layout/pinlist_text.xml
diff --git a/app/src/main/java/io/agora/chatdemo/DemoHelper.java b/app/src/main/java/io/agora/chatdemo/DemoHelper.java
index c03d14ae..9cdfd53a 100644
--- a/app/src/main/java/io/agora/chatdemo/DemoHelper.java
+++ b/app/src/main/java/io/agora/chatdemo/DemoHelper.java
@@ -163,12 +163,7 @@ public EMAREncryptUtils getEncryptUtils(){
private boolean initSDK(Context context) {
// Set Chat Options
ChatOptions options = initChatOptions(context);
- // Configure custom rest server and im server
-// options.setRestServer("a1-hsb.easemob.com");
-// options.setAppKey("easemob-demo#chatdemoui");
-// options.setIMServer("106.75.100.247");
-// options.setImPort(6717);
options.setUsingHttpsOnly(true);
boolean hasAppkey = checkAgoraChatAppKey(context, options);
// You can set your AppKey by options.setAppKey(appkey)
diff --git a/app/src/main/java/io/agora/chatdemo/chat/ChatReportActivity.java b/app/src/main/java/io/agora/chatdemo/chat/ChatReportActivity.java
index 7707515d..4b94dfa5 100644
--- a/app/src/main/java/io/agora/chatdemo/chat/ChatReportActivity.java
+++ b/app/src/main/java/io/agora/chatdemo/chat/ChatReportActivity.java
@@ -140,7 +140,7 @@ public void initData(){
}
}
viewModel = new ViewModelProvider(this).get(ChatViewModel.class);
- viewModel.getChatManagerObservable().observe(this,response->{
+ viewModel.getReportMessageObservable().observe(this, response->{
parseResource(response, new OnResourceParseCallback() {
@Override
public void onSuccess(@Nullable Boolean data) {
diff --git a/app/src/main/java/io/agora/chatdemo/chat/CustomChatFragment.java b/app/src/main/java/io/agora/chatdemo/chat/CustomChatFragment.java
index 7dac0f92..aa0aa1f6 100644
--- a/app/src/main/java/io/agora/chatdemo/chat/CustomChatFragment.java
+++ b/app/src/main/java/io/agora/chatdemo/chat/CustomChatFragment.java
@@ -1,6 +1,7 @@
package io.agora.chatdemo.chat;
import static io.agora.chat.uikit.menu.EaseChatType.SINGLE_CHAT;
+import static io.agora.chatdemo.general.utils.ToastUtils.showToast;
import android.Manifest;
import android.app.Activity;
@@ -14,7 +15,6 @@
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.ForegroundColorSpan;
-import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
@@ -35,6 +35,8 @@
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+import com.google.android.gms.common.util.CollectionUtils;
+
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
@@ -44,11 +46,13 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import io.agora.MessageListener;
import io.agora.chat.ChatClient;
import io.agora.chat.ChatMessage;
import io.agora.chat.ChatRoom;
import io.agora.chat.CustomMessageBody;
import io.agora.chat.LocationMessageBody;
+import io.agora.chat.MessagePinInfo;
import io.agora.chat.TextMessageBody;
import io.agora.chat.uikit.chat.EaseChatFragment;
import io.agora.chat.uikit.chat.adapter.EaseMessageAdapter;
@@ -65,15 +69,22 @@
import io.agora.chatdemo.R;
import io.agora.chatdemo.chat.adapter.CustomMessageAdapter;
import io.agora.chatdemo.chat.viewmodel.ChatViewModel;
+import io.agora.chatdemo.general.callbacks.OnResourceParseCallback;
import io.agora.chatdemo.general.constant.DemoConstant;
import io.agora.chatdemo.general.dialog.AlertDialog;
+import io.agora.chatdemo.general.dialog.SimpleDialog;
import io.agora.chatdemo.general.enums.Status;
import io.agora.chatdemo.general.interfaces.TranslationListener;
import io.agora.chatdemo.general.livedatas.EaseEvent;
import io.agora.chatdemo.general.livedatas.LiveDataBus;
+import io.agora.chatdemo.general.net.Resource;
import io.agora.chatdemo.general.permission.PermissionCompat;
import io.agora.chatdemo.general.permission.PermissionsManager;
import io.agora.chatdemo.general.utils.RecyclerViewUtils;
+import io.agora.chatdemo.general.utils.ToastUtils;
+import io.agora.chatdemo.general.utils.UIUtils;
+import io.agora.chatdemo.general.widget.PinInfoView;
+import io.agora.chatdemo.general.widget.PinMessageListViewGroup;
import io.agora.chatdemo.group.GroupHelper;
import io.agora.chatdemo.group.model.MemberAttributeBean;
import io.agora.chatdemo.group.viewmodel.GroupDetailViewModel;
@@ -82,7 +93,7 @@
import io.agora.chatdemo.me.TranslationSettingsActivity;
import io.agora.util.EMLog;
-public class CustomChatFragment extends EaseChatFragment {
+public class CustomChatFragment extends EaseChatFragment implements MessageListener {
private static final int REQUEST_CODE_STORAGE_PICTURE = 111;
private static final int REQUEST_CODE_STORAGE_VIDEO = 112;
private static final int REQUEST_CODE_STORAGE_FILE = 113;
@@ -100,6 +111,7 @@ public class CustomChatFragment extends EaseChatFragment {
, result -> onRequestResult(result, REQUEST_CODE_STORAGE_VIDEO));
private final ActivityResultLauncher requestFilePermission = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions()
, result -> onRequestResult(result, REQUEST_CODE_STORAGE_FILE));
+ private PinInfoView pinInfoView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -109,8 +121,8 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
boolean enable = DemoHelper.getInstance().getModel().getDemandTranslationEnable();
- if (enable && !TextUtils.isEmpty(getPreferredLanguageCode())){
- translationMessage(translationMsg,getPreferredLanguageCode());
+ if (enable && !TextUtils.isEmpty(getPreferredLanguageCode())) {
+ translationMessage(translationMsg, getPreferredLanguageCode());
}
}
}
@@ -120,53 +132,130 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
@Override
public void initData() {
super.initData();
- groupDetailViewModel = new ViewModelProvider((AppCompatActivity)mContext).get(GroupDetailViewModel.class);
- groupDetailViewModel.getFetchMemberAttributesObservable().observe(this,response ->{
- if(response == null || isDestroy) {
+ groupDetailViewModel = new ViewModelProvider((AppCompatActivity) mContext).get(GroupDetailViewModel.class);
+ groupDetailViewModel.getFetchMemberAttributesObservable().observe(this, response -> {
+ if (response == null || isDestroy) {
return;
}
- if(response.status == Status.SUCCESS) {
+ if (response.status == Status.SUCCESS) {
chatLayout.getChatMessageListLayout().refreshMessages();
}
});
viewModel = new ViewModelProvider(this).get(ChatViewModel.class);
- viewModel.getTranslationObservable().observe(this,response ->{
- if(response == null || isDestroy) {
+ viewModel.getTranslationObservable().observe(this, response -> {
+ if (response == null || isDestroy) {
return;
}
- if(response.status == Status.SUCCESS) {
+ if (response.status == Status.SUCCESS) {
chatLayout.getChatMessageListLayout().refreshMessages();
- }else {
- EMLog.e("translationMessage","onError: " + response.errorCode + " - " + response.getMessage());
+ } else {
+ EMLog.e("translationMessage", "onError: " + response.errorCode + " - " + response.getMessage());
}
});
+ viewModel.pinMessageObservable().observe(this, response -> {
+ parseResource(response, new OnResourceParseCallback() {
+ @Override
+ public void onSuccess(ChatMessage message) {
+ updatePinMessage(message,ChatClient.getInstance().getCurrentUser());
+ }
+
+ @Override
+ public void onError(int code, String message) {
+ super.onError(code, message);
+ showToast(message);
+ }
+ });
+ });
LiveDataBus.get().with(DemoConstant.GROUP_MEMBER_ATTRIBUTE_CHANGE, EaseEvent.class).observe(getViewLifecycleOwner(), event -> {
- if(event == null || isDestroy) {
+ if (event == null || isDestroy) {
return;
}
chatLayout.getChatMessageListLayout().refreshMessages();
});
LiveDataBus.get().with(DemoConstant.MESSAGE_CHANGE_CHANGE, EaseEvent.class).observe(getViewLifecycleOwner(), event -> {
- if(event == null || isDestroy) {
+ if (event == null || isDestroy) {
return;
}
- if(event.isMessageChange()) {
+ if (event.isMessageChange()) {
chatLayout.getChatMessageListLayout().refreshMessages();
}
});
LiveDataBus.get().with(DemoConstant.EVENT_CHAT_MODEL_TO_NORMAL, EaseEvent.class).observe(this, event -> {
- if(event == null || isDestroy) {
+ if (event == null || isDestroy) {
return;
}
- if(event.type == EaseEvent.TYPE.NOTIFY && TextUtils.isEmpty(event.message)) {
+ if (event.type == EaseEvent.TYPE.NOTIFY && TextUtils.isEmpty(event.message)) {
IChatTopExtendMenu chatTopExtendMenu = chatLayout.getChatInputMenu().getChatTopExtendMenu();
- if(chatTopExtendMenu instanceof EaseChatMultiSelectView) {
+ if (chatTopExtendMenu instanceof EaseChatMultiSelectView) {
((EaseChatMultiSelectView) chatTopExtendMenu).dismissSelectView(null);
}
titleBar.setVisibility(View.GONE);
}
});
+
+ viewModel.getPinMessageObservable().observe(this, response -> {
+ parseResource(response, new OnResourceParseCallback>() {
+ @Override
+ public void onSuccess(List messages) {
+ if (CollectionUtils.isEmpty(messages)) {
+ pinInfoView.setVisibility(View.GONE);
+ } else {
+ pinInfoView.setData(messages);
+ }
+ }
+
+ @Override
+ public void onError(int code, String message) {
+ super.onError(code, message);
+ }
+ });
+ });
+
+ if (chatType != SINGLE_CHAT) {
+ viewModel.getPinnedMessagesFromServer(conversationId);
+ }
+ }
+
+ private void updatePinMessage(ChatMessage message,String operationUser) {
+ runOnUiThread(()->{
+ boolean isPined = message.pinnedInfo()==null||TextUtils.isEmpty(message.pinnedInfo().operatorId());
+ ToastUtils.showToast((isPined ? "unpin success" : "pin success"));
+ if(isPined){
+ pinInfoView.removeData(message);
+ }else{
+ pinInfoView.addData(message);
+ }
+ //insert pin message info in local
+ insertPinNotificationInLocal(message, operationUser);
+ chatLayout.getChatMessageListLayout().refreshToLatest();
+ });
+ }
+
+ private void insertPinNotificationInLocal(ChatMessage msg,String operationUser) {
+ ChatMessage msgNotification = ChatMessage.createReceiveMessage(ChatMessage.Type.TXT);
+ String content;
+ if(msg.pinnedInfo()==null||TextUtils.isEmpty(msg.pinnedInfo().operatorId())) {
+ content = operationUser+" removed a pin message";
+ }else{
+ content = operationUser+" pinned a message";
+ }
+ if(TextUtils.equals(operationUser, ChatClient.getInstance().getCurrentUser())){
+ content = content.replace(operationUser, "You");
+ }
+ TextMessageBody txtBody = new TextMessageBody(content);
+ msgNotification.addBody(txtBody);
+ msgNotification.setFrom(msg.getFrom());
+ msgNotification.setTo(msg.getTo());
+ msgNotification.setUnread(false);
+ msgNotification.setMsgTime(System.currentTimeMillis());
+ msgNotification.setLocalTime(System.currentTimeMillis());
+ msgNotification.setChatType(msg.getChatType());
+ //Just to reuse the recall layout
+ msgNotification.setAttribute(EaseConstant.MESSAGE_TYPE_RECALL, true);
+ msgNotification.setStatus(ChatMessage.Status.SUCCESS);
+ msgNotification.setIsChatThreadMessage(msg.isChatThreadMessage());
+ ChatClient.getInstance().chatManager().saveMessage(msgNotification);
}
@Override
@@ -174,11 +263,11 @@ public void initListener() {
super.initListener();
listenerRecyclerViewItemFinishLayout();
EditText editText = chatLayout.getChatInputMenu().getPrimaryMenu().getEditText();
- if (editText != null){
+ if (editText != null) {
editText.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
- return removePickAt(v,keyCode,event);
+ return removePickAt(v, keyCode, event);
}
});
editText.addTextChangedListener(new TextWatcher() {
@@ -189,20 +278,20 @@ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
- if(!chatLayout.getChatMessageListLayout().isGroupChat()) {
+ if (!chatLayout.getChatMessageListLayout().isGroupChat()) {
return;
}
- if(count == 1 && "@".equals(String.valueOf(s.charAt(start)))){
+ if (count == 1 && "@".equals(String.valueOf(s.charAt(start)))) {
Bundle bundle = new Bundle();
bundle.putString(EaseConstant.EXTRA_CONVERSATION_ID, conversationId);
PickAtUserDialogFragment fragment = new PickAtUserDialogFragment();
fragment.setPickAtSelectListener(username -> {
- chatLayout.inputAtUsername(username,false);
+ chatLayout.inputAtUsername(username, false);
});
fragment.setArguments(bundle);
- if (getActivity() != null){
+ if (getActivity() != null) {
fragment.show(getActivity().getSupportFragmentManager(), "pick_at_user");
- if (getActivity() != null){
+ if (getActivity() != null) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
@@ -210,7 +299,7 @@ public void run() {
editText.requestFocus();
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
- },200);
+ }, 200);
}
}
}
@@ -222,6 +311,53 @@ public void afterTextChanged(Editable editable) {
}
});
}
+
+ if(pinInfoView!=null){
+ pinInfoView.setOnItemClickListener(new PinMessageListViewGroup.OnItemClickListener() {
+ @Override
+ public void onItemClick(ChatMessage message) {
+ pinInfoView.restView();
+ //click for pin message list
+ List messageList = chatLayout.getChatMessageListLayout().getMessageAdapter().getData();
+ boolean isExist = false;
+ for (int i = 0; i < messageList.size(); i++) {
+ ChatMessage chatMessage = messageList.get(i);
+ if (chatMessage.getMsgId().equals(message.getMsgId())) {
+ isExist = true;
+ break;
+ }
+ }
+ if (!isExist) {
+ ToastUtils.showToast(getString(R.string.pin_skip_not_exist));
+ }else{
+ chatLayout.getChatMessageListLayout().moveToTarget(message);
+ }
+ }
+ });
+ pinInfoView.setOnItemSubViewClickListener(new EaseMessageAdapter.OnItemSubViewClickListener() {
+ @Override
+ public void onItemSubViewClick(View view, int position) {
+ ChatMessage message=pinInfoView.getPinMessages().get(position);
+ showUnPinConfirmDialog(message);
+ }
+ });
+ }
+
+ ChatClient.getInstance().chatManager().addMessageListener(this);
+ }
+
+ private void showUnPinConfirmDialog(ChatMessage message) {
+ new SimpleDialog.Builder(getActivity())
+ .setTitle(R.string.unpin_confirm_message)
+ .showCancelButton(true)
+ .hideConfirmButton(false)
+ .setOnConfirmClickListener(R.string.dialog_btn_to_confirm, new SimpleDialog.OnConfirmClickListener() {
+ @Override
+ public void onConfirmClick(View view) {
+ viewModel.pinMessage(message, false);
+ }
+ })
+ .show();
}
@@ -234,28 +370,46 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
@Override
public void initView() {
super.initView();
+ MenuItemBean pinItemBean = new MenuItemBean(0, R.id.action_chat_pin, 76, getResources().getString(R.string.ease_action_pin));
+ pinItemBean.setResourceId(R.drawable.chat_item_menu_pin);
MenuItemBean menuItemBean = new MenuItemBean(0, R.id.action_chat_report, 99, getResources().getString(R.string.ease_action_report));
menuItemBean.setResourceId(R.drawable.chat_item_menu_report);
- MenuItemBean menuTranslationBean = new MenuItemBean(0, R.id.action_chat_translation,88, getResources().getString(R.string.ease_action_translation));
+ MenuItemBean menuTranslationBean = new MenuItemBean(0, R.id.action_chat_translation, 88, getResources().getString(R.string.ease_action_translation));
menuTranslationBean.setResourceId(R.drawable.chat_item_menu_translation);
- MenuItemBean menuReTranslationBean = new MenuItemBean(0, R.id.action_chat_re_translation,111, getResources().getString(R.string.ease_action_re_translation));
+ MenuItemBean menuReTranslationBean = new MenuItemBean(0, R.id.action_chat_re_translation, 111, getResources().getString(R.string.ease_action_re_translation));
menuReTranslationBean.setResourceId(R.drawable.chat_item_menu_translation);
+ chatLayout.getMenuHelper().addItemMenu(pinItemBean);
chatLayout.getMenuHelper().addItemMenu(menuItemBean);
chatLayout.getMenuHelper().addItemMenu(menuTranslationBean);
chatLayout.getMenuHelper().addItemMenu(menuReTranslationBean);
chatLayout.setPresenter(new ChatCustomPresenter());
EaseMessageAdapter adapter = chatLayout.getChatMessageListLayout().getMessageAdapter();
- if (adapter instanceof CustomMessageAdapter){
- ((CustomMessageAdapter)adapter).setTranslationListener(new TranslationListener() {
+ if (adapter instanceof CustomMessageAdapter) {
+ ((CustomMessageAdapter) adapter).setTranslationListener(new TranslationListener() {
@Override
- public void onTranslationRetry(ChatMessage message,String languageCode) {
- if (message.getBody() instanceof TextMessageBody){
- translationMessage(message,languageCode);
+ public void onTranslationRetry(ChatMessage message, String languageCode) {
+ if (message.getBody() instanceof TextMessageBody) {
+ translationMessage(message, languageCode);
}
}
});
}
+
+ if(chatType!=SINGLE_CHAT){
+ pinInfoView = new PinInfoView(getContext());
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ pinInfoView.setVisibility(View.GONE);
+ chatLayout.addView(pinInfoView, layoutParams);
+ chatLayout.post(new Runnable() {
+ @Override
+ public void run() {
+ pinInfoView.setInnerLayoutMaxHeight(chatLayout.getHeight()- UIUtils.dp2px(getContext(), 145));
+ }
+ });
+ }
}
@Override
@@ -290,14 +444,16 @@ public void onPreMenu(EasePopupWindowHelper helper, ChatMessage message) {
helper.findItemVisible(R.id.action_chat_translation, false);
helper.findItemVisible(R.id.action_chat_re_translation, false);
}
+ helper.findItem(R.id.action_chat_pin).setTitle((message.pinnedInfo()==null||TextUtils.isEmpty(message.pinnedInfo().operatorId())) ? getString(R.string.ease_action_pin):getString(R.string.ease_action_unpin));
+ helper.findItemVisible(R.id.action_chat_pin, chatType==SINGLE_CHAT?false:true);
}
@Override
public boolean onMenuItemClick(MenuItemBean item, ChatMessage message) {
- switch (item.getItemId()){
+ switch (item.getItemId()) {
case R.id.action_chat_report:
if (message.status() == ChatMessage.Status.SUCCESS)
- ChatReportActivity.actionStart(getActivity(),message.getMsgId());
+ ChatReportActivity.actionStart(getActivity(), message.getMsgId());
break;
case R.id.action_chat_select:
showSelectModelTitle();
@@ -306,19 +462,22 @@ public boolean onMenuItemClick(MenuItemBean item, ChatMessage message) {
case R.id.action_chat_translation:
case R.id.action_chat_re_translation:
translationMsg = message;
- if (!TextUtils.isEmpty(getPreferredLanguageCode())){
+ if (!TextUtils.isEmpty(getPreferredLanguageCode())) {
boolean enable = DemoHelper.getInstance().getModel().getDemandTranslationEnable();
- if (enable){
- translationMessage(message,getPreferredLanguageCode());
+ if (enable) {
+ translationMessage(message, getPreferredLanguageCode());
break;
- }else {
+ } else {
translationType = DemoConstant.TRANSLATION_DEMAND_ENABLE;
}
- }else {
+ } else {
translationType = DemoConstant.TRANSLATION_NO_LANGUAGE;
}
showTranslationDialog();
break;
+ case R.id.action_chat_pin:
+ viewModel.pinMessage(message, message.pinnedInfo()==null||TextUtils.isEmpty(message.pinnedInfo().operatorId()));
+ break;
}
return super.onMenuItemClick(item, message);
}
@@ -334,17 +493,17 @@ public boolean onChatExtendMenuItemClick(View view, int itemId) {
}
break;
case R.id.extend_item_picture:
- if(!PermissionCompat.checkMediaPermission(mContext, requestImagePermission, Manifest.permission.READ_MEDIA_IMAGES)) {
+ if (!PermissionCompat.checkMediaPermission(mContext, requestImagePermission, Manifest.permission.READ_MEDIA_IMAGES)) {
return true;
}
break;
case R.id.extend_item_video:
- if(!PermissionCompat.checkMediaPermission(mContext, requestVideoPermission, Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.CAMERA)) {
+ if (!PermissionCompat.checkMediaPermission(mContext, requestVideoPermission, Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.CAMERA)) {
return true;
}
break;
case R.id.extend_item_file:
- if(!PermissionCompat.checkMediaPermission(mContext, requestFilePermission, Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO)) {
+ if (!PermissionCompat.checkMediaPermission(mContext, requestFilePermission, Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO)) {
return true;
}
break;
@@ -353,16 +512,16 @@ public boolean onChatExtendMenuItemClick(View view, int itemId) {
}
private void onRequestResult(Map result, int requestCode) {
- if(result != null && result.size() > 0) {
+ if (result != null && result.size() > 0) {
for (Map.Entry entry : result.entrySet()) {
EMLog.e("chat", "onRequestResult: " + entry.getKey() + " " + entry.getValue());
}
- if(PermissionCompat.getMediaAccess(mContext) != PermissionCompat.StorageAccess.Denied) {
- if(requestCode == REQUEST_CODE_STORAGE_PICTURE) {
+ if (PermissionCompat.getMediaAccess(mContext) != PermissionCompat.StorageAccess.Denied) {
+ if (requestCode == REQUEST_CODE_STORAGE_PICTURE) {
selectPicFromLocal();
- }else if(requestCode == REQUEST_CODE_STORAGE_VIDEO) {
+ } else if (requestCode == REQUEST_CODE_STORAGE_VIDEO) {
selectVideoFromLocal();
- }else if(requestCode == REQUEST_CODE_STORAGE_FILE) {
+ } else if (requestCode == REQUEST_CODE_STORAGE_FILE) {
selectFileFromLocal();
}
}
@@ -378,9 +537,9 @@ private void showSelectModelTitle() {
titleBar.getIcon().setVisibility(View.VISIBLE);
titleBar.getLeftLayout().setVisibility(View.GONE);
ViewParent parent = titleBar.getTitle().getParent();
- if(parent instanceof ViewGroup) {
+ if (parent instanceof ViewGroup) {
ViewGroup.LayoutParams params = ((ViewGroup) parent).getLayoutParams();
- if(params instanceof RelativeLayout.LayoutParams) {
+ if (params instanceof RelativeLayout.LayoutParams) {
((RelativeLayout.LayoutParams) params).leftMargin = (int) EaseUtils.dip2px(mContext, 12);
}
}
@@ -390,9 +549,9 @@ public void onRightClick(View view) {
LiveDataBus.get().with(DemoConstant.EVENT_CHAT_MODEL_TO_NORMAL).postValue(EaseEvent.create(DemoConstant.EVENT_CHAT_MODEL_TO_NORMAL, EaseEvent.TYPE.NOTIFY));
}
});
- if(chatType != SINGLE_CHAT) {
+ if (chatType != SINGLE_CHAT) {
boolean hasProvided = DemoHelper.getInstance().setGroupInfo(mContext, conversationId, titleBar.getTitle(), titleBar.getIcon());
- if(!hasProvided) {
+ if (!hasProvided) {
setGroupInfo();
}
} else {
@@ -404,16 +563,16 @@ public void onRightClick(View view) {
private void setGroupInfo() {
String title = "";
- if(chatType == EaseChatType.GROUP_CHAT) {
+ if (chatType == EaseChatType.GROUP_CHAT) {
title = GroupHelper.getGroupName(conversationId);
titleBar.getIcon().setImageResource(R.drawable.icon);
- }else if(chatType == EaseChatType.CHATROOM) {
+ } else if (chatType == EaseChatType.CHATROOM) {
titleBar.getIcon().setImageResource(R.drawable.icon);
ChatRoom room = ChatClient.getInstance().chatroomManager().getChatRoom(conversationId);
- if(room == null) {
+ if (room == null) {
return;
}
- title = TextUtils.isEmpty(room.getName()) ? conversationId : room.getName();
+ title = TextUtils.isEmpty(room.getName()) ? conversationId : room.getName();
}
titleBar.getTitle().setText(title);
}
@@ -500,22 +659,22 @@ public void onModifyMessageSuccess(ChatMessage messageModified) {
}
- private void translationMessage(ChatMessage message,String language){
+ private void translationMessage(ChatMessage message, String language) {
List list = new ArrayList<>();
list.add(language);
- viewModel.translationMessage(message,list);
+ viewModel.translationMessage(message, list);
}
@Override
public void addMsgAttrsBeforeSend(ChatMessage message) {
super.addMsgAttrsBeforeSend(message);
String[] autoLanguage = TranslationHelper.getLanguageByType(DemoConstant.TRANSLATION_TYPE_AUTO, conversationId);
- if (!TextUtils.isEmpty(autoLanguage[0])){
- translationMessage(message,autoLanguage[0]);
+ if (!TextUtils.isEmpty(autoLanguage[0])) {
+ translationMessage(message, autoLanguage[0]);
}
}
- private void setPickAtContentStyle(Editable editable){
+ private void setPickAtContentStyle(Editable editable) {
Pattern pattern = Pattern.compile("@([^\\s]+)");
Matcher matcher = pattern.matcher(editable);
while (matcher.find()) {
@@ -528,18 +687,18 @@ private void setPickAtContentStyle(Editable editable){
}
}
- private boolean removePickAt(View v, int keyCode, KeyEvent event){
+ private boolean removePickAt(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN && v instanceof EditText) {
- int selectionStart = ((EditText)v).getSelectionStart();
- int selectionEnd = ((EditText)v).getSelectionEnd();
- SpannableStringBuilder text = (SpannableStringBuilder) ((EditText)v).getText();
+ int selectionStart = ((EditText) v).getSelectionStart();
+ int selectionEnd = ((EditText) v).getSelectionEnd();
+ SpannableStringBuilder text = (SpannableStringBuilder) ((EditText) v).getText();
ForegroundColorSpan[] spans = text.getSpans(0, text.length(), ForegroundColorSpan.class);
for (ForegroundColorSpan span : spans) {
int spanStart = text.getSpanStart(span);
int spanEnd = text.getSpanEnd(span);
if (selectionStart >= spanStart && selectionEnd <= spanEnd) {
- if (spanStart != -1 && spanEnd != -1){
- text.delete(spanStart+1, spanEnd);
+ if (spanStart != -1 && spanEnd != -1) {
+ text.delete(spanStart + 1, spanEnd);
}
}
}
@@ -547,14 +706,16 @@ private boolean removePickAt(View v, int keyCode, KeyEvent event){
return false;
}
- private void showTranslationDialog(){
- if (translationType == 0){ return;}
+ private void showTranslationDialog() {
+ if (translationType == 0) {
+ return;
+ }
translationDialog = new AlertDialog.Builder(mContext)
.setContentView(R.layout.dialog_auto_translation)
.setText(R.id.tv_content,
translationType == DemoConstant.TRANSLATION_NO_LANGUAGE ?
getString(R.string.translation_auto_about_info)
- : getString(R.string.translation_unable)
+ : getString(R.string.translation_unable)
)
.setText(R.id.btn_ok, getString(R.string.translation_setting))
.setLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
@@ -564,11 +725,11 @@ private void showTranslationDialog(){
@Override
public void onClick(View v) {
Intent starter;
- if (translationType == DemoConstant.TRANSLATION_NO_LANGUAGE){
+ if (translationType == DemoConstant.TRANSLATION_NO_LANGUAGE) {
starter = new Intent(mContext, LanguageActivity.class);
starter.putExtra(DemoConstant.TRANSLATION_TYPE, DemoConstant.TRANSLATION_TYPE_MESSAGE);
starter.putExtra(DemoConstant.TRANSLATION_SELECT_MAX_COUNT, 1);
- }else {
+ } else {
starter = new Intent(mContext, TranslationSettingsActivity.class);
}
launcher.launch(starter);
@@ -583,7 +744,7 @@ public void onClick(View v) {
translationDialog.show();
}
- private String getPreferredLanguageCode(){
+ private String getPreferredLanguageCode() {
String[] language = TranslationHelper.getLanguageByType(DemoConstant.TRANSLATION_TYPE_MESSAGE, "");
return language[0];
}
@@ -597,8 +758,53 @@ public void onResume() {
@Override
public void onPause() {
super.onPause();
- if(mContext != null && mContext.isFinishing()) {
+ if (mContext != null && mContext.isFinishing()) {
isDestroy = true;
}
}
+
+ /**
+ * Parse Resource
+ *
+ * @param response
+ * @param callback
+ * @param
+ */
+ public void parseResource(Resource response, @NonNull OnResourceParseCallback callback) {
+ if (response == null) {
+ return;
+ }
+ if (response.status == Status.SUCCESS) {
+ callback.onHideLoading();
+ callback.onSuccess(response.data);
+ } else if (response.status == Status.ERROR) {
+ callback.onHideLoading();
+ callback.onError(response.errorCode, response.getMessage());
+ } else if (response.status == Status.LOADING) {
+ callback.onLoading(response.data);
+ }
+ }
+
+ @Override
+ public void onMessageReceived(List messages) {
+
+ }
+
+ @Override
+ public void onMessagePinChanged(String messageId, String conversationId, MessagePinInfo.PinOperation pinOperation, MessagePinInfo pinInfo) {
+ ChatMessage message = ChatClient.getInstance().chatManager().getMessage(messageId);
+ if(message!=null) {
+ updatePinMessage(message,pinInfo.operatorId());
+ }else{
+ viewModel.getPinnedMessagesFromServer(conversationId);
+ }
+ }
+
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ ChatClient.getInstance().chatManager().removeMessageListener(this);
+ }
+
}
diff --git a/app/src/main/java/io/agora/chatdemo/chat/PinListItemSpaceDecoration.java b/app/src/main/java/io/agora/chatdemo/chat/PinListItemSpaceDecoration.java
new file mode 100644
index 00000000..6cc676f9
--- /dev/null
+++ b/app/src/main/java/io/agora/chatdemo/chat/PinListItemSpaceDecoration.java
@@ -0,0 +1,29 @@
+package io.agora.chatdemo.chat;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+public class PinListItemSpaceDecoration extends RecyclerView.ItemDecoration {
+ private int space;
+
+ public PinListItemSpaceDecoration(int space) {
+ this.space = space;
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ outRect.left = space;
+ outRect.right = space;
+ outRect.bottom = space;
+
+
+ if (parent.getChildAdapterPosition(view) == 0) {
+ outRect.top = space;
+ } else {
+ outRect.top = 0;
+ }
+ }
+}
+
diff --git a/app/src/main/java/io/agora/chatdemo/chat/adapter/PinMessageListAdapter.java b/app/src/main/java/io/agora/chatdemo/chat/adapter/PinMessageListAdapter.java
new file mode 100644
index 00000000..a8bf64ba
--- /dev/null
+++ b/app/src/main/java/io/agora/chatdemo/chat/adapter/PinMessageListAdapter.java
@@ -0,0 +1,44 @@
+package io.agora.chatdemo.chat.adapter;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+
+import io.agora.chat.ChatMessage;
+import io.agora.chat.uikit.adapter.EaseBaseRecyclerViewAdapter;
+import io.agora.chatdemo.R;
+import io.agora.chatdemo.chat.viewholder.pinmessage.PinDefaultViewHolder;
+import io.agora.chatdemo.chat.viewholder.pinmessage.PinImageMessageViewHolder;
+import io.agora.chatdemo.chat.viewholder.pinmessage.PinTextMessageViewHolder;
+
+
+public class PinMessageListAdapter extends EaseBaseRecyclerViewAdapter {
+
+ @Override
+ public int getItemNotEmptyViewType(int position) {
+ return mData.get(position).getType().ordinal();
+ }
+
+ @Override
+ public ViewHolder getViewHolder(ViewGroup parent, int viewType) {
+ ViewHolder viewHolder ;
+ switch (ChatMessage.Type.values()[viewType]) {
+ case TXT:
+ viewHolder=new PinTextMessageViewHolder(mItemSubViewListener,LayoutInflater.from(mContext).inflate(R.layout.pinlist_text, parent, false));
+ break;
+ case IMAGE:
+ viewHolder=new PinImageMessageViewHolder(mItemSubViewListener,LayoutInflater.from(mContext).inflate(R.layout.pinlist_image, parent, false));
+ break;
+ default:
+ viewHolder=new PinDefaultViewHolder(mItemSubViewListener,LayoutInflater.from(mContext).inflate(R.layout.pinlist_default, parent, false));
+ break;
+ }
+ return viewHolder;
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ super.onBindViewHolder(holder, position);
+ }
+}
diff --git a/app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinDefaultViewHolder.java b/app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinDefaultViewHolder.java
new file mode 100644
index 00000000..6d55031a
--- /dev/null
+++ b/app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinDefaultViewHolder.java
@@ -0,0 +1,68 @@
+package io.agora.chatdemo.chat.viewholder.pinmessage;
+
+
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+import io.agora.chat.ChatMessage;
+import io.agora.chat.FileMessageBody;
+import io.agora.chat.uikit.adapter.EaseBaseRecyclerViewAdapter;
+import io.agora.chatdemo.R;
+
+public class PinDefaultViewHolder extends EaseBaseRecyclerViewAdapter.ViewHolder {
+ private final EaseBaseRecyclerViewAdapter.OnItemSubViewClickListener mItemSubViewListener;
+ private TextView from;
+ private TextView content;
+ private TextView time;
+ private ImageView state;
+ public PinDefaultViewHolder(EaseBaseRecyclerViewAdapter.OnItemSubViewClickListener mItemSubViewListener, @NonNull View itemView) {
+ super(itemView);
+ this.mItemSubViewListener = mItemSubViewListener;
+ }
+
+ @Override
+ public void initView(View itemView) {
+ super.initView(itemView);
+ from = findViewById(R.id.tv_from);
+ content = findViewById(R.id.tv_content);
+ time = findViewById(R.id.tv_time);
+ state=findViewById(R.id.iv_state);
+ }
+
+ @Override
+ public void setData(ChatMessage message, int position) {
+ String operatorId = message.pinnedInfo().operatorId();
+ long pinTime = message.pinnedInfo().pinTime();
+
+ from.setText(operatorId +" pinned "+message.getFrom()+"'s message");
+ if(message.getType()== ChatMessage.Type.FILE) {
+ FileMessageBody body= (FileMessageBody) message.getBody();
+ content.setText("[File]"+body.displayName());
+ }else if(message.getType()== ChatMessage.Type.CUSTOM) {
+ content.setText("[Custom]");
+ }else{
+ content.setText("["+message.getType().name()+"]");
+ }
+
+ SimpleDateFormat sdf = new SimpleDateFormat("MM-dd, HH:mm");
+ sdf.setTimeZone(TimeZone.getTimeZone("GMT+8"));
+ String formattedDate = sdf.format(new Date(pinTime));
+ time.setText(formattedDate);
+
+ state.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if(mItemSubViewListener!=null) {
+ mItemSubViewListener.onItemSubViewClick(v, position);
+ }
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinImageMessageViewHolder.java b/app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinImageMessageViewHolder.java
new file mode 100644
index 00000000..9dbcb0fb
--- /dev/null
+++ b/app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinImageMessageViewHolder.java
@@ -0,0 +1,65 @@
+package io.agora.chatdemo.chat.viewholder.pinmessage;
+
+
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.bumptech.glide.Glide;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+import io.agora.chat.ChatMessage;
+import io.agora.chat.ImageMessageBody;
+import io.agora.chat.uikit.adapter.EaseBaseRecyclerViewAdapter;
+import io.agora.chatdemo.R;
+
+public class PinImageMessageViewHolder extends EaseBaseRecyclerViewAdapter.ViewHolder {
+ private final EaseBaseRecyclerViewAdapter.OnItemSubViewClickListener mItemSubViewListener;
+ private TextView from;
+ private ImageView content;
+ private TextView time;
+ private ImageView state;
+ public PinImageMessageViewHolder(EaseBaseRecyclerViewAdapter.OnItemSubViewClickListener mItemSubViewListener, @NonNull View itemView) {
+ super(itemView);
+ this.mItemSubViewListener = mItemSubViewListener;
+ }
+
+ @Override
+ public void initView(View itemView) {
+ super.initView(itemView);
+ from = findViewById(R.id.tv_from);
+ content = findViewById(R.id.iv_content);
+ time = findViewById(R.id.tv_time);
+ state=findViewById(R.id.iv_state);
+ }
+
+ @Override
+ public void setData(ChatMessage message, int position) {
+ String operatorId = message.pinnedInfo().operatorId();
+ long pinTime = message.pinnedInfo().pinTime();
+
+ from.setText(operatorId +" pinned "+message.getFrom()+"'s message");
+
+ ImageMessageBody body = (ImageMessageBody) message.getBody();
+ Glide.with(itemView.getContext()).load(body.getRemoteUrl()).into(content);
+
+ SimpleDateFormat sdf = new SimpleDateFormat("MM-dd, HH:mm");
+ sdf.setTimeZone(TimeZone.getTimeZone("GMT+8"));
+ String formattedDate = sdf.format(new Date(pinTime));
+ time.setText(formattedDate);
+
+ state.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if(mItemSubViewListener!=null) {
+ mItemSubViewListener.onItemSubViewClick(v, position);
+ }
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinTextMessageViewHolder.java b/app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinTextMessageViewHolder.java
new file mode 100644
index 00000000..c713744f
--- /dev/null
+++ b/app/src/main/java/io/agora/chatdemo/chat/viewholder/pinmessage/PinTextMessageViewHolder.java
@@ -0,0 +1,68 @@
+package io.agora.chatdemo.chat.viewholder.pinmessage;
+
+
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+import io.agora.chat.ChatMessage;
+import io.agora.chat.TextMessageBody;
+import io.agora.chat.uikit.adapter.EaseBaseRecyclerViewAdapter;
+import io.agora.chatdemo.R;
+
+public class PinTextMessageViewHolder extends EaseBaseRecyclerViewAdapter.ViewHolder {
+
+ private final EaseBaseRecyclerViewAdapter.OnItemSubViewClickListener mItemSubViewListener;
+ private TextView from;
+ private TextView content;
+ private TextView time;
+ private ImageView state;
+
+ public PinTextMessageViewHolder(EaseBaseRecyclerViewAdapter.OnItemSubViewClickListener mItemSubViewListener, @NonNull View itemView) {
+ super(itemView);
+ this.mItemSubViewListener = mItemSubViewListener;
+ }
+
+ @Override
+ public void initView(View itemView) {
+ super.initView(itemView);
+ from = findViewById(R.id.tv_from);
+ content = findViewById(R.id.tv_content);
+ time = findViewById(R.id.tv_time);
+ state=findViewById(R.id.iv_state);
+
+ }
+
+ @Override
+ public void setData(ChatMessage message, int position) {
+ String operatorId = message.pinnedInfo().operatorId();
+ long pinTime = message.pinnedInfo().pinTime();
+
+ from.setText(operatorId +" pinned "+message.getFrom()+"'s message");
+
+ TextMessageBody body = (TextMessageBody) message.getBody();
+ content.setText(body.getMessage());
+
+ SimpleDateFormat sdf = new SimpleDateFormat("MM-dd, HH:mm");
+ sdf.setTimeZone(TimeZone.getTimeZone("GMT+8"));
+ String formattedDate = sdf.format(new Date(pinTime));
+ time.setText(formattedDate);
+
+ state.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if(mItemSubViewListener!=null) {
+ mItemSubViewListener.onItemSubViewClick(v, position);
+ }
+ }
+ });
+
+
+ }
+}
diff --git a/app/src/main/java/io/agora/chatdemo/chat/viewmodel/ChatViewModel.java b/app/src/main/java/io/agora/chatdemo/chat/viewmodel/ChatViewModel.java
index 7311714d..8684ec3f 100644
--- a/app/src/main/java/io/agora/chatdemo/chat/viewmodel/ChatViewModel.java
+++ b/app/src/main/java/io/agora/chatdemo/chat/viewmodel/ChatViewModel.java
@@ -29,9 +29,11 @@ public class ChatViewModel extends AndroidViewModel {
private SingleSourceLiveData> makeConversationReadObservable;
private SingleSourceLiveData>> getNoPushUsersObservable;
private SingleSourceLiveData> setNoPushUsersObservable;
- private SingleSourceLiveData> chatManagerObservable;
+ private SingleSourceLiveData> reportMessageObservable;
private SingleSourceLiveData> removeMessagesObservable;
private SingleSourceLiveData> translationMessagesObservable;
+ private SingleSourceLiveData> pinMessageObservable;
+ private SingleSourceLiveData>> getPinMessageObservable;
public ChatViewModel(@NonNull Application application) {
super(application);
@@ -43,9 +45,11 @@ public ChatViewModel(@NonNull Application application) {
getNoPushUsersObservable = new SingleSourceLiveData<>();
setNoPushUsersObservable = new SingleSourceLiveData<>();
presenceObservable = new SingleSourceLiveData<>();
- chatManagerObservable = new SingleSourceLiveData<>();
+ reportMessageObservable = new SingleSourceLiveData<>();
removeMessagesObservable = new SingleSourceLiveData<>();
translationMessagesObservable = new SingleSourceLiveData<>();
+ pinMessageObservable = new SingleSourceLiveData<>();
+ getPinMessageObservable = new SingleSourceLiveData<>();
}
public LiveData>> getPresenceObservable(){
return presenceObservable;
@@ -56,8 +60,8 @@ public void fetchPresenceStatus(List userIds){
public LiveData> getChatRoomObservable() {
return chatRoomObservable;
}
- public LiveData> getChatManagerObservable(){
- return chatManagerObservable;
+ public LiveData> getReportMessageObservable(){
+ return reportMessageObservable;
}
public LiveData>> getNoPushUsersObservable() {
return getNoPushUsersObservable;
@@ -104,7 +108,7 @@ public LiveData> getMakeConversationReadObservable() {
}
public void reportMessage(String reportMsgId, String reportType, String reportReason ){
- chatManagerObservable.setSource(chatManagerRepository.reportMessage(reportMsgId,reportType,reportReason));
+ reportMessageObservable.setSource(chatManagerRepository.reportMessage(reportMsgId,reportType,reportReason));
}
public LiveData> getRemoveMessagesObservable() {
@@ -129,4 +133,19 @@ public void removeMessagesFromServer(String conversationId, Conversation.Convers
public void translationMessage(ChatMessage message,List targetLanguage){
translationMessagesObservable.setSource(chatManagerRepository.translationMessage(message,targetLanguage));
}
+
+ public void pinMessage(ChatMessage message,boolean isPinned) {
+ pinMessageObservable.setSource(chatManagerRepository.pinMessage(message,isPinned));
+ }
+
+ public LiveData> pinMessageObservable(){
+ return pinMessageObservable;
+ }
+
+ public void getPinnedMessagesFromServer(String conversationId) {
+ getPinMessageObservable.setSource(chatManagerRepository.getPinnedMessagesFromServer(conversationId));
+ }
+ public LiveData>> getPinMessageObservable(){
+ return getPinMessageObservable;
+ }
}
diff --git a/app/src/main/java/io/agora/chatdemo/conversation/ConversationListFragment.java b/app/src/main/java/io/agora/chatdemo/conversation/ConversationListFragment.java
index 3cfaa511..a72238f1 100644
--- a/app/src/main/java/io/agora/chatdemo/conversation/ConversationListFragment.java
+++ b/app/src/main/java/io/agora/chatdemo/conversation/ConversationListFragment.java
@@ -114,7 +114,6 @@ public void afterTextChanged(Editable s) {
presenceView = titleBarLayout.findViewById(R.id.presence_view);
if(presenceView != null) {
presenceView.setVisibility(View.VISIBLE);
- presenceView.setPresenceTextViewArrowVisible(true);
presenceView.setNameTextViewVisibility(View.INVISIBLE);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) presenceView.getLayoutParams();
params.setMargins(UIUtils.dp2px(mContext, 16), 0, 0, 0);
diff --git a/app/src/main/java/io/agora/chatdemo/general/dialog/SimpleDialog.java b/app/src/main/java/io/agora/chatdemo/general/dialog/SimpleDialog.java
index 6f1d523d..98b7029c 100644
--- a/app/src/main/java/io/agora/chatdemo/general/dialog/SimpleDialog.java
+++ b/app/src/main/java/io/agora/chatdemo/general/dialog/SimpleDialog.java
@@ -19,9 +19,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
-import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.Group;
import androidx.core.content.ContextCompat;
+import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
import java.lang.reflect.Field;
@@ -256,12 +256,12 @@ public T getViewById(int viewId) {
}
public static class Builder {
- public AppCompatActivity context;
+ public FragmentActivity context;
private OnConfirmClickListener listener;
private onCancelClickListener cancelClickListener;
protected Bundle bundle;
- public Builder(AppCompatActivity context) {
+ public Builder(FragmentActivity context) {
this.context = context;
this.bundle = new Bundle();
}
diff --git a/app/src/main/java/io/agora/chatdemo/general/manager/UsersManager.java b/app/src/main/java/io/agora/chatdemo/general/manager/UsersManager.java
index 78583b23..394fc92c 100644
--- a/app/src/main/java/io/agora/chatdemo/general/manager/UsersManager.java
+++ b/app/src/main/java/io/agora/chatdemo/general/manager/UsersManager.java
@@ -281,6 +281,7 @@ public void updateUserPresenceView(String username,EasePresenceView presenceView
Presence presence = DemoHelper.getInstance().getPresences().get(username);
if(presence!=null && presenceView != null) {
presenceView.setVisibility(View.VISIBLE);
+ presenceView.setPresenceTextViewArrowVisible(true);
presenceView.setPresenceData(getUserInfo(username).getAvatar(),presence);
}
}
diff --git a/app/src/main/java/io/agora/chatdemo/general/repositories/EMChatManagerRepository.java b/app/src/main/java/io/agora/chatdemo/general/repositories/EMChatManagerRepository.java
index 02ef2fd1..92e984ae 100644
--- a/app/src/main/java/io/agora/chatdemo/general/repositories/EMChatManagerRepository.java
+++ b/app/src/main/java/io/agora/chatdemo/general/repositories/EMChatManagerRepository.java
@@ -277,4 +277,56 @@ public void onError(int error, String errorMsg) {
}.asLiveData();
}
+ public LiveData> pinMessage(@NonNull ChatMessage message,boolean isPined) {
+ return new NetworkOnlyResource() {
+ @Override
+ protected void createCall(@NonNull ResultCallBack> callBack) {
+ if(isPined) {
+ getChatManager().asyncPinMessage(message.getMsgId(), new CallBack() {
+ @Override
+ public void onSuccess() {
+ callBack.onSuccess(createLiveData(message));
+ }
+
+ @Override
+ public void onError(int code, String error) {
+ callBack.onError(code, error);
+ }
+ });
+ }else{
+ getChatManager().asyncUnPinMessage(message.getMsgId(), new CallBack() {
+ @Override
+ public void onSuccess() {
+ callBack.onSuccess(createLiveData(message));
+ }
+
+ @Override
+ public void onError(int code, String error) {
+ callBack.onError(code, error);
+ }
+ });
+ }
+
+ }
+ }.asLiveData();
+ }
+
+ public LiveData>> getPinnedMessagesFromServer(String conversationId) {
+ return new NetworkOnlyResource>() {
+ @Override
+ protected void createCall(@NonNull ResultCallBack>> callBack) {
+ getChatManager().asyncGetPinnedMessagesFromServer(conversationId, new ValueCallBack>() {
+ @Override
+ public void onSuccess(List value) {
+ callBack.onSuccess(createLiveData(value));
+ }
+
+ @Override
+ public void onError(int error, String errorMsg) {
+ callBack.onError(error,errorMsg);
+ }
+ });
+ }
+ }.asLiveData();
+ }
}
diff --git a/app/src/main/java/io/agora/chatdemo/general/repositories/EMClientRepository.java b/app/src/main/java/io/agora/chatdemo/general/repositories/EMClientRepository.java
index d5c0cd9e..7448f6e3 100644
--- a/app/src/main/java/io/agora/chatdemo/general/repositories/EMClientRepository.java
+++ b/app/src/main/java/io/agora/chatdemo/general/repositories/EMClientRepository.java
@@ -41,7 +41,6 @@
import io.agora.chatdemo.general.net.ErrorCode;
import io.agora.chatdemo.general.net.Resource;
import io.agora.chatdemo.general.utils.CommonUtils;
-import io.agora.chatdemo.sign.SignInActivity;
import io.agora.cloud.HttpClientManager;
import io.agora.cloud.HttpResponse;
import io.agora.exceptions.ChatException;
@@ -408,7 +407,8 @@ protected void createCall(@NonNull ResultCallBack> callBack) {
ChatClient.getInstance().login(username, pwd, new CallBack() {
@Override
public void onSuccess() {
- success(username, callBack);
+ DemoHelper.getInstance().getUsersManager().setCurrentUser(username);
+ success(pwd, callBack);
}
@Override
diff --git a/app/src/main/java/io/agora/chatdemo/general/widget/PinInfoView.java b/app/src/main/java/io/agora/chatdemo/general/widget/PinInfoView.java
new file mode 100644
index 00000000..7818670c
--- /dev/null
+++ b/app/src/main/java/io/agora/chatdemo/general/widget/PinInfoView.java
@@ -0,0 +1,118 @@
+package io.agora.chatdemo.general.widget;
+
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;
+
+import com.google.android.gms.common.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.agora.chat.ChatMessage;
+import io.agora.chat.uikit.adapter.EaseBaseRecyclerViewAdapter;
+import io.agora.chatdemo.R;
+import io.agora.util.EMLog;
+
+public class PinInfoView extends RelativeLayout {
+
+ private List pinMessages=new ArrayList<>();
+ private View primaryView;
+ private PinMessageListViewGroup pinMessageListView;
+
+ public PinInfoView(Context context) {
+ super(context);
+ init();
+ }
+
+ public PinInfoView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ primaryView = LayoutInflater.from(getContext()).inflate(R.layout.pin_info_view, this, false);
+ addView(primaryView);
+ findViewById(R.id.tv_info2).setOnClickListener(v->{
+ showPinListView();
+ });
+
+ pinMessageListView = new PinMessageListViewGroup(getContext());
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ pinMessageListView.setVisibility(View.GONE);
+ addView(pinMessageListView, layoutParams);
+ }
+
+ private void showPinListView() {
+ primaryView.setVisibility(GONE);
+ pinMessageListView.show(pinMessages);
+ }
+ private void showPrimaryView() {
+ setVisibility(VISIBLE);
+ primaryView.setVisibility(VISIBLE);
+ pinMessageListView.setVisibility(GONE);
+ }
+
+ public void setData(List messages) {
+ pinMessages.clear();
+ if(!CollectionUtils.isEmpty(messages)) {
+ pinMessages.addAll(0,messages);
+ }
+ setVisibility(VISIBLE);
+ }
+
+ public void removeData(ChatMessage message) {
+ if(message!=null) {
+ for (int i = 0; i < pinMessages.size(); i++) {
+ if(pinMessages.get(i).getMsgId().equals(message.getMsgId())) {
+ pinMessages.remove(message);
+ break;
+ }
+ }
+ pinMessageListView.removeData(message);
+ if(pinMessages.isEmpty()) {
+ setVisibility(GONE);
+ }
+ }
+ }
+
+ public void addData(ChatMessage message) {
+ if(message!=null) {
+ pinMessages.add(0,message);
+ }
+ showPrimaryView();
+ }
+
+ public void restView() {
+ primaryView.setVisibility(VISIBLE);
+ pinMessageListView.setVisibility(GONE);
+ }
+
+ public List getPinMessages() {
+ return pinMessages;
+ }
+
+ public void setOnItemClickListener(PinMessageListViewGroup.OnItemClickListener listener) {
+ pinMessageListView.setOnItemClickListener(listener);
+ }
+
+ public void setOnItemSubViewClickListener(EaseBaseRecyclerViewAdapter.OnItemSubViewClickListener onItemSubViewClickListener) {
+ pinMessageListView.setOnItemSubViewClickListener(onItemSubViewClickListener);
+ }
+
+ public void setInnerLayoutMaxHeight(int height) {
+ if(pinMessageListView!=null) {
+ EMLog.d("PinInfoView", "setInnerLayoutMaxHeight: " + height);
+ pinMessageListView.setConstraintLayoutMaxHeight(height);
+ }
+ }
+}
+
+
+
diff --git a/app/src/main/java/io/agora/chatdemo/general/widget/PinMessageListViewGroup.java b/app/src/main/java/io/agora/chatdemo/general/widget/PinMessageListViewGroup.java
new file mode 100644
index 00000000..779de9e4
--- /dev/null
+++ b/app/src/main/java/io/agora/chatdemo/general/widget/PinMessageListViewGroup.java
@@ -0,0 +1,164 @@
+package io.agora.chatdemo.general.widget;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.ConstraintSet;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.google.android.gms.common.util.CollectionUtils;
+
+import java.util.List;
+
+import io.agora.chat.ChatMessage;
+import io.agora.chat.uikit.adapter.EaseBaseRecyclerViewAdapter;
+import io.agora.chat.uikit.widget.EaseRecyclerView;
+import io.agora.chatdemo.R;
+import io.agora.chatdemo.chat.PinListItemSpaceDecoration;
+import io.agora.chatdemo.chat.adapter.PinMessageListAdapter;
+
+public class PinMessageListViewGroup extends LinearLayout {
+
+ private ConstraintLayout constraintLayout;
+ private EaseRecyclerView recyclerView;
+ private PinMessageListAdapter adapter;
+ private OnItemClickListener itemClickListener;
+ private TextView tvCount;
+ private View clBottom;
+ private EaseBaseRecyclerViewAdapter.OnItemSubViewClickListener itemSubViewClickListener;
+
+ public PinMessageListViewGroup(Context context) {
+ super(context);
+ init();
+ }
+
+ public PinMessageListViewGroup(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ setClickable(true);
+ setBackgroundColor(Color.parseColor("#80000000"));
+
+ constraintLayout = (ConstraintLayout) LayoutInflater.from(getContext()).inflate(R.layout.pin_message_list_view_group, this, false);
+ addView(constraintLayout);
+
+ tvCount = findViewById(R.id.tv_count);
+ recyclerView = findViewById(R.id.rv_list);
+ clBottom = findViewById(R.id.cl_bottom);
+
+
+ adapter = new PinMessageListAdapter();
+
+
+ int space = dpToPx(8);
+ PinListItemSpaceDecoration itemDecoration = new PinListItemSpaceDecoration(space);
+ recyclerView.addItemDecoration(itemDecoration);
+ recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+
+ adapter.setOnItemClickListener(new io.agora.chat.uikit.interfaces.OnItemClickListener() {
+ @Override
+ public void onItemClick(View view, int position) {
+ if (itemClickListener != null) {
+ itemClickListener.onItemClick(adapter.getItem(position));
+ }
+ }
+ });
+ adapter.setOnItemSubViewClickListener(new EaseBaseRecyclerViewAdapter.OnItemSubViewClickListener() {
+ @Override
+ public void onItemSubViewClick(View view, int position) {
+ if (itemSubViewClickListener != null) {
+ itemSubViewClickListener.onItemSubViewClick(view, position);
+ }
+ }
+ });
+
+ recyclerView.setAdapter(adapter);
+
+ }
+
+ long startY = 0;
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ startY = (long) event.getY();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (!recyclerView.canScrollVertically(-1) && startY-event.getY()> 20) {
+ ((PinInfoView) getParent()).restView();
+ return true;
+ }
+ if (event.getX() < 0 || event.getX() > getWidth() ||
+ event.getY() < 0 || event.getY() > getHeight()
+ || event.getY() > clBottom.getTop()) {
+ ((PinInfoView) getParent()).restView();
+ return true;
+ }
+ break;
+ }
+ return super.dispatchTouchEvent(event);
+ }
+
+ public void setData(List data) {
+ tvCount.setText(String.valueOf(data.size()) + " Pin Message");
+ adapter.setData(data);
+ }
+
+ public void setOnItemClickListener(OnItemClickListener listener) {
+ this.itemClickListener = listener;
+ }
+
+ public void setOnItemSubViewClickListener(EaseBaseRecyclerViewAdapter.OnItemSubViewClickListener listener) {
+ this.itemSubViewClickListener = listener;
+ }
+
+ public void show(List messages) {
+ setVisibility(VISIBLE);
+ setData(messages);
+ }
+
+ public void removeData(ChatMessage message) {
+ List messageList = adapter.getData();
+ if (messageList != null && message != null) {
+ for (int i = 0; i < messageList.size(); i++) {
+ if (messageList.get(i).getMsgId().equals(message.getMsgId())) {
+ messageList.remove(message);
+ break;
+ }
+ }
+ adapter.notifyDataSetChanged();
+ }
+ if (CollectionUtils.isEmpty(adapter.getData())) {
+ setVisibility(GONE);
+ }
+ }
+
+ public void setConstraintLayoutMaxHeight(int height) {
+ ConstraintSet constraintSet = new ConstraintSet();
+ constraintSet.clone(constraintLayout);
+ int recyclerViewId = R.id.rv_list;
+ int rvHeight = height - tvCount.getHeight() - clBottom.getHeight();
+ constraintSet.constrainMaxHeight(recyclerViewId, rvHeight);
+ constraintSet.applyTo(constraintLayout);
+ }
+
+ public interface OnItemClickListener {
+ void onItemClick(ChatMessage message);
+ }
+
+ private int dpToPx(int dp) {
+ float density = getResources().getDisplayMetrics().density;
+ return (int) (dp * density + 0.5f);
+ }
+}
+
diff --git a/app/src/main/res/drawable-xxhdpi/chat_item_menu_pin.png b/app/src/main/res/drawable-xxhdpi/chat_item_menu_pin.png
new file mode 100644
index 0000000000000000000000000000000000000000..63482071474c8146c114dad523aa0eafde235024
GIT binary patch
literal 760
zcmVFa|p=j-FJ?c!vgeyBTNH^a{!@}GsZ^40p<
zk1Ts_jr+kbj=lM{uN}q*`LMd7O^1c~25yi~1zTeQtXH;37v#g*V8*@G;m~vT6IX{B
zhH7W)ltcWr+6~7r3l8TEb5@}`oM)KNcKF{SpD>TrqEKt33B-nOgZn+L^7S*KjRu8r
zNE2pz(9Wu60
zTweo@;c7&?S+4ZkklasDt7rbcg)$~gD+dzhO}YxxO_qBcrcwRyyWP~QF}|mL_O%7U
zJMc_7Y#g}rT?Z28S$@%X9FLfR{z18VkSI_xW}rRF)q{lnC=UJFcmg)E-h+gF>3N6p
zP>-1%Xy~$8PN>Js4s?t1Y>~MR(_DfwssmYy75kW#^>h1<0@ztThbKisdZzEcTEte093$hmhGr$WAgRw&dp@(Z&;;acPq=5Ne^eV*p<6y7anv}SwmP-hk9
zsdId$USsBJOf#jji;EQ&!XfSf+1T1(q4&5PxWU?4yn_4-w#K+Amc7P?w(h#Vpu+QC
zsI_hNu#ZBF)ZlTT4KkKuwQ125D47B!Q=nuDluUt=DNr&6N~S=`6ewBlfo`AJR^k(W
qS!r-sEMC-tv51I>h=_=YOye&{$l-{BxQvMa0000pB*akqtbeWaDyJ9oB(dHa)N=jWz?L
z!+9TQ)H`ZyC$%vu-0bvf1fT_s)j*$s|FnC@-f&gWl5C7qB3s?V!(dfABf?@tr}T^&;%T|-TUB;qw5)vjoYZR
zuCj4EHN-PJpT@SZCmXkwr|Az|g2QY6s|!}JD=Sb+S_G38tg);>T3C#(g{3D(UAA#D
zsDcL4!tv-@KwVZOO(5a5*zkZNQo{B|S5G!xAu4zHXB`xg66T4j2F6Jx8^I9X0Y&7d
zw0P?=N_yD{#;U5*T27du8W_Wn?3~Iie-{P?5`~-oscUYpY*aP^Gip{CoDCO@U|v?h
zcYQ7>kSH7iGBCP!WFs7S1qvhz^C^>3*rKcksS5zBd0w)DL}3S^0JO+RmcS2{@Z=FD
zvjCLIZe$TKr4nzVaMu?%gcLf>b{bQRM=J5MJFT%)Tn^7{nHTa^;-!QUrQal?vQJRF
z_^x_tAtlV0rs3*{oU$=vm!OE0aP%CbKjqS8@>_cNNy7*fkzR)xKmPdp+Z@+f3`3n#
zkF;=K9C9s<1<+}7ce%6Ed@kegkMag5)
zC!0eGvX(Vbrqw@NT0_~mA9dDMR-pWPn8!0~N>;F;5}ftY!+^H?N8SY(L|RxzRut?h
z3+s>yF3OjkGP}F*eP8>(d!FoFCP?*Q>usmMizwMeQK4M2;>o8jV{lcb)1eK~#buHL
z_IaVZgv0NT8c8RS#>sh=QtV<%YrNr`2c3+0#qm;nmdSK%Q}Q9o!nzq#)VB$CZWdqg
zy>RcNw+9)Mn58fH`zB99Z9e73rZZ+9NF}X)>%GrEeE1KrT&8_~6vDQ+hs{i~a1xmb
sFOpw`{tNut!Pi?uN3R711qCmFU*wjrbM6~;C;$Ke07*qoM6N<$f?0;m`2YX_
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable-xxhdpi/chat_pin_rectangle.png b/app/src/main/res/drawable-xxhdpi/chat_pin_rectangle.png
new file mode 100644
index 0000000000000000000000000000000000000000..74b1e28373bb69b192702c193310f5351a3c05aa
GIT binary patch
literal 373
zcmV-*0gC>KP){X%aLX9^ppO;IS%{z`^>S<$-uL7C{~vC{eu>BpOjg86J@>O_xOdZo
zkFZ}`V0mI>o$|q?(%M~k637co)`WZw^Xav|-s!NJZux%!khxb*
Tioq2a00000NkvXXu0mjf@adY0
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable-xxhdpi/chat_pininfo_icon.png b/app/src/main/res/drawable-xxhdpi/chat_pininfo_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d966f0fd93005369b4a3e9f6203695409ffd91b
GIT binary patch
literal 554
zcmV+_0@eMAP)%gKW@lKsM+IWdrC0utC`XIss*ZumN-eYKtIf1QyRE&k6OBt_X_b
zNt8&MCO(H3{}-95{7ysRh6qdIC7!s-lcDsBn)B9t(N8Si>IEI
z)ia7+gG=S1k;qTfH}I4nqJ$yb)WB2k#B-fNfidC6jHlj=uoqGr1>weg82pJu#62=H
zuKO%Z;JF^cp_kOywH@c1=Uks=@9kwm_-DA+d!a%>Yc=FZBV!uFrp*7EvMKouI>#@QApt-Lnl(>e*U&Pi
szPhDl?CZ?&O<-ujrgNJ0Yn?}ZpK81R{#J207*qoM6N<$f>FKlvj6}9
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable/shape_gray_ebebeb_corner_8.xml b/app/src/main/res/drawable/shape_gray_ebebeb_corner_8.xml
new file mode 100644
index 00000000..7435f4ae
--- /dev/null
+++ b/app/src/main/res/drawable/shape_gray_ebebeb_corner_8.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shape_gray_f5f5f5_corner_8.xml b/app/src/main/res/drawable/shape_gray_f5f5f5_corner_8.xml
new file mode 100644
index 00000000..51bef490
--- /dev/null
+++ b/app/src/main/res/drawable/shape_gray_f5f5f5_corner_8.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/pin_info_view.xml b/app/src/main/res/layout/pin_info_view.xml
new file mode 100644
index 00000000..c4a475be
--- /dev/null
+++ b/app/src/main/res/layout/pin_info_view.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/pin_message_list_view_group.xml b/app/src/main/res/layout/pin_message_list_view_group.xml
new file mode 100644
index 00000000..89f66640
--- /dev/null
+++ b/app/src/main/res/layout/pin_message_list_view_group.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/pinlist_default.xml b/app/src/main/res/layout/pinlist_default.xml
new file mode 100644
index 00000000..313a185b
--- /dev/null
+++ b/app/src/main/res/layout/pinlist_default.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/pinlist_image.xml b/app/src/main/res/layout/pinlist_image.xml
new file mode 100644
index 00000000..0ad95f3d
--- /dev/null
+++ b/app/src/main/res/layout/pinlist_image.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/pinlist_text.xml b/app/src/main/res/layout/pinlist_text.xml
new file mode 100644
index 00000000..0d0eac24
--- /dev/null
+++ b/app/src/main/res/layout/pinlist_text.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 128d8f9a..379132dd 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -70,6 +70,8 @@
#979797
#999999
#E6E6E6
+ #F5F5F5
+ #EbEbEb
#EBF2FF
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
index 8bf1a06a..f376d544 100644
--- a/app/src/main/res/values/ids.xml
+++ b/app/src/main/res/values/ids.xml
@@ -15,4 +15,5 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 17ad949e..92d2a273 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -403,6 +403,8 @@
%d/10
0/10
Report
+ Pin
+ UnPin
Delete %1$d messages
My Alias in Group
@@ -444,5 +446,8 @@
Translation failed Retry
Translated by Agora Chat View Original Text
Translated by Agora Chat View Translation
+ Pin Message
+ Confirm to remove pinned message?
+ The quoted message does not exist
\ No newline at end of file
From beb78a0a72b976b043de301cd1386c108f7327d0 Mon Sep 17 00:00:00 2001
From: xuchengpu mac <1550540124@qq.com>
Date: Sun, 7 Apr 2024 17:27:15 +0800
Subject: [PATCH 09/11] update version to 1.3.0
---
app/build.gradle | 1 -
settings.gradle | 4 +++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 4da01947..9b13c96f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -48,7 +48,6 @@ android {
//
// externalNativeBuild {
// ndkBuild {
-//// arguments "NDK_LIBS_OUT=libs", "all"
// abiFilters "arm64-v8a","armeabi-v7a"
// arguments '-j8'
// }
diff --git a/settings.gradle b/settings.gradle
index 3d4a09ce..09cf7fd3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -4,4 +4,6 @@ include ':app'
//include ':chat-callkit'
//project(':chat-callkit').projectDir = new File('../AgoraChat-CallKit-android/chat-callkit')
//include ':hyphenatechatsdk'
-//project(':hyphenatechatsdk').projectDir = new File('../emclient-android/hyphenatechatsdk')
\ No newline at end of file
+//project(':hyphenatechatsdk').projectDir = new File('../emclient-android/hyphenatechatsdk')
+//include ':ease-linux'
+//project(':ease-linux').projectDir = new File('../emclient-linux')
\ No newline at end of file
From 35e4bf32ce8632f540f2d21cadab1f60b374a62c Mon Sep 17 00:00:00 2001
From: apex-wang <1746807718@qq.com>
Date: Thu, 24 Oct 2024 18:34:12 +0800
Subject: [PATCH 10/11] add app-kotlin
---
README.md | 55 ++-
app-kotlin/.gitignore | 2 +
app-kotlin/README.md | 292 +++++++++++++
app-kotlin/build.gradle.kts | 160 +++++++
app-kotlin/google-services.json | 39 ++
app-kotlin/jni/Android.mk | 24 +
app-kotlin/jni/Application.mk | 6 +
app-kotlin/keystore/sdkdemo.jks | Bin 0 -> 4999 bytes
app-kotlin/proguard-rules.pro | 21 +
app-kotlin/src/main/AndroidManifest.xml | 154 +++++++
.../io/agora/chatdemo/DemoApplication.kt | 57 +++
.../kotlin/io/agora/chatdemo/DemoHelper.kt | 156 +++++++
.../kotlin/io/agora/chatdemo/MainActivity.kt | 323 ++++++++++++++
.../io/agora/chatdemo/base/ActivityState.kt | 32 ++
.../agora/chatdemo/base/BaseDialogFragment.kt | 131 ++++++
.../agora/chatdemo/base/BaseInitActivity.kt | 38 ++
.../io/agora/chatdemo/base/BaseRepository.kt | 10 +
.../base/UserActivityLifecycleCallbacks.kt | 104 +++++
.../kotlin/io/agora/chatdemo/bean/Language.kt | 26 ++
.../io/agora/chatdemo/bean/LoginResult.kt | 11 +
.../io/agora/chatdemo/bean/PresenceData.kt | 34 ++
.../CallKitActivityLifecycleCallback.kt | 109 +++++
.../agora/chatdemo/callkit/CallKitManager.kt | 303 +++++++++++++
.../io/agora/chatdemo/callkit/CallUserInfo.kt | 40 ++
.../callkit/ChatVoiceCallViewHolder.kt | 48 ++
.../chatdemo/callkit/DemoCallKitListener.kt | 182 ++++++++
.../callkit/MultipleInviteViewHolder.kt | 6 +
.../activity/CallMultipleBaseActivity.kt | 14 +
.../activity/CallMultipleInviteActivity.kt | 131 ++++++
.../activity/CallSingleBaseActivity.kt | 24 +
.../adapter/ConferenceInviteAdapter.kt | 44 ++
.../chatdemo/callkit/extensions/Activity.kt | 88 ++++
.../chatdemo/callkit/extensions/JSONObject.kt | 14 +
.../fragment/ConferenceInviteFragment.kt | 81 ++++
.../ConferenceMemberSelectViewHolder.kt | 76 ++++
.../callkit/views/ChatRowConferenceInvite.kt | 39 ++
.../callkit/views/ChatRowVoiceCall.kt | 42 ++
.../io/agora/chatdemo/common/DemoConstant.kt | 26 ++
.../io/agora/chatdemo/common/DemoDataModel.kt | 330 ++++++++++++++
.../io/agora/chatdemo/common/ErrorCode.kt | 66 +++
.../agora/chatdemo/common/ListenersWrapper.kt | 148 +++++++
.../chatdemo/common/PreferenceManager.kt | 55 +++
.../io/agora/chatdemo/common/PresenceCache.kt | 31 ++
.../common/PushActivityLifecycleCallback.kt | 39 ++
.../io/agora/chatdemo/common/PushManager.kt | 86 ++++
.../chatdemo/common/dialog/SimpleDialog.kt | 269 ++++++++++++
.../fragment/DemoAgreementDialogFragment.kt | 96 ++++
.../dialog/fragment/DemoDialogFragment.kt | 382 ++++++++++++++++
.../chatdemo/common/extensions/String.kt | 32 ++
.../common/extensions/internal/Activity.kt | 10 +
.../common/extensions/internal/ChatGroup.kt | 8 +
.../common/extensions/internal/ChatOptions.kt | 27 ++
.../extensions/internal/ChatUserInfo.kt | 12 +
.../common/extensions/internal/EditText.kt | 101 +++++
.../common/extensions/internal/SwitchView.kt | 9 +
.../common/helper/DeveloperModeHelper.kt | 22 +
.../common/helper/LocalNotifyHelper.kt | 50 +++
.../common/helper/MenuFilterHelper.kt | 27 ++
.../agora/chatdemo/common/room/AppDatabase.kt | 42 ++
.../chatdemo/common/room/dao/DemoUserDao.kt | 96 ++++
.../chatdemo/common/room/entity/DemoUser.kt | 21 +
.../common/room/extensions/DbEntity.kt | 15 +
.../common/room/viewmodel/BusUserViewModel.kt | 130 ++++++
.../suspend/ChatUserInfoManagerSuspend.kt | 64 +++
.../common/suspend/PresenceManagerSuspend.kt | 118 +++++
.../common/suspend/PushManagerSuspend.kt | 45 ++
.../io/agora/chatdemo/fcm/FCMMSGService.kt | 26 ++
.../presence/controller/PresenceController.kt | 135 ++++++
.../presence/interfaces/IPresenceRequest.kt | 38 ++
.../interfaces/IPresenceResultView.kt | 61 +++
.../repository/ChatPresenceRepository.kt | 39 ++
.../presence/utils/EasePresenceUtil.kt | 101 +++++
.../presence/viewmodel/PresenceViewModel.kt | 96 ++++
.../agora/chatdemo/interfaces/IAttachView.kt | 10 +
.../agora/chatdemo/interfaces/IMainRequest.kt | 15 +
.../chatdemo/interfaces/IMainResultView.kt | 16 +
.../LanguageListItemSelectListener.kt | 8 +
.../agora/chatdemo/page/chat/ChatActivity.kt | 48 ++
.../agora/chatdemo/page/chat/ChatFragment.kt | 130 ++++++
.../page/chat/CustomMessagesAdapter.kt | 52 +++
.../page/contact/ChatContactCheckActivity.kt | 59 +++
.../page/contact/ChatContactDetailActivity.kt | 156 +++++++
.../contact/ChatContactListFragmentEvent.kt | 121 ++++++
.../page/contact/ChatNewRequestsActivity.kt | 23 +
.../conversation/ConversationListFragment.kt | 136 ++++++
.../page/group/ChatCreateGroupActivity.kt | 32 ++
.../page/group/ChatGroupDetailActivity.kt | 61 +++
.../page/group/repository/GroupRepository.kt | 32 ++
.../chatdemo/page/login/LoginActivity.kt | 61 +++
.../page/login/fragment/LoginFragment.kt | 281 ++++++++++++
.../page/login/fragment/ServerSetFragment.kt | 221 ++++++++++
.../page/login/viewModel/LoginViewModel.kt | 46 ++
.../page/me/CameraAndCroppingController.kt | 145 +++++++
.../page/me/activity/AboutActivity.kt | 65 +++
.../page/me/activity/CurrencyActivity.kt | 141 ++++++
.../me/activity/EditUserNicknameActivity.kt | 101 +++++
.../page/me/activity/FeaturesActivity.kt | 89 ++++
.../me/activity/LanguageSettingActivity.kt | 196 +++++++++
.../page/me/activity/NotifyActivity.kt | 104 +++++
.../me/activity/UserInformationActivity.kt | 409 ++++++++++++++++++
.../page/me/activity/WebViewActivity.kt | 77 ++++
.../page/me/fragment/AboutMeFragment.kt | 261 +++++++++++
.../chatdemo/page/splash/SplashActivity.kt | 106 +++++
.../splash/repository/ChatClientRepository.kt | 263 +++++++++++
.../page/splash/viewModel/SplashViewModel.kt | 15 +
.../repository/ProfileInfoRepository.kt | 245 +++++++++++
.../chatdemo/repository/PushRepository.kt | 30 ++
.../io/agora/chatdemo/uikit/UIKitManager.kt | 148 +++++++
.../chatdemo/utils/CameraAndCropFileUtils.kt | 90 ++++
.../io/agora/chatdemo/utils/LanguageUtil.kt | 12 +
.../io/agora/chatdemo/utils/ToastUtils.kt | 401 +++++++++++++++++
.../agora/chatdemo/viewmodel/MainViewModel.kt | 59 +++
.../viewmodel/ProfileInfoViewModel.kt | 65 +++
.../agora/chatdemo/viewmodel/PushViewModel.kt | 27 ++
.../src/main/res/anim/slide_in_from_left.xml | 14 +
.../src/main/res/anim/slide_in_from_right.xml | 14 +
.../src/main/res/anim/slide_out_to_left.xml | 14 +
.../src/main/res/anim/slide_out_to_right.xml | 14 +
.../demo_dialog_btn_text_color_selector.xml | 5 +
.../res/color/demo_main_tab_text_selector.xml | 5 +
.../main/res/drawable-hdpi/contact_title.png | Bin 0 -> 1920 bytes
.../res/drawable-hdpi/conversation_title.png | Bin 0 -> 1091 bytes
.../drawable-v24/ic_launcher_foreground.xml | 30 ++
.../main/res/drawable-xhdpi/contact_title.png | Bin 0 -> 2491 bytes
.../res/drawable-xhdpi/conversation_title.png | Bin 0 -> 1350 bytes
.../res/drawable-xhdpi/d_chat_voice_call.png | Bin 0 -> 554 bytes
.../main/res/drawable-xhdpi/em_toast_fail.png | Bin 0 -> 1083 bytes
.../res/drawable-xhdpi/em_toast_success.png | Bin 0 -> 1023 bytes
.../res/drawable-xhdpi/splash_bg_dark.webp | Bin 0 -> 8410 bytes
.../res/drawable-xhdpi/splash_bg_light.webp | Bin 0 -> 17812 bytes
.../res/drawable-xxhdpi/contact_title.png | Bin 0 -> 3720 bytes
.../drawable-xxhdpi/conversation_title.png | Bin 0 -> 2031 bytes
.../res/drawable-xxhdpi/d_chat_voice_call.png | Bin 0 -> 788 bytes
.../drawable-xxhdpi/demo_check_checked.png | Bin 0 -> 615 bytes
.../drawable-xxhdpi/demo_check_uncheck.png | Bin 0 -> 461 bytes
.../drawable-xxhdpi/ease_presence_away.png | Bin 0 -> 2021 bytes
.../drawable-xxhdpi/ease_presence_busy.png | Bin 0 -> 1750 bytes
.../drawable-xxhdpi/ease_presence_custom.png | Bin 0 -> 7033 bytes
.../ease_presence_do_not_disturb.png | Bin 0 -> 2302 bytes
.../drawable-xxhdpi/ease_presence_offline.png | Bin 0 -> 1299 bytes
.../drawable-xxhdpi/ease_presence_online.png | Bin 0 -> 1361 bytes
.../main/res/drawable-xxhdpi/icon_about.png | Bin 0 -> 1398 bytes
.../res/drawable-xxhdpi/icon_currency.png | Bin 0 -> 1840 bytes
.../res/drawable-xxhdpi/icon_information.png | Bin 0 -> 1600 bytes
.../icon_login_attention_line.png | Bin 0 -> 807 bytes
.../main/res/drawable-xxhdpi/icon_logo.png | Bin 0 -> 78402 bytes
.../main/res/drawable-xxhdpi/icon_next.png | Bin 0 -> 455 bytes
.../res/drawable-xxhdpi/icon_presence.png | Bin 0 -> 1953 bytes
.../main/res/drawable-xxhdpi/icon_privacy.png | Bin 0 -> 1671 bytes
.../drawable-xxhdpi/main_tab_conversation.png | Bin 0 -> 1568 bytes
.../main_tab_conversation_selected.png | Bin 0 -> 1304 bytes
.../res/drawable-xxhdpi/main_tab_friends.png | Bin 0 -> 1994 bytes
.../main_tab_friends_selected.png | Bin 0 -> 1608 bytes
.../main/res/drawable-xxhdpi/main_tab_me.png | Bin 0 -> 1284 bytes
.../drawable-xxhdpi/main_tab_me_selected.png | Bin 0 -> 1061 bytes
.../main/res/drawable-xxhdpi/phone_pick.png | Bin 0 -> 1138 bytes
.../res/drawable-xxhdpi/sign_clear_icon.png | Bin 0 -> 1404 bytes
.../src/main/res/drawable-xxhdpi/sign_eye.png | Bin 0 -> 1403 bytes
.../res/drawable-xxhdpi/sign_eye_slash.png | Bin 0 -> 1499 bytes
.../main/res/drawable-xxhdpi/video_call.png | Bin 0 -> 1474 bytes
.../main/res/drawable-xxhdpi/video_camera.png | Bin 0 -> 827 bytes
.../res/drawable-xxxhdpi/contact_title.png | Bin 0 -> 4919 bytes
.../drawable-xxxhdpi/conversation_title.png | Bin 0 -> 2559 bytes
.../main/res/drawable-xxxhdpi/icon_notify.png | Bin 0 -> 1476 bytes
.../res/drawable/demo_cb_agreement_select.xml | 5 +
.../demo_dialog_btn_left_selector.xml | 34 ++
.../demo_dialog_btn_right_selector.xml | 34 ++
.../res/drawable/demo_dialog_btn_selector.xml | 34 ++
.../main/res/drawable/demo_login_btn_bg.xml | 23 +
.../main/res/drawable/demo_login_et_bg.xml | 8 +
.../res/drawable/demo_login_version_bg.xml | 10 +
.../demo_main_tab_conversation_selector.xml | 5 +
.../demo_main_tab_friends_selector.xml | 5 +
.../drawable/demo_main_tab_me_selector.xml | 5 +
.../drawable/demo_main_unread_count_bg.xml | 7 +
.../drawable/demo_switch_thumb_selector.xml | 19 +
.../drawable/demo_switch_track_selector.xml | 20 +
.../src/main/res/drawable/demo_toast_bg.xml | 7 +
.../res/drawable/ic_launcher_background.xml | 170 ++++++++
.../main/res/layout/activity_main_layout.xml | 36 ++
.../main/res/layout/demo_activity_about.xml | 119 +++++
.../demo_activity_conference_invite.xml | 22 +
.../res/layout/demo_activity_currency.xml | 46 ++
.../res/layout/demo_activity_features.xml | 82 ++++
.../res/layout/demo_activity_language.xml | 26 ++
.../main/res/layout/demo_activity_login.xml | 23 +
.../layout/demo_activity_me_information.xml | 121 ++++++
.../demo_activity_me_information_edit.xml | 51 +++
.../main/res/layout/demo_activity_notify.xml | 35 ++
.../main/res/layout/demo_activity_webview.xml | 25 ++
.../src/main/res/layout/demo_badge_home.xml | 16 +
.../res/layout/demo_fragment_about_me.xml | 146 +++++++
.../res/layout/demo_fragment_dialog_base.xml | 87 ++++
.../main/res/layout/demo_fragment_login.xml | 122 ++++++
.../layout/demo_fragment_middle_agreement.xml | 28 ++
.../res/layout/demo_fragment_server_set.xml | 101 +++++
.../demo_row_received_conference_invite.xml | 56 +++
.../layout/demo_row_received_voice_call.xml | 70 +++
.../demo_row_sent_conference_invite.xml | 80 ++++
.../res/layout/demo_row_sent_voice_call.xml | 94 ++++
.../main/res/layout/demo_splash_activity.xml | 34 ++
.../src/main/res/layout/demo_toast_layout.xml | 58 +++
.../res/layout/ease_layout_language_item.xml | 41 ++
.../main/res/menu/bottom_main_nav_menu.xml | 21 +
.../src/main/res/menu/demo_chat_menu.xml | 15 +
.../res/menu/demo_conference_invite_menu.xml | 12 +
.../res/menu/demo_language_menu_confirm.xml | 11 +
.../main/res/menu/demo_server_set_menu.xml | 11 +
.../res/mipmap-anydpi-v26/ic_launcher.xml | 6 +
.../mipmap-anydpi-v26/ic_launcher_round.xml | 6 +
.../main/res/mipmap-hdpi/demo_launcher.png | Bin 0 -> 4008 bytes
.../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes
.../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes
.../main/res/mipmap-mdpi/demo_launcher.png | Bin 0 -> 2724 bytes
.../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes
.../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes
.../main/res/mipmap-xhdpi/demo_launcher.png | Bin 0 -> 6911 bytes
.../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes
.../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes
.../main/res/mipmap-xxhdpi/demo_launcher.png | Bin 0 -> 10348 bytes
.../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes
.../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes
.../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes
.../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes
.../src/main/res/values-night/colors.xml | 5 +
.../src/main/res/values-night/styles.xml | 39 ++
.../src/main/res/values-night/themes.xml | 8 +
app-kotlin/src/main/res/values/colors.xml | 28 ++
app-kotlin/src/main/res/values/dimens.xml | 22 +
app-kotlin/src/main/res/values/ids.xml | 26 ++
app-kotlin/src/main/res/values/strings.xml | 154 +++++++
app-kotlin/src/main/res/values/styles.xml | 187 ++++++++
app-kotlin/src/main/res/values/themes.xml | 10 +
app/build.gradle | 12 +-
app/src/main/AndroidManifest.xml | 3 +-
.../chatdemo/av/DemoCallKitListener.java | 2 +-
.../io/agora/chatdemo/base/BaseActivity.java | 2 +-
.../io/agora/chatdemo/base/BaseFragment.java | 4 +-
.../io/agora/chatdemo/chat/ChatActivity.java | 8 +-
.../io/agora/chatdemo/chat/ChatRowCall.java | 32 +-
.../chatdemo/chat/CustomChatFragment.java | 22 +-
.../chat/chatrow/ChatRowCustomTextView.java | 8 +-
.../chatrow/ChatRowSystemNotification.java | 2 +-
.../chatthread/ChatThreadActivity.java | 4 +-
.../chatthread/ChatThreadCreateActivity.java | 6 +-
.../chatthread/ChatThreadFragment.java | 11 +-
.../contact/AddContactOrGroupFragment.java | 2 +-
.../contact/SearchContactFragment.java | 2 +-
.../ConversationListFragment.java | 4 +-
.../general/manager/UsersManager.java | 2 +-
.../chatdemo/general/models/PresenceData.java | 12 +-
.../general/widget/EasePresenceView.java | 8 +-
.../global/BottomSheetChildHelper.java | 2 +-
.../group/adapter/HomeHeaderMenuAdapter.java | 2 +-
.../fragments/BottomSheetMenuFragment.java | 2 +-
.../fragments/GroupAllMembersFragment.java | 4 +-
...ultiplyVideoSelectMemberChildFragment.java | 2 +-
.../chatdemo/me/CustomPresenceActivity.java | 4 +-
.../java/io/agora/chatdemo/me/MeFragment.java | 4 +-
.../chatdemo/me/SetPresenceFragment.java | 6 +-
build.gradle | 51 ---
build.gradle.kts | 29 ++
gradle.properties | 7 +-
gradle/wrapper/gradle-wrapper.properties | 4 +-
settings.gradle | 9 -
settings.gradle.kts | 37 ++
266 files changed, 13027 insertions(+), 170 deletions(-)
create mode 100644 app-kotlin/.gitignore
create mode 100644 app-kotlin/README.md
create mode 100644 app-kotlin/build.gradle.kts
create mode 100644 app-kotlin/google-services.json
create mode 100644 app-kotlin/jni/Android.mk
create mode 100644 app-kotlin/jni/Application.mk
create mode 100644 app-kotlin/keystore/sdkdemo.jks
create mode 100644 app-kotlin/proguard-rules.pro
create mode 100644 app-kotlin/src/main/AndroidManifest.xml
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/DemoApplication.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/DemoHelper.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/MainActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/base/ActivityState.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseDialogFragment.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseInitActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseRepository.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/base/UserActivityLifecycleCallbacks.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/Language.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/LoginResult.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/PresenceData.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallKitActivityLifecycleCallback.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallKitManager.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallUserInfo.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/ChatVoiceCallViewHolder.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/DemoCallKitListener.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/MultipleInviteViewHolder.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallMultipleBaseActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallMultipleInviteActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallSingleBaseActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/adapter/ConferenceInviteAdapter.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/extensions/Activity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/extensions/JSONObject.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/fragment/ConferenceInviteFragment.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/holder/ConferenceMemberSelectViewHolder.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/views/ChatRowConferenceInvite.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/views/ChatRowVoiceCall.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/DemoConstant.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/DemoDataModel.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/ErrorCode.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/ListenersWrapper.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PreferenceManager.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PresenceCache.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PushActivityLifecycleCallback.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PushManager.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/SimpleDialog.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/fragment/DemoAgreementDialogFragment.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/fragment/DemoDialogFragment.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/String.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/Activity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatGroup.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatOptions.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatUserInfo.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/EditText.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/SwitchView.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/DeveloperModeHelper.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/LocalNotifyHelper.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/MenuFilterHelper.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/AppDatabase.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/dao/DemoUserDao.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/entity/DemoUser.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/extensions/DbEntity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/viewmodel/BusUserViewModel.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/ChatUserInfoManagerSuspend.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/PresenceManagerSuspend.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/PushManagerSuspend.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/fcm/FCMMSGService.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/controller/PresenceController.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/interfaces/IPresenceRequest.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/interfaces/IPresenceResultView.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/repository/ChatPresenceRepository.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/utils/EasePresenceUtil.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/viewmodel/PresenceViewModel.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IAttachView.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IMainRequest.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IMainResultView.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/LanguageListItemSelectListener.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/ChatActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/ChatFragment.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/CustomMessagesAdapter.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactCheckActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactDetailActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactListFragmentEvent.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatNewRequestsActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/conversation/ConversationListFragment.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/ChatCreateGroupActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/ChatGroupDetailActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/repository/GroupRepository.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/LoginActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/fragment/LoginFragment.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/fragment/ServerSetFragment.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/viewModel/LoginViewModel.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/CameraAndCroppingController.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/AboutActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/CurrencyActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/EditUserNicknameActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/FeaturesActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/LanguageSettingActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/NotifyActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/UserInformationActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/WebViewActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/fragment/AboutMeFragment.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/splash/SplashActivity.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/splash/repository/ChatClientRepository.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/page/splash/viewModel/SplashViewModel.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/repository/ProfileInfoRepository.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/repository/PushRepository.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/uikit/UIKitManager.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/utils/CameraAndCropFileUtils.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/utils/LanguageUtil.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/utils/ToastUtils.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/viewmodel/MainViewModel.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/viewmodel/ProfileInfoViewModel.kt
create mode 100644 app-kotlin/src/main/kotlin/io/agora/chatdemo/viewmodel/PushViewModel.kt
create mode 100644 app-kotlin/src/main/res/anim/slide_in_from_left.xml
create mode 100644 app-kotlin/src/main/res/anim/slide_in_from_right.xml
create mode 100644 app-kotlin/src/main/res/anim/slide_out_to_left.xml
create mode 100644 app-kotlin/src/main/res/anim/slide_out_to_right.xml
create mode 100644 app-kotlin/src/main/res/color/demo_dialog_btn_text_color_selector.xml
create mode 100644 app-kotlin/src/main/res/color/demo_main_tab_text_selector.xml
create mode 100644 app-kotlin/src/main/res/drawable-hdpi/contact_title.png
create mode 100644 app-kotlin/src/main/res/drawable-hdpi/conversation_title.png
create mode 100644 app-kotlin/src/main/res/drawable-v24/ic_launcher_foreground.xml
create mode 100644 app-kotlin/src/main/res/drawable-xhdpi/contact_title.png
create mode 100644 app-kotlin/src/main/res/drawable-xhdpi/conversation_title.png
create mode 100644 app-kotlin/src/main/res/drawable-xhdpi/d_chat_voice_call.png
create mode 100644 app-kotlin/src/main/res/drawable-xhdpi/em_toast_fail.png
create mode 100644 app-kotlin/src/main/res/drawable-xhdpi/em_toast_success.png
create mode 100644 app-kotlin/src/main/res/drawable-xhdpi/splash_bg_dark.webp
create mode 100644 app-kotlin/src/main/res/drawable-xhdpi/splash_bg_light.webp
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/contact_title.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/conversation_title.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/d_chat_voice_call.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/demo_check_checked.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/demo_check_uncheck.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/ease_presence_away.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/ease_presence_busy.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/ease_presence_custom.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/ease_presence_do_not_disturb.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/ease_presence_offline.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/ease_presence_online.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/icon_about.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/icon_currency.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/icon_information.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/icon_login_attention_line.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/icon_logo.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/icon_next.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/icon_presence.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/icon_privacy.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/main_tab_conversation.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/main_tab_conversation_selected.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/main_tab_friends.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/main_tab_friends_selected.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/main_tab_me.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/main_tab_me_selected.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/phone_pick.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/sign_clear_icon.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/sign_eye.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/sign_eye_slash.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/video_call.png
create mode 100644 app-kotlin/src/main/res/drawable-xxhdpi/video_camera.png
create mode 100644 app-kotlin/src/main/res/drawable-xxxhdpi/contact_title.png
create mode 100644 app-kotlin/src/main/res/drawable-xxxhdpi/conversation_title.png
create mode 100644 app-kotlin/src/main/res/drawable-xxxhdpi/icon_notify.png
create mode 100644 app-kotlin/src/main/res/drawable/demo_cb_agreement_select.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_dialog_btn_left_selector.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_dialog_btn_right_selector.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_dialog_btn_selector.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_login_btn_bg.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_login_et_bg.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_login_version_bg.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_main_tab_conversation_selector.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_main_tab_friends_selector.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_main_tab_me_selector.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_main_unread_count_bg.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_switch_thumb_selector.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_switch_track_selector.xml
create mode 100644 app-kotlin/src/main/res/drawable/demo_toast_bg.xml
create mode 100644 app-kotlin/src/main/res/drawable/ic_launcher_background.xml
create mode 100644 app-kotlin/src/main/res/layout/activity_main_layout.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_activity_about.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_activity_conference_invite.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_activity_currency.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_activity_features.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_activity_language.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_activity_login.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_activity_me_information.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_activity_me_information_edit.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_activity_notify.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_activity_webview.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_badge_home.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_fragment_about_me.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_fragment_dialog_base.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_fragment_login.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_fragment_middle_agreement.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_fragment_server_set.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_row_received_conference_invite.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_row_received_voice_call.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_row_sent_conference_invite.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_row_sent_voice_call.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_splash_activity.xml
create mode 100644 app-kotlin/src/main/res/layout/demo_toast_layout.xml
create mode 100644 app-kotlin/src/main/res/layout/ease_layout_language_item.xml
create mode 100644 app-kotlin/src/main/res/menu/bottom_main_nav_menu.xml
create mode 100644 app-kotlin/src/main/res/menu/demo_chat_menu.xml
create mode 100644 app-kotlin/src/main/res/menu/demo_conference_invite_menu.xml
create mode 100644 app-kotlin/src/main/res/menu/demo_language_menu_confirm.xml
create mode 100644 app-kotlin/src/main/res/menu/demo_server_set_menu.xml
create mode 100644 app-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
create mode 100644 app-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
create mode 100644 app-kotlin/src/main/res/mipmap-hdpi/demo_launcher.png
create mode 100644 app-kotlin/src/main/res/mipmap-hdpi/ic_launcher.webp
create mode 100644 app-kotlin/src/main/res/mipmap-hdpi/ic_launcher_round.webp
create mode 100644 app-kotlin/src/main/res/mipmap-mdpi/demo_launcher.png
create mode 100644 app-kotlin/src/main/res/mipmap-mdpi/ic_launcher.webp
create mode 100644 app-kotlin/src/main/res/mipmap-mdpi/ic_launcher_round.webp
create mode 100644 app-kotlin/src/main/res/mipmap-xhdpi/demo_launcher.png
create mode 100644 app-kotlin/src/main/res/mipmap-xhdpi/ic_launcher.webp
create mode 100644 app-kotlin/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
create mode 100644 app-kotlin/src/main/res/mipmap-xxhdpi/demo_launcher.png
create mode 100644 app-kotlin/src/main/res/mipmap-xxhdpi/ic_launcher.webp
create mode 100644 app-kotlin/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
create mode 100644 app-kotlin/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
create mode 100644 app-kotlin/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
create mode 100644 app-kotlin/src/main/res/values-night/colors.xml
create mode 100644 app-kotlin/src/main/res/values-night/styles.xml
create mode 100644 app-kotlin/src/main/res/values-night/themes.xml
create mode 100644 app-kotlin/src/main/res/values/colors.xml
create mode 100644 app-kotlin/src/main/res/values/dimens.xml
create mode 100644 app-kotlin/src/main/res/values/ids.xml
create mode 100644 app-kotlin/src/main/res/values/strings.xml
create mode 100644 app-kotlin/src/main/res/values/styles.xml
create mode 100644 app-kotlin/src/main/res/values/themes.xml
delete mode 100644 build.gradle
create mode 100644 build.gradle.kts
delete mode 100644 settings.gradle
create mode 100644 settings.gradle.kts
diff --git a/README.md b/README.md
index 397b50e7..2e0064f0 100644
--- a/README.md
+++ b/README.md
@@ -1,49 +1,58 @@
# Agora chat demo
-This repository will help you learn how to use Agora chat SDK to implement a simple chat android application, like whatsapp or wechat.
+This repository will help you learn how to use Agora chat SDK to implement a simple android chat app, like whatsapp or wechat.
With this sample app, you can:
-- Login chat server
+- Log in to the chat server
- Start a chat
-- Manage conversation list
+- Manage the conversation list
- Add contacts
- Join group chats
-- Join chat rooms
-- Add your contacts to your blacklist
-- Send various types of messages, Such as: text, expression, picture, voice, file and so on
-- Logout
+- Add your contacts to your block list
+- Send various types of messages, such as text, emoji, image, voice and file messages
+- Log out of the chat server
## Prerequisites
-* Make sure you have made the preparations mentioned in the [Agora Chat SDK quickstart](https://docs.agora.io/en/agora-chat/get-started/get-started-sdk?platform=android).
-* Prepare the development environment:
- * Java Development Kit (JDK)
- * Android Studio 3.6 or later
+
+- Make sure you have made the preparations mentioned in the [Agora Chat SDK quickstart](https://docs.agora.io/en/agora-chat/get-started/get-started-sdk?platform=android).
+- Prepare the development environment:
+ - Java Development Kit (JDK)
+ - Android Studio Flamingo | 2022.2.1 or later
+
## Run the sample project
-Follow these steps to run the sample project:\
-### 1. Clone the repository to your local machine.
+Follow these steps to run the sample project:
+
+### 1. Clone the repository to your local device
+
```java
git clone git@github.com:AgoraIO-Usecase/AgoraChat-android.git
```
-### 2. Open the Android project with Android Studio.
+### 2. Open the Android project with Android Studio
-### 3. Configure keys.
-Set your appkey applied from [Agora Developer Console](https://console.agora.io/) before calling ChatClient#init().
-```java
-ChatOptions options = new ChatOptions();
-// Set your appkey
-options.setAppKey("Your appkey");
-...
-//initialization
-ChatClient.getInstance().init(applicationContext, options);
+### 3. Configure keys
+
+Set your appkey obtained from the [Agora Console](https://console.agora.io/) before calling ChatClient#init().
+
+```kotlin
+val chatOptions = ChatOptions().apply {
+ // Set your appkey
+ appKey = "Your appkey"
+ ...
+}
+// initialization
+ChatClient.getInstance().init(context, chatOptions)
```
+
For details, see the [prerequisites](https://docs.agora.io/en/agora-chat/get-started/get-started-sdk?platform=android) in Agora Chat SDK Guide.
## Contact Us
+
- You can find full API document at [Document Center](https://docs.agora.io/en/agora-chat/overview/product-overview?platform=android)
- You can file bugs about this demo at [issue](https://github.com/AgoraIO-Usecase/AgoraChat-android/issues)
## License
+
The MIT License (MIT).
diff --git a/app-kotlin/.gitignore b/app-kotlin/.gitignore
new file mode 100644
index 00000000..b61e41ab
--- /dev/null
+++ b/app-kotlin/.gitignore
@@ -0,0 +1,2 @@
+/build
+/src/google-services.json
\ No newline at end of file
diff --git a/app-kotlin/README.md b/app-kotlin/README.md
new file mode 100644
index 00000000..68ad2da3
--- /dev/null
+++ b/app-kotlin/README.md
@@ -0,0 +1,292 @@
+# 快速开始
+
+利用环信单群聊 UIKit,你可以轻松实现单群和群聊。本文介绍如何快速实现在单聊会话中发送消息。
+
+## 前提条件
+
+### 开发环境要求
+
+- Android Studio 4.0 及以上
+- Gradle 4.10.x 及以上
+- targetVersion 26 及以上
+- Android SDK API 21 及以上
+- JDK 11 及以上
+
+## 项目准备
+
+下面将介绍将单群聊 UIKit 引入项目中的必要环境配置。
+
+1. 用 **Android Studio** 创建一个[新的项目](https://developer.android.com/studio/projects/create-project),在 **Phone and Tablet** 标签选择 **Empty Views Activity**,**Minimum SDK** 选择 **API 21: Android 5.0 (Lollipop)**,**Language** 选择 **Kotlin**。创建项目成功后,请确保项目同步完成。
+
+2. 检查工程是否引入 **mavenCentral** 仓库。
+
+ a. Gradle 7.0 之前
+ 在 `/Gradle Scripts/build.gradle.kts(Project: )`文件内,检查是否有 **mavenCentral** 仓库。
+ ```kotlin
+ buildscript {
+ repositories {
+ mavenCentral()
+ }
+ }
+ ```
+ b. Gradle 7.0 之后
+ 在 `/Gradle Scripts/settings.gradle.kts(Project Settings)`文件内,检查是否有 **mavenCentral** 仓库。
+ ```kotlin
+ dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ mavenCentral()
+ }
+ }
+ ```
+3. 在项目中引入单群聊 UIKit
+
+从 GitHub 获取[单群聊 UIKit](https://github.com/easemob/chatuikit-android) 源码,按照下面的方式集成:
+
+- 在根目录 `settings.gradle.kts` 文件(/Gradle Scripts/settings.gradle.kts)中添加如下代码:
+
+```kotlin
+include(":ease-im-kit")
+project(":ease-im-kit").projectDir = File("../chatuikit-android/ease-im-kit")
+```
+
+- 在 app 的 `build.gradle.kts` 文件(/Gradle Scripts/build.gradle)中添加如下代码:
+
+```kotlin
+//chatuikit-android
+implementation(project(mapOf("path" to ":ease-im-kit")))
+```
+
+4. 防止代码混淆
+
+在 `/Gradle Scripts/proguard-rules.pro` 文件中添加如下代码:
+
+ ```
+ -keep class com.hyphenate.** {*;}
+ -dontwarn com.hyphenate.**
+ ```
+## 实现单聊发消息
+
+这部分将介绍如何通过单群聊 UIKit 一步一步的实现单聊发送消息的。
+
+### 创建页面相关
+
+1. 打开 `app/res/values/strings.xml` 文件,并替换为如下内容:
+
+```xml
+
+ quickstart
+
+ [您申请的appkey]
+
+
+```
+这里需要注意的是,需要将 **app_key** 替换为您申请的 appkey。
+
+2. 打开 `app/res/layout/activity_main.xml` 文件,并替换为如下内容:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 实现代码逻辑
+
+1. 实现登录和退出页面。
+
+打开 `MainActivity` 文件,并替换为如下代码:
+
+```kotlin
+package com.easemob.quickstart
+
+import android.content.Context
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import com.easemob.quickstart.databinding.ActivityMainBinding
+import com.hyphenate.easeui.EaseIM
+import com.hyphenate.easeui.common.ChatConnectionListener
+import com.hyphenate.easeui.common.ChatLog
+import com.hyphenate.easeui.common.ChatOptions
+import com.hyphenate.easeui.feature.messages.EaseChatType
+import com.hyphenate.easeui.feature.messages.activities.EaseChatActivity
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class MainActivity : AppCompatActivity(), ChatConnectionListener {
+ private val binding: ActivityMainBinding by lazy { ActivityMainBinding.inflate(layoutInflater) }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+ initSDK()
+ initListener()
+ }
+
+ private fun initSDK() {
+ val appkey = getString(R.string.app_key)
+ if (appkey.isNullOrEmpty()) {
+ showToast("You should set your AppKey first!")
+ ChatLog.e(TAG, "You should set your AppKey first!")
+ return
+ }
+ ChatOptions().apply {
+ // Set your own appkey here
+ this.appKey = appkey
+ // Set not to log in automatically
+ this.autoLogin = false
+ // Set whether confirmation of delivery is required by the recipient. Default: false
+ this.requireDeliveryAck = true
+ }.let {
+ EaseIM.init(applicationContext, it)
+ }
+ }
+
+ private fun initListener() {
+ EaseIM.subscribeConnectionDelegates(this)
+ }
+
+ fun login(view: View) {
+ val username = binding.etUserId.text.toString().trim()
+ val password = binding.etPassword.text.toString().trim()
+ if (username.isEmpty() || password.isEmpty()) {
+ showToast("Username or password cannot be empty!")
+ ChatLog.e(TAG, "Username or password cannot be empty!")
+ return
+ }
+ if (!EaseIM.isInited()) {
+ showToast("Please init first!")
+ ChatLog.e(TAG, "Please init first!")
+ return
+ }
+ EaseIM.login(username, password
+ , onSuccess = {
+ showToast("Login successfully!")
+ ChatLog.e(TAG, "Login successfully!")
+ }, onError = { code, message ->
+ showToast("Login failed: $message")
+ ChatLog.e(TAG, "Login failed: $message")
+ }
+ )
+ }
+
+ fun logout(view: View) {
+ if (!EaseIM.isInited()) {
+ showToast("Please init first!")
+ ChatLog.e(TAG, "Please init first!")
+ return
+ }
+ EaseIM.logout(false
+ , onSuccess = {
+ showToast("Logout successfully!")
+ ChatLog.e(TAG, "Logout successfully!")
+ }
+ )
+ }
+
+ fun startChat(view: View) {
+ val username = binding.etPeerId.text.toString().trim()
+ if (username.isEmpty()) {
+ showToast("Peer id cannot be empty!")
+ ChatLog.e(TAG, "Peer id cannot be empty!")
+ return
+ }
+ if (!EaseIM.isLoggedIn()) {
+ showToast("Please login first!")
+ ChatLog.e(TAG, "Please login first!")
+ return
+ }
+ EaseChatActivity.actionStart(this, username, EaseChatType.SINGLE_CHAT)
+ }
+
+ override fun onConnected() {}
+
+ override fun onDisconnected(errorCode: Int) {}
+
+ override fun onLogout(errorCode: Int, info: String?) {
+ super.onLogout(errorCode, info)
+ showToast("You have been logged out, please log in again!")
+ ChatLog.e(TAG, "")
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ EaseIM.unsubscribeConnectionDelegates(this)
+ }
+
+ companion object {
+ private const val TAG = "MainActivity"
+ }
+}
+
+fun Context.showToast(msg: String) {
+ CoroutineScope(Dispatchers.Main).launch {
+ Toast.makeText(this@showToast, msg, Toast.LENGTH_SHORT).show()
+ }
+}
+```
+
+2. 点击 `Sync Project with Gradle Files` 同步工程。现在可以测试你的应用了。
+
+## 测试应用
+
+1. 在 Android Studio 中,点击 `Run ‘app’` 按钮,将应用运行到您的设备或者模拟器上。
+
+2. 输入用户名和密码,点击 `Login` 按钮进行登录,登录成功或者失败有 `Toast` 提示,或者通过 Logcat 查看。
+
+3. 在另一台设备或者模拟器上登录另一个账号。
+
+4. 两台设别或者模拟器分别输入对方的账号,并点击 `Start Chat` 按钮,进入聊天页面。现在你可以在两个账号间进行聊天了。
\ No newline at end of file
diff --git a/app-kotlin/build.gradle.kts b/app-kotlin/build.gradle.kts
new file mode 100644
index 00000000..08e859fe
--- /dev/null
+++ b/app-kotlin/build.gradle.kts
@@ -0,0 +1,160 @@
+import java.util.Properties
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ // Add the Google services Gradle plugin
+ id("com.google.gms.google-services")
+ // Add the ksp plugin when using Room
+ id("com.google.devtools.ksp")
+}
+
+val properties = Properties()
+val inputStream = project.rootProject.file("local.properties").inputStream()
+properties.load( inputStream )
+
+android {
+ namespace = "io.agora.chatdemo"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "io.agora.chatdemo"
+ minSdk = 21
+ targetSdk = 34
+ versionCode = 13
+ versionName = "1.3.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ // Set app server info from local.properties
+ buildConfigField ("String", "APP_SERVER_PROTOCOL", "\"https\"")
+ buildConfigField ("String", "APP_SERVER_DOMAIN", "\"${properties.getProperty("APP_SERVER_DOMAIN")}\"")
+ buildConfigField ("String", "APP_SERVER_URL", "\"${properties.getProperty("APP_SERVER_URL")}\"")
+ buildConfigField ("String", "APP_SERVER_REGISTER", "\"${properties.getProperty("APP_SERVER_REGISTER")}\"")
+ buildConfigField ("String", "APP_BASE_USER", "\"${properties.getProperty("APP_BASE_USER")}\"")
+ buildConfigField ("String", "APP_UPLOAD_AVATAR", "\"${properties.getProperty("APP_UPLOAD_AVATAR")}\"")
+ buildConfigField ("String", "APP_BASE_GROUP", "\"${properties.getProperty("APP_BASE_GROUP")}\"")
+ buildConfigField ("String", "APP_GROUP_AVATAR", "\"${properties.getProperty("APP_GROUP_AVATAR")}\"")
+ buildConfigField ("String", "APP_RTC_TOKEN_URL", "\"${properties.getProperty("APP_RTC_TOKEN_URL")}\"")
+ buildConfigField ("String", "APP_RTC_CHANNEL_MAPPER_URL", "\"${properties.getProperty("APP_RTC_CHANNEL_MAPPER_URL")}\"")
+
+ // Set appkey from local.properties
+ buildConfigField("String", "AGORA_CHAT_APPKEY", "\"${properties.getProperty("AGORA_CHAT_APPKEY")}\"")
+ // Set push info from local.properties
+ buildConfigField("String", "FCM_SENDERID", "\"${properties.getProperty("FCM_SENDERID")}\"")
+ // Set RTC appId from local.properties
+ buildConfigField("String", "AGORA_APPID", "\"${properties.getProperty("AGORA_APPID")}\"")
+
+ //指定room.schemaLocation生成的文件路径 处理Room 警告 Schema export Error
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments(mapOf(
+ "room.schemaLocation" to "$projectDir/schemas",
+ "room.incremental" to "true",
+ "room.expandProjection" to "true"
+ ))
+ }
+ }
+
+ ndk {
+ abiFilters.addAll(mutableSetOf("arm64-v8a","armeabi-v7a"))
+ }
+ //用于设置使用as打包so时指定输出目录
+ externalNativeBuild {
+ ndkBuild {
+ abiFilters("arm64-v8a","armeabi-v7a")
+ arguments("-j8")
+ }
+ }
+
+
+ }
+
+ signingConfigs {
+ getByName("debug") {
+ storeFile = file(properties.getProperty("DEBUG_STORE_FILE_PATH", "./keystore/sdkdemo.jks"))
+ storePassword = properties.getProperty("DEBUG_STORE_PASSWORD", "123456")
+ keyAlias = properties.getProperty("DEBUG_KEY_ALIAS", "easemob")
+ keyPassword = properties.getProperty("DEBUG_KEY_PASSWORD", "123456")
+ }
+ create("release") {
+ storeFile = file(properties.getProperty("RELEASE_STORE_FILE_PATH", "./keystore/sdkdemo.jks"))
+ storePassword = properties.getProperty("RELEASE_STORE_PASSWORD", "123456")
+ keyAlias = properties.getProperty("RELEASE_KEY_ALIAS", "easemob")
+ keyPassword = properties.getProperty("RELEASE_KEY_PASSWORD", "123456")
+ }
+ }
+
+ buildTypes {
+ debug {
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ release {
+ signingConfig = signingConfigs.getByName("release")
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+
+ buildFeatures{
+ viewBinding = true
+ buildConfig = true
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ // Set toolchain version
+ kotlin {
+ jvmToolchain(8)
+ }
+
+ //打开注释后,可以直接在studio里查看和编辑emclient-linux里的代码
+ externalNativeBuild {
+ ndkBuild {
+ path = File("jni/Android.mk")
+ }
+ }
+}
+
+dependencies {
+ implementation("androidx.core:core-ktx:1.10.1")
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation("com.google.android.material:material:1.9.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ // lifecycle
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+ // lifecycle viewmodel
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
+ // Coil: load image library
+ implementation("io.coil-kt:coil:2.5.0")
+ // image corp library
+ implementation("com.github.yalantis:ucrop:2.2.8")
+ implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
+ implementation("io.github.scwang90:refresh-layout-kernel:2.1.0")
+ implementation("io.github.scwang90:refresh-header-material:2.1.0")
+ implementation("io.github.scwang90:refresh-header-classics:2.1.0")
+ implementation("pub.devrel:easypermissions:3.0.0")
+ // Room
+ implementation("androidx.room:room-runtime:2.5.1")
+ ksp("androidx.room:room-compiler:2.5.1")
+ // optional - Kotlin Extensions and Coroutines support for Room
+ // To use Kotlin Flow and coroutines with Room, must include the room-ktx artifact in build.gradle file.
+ implementation("androidx.room:room-ktx:2.5.1")
+ // Import the BoM for the Firebase platform
+ implementation(platform("com.google.firebase:firebase-bom:32.7.4"))
+ // Declare the dependencies for the Firebase Cloud Messaging and Analytics libraries
+ // When using the BoM, you don't specify versions in Firebase library dependencies
+ implementation("com.google.firebase:firebase-messaging")
+ implementation("com.google.firebase:firebase-analytics")
+
+// implementation("io.agora.rtc:chat-uikit:1.3.0")
+ implementation(project(mapOf("path" to ":chat-uikit")))
+// implementation("io.agora.rtc:chat-callkit:1.3.0")
+ implementation(project(mapOf("path" to ":chat-callkit")))
+}
\ No newline at end of file
diff --git a/app-kotlin/google-services.json b/app-kotlin/google-services.json
new file mode 100644
index 00000000..1fd93cfb
--- /dev/null
+++ b/app-kotlin/google-services.json
@@ -0,0 +1,39 @@
+{
+ "project_info": {
+ "project_number": "142290967082",
+ "project_id": "chatim-9b3ae",
+ "storage_bucket": "chatim-9b3ae.appspot.com"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:142290967082:android:fa1734c82786f78184c153",
+ "android_client_info": {
+ "package_name": "io.agora.chatdemo"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "142290967082-kffai4lca6v8f70lbku18hquh72747qa.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyDE682uNgGqIfEL7o6BkShzx5Pi2nuIIS0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "142290967082-kffai4lca6v8f70lbku18hquh72747qa.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/app-kotlin/jni/Android.mk b/app-kotlin/jni/Android.mk
new file mode 100644
index 00000000..f094fe4e
--- /dev/null
+++ b/app-kotlin/jni/Android.mk
@@ -0,0 +1,24 @@
+# Copyright (C) 2009 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+PB_LITE=1
+ENABLE_CALL=0
+USE_SQLCIPHER=1
+ENABLE_AGORA=1
+#libhyphenate.so
+include $(LOCAL_PATH)/../../../emclient-linux/Android.mk
diff --git a/app-kotlin/jni/Application.mk b/app-kotlin/jni/Application.mk
new file mode 100644
index 00000000..67e034dd
--- /dev/null
+++ b/app-kotlin/jni/Application.mk
@@ -0,0 +1,6 @@
+#APP_OPTIM := debug
+APP_ABI := arm64-v8a,armeabi-v7a, x86, x86_64
+#高版本ndk
+APP_STL := c++_static
+LOCAL_MULTILIB := 32
+APP_PLATFORM:=android-21
diff --git a/app-kotlin/keystore/sdkdemo.jks b/app-kotlin/keystore/sdkdemo.jks
new file mode 100644
index 0000000000000000000000000000000000000000..576fe9fc725805874be4b09ab4370ddc1ace38d1
GIT binary patch
literal 4999
zcma*pRa6viw+8Tm0S1tip^*+HXXtJODe3N%mZ3vpXauAi0YL;5kdOg}E{787l!hUs
zb42iX>i@0p=B#t`zH2?t+WYdiHUv%)g^dM-z$v8h2)JX_Vy}s@Kv*Sk3O*b-#go4=
z2Lw(y^luem8xEYX`ET3+f#d4nJ@}s~B0?MyEs-u9ruiBh2phhLh+Y>&58#S65
zF>nhg2rhmQeY|=&)@3xtBAHNAT1&%Fm&$RUAFvJ)IWH$8
z0kq%}@=P~TndaWCFj7dXml#*R@*Ga+`P0W{D+?OIwbT|~rnfS8wi%s@YX}Ggwe|4Z
z@G8!QKL_KZ6a7hM+MJqRr4K9B!;C23$0WGSwQHlIVuK{}fC?zjzI-*1adTErRKa#369Ijk2?iWGg?ElZ=ni=odmYIXuL5PX
z)7m?v>W7pmzV0YEBo&F`)4wlONgqt(L3WvPSxVBt()~&RuGAkV^aDADa^F`qIg(^+
zuV;!%8uBLF(KWa2q4*#BmBib~-FWM+uQs0Mm!2Z~rgBTf7If#Qrh*BzsjWfUdyja0-B=5kC^s80#V)DC-MB>oh`+A0#j1Xo<%{;fKL+=
z8u2l$xy+RZY|F_?ki-~&
z-tcbbUw6x(oylQx-X$)3%%|=&Z#k)cSR273>2tS4OR+MCeIBd^W(&(8c8ICoV(OqrXFj;baY}%{Z8G_W)~%Xt%{>D8x~DV4-C^3mn-t
z@=+8liE!`9FrWq1ve0ALxc}zG8>JTNObc9b7fj95Px5|b4D%~7y!O7hNL?18vu(+&
zZ_n13I=N1_wbqxR1UQ^o|cb7_PR4bYXf
zEK_$=xScdoQAif05r?N)J)Gb^&2Qn%Gj11Tr*&i20Dfi?khKLy&FF-vs^+=-2Rv>4
zso(pZLb_WeIio|
ze-eETpJ%ZBh=6T(10S&KvZla*Z{si$CGC`rH&F{%8(uR^8B?QB;4o>~om3x@+LVmo
z!06y_C$5}Oxg8z-JM*kwOl@Vr?qkuBJN(T-15*X8`d2#f2bX%0YUkt>I0&A%n&t6r
zZigm;s1Ek+FMQi2Z-SOxreCd6y5&>G-jo)y$zzI$Qvc&Nz2^V5&mFXSNp;PBVT+QBUu;4?b>!_qI)k@H`+`;KBn
zrRL!+mmtdw;>m*obm2$#KrDluYS=?h7m0go1G%8n}2kGDAfqgdq?ysE`CiMDVYv
zX8%u9Nr35fUi0@cb&hY}o+2iRl|MMu{bTC;K^!roZ--i*{c=>Z)%a-RnPi8_ME+a&
zuO#dteDcqvO=r#5Ii9kvOS+l&4a48`?~@ht=d@3(F>MB9)&7FV#}Anlv&h8t%r~`TmNoalj(BIpZSbJca`4N5dg0ZudC(sPAhe#(H;vi-U0oG-->#2y
zsD}ziOb9%UQ&RfKk`?Fa$};O)w!seYdN*yw+Iiw9$DCRIB8SgZ2BkLR{Yo=Z&l^gD
zRrHI^7}>|tn*#KQU#rbMzYpopAJ>1;Y5$d~5dX%0rbA{Y58%H-9m
zw(e2EwH<%M6X@-2S5GJ3H17BH5GJLpIk|(?cTK5hCBR1a{3%Bsr^}{WztEfIqDY6z
zB`a8ANfnP)MN>1q`Z(gtph0JgyoSK;onYlpK*JoCJ{ra!(LYDB9HvMu6qhg^Ra_Yf
z*e|#S#miea(rKxdlBh=#m6m0svoNSU!$9=d2b?Qb!PKG*PHI-ayv3Bm6DE)uH+fKL
zlZ`IW?i}~Hg;m=oQ4*kUbSC~0_V1iwutA)At|j1>(6(>8i)UBt_`4@
z4M?bCR68bYa@{Y_dMw5C-5q$wn1#e=F&?dUI`6Wnes;CvuP}-|~zw@#`A#HRKfF
zUTv8l&qv1aNmE>u$p6ja!`vpXn>9JYHsGwQicZFDLTH?&ydfhgX8n|4kQc=-%O7-H
z7AEC1CHL&l;8o(Pit0Dj<|jzFRw8Pr
z8+(K=DBA%N*3$I_Ocl?%&cM}It9$ELzEPMYuG6uf-IwEwvCfRqEQwFYI=n13x5Zb3
z+#(TSFn6&FLfhwEz#;yD@5)b{qrM@X%DD$McjUg)3M5D}vchTar1N_(-Y`W+$W#b&
z$GtraewtUYCgw<7ZQFwItYKgzqIq%dFoD|ztkgvBGk(y$5E!S#&}J1+$Gj|e>-a^}
zh)7G=`B}VDUZudoDG+sXTJ0Kz;BFY9HINoDbVXeeU^*GXt9_xQ^qT_Yfw82TGm~jg
z0%}FIE?g~Vb{*+NEIu^m-oaOsL&g`+e{)s&rU~&}!QbG@4A~ow1g*Hv&Mgk9oAE4c
zg=mgVxRpK%O+>OaPQ+T)P3R*&b*(8?rLVuQodg&>87pylrZK~VL#(}zR;TX*D3^uY
zoABXgUf!Jrk+blu^x8AwR=vI!i}j#Ci}Tmt6>Ksr@lyI``WKpDC>FF
zll)hM^=}s}-hW=OSNjrAM<6VVMb6h8A_5H)v{AE{^2z?wuIUgi2^SUb%#J8d+p;KF2E*Gvtv;V_Tr
z$gu?=+bJi}F(Bt>ip#4#Q{+iN_@!U;VRcSe>LcM9;#?~~W#*b;WW`1U(^vE1$Onyz
zdS(Sjprwi;J>L_S!QumdaU-&np^1%z#DnZ2G8$@0yhJi116qyT(MeCncONk;MkA+F
znB~p=FHlFd;--xln)x6bMMeGtS!$L*qy4r5bCq!8i(tvho+P!``)D>t5jrVNt}&~Oyb(j%v^?y?RY_ET`5
z`DnGiNgI;-(^~xV*5O&_%TZ|~#cLud>pOx!@p
zlx`KQh~?5N3ERIlgu~P1hdK~;+}cc=wGyX>=mS|2k~&w>NvA%(w&U9<{7;2)q!w^^
z;)}2Z^-X?G_YK|=WbtyH#4Gz*f5V!(7@q#jMsHTemCCBt+Fl=9#oQQsnTf=QAe;5N
zHD6W^87(Q6xaK?_WdscQth@ms>TFjwFdE!-xy*exnA91}9)MV=0A7hI!K-(1FOW6s
zp%W_Rk;?DPW;@)a8+u>+6TgCL+IC$?3gQ_kdk=(x6*{}>4d$Gm09(utP(^!+*iJF3
zw)`rEDoSGwPBkHW){+^}EJ0e^xAK~M1-|L;qL!-?a+;(9iy3wUj%Gf@57~DWE5Ue!
zwosB|(ER2}Mxl>5{^~Te%c2?_XpxX$GO
zyK1c@K9(L)V|jPHe*5oEqN{ETS(ByBxEkNf&gKlbP1z-9m&2d4ksyqsg(@k(y6D=$
z=OQwaJY{K=?!|JV3nBf{#u(zGKf)qsu0D!1jDK{Qm9k=Gi#b9~CcrMkX>BIu;*9!C
z`fOKU>CkbFnd3X%pKW~^kS|-Q=Qga1v|X(SsUFozzS<4JS53~$-6&;9Q~p3~u@AvL
zl(33-mY`9m5if&gsFInL0E+yNjXnHtj|2eQ6%DXlg=?74WdKJRNg<`opyE@h{&V8#
zbiH#`KfNE}A)&5;ZdfHckpw-9=R*sxU!x+qa{R8cTYX0`HVV8oyvDv~diz}WtzVW%
z4%Qr}X(@7&xo@$6W=e9Pha-Pp&dHt-|JZ_IfKnUGMd;B2ij&q7w&}ATK{De-&Ym@;
z;V2c0c08s04b$BbQr{r)=={vvV646d1Ez8EOyu>Z3-w6OgEu`B-1QIgYj_D(w`{b=
ze$9rY5DDRGz1E^+zDZTO5SP1+?w`ixtddA#$endX4KKr}zam0!lH@Y5-n`YMu+1y7
z-c(I+`&|W20S750MM=lKrm&1`i%gpw
z79iPY@8i>6gZ@g_e@En~YMRu?RcDU41KNSIkSX;pH5YWWtn|Fx)L~D@nUG97RUorT
zMo+^{hm@+Lc|qk>2Q^mkS$Uo4F6UvRhT|uGgrjB*XT-5EBq3>jc#dGH+TUD=+Q(r!^D>X
zmP%LYi}weYaUlvlhRosH4gnrvvby87eLsEB&PIfvXeb(UjEs2|k?03iTX<#dRWAO1
zkRo2!=dja1vitU)E(IyOi&}1CnNez&?OJj>R1g>oXQ*w~yeC;NBk>cWUn6ZU-|v&7
z5``zWwz5=k^`9k7o=s!1Ht@Od=xsafvGT@ej|S*caj{;I#lSKzlaDWnbEwydZP?Az
z>s?I4?A@kv)=}*YQ46CagoneIog#HM!WxrL>j+w`L}j&iD`@SN?GvMD%y(_e?qV5I
zM-E<8IYGEoZFdaaiDh`@O=l{y1y5ENIHY#CF5i2nlrTtEV7%4iyqJ}p_m`TsCr!LQ
z(*ZIV6K5~xGAQ2xGOeK<~SmgQqq)s6FVsos6xUAo73_HvcZ?;Rz@
zzcCr#WZcO5_C(vQAh9PQx%M<0Q6oqJ(*gr(z2s9%UQJ6t8e{qewOf5K^v+D8Z1(3X
za>>O5Yv%K%ZmID!2DPbs!O2%j@W|U2oq6R-iIfIo9W!hp?>!P6u2t_-wCGSC$Lw<&
zS%TB@hpKPZUqLkF6Q3nZ!n+0I8zBoDCtbu%v*Gvj8KK}~Rjcf%Sw7H(#N$jtGBx?o
z&iS1{4@NaFUYXHx6+6}IiiMgpxZ|do1pCk$-7CX^7msZ%<-mJd!c>nIbvS!XyWYj#
zQ!MwI1tZqMSj8`$f2ect7RI#R*Pg0Y>Jr5KP!pF{61>xTC79^^TFgQ}2poA#L3o_6
z&_ezv3ex4aPP9};LL`LN$R#qK^igCF7kjd5%5+;EMrq+&1X}JgNa4Sr%KrRLP9j%m
zAtMa+L(T&t3t@o};NkL+0I}(C0XX0q=JcOkNWs+Vfr?_+h`D5mlWj#DaI4SlZ-af~
X`u;Q61{S+ySxiEq12zyVw&Xtmoz_^-
literal 0
HcmV?d00001
diff --git a/app-kotlin/proguard-rules.pro b/app-kotlin/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/app-kotlin/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app-kotlin/src/main/AndroidManifest.xml b/app-kotlin/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..103e67c5
--- /dev/null
+++ b/app-kotlin/src/main/AndroidManifest.xml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/DemoApplication.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/DemoApplication.kt
new file mode 100644
index 00000000..1f1d6d8d
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/DemoApplication.kt
@@ -0,0 +1,57 @@
+package io.agora.chatdemo
+
+import android.app.Application
+import androidx.appcompat.app.AppCompatDelegate
+import io.agora.chatdemo.base.UserActivityLifecycleCallbacks
+import io.agora.chatdemo.bean.LanguageType
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.PreferenceManager
+import io.agora.chatdemo.utils.LanguageUtil
+import io.agora.uikit.EaseIM
+
+class DemoApplication: Application() {
+
+ private val mLifecycleCallbacks = UserActivityLifecycleCallbacks()
+
+ companion object {
+ private lateinit var instance: DemoApplication
+ fun getInstance(): DemoApplication {
+ return instance
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ instance = this
+ registerActivityLifecycleCallbacks()
+
+ DemoHelper.getInstance().init(this)
+ initFeatureConfig()
+ }
+
+ private fun registerActivityLifecycleCallbacks() {
+ this.registerActivityLifecycleCallbacks(mLifecycleCallbacks)
+ }
+
+ fun getLifecycleCallbacks(): UserActivityLifecycleCallbacks {
+ return mLifecycleCallbacks
+ }
+
+ private fun initFeatureConfig(){
+ val isBlack = DemoHelper.getInstance().getDataModel().getBoolean(DemoConstant.IS_BLACK_THEME)
+ AppCompatDelegate.setDefaultNightMode(if (isBlack) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
+
+ val enableTranslation = DemoHelper.getInstance().getDataModel().getBoolean(DemoConstant.FEATURES_TRANSLATION,true)
+ val enableThread = DemoHelper.getInstance().getDataModel().getBoolean(DemoConstant.FEATURES_THREAD,true)
+ val enableReaction = DemoHelper.getInstance().getDataModel().getBoolean(DemoConstant.FEATURES_REACTION,true)
+ val enableTyping = DemoHelper.getInstance().getDataModel().getBoolean(DemoConstant.IS_TYPING_ON,false)
+ val targetLanguage = PreferenceManager.getValue(DemoConstant.TARGET_LANGUAGE, LanguageType.EN.value)
+ EaseIM.getConfig()?.chatConfig?.targetTranslationLanguage = targetLanguage
+ LanguageUtil.changeLanguage("en")
+
+ EaseIM.getConfig()?.chatConfig?.enableTranslationMessage = enableTranslation
+ EaseIM.getConfig()?.chatConfig?.enableChatThreadMessage = enableThread
+ EaseIM.getConfig()?.chatConfig?.enableMessageReaction = enableReaction
+ EaseIM.getConfig()?.chatConfig?.enableChatTyping = enableTyping
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/DemoHelper.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/DemoHelper.kt
new file mode 100644
index 00000000..f33f2585
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/DemoHelper.kt
@@ -0,0 +1,156 @@
+package io.agora.chatdemo
+
+import android.content.Context
+import android.util.Log
+import io.agora.chatdemo.callkit.CallKitManager
+import io.agora.chatdemo.common.DemoDataModel
+import io.agora.chatdemo.common.ListenersWrapper
+import io.agora.chatdemo.common.PushManager
+import io.agora.chatdemo.common.extensions.internal.checkAppKey
+import io.agora.chatdemo.uikit.UIKitManager
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatOptions
+import io.agora.uikit.common.PushConfigBuilder
+
+class DemoHelper private constructor(){
+
+ private lateinit var dataModel: DemoDataModel
+ var hasAppKey = false
+ lateinit var context: Context
+
+ @Synchronized
+ fun init(context: Context) {
+ this.context = context.applicationContext
+ dataModel = DemoDataModel(context)
+ initSDK()
+ }
+
+ fun getDataModel(): DemoDataModel {
+ return dataModel
+ }
+
+ /**
+ * Check if the SDK has been initialized.
+ */
+ fun isSDKInited(): Boolean {
+ return EaseIM.isInited()
+ }
+
+ /**
+ * Initialize the SDK.
+ */
+ @Synchronized
+ fun initSDK() {
+ if (::context.isInitialized.not()) {
+ Log.e(TAG, "Please call init method first.")
+ return
+ }
+ initChatOptions(context).apply {
+ hasAppKey = checkAppKey(context)
+ if (!hasAppKey) {
+ Log.e(TAG, "App key is null or empty.")
+ return
+ }
+ // Register necessary listeners
+ ListenersWrapper.registerListeners()
+ isLoadEmptyConversations = true
+ EaseIM.init(context, this)
+ if (EaseIM.isInited()) {
+ // debug mode, you'd better set it to false, if you want release your App officially.
+ ChatClient.getInstance().setDebugMode(true)
+ // Initialize push.
+ initPush()
+ // Set the UIKit options.
+ addUIKitSettings()
+ // Initialize the callkit module.
+ initCallKit()
+ }
+ }
+ }
+
+ private fun addUIKitSettings() {
+ UIKitManager.addUIKitSettings(context)
+ }
+
+ private fun initPush() {
+ PushManager.initPush(context)
+ }
+
+ /**
+ * Get the notifier.
+ */
+ fun getNotifier() = EaseIM.getNotifier()
+
+ private fun initCallKit() {
+ CallKitManager.init(context)
+ }
+
+ /**
+ * Set chat options.
+ * Note: Developers need to set the options according to needs.
+ */
+ private fun initChatOptions(context: Context): ChatOptions {
+ return ChatOptions().apply {
+ // set the appkey
+ appKey = BuildConfig.AGORA_CHAT_APPKEY
+ // set if accept the invitation automatically, default true
+ acceptInvitationAlways = false
+ // set if you need read ack
+ requireAck = true
+ // Set whether the sent message is included in the message listener, default false
+ isIncludeSendMessageInMessageListener = true
+
+ getDataModel().setUseFCM(true)
+
+ /**
+ * Note: Developers need to apply your own push accounts and replace the following
+ */
+ pushConfig = PushConfigBuilder(context)
+ .enableFCM(BuildConfig.FCM_SENDERID)
+ .build()
+
+ if (dataModel.isDeveloperMode()) {
+
+ if (dataModel.getCustomAppKey().isNotEmpty()){
+ dataModel.getCustomAppKey().let {
+ appKey = it
+ }
+ }
+
+ if (dataModel.isCustomServerEnable()) {
+ // Turn off DNS configuration
+ enableDNSConfig(false)
+ restServer = dataModel.getRestServer()?.ifEmpty { null }
+ setIMServer(dataModel.getIMServer()?.let {
+ if (it.contains(":")) {
+ imPort = it.split(":")[1].toInt()
+ it.split(":")[0]
+ } else {
+ it.ifEmpty { null }
+ }
+ })
+ val port = dataModel.getIMServerPort()
+ if (port != 0) {
+ imPort = port
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "DemoHelper"
+ private var instance: DemoHelper? = null
+ fun getInstance(): DemoHelper {
+ if (instance == null) {
+ synchronized(DemoHelper::class.java) {
+ if (instance == null) {
+ instance = DemoHelper()
+ }
+ }
+ }
+ return instance!!
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/MainActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/MainActivity.kt
new file mode 100644
index 00000000..6edad110
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/MainActivity.kt
@@ -0,0 +1,323 @@
+package io.agora.chatdemo
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.bottomnavigation.BottomNavigationItemView
+import com.google.android.material.bottomnavigation.BottomNavigationMenuView
+import com.google.android.material.navigation.NavigationBarView
+import io.agora.chatdemo.base.BaseInitActivity
+import io.agora.chatdemo.callkit.CallKitManager
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.databinding.ActivityMainLayoutBinding
+import io.agora.chatdemo.interfaces.IMainResultView
+import io.agora.chatdemo.page.contact.ChatContactListFragmentEvent
+import io.agora.chatdemo.page.conversation.ConversationListFragment
+import io.agora.chatdemo.page.me.fragment.AboutMeFragment
+import io.agora.chatdemo.viewmodel.MainViewModel
+import io.agora.chatdemo.viewmodel.ProfileInfoViewModel
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatError
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.ChatMessage
+import io.agora.uikit.common.EaseConstant
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.common.extensions.catchChatException
+import io.agora.uikit.common.extensions.showToast
+import io.agora.uikit.feature.conversation.EaseConversationListFragment
+import io.agora.uikit.interfaces.EaseContactListener
+import io.agora.uikit.interfaces.EaseMessageListener
+import io.agora.uikit.interfaces.OnEventResultListener
+import io.agora.uikit.model.EaseEvent
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class MainActivity : BaseInitActivity(), NavigationBarView.OnItemSelectedListener,
+ OnEventResultListener, IMainResultView {
+ override fun getViewBinding(inflater: LayoutInflater): ActivityMainLayoutBinding? {
+ return ActivityMainLayoutBinding.inflate(inflater)
+ }
+
+ private var mConversationListFragment: Fragment? = null
+ private var mContactFragment: Fragment? = null
+ private var mAboutMeFragment: Fragment? = null
+ private var mCurrentFragment: Fragment? = null
+ private val badgeMap = mutableMapOf()
+ private val mainViewModel: MainViewModel by lazy {
+ ViewModelProvider(this)[MainViewModel::class.java]
+ }
+ private val mProfileViewModel: ProfileInfoViewModel by lazy {
+ ViewModelProvider(this)[ProfileInfoViewModel::class.java]
+ }
+
+ private val chatMessageListener = object : EaseMessageListener() {
+ override fun onMessageReceived(messages: MutableList?) {
+ mainViewModel.getUnreadMessageCount()
+ }
+ }
+
+ companion object {
+ fun actionStart(context: Context) {
+ Intent(context, MainActivity::class.java).apply {
+ context.startActivity(this)
+ }
+ }
+ }
+
+ override fun initView(savedInstanceState: Bundle?) {
+ super.initView(savedInstanceState)
+ binding.navView.itemIconTintList = null
+ switchToHome()
+ checkIfShowSavedFragment(savedInstanceState)
+ addTabBadge()
+ mainViewModel.getRequestUnreadCount()
+ }
+
+ override fun initListener() {
+ super.initListener()
+ binding.navView.setOnItemSelectedListener(this)
+ EaseIM.addEventResultListener(this)
+ EaseIM.addChatMessageListener(chatMessageListener)
+ EaseIM.addContactListener(contactListener)
+ }
+
+ override fun initData() {
+ super.initData()
+ mainViewModel.attachView(this)
+ synchronizeProfile()
+ EaseFlowBus.with(EaseEvent.EVENT.ADD.name).register(this){
+ // check unread message count
+ mainViewModel.getUnreadMessageCount()
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.REMOVE.name).register(this){
+ // check unread message count
+ mainViewModel.getUnreadMessageCount()
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.DESTROY.name).register(this){
+ // check unread message count
+ mainViewModel.getUnreadMessageCount()
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.LEAVE.name).register(this){
+ // check unread message count
+ mainViewModel.getUnreadMessageCount()
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE.name).register(this){
+ // check unread message count
+ mainViewModel.getUnreadMessageCount()
+ }
+ EaseFlowBus.withStick(EaseEvent.EVENT.UPDATE.name).register(this){
+ // check unread message count
+ mainViewModel.getUnreadMessageCount()
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE.name).register(this) {
+ if (it.isNotifyChange) {
+ mainViewModel.getRequestUnreadCount()
+ }
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.ADD.name).register(this) {
+ if (it.isNotifyChange) {
+ mainViewModel.getRequestUnreadCount()
+ }
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.ADD + EaseEvent.TYPE.CONVERSATION).register(this) {
+ if (it.isConversationChange) {
+ mainViewModel.getUnreadMessageCount()
+ }
+ }
+ }
+
+ private fun switchToHome() {
+ if (mConversationListFragment == null) {
+ mConversationListFragment = EaseConversationListFragment.Builder()
+ .useTitleBar(true)
+ .enableTitleBarPressBack(false)
+ .useSearchBar(true)
+ .setCustomFragment(ConversationListFragment())
+ .build()
+ }
+ mConversationListFragment?.let {
+ replace(it, "conversation")
+ }
+ }
+
+ private fun switchToContacts() {
+ if (mContactFragment == null) {
+ mContactFragment = ChatContactListFragmentEvent.Builder()
+ .useTitleBar(true)
+ .useSearchBar(true)
+ .enableTitleBarPressBack(false)
+ .setHeaderItemVisible(true)
+ .build()
+ }
+ mContactFragment?.let {
+ replace(it, "contact")
+ }
+ }
+
+ private fun switchToAboutMe() {
+ if (mAboutMeFragment == null) {
+ mAboutMeFragment = AboutMeFragment()
+ }
+ mAboutMeFragment?.let {
+ replace(it, "me")
+ }
+ }
+
+ override fun onDestroy() {
+ EaseIM.removeEventResultListener(this)
+ EaseIM.removeChatMessageListener(chatMessageListener)
+ EaseIM.removeContactListener(contactListener)
+ super.onDestroy()
+ }
+
+ private fun replace(fragment: Fragment, tag: String) {
+ if (mCurrentFragment !== fragment) {
+ val t = supportFragmentManager.beginTransaction()
+ mCurrentFragment?.let {
+ t.hide(it)
+ }
+ mCurrentFragment = fragment
+ if (!fragment.isAdded) {
+ t.add(R.id.fl_main_fragment, fragment, tag).show(fragment).commit()
+ } else {
+ t.show(fragment).commit()
+ }
+ }
+ }
+
+ /**
+ * 用于展示是否已经存在的Fragment
+ * @param savedInstanceState
+ */
+ private fun checkIfShowSavedFragment(savedInstanceState: Bundle?) {
+ if (savedInstanceState != null) {
+ val tag = savedInstanceState.getString("tag")
+ if (!tag.isNullOrEmpty()) {
+ val fragment = supportFragmentManager.findFragmentByTag(tag)
+ if (fragment is Fragment) {
+ replace(fragment, tag)
+ }
+ }
+ }
+ }
+
+ private fun addTabBadge() {
+ (binding.navView.getChildAt(0) as? BottomNavigationMenuView)?.let { menuView->
+ val childCount = menuView.childCount
+ for (i in 0 until childCount) {
+ val itemView = menuView.getChildAt(i) as BottomNavigationItemView
+ val badge = LayoutInflater.from(this).inflate(R.layout.demo_badge_home, menuView, false)
+ badgeMap[i] = badge.findViewById(R.id.tv_main_home_msg)
+ itemView.addView(badge)
+ }
+ }
+ }
+
+ override fun onNavigationItemSelected(item: MenuItem): Boolean {
+ var showNavigation = false
+ when (item.itemId) {
+ R.id.em_main_nav_home -> {
+ switchToHome()
+ showNavigation = true
+ }
+
+ R.id.em_main_nav_friends -> {
+ switchToContacts()
+ showNavigation = true
+ }
+
+ R.id.em_main_nav_me -> {
+ switchToAboutMe()
+ showNavigation = true
+ }
+ }
+ invalidateOptionsMenu()
+ return showNavigation
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ if (mCurrentFragment != null) {
+ outState.putString("tag", mCurrentFragment!!.tag)
+ }
+ }
+
+ override fun onEventResult(function: String, errorCode: Int, errorMessage: String?) {
+ when(function){
+ EaseConstant.API_ASYNC_ADD_CONTACT -> {
+ if (errorCode == ChatError.EM_NO_ERROR){
+ runOnUiThread{
+ mContext.showToast(mContext.resources.getString(R.string.em_main_add_contact_success))
+ }
+ }else{
+ runOnUiThread{
+ if (errorCode == ChatError.USER_NOT_FOUND){
+ mContext.showToast(mContext.resources.getString(R.string.em_main_add_contact_not_found))
+ }else{
+ mContext.showToast(errorMessage.toString())
+ }
+ }
+ }
+ }
+ else -> {}
+ }
+ }
+
+ override fun getUnreadCountSuccess(count: String?) {
+ if (count.isNullOrEmpty()) {
+ badgeMap[0]?.text = ""
+ badgeMap[0]?.visibility = View.GONE
+ } else {
+ badgeMap[0]?.text = count
+ badgeMap[0]?.visibility = View.VISIBLE
+ }
+ }
+
+ override fun getRequestUnreadCountSuccess(count: String?) {
+ if (count.isNullOrEmpty()) {
+ badgeMap[1]?.text = ""
+ badgeMap[1]?.visibility = View.GONE
+ } else {
+ badgeMap[1]?.text = count
+ badgeMap[1]?.visibility = View.VISIBLE
+ }
+ }
+
+ private val contactListener = object : EaseContactListener() {
+ override fun onContactInvited(username: String?, reason: String?) {
+ mainViewModel.getRequestUnreadCount()
+ }
+ }
+
+ private fun synchronizeProfile(){
+ lifecycleScope.launch {
+ mProfileViewModel.synchronizeProfile()
+ .onCompletion { dismissLoading() }
+ .catchChatException { e ->
+ ChatLog.e("MainActivity", " synchronizeProfile fail error message = " + e.description)
+ }
+ .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(5000), null)
+ .collect {
+ ChatLog.e("MainActivity","synchronizeProfile result $it")
+ it?.let {
+ DemoHelper.getInstance().getDataModel().insertUser(it)
+ EaseIM.updateCurrentUser(it)
+ CallKitManager.setEaseCallKitUserInfo(it.id)
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE + EaseEvent.TYPE.CONTACT)
+ .post(lifecycleScope, EaseEvent(DemoConstant.EVENT_UPDATE_SELF, EaseEvent.TYPE.CONTACT))
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/ActivityState.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/ActivityState.kt
new file mode 100644
index 00000000..c4c33af0
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/ActivityState.kt
@@ -0,0 +1,32 @@
+package io.agora.chatdemo.base
+
+import android.app.Activity
+
+/**
+ * Created by shuwei on 2017/12/18.
+ */
+interface ActivityState {
+ /**
+ * 得到当前Activity
+ * @return
+ */
+ fun current(): Activity?
+
+ /**
+ * 得到Activity集合
+ * @return
+ */
+ val activityList: List?
+
+ /**
+ * 任务栈中Activity的总数
+ * @return
+ */
+ fun count(): Int
+
+ /**
+ * 判断应用是否处于前台,即是否可见
+ * @return
+ */
+ val isFront: Boolean
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseDialogFragment.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseDialogFragment.kt
new file mode 100644
index 00000000..6b494755
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseDialogFragment.kt
@@ -0,0 +1,131 @@
+package io.agora.chatdemo.base
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.annotation.IdRes
+import androidx.fragment.app.DialogFragment
+
+/**
+ * As the base class for dialog fragments
+ */
+abstract class BaseDialogFragment : DialogFragment() {
+ var mContext: Activity? = null
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ mContext = context as Activity
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ initArgument()
+ val view = inflater.inflate(layoutId, container, false)
+ setChildView(view)
+ setDialogAttrs()
+ return view
+ }
+
+ open fun setChildView(view: View?) {}
+
+ abstract val layoutId: Int
+ private fun setDialogAttrs() {
+ try {
+ dialog!!.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initView(savedInstanceState)
+ initListener()
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ initData()
+ }
+
+ open fun initArgument() {}
+ open fun initView(savedInstanceState: Bundle?) {}
+ open fun initListener() {}
+ open fun initData() {}
+
+ /**
+ * To obtain the current view control through the ID, it needs to be called in the lifecycle after onViewCreated ()
+ * @param id
+ * @param
+ * @return
+ */
+ protected fun findViewById(@IdRes id: Int): T? {
+ return requireView().findViewById(id)
+ }
+
+ /**
+ * Dialog width is full, height is customizable
+ */
+ fun setDialogParams() {
+ try {
+ val dialogWindow = dialog!!.window
+ val lp = dialogWindow!!.attributes
+ lp.dimAmount = 0.6f
+ lp.width = ViewGroup.LayoutParams.MATCH_PARENT
+ lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
+ lp.gravity = Gravity.BOTTOM
+ setDialogParams(lp)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * Full screen dialog
+ */
+ fun setDialogFullParams() {
+ val dialogHeight = getContextRect(mContext)
+ val height = if (dialogHeight == 0) ViewGroup.LayoutParams.MATCH_PARENT else dialogHeight
+ setDialogParams(ViewGroup.LayoutParams.MATCH_PARENT, height, 0.0f)
+ }
+
+ open fun setDialogParams(width: Int, height: Int, dimAmount: Float) {
+ try {
+ val dialogWindow = dialog!!.window
+ val lp = dialogWindow!!.attributes
+ lp.dimAmount = dimAmount
+ lp.width = width
+ lp.height = height
+ dialogWindow.attributes = lp
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ open fun setDialogParams(layoutParams: WindowManager.LayoutParams?) {
+ try {
+ val dialogWindow = dialog!!.window
+ dialogWindow!!.attributes = layoutParams
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ //Get content area
+ private fun getContextRect(activity: Activity?): Int {
+ //application area
+ val outRect1 = Rect()
+ activity?.window?.decorView?.getWindowVisibleDisplayFrame(outRect1)
+ return outRect1.height()
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseInitActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseInitActivity.kt
new file mode 100644
index 00000000..ba570133
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseInitActivity.kt
@@ -0,0 +1,38 @@
+package io.agora.chatdemo.base
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.viewbinding.ViewBinding
+import io.agora.uikit.base.EaseBaseActivity
+
+abstract class BaseInitActivity : EaseBaseActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ initIntent(intent)
+ initView(savedInstanceState)
+ initListener()
+ initData()
+ }
+
+ /**
+ * init intent
+ * @param intent
+ */
+ protected open fun initIntent(intent: Intent?) {}
+
+ /**
+ * init view
+ * @param savedInstanceState
+ */
+ protected open fun initView(savedInstanceState: Bundle?) {}
+
+ /**
+ * init listener
+ */
+ protected open fun initListener() {}
+
+ /**
+ * init data
+ */
+ protected open fun initData() {}
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseRepository.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseRepository.kt
new file mode 100644
index 00000000..8019b8f1
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/BaseRepository.kt
@@ -0,0 +1,10 @@
+package io.agora.chatdemo.base
+
+import android.content.Context
+import io.agora.chatdemo.DemoApplication
+
+open class BaseRepository {
+ fun getContext(): Context {
+ return DemoApplication.getInstance()
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/UserActivityLifecycleCallbacks.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/UserActivityLifecycleCallbacks.kt
new file mode 100644
index 00000000..25e366bd
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/base/UserActivityLifecycleCallbacks.kt
@@ -0,0 +1,104 @@
+package io.agora.chatdemo.base
+
+import android.app.Activity
+import android.app.Application
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+
+/**
+ * Used for maintenance Activity lifecycle
+ */
+class UserActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks, ActivityState {
+ override val activityList: MutableList = ArrayList()
+ private val resumeActivity: MutableList = ArrayList()
+ override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
+ Log.e("ActivityLifecycle", "onActivityCreated " + activity.localClassName)
+ activityList.add(0, activity)
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+ Log.e("ActivityLifecycle", "onActivityStarted " + activity.localClassName)
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ Log.e(
+ "ActivityLifecycle",
+ "onActivityResumed activity's taskId = " + activity.taskId + " name: " + activity.localClassName
+ )
+ if (!resumeActivity.contains(activity)) {
+ resumeActivity.add(activity)
+ if (resumeActivity.size == 1) {
+ //do nothing
+ }
+ }
+ }
+
+ override fun onActivityPaused(activity: Activity) {
+ Log.e("ActivityLifecycle", "onActivityPaused " + activity.localClassName)
+ }
+
+ override fun onActivityStopped(activity: Activity) {
+ Log.e("ActivityLifecycle", "onActivityStopped " + activity.localClassName)
+ resumeActivity.remove(activity)
+ if (resumeActivity.isEmpty()) {
+ Log.e("ActivityLifecycle", "在后台了")
+ }
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {
+ Log.e("ActivityLifecycle", "onActivitySaveInstanceState " + activity.localClassName)
+ }
+
+ override fun onActivityDestroyed(activity: Activity) {
+ Log.e("ActivityLifecycle", "onActivityDestroyed " + activity.localClassName)
+ activityList.remove(activity)
+ }
+
+ override fun current(): Activity? {
+ return if (activityList.size > 0) activityList[0] else null
+ }
+
+ override fun count(): Int {
+ return activityList.size
+ }
+
+ override val isFront: Boolean
+ get() = resumeActivity.size > 0
+
+ /**
+ * 跳转到目标activity
+ * @param cls
+ */
+ fun skipToTarget(cls: Class<*>?) {
+ if (activityList.size > 0) {
+ current()?.startActivity(Intent(current(), cls))
+ for (activity in activityList) {
+ activity.finish()
+ }
+ }
+ }
+
+ /**
+ * finish target activity
+ * @param cls
+ */
+ fun finishTarget(cls: Class<*>) {
+ if (activityList.isNotEmpty()) {
+ for (activity in activityList) {
+ if (activity.javaClass == cls) {
+ activity.finish()
+ }
+ }
+ }
+ }
+
+ val isOnForeground: Boolean
+ /**
+ * 判断app是否在前台
+ * @return
+ */
+ get() = resumeActivity.isNotEmpty()
+
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/Language.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/Language.kt
new file mode 100644
index 00000000..f2e3b2b5
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/Language.kt
@@ -0,0 +1,26 @@
+package io.agora.chatdemo.bean
+
+data class Language(
+ val type:LanguageType,
+ val tag:String
+)
+
+enum class LanguageType(val value:String){
+ ZH("zh"),
+ EN("en");
+
+ companion object {
+ fun from(value: String): LanguageType {
+ val types = LanguageType.values()
+ val length = types.size
+ for (i in 0 until length) {
+ val type = types[i]
+ if (type.value == value) {
+ return type
+ }
+ }
+ return ZH
+ }
+ }
+}
+
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/LoginResult.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/LoginResult.kt
new file mode 100644
index 00000000..0c07af64
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/LoginResult.kt
@@ -0,0 +1,11 @@
+package io.agora.chatdemo.bean
+
+class LoginResult {
+ var code = 0
+ var agoraUid: Int = 0
+ var chatUserName: String? = null
+ var avatarUrl: String? = null
+ var token: String? = null
+ var accessToken: String? = null
+ var expireTimestamp: Long? = null
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/PresenceData.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/PresenceData.kt
new file mode 100644
index 00000000..46fea61a
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/bean/PresenceData.kt
@@ -0,0 +1,34 @@
+package io.agora.chatdemo.bean
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import io.agora.chatdemo.R
+
+enum class PresenceData(
+ @field:StringRes @get:StringRes
+ @param:StringRes var presence: Int, @field:DrawableRes @get:DrawableRes
+ @param:DrawableRes var presenceIcon: Int
+) {
+ ONLINE(
+ R.string.ease_presence_online,
+ R.drawable.ease_presence_online
+ ),
+ BUSY(
+ R.string.ease_presence_busy,
+ R.drawable.ease_presence_busy
+ ),
+ DO_NOT_DISTURB(
+ R.string.ease_presence_do_not_disturb,
+ R.drawable.ease_presence_do_not_disturb
+ ),
+ AWAY(
+ R.string.ease_presence_away,
+ R.drawable.ease_presence_away
+ ),
+ OFFLINE(
+ R.string.ease_presence_offline,
+ R.drawable.ease_presence_offline
+ ),
+ CUSTOM(R.string.ease_presence_custom, R.drawable.ease_presence_custom)
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallKitActivityLifecycleCallback.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallKitActivityLifecycleCallback.kt
new file mode 100644
index 00000000..1f5508cd
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallKitActivityLifecycleCallback.kt
@@ -0,0 +1,109 @@
+package io.agora.chatdemo.callkit
+
+import android.app.Activity
+import android.app.Application
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import io.agora.chat.callkit.general.EaseCallFloatWindow
+import io.agora.chatdemo.MainActivity
+import io.agora.chatdemo.base.ActivityState
+import io.agora.chatdemo.callkit.extensions.isTargetActivity
+import io.agora.chatdemo.common.extensions.internal.makeTaskToFront
+import io.agora.chatdemo.page.splash.SplashActivity
+
+class CallKitActivityLifecycleCallback: Application.ActivityLifecycleCallbacks, ActivityState {
+ private val resumeActivity = mutableListOf()
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ activityList.add(0, activity)
+ if (activity is MainActivity) {
+ CallKitManager.receiveCallPush(activity)
+ }
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ if (!resumeActivity.contains(activity)) {
+ resumeActivity.add(activity)
+ restartSingleInstanceActivity(activity)
+ }
+ }
+
+ override fun onActivityPaused(activity: Activity) {
+
+ }
+
+ override fun onActivityStopped(activity: Activity) {
+ resumeActivity.remove(activity)
+ if (resumeActivity.isEmpty()) {
+ val a = getOtherTaskSingleInstanceActivity(activity.taskId)
+ if (a != null && a.isTargetActivity() && !EaseCallFloatWindow.getInstance().isShowing) {
+ a.makeTaskToFront()
+ }
+ Log.e("ActivityLifecycle", "在后台了")
+ }
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
+
+ }
+
+ override fun onActivityDestroyed(activity: Activity) {
+ activityList.remove(activity)
+ }
+
+ override fun current(): Activity? {
+ return if (activityList.size > 0) activityList[0] else null
+ }
+
+ override val activityList: MutableList
+ get() = mutableListOf()
+
+ override fun count(): Int {
+ return activityList.size
+ }
+
+ override val isFront: Boolean
+ get() = resumeActivity.size > 0
+
+ private fun getOtherTaskSingleInstanceActivity(taskId: Int): Activity? {
+ if (taskId != 0 && activityList.size > 1) {
+ for (activity in activityList) {
+ if (activity.taskId != taskId) {
+ if (activity.isTargetActivity()) {
+ return activity
+ }
+ }
+ }
+ }
+ return null
+ }
+
+ /**
+ * 用于按下home键,点击图标,检查启动模式是singleInstance,且在activity列表中首位的Activity
+ * 下面的方法,专用于解决启动模式是singleInstance, 为开启悬浮框的情况
+ * @param activity
+ */
+ private fun restartSingleInstanceActivity(activity: Activity) {
+ val isClickByFloat = activity.intent.getBooleanExtra("isClickByFloat", false)
+ if (isClickByFloat) {
+ return
+ }
+ //刚启动,或者从桌面返回app
+ if (resumeActivity.size == 1 && resumeActivity[0] is SplashActivity) {
+ return
+ }
+ //至少需要activityList中至少两个activity
+ if (resumeActivity.size >= 1 && activityList.size > 1) {
+ val a = getOtherTaskSingleInstanceActivity(resumeActivity[0].taskId)
+ if (//当前activity和列表中首个activity不相同
+ a != null && !a.isFinishing && a !== activity && a.taskId != activity.taskId && !EaseCallFloatWindow.getInstance().isShowing) {
+ Log.e("ActivityLifecycle", "启动了activity = " + a.javaClass.name)
+ activity.startActivity(Intent(activity, a.javaClass))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallKitManager.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallKitManager.kt
new file mode 100644
index 00000000..92995787
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallKitManager.kt
@@ -0,0 +1,303 @@
+package io.agora.chatdemo.callkit
+
+import android.app.Application
+import android.content.Context
+import android.content.Intent
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentActivity
+import io.agora.chat.callkit.EaseCallKit
+import io.agora.chat.callkit.bean.EaseCallUserInfo
+import io.agora.chat.callkit.bean.EaseUserAccount
+import io.agora.chat.callkit.general.EaseCallKitConfig
+import io.agora.chat.callkit.general.EaseCallType
+import io.agora.chat.callkit.listener.EaseCallGetUserAccountCallback
+import io.agora.chat.callkit.listener.EaseCallKitTokenCallback
+import io.agora.chatdemo.BuildConfig
+import io.agora.chatdemo.R
+import io.agora.chatdemo.callkit.activity.CallMultipleBaseActivity
+import io.agora.chatdemo.callkit.activity.CallSingleBaseActivity
+import io.agora.chatdemo.callkit.activity.CallMultipleInviteActivity
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatError
+import io.agora.uikit.common.ChatHttpClientManagerBuilder
+import io.agora.uikit.common.ChatHttpResponse
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.dialog.SimpleListSheetDialog
+import io.agora.uikit.common.dialog.SimpleSheetType
+import io.agora.uikit.common.extensions.toUser
+import io.agora.uikit.feature.chat.enums.EaseChatType
+import io.agora.uikit.interfaces.SimpleListSheetItemClickListener
+import io.agora.uikit.model.EaseMenuItem
+import io.agora.uikit.model.EaseUser
+import io.agora.uikit.model.getNickname
+import io.agora.uikit.provider.getSyncUser
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.json.JSONObject
+
+object CallKitManager {
+
+ /**
+ * Whether it is a rtc call.
+ */
+ var isRtcCall = false
+
+ /**
+ * Rtc call type.
+ */
+ var rtcType = 0
+
+ /**
+ * If multiple call, should set groupId.
+ */
+ var currentCallGroupId: String? = null
+
+ var channelName: String? = null
+
+ private const val TAG = "CallKitManager"
+ private const val RESULT_PARAM_TOKEN = "accessToken"
+ private const val RESULT_PARAM_UID = "agoraUid"
+ private const val RESULT_PARAM_RESULT = "result"
+ const val KEY_GROUP_ID = "groupId"
+ const val KEY_CALL_TYPE = "easeCallType"
+ const val KEY_INVITE_PARAMS = "invite_params"
+ const val EXTRA_CONFERENCE_GROUP_ID = "groupId"
+ const val EXTRA_CONFERENCE_GROUP_EXIT_MEMBERS = "existMembers"
+
+
+ fun init(context: Context) {
+ EaseCallKitConfig().apply {
+ callTimeOut = 30
+ agoraAppId = BuildConfig.AGORA_APPID
+ isEnableRTCToken = true
+ defaultHeadImage = EaseIM.getCurrentUser()?.avatar
+ EaseCallKit.getInstance().init(context,this)
+ }
+ (context.applicationContext as Application).registerActivityLifecycleCallbacks(CallKitActivityLifecycleCallback())
+ // Register the activities which you have registered in manifest
+ EaseCallKit.getInstance().registerVideoCallClass(CallSingleBaseActivity::class.java)
+ EaseCallKit.getInstance().registerMultipleVideoClass(CallMultipleBaseActivity::class.java)
+ val callKitListener = DemoCallKitListener(context)
+ EaseCallKit.getInstance().setCallKitListener(callKitListener)
+ }
+
+ /**
+ * Show single chat video call dialog.
+ */
+ fun showSelectDialog(type: EaseChatType?, context: Context, conversationId: String?) {
+ val context = (context as FragmentActivity)
+ val mutableListOf = mutableListOf(
+ EaseMenuItem(
+ menuId = R.id.chat_video_call_voice,
+ title = context.getString(R.string.voice_call),
+ resourceId = R.drawable.phone_pick,
+ titleColor = ContextCompat.getColor(context, R.color.color_primary),
+ resourceTintColor = ContextCompat.getColor(context, R.color.color_primary)
+ ),
+ EaseMenuItem(
+ menuId = R.id.chat_video_call_video,
+ title = context.getString(R.string.video_call),
+ resourceId = R.drawable.video_camera,
+ titleColor = ContextCompat.getColor(context, R.color.color_primary),
+ resourceTintColor = ContextCompat.getColor(context, R.color.color_primary)
+ ),
+ )
+ val dialog = SimpleListSheetDialog(
+ context = context,
+ itemList = mutableListOf,
+ type = SimpleSheetType.ITEM_LAYOUT_DIRECTION_START)
+ dialog.setSimpleListSheetItemClickListener(object : SimpleListSheetItemClickListener {
+ override fun onItemClickListener(position: Int, menu: EaseMenuItem) {
+ dialog.dismiss()
+ when(menu.menuId){
+ R.id.chat_video_call_voice -> {
+ type?.let {
+ if (it == EaseChatType.SINGLE_CHAT){
+ startSingleAudioCall(conversationId)
+ }else{
+ startConferenceCall(EaseCallType.CONFERENCE_VOICE_CALL,context, conversationId)
+ }
+ }
+ }
+ R.id.chat_video_call_video -> {
+ type?.let {
+ if (it == EaseChatType.SINGLE_CHAT){
+ startSingleVideoCall(conversationId)
+ }else{
+ startConferenceCall(EaseCallType.CONFERENCE_VIDEO_CALL, context, conversationId)
+ }
+ }
+ }
+ else -> {}
+ }
+ }
+ })
+ context.supportFragmentManager.let { dialog.show(it,"video_call_dialog") }
+ }
+
+ /**
+ * Start single audio call.
+ */
+ fun startSingleAudioCall(conversationId: String?) {
+ channelName = ""
+ rtcType = EaseCallType.SINGLE_VOICE_CALL.ordinal
+ EaseCallKit.getInstance().startSingleCall(
+ EaseCallType.SINGLE_VOICE_CALL, conversationId, null,
+ CallSingleBaseActivity::class.java
+ )
+ }
+
+ /**
+ * Start single video call.
+ */
+ fun startSingleVideoCall(conversationId: String?) {
+ channelName = ""
+ rtcType = EaseCallType.SINGLE_VIDEO_CALL.ordinal
+ EaseCallKit.getInstance().startSingleCall(
+ EaseCallType.SINGLE_VIDEO_CALL, conversationId, null,
+ CallSingleBaseActivity::class.java
+ )
+ }
+
+
+ /**
+ * Receive call push.
+ */
+ fun receiveCallPush(context: Context) {
+ if (isRtcCall) {
+ if (EaseCallType.getfrom(rtcType) != EaseCallType.CONFERENCE_VIDEO_CALL && EaseCallType.getfrom(rtcType) !=EaseCallType.CONFERENCE_VOICE_CALL) {
+ startVideoCallActivity(context)
+ } else {
+ startMultipleVideoActivity(context)
+ }
+ isRtcCall = false
+ }
+ }
+
+ private fun startVideoCallActivity(context: Context) {
+ rtcType = EaseCallType.SINGLE_VIDEO_CALL.ordinal
+ Intent(context, CallSingleBaseActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(this)
+ }
+ }
+
+ private fun startMultipleVideoActivity(context: Context) {
+ rtcType = EaseCallType.CONFERENCE_VIDEO_CALL.ordinal
+ Intent(context, CallMultipleBaseActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(this)
+ }
+ }
+
+ /**
+ * Start conference call.
+ */
+ fun startConferenceCall(callType:EaseCallType,context: Context, groupId: String?) {
+ rtcType = callType.ordinal
+ val intent = Intent(context, CallMultipleInviteActivity::class.java)
+ intent.putExtra(EXTRA_CONFERENCE_GROUP_ID, groupId)
+ context.startActivity(intent)
+ }
+
+
+ /**
+ * Get rtc token from server.
+ */
+ fun getRtcToken(tokenUrl: String, callback: EaseCallKitTokenCallback?) {
+ executeGetRequest(tokenUrl) {
+ it?.let { response ->
+ ChatLog.d(TAG, "getRtcToken: url:$tokenUrl ${response.code}, ${response.content}")
+ if (response.code == 200) {
+ response.content?.let { body ->
+ try {
+ val result = JSONObject(body)
+ val token = result.getString(RESULT_PARAM_TOKEN)
+ val uid = result.getInt(RESULT_PARAM_UID)
+ EaseIM.getCurrentUser()?.let { profile->
+ setEaseCallKitUserInfo(profile.id)
+ }
+ callback?.onSetToken(token, uid)
+ } catch (e: Exception) {
+ e.stackTrace
+ callback?.onGetTokenError(ChatError.GENERAL_ERROR, e.message)
+ }
+ }
+ } else {
+ callback?.onGetTokenError(response.code, response.content)
+ }
+ } ?: kotlin.run {
+ callback?.onSetToken(null, 0)
+ }
+ }
+ }
+
+ fun getUserIdByAgoraUid(uId:Int,url: String, callback: EaseCallGetUserAccountCallback?) {
+ executeGetRequest(url) {
+ it?.let { response ->
+ ChatLog.d(TAG, "getAllUsersByUid: url:$url ${response.code}, ${response.content}")
+ if (response.code == 200) {
+ response.content?.let { body ->
+ try {
+ val result = JSONObject(body)
+ val userList = result.getJSONObject(RESULT_PARAM_RESULT)
+ var account: EaseUserAccount? = null
+ userList.keys().forEach { uIdStr ->
+ val uid = Integer.valueOf(uIdStr)
+ val userId = userList.optString(uIdStr)
+ // Set user info to call kit.
+ CallUserInfo(userId).getUserInfo(currentCallGroupId).parse().apply {
+ EaseCallKit.getInstance().callKitConfig.setUserInfo(userId, this)
+ }
+ if (uid == uId || uid == 0){
+ account = EaseUserAccount(uIdStr.toInt(), userId)
+ }
+ }
+ callback?.onUserAccount(account)
+ } catch (e: Exception) {
+ e.stackTrace
+ callback?.onSetUserAccountError(ChatError.GENERAL_ERROR, e.message)
+ }
+ }
+ } else {
+ callback?.onSetUserAccountError(response.code, response.content)
+ }
+ } ?: kotlin.run {
+ callback?.onSetUserAccountError(ChatError.GENERAL_ERROR, "response is null")
+ }
+ }
+ }
+
+ /**
+ * Base get request.
+ */
+ private fun executeGetRequest(url: String, callback: (ChatHttpResponse?) -> Unit) {
+ CoroutineScope(Dispatchers.IO).launch {
+ ChatHttpClientManagerBuilder()
+ .get()
+ .setUrl(url)
+ .withToken(true)
+ .execute()?.let { response ->
+ callback(response)
+ } ?: kotlin.run {
+ callback(null)
+ }
+ }
+ }
+
+ fun setEaseCallKitUserInfo(userName: String) {
+ val user: EaseUser? = EaseIM.getUserProvider()?.getSyncUser(userName)?.toUser()
+ val userInfo = EaseCallUserInfo()
+ user?.let {
+ userInfo.nickName = user.getNickname()?:userName
+ userInfo.headImage = user.avatar
+ }
+ EaseCallKit.getInstance().callKitConfig.setUserInfo(userName, userInfo)
+ }
+
+ fun checkChannelNameNullOrEmpty():Boolean{
+ return channelName.isNullOrEmpty()
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallUserInfo.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallUserInfo.kt
new file mode 100644
index 00000000..2799497d
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/CallUserInfo.kt
@@ -0,0 +1,40 @@
+package io.agora.chatdemo.callkit
+
+import android.util.Log
+import io.agora.chat.callkit.bean.EaseCallUserInfo
+import io.agora.uikit.EaseIM
+import io.agora.uikit.model.EaseProfile
+import io.agora.uikit.provider.getSyncUser
+
+
+data class CallUserInfo(
+ val userId: String?,
+ var nickName: String? = null,
+ var headImage: String? = null
+)
+
+internal fun CallUserInfo.getUserInfo(groupId: String?): CallUserInfo {
+ return if (!groupId.isNullOrEmpty()) {
+ EaseProfile.getGroupMember(groupId, this.userId)?.let {
+ this.nickName = it.getNotEmptyName()
+ this.headImage = it.avatar
+ }
+ this
+ } else {
+ EaseIM.getUserProvider()?.getSyncUser(this.userId)?.let {
+ this.nickName = it.getNotEmptyName()
+ this.headImage = it.avatar
+ }
+ this
+ }
+}
+
+/**
+ * Parse to EaseCallUserInfo.
+ */
+internal fun CallUserInfo.parse(): EaseCallUserInfo {
+ return EaseCallUserInfo(nickName, headImage).let {
+ it.userId = this.userId
+ it
+ }
+}
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/ChatVoiceCallViewHolder.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/ChatVoiceCallViewHolder.kt
new file mode 100644
index 00000000..1d392c92
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/ChatVoiceCallViewHolder.kt
@@ -0,0 +1,48 @@
+package io.agora.chatdemo.callkit
+
+import android.view.View
+import io.agora.chat.callkit.EaseCallKit
+import io.agora.chat.callkit.general.EaseCallType
+import io.agora.chat.callkit.utils.EaseCallMsgUtils
+import io.agora.chatdemo.callkit.activity.CallSingleBaseActivity
+import io.agora.uikit.common.ChatMessage
+import io.agora.uikit.common.ChatMessageDirection
+import io.agora.uikit.feature.chat.viewholders.EaseChatRowViewHolder
+
+class ChatVoiceCallViewHolder(itemView: View): EaseChatRowViewHolder(itemView) {
+
+ override fun onBubbleClick(message: ChatMessage?) {
+ super.onBubbleClick(message)
+ message?.let {
+ if (it.getIntAttribute(EaseCallMsgUtils.CALL_TYPE, 0) == EaseCallType.SINGLE_VOICE_CALL.ordinal) {
+ if (it.direct() == ChatMessageDirection.RECEIVE) {
+ // answer call
+ EaseCallKit.getInstance().startSingleCall(
+ EaseCallType.SINGLE_VOICE_CALL, message.from, null,
+ CallSingleBaseActivity::class.java
+ )
+ } else {
+ // make call
+ EaseCallKit.getInstance().startSingleCall(
+ EaseCallType.SINGLE_VOICE_CALL, message.to, null,
+ CallSingleBaseActivity::class.java
+ )
+ }
+ } else {
+ if (it.direct() == ChatMessageDirection.RECEIVE) {
+ // answer call
+ EaseCallKit.getInstance().startSingleCall(
+ EaseCallType.SINGLE_VIDEO_CALL, message.from, null,
+ CallSingleBaseActivity::class.java
+ )
+ } else {
+ // make call
+ EaseCallKit.getInstance().startSingleCall(
+ EaseCallType.SINGLE_VIDEO_CALL, message.to, null,
+ CallSingleBaseActivity::class.java
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/DemoCallKitListener.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/DemoCallKitListener.kt
new file mode 100644
index 00000000..f2bdf8a9
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/DemoCallKitListener.kt
@@ -0,0 +1,182 @@
+package io.agora.chatdemo.callkit
+
+import android.content.Context
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.text.SpannableStringBuilder
+import com.hyphenate.chatdemo.callkit.extensions.getStringOrNull
+import io.agora.chat.callkit.EaseCallKit
+import io.agora.chat.callkit.bean.EaseUserAccount
+import io.agora.chat.callkit.general.EaseCallEndReason
+import io.agora.chat.callkit.general.EaseCallError
+import io.agora.chat.callkit.general.EaseCallType
+import io.agora.chat.callkit.listener.EaseCallGetUserAccountCallback
+import io.agora.chat.callkit.listener.EaseCallKitListener
+import io.agora.chat.callkit.listener.EaseCallKitTokenCallback
+import io.agora.chatdemo.BuildConfig
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.R
+import io.agora.chatdemo.callkit.activity.CallMultipleInviteActivity
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.utils.ToastUtils.showToast
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.common.extensions.mainScope
+import io.agora.uikit.model.EaseEvent
+import io.agora.uikit.provider.getSyncUser
+import io.agora.util.EMLog
+import org.json.JSONObject
+import java.text.SimpleDateFormat
+import java.util.TimeZone
+
+class DemoCallKitListener(val mContext: Context): EaseCallKitListener {
+
+ companion object{
+ private val TAG = DemoCallKitListener::class.java.simpleName
+ private const val PARAM_USER = "userAccount="
+ private const val PARAM_CHANNEL_NAME = "channelName="
+ }
+
+
+ //The URL here is for the demo example, and the actual project users should obtain it from their App Server
+ private val FETCH_TOKEN_URL = BuildConfig.APP_SERVER_PROTOCOL+ "://" + BuildConfig.APP_SERVER_DOMAIN + BuildConfig.APP_RTC_TOKEN_URL
+ private val FETCH_USER_MAPPER = BuildConfig.APP_SERVER_PROTOCOL+ "://" + BuildConfig.APP_SERVER_DOMAIN + BuildConfig.APP_RTC_CHANNEL_MAPPER_URL
+
+ private val handler: Handler = object : Handler(Looper.getMainLooper()) {
+ override fun handleMessage(msg: Message) {
+ super.handleMessage(msg)
+ val obj = msg.obj
+ if (obj is String) {
+ showToast(obj)
+ }
+ }
+ }
+
+ override fun onInviteUsers(
+ callType: EaseCallType?,
+ users: Array?,
+ ext: JSONObject?
+ ) {
+ CallKitManager.currentCallGroupId = ext?.getStringOrNull(CallKitManager.KEY_GROUP_ID)
+ Intent(mContext, CallMultipleInviteActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ putExtra(CallKitManager.EXTRA_CONFERENCE_GROUP_ID,CallKitManager.currentCallGroupId)
+ putExtra(CallKitManager.EXTRA_CONFERENCE_GROUP_EXIT_MEMBERS,users)
+ mContext.startActivity(this)
+ }
+ }
+
+ override fun onEndCallWithReason(
+ callType: EaseCallType?,
+ channelName: String?,
+ reason: EaseCallEndReason,
+ callTime: Long
+ ) {
+ EMLog.d( TAG,
+ "onEndCallWithReason" + (callType?.name
+ ?: " callType is null ") + " reason:" + reason + " time:" + callTime
+ )
+ val formatter = SimpleDateFormat("mm:ss")
+ formatter.timeZone = TimeZone.getTimeZone("UTC")
+ val callString: String = mContext.getString(R.string.call_duration,(formatter.format(callTime))?:"")
+ val message = handler.obtainMessage()
+ when (reason) {
+ EaseCallEndReason.EaseCallEndReasonHangup -> message.obj = callString
+ EaseCallEndReason.EaseCallEndReasonCancel -> {}
+ EaseCallEndReason.EaseCallEndReasonRemoteCancel -> message.obj = callString
+ EaseCallEndReason.EaseCallEndReasonRefuse -> message.obj =
+ mContext.getString(R.string.demo_call_end_reason_refuse)
+
+ EaseCallEndReason.EaseCallEndReasonBusy -> message.obj =
+ mContext.getString(R.string.demo_call_end_reason_busy)
+
+ EaseCallEndReason.EaseCallEndReasonNoResponse -> {}
+ EaseCallEndReason.EaseCallEndReasonRemoteNoResponse -> message.obj =
+ mContext.getString(R.string.demo_call_end_reason_busy_remote_no_response)
+
+ EaseCallEndReason.EaseCallEndReasonHandleOnOtherDeviceAgreed -> message.obj =
+ mContext.getString(R.string.demo_call_end_reason_other_device_agreed)
+
+ EaseCallEndReason.EaseCallEndReasonHandleOnOtherDeviceRefused -> message.obj =
+ mContext.getString(R.string.demo_call_end_reason_other_device_refused)
+ }
+ CallKitManager.channelName = ""
+ handler.sendMessage(message)
+ }
+
+ override fun onGenerateRTCToken(
+ userAccount: String,
+ channelName: String,
+ callback: EaseCallKitTokenCallback
+ ) {
+ val agoraUid: Int =DemoHelper.getInstance().getDataModel().getCurrentUserAgoraUid()
+ CallKitManager.channelName = channelName
+ EMLog.d(TAG, "onGenerateToken userId:$userAccount channelName:$channelName agoraUid:$agoraUid")
+ SpannableStringBuilder(FETCH_TOKEN_URL).apply {
+ append("/$channelName?$PARAM_USER$userAccount")
+ CallKitManager.getRtcToken(this.toString(), callback)
+ }
+ }
+
+ override fun onReceivedCall(callType: EaseCallType, fromUserId: String, ext: JSONObject?) {
+ EMLog.d(TAG, "onRecivedCall" + callType.name + " fromUserId:" + fromUserId)
+ ext?.getStringOrNull(CallKitManager.KEY_GROUP_ID)?.let { groupId ->
+ CallKitManager.currentCallGroupId = groupId
+ CallUserInfo(fromUserId).getUserInfo(groupId).parse().apply {
+ EaseCallKit.getInstance().callKitConfig.setUserInfo(userId, this)
+ }
+ } ?: kotlin.run {
+ CallKitManager.currentCallGroupId = null
+ CallUserInfo(fromUserId).apply {
+ EaseIM.getUserProvider()?.getSyncUser(userId)?.let { user ->
+ this.nickName = user.getNotEmptyName()
+ this.headImage = user.avatar
+ }
+ EaseCallKit.getInstance().callKitConfig.setUserInfo(userId, this.parse())
+ } // Single call
+ }
+ }
+
+ override fun onCallError(type: EaseCallError, errorCode: Int, description: String) {
+ EMLog.d(TAG, "onCallError" + type.name + " description:" + description)
+ if (type == EaseCallError.PROCESS_ERROR) {
+ showToast(description)
+ }
+ }
+
+ override fun onInViteCallMessageSent() {
+ if (ChatClient.getInstance().options.isIncludeSendMessageInMessageListener.not()) {
+ EaseFlowBus.with(EaseEvent.EVENT.ADD + EaseEvent.TYPE.MESSAGE)
+ .post(DemoHelper.getInstance().context.mainScope(), EaseEvent(DemoConstant.CALL_INVITE_MESSAGE, EaseEvent.TYPE.MESSAGE))
+ }
+ }
+
+ override fun onRemoteUserJoinChannel(
+ channelName: String?,
+ userName: String?,
+ uid: Int,
+ callback: EaseCallGetUserAccountCallback
+ ) {
+ // Only multi call callback this method
+ if (userName.isNullOrEmpty()) {
+ SpannableStringBuilder(FETCH_USER_MAPPER).apply {
+ append("?$PARAM_CHANNEL_NAME$channelName")
+ CallKitManager.getUserIdByAgoraUid(uid,this.toString(), callback)
+ }
+ } else {
+ // Set user info to call kit.
+ CallUserInfo(userName).getUserInfo(CallKitManager.currentCallGroupId).parse().apply {
+ EaseCallKit.getInstance().callKitConfig.setUserInfo(userId, this)
+ }
+ callback.onUserAccount(EaseUserAccount(uid, userName))
+ }
+ }
+
+ override fun onUserInfoUpdate(userName: String) {
+ //set user's nickname and avater
+ CallKitManager.setEaseCallKitUserInfo(userName)
+ }
+}
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/MultipleInviteViewHolder.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/MultipleInviteViewHolder.kt
new file mode 100644
index 00000000..d9f3215d
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/MultipleInviteViewHolder.kt
@@ -0,0 +1,6 @@
+package io.agora.chatdemo.callkit
+
+import android.view.View
+import io.agora.uikit.feature.chat.viewholders.EaseChatRowViewHolder
+
+class MultipleInviteViewHolder(itemView: View): EaseChatRowViewHolder(itemView)
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallMultipleBaseActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallMultipleBaseActivity.kt
new file mode 100644
index 00000000..4862d808
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallMultipleBaseActivity.kt
@@ -0,0 +1,14 @@
+package io.agora.chatdemo.callkit.activity
+
+import android.graphics.Color
+import io.agora.chat.callkit.ui.EaseCallMultipleBaseActivity
+import io.agora.chatdemo.callkit.extensions.setFitSystemForTheme
+import io.agora.uikit.common.utils.StatusBarCompat
+
+class CallMultipleBaseActivity : EaseCallMultipleBaseActivity() {
+ override fun initView() {
+ setFitSystemForTheme(true)
+ StatusBarCompat.compat(this, Color.parseColor("#858585"))
+ super.initView()
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallMultipleInviteActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallMultipleInviteActivity.kt
new file mode 100644
index 00000000..35180094
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallMultipleInviteActivity.kt
@@ -0,0 +1,131 @@
+package io.agora.chatdemo.callkit.activity
+
+import android.content.Intent
+import android.os.Bundle
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.ForegroundColorSpan
+import android.util.Log
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import androidx.core.content.ContextCompat
+import io.agora.chat.callkit.EaseCallKit
+import io.agora.chat.callkit.general.EaseCallType
+import io.agora.chatdemo.R
+import io.agora.chatdemo.base.BaseInitActivity
+import io.agora.chatdemo.callkit.CallKitManager
+import io.agora.chatdemo.callkit.fragment.ConferenceInviteFragment
+import io.agora.chatdemo.databinding.DemoActivityConferenceInviteBinding
+import io.agora.chatdemo.utils.ToastUtils.showToast
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.interfaces.OnContactSelectedListener
+
+class CallMultipleInviteActivity: BaseInitActivity() {
+ private val existMembers = mutableListOf()
+ private var groupId: String? = null
+ private var selectedMembers:MutableList = mutableListOf()
+ private var isFirstMeeting:Boolean = true
+
+ companion object {
+ private const val TAG = "ConferenceInvite"
+ }
+
+ override fun getViewBinding(inflater: LayoutInflater): DemoActivityConferenceInviteBinding? {
+ return DemoActivityConferenceInviteBinding.inflate(inflater)
+ }
+
+ override fun initIntent(intent: Intent?) {
+ super.initIntent(intent)
+ isFirstMeeting = true
+ intent?.let {
+ groupId = it.getStringExtra(CallKitManager.EXTRA_CONFERENCE_GROUP_ID)
+ it.getStringArrayExtra(CallKitManager.EXTRA_CONFERENCE_GROUP_EXIT_MEMBERS)?.let { members->
+ if (members.isNotEmpty()) {
+ existMembers.addAll(members)
+ }
+ if (!existMembers.contains(EaseIM.getCurrentUser()?.id)){
+ existMembers.add(ChatClient.getInstance().currentUser)
+ }
+ }?: kotlin.run {
+ existMembers.add(ChatClient.getInstance().currentUser)
+ }
+ }
+ }
+
+ override fun initView(savedInstanceState: Bundle?) {
+ super.initView(savedInstanceState)
+ if (groupId.isNullOrEmpty()) {
+ ChatLog.e(TAG, "groupId is null or empty")
+ finish()
+ return
+ }
+
+ val fragment = ConferenceInviteFragment.newInstance(groupId!!, existMembers)
+ fragment.setOnGroupMemberSelectedListener(object : OnContactSelectedListener {
+ override fun onContactSelectedChanged(v: View, selectedMembers: MutableList) {
+ this@CallMultipleInviteActivity.selectedMembers = selectedMembers
+ resetMenuInfo(selectedMembers.size)
+ }
+ })
+ supportFragmentManager.beginTransaction().replace(binding.flFragment.id, fragment).commit()
+
+ resetMenuInfo(selectedMembers.size)
+ }
+
+ private fun resetMenuInfo(size: Int) {
+ binding.titleBar.getToolBar().menu.findItem(R.id.chat_menu_member_call).let {
+ it.isEnabled = size > 0
+ it.title = getString(R.string.menu_member_call, size)
+ it.title?.let { title->
+ if (it.isEnabled) {
+ SpannableStringBuilder(title).let { span ->
+ span.setSpan(ForegroundColorSpan(ContextCompat.getColor(mContext, R.color.color_primary))
+ , 0, span.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ it.title = span
+ }
+ }
+ }
+ }
+ }
+
+ override fun initListener() {
+ super.initListener()
+
+ binding.titleBar.setOnMenuItemClickListener { item ->
+ when(item.itemId) {
+ R.id.chat_menu_member_call -> {
+ if (selectedMembers.isEmpty()) {
+ showToast(R.string.tips_select_contacts_first)
+ return@setOnMenuItemClickListener true
+ }
+ isFirstMeeting = false
+ val members = selectedMembers.toTypedArray()
+ val params: Map = mutableMapOf(CallKitManager.KEY_GROUP_ID to groupId!!)
+ EaseCallKit.getInstance().startInviteMultipleCall(EaseCallType.getfrom(CallKitManager.rtcType),members, params)
+ finish()
+ true
+ }
+ else -> false
+ }
+ }
+ binding.titleBar.setNavigationOnClickListener {
+ selectedMembers.clear()
+ setResult(RESULT_CANCELED)
+ onBackPressed()
+ if ((!isFirstMeeting || !CallMultipleBaseActivity().isFinishing) && !CallKitManager.checkChannelNameNullOrEmpty() ){
+ EaseCallKit.getInstance().startInviteMultipleCall(EaseCallType.getfrom(CallKitManager.rtcType),null, null)
+ }
+ }
+ }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ onBackPressed()
+ }
+ return super.onKeyDown(keyCode, event)
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallSingleBaseActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallSingleBaseActivity.kt
new file mode 100644
index 00000000..d7695fe9
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/activity/CallSingleBaseActivity.kt
@@ -0,0 +1,24 @@
+package io.agora.chatdemo.callkit.activity
+
+import android.graphics.Color
+import androidx.constraintlayout.widget.ConstraintLayout
+import io.agora.chat.callkit.general.EaseCallType
+import io.agora.chat.callkit.ui.EaseCallSingleBaseActivity
+import io.agora.chat.callkit.R
+import io.agora.chatdemo.callkit.extensions.setFitSystemForTheme
+import io.agora.uikit.common.utils.StatusBarCompat
+
+class CallSingleBaseActivity: EaseCallSingleBaseActivity() {
+ override fun initView() {
+ setFitSystemForTheme(true)
+ if (callType == EaseCallType.SINGLE_VIDEO_CALL) {
+ StatusBarCompat.compat(this, Color.parseColor("#000000"))
+ } else {
+ StatusBarCompat.compat(this, Color.parseColor("#bbbbbb"))
+ }
+ super.initView()
+ val rootLayout = findViewById(R.id.root_layout)
+ rootLayout.setBackgroundColor(resources.getColor(io.agora.uikit.R.color.ease_neutral_10))
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/adapter/ConferenceInviteAdapter.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/adapter/ConferenceInviteAdapter.kt
new file mode 100644
index 00000000..557887b3
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/adapter/ConferenceInviteAdapter.kt
@@ -0,0 +1,44 @@
+package io.agora.chatdemo.callkit.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import io.agora.chatdemo.callkit.holder.ConferenceMemberSelectViewHolder
+import io.agora.uikit.base.EaseBaseRecyclerViewAdapter
+import io.agora.uikit.databinding.EaseLayoutGroupSelectContactBinding
+import io.agora.uikit.feature.search.interfaces.OnContactSelectListener
+import io.agora.uikit.model.EaseUser
+
+class ConferenceInviteAdapter(private val groupId: String?): EaseBaseRecyclerViewAdapter() {
+ private var selectedListener: OnContactSelectListener? = null
+ private var existMembers:MutableList = mutableListOf()
+ private val checkedList:MutableList = mutableListOf()
+
+ override fun getViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ConferenceMemberSelectViewHolder(groupId, checkedList,
+ EaseLayoutGroupSelectContactBinding.inflate(LayoutInflater.from(parent.context),
+ parent, false)
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ if (holder is ConferenceMemberSelectViewHolder){
+ holder.setSelectedMembers(existMembers)
+ holder.setCheckBoxSelectListener(selectedListener)
+ }
+ super.onBindViewHolder(holder, position)
+ }
+
+ fun setExistMembers(existMembers:MutableList){
+ this.existMembers = existMembers
+ notifyDataSetChanged()
+ }
+
+ /**
+ * Set the listener for the checkbox selection.
+ */
+ fun setCheckBoxSelectListener(listener: OnContactSelectListener){
+ this.selectedListener = listener
+ notifyDataSetChanged()
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/extensions/Activity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/extensions/Activity.kt
new file mode 100644
index 00000000..ae4c2a21
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/extensions/Activity.kt
@@ -0,0 +1,88 @@
+package io.agora.chatdemo.callkit.extensions
+
+import android.app.Activity
+import android.graphics.Color
+import android.os.Build
+import android.text.TextUtils
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import androidx.annotation.ColorInt
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.content.ContextCompat
+import io.agora.chatdemo.R
+import io.agora.uikit.common.utils.StatusBarCompat
+
+/**
+ * Check if the current activity is the target activity.
+ */
+internal fun Activity.isTargetActivity(): Boolean {
+ if (TextUtils.equals(title, getString(R.string.demo_activity_label_video_call))
+ || TextUtils.equals(title, getString(R.string.demo_activity_label_multi_call))
+ ) {
+ return true
+ }
+ return false
+}
+
+/**
+ * Common settings for activity
+ * @param fitSystemForTheme
+ */
+internal fun Activity.setFitSystemForTheme(fitSystemForTheme: Boolean) {
+ val colorResource = ContextCompat.getColor(this, io.agora.uikit.R.color.ease_color_background)
+ val isDark = AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_YES
+ setFitSystemForTheme(fitSystemForTheme, colorResource, isDark)
+}
+
+
+/**
+ * Can set the status bar's style and change the background color
+ * @param fitSystemForTheme
+ * @param color Color
+ */
+internal fun Activity.setFitSystemForTheme(
+ fitSystemForTheme: Boolean,
+ @ColorInt color: Int,
+ isDark: Boolean
+) {
+ setFitSystem(fitSystemForTheme)
+ StatusBarCompat.compat(this, color)
+ StatusBarCompat.setLightStatusBar(this, isDark)
+}
+
+/**
+ * Can set the status bar's style and change the background color.
+ * @param fitSystemForTheme
+ * @param color Color string
+ */
+internal fun Activity.setFitSystemForTheme(fitSystemForTheme: Boolean, color: String?, isDark: Boolean) {
+ setFitSystem(fitSystemForTheme)
+ StatusBarCompat.compat(this, Color.parseColor(color))
+ StatusBarCompat.setLightStatusBar(this, isDark)
+}
+
+/**
+ * Set status bar style.
+ * @param fitSystemForTheme
+ */
+internal fun Activity.setFitSystem(fitSystemForTheme: Boolean) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ }
+ if (fitSystemForTheme) {
+ val contentFrameLayout = findViewById(Window.ID_ANDROID_CONTENT) as ViewGroup
+ val parentView = contentFrameLayout.getChildAt(0)
+ if (parentView != null && Build.VERSION.SDK_INT >= 14) {
+ parentView.fitsSystemWindows = true
+ }
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ val window = window
+ window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ window.decorView.systemUiVisibility =
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/extensions/JSONObject.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/extensions/JSONObject.kt
new file mode 100644
index 00000000..e0eaab98
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/extensions/JSONObject.kt
@@ -0,0 +1,14 @@
+package com.hyphenate.chatdemo.callkit.extensions
+
+import org.json.JSONObject
+
+/**
+ * Get string value from JSONObject or return null if the key does not exist.
+ */
+internal fun JSONObject.getStringOrNull(name: String): String? {
+ return if (has(name)) {
+ getString(name)
+ } else {
+ null
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/fragment/ConferenceInviteFragment.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/fragment/ConferenceInviteFragment.kt
new file mode 100644
index 00000000..5fdf5132
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/fragment/ConferenceInviteFragment.kt
@@ -0,0 +1,81 @@
+package io.agora.chatdemo.callkit.fragment
+
+import android.os.Bundle
+import android.view.View
+import io.agora.chatdemo.callkit.CallKitManager
+import io.agora.chatdemo.callkit.adapter.ConferenceInviteAdapter
+import io.agora.uikit.base.EaseBaseRecyclerViewAdapter
+import io.agora.uikit.common.EaseConstant
+import io.agora.uikit.feature.group.fragments.EaseGroupMemberFragment
+import io.agora.uikit.feature.search.interfaces.OnContactSelectListener
+import io.agora.uikit.interfaces.OnContactSelectedListener
+import io.agora.uikit.model.EaseUser
+
+class ConferenceInviteFragment: EaseGroupMemberFragment() {
+ companion object {
+ fun newInstance(groupId: String, existMembers: MutableList): ConferenceInviteFragment {
+ val fragment = ConferenceInviteFragment()
+ val bundle = Bundle()
+ bundle.putString(EaseConstant.EXTRA_CONVERSATION_ID, groupId)
+ bundle.putStringArrayList(CallKitManager.EXTRA_CONFERENCE_GROUP_EXIT_MEMBERS, ArrayList(existMembers))
+ fragment.arguments = bundle
+ return fragment
+ }
+ }
+
+ private var existMembers: MutableList = mutableListOf()
+ private var selectedMembers:MutableList = mutableListOf()
+ private var contactSelectedListener: OnContactSelectedListener? = null
+
+ override fun initAdapter(): EaseBaseRecyclerViewAdapter {
+ return ConferenceInviteAdapter(groupId)
+ }
+
+ override fun initView(savedInstanceState: Bundle?) {
+ super.initView(savedInstanceState)
+ arguments?.let {
+ existMembers = it.getStringArrayList(CallKitManager.EXTRA_CONFERENCE_GROUP_EXIT_MEMBERS) ?: mutableListOf()
+ }
+ binding?.srlContactRefresh?.setEnableLoadMore(false)
+ }
+
+ override fun initListener() {
+ super.initListener()
+ if (mListAdapter is ConferenceInviteAdapter) {
+ (mListAdapter as ConferenceInviteAdapter).setCheckBoxSelectListener(object :
+ OnContactSelectListener {
+
+ override fun onContactSelectedChanged(
+ v: View,
+ userId: String,
+ isSelected: Boolean
+ ) {
+ if (isSelected){
+ if (!selectedMembers.contains(userId)){
+ selectedMembers.add(userId)
+ }
+ }else{
+ if (selectedMembers.contains(userId)){
+ selectedMembers.remove(userId)
+ }
+ }
+ contactSelectedListener?.onContactSelectedChanged(v,selectedMembers)
+ }
+ })
+ }
+ }
+
+ override fun initData() {
+ super.initData()
+ if (mListAdapter is ConferenceInviteAdapter) {
+ (mListAdapter as ConferenceInviteAdapter).setExistMembers(existMembers)
+ }
+ }
+
+ /**
+ * Set the listener for the group member selection.
+ */
+ fun setOnGroupMemberSelectedListener(listener: OnContactSelectedListener?){
+ this.contactSelectedListener = listener
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/holder/ConferenceMemberSelectViewHolder.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/holder/ConferenceMemberSelectViewHolder.kt
new file mode 100644
index 00000000..c6c02026
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/holder/ConferenceMemberSelectViewHolder.kt
@@ -0,0 +1,76 @@
+package io.agora.chatdemo.callkit.holder
+
+import android.text.TextUtils
+import android.view.View
+import androidx.viewbinding.ViewBinding
+import io.agora.uikit.common.extensions.toProfile
+import io.agora.uikit.databinding.EaseLayoutGroupSelectContactBinding
+import io.agora.uikit.feature.group.adapter.EaseGroupMemberListAdapter
+import io.agora.uikit.feature.group.viewholders.EaseSelectContactViewHolder
+import io.agora.uikit.model.EaseProfile
+import io.agora.uikit.model.EaseUser
+
+class ConferenceMemberSelectViewHolder(
+ private val groupId: String?,
+ private val checkedMemberList: MutableList,
+ viewBinding: EaseLayoutGroupSelectContactBinding
+): EaseSelectContactViewHolder(viewBinding) {
+ private var isShowInitLetter:Boolean = false
+
+ fun setShowInitialLetter(isShow:Boolean){
+ this.isShowInitLetter = isShow
+ }
+
+ override fun initView(viewBinding: ViewBinding?) {
+ if (viewBinding is EaseLayoutGroupSelectContactBinding) {
+ viewBinding.cbSelect.isClickable = false
+ }
+ }
+
+ override fun setData(item: EaseUser?, position: Int) {
+ item?.let { user->
+ with(viewBinding) {
+ itemLayout.setOnClickListener {
+ val isChecked = cbSelect.isChecked
+ cbSelect.isChecked = !isChecked
+ if (!isChecked) {
+ if (!checkedMemberList.contains(user.userId)) {
+ checkedMemberList.add(user.userId)
+ }
+ }else {
+ if (checkedMemberList.contains(user.userId)) {
+ checkedMemberList.remove(user.userId)
+ }
+ }
+ selectedListener?.onContactSelectedChanged(it, user.userId, cbSelect.isChecked)
+ }
+ cbSelect.isChecked = checkedMemberList.contains(user.userId)
+ cbSelect.isSelected = false
+ itemLayout.isEnabled = true
+ if (checkedList.isNotEmpty() && isContains(checkedList,item.userId)) {
+ cbSelect.isSelected = true
+ itemLayout.isEnabled = false
+ }
+ val header = user.initialLetter
+ letterHeader.visibility = View.GONE
+ emPresence.setUserAvatarData(user.toProfile())
+ tvName.text = user.nickname ?: user.userId
+
+ groupId?.let { id ->
+ EaseProfile.getGroupMember(id, user.userId)?.let { profile ->
+ emPresence.setUserAvatarData(profile)
+ tvName.text = profile.getRemarkOrName()
+ }
+ }
+
+ if (position == 0 || header != null && adapter is EaseGroupMemberListAdapter
+ && header != (adapter as EaseGroupMemberListAdapter).getItem(position - 1)?.initialLetter) {
+ if (!TextUtils.isEmpty(header) && isShowInitLetter) {
+ letterHeader.visibility = View.VISIBLE
+ letterHeader.text = header
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/views/ChatRowConferenceInvite.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/views/ChatRowConferenceInvite.kt
new file mode 100644
index 00000000..480932b1
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/views/ChatRowConferenceInvite.kt
@@ -0,0 +1,39 @@
+package io.agora.chatdemo.callkit.views
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.TextView
+import io.agora.chatdemo.R
+import io.agora.uikit.common.ChatTextMessageBody
+import io.agora.uikit.widget.chatrow.EaseChatRow
+
+@SuppressLint("ViewConstructor")
+class ChatRowConferenceInvite @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ isSender: Boolean
+) : EaseChatRow(context, attrs, defStyleAttr, isSender) {
+ protected val contentView: TextView? by lazy { findViewById(R.id.tv_chatcontent) }
+ override fun onInflateView() {
+ inflater.inflate(
+ if (!isSender) R.layout.demo_row_received_conference_invite else R.layout.demo_row_sent_conference_invite,
+ this
+ )
+ }
+
+ override fun onSetUpView() {
+ (message?.body as? ChatTextMessageBody)?.let {
+ var message = it.message
+ if (message.isNullOrEmpty().not() && message.contains("-")) {
+ message = """
+ ${message.substring(0, message.indexOf("-") + 1)}
+ ${message.substring(message.indexOf("-") + 1)}
+ """.trimIndent()
+ }
+ contentView?.text = message
+ }
+ }
+
+}
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/views/ChatRowVoiceCall.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/views/ChatRowVoiceCall.kt
new file mode 100644
index 00000000..2d203166
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/callkit/views/ChatRowVoiceCall.kt
@@ -0,0 +1,42 @@
+package io.agora.chatdemo.callkit.views
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageView
+import android.widget.TextView
+import io.agora.chatdemo.R
+import io.agora.uikit.common.ChatTextMessageBody
+import io.agora.uikit.widget.chatrow.EaseChatRow
+
+@SuppressLint("ViewConstructor")
+class ChatRowVoiceCall @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ isSender: Boolean
+) : EaseChatRow(context, attrs, defStyleAttr, isSender) {
+ protected val contentView: TextView? by lazy { findViewById(R.id.tv_chatcontent) }
+ private val ivCallIcon: ImageView by lazy { findViewById(R.id.iv_call_icon) }
+ override fun onInflateView() {
+ inflater.inflate(
+ if (!isSender) R.layout.demo_row_received_voice_call else R.layout.demo_row_sent_voice_call,
+ this
+ )
+ }
+
+ override fun onSetUpView() {
+ (message?.body as? ChatTextMessageBody)?.let {
+ contentView?.text = it.message
+ }
+ message?.let {
+// val type = it.getIntAttribute(EaseMsgUtils.CALL_TYPE, 0)
+// if (type == EaseCallType.SINGLE_VIDEO_CALL.ordinal) {
+// ivCallIcon.setImageResource(com.hyphenate.easecallkit.R.drawable.d_chat_video_call_self)
+// } else {
+// ivCallIcon.setImageResource(com.hyphenate.easecallkit.R.drawable.d_chat_voice_call)
+// }
+ }
+ }
+
+}
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/DemoConstant.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/DemoConstant.kt
new file mode 100644
index 00000000..1af9182e
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/DemoConstant.kt
@@ -0,0 +1,26 @@
+package io.agora.chatdemo.common
+
+object DemoConstant {
+ const val SKIP_DEVELOPER_CONFIG = "skip_developer_config"
+ const val CALL_INVITE_MESSAGE = "call_invite_message"
+
+ const val EVENT_UPDATE_SELF = "event_update_self"
+ const val EVENT_UPDATE_USER_SUFFIX = "/info"
+
+ const val IS_BLACK_THEME = "isBlack"
+ const val TARGET_LANGUAGE = "target_language"
+ const val APP_LANGUAGE = "app_language"
+ const val MSG_NO_DISTURBANCE = "msg_no_disturbance"
+
+ const val FEATURES_TRANSLATION = "features_translation"
+ const val FEATURES_THREAD = "features_thread"
+ const val FEATURES_REACTION = "features_reaction"
+
+ const val IS_TYPING_ON = "isTyping"
+
+ const val PRESENCE_ONLINE = "Online"
+ const val PRESENCE_OFFLINE = "Offline"
+ const val PRESENCE_BUSY = "Busy"
+ const val PRESENCE_DO_NOT_DISTURB = "Do Not Disturb"
+ const val PRESENCE_AWAY = "Away"
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/DemoDataModel.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/DemoDataModel.kt
new file mode 100644
index 00000000..ea05c66a
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/DemoDataModel.kt
@@ -0,0 +1,330 @@
+package io.agora.chatdemo.common
+
+import android.content.Context
+import android.util.Log
+import io.agora.chatdemo.BuildConfig
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.callkit.CallKitManager
+import io.agora.chatdemo.common.room.AppDatabase
+import io.agora.chatdemo.common.room.dao.DemoUserDao
+import io.agora.chatdemo.common.room.entity.DemoUser
+import io.agora.chatdemo.common.room.entity.parse
+import io.agora.chatdemo.common.room.extensions.parseToDbBean
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatContact
+import io.agora.uikit.common.ChatException
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.ChatValueCallback
+import io.agora.uikit.common.extensions.toProfile
+import io.agora.uikit.common.extensions.toUser
+import io.agora.uikit.model.EaseProfile
+import io.agora.uikit.model.EaseUser
+import java.util.concurrent.ConcurrentHashMap
+
+class DemoDataModel(private val context: Context) {
+
+ private val database by lazy { AppDatabase.getDatabase(context, ChatClient.getInstance().currentUser) }
+
+ private val contactList = ConcurrentHashMap()
+
+
+ init {
+ PreferenceManager.init(context)
+ }
+
+ /**
+ * Initialize the local database.
+ */
+ fun initDb() {
+ if (EaseIM.isInited().not()) {
+ throw IllegalStateException("EaseIM SDK must be inited before using.")
+ }
+ database
+ resetUsersTimes()
+ contactList.clear()
+ val data = getAllContacts().values.map { it.toProfile() }
+ if (data.isNotEmpty()){
+ EaseIM.updateUsersInfo(data)
+ data.map { CallKitManager.setEaseCallKitUserInfo(it.id)}
+ }
+ }
+
+ /**
+ * Get the user data access object.
+ */
+ fun getUserDao(): DemoUserDao {
+ if (EaseIM.isInited().not()) {
+ throw IllegalStateException("EaseIM SDK must be inited before using.")
+ }
+ return database.userDao()
+ }
+
+ /**
+ * Get all contacts from cache.
+ */
+ fun getAllContacts(): Map {
+ if (contactList.isEmpty()) {
+ loadContactFromDb()
+ }
+ return contactList.mapValues { it.value.parse().toUser() }
+ }
+
+ private fun loadContactFromDb() {
+ contactList.clear()
+ try {
+ val localData = ChatClient.getInstance().contactManager().contactsFromLocal
+ getUserDao().getAll().forEach {
+ val profile = it.parse()
+ localData?.forEach { contact->
+ if (contact.equals(profile.id)){
+ contactList[it.userId] = profile.parseToDbBean()
+ }
+ }
+ }
+ }catch (e:ChatException){
+ ChatLog.e("DemoDataModel","loadContactFromDb error ${e.description}")
+ }
+ }
+
+ /**
+ * Get user by userId from local db.
+ */
+ fun getUser(userId: String?): DemoUser? {
+ if (userId.isNullOrEmpty()) {
+ return null
+ }
+ if (contactList.containsKey(userId)) {
+ return contactList[userId]
+ }
+ return getUserDao().getUser(userId)
+ }
+
+ /**
+ * Insert user to local db.
+ */
+ fun insertUser(user: EaseProfile,isInsertDb:Boolean = true) {
+ if (isInsertDb){
+ getUserDao().insertUser(user.parseToDbBean())
+ }
+ contactList[user.id] = user.parseToDbBean()
+ }
+
+ /**
+ * Insert users to local db.
+ */
+ fun insertUsers(users: List) {
+ getUserDao().insertUsers(users.map { it.parseToDbBean() })
+ users.forEach {
+ contactList[it.id] = it.parseToDbBean()
+ }
+ }
+
+ /**
+ * Update user update times.
+ */
+ fun updateUsersTimes(userIds: List) {
+ if (userIds.isNotEmpty()) {
+ userIds?.map { it.id }?.let { userIds ->
+ getUserDao().updateUsersTimes(userIds)
+ loadContactFromDb()
+ }
+ }
+ }
+
+ private fun resetUsersTimes() {
+ getUserDao().resetUsersTimes()
+ }
+
+ fun clearCache(){
+ contactList.clear()
+ }
+
+ /**
+ * Update UIKit's user cache.
+ */
+ fun updateUserCache(userId: String?) {
+ if (userId.isNullOrEmpty()) {
+ return
+ }
+ val user = contactList[userId]?.parse() ?: return
+ EaseIM.updateUsersInfo(mutableListOf(user))
+ }
+
+
+ /**
+ * Set the flag whether to use google push.
+ * @param useFCM
+ */
+ fun setUseFCM(useFCM: Boolean) {
+ PreferenceManager.putValue(KEY_PUSH_USE_FCM, useFCM)
+ }
+
+ /**
+ * Get the flag whether to use google push.
+ * @return
+ */
+ fun isUseFCM(): Boolean {
+ return PreferenceManager.getValue(KEY_PUSH_USE_FCM, false)
+ }
+
+ /**
+ * Set the developer mode.
+ * @param isDeveloperMode The developer mode.
+ */
+ fun setDeveloperMode(isDeveloperMode: Boolean) {
+ PreferenceManager.putValue(KEY_DEVELOPER_MODE, isDeveloperMode)
+ }
+
+ /**
+ * Get the developer mode.
+ * @return The developer mode.
+ */
+ fun isDeveloperMode(): Boolean {
+ return PreferenceManager.getValue(KEY_DEVELOPER_MODE, false)
+ }
+
+ /**
+ * Set the custom appKey.
+ * @param appKey
+ */
+ fun setCustomAppKey(appKey: String?) {
+ PreferenceManager.putValue(KEY_CUSTOM_APPKEY, appKey)
+ }
+
+ /**
+ * Get the custom appKey.
+ * @return
+ */
+ fun getCustomAppKey(): String {
+ return PreferenceManager.getValue(KEY_CUSTOM_APPKEY, "")
+ }
+
+ /**
+ * Get whether the custom configuration is enabled.
+ * @return
+ */
+ fun isCustomSetEnable(): Boolean {
+ return PreferenceManager.getValue(KEY_ENABLE_CUSTOM_SET, false)
+ }
+
+ /**
+ * Set whether the custom configuration is enabled.
+ * @param enable
+ */
+ fun enableCustomSet(enable: Boolean) {
+ PreferenceManager.putValue(KEY_ENABLE_CUSTOM_SET, enable)
+ }
+
+ /**
+ * Get whether the custom server is enabled.
+ * @return
+ */
+ fun isCustomServerEnable(): Boolean {
+ return PreferenceManager.getValue(KEY_ENABLE_CUSTOM_SERVER, false)
+ }
+
+ /**
+ * Set whether the custom server is enabled.
+ * @param enable
+ */
+ fun enableCustomServer(enable: Boolean) {
+ PreferenceManager.putValue(KEY_ENABLE_CUSTOM_SERVER, enable)
+ }
+
+ /**
+ * Set the REST server.
+ * @param restServer
+ */
+ fun setRestServer(restServer: String?) {
+ PreferenceManager.putValue(KEY_REST_SERVER, restServer)
+ }
+
+ /**
+ * Get the REST server.
+ * @return
+ */
+ fun getRestServer(): String? {
+ return PreferenceManager.getValue(KEY_REST_SERVER, "")
+ }
+
+ /**
+ * Set the IM server.
+ * @param imServer
+ */
+ fun setIMServer(imServer: String?) {
+ PreferenceManager.putValue(KEY_IM_SERVER, imServer)
+ }
+
+ /**
+ * Get the IM server.
+ * @return
+ */
+ fun getIMServer(): String? {
+ return PreferenceManager.getValue(KEY_IM_SERVER, "")
+ }
+
+ /**
+ * Set the port of the IM server.
+ * @param port
+ */
+ fun setIMServerPort(port: Int) {
+ PreferenceManager.putValue(KEY_IM_SERVER_PORT, port)
+ }
+
+ /**
+ * Get the port of the IM server.
+ */
+ fun getIMServerPort(): Int {
+ return PreferenceManager.getValue(KEY_IM_SERVER_PORT, 0)
+ }
+
+ /**
+ * Set the silent mode for the App.
+ */
+ fun setAppPushSilent(isSilent: Boolean) {
+ PreferenceManager.putValue(KEY_PUSH_APP_SILENT_MODEL, isSilent)
+ }
+
+ /**
+ * Get the silent mode for the App.
+ */
+ fun isAppPushSilent(): Boolean {
+ return PreferenceManager.getValue(KEY_PUSH_APP_SILENT_MODEL, false)
+ }
+
+ fun setCurrentUserAgoraUid(agoraUid:Int){
+ PreferenceManager.putValue("${BuildConfig.AGORA_CHAT_APPKEY}$SHARED_KEY_CURRENTUSER_AGORAUID",agoraUid)
+ }
+
+ fun getCurrentUserAgoraUid():Int{
+ return PreferenceManager.getValue("${BuildConfig.AGORA_CHAT_APPKEY}$SHARED_KEY_CURRENTUSER_AGORAUID",0)
+ }
+
+ fun putBoolean(key: String, value: Boolean){
+ PreferenceManager.putValue(key,value)
+ }
+
+ fun getBoolean(key: String,default:Boolean?=false): Boolean {
+ return if (default == null){
+ PreferenceManager.getValue(key, false)
+ }else{
+ PreferenceManager.getValue(key, default)
+ }
+ }
+
+ companion object {
+ private const val KEY_DEVELOPER_MODE = "shared_is_developer"
+ private const val KEY_AGREE_AGREEMENT = "shared_key_agree_agreement"
+ private const val KEY_CUSTOM_APPKEY = "SHARED_KEY_CUSTOM_APPKEY"
+ private const val KEY_REST_SERVER = "SHARED_KEY_REST_SERVER"
+ private const val KEY_IM_SERVER = "SHARED_KEY_IM_SERVER"
+ private const val KEY_IM_SERVER_PORT = "SHARED_KEY_IM_SERVER_PORT"
+ private const val KEY_ENABLE_CUSTOM_SERVER = "SHARED_KEY_ENABLE_CUSTOM_SERVER"
+ private const val KEY_ENABLE_CUSTOM_SET = "SHARED_KEY_ENABLE_CUSTOM_SET"
+ private const val KEY_PUSH_USE_FCM = "shared_key_push_use_fcm"
+ private const val KEY_PUSH_APP_SILENT_MODEL = "key_push_app_silent_model"
+ private const val SHARED_KEY_CURRENTUSER_AGORAUID = "SHARED_KEY_CURRENTUSER_AGORAUID"
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/ErrorCode.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/ErrorCode.kt
new file mode 100644
index 00000000..6f449e56
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/ErrorCode.kt
@@ -0,0 +1,66 @@
+package io.agora.chatdemo.common
+
+import io.agora.uikit.common.ChatError
+
+
+/**
+ * 定义一些本地的错误code
+ */
+object ErrorCode : ChatError() {
+ /**
+ * 当前网络不可用
+ */
+ const val EM_NETWORK_ERROR = -2
+
+ /**
+ * 未登录过环信
+ */
+ const val EM_NOT_LOGIN = -8
+
+ /**
+ * result解析错误
+ */
+ const val EM_PARSE_ERROR = -10
+
+ /**
+ * 网络问题请稍后重试
+ */
+ const val EM_ERR_UNKNOWN = -20
+
+ /**
+ * 安卓版本问题,只支持4.4以上
+ */
+ const val EM_ERR_IMAGE_ANDROID_MIN_VERSION = -50
+
+ /**
+ * 文件不存在
+ */
+ const val EM_ERR_FILE_NOT_EXIST = -55
+
+ /**
+ * 添加自己为好友
+ */
+ const val EM_ADD_SELF_ERROR = -100
+
+ /**
+ * 已经是好友
+ */
+ const val EM_FRIEND_ERROR = -101
+
+ /**
+ * 已经添加到黑名单中
+ */
+ const val EM_FRIEND_BLACK_ERROR = -102
+
+ /**
+ * 没有群组成员
+ */
+ const val EM_ERR_GROUP_NO_MEMBERS = -105
+
+ /**
+ * 删除对话失败
+ */
+ const val EM_DELETE_CONVERSATION_ERROR = -110
+ const val EM_DELETE_SYS_MSG_ERROR = -115
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/ListenersWrapper.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/ListenersWrapper.kt
new file mode 100644
index 00000000..47335e4e
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/ListenersWrapper.kt
@@ -0,0 +1,148 @@
+package io.agora.chatdemo.common
+
+import android.content.Intent
+import android.util.Log
+import io.agora.chatdemo.DemoApplication
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.common.helper.LocalNotifyHelper
+import io.agora.chatdemo.page.login.LoginActivity
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatGroup
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.ChatMessage
+import io.agora.uikit.common.ChatPresence
+import io.agora.uikit.common.ChatPresenceListener
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.common.extensions.ioScope
+import io.agora.uikit.common.extensions.mainScope
+import io.agora.uikit.common.impl.ValueCallbackImpl
+import io.agora.uikit.interfaces.EaseConnectionListener
+import io.agora.uikit.interfaces.EaseContactListener
+import io.agora.uikit.interfaces.EaseMessageListener
+import io.agora.uikit.model.EaseEvent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+object ListenersWrapper {
+ private var isLoadGroupList = false
+
+ private val connectListener by lazy {
+ object : EaseConnectionListener() {
+ override fun onConnected() {
+ // do something
+ CoroutineScope(Dispatchers.IO).launch {
+ val groups = ChatClient.getInstance().groupManager().allGroups
+ if (isLoadGroupList.not() && groups.isEmpty()) {
+ ChatClient.getInstance().groupManager().asyncGetJoinedGroupsFromServer(
+ ValueCallbackImpl>(onSuccess = {
+ isLoadGroupList = true
+ if (it.isEmpty().not()) {
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE.name)
+ .post(
+ DemoHelper.getInstance().context.ioScope(),
+ EaseEvent(EaseEvent.EVENT.UPDATE.name, EaseEvent.TYPE.GROUP)
+ )
+ }
+ }, onError = {_,_ ->
+
+ })
+ )
+ }
+ }
+
+ }
+
+ override fun onTokenExpired() {
+ super.onTokenExpired()
+ logout(false)
+ }
+
+
+ override fun onLogout(errorCode: Int, info: String?) {
+ super.onLogout(errorCode, info)
+ ChatLog.e("app","onLogout: $errorCode $info")
+ logout()
+ }
+ }
+ }
+
+ private fun logout(unbindPushToken:Boolean = true){
+ EaseIM.logout(unbindPushToken,
+ onSuccess = {
+ ChatLog.e("ListenersWrapper","logout success")
+ DemoApplication.getInstance().getLifecycleCallbacks().activityList.forEach {
+ it.finish()
+ }
+ DemoApplication.getInstance().apply {
+ val intent = Intent(this, LoginActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
+ }
+ },
+ onError = {code, error ->
+ ChatLog.e("ListenersWrapper","logout error $code $error")
+ }
+ )
+ }
+
+ private val messageListener by lazy { object : EaseMessageListener(){
+ override fun onMessageReceived(messages: MutableList?) {
+ super.onMessageReceived(messages)
+ if (DemoHelper.getInstance().getDataModel().isAppPushSilent()) {
+ return
+ }
+ // do something
+ messages?.forEach { message ->
+
+ if (EaseIM.checkMutedConversationList(message.conversationId())) {
+ return@forEach
+ }
+ if (DemoApplication.getInstance().getLifecycleCallbacks().isFront.not()) {
+ DemoHelper.getInstance().getNotifier()?.notify(message)
+ }
+ }
+ }
+ } }
+
+ private val presenceListener by lazy{
+ ChatPresenceListener {
+ defaultPresencesEvent(it)
+ }
+ }
+
+ private fun defaultPresencesEvent(presences: MutableList?){
+ presences?.forEach { presence->
+ PresenceCache.insertPresences(presence.publisher,presence)
+ EaseIM.getContext()?.let {
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE.name)
+ .post(it.mainScope(), EaseEvent(EaseEvent.EVENT.UPDATE.name, EaseEvent.TYPE.PRESENCE,presence.publisher))
+ }
+ }
+ }
+
+ private val contactListener by lazy { object : EaseContactListener(){
+
+ override fun onFriendRequestAccepted(username: String?) {
+ val notifyMsg = LocalNotifyHelper.createContactNotifyMessage(username)
+ ChatClient.getInstance().chatManager().saveMessage(notifyMsg)
+ DemoHelper.getInstance().context.let {
+ EaseFlowBus.with(EaseEvent.EVENT.ADD.name)
+ .post(it.mainScope(), EaseEvent(EaseEvent.EVENT.ADD.name, EaseEvent.TYPE.CONTACT))
+ }
+ }
+
+ override fun onContactDeleted(username: String?) {
+ LocalNotifyHelper.removeContactNotifyMessage(username)
+ }
+ } }
+
+ fun registerListeners() {
+ // register connection listener
+ EaseIM.addConnectionListener(connectListener)
+ EaseIM.addChatMessageListener(messageListener)
+ EaseIM.addPresenceListener(presenceListener)
+ EaseIM.addContactListener(contactListener)
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PreferenceManager.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PreferenceManager.kt
new file mode 100644
index 00000000..539cf241
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PreferenceManager.kt
@@ -0,0 +1,55 @@
+package io.agora.chatdemo.common
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.SharedPreferences
+
+internal object PreferenceManager {
+
+ private var mSharedPreferences: SharedPreferences? = null
+ private const val PREF_NAME = "saveInfo"
+
+ @Synchronized
+ fun init(context: Context) {
+ if (mSharedPreferences == null) {
+ mSharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+ }
+ }
+
+ /**
+ * Save the value to the preference.
+ * @param key The key of the preference.
+ * @param value The value of the preference.
+ */
+ @SuppressLint("NewApi")
+ fun putValue(key: String, value: T) {
+ val editor = mSharedPreferences?.edit()
+ when (value) {
+ is String -> editor?.putString(key, value)
+ is Int -> editor?.putInt(key, value)
+ is Boolean -> editor?.putBoolean(key, value)
+ is Float -> editor?.putFloat(key, value)
+ is Long -> editor?.putLong(key, value)
+ else -> editor?.putString(key, value.toString())
+ }
+ editor?.apply()
+ }
+
+ /**
+ * Get the value from the preference.
+ * @param key The key of the preference.
+ * @param defValue The default value of the preference.
+ */
+ fun getValue(key: String, defValue: T): T {
+ val value = when (defValue) {
+ is String -> mSharedPreferences?.getString(key, defValue)
+ is Int -> mSharedPreferences?.getInt(key, defValue)
+ is Boolean -> mSharedPreferences?.getBoolean(key, defValue)
+ is Float -> mSharedPreferences?.getFloat(key, defValue)
+ is Long -> mSharedPreferences?.getLong(key, defValue)
+ else -> mSharedPreferences?.getString(key, defValue.toString())
+ }
+ return value as T
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PresenceCache.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PresenceCache.kt
new file mode 100644
index 00000000..98792fd6
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PresenceCache.kt
@@ -0,0 +1,31 @@
+package io.agora.chatdemo.common
+
+import io.agora.uikit.common.ChatPresence
+import java.util.concurrent.ConcurrentHashMap
+
+object PresenceCache {
+
+ private val presencesMap: ConcurrentHashMap = ConcurrentHashMap()
+
+ @Synchronized
+ fun insertPresences(userId: String?,chatPresence: ChatPresence){
+ presencesMap.let { presence->
+ userId?.let {
+ presence[it] = chatPresence
+ }
+ }
+ }
+
+ fun getUserPresence(userId: String):ChatPresence?{
+ if (presencesMap.size > 0 && presencesMap.containsKey(userId)){
+ return presencesMap[userId]
+ }
+ return null
+ }
+
+ var getPresenceInfo:ConcurrentHashMap = presencesMap
+
+ fun clear(){
+ presencesMap.clear()
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PushActivityLifecycleCallback.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PushActivityLifecycleCallback.kt
new file mode 100644
index 00000000..dc6efd80
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PushActivityLifecycleCallback.kt
@@ -0,0 +1,39 @@
+package io.agora.chatdemo.common
+
+import android.app.Activity
+import android.app.Application
+import android.os.Bundle
+import io.agora.chatdemo.MainActivity
+
+class PushActivityLifecycleCallback: Application.ActivityLifecycleCallbacks {
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ // When MainActivity is created, get the push token and send it to the server
+ if (activity is MainActivity) {
+ PushManager.getPushTokenAndSend(activity)
+ }
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+
+ }
+
+ override fun onActivityPaused(activity: Activity) {
+
+ }
+
+ override fun onActivityStopped(activity: Activity) {
+
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
+
+ }
+
+ override fun onActivityDestroyed(activity: Activity) {
+
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PushManager.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PushManager.kt
new file mode 100644
index 00000000..600de61d
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/PushManager.kt
@@ -0,0 +1,86 @@
+package io.agora.chatdemo.common
+
+import android.app.Application
+import android.content.Context
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailabilityLight
+import com.google.firebase.analytics.FirebaseAnalytics
+import com.google.firebase.messaging.FirebaseMessaging
+import io.agora.chatdemo.DemoHelper
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.ChatPushHelper
+import io.agora.uikit.common.ChatPushListener
+import io.agora.uikit.common.ChatPushType
+import io.agora.uikit.common.PushConfig
+import io.agora.uikit.common.extensions.isMainProcess
+
+object PushManager {
+
+ /**
+ * Initialize push.
+ */
+ fun initPush(context: Context) {
+ if (context.isMainProcess()) {
+ // Register push activity lifecycle callback.
+ (context.applicationContext as? Application)?.registerActivityLifecycleCallbacks(PushActivityLifecycleCallback())
+ // Set pushListener to control the push type.
+ ChatPushHelper.getInstance().setPushListener(object : ChatPushListener() {
+
+ override fun onError(pushType: ChatPushType?, errorCode: Long) {
+ // 返回的errorCode仅9xx为环信内部错误,可从EMError中查询,其他错误请根据pushType去相应第三方推送网站查询。
+ ChatLog.e("PushManager", "onError: pushType: $pushType, errorCode: $errorCode")
+ }
+
+ override fun isSupportPush(
+ pushType: ChatPushType?,
+ pushConfig: PushConfig?
+ ): Boolean {
+ if (pushType == ChatPushType.FCM) {
+ ChatLog.d("FCM",
+ "GooglePlayServiceCode:" + GoogleApiAvailabilityLight.getInstance()
+ .isGooglePlayServicesAvailable(context)
+ )
+ return DemoHelper.getInstance().getDataModel().isUseFCM()
+ && GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
+ }
+ return super.isSupportPush(pushType, pushConfig)
+ }
+
+ })
+
+ }
+ }
+
+ /**
+ * Get the push token and send to Chat Server.
+ */
+ fun getPushTokenAndSend(context: Context) {
+ // Get FCM push token.
+ getFCMTokenAndSend(context)
+ }
+
+ /**
+ * Get FCM push token and send to Chat Server.
+ */
+ private fun getFCMTokenAndSend(context: Context) {
+ if (DemoHelper.getInstance().getDataModel().isUseFCM()
+ && GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) {
+ // Enable FCM automatic initialization
+ if (FirebaseMessaging.getInstance().isAutoInitEnabled.not()) {
+ FirebaseMessaging.getInstance().isAutoInitEnabled = true
+ FirebaseAnalytics.getInstance(context).setAnalyticsCollectionEnabled(true)
+ }
+ // Get FCM push token and send to Chat Server.
+ FirebaseMessaging.getInstance().token.addOnCompleteListener {
+ if (it.isSuccessful.not()) {
+ ChatLog.e("FCM", "get FCM push token failed: ${it.exception}")
+ return@addOnCompleteListener
+ }
+ val token = it.result
+ ChatLog.d("FCM", "get FCM push token: $token")
+ ChatClient.getInstance().sendFCMTokenToServer(token)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/SimpleDialog.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/SimpleDialog.kt
new file mode 100644
index 00000000..28733c2a
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/SimpleDialog.kt
@@ -0,0 +1,269 @@
+package io.agora.chatdemo.common.dialog
+
+import android.app.Dialog
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.annotation.ColorInt
+import io.agora.uikit.databinding.EaseLayoutCustomDialogBinding
+
+class SimpleDialog(
+ context: Context,
+ private var title: String? = "",
+ private var subtitle: String? = "",
+ private var isEditTextMode: Boolean = false,
+ private var inputHint: String? = "",
+ private var onNegativeButtonClickListener: (() -> Unit)? = {},
+ private var onPositiveButtonClickListener: (() -> Unit)? = {},
+ private var onInputTextChangeListener: ((String) -> Unit)? = {},
+ private var onInputModeConfirmListener: ((String) -> Unit)? = {},
+) : Dialog(context) {
+
+ private var mPositiveButtonText: String? = null
+ private var mNegativeButtonText: String? = null
+ private var showCancelButton: Boolean = true
+ private var showConfirmButton: Boolean = true
+ private val binding by lazy { EaseLayoutCustomDialogBinding.inflate(LayoutInflater.from(context)) }
+
+ init {
+ setContentView(binding.root)
+ setCancelable(true)
+ setCanceledOnTouchOutside(true)
+ window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ window?.setGravity(Gravity.CENTER)
+ window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
+
+ setData()
+
+ binding.leftButton.setOnClickListener {
+ onNegativeButtonClickListener?.invoke()
+ dismiss()
+ }
+
+ binding.rightButton.setOnClickListener {
+ if (isEditTextMode){
+ if (binding.editText.text.isNotEmpty()){
+ onInputModeConfirmListener?.invoke(binding.editText.text.toString())
+ }
+ }else{
+ onPositiveButtonClickListener?.invoke()
+ }
+ dismiss()
+ }
+ }
+
+ fun setData() {
+ if (mPositiveButtonText.isNullOrEmpty().not()) {
+ binding.rightButton.text = mPositiveButtonText
+ }
+ if (mNegativeButtonText.isNullOrEmpty().not()) {
+ binding.leftButton.text = mNegativeButtonText
+ }
+ if (title.isNullOrEmpty()) {
+ binding.titleTextView.visibility = View.GONE
+ } else {
+ binding.titleTextView.text = title
+ binding.titleTextView.visibility = View.VISIBLE
+ }
+
+ if (subtitle.isNullOrEmpty()) {
+ binding.subtitleTextView.visibility = View.GONE
+ } else {
+ binding.subtitleTextView.text = subtitle
+ binding.subtitleTextView.visibility = View.VISIBLE
+ }
+ if (isEditTextMode) {
+ binding.editText.requestFocus()
+ binding.inputClear.setOnClickListener{
+ binding.editText.setText("")
+ }
+ binding.editText.visibility = View.VISIBLE
+ binding.editText.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(
+ s: CharSequence?,
+ start: Int,
+ count: Int,
+ after: Int
+ ) {
+ }
+
+ override fun onTextChanged(
+ s: CharSequence,
+ start: Int,
+ before: Int,
+ count: Int
+ ) {
+ if (s.isEmpty()){
+ binding.inputClear.visibility = View.GONE
+ binding.rightButton.isSelected = false
+ }else{
+ binding.inputClear.visibility = View.VISIBLE
+ binding.rightButton.isSelected = true
+ }
+ onInputTextChangeListener?.invoke(s.toString())
+ }
+
+ override fun afterTextChanged(s: Editable?) {
+ }
+ })
+ } else {
+ binding.editText.visibility = View.GONE
+ binding.rightButton.isSelected = true
+ binding.rightButton.visibility = View.VISIBLE
+ }
+ binding.leftButton.visibility = if (showCancelButton) View.VISIBLE else View.GONE
+ binding.rightButton.visibility = if (showConfirmButton) View.VISIBLE else View.GONE
+ }
+
+ class Builder(context: Context) {
+ private val dialog = SimpleDialog(context)
+
+ /**
+ * Set the dialog title.
+ */
+ fun setTitle(title: String?): Builder {
+ dialog.title = title
+ return this
+ }
+
+ /**
+ * Set the dialog subtitle.
+ */
+ fun setSubtitle(subtitle: String?): Builder {
+ dialog.subtitle = subtitle
+ return this
+ }
+
+ /**
+ * Set the dialog mode.
+ */
+ fun setEditTextMode(isEditTextMode: Boolean): Builder {
+ dialog.isEditTextMode = isEditTextMode
+ return this
+ }
+
+ /**
+ * Set the dialog input hint when the dialog model is edit.
+ */
+ fun setInputHint(inputHint: String?): Builder {
+ dialog.inputHint = inputHint
+ return this
+ }
+
+ /**
+ * Dismiss the dialog positive button.
+ */
+ fun dismissPositiveButton(): Builder {
+ dialog.showConfirmButton = false
+ return this
+ }
+
+ /**
+ * Dismiss the dialog negative button.
+ */
+ fun dismissNegativeButton(): Builder {
+ dialog.showCancelButton = false
+ return this
+ }
+
+ /**
+ * Set the dialog positive button.
+ */
+ fun setPositiveButton(text: String? = "", onClickListener: () -> Unit): Builder {
+ dialog.mPositiveButtonText = text
+ dialog.onPositiveButtonClickListener = onClickListener
+ return this
+ }
+
+ /**
+ * Set the dialog negative button.
+ */
+ fun setNegativeButton(text: String? = "", onClickListener: () -> Unit): Builder {
+ dialog.mNegativeButtonText = text
+ dialog.onNegativeButtonClickListener = onClickListener
+ return this
+ }
+
+ /**
+ * Set the dialog dismiss listener.
+ */
+ fun setOnDismissListener(onDismissListener: () -> Unit): Builder {
+ dialog.setOnDismissListener {
+ onDismissListener.invoke()
+ }
+ return this
+ }
+
+ /**
+ * Set the dialog cancelable.
+ */
+ fun setCancelable(cancelable: Boolean): Builder {
+ dialog.setCancelable(cancelable)
+ return this
+ }
+
+ /**
+ * Set the dialog canceled on touch outside.
+ */
+ fun setCanceledOnTouchOutside(canceledOnTouchOutside: Boolean): Builder {
+ dialog.setCanceledOnTouchOutside(canceledOnTouchOutside)
+ return this
+ }
+
+ /**
+ * Set the dialog positive button text color.
+ */
+ fun setPositiveButtonTextColor(@ColorInt color: Int): Builder {
+ dialog.binding.rightButton.setTextColor(color)
+ return this
+ }
+
+ /**
+ * Set the dialog negative button text color.
+ */
+ fun setNegativeButtonTextColor(@ColorInt color: Int): Builder {
+ dialog.binding.leftButton.setTextColor(color)
+ return this
+ }
+
+ /**
+ * Set the dialog input text change listener when the dialog is in edit model.
+ */
+ fun setOnInputTextChangeListener(onInputTextChangeListener: (String) -> Unit): Builder {
+ dialog.onInputTextChangeListener = onInputTextChangeListener
+ return this
+ }
+
+ /**
+ * Set the dialog input mode confirm listener when the dialog is in edit model.
+ */
+ fun setOnInputModeConfirmListener(onInputModeConfirmListener: (String) -> Unit): Builder {
+ dialog.onInputModeConfirmListener = onInputModeConfirmListener
+ return this
+ }
+
+ /**
+ * Create the dialog.
+ */
+ fun build(): SimpleDialog {
+ dialog.setData()
+ return dialog
+ }
+
+ /**
+ * Show the dialog.
+ */
+ fun show() {
+ build()
+ dialog.show()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/fragment/DemoAgreementDialogFragment.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/fragment/DemoAgreementDialogFragment.kt
new file mode 100644
index 00000000..d9ae5db1
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/fragment/DemoAgreementDialogFragment.kt
@@ -0,0 +1,96 @@
+package io.agora.chatdemo.common.dialog.fragment
+
+import android.content.Intent
+import android.graphics.Color
+import android.net.Uri
+import android.os.Bundle
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.TextPaint
+import android.text.method.LinkMovementMethod
+import android.text.style.ClickableSpan
+import android.text.style.ForegroundColorSpan
+import android.view.View
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import io.agora.chatdemo.R
+
+class DemoAgreementDialogFragment : DemoDialogFragment() {
+ override val middleLayoutId: Int
+ get() = R.layout.demo_fragment_middle_agreement
+
+ override fun initView(savedInstanceState: Bundle?) {
+ super.initView(savedInstanceState)
+ val tv_privacy = findViewById(R.id.tv_privacy)
+ tv_privacy?.text = spannable
+ tv_privacy?.movementMethod = LinkMovementMethod.getInstance()
+ mBtnDialogConfirm?.setTextColor(ContextCompat.getColor(requireContext(), R.color.color_primary))
+ }
+
+ override fun initData() {
+ super.initData()
+ if (dialog != null) {
+ dialog!!.setCancelable(false)
+ dialog!!.setCanceledOnTouchOutside(false)
+ }
+ }
+
+ private val spannable: SpannableString
+ get() {
+ val spanStr = SpannableString(getString(R.string.demo_login_dialog_content_privacy))
+ val start1 = 18
+ val end1 = 25
+ val start2 = 30
+ val end2 = 44
+ spanStr.setSpan(object : MyClickableSpan() {
+ override fun onClick(widget: View) {
+ jumpToAgreement()
+ }
+ }, start1, end1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ spanStr.setSpan(
+ ForegroundColorSpan(ContextCompat.getColor(requireContext(), R.color.color_primary)),
+ start1,
+ end1,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+
+ //spanStr.setSpan(new UnderlineSpan(), 10, 14, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spanStr.setSpan(object : MyClickableSpan() {
+ override fun onClick(widget: View) {
+ jumpToProtocol()
+ }
+ }, start2, end2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ spanStr.setSpan(
+ ForegroundColorSpan(ContextCompat.getColor(requireContext(), R.color.color_primary)),
+ start2,
+ end2,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ return spanStr
+ }
+
+ private fun jumpToAgreement() {
+ val uri = Uri.parse("http://www.easemob.com/agreement")
+ val it = Intent(Intent.ACTION_VIEW, uri)
+ startActivity(it)
+ }
+
+ private fun jumpToProtocol() {
+ val uri = Uri.parse("http://www.easemob.com/protocol")
+ val it = Intent(Intent.ACTION_VIEW, uri)
+ startActivity(it)
+ }
+
+ private abstract inner class MyClickableSpan : ClickableSpan() {
+ override fun updateDrawState(ds: TextPaint) {
+ super.updateDrawState(ds)
+ ds.bgColor = Color.TRANSPARENT
+ }
+ }
+
+ open class Builder(context: AppCompatActivity) : DemoDialogFragment.Builder(context) {
+ override val fragment: DemoDialogFragment
+ protected get() = DemoAgreementDialogFragment()
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/fragment/DemoDialogFragment.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/fragment/DemoDialogFragment.kt
new file mode 100644
index 00000000..65ca79cd
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/dialog/fragment/DemoDialogFragment.kt
@@ -0,0 +1,382 @@
+package io.agora.chatdemo.common.dialog.fragment
+
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.TextUtils
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorRes
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AppCompatActivity
+import androidx.constraintlayout.widget.Group
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentTransaction
+import io.agora.chatdemo.R
+import io.agora.chatdemo.base.BaseDialogFragment
+import io.agora.uikit.common.extensions.dpToPx
+
+open class DemoDialogFragment : BaseDialogFragment(), View.OnClickListener {
+ var mTvDialogTitle: TextView? = null
+ var mBtnDialogCancel: Button? = null
+ var mBtnDialogConfirm: Button? = null
+ var mOnConfirmClickListener: OnConfirmClickListener? = null
+ var mOnCancelClickListener: OnCancelClickListener? = null
+ var dismissListener: DialogInterface.OnDismissListener? = null
+ var mGroupMiddle: Group? = null
+ var title: String? = null
+ var content: String? = null
+ override val layoutId: Int
+ get() = R.layout.demo_fragment_dialog_base
+
+ override fun setChildView(view: View?) {
+ super.setChildView(view)
+ val layoutId = middleLayoutId
+ if (layoutId > 0) {
+ view?.findViewById(R.id.rl_dialog_middle)?.let {
+ LayoutInflater.from(mContext).inflate(layoutId, it)
+ view.findViewById(R.id.group_middle)?.visibility = View.VISIBLE
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ //宽度填满,高度自适应
+ try {
+ dialog?.window?.let {
+ val lp: WindowManager.LayoutParams = it.attributes
+ lp.width = ViewGroup.LayoutParams.MATCH_PARENT
+ lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
+ it.attributes = lp
+ }
+ requireView().let {
+ val params = it.layoutParams
+ if (params is FrameLayout.LayoutParams) {
+ val margin = 30.dpToPx(it.context)
+ params.setMargins(margin, 0, margin, 0)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ fun showAllowingStateLoss(transaction: FragmentTransaction, tag: String?): Int {
+ try {
+ val dismissed = DemoDialogFragment::class.java.getDeclaredField("mDismissed")
+ dismissed.isAccessible = true
+ dismissed[this] = false
+ } catch (e: NoSuchFieldException) {
+ e.printStackTrace()
+ } catch (e: IllegalAccessException) {
+ e.printStackTrace()
+ }
+ try {
+ val shown = DemoDialogFragment::class.java.getDeclaredField("mShownByMe")
+ shown.isAccessible = true
+ shown[this] = true
+ } catch (e: NoSuchFieldException) {
+ e.printStackTrace()
+ } catch (e: IllegalAccessException) {
+ e.printStackTrace()
+ }
+ transaction.add(this, tag)
+ try {
+ val viewDestroyed = DemoDialogFragment::class.java.getDeclaredField("mViewDestroyed")
+ viewDestroyed.isAccessible = true
+ viewDestroyed[this] = false
+ } catch (e: NoSuchFieldException) {
+ e.printStackTrace()
+ } catch (e: IllegalAccessException) {
+ e.printStackTrace()
+ }
+ val mBackStackId = transaction.commitAllowingStateLoss()
+ try {
+ val backStackId = DemoDialogFragment::class.java.getDeclaredField("mBackStackId")
+ backStackId.isAccessible = true
+ backStackId[this] = mBackStackId
+ } catch (e: NoSuchFieldException) {
+ e.printStackTrace()
+ } catch (e: IllegalAccessException) {
+ e.printStackTrace()
+ }
+ return mBackStackId
+ }
+
+ open val middleLayoutId: Int
+ /**
+ * 获取中间布局的id
+ * @return
+ */
+ get() = 0
+
+ override fun initView(savedInstanceState: Bundle?) {
+ mTvDialogTitle = findViewById(R.id.tv_dialog_title)
+ mBtnDialogCancel = findViewById(R.id.btn_dialog_cancel)
+ mBtnDialogConfirm = findViewById(R.id.btn_dialog_confirm)
+ mGroupMiddle = findViewById(R.id.group_middle)
+ arguments?.let { bundle ->
+ title = bundle.getString(ParameterName.titleString)
+ if (!TextUtils.isEmpty(title)) {
+ mTvDialogTitle?.text = title
+ }
+ content = bundle.getString(ParameterName.contentString)
+ val titleColor = bundle.getInt(ParameterName.titleColorInt, 0)
+ if (titleColor != 0) {
+ mTvDialogTitle?.setTextColor(titleColor)
+ }
+ val titleSize = bundle.getInt(ParameterName.titleSize, 0)
+ if (titleSize != 0) {
+ mTvDialogTitle?.setTextSize(TypedValue.COMPLEX_UNIT_SP, titleSize.toFloat())
+ }
+ val confirm = bundle.getString(ParameterName.confirmString)
+ if (!TextUtils.isEmpty(confirm)) {
+ mBtnDialogConfirm?.text = confirm
+ }
+ val confirmColor = bundle.getInt(ParameterName.confirmColorInt, 0)
+ if (confirmColor != 0) {
+ mBtnDialogConfirm?.setTextColor(confirmColor)
+ }
+ val cancel = bundle.getString(ParameterName.cancelString)
+ if (!TextUtils.isEmpty(cancel)) {
+ mBtnDialogCancel?.text = cancel
+ }
+ val showCancel = bundle.getBoolean(ParameterName.showCancel, false)
+ if (showCancel) {
+ mGroupMiddle?.visibility = View.VISIBLE
+ }
+ val canceledOnTouchOutside =
+ bundle.getBoolean(ParameterName.canceledOnTouchOutside, false)
+ dialog?.setCanceledOnTouchOutside(canceledOnTouchOutside)
+ }
+ }
+
+ override fun initListener() {
+ mBtnDialogCancel?.setOnClickListener(this)
+ mBtnDialogConfirm?.setOnClickListener(this)
+ }
+
+ override fun initData() {}
+ override fun onClick(v: View) {
+ when (v.id) {
+ R.id.btn_dialog_cancel -> onCancelClick(v)
+ R.id.btn_dialog_confirm -> onConfirmClick(v)
+ }
+ }
+
+ override fun dismiss() {
+ super.dismiss()
+ dismissListener?.onDismiss(dialog)
+ }
+
+ /**
+ * 设置确定按钮的点击事件
+ * @param listener
+ */
+ fun setOnConfirmClickListener(listener: OnConfirmClickListener?) {
+ mOnConfirmClickListener = listener
+ }
+
+ /**
+ * 设置取消事件
+ * @param cancelClickListener
+ */
+ fun setOnCancelClickListener(cancelClickListener: OnCancelClickListener?) {
+ mOnCancelClickListener = cancelClickListener
+ }
+
+ private fun setOnDismissListener(dismissListener: DialogInterface.OnDismissListener?) {
+ this.dismissListener = dismissListener
+ }
+
+ /**
+ * 点击了取消按钮
+ * @param v
+ */
+ fun onCancelClick(v: View?) {
+ dismiss()
+ mOnCancelClickListener?.onCancelClick(v)
+ }
+
+ /**
+ * 点击了确认按钮
+ * @param v
+ */
+ fun onConfirmClick(v: View?) {
+ dismiss()
+ mOnConfirmClickListener?.onConfirmClick(v)
+ }
+
+ /**
+ * 确定事件的点击事件
+ */
+ interface OnConfirmClickListener {
+ fun onConfirmClick(view: View?)
+ }
+
+ /**
+ * 点击取消
+ */
+ interface OnCancelClickListener {
+ fun onCancelClick(view: View?)
+ }
+
+ open class Builder(private val context: AppCompatActivity) {
+ private var listener: OnConfirmClickListener? = null
+ private var cancelClickListener: OnCancelClickListener? = null
+ private var dismissListener: DialogInterface.OnDismissListener? = null
+ private val currentFragment: DemoDialogFragment? = null
+ protected val bundle: Bundle = Bundle()
+
+ fun setTitle(@StringRes title: Int): Builder {
+ bundle.putString(ParameterName.titleString, context.getString(title))
+ return this
+ }
+
+ fun setTitle(title: String?): Builder {
+ bundle.putString(ParameterName.titleString, title)
+ return this
+ }
+
+ fun setTitleColor(@ColorRes color: Int): Builder {
+ bundle.putInt(ParameterName.titleColorInt, ContextCompat.getColor(context, color))
+ return this
+ }
+
+ fun setTitleColorInt(@ColorInt color: Int): Builder {
+ bundle.putInt(ParameterName.titleColorInt, color)
+ return this
+ }
+
+ fun setTitleSize(size: Float): Builder {
+ bundle.putFloat(ParameterName.titleSize, size)
+ return this
+ }
+
+ fun setContent(@StringRes content: Int): Builder {
+ bundle.putString(ParameterName.contentString, context.getString(content))
+ return this
+ }
+
+ fun setContent(content: String?): Builder {
+ bundle.putString(ParameterName.contentString, content)
+ return this
+ }
+
+ fun showCancelButton(showCancel: Boolean): Builder {
+ bundle.putBoolean(ParameterName.showCancel, showCancel)
+ return this
+ }
+
+ fun setCanceledOnTouchOutside(cancel: Boolean): Builder {
+ bundle.putBoolean(ParameterName.canceledOnTouchOutside, cancel)
+ return this
+ }
+
+ fun setOnConfirmClickListener(
+ @StringRes confirm: Int,
+ listener: OnConfirmClickListener?
+ ): Builder {
+ bundle.putString(ParameterName.confirmString, context.getString(confirm))
+ this.listener = listener
+ return this
+ }
+
+ fun setOnConfirmClickListener(
+ confirm: String?,
+ listener: OnConfirmClickListener?
+ ): Builder {
+ bundle.putString(ParameterName.confirmString, confirm)
+ this.listener = listener
+ return this
+ }
+
+ fun setOnConfirmClickListener(listener: OnConfirmClickListener?): Builder {
+ this.listener = listener
+ return this
+ }
+
+ fun setConfirmColor(@ColorRes color: Int): Builder {
+ bundle.putInt(ParameterName.confirmColorInt, ContextCompat.getColor(context, color))
+ return this
+ }
+
+ fun setConfirmColorInt(@ColorInt color: Int): Builder {
+ bundle.putInt(ParameterName.confirmColorInt, color)
+ return this
+ }
+
+ fun setOnCancelClickListener(
+ @StringRes cancel: Int,
+ listener: OnCancelClickListener?
+ ): Builder {
+ bundle.putString(ParameterName.cancelString, context.getString(cancel))
+ cancelClickListener = listener
+ return this
+ }
+
+ fun setOnCancelClickListener(cancel: String?, listener: OnCancelClickListener?): Builder {
+ bundle.putString(ParameterName.cancelString, cancel)
+ cancelClickListener = listener
+ return this
+ }
+
+ fun setOnCancelClickListener(listener: OnCancelClickListener?): Builder {
+ cancelClickListener = listener
+ return this
+ }
+
+ fun setOnDismissListener(listener: DialogInterface.OnDismissListener?): Builder {
+ dismissListener = listener
+ return this
+ }
+
+ fun setArgument(bundle: Bundle?): Builder {
+ if (bundle != null) {
+ this.bundle.putAll(bundle)
+ }
+ return this
+ }
+
+ fun build(): DemoDialogFragment {
+ val fragment = fragment
+ fragment.setOnConfirmClickListener(listener)
+ fragment.setOnCancelClickListener(cancelClickListener)
+ fragment.setOnDismissListener(dismissListener)
+ fragment.setArguments(bundle)
+ return fragment
+ }
+
+ protected open val fragment: DemoDialogFragment
+ protected get() = DemoDialogFragment()
+
+ fun show(): DemoDialogFragment {
+ val fragment = build()
+ val transaction: FragmentTransaction =
+ context.supportFragmentManager.beginTransaction()
+ transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+ fragment.showAllowingStateLoss(transaction, null)
+ return fragment
+ }
+ }
+
+ private object ParameterName {
+ const val titleString = "titleString"
+ const val titleColorInt = "titleColorInt"
+ const val titleSize = "titleSize"
+ const val contentString = "contentString"
+ const val showCancel = "showCancel"
+ const val canceledOnTouchOutside = "canceledOnTouchOutside"
+ const val confirmString = "confirmString"
+ const val confirmColorInt = "confirmColorInt"
+ const val cancelString = "cancelString"
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/String.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/String.kt
new file mode 100644
index 00000000..944191ba
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/String.kt
@@ -0,0 +1,32 @@
+package io.agora.chatdemo.common.extensions
+
+import java.io.UnsupportedEncodingException
+import java.security.MessageDigest
+import java.security.NoSuchAlgorithmException
+
+/**
+ * MD5 encryption.
+ */
+fun String.MD5(): String {
+ if (this.isEmpty()) {
+ return ""
+ }
+ var hexStr = ""
+ try {
+ val hash = MessageDigest.getInstance("MD5").digest(toByteArray(charset("utf-8")))
+ val hex = StringBuilder(hash.size * 2)
+ for (b in hash) {
+ if (b.toInt() and 0xFF < 0x10) {
+ hex.append("0")
+ }
+ hex.append(Integer.toHexString(b.toInt() and 0xFF))
+ }
+ hexStr = hex.toString()
+ } catch (e: NoSuchAlgorithmException) {
+ e.printStackTrace()
+ } catch (e: UnsupportedEncodingException) {
+ e.printStackTrace()
+ }
+
+ return hexStr
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/Activity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/Activity.kt
new file mode 100644
index 00000000..c0d3460d
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/Activity.kt
@@ -0,0 +1,10 @@
+package io.agora.chatdemo.common.extensions.internal
+
+import android.app.Activity
+import android.app.ActivityManager
+import android.content.Context
+
+internal fun Activity.makeTaskToFront() {
+ (getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager)
+ ?.moveTaskToFront(taskId, ActivityManager.MOVE_TASK_WITH_HOME)
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatGroup.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatGroup.kt
new file mode 100644
index 00000000..fdb4c8c5
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatGroup.kt
@@ -0,0 +1,8 @@
+package io.agora.chatdemo.common.extensions.internal
+
+import io.agora.uikit.common.ChatGroup
+import io.agora.uikit.model.EaseGroupProfile
+
+internal fun ChatGroup.parse(): EaseGroupProfile {
+ return EaseGroupProfile(groupId, groupName, extension)
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatOptions.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatOptions.kt
new file mode 100644
index 00000000..dfebce57
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatOptions.kt
@@ -0,0 +1,27 @@
+package io.agora.chatdemo.common.extensions.internal
+
+import android.content.Context
+import android.content.pm.PackageManager
+import io.agora.uikit.common.ChatOptions
+
+/**
+ * Check if set the app key.
+ */
+internal fun ChatOptions.checkAppKey(context: Context): Boolean {
+ if (appKey.isNullOrEmpty().not()) {
+ return true
+ }
+ val appPackageName = context.packageName
+ try {
+ context.packageManager.getApplicationInfo(appPackageName, PackageManager.GET_META_DATA).let { info ->
+ info.metaData?.getString("EASEMOB_APPKEY")?.let { key ->
+ if (key.isEmpty().not()) {
+ return true
+ }
+ }
+ }
+ } catch (e: PackageManager.NameNotFoundException) {
+ e.printStackTrace()
+ }
+ return false
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatUserInfo.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatUserInfo.kt
new file mode 100644
index 00000000..75247777
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/ChatUserInfo.kt
@@ -0,0 +1,12 @@
+package io.agora.chatdemo.common.extensions.internal
+
+import io.agora.uikit.common.ChatUserInfo
+import io.agora.uikit.model.EaseProfile
+
+internal fun ChatUserInfo.toProfile(): EaseProfile {
+ return EaseProfile(
+ id = userId,
+ name = nickname,
+ avatar = avatarUrl
+ )
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/EditText.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/EditText.kt
new file mode 100644
index 00000000..584e7dc2
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/EditText.kt
@@ -0,0 +1,101 @@
+package io.agora.chatdemo.common.extensions.internal
+
+import android.annotation.SuppressLint
+import android.graphics.drawable.Drawable
+import android.text.Editable
+import android.text.InputType
+import android.text.TextWatcher
+import android.view.MotionEvent
+import android.view.View
+import android.widget.EditText
+
+@SuppressLint("ClickableViewAccessibility")
+internal fun EditText.changePwdDrawable(
+ eyeOpen: Drawable?,
+ eyeClose: Drawable?,
+ left: Drawable?,
+ top: Drawable?,
+ bottom: Drawable?
+) {
+ //Can the identification password be seen
+ val canBeSeen = booleanArrayOf(false)
+ setOnTouchListener { v: View?, event: MotionEvent ->
+ val drawable = compoundDrawables[2] ?: return@setOnTouchListener false
+ //If there is no image on the right, it will not be processed anymore
+ //If it is not a press event, no further processing
+ if (event.action != MotionEvent.ACTION_UP) return@setOnTouchListener false
+ if (event.x > (width
+ - paddingRight
+ - drawable.intrinsicWidth)
+ ) {
+ if (canBeSeen[0]) {
+ inputType =
+ InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
+ setCompoundDrawablesWithIntrinsicBounds(left, top, eyeClose, bottom)
+ canBeSeen[0] = false
+ } else {
+ inputType =
+ InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_NORMAL
+ setCompoundDrawablesWithIntrinsicBounds(left, top, eyeOpen, bottom)
+ canBeSeen[0] = true
+ }
+ setSelection(text.toString().length)
+ isFocusable = true
+ isFocusableInTouchMode = true
+ requestFocus()
+ return@setOnTouchListener true
+ }
+ false
+ }
+}
+
+/**
+ * Show EditText's right drawable when text is not null.
+ */
+internal fun EditText.showRightDrawable(right: Drawable?) {
+ val content = text.toString().trim { it <= ' ' }
+ setCompoundDrawablesWithIntrinsicBounds(
+ null,
+ null,
+ if (content.isEmpty()) null else right,
+ null
+ )
+}
+
+@SuppressLint("ClickableViewAccessibility")
+internal fun EditText.clearEditTextListener() {
+ setOnTouchListener { v: View?, event: MotionEvent ->
+ val drawable = compoundDrawables[2] ?: return@setOnTouchListener false
+ //如果右边没有图片,不再处理
+ //如果不是按下事件,不再处理
+ if (event.action != MotionEvent.ACTION_UP) return@setOnTouchListener false
+ if (event.x > (width
+ - paddingRight
+ - drawable.intrinsicWidth)
+ ) {
+ setText("")
+ return@setOnTouchListener true
+ }
+ false
+ }
+}
+
+/**
+ * Implement a default text changed listener.
+ */
+internal fun EditText.addDefaultTextChangedListener(listener: (Editable?) -> Unit) {
+ addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
+ // do nothing
+ }
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ // do nothing
+ }
+
+ override fun afterTextChanged(s: Editable?) {
+ listener(s)
+ }
+
+ })
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/SwitchView.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/SwitchView.kt
new file mode 100644
index 00000000..e4831392
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/extensions/internal/SwitchView.kt
@@ -0,0 +1,9 @@
+package io.agora.chatdemo.common.extensions.internal
+
+import io.agora.uikit.widget.EaseSwitchItemView
+
+
+internal fun EaseSwitchItemView.setSwitchDefaultStyle(){
+ setSwitchTarckDrawable(io.agora.uikit.R.drawable.ease_switch_track_selector)
+ setSwitchThumbDrawable(io.agora.uikit.R.drawable.ease_switch_thumb_selector)
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/DeveloperModeHelper.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/DeveloperModeHelper.kt
new file mode 100644
index 00000000..460fa3e8
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/DeveloperModeHelper.kt
@@ -0,0 +1,22 @@
+package io.agora.chatdemo.common.helper
+
+import io.agora.chatdemo.DemoHelper
+
+object DeveloperModeHelper {
+
+ fun isDeveloperMode():Boolean{
+ return DemoHelper.getInstance().getDataModel().isDeveloperMode()
+ }
+
+ fun setDeveloperMode(developerMode:Boolean){
+ DemoHelper.getInstance().getDataModel().setDeveloperMode(developerMode)
+ }
+
+ fun setEnableCustom(enable:Boolean){
+ DemoHelper.getInstance().getDataModel().enableCustomSet(enable)
+ }
+
+ fun isCustomSetEnable():Boolean{
+ return DemoHelper.getInstance().getDataModel().isCustomSetEnable()
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/LocalNotifyHelper.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/LocalNotifyHelper.kt
new file mode 100644
index 00000000..c200c790
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/LocalNotifyHelper.kt
@@ -0,0 +1,50 @@
+package io.agora.chatdemo.common.helper
+
+import io.agora.chat.Conversation
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.R
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatMessage
+import io.agora.uikit.common.ChatMessageStatus
+import io.agora.uikit.common.ChatMessageType
+import io.agora.uikit.common.ChatTextMessageBody
+import io.agora.uikit.common.ChatType
+import io.agora.uikit.provider.getSyncUser
+
+object LocalNotifyHelper {
+ /**
+ * Create a local message when receive a unsent message.
+ */
+ fun createContactNotifyMessage(userId:String?): ChatMessage {
+ val user = EaseIM.getUserProvider()?.getSyncUser(userId)
+ val msgNotification = ChatMessage.createReceiveMessage(ChatMessageType.TXT)
+ val text: String = DemoHelper.getInstance().context.resources?.
+ getString(R.string.demo_contact_added_notify,user?.getNotEmptyName())?:"$userId"
+ val txtBody = ChatTextMessageBody(text)
+ msgNotification.addBody(txtBody)
+ msgNotification.to = EaseIM.getCurrentUser()?.id
+ msgNotification.from = user?.id
+ msgNotification.msgTime = System.currentTimeMillis()
+ msgNotification.chatType = ChatType.Chat
+ msgNotification.setLocalTime(System.currentTimeMillis())
+ msgNotification.setAttribute(io.agora.uikit.common.EaseConstant.MESSAGE_TYPE_CONTACT_NOTIFY, true)
+ msgNotification.setStatus(ChatMessageStatus.SUCCESS)
+ msgNotification.setIsChatThreadMessage(false)
+ return msgNotification
+ }
+
+ /**
+ * Remove a local message when receive contact notify message.
+ */
+ fun removeContactNotifyMessage(userId:String?){
+ val conversation = ChatClient.getInstance().chatManager().getConversation(userId,Conversation.ConversationType.Chat)
+ conversation?.let {
+ it.allMessages.map { msg->
+ if (msg.ext().containsKey(io.agora.uikit.common.EaseConstant.MESSAGE_TYPE_CONTACT_NOTIFY)){
+ it.removeMessage(msg.msgId)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/MenuFilterHelper.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/MenuFilterHelper.kt
new file mode 100644
index 00000000..e7b1743d
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/helper/MenuFilterHelper.kt
@@ -0,0 +1,27 @@
+package io.agora.chatdemo.common.helper
+
+import android.text.TextUtils
+import io.agora.chat.callkit.utils.EaseCallMsgUtils
+import io.agora.uikit.common.ChatMessage
+import io.agora.uikit.common.ChatMessageType
+import io.agora.uikit.menu.chat.EaseChatMenuHelper
+
+object MenuFilterHelper {
+ fun filterMenu(helper: EaseChatMenuHelper?, message: ChatMessage?){
+ message?.let {
+ when(it.type){
+ ChatMessageType.TXT ->{
+ if (it.ext().containsKey(EaseCallMsgUtils.CALL_MSG_TYPE)){
+ val msgType = it.getStringAttribute(EaseCallMsgUtils.CALL_MSG_TYPE,"")
+ if (TextUtils.equals(msgType, EaseCallMsgUtils.CALL_MSG_INFO)) {
+ helper?.setAllItemsVisible(false)
+ helper?.clearTopView()
+ helper?.findItemVisible(io.agora.uikit.R.id.action_chat_delete,true)
+ }
+ }
+ }
+ else -> {}
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/AppDatabase.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/AppDatabase.kt
new file mode 100644
index 00000000..b9fd641f
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/AppDatabase.kt
@@ -0,0 +1,42 @@
+package io.agora.chatdemo.common.room
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import io.agora.chatdemo.BuildConfig
+import io.agora.chatdemo.common.extensions.MD5
+import io.agora.chatdemo.common.room.dao.DemoUserDao
+import io.agora.chatdemo.common.room.entity.DemoUser
+
+@Database(entities = [DemoUser::class], version = 3)
+abstract class AppDatabase: RoomDatabase() {
+
+ /**
+ * Get the user data access object.
+ */
+ abstract fun userDao(): DemoUserDao
+
+ companion object {
+ @Volatile
+ private var INSTANCE: AppDatabase? = null
+
+ // 以下数据库升级设置,为升级数据库将清掉之前的数据,如果要保留数据,慎重采用此种方式
+ // 可以采用addMigrations()的方式,进行数据库的升级
+ fun getDatabase(context: Context, userId: String): AppDatabase {
+ return INSTANCE ?: synchronized(this) {
+ val dbName = (BuildConfig.AGORA_CHAT_APPKEY + userId).MD5()
+ val instance = Room.databaseBuilder(
+ context.applicationContext,
+ AppDatabase::class.java,
+ dbName
+ )
+ .allowMainThreadQueries()
+ .fallbackToDestructiveMigration()
+ .build()
+ INSTANCE = instance
+ instance
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/dao/DemoUserDao.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/dao/DemoUserDao.kt
new file mode 100644
index 00000000..79943ab9
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/dao/DemoUserDao.kt
@@ -0,0 +1,96 @@
+package io.agora.chatdemo.common.room.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Update
+import io.agora.chatdemo.common.room.entity.DemoUser
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface DemoUserDao {
+
+ @Query("SELECT * FROM DemoUser")
+ fun getAll(): List
+
+ @Query("SELECT * FROM DemoUser WHERE userId = :userId")
+ fun getUserById(userId: String): Flow
+
+ @Query("SELECT * FROM DemoUser WHERE userId = :userId")
+ fun getUser(userId: String): DemoUser?
+
+ @Query("SELECT * FROM DemoUser WHERE userId IN (:userIds)")
+ fun getUsersByIds(userIds: List): Flow>
+
+ @Query("SELECT * FROM DemoUser WHERE name LIKE :name")
+ fun getUsersByName(name: String?): Flow>
+
+ // Insert by DemoUser
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertUser(user: DemoUser)
+
+ // Insert DemoUser list
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertUsers(users: List)
+
+ // Update
+ @Query("UPDATE DemoUser SET name = :name, avatar = :avatar, remark = :remark WHERE userId = :userId")
+ fun updateUser(userId: String, name: String, avatar: String, remark: String)
+
+ // Update by DemoUser
+ @Update
+ fun updateUser(user: DemoUser)
+
+ // Update DemoUser list
+ @Update
+ fun updateUsers(users: List)
+
+ // Update name
+ @Query("UPDATE DemoUser SET name = :name WHERE userId = :userId")
+ fun updateUserName(userId: String, name: String)
+
+ // Update avatar
+ @Query("UPDATE DemoUser SET avatar = :avatar WHERE userId = :userId")
+ fun updateUserAvatar(userId: String, avatar: String)
+
+ // Update remark
+ @Query("UPDATE DemoUser SET remark = :remark WHERE userId = :userId")
+ fun updateUserRemark(userId: String, remark: String)
+
+ // Update update times
+ @Query("UPDATE DemoUser SET update_times = update_times + 1 WHERE userId = :userId")
+ fun updateUserTimes(userId: String)
+
+ // Update users update times
+ @Query("UPDATE DemoUser SET update_times = update_times + 1 WHERE userId IN (:userIds)")
+ fun updateUsersTimes(userIds: List)
+
+ /**
+ * Reset the update times of all users.
+ */
+ @Query("UPDATE DemoUser SET update_times = 0")
+ fun resetUsersTimes()
+
+ // Delete
+ @Delete
+ fun deleteUser(user: DemoUser)
+
+ // Delete user list
+ @Delete
+ fun deleteUsers(users: List)
+
+ // Delete by userId
+ @Query("DELETE FROM DemoUser WHERE userId = :userId")
+ fun deleteUserById(userId: String)
+
+ // Delete by userId list
+ @Query("DELETE FROM DemoUser WHERE userId IN (:userIds)")
+ fun deleteUsersByIds(userIds: List)
+
+
+ // Delete all users
+ @Query("DELETE FROM DemoUser")
+ fun deleteAll()
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/entity/DemoUser.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/entity/DemoUser.kt
new file mode 100644
index 00000000..09e1bb38
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/entity/DemoUser.kt
@@ -0,0 +1,21 @@
+package io.agora.chatdemo.common.room.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import io.agora.uikit.model.EaseProfile
+
+@Entity
+data class DemoUser(
+ @PrimaryKey val userId: String,
+ val name: String?,
+ val avatar: String?,
+ val remark: String? = null,
+ @ColumnInfo(name = "update_times")
+ var updateTimes: Int = 0
+)
+
+/**
+ * Convert the user data to the profile data.
+ */
+internal fun DemoUser.parse() = EaseProfile(userId, name, avatar, remark)
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/extensions/DbEntity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/extensions/DbEntity.kt
new file mode 100644
index 00000000..b584cd86
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/extensions/DbEntity.kt
@@ -0,0 +1,15 @@
+package io.agora.chatdemo.common.room.extensions
+
+import io.agora.chatdemo.common.room.entity.DemoUser
+import io.agora.uikit.common.ChatUserInfo
+import io.agora.uikit.model.EaseProfile
+
+internal fun EaseProfile.parseToDbBean() = DemoUser(id, name, avatar, remark)
+
+internal fun ChatUserInfo.parseToDbBean(): DemoUser {
+ return DemoUser(
+ userId = userId,
+ name = nickname,
+ avatar = avatarUrl
+ )
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/viewmodel/BusUserViewModel.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/viewmodel/BusUserViewModel.kt
new file mode 100644
index 00000000..00bdda6b
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/room/viewmodel/BusUserViewModel.kt
@@ -0,0 +1,130 @@
+package io.agora.chatdemo.common.room.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import io.agora.chatdemo.common.room.dao.DemoUserDao
+import io.agora.chatdemo.common.room.entity.DemoUser
+
+class BusUserViewModel(private val userDao: DemoUserDao): ViewModel() {
+
+ /**
+ * Query all users from the database.
+ * @return All users in the database.
+ */
+ fun getAllUsers() = userDao.getAll()
+
+ /**
+ * Query user by id from the database.
+ * @param userId The id of the user.
+ * @return The user with the specified id.
+ */
+ fun getUserById(userId: String) = userDao.getUserById(userId)
+
+ /**
+ * Query users by ids from the database.
+ * @param userIds The ids of the users.
+ * @return The users with the specified ids.
+ */
+ fun getUsersByIds(userIds: List) = userDao.getUsersByIds(userIds)
+
+ /**
+ * Query users by name from the database.
+ * @param name The name of the user.
+ * @return The users with the specified name.
+ */
+ fun getUsersByName(name: String?) = userDao.getUsersByName(name)
+
+ /**
+ * Insert a user into the database.
+ * @param user The user to be inserted.
+ */
+ fun insertUser(user: DemoUser) = userDao.insertUser(user)
+
+ /**
+ * Insert a list of users into the database.
+ * @param users The list of users to be inserted.
+ */
+ fun insertUsers(users: List) = userDao.insertUsers(users)
+
+ /**
+ * Update a user in the database.
+ * @param userId The id of the user.
+ * @param name The name of the user.
+ * @param avatar The avatar of the user.
+ * @param remark The remark of the user.
+ */
+ fun updateUser(userId: String, name: String, avatar: String, remark: String) = userDao.updateUser(userId, name, avatar, remark)
+
+ /**
+ * Update a user in the database.
+ * @param user The user to be updated.
+ */
+ fun updateUser(user: DemoUser) = userDao.updateUser(user)
+
+ /**
+ * Update a list of users in the database.
+ * @param users The list of users to be updated.
+ */
+ fun updateUsers(users: List) = userDao.updateUsers(users)
+
+ /**
+ * Update the name of a user in the database.
+ * @param userId The id of the user.
+ * @param name The name of the user.
+ */
+ fun updateUserName(userId: String, name: String) = userDao.updateUserName(userId, name)
+
+ /**
+ * Update the avatar of a user in the database.
+ * @param userId The id of the user.
+ * @param avatar The avatar of the user.
+ */
+ fun updateUserAvatar(userId: String, avatar: String) = userDao.updateUserAvatar(userId, avatar)
+
+ /**
+ * Update the remark of a user in the database.
+ * @param userId The id of the user.
+ * @param remark The remark of the user.
+ */
+ fun updateUserRemark(userId: String, remark: String) = userDao.updateUserRemark(userId, remark)
+
+ /**
+ * Delete a user from the database.
+ * @param user The user to be deleted.
+ */
+ fun deleteUser(user: DemoUser) = userDao.deleteUser(user)
+
+ /**
+ * Delete a list of users from the database.
+ * @param users The list of users to be deleted.
+ */
+ fun deleteUsers(users: List) = userDao.deleteUsers(users)
+
+ /**
+ * Delete a user from the database.
+ * @param userId The id of the user to be deleted.
+ */
+ fun deleteUserById(userId: String) = userDao.deleteUserById(userId)
+
+ /**
+ * Delete a list of users from the database.
+ * @param userIds The ids of the users to be deleted.
+ */
+ fun deleteUsersByIds(userIds: List) = userDao.deleteUsersByIds(userIds)
+
+ /**
+ * Delete all users from the database.
+ */
+ fun deleteAllUsers() = userDao.deleteAll()
+}
+
+class BusUserViewModelFactory(private val userDao: DemoUserDao): ViewModelProvider.Factory {
+
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(BusUserViewModel::class.java)) {
+ @Suppress("UNCHECKED_CAST")
+ return BusUserViewModel(userDao) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class")
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/ChatUserInfoManagerSuspend.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/ChatUserInfoManagerSuspend.kt
new file mode 100644
index 00000000..f73b2c17
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/ChatUserInfoManagerSuspend.kt
@@ -0,0 +1,64 @@
+package io.agora.chatdemo.common.suspend
+
+import io.agora.uikit.common.ChatError
+import io.agora.uikit.common.ChatException
+import io.agora.uikit.common.ChatUserInfo
+import io.agora.uikit.common.ChatUserInfoManager
+import io.agora.uikit.common.ChatUserInfoType
+import io.agora.uikit.common.impl.ValueCallbackImpl
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+/**
+ * Suspend method for [ChatUserInfoManager.updateOwnInfoByAttribute]
+ * @param type The type of the attribute to be updated
+ * @param value The value of the attribute to be updated
+ */
+suspend fun ChatUserInfoManager.updateOwnAttribute(type: ChatUserInfoType, value: String): Int {
+ return suspendCoroutine { continuation ->
+ updateOwnInfoByAttribute(type, value, ValueCallbackImpl(
+ onSuccess = {
+ continuation.resume(ChatError.EM_NO_ERROR)
+ },
+ onError = { code, message -> continuation.resumeWithException(ChatException(code, message)) }
+ ))
+ }
+}
+
+/**
+ * Suspend method for [ChatUserInfoManager.fetchUserInfoByUserId]
+ * @param userIds The user id list
+ * @return The user information map
+ */
+suspend fun ChatUserInfoManager.fetchUserInfo(userIds: List): Map {
+ return suspendCoroutine { continuation ->
+ fetchUserInfoByUserId(userIds.toTypedArray(), ValueCallbackImpl(
+ onSuccess = { value ->
+ value?.let {
+ continuation.resume(it)
+ } ?: continuation.resume(emptyMap())
+ },
+ onError = { code, message -> continuation.resumeWithException(ChatException(code, message)) }
+ ))
+ }
+}
+
+/**
+ * Suspend method for [ChatUserInfoManager.fetchUserInfoByAttribute]
+ * @param userIds The user id list
+ * @param attributes The attribute list
+ * @return The user information map
+ */
+suspend fun ChatUserInfoManager.fetUserInfo(userIds: List, attributes: List): Map {
+ return suspendCoroutine { continuation ->
+ fetchUserInfoByAttribute(userIds.toTypedArray(), attributes.toTypedArray(), ValueCallbackImpl(
+ onSuccess = { value ->
+ value?.let {
+ continuation.resume(it)
+ } ?: continuation.resume(emptyMap())
+ },
+ onError = { code, message -> continuation.resumeWithException(ChatException(code, message)) }
+ ))
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/PresenceManagerSuspend.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/PresenceManagerSuspend.kt
new file mode 100644
index 00000000..f91b1f23
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/PresenceManagerSuspend.kt
@@ -0,0 +1,118 @@
+package io.agora.chatdemo.common.suspend
+
+import io.agora.chatdemo.common.PresenceCache
+import io.agora.uikit.common.ChatError
+import io.agora.uikit.common.ChatException
+import io.agora.uikit.common.ChatPresence
+import io.agora.uikit.common.ChatPresenceManager
+import io.agora.uikit.common.impl.CallbackImpl
+import io.agora.uikit.common.impl.ValueCallbackImpl
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+
+/**
+ * Suspend method for [ChatPresenceManager.publishExtPresence]
+ * @param customStatus Custom Status.
+ * @return [ChatError] The error code of the request.
+ */
+suspend fun ChatPresenceManager.publishExtPresence(customStatus:String):Int{
+ return suspendCoroutine { continuation ->
+ publishPresence(customStatus, CallbackImpl(
+ onSuccess = {
+ continuation.resume(ChatError.EM_NO_ERROR)
+ },
+ onError = {code, message ->
+ continuation.resumeWithException(ChatException(code, message))
+ })
+ )
+ }
+}
+
+
+/**
+ * Suspend method for [ChatPresenceManager.fetchUserPresenceStatus]
+ * @param userIds Subscribe userId list.
+ * @return [ChatError] The error code of the request.
+ */
+suspend fun ChatPresenceManager.fetchUserPresenceStatus(userIds:MutableList):MutableList{
+ return suspendCoroutine { continuation ->
+ val presence = mutableListOf()
+ val ids = mutableListOf()
+ val presenceInfo = PresenceCache.getPresenceInfo
+
+ if (userIds.size > 0){
+ for (userId in userIds) {
+ if (presenceInfo.containsKey(userId)){
+ val cachePresence = PresenceCache.getUserPresence(userId)
+ if (cachePresence == null){
+ ids.add(userId)
+ }else{
+ presence.add(cachePresence)
+ }
+ }else{
+ ids.add(userId)
+ }
+ }
+ }
+
+ if (ids.isEmpty()){
+ continuation.resume(presence)
+ }else{
+ fetchPresenceStatus(ids, ValueCallbackImpl>(
+ onSuccess = {
+ presence.addAll(it)
+ it.forEach { presence ->
+ PresenceCache.insertPresences(presence.publisher,presence)
+ }
+ continuation.resume(presence)
+ },
+ onError = {code, message ->
+ continuation.resumeWithException(ChatException(code, message))
+ })
+ )
+ }
+ }
+}
+
+
+/**
+ * Suspend method for [ChatPresenceManager.subscribeUsersPresence]
+ * @param userIds subscribe userId list.
+ * @param expiry The expiration time of the presence subscription.
+ * @return [ChatError] The error code of the request.
+ */
+suspend fun ChatPresenceManager.subscribeUsersPresence(
+ userIds:MutableList,expiry:Long
+):MutableList{
+ return suspendCoroutine { continuation ->
+ subscribePresences(userIds,expiry, ValueCallbackImpl>(
+ onSuccess = {
+ continuation.resume(it)
+ },
+ onError = {code, message ->
+ continuation.resumeWithException(ChatException(code, message))
+ })
+ )
+ }
+}
+
+
+/**
+ * Suspend method for [ChatPresenceManager.unSubscribeUsersPresence]
+ * @param userIds unSubscribe userId list.
+ * @return [ChatError] The error code of the request.
+ */
+suspend fun ChatPresenceManager.unSubscribeUsersPresence(userIds:MutableList):Int{
+ return suspendCoroutine { continuation ->
+ unsubscribePresences(userIds, CallbackImpl(
+ onSuccess = {
+ continuation.resume(ChatError.EM_NO_ERROR)
+ },
+ onError = {code, message ->
+ continuation.resumeWithException(ChatException(code, message))
+ })
+ )
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/PushManagerSuspend.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/PushManagerSuspend.kt
new file mode 100644
index 00000000..6e8fd872
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/common/suspend/PushManagerSuspend.kt
@@ -0,0 +1,45 @@
+package io.agora.chatdemo.common.suspend
+
+import io.agora.uikit.common.ChatException
+import io.agora.uikit.common.ChatPushManager
+import io.agora.uikit.common.ChatSilentModeParam
+import io.agora.uikit.common.ChatSilentModeResult
+import io.agora.uikit.common.impl.ValueCallbackImpl
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+/**
+ * Set the silent mode for the App.
+ * @param silentModeParam The silent mode param, see [ChatSilentModeParam].
+ * @return The result of setting silent mode. See [ChatSilentModeResult].
+ */
+suspend fun ChatPushManager.setSilentModeForApp(silentModeParam: ChatSilentModeParam): ChatSilentModeResult {
+ return suspendCoroutine { continuation ->
+ setSilentModeForAll(silentModeParam, ValueCallbackImpl(
+ onSuccess = {
+ continuation.resume(it)
+ },
+ onError = { error, errorDescription ->
+ continuation.resumeWithException(ChatException(error, errorDescription))
+ }
+ ))
+ }
+}
+
+/**
+ * Get the silent mode for the App.
+ * @return The result of getting silent mode. See [ChatSilentModeResult].
+ */
+suspend fun ChatPushManager.getSilentModeForApp(): ChatSilentModeResult {
+ return suspendCoroutine { continuation ->
+ getSilentModeForAll(ValueCallbackImpl(
+ onSuccess = {
+ continuation.resume(it)
+ },
+ onError = { error, errorDescription ->
+ continuation.resumeWithException(ChatException(error, errorDescription))
+ }
+ ))
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/fcm/FCMMSGService.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/fcm/FCMMSGService.kt
new file mode 100644
index 00000000..45682ddc
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/fcm/FCMMSGService.kt
@@ -0,0 +1,26 @@
+package io.agora.chatdemo.fcm
+
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import io.agora.chat.ChatClient
+import io.agora.chatdemo.DemoHelper
+import io.agora.util.EMLog
+
+class FCMMSGService: FirebaseMessagingService() {
+ private val TAG = "FCMMSGService"
+
+ override fun onMessageReceived(remoteMessage: RemoteMessage) {
+ super.onMessageReceived(remoteMessage)
+ if (remoteMessage.data.isNotEmpty()) {
+ val message = remoteMessage.data["alert"]
+ EMLog.i(TAG, "onMessageReceived: $message")
+ DemoHelper.getInstance().getNotifier()?.notify(message)
+ }
+ }
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ EMLog.i(TAG, "onNewToken: $token")
+ ChatClient.getInstance().sendFCMTokenToServer(token)
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/controller/PresenceController.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/controller/PresenceController.kt
new file mode 100644
index 00000000..6b1cb476
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/controller/PresenceController.kt
@@ -0,0 +1,135 @@
+package io.agora.chatdemo.feature.presence.controller
+
+import android.content.Context
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import io.agora.chatdemo.R
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.feature.presence.interfaces.IPresenceResultView
+import io.agora.chatdemo.feature.presence.utils.EasePresenceUtil
+import io.agora.chatdemo.feature.presence.viewmodel.PresenceViewModel
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.ChatPresence
+import io.agora.uikit.common.dialog.CustomDialog
+import io.agora.uikit.common.dialog.SimpleListSheetDialog
+import io.agora.uikit.interfaces.SimpleListSheetItemClickListener
+import io.agora.uikit.model.EaseMenuItem
+
+class PresenceController(
+ private val context: Context,
+ private val presenceVideModel: PresenceViewModel,
+): IPresenceResultView {
+ init {
+ presenceVideModel.attachView(this)
+ }
+ private var presenceDialog: SimpleListSheetDialog? = null
+ private var currentPresence:String? = null
+
+ fun showPresenceStatusDialog(presence: ChatPresence?){
+ val tag = EasePresenceUtil.getPresenceString(context,presence)
+ if (
+ tag == context.getString(R.string.ease_presence_online) ||
+ tag == context.getString(R.string.ease_presence_busy) ||
+ tag == context.getString(R.string.ease_presence_do_not_disturb) ||
+ tag == context.getString(R.string.ease_presence_away) ||
+ tag == context.getString(R.string.ease_presence_offline) || tag.isEmpty()
+ ){ }else{
+ currentPresence = tag
+ }
+
+ presenceDialog = SimpleListSheetDialog(
+ context = context,
+ itemList = defaultItems(),
+ itemListener = object : SimpleListSheetItemClickListener {
+ override fun onItemClickListener(position: Int, menu: EaseMenuItem) {
+ simpleMenuItemClickListener(position, menu)
+ }
+ })
+ if (context is FragmentActivity){
+ context.supportFragmentManager.let { presenceDialog?.show(it,"presence_status_dialog") }
+ }else if (context is Fragment){
+ context.parentFragmentManager.let { presenceDialog?.show(it,"presence_status_dialog") }
+ }
+ }
+
+ private fun simpleMenuItemClickListener(position: Int,menu: EaseMenuItem){
+ when(menu.menuId){
+ R.id.presence_status_online -> {
+ presenceVideModel.publishPresence("")
+ presenceDialog?.dismiss()
+ }
+ R.id.presence_status_busy -> {
+ presenceVideModel.publishPresence(DemoConstant.PRESENCE_BUSY)
+ presenceDialog?.dismiss()
+ }
+ R.id.presence_status_do_not_disturb -> {
+ presenceVideModel.publishPresence(DemoConstant.PRESENCE_DO_NOT_DISTURB)
+ presenceDialog?.dismiss()
+ }
+ R.id.presence_status_away -> {
+ presenceVideModel.publishPresence(DemoConstant.PRESENCE_AWAY)
+ presenceDialog?.dismiss()
+ }
+ R.id.presence_status_custom -> {
+ presenceDialog?.dismiss()
+ showCustomDialog()
+ }
+ else -> {}
+ }
+ }
+
+ private fun defaultItems():MutableList{
+ return mutableListOf(
+ EaseMenuItem(
+ menuId = R.id.presence_status_online,
+ title = context.getString(R.string.ease_presence_online),
+ titleColor = ContextCompat.getColor(context, R.color.color_primary)
+ ),
+ EaseMenuItem(
+ menuId = R.id.presence_status_busy,
+ title = context.getString(R.string.ease_presence_busy),
+ titleColor = ContextCompat.getColor(context, R.color.color_primary)
+ ),
+ EaseMenuItem(
+ menuId = R.id.presence_status_away,
+ title = context.getString(R.string.ease_presence_away),
+ titleColor = ContextCompat.getColor(context, R.color.color_primary)
+ ),
+ EaseMenuItem(
+ menuId = R.id.presence_status_do_not_disturb,
+ title = context.getString(R.string.ease_presence_do_not_disturb),
+ titleColor = ContextCompat.getColor(context, R.color.color_primary)
+ ),
+ EaseMenuItem(
+ menuId = R.id.presence_status_custom,
+ title = context.getString(R.string.ease_presence_custom),
+ titleColor = ContextCompat.getColor(context, R.color.color_primary)
+ )
+ )
+ }
+
+ private fun showCustomDialog(){
+ val customDialog = CustomDialog(
+ context = context,
+ title = context.getString(R.string.presence_dialog_title),
+ inputHint = context.getString(R.string.presence_dialog_input_hint),
+ isEditTextMode = true,
+ onInputModeConfirmListener = {
+ presenceVideModel.publishPresence(it)
+ }
+ )
+ customDialog.show()
+ }
+
+ override fun onPublishPresenceSuccess() {
+ super.onPublishPresenceSuccess()
+ ChatLog.e("ChatPresenceController","onPublishPresenceSuccess")
+ }
+
+ override fun onPublishPresenceFail(code: Int, message: String?) {
+ super.onPublishPresenceFail(code, message)
+ ChatLog.e("ChatPresenceController","onPublishPresenceFail $code $message")
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/interfaces/IPresenceRequest.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/interfaces/IPresenceRequest.kt
new file mode 100644
index 00000000..7d1c5f30
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/interfaces/IPresenceRequest.kt
@@ -0,0 +1,38 @@
+package io.agora.chatdemo.feature.presence.interfaces
+
+import io.agora.chatdemo.interfaces.IAttachView
+
+interface IPresenceRequest: IAttachView {
+
+ /**
+ * Publish your own status
+ */
+ fun publishPresence(ext:String?){}
+
+ /**
+ * Gets the current presence state of users.
+ * @param userIds The array of IDs of users whose current presence state you want to check.
+ */
+ fun fetchPresenceStatus(userIds:MutableList?){}
+
+ /**
+ * Subscribes to a user's presence states. If the subscription succeeds, the subscriber will receive the callback when the user's presence state changes.
+ * @param userIds Subscription ID List
+ * @param expiry The expiration time of the presence subscription.
+ */
+ fun subscribePresences(userIds:MutableList?,expiry:Long?=null){}
+
+ /**
+ * Unsubscribes from a user's presence states.
+ * @param userIds Subscription ID List
+ */
+ fun unsubscribePresences(userIds:MutableList?){}
+
+
+ /**
+ * fetch the current user status of a specified user
+ * @param userIds
+ */
+ fun fetchChatPresence(userIds:MutableList)
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/interfaces/IPresenceResultView.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/interfaces/IPresenceResultView.kt
new file mode 100644
index 00000000..bfca2e2f
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/interfaces/IPresenceResultView.kt
@@ -0,0 +1,61 @@
+package io.agora.chatdemo.feature.presence.interfaces
+
+import io.agora.uikit.common.ChatPresence
+import io.agora.uikit.common.interfaces.IControlDataView
+
+interface IPresenceResultView: IControlDataView {
+
+ /**
+ * Publish custom status success
+ */
+ fun onPublishPresenceSuccess(){}
+
+ /**
+ * Publish custom status fail
+ */
+ fun onPublishPresenceFail(code: Int, message: String?){}
+
+ /**
+ * Gets the current presence state of users success
+ */
+ fun fetchPresenceStatusSuccess(presence:MutableList){}
+
+ /**
+ * Gets the current presence state of users fail
+ */
+ fun fetchPresenceStatusFail(code: Int, message: String?){}
+
+ /**
+ * Subscribe user status success
+ */
+ fun subscribePresenceSuccess(result: MutableList){}
+
+ /**
+ * Subscribe user status fail
+ */
+ fun subscribePresenceFail(code: Int, message: String?){}
+
+ /**
+ * unSubscribe user status success
+ */
+ fun unSubscribePresenceSuccess(){}
+
+ /**
+ * unSubscribe user status fail
+ */
+ fun unSubscribePresenceFail(code: Int, message: String?){}
+
+ /**
+ * fetch the current user status of a specified user success.
+ * @param presence
+ */
+ fun fetchChatPresenceSuccess(presence:MutableList){}
+
+ /**
+ * fetch the current user status of a specified user fail.
+ * @param code error code.
+ * @param error error message.
+ */
+ fun fetchChatPresenceFail(code: Int, error: String){}
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/repository/ChatPresenceRepository.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/repository/ChatPresenceRepository.kt
new file mode 100644
index 00000000..4d355f2b
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/repository/ChatPresenceRepository.kt
@@ -0,0 +1,39 @@
+package io.agora.chatdemo.feature.presence.repository
+
+import io.agora.chatdemo.common.suspend.fetchUserPresenceStatus
+import io.agora.chatdemo.common.suspend.publishExtPresence
+import io.agora.chatdemo.common.suspend.subscribeUsersPresence
+import io.agora.chatdemo.common.suspend.unSubscribeUsersPresence
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatPresenceManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class ChatPresenceRepository(
+ private val presenceManager: ChatPresenceManager = ChatClient.getInstance().presenceManager(),
+) {
+
+ suspend fun publishPresence(customStatus: String) =
+ withContext(Dispatchers.IO) {
+ presenceManager.publishExtPresence(customStatus)
+ }
+
+
+ suspend fun subscribePresences(userIds:MutableList,expiry:Long) =
+ withContext(Dispatchers.IO) {
+ presenceManager.subscribeUsersPresence(userIds,expiry)
+ }
+
+
+ suspend fun unSubscribePresences(userIds:MutableList) =
+ withContext(Dispatchers.IO) {
+ presenceManager.unSubscribeUsersPresence(userIds)
+ }
+
+ suspend fun fetchPresenceStatus(userIds:MutableList) =
+ withContext(Dispatchers.IO) {
+ presenceManager.fetchUserPresenceStatus(userIds)
+ }
+
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/utils/EasePresenceUtil.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/utils/EasePresenceUtil.kt
new file mode 100644
index 00000000..494a0e64
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/utils/EasePresenceUtil.kt
@@ -0,0 +1,101 @@
+package io.agora.chatdemo.feature.presence.utils
+
+import android.content.Context
+import android.text.TextUtils
+import androidx.annotation.DrawableRes
+import io.agora.chatdemo.bean.PresenceData
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.uikit.common.ChatPresence
+
+object EasePresenceUtil {
+
+ fun getPresenceString(context: Context, presence: ChatPresence?): String {
+ if (presence != null) {
+ var isOnline = false
+ val statusList: Map = presence.statusList
+ for (entry in statusList) {
+ if (entry.value == 1){
+ isOnline = true
+ break
+ }
+ }
+
+ if (isOnline) {
+ val ext: String = presence.ext
+ return if (TextUtils.isEmpty(ext) || TextUtils.equals(
+ ext,
+ DemoConstant.PRESENCE_ONLINE
+ )
+ ) {
+ context.getString(PresenceData.ONLINE.presence)
+ } else if (TextUtils.equals(
+ ext,
+ DemoConstant.PRESENCE_BUSY
+ )
+ ) {
+ context.getString(PresenceData.BUSY.presence)
+ } else if (TextUtils.equals(
+ ext,
+ DemoConstant.PRESENCE_DO_NOT_DISTURB
+ )
+ ) {
+ context.getString(PresenceData.DO_NOT_DISTURB.presence)
+ } else if (TextUtils.equals(
+ ext,
+ DemoConstant.PRESENCE_AWAY
+ )
+ ) {
+ context.getString(PresenceData.AWAY.presence)
+ } else {
+ ext
+ }
+ }
+ }
+ return context.getString(PresenceData.OFFLINE.presence)
+ }
+
+ @DrawableRes
+ fun getPresenceIcon(context: Context, presence: ChatPresence?): Int {
+ if (presence != null){
+ var isOnline = false
+ val statusList: Map = presence.statusList
+ for (entry in statusList) {
+ if (entry.value == 1){
+ isOnline = true
+ break
+ }
+ }
+ if (isOnline) {
+ val ext: String = presence.ext
+ return if (TextUtils.isEmpty(ext) || TextUtils.equals(
+ ext,
+ DemoConstant.PRESENCE_ONLINE
+ )
+ ) {
+ PresenceData.ONLINE.presenceIcon
+ } else if (TextUtils.equals(
+ ext,
+ DemoConstant.PRESENCE_BUSY
+ )
+ ) {
+ PresenceData.BUSY.presenceIcon
+ } else if (TextUtils.equals(
+ ext,
+ DemoConstant.PRESENCE_DO_NOT_DISTURB
+ )
+ ) {
+ PresenceData.DO_NOT_DISTURB.presenceIcon
+ } else if (TextUtils.equals(
+ ext,
+ DemoConstant.PRESENCE_AWAY
+ )
+ ) {
+ PresenceData.AWAY.presenceIcon
+ } else {
+ PresenceData.CUSTOM.presenceIcon
+ }
+ }
+ }
+ return PresenceData.OFFLINE.presenceIcon
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/viewmodel/PresenceViewModel.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/viewmodel/PresenceViewModel.kt
new file mode 100644
index 00000000..05d48bcc
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/feature/presence/viewmodel/PresenceViewModel.kt
@@ -0,0 +1,96 @@
+package io.agora.chatdemo.feature.presence.viewmodel
+
+import androidx.lifecycle.viewModelScope
+import io.agora.chatdemo.feature.presence.interfaces.IPresenceRequest
+import io.agora.chatdemo.feature.presence.interfaces.IPresenceResultView
+import io.agora.chatdemo.feature.presence.repository.ChatPresenceRepository
+import io.agora.uikit.common.extensions.catchChatException
+import io.agora.uikit.viewmodel.EaseBaseViewModel
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.launch
+
+class PresenceViewModel: EaseBaseViewModel(), IPresenceRequest {
+
+ private val presenceRepository by lazy { ChatPresenceRepository() }
+ private val expiryTime = (7 * 24 * 60 * 60).toLong()
+
+ override fun publishPresence(ext: String?) {
+ viewModelScope.launch {
+ flow {
+ ext?.let {
+ emit(presenceRepository.publishPresence(it))
+ }
+ }
+ .catchChatException { e ->
+ view?.onPublishPresenceFail(e.errorCode, e.description)
+ }
+ .collect {
+ view?.onPublishPresenceSuccess()
+ }
+ }
+ }
+
+ override fun fetchPresenceStatus(userIds: MutableList?) {
+ viewModelScope.launch {
+ flow {
+ userIds?.let {
+ emit(presenceRepository.fetchPresenceStatus(it))
+ }
+ }
+ .catchChatException { e ->
+ view?.fetchPresenceStatusFail(e.errorCode, e.description)
+ }
+ .collect {
+ view?.fetchPresenceStatusSuccess(it)
+ }
+ }
+ }
+
+ override fun subscribePresences(userIds: MutableList?, expiry: Long?) {
+ viewModelScope.launch {
+ flow {
+ userIds?.let {
+ emit(presenceRepository.subscribePresences(it,expiry?:expiryTime))
+ }
+ }
+ .catchChatException { e ->
+ view?.subscribePresenceFail(e.errorCode, e.description)
+ }
+ .collect {
+ view?.subscribePresenceSuccess(it)
+ }
+ }
+ }
+
+ override fun unsubscribePresences(userIds: MutableList?) {
+ viewModelScope.launch {
+ flow {
+ userIds?.let {
+ emit(presenceRepository.unSubscribePresences(it))
+ }
+ }
+ .catchChatException { e ->
+ view?.unSubscribePresenceFail(e.errorCode, e.description)
+ }
+ .collect {
+ view?.unSubscribePresenceSuccess()
+ }
+ }
+ }
+
+ override fun fetchChatPresence(userIds: MutableList) {
+ viewModelScope.launch {
+ flow {
+ emit(presenceRepository.fetchPresenceStatus(userIds))
+ }
+ .catchChatException { e->
+ view?.fetchChatPresenceFail(e.errorCode, e.description)
+ }
+ .collect {
+ view?.fetchChatPresenceSuccess(it)
+ }
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IAttachView.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IAttachView.kt
new file mode 100644
index 00000000..07c731ec
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IAttachView.kt
@@ -0,0 +1,10 @@
+package io.agora.chatdemo.interfaces
+
+import io.agora.uikit.common.interfaces.IControlDataView
+
+
+interface IAttachView {
+ fun attachView(view: IControlDataView)
+
+ fun detachView()
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IMainRequest.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IMainRequest.kt
new file mode 100644
index 00000000..270dfefb
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IMainRequest.kt
@@ -0,0 +1,15 @@
+package io.agora.chatdemo.interfaces
+
+
+interface IMainRequest: IAttachView {
+
+ /**
+ * Get all unread message count.
+ */
+ fun getUnreadMessageCount()
+
+ /**
+ * Get all unread request count.
+ */
+ fun getRequestUnreadCount()
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IMainResultView.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IMainResultView.kt
new file mode 100644
index 00000000..0b74ddff
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/IMainResultView.kt
@@ -0,0 +1,16 @@
+package io.agora.chatdemo.interfaces
+
+import io.agora.uikit.common.interfaces.IControlDataView
+
+interface IMainResultView: IControlDataView {
+
+ /**
+ * Get unread message count successfully.
+ */
+ fun getUnreadCountSuccess(count: String?)
+
+ /**
+ * Get all unread request count successfully.
+ */
+ fun getRequestUnreadCountSuccess(count: String?)
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/LanguageListItemSelectListener.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/LanguageListItemSelectListener.kt
new file mode 100644
index 00000000..e228db88
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/interfaces/LanguageListItemSelectListener.kt
@@ -0,0 +1,8 @@
+package io.agora.chatdemo.interfaces
+
+import io.agora.chatdemo.bean.Language
+
+
+interface LanguageListItemSelectListener {
+ fun onSelectListener(position:Int,language: Language)
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/ChatActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/ChatActivity.kt
new file mode 100644
index 00000000..e676e8f4
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/ChatActivity.kt
@@ -0,0 +1,48 @@
+package io.agora.chatdemo.page.chat
+
+import io.agora.chatdemo.R
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatMessage
+import io.agora.uikit.common.extensions.showToast
+import io.agora.uikit.feature.chat.EaseChatFragment
+import io.agora.uikit.feature.chat.activities.EaseChatActivity
+import io.agora.uikit.feature.chat.interfaces.OnMessageForwardCallback
+import io.agora.uikit.feature.chat.interfaces.OnModifyMessageListener
+import io.agora.uikit.feature.chat.interfaces.OnSendCombineMessageCallback
+
+class ChatActivity: EaseChatActivity() {
+
+ override fun setChildSettings(builder: EaseChatFragment.Builder) {
+ super.setChildSettings(builder)
+ builder.setOnMessageForwardCallback(object : OnMessageForwardCallback {
+ override fun onForwardSuccess(message: ChatMessage?) {
+ mContext.showToast(R.string.message_forward_success)
+ }
+
+ override fun onForwardError(code: Int, errorMsg: String?) {
+ mContext.showToast(R.string.message_forward_fail)
+ }
+ })
+ builder.setOnSendCombineMessageCallback(object : OnSendCombineMessageCallback {
+ override fun onSendCombineSuccess(message: ChatMessage?) {
+ mContext.showToast(R.string.message_combine_success)
+ }
+
+ override fun onSendCombineError(message: ChatMessage?, code: Int, errorMsg: String?) {
+ mContext.showToast(R.string.message_combine_fail)
+ }
+ })
+ builder.setOnModifyMessageListener(object : OnModifyMessageListener {
+ override fun onModifyMessageSuccess(messageModified: ChatMessage?) {
+
+ }
+
+ override fun onModifyMessageFailure(messageId: String?, code: Int, error: String?) {
+ mContext.showToast(R.string.message_modify_fail)
+ }
+ })
+ builder.turnOnTypingMonitor(EaseIM.getConfig()?.chatConfig?.enableChatTyping?:true)
+ builder.setCustomFragment(ChatFragment())
+ .setCustomAdapter(CustomMessagesAdapter())
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/ChatFragment.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/ChatFragment.kt
new file mode 100644
index 00000000..a95ed0a1
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/ChatFragment.kt
@@ -0,0 +1,130 @@
+package io.agora.chatdemo.page.chat
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.util.Log
+import android.view.MenuItem
+import android.view.View
+import androidx.lifecycle.ViewModelProvider
+import io.agora.chatdemo.R
+import io.agora.chatdemo.callkit.CallKitManager
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.PresenceCache
+import io.agora.chatdemo.common.helper.MenuFilterHelper
+import io.agora.chatdemo.feature.presence.interfaces.IPresenceRequest
+import io.agora.chatdemo.feature.presence.interfaces.IPresenceResultView
+import io.agora.chatdemo.feature.presence.utils.EasePresenceUtil
+import io.agora.chatdemo.feature.presence.viewmodel.PresenceViewModel
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatMessage
+import io.agora.uikit.common.ChatPresence
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.feature.chat.EaseChatFragment
+import io.agora.uikit.feature.chat.enums.EaseChatType
+import io.agora.uikit.feature.chat.widgets.EaseChatLayout
+import io.agora.uikit.menu.chat.EaseChatMenuHelper
+import io.agora.uikit.model.EaseEvent
+
+class ChatFragment: EaseChatFragment() , IPresenceResultView {
+ private var presenceViewModel: IPresenceRequest? = null
+ override fun initView(savedInstanceState: Bundle?) {
+ super.initView(savedInstanceState)
+ binding?.titleBar?.inflateMenu(R.menu.demo_chat_menu)
+ updatePresence()
+ }
+
+ override fun initEventBus() {
+ super.initEventBus()
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE + EaseEvent.TYPE.CONTACT + DemoConstant.EVENT_UPDATE_USER_SUFFIX).register(this) {
+ if (it.isContactChange && it.message.isNullOrEmpty().not()) {
+ val userId = it.message
+ if (chatType == EaseChatType.SINGLE_CHAT && userId == conversationId) {
+ setDefaultHeader(true)
+ }
+ binding?.layoutChat?.chatMessageListLayout?.refreshMessages()
+ }
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE.name).register(this) {
+ if (it.isPresenceChange && it.message.equals(conversationId) ) {
+ updatePresence()
+ }
+ }
+ }
+
+ override fun initViewModel() {
+ super.initViewModel()
+ presenceViewModel = ViewModelProvider(this)[PresenceViewModel::class.java]
+ presenceViewModel?.attachView(this)
+ }
+
+ override fun initData() {
+ super.initData()
+ conversationId?.let {
+ if (it != EaseIM.getCurrentUser()?.id){
+ presenceViewModel?.fetchChatPresence(mutableListOf(it))
+ presenceViewModel?.subscribePresences(mutableListOf(it))
+ }
+ }
+ }
+
+ override fun setMenuItemClick(item: MenuItem): Boolean {
+ when(item.itemId) {
+ R.id.chat_menu_video_call -> {
+ showVideoCall()
+ return true
+ }
+ }
+ return super.setMenuItemClick(item)
+ }
+
+ private fun showVideoCall() {
+ CallKitManager.showSelectDialog(chatType, mContext, conversationId)
+ }
+
+ override fun onPreMenu(helper: EaseChatMenuHelper?, message: ChatMessage?) {
+ super.onPreMenu(helper, message)
+ MenuFilterHelper.filterMenu(helper, message)
+ }
+
+ private fun updatePresence(){
+ if (chatType == EaseChatType.SINGLE_CHAT){
+ conversationId?.let {
+ val presence = PresenceCache.getUserPresence(it)
+ presence?.let {
+ val logoStatus = EasePresenceUtil.getPresenceIcon(mContext,presence)
+ val subtitle = EasePresenceUtil.getPresenceString(mContext,presence)
+ binding?.run{
+ titleBar.setLogoStatusMargin(end = -1, bottom = -1)
+ titleBar.setLogoStatus(logoStatus)
+ titleBar.setSubtitle(subtitle)
+ titleBar.getStatusView().visibility = View.VISIBLE
+ titleBar.setLogoStatusSize(resources.getDimensionPixelSize(R.dimen.em_title_bar_status_icon_size))
+ }
+ }
+ }
+ }
+ }
+
+ override fun onPeerTyping(action: String?) {
+ if (TextUtils.equals(action, EaseChatLayout.ACTION_TYPING_BEGIN)) {
+ binding?.titleBar?.setSubtitle(getString(io.agora.uikit.R.string.alert_during_typing))
+ binding?.titleBar?.visibility = View.VISIBLE
+ } else if (TextUtils.equals(action, EaseChatLayout.ACTION_TYPING_END)) {
+ updatePresence()
+ }
+ }
+
+
+ override fun onDestroy() {
+ conversationId?.let {
+ if (it != EaseIM.getCurrentUser()?.id){
+ presenceViewModel?.unsubscribePresences(mutableListOf(it))
+ }
+ }
+ super.onDestroy()
+ }
+
+ override fun fetchChatPresenceSuccess(presence: MutableList) {
+ updatePresence()
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/CustomMessagesAdapter.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/CustomMessagesAdapter.kt
new file mode 100644
index 00000000..29664c11
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/chat/CustomMessagesAdapter.kt
@@ -0,0 +1,52 @@
+package io.agora.chatdemo.page.chat
+
+import android.text.TextUtils
+import android.view.ViewGroup
+import io.agora.chat.callkit.general.EaseCallType
+import io.agora.chat.callkit.utils.EaseCallMsgUtils
+import io.agora.chatdemo.callkit.MultipleInviteViewHolder
+import io.agora.chatdemo.callkit.ChatVoiceCallViewHolder
+import io.agora.chatdemo.callkit.views.ChatRowConferenceInvite
+import io.agora.chatdemo.callkit.views.ChatRowVoiceCall
+import io.agora.uikit.common.ChatMessage
+import io.agora.uikit.common.ChatMessageDirection
+import io.agora.uikit.feature.chat.adapter.EaseMessagesAdapter
+
+class CustomMessagesAdapter: EaseMessagesAdapter() {
+
+ companion object {
+ const val VIEW_TYPE_MESSAGE_CALL_SEND = 1000
+ const val VIEW_TYPE_MESSAGE_CALL_RECEIVE = 1001
+ const val VIEW_TYPE_MESSAGE_INVITE_SEND = 1002
+ const val VIEW_TYPE_MESSAGE_INVITE_RECEIVE = 1003
+ }
+ //继承EaseMessagesAdapter 重写getItemNotEmptyViewType 添加自定义ViewType
+ //下方示例 增加自定义 call 消息提醒类型
+ override fun getItemNotEmptyViewType(position: Int): Int {
+ getItem(position)?.let {
+ val msgType = it.getStringAttribute(EaseCallMsgUtils.CALL_MSG_TYPE,"")
+ val callType = it.getIntAttribute(EaseCallMsgUtils.CALL_TYPE, 0)
+ if (TextUtils.equals(msgType, EaseCallMsgUtils.CALL_MSG_INFO)) {
+ if (callType == EaseCallType.CONFERENCE_VIDEO_CALL.ordinal || callType == EaseCallType.CONFERENCE_VOICE_CALL.ordinal) {
+ return if (it.direct() == ChatMessageDirection.SEND) VIEW_TYPE_MESSAGE_INVITE_SEND else VIEW_TYPE_MESSAGE_INVITE_RECEIVE
+ }
+ return if (it.direct() == ChatMessageDirection.SEND) VIEW_TYPE_MESSAGE_CALL_SEND else VIEW_TYPE_MESSAGE_CALL_RECEIVE
+ }
+ }
+ return super.getItemNotEmptyViewType(position)
+ }
+
+ // 继承EaseMessagesAdapter getViewHolder 添加自定义ViewHolder 和 ui布局
+ // 下方示例 增加单聊、群聊 call 消息提醒类型布局和事件处理
+ override fun getViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ when (viewType) {
+ VIEW_TYPE_MESSAGE_CALL_SEND, VIEW_TYPE_MESSAGE_CALL_RECEIVE -> {
+ return ChatVoiceCallViewHolder(ChatRowVoiceCall(parent.context, isSender = viewType == VIEW_TYPE_MESSAGE_CALL_SEND))
+ }
+ VIEW_TYPE_MESSAGE_INVITE_SEND, VIEW_TYPE_MESSAGE_INVITE_RECEIVE -> {
+ return MultipleInviteViewHolder(ChatRowConferenceInvite(parent.context, isSender = viewType == VIEW_TYPE_MESSAGE_INVITE_SEND))
+ }
+ }
+ return super.getViewHolder(parent, viewType)
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactCheckActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactCheckActivity.kt
new file mode 100644
index 00000000..22930916
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactCheckActivity.kt
@@ -0,0 +1,59 @@
+package io.agora.chatdemo.page.contact
+
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import coil.load
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.room.entity.parse
+import io.agora.chatdemo.common.room.extensions.parseToDbBean
+import io.agora.chatdemo.viewmodel.ProfileInfoViewModel
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.ChatUserInfoType
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.common.extensions.catchChatException
+import io.agora.uikit.feature.contact.EaseContactCheckActivity
+import io.agora.uikit.model.EaseEvent
+import kotlinx.coroutines.launch
+
+class ChatContactCheckActivity: EaseContactCheckActivity() {
+ private lateinit var model: ProfileInfoViewModel
+
+ override fun initData() {
+ super.initData()
+ model = ViewModelProvider(this)[ProfileInfoViewModel::class.java]
+ lifecycleScope.launch {
+ user?.let { user->
+ model.fetchUserInfoAttribute(listOf(user.userId), listOf(ChatUserInfoType.NICKNAME, ChatUserInfoType.AVATAR_URL))
+ .catchChatException {
+ ChatLog.e("ChatContactCheckActivity", "fetchUserInfoAttribute error: ${it.description}")
+ }
+ .collect {
+ it[user.userId]?.parseToDbBean()?.let { u->
+ u.parse().apply {
+ EaseIM.updateUsersInfo(mutableListOf(this))
+ DemoHelper.getInstance().getDataModel().insertUser(this)
+ }
+ updateUserInfo()
+ }
+ }
+ }
+ }
+ }
+
+ private fun updateUserInfo() {
+ DemoHelper.getInstance().getDataModel().getUser(user?.userId)?.let {
+ val ph = AppCompatResources.getDrawable(this, io.agora.uikit.R.drawable.ease_default_avatar)
+ val ep = AppCompatResources.getDrawable(this, io.agora.uikit.R.drawable.ease_default_avatar)
+ binding.ivAvatar.load(it.parse().avatar ?: ph) {
+ placeholder(ph)
+ error(ep)
+ }
+ binding.tvName.text = it.name?.ifEmpty { it.userId } ?: it.userId
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactDetailActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactDetailActivity.kt
new file mode 100644
index 00000000..f45b9337
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactDetailActivity.kt
@@ -0,0 +1,156 @@
+package io.agora.chatdemo.page.contact
+
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.R
+import io.agora.chatdemo.callkit.CallKitManager
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.PresenceCache
+import io.agora.chatdemo.common.room.entity.parse
+import io.agora.chatdemo.common.room.extensions.parseToDbBean
+import io.agora.chatdemo.feature.presence.interfaces.IPresenceResultView
+import io.agora.chatdemo.feature.presence.utils.EasePresenceUtil
+import io.agora.chatdemo.feature.presence.viewmodel.PresenceViewModel
+import io.agora.chatdemo.viewmodel.ProfileInfoViewModel
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.ChatPresence
+import io.agora.uikit.common.ChatUserInfoType
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.common.extensions.catchChatException
+import io.agora.uikit.common.extensions.toProfile
+import io.agora.uikit.feature.contact.EaseContactDetailsActivity
+import io.agora.uikit.model.EaseEvent
+import io.agora.uikit.model.EaseMenuItem
+import kotlinx.coroutines.launch
+
+
+class ChatContactDetailActivity: EaseContactDetailsActivity(), IPresenceResultView {
+ private lateinit var model: ProfileInfoViewModel
+ private lateinit var presenceModel: PresenceViewModel
+
+ companion object {
+ private const val TAG = "ChatContactDetailActivity"
+ }
+
+ override fun initView() {
+ super.initView()
+ model = ViewModelProvider(this)[ProfileInfoViewModel::class.java]
+ presenceModel = ViewModelProvider(this)[PresenceViewModel::class.java]
+ presenceModel.attachView(this)
+ updateUserInfo()
+ }
+
+ override fun initEvent() {
+ super.initEvent()
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE.name).register(this) {
+ if (it.isPresenceChange ) {
+ updatePresence()
+ }
+ }
+ }
+
+ override fun initData() {
+ super.initData()
+ user?.let {
+ presenceModel.fetchChatPresence(mutableListOf(it.userId))
+ }
+ lifecycleScope.launch {
+ user?.let { user->
+ model.fetchUserInfoAttribute(listOf(user.userId), listOf(ChatUserInfoType.NICKNAME, ChatUserInfoType.AVATAR_URL))
+ .catchChatException {
+ ChatLog.e("ContactDetail", "fetchUserInfoAttribute error: ${it.description}")
+ }
+ .collect {
+ it[user.userId]?.parseToDbBean()?.let {u->
+ u.parse().apply {
+ EaseIM.updateUsersInfo(mutableListOf(this))
+ DemoHelper.getInstance().getDataModel().insertUser(this)
+ }
+ updateUserInfo()
+ notifyUpdateRemarkEvent()
+ }
+ }
+ }
+ }
+ }
+
+ private fun updateUserInfo() {
+ DemoHelper.getInstance().getDataModel().getUser(user?.userId)?.let {
+ binding.epPresence.setUserAvatarData(it.parse())
+ binding.tvName.text = it.parse().getNotEmptyName()
+ binding.tvNumber.text = it.userId
+ }
+ }
+
+ override fun getDetailItem(): MutableList? {
+ val list = super.getDetailItem()
+ val audioItem = EaseMenuItem(
+ title = getString(R.string.detail_item_audio),
+ resourceId = io.agora.uikit.R.drawable.ease_phone_pick,
+ menuId = R.id.contact_item_audio_call,
+ titleColor = ContextCompat.getColor(this, R.color.color_primary),
+ order = 2
+ )
+
+ val videoItem = EaseMenuItem(
+ title = getString(R.string.detail_item_video),
+ resourceId = io.agora.uikit.R.drawable.ease_video_camera,
+ menuId = R.id.contact_item_video_call,
+ titleColor = ContextCompat.getColor(this, R.color.color_primary),
+ order = 3
+ )
+ list?.add(audioItem)
+ list?.add(videoItem)
+ return list
+ }
+
+ override fun onMenuItemClick(item: EaseMenuItem?, position: Int): Boolean {
+ item?.let {
+ when(item.menuId){
+ R.id.contact_item_audio_call -> {
+ CallKitManager.startSingleAudioCall(user?.userId)
+ return true
+ }
+ R.id.contact_item_video_call -> {
+ CallKitManager.startSingleVideoCall(user?.userId)
+ return true
+ }
+ else -> {
+ return super.onMenuItemClick(item, position)
+ }
+ }
+ }
+ return false
+ }
+
+
+ private fun notifyUpdateRemarkEvent() {
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE + EaseEvent.TYPE.CONTACT + DemoConstant.EVENT_UPDATE_USER_SUFFIX)
+ .post(lifecycleScope, EaseEvent(DemoConstant.EVENT_UPDATE_USER_SUFFIX, EaseEvent.TYPE.CONTACT, user?.userId))
+ }
+
+ private fun updatePresence(){
+ val map = PresenceCache.getPresenceInfo
+ user?.let { user->
+ map.let {
+ binding.epPresence.getStatusView().visibility = View.VISIBLE
+ binding.epPresence.setUserAvatarData(user.toProfile(),
+ EasePresenceUtil.getPresenceIcon(mContext,it[user.userId]))
+ }
+ }
+ }
+
+ override fun fetchChatPresenceSuccess(presence: MutableList) {
+ ChatLog.e(TAG,"fetchChatPresenceSuccess $presence")
+ updatePresence()
+ }
+
+ override fun fetchChatPresenceFail(code: Int, error: String) {
+ ChatLog.e(TAG,"fetchChatPresenceFail $code $error")
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactListFragmentEvent.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactListFragmentEvent.kt
new file mode 100644
index 00000000..2dbdb939
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatContactListFragmentEvent.kt
@@ -0,0 +1,121 @@
+package io.agora.chatdemo.page.contact
+
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.ViewModelProvider
+import io.agora.chatdemo.R
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.PresenceCache
+import io.agora.chatdemo.feature.presence.controller.PresenceController
+import io.agora.chatdemo.feature.presence.utils.EasePresenceUtil
+import io.agora.chatdemo.feature.presence.viewmodel.PresenceViewModel
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.common.extensions.dpToPx
+import io.agora.uikit.configs.setAvatarStyle
+import io.agora.uikit.configs.setStatusStyle
+import io.agora.uikit.feature.contact.EaseContactsListFragment
+import io.agora.uikit.model.EaseEvent
+import io.agora.uikit.model.EaseUser
+
+class ChatContactListFragmentEvent : EaseContactsListFragment() {
+
+ private var isFirstLoadData = false
+ private val presenceViewModel by lazy { ViewModelProvider(this)[PresenceViewModel::class.java] }
+ private val presenceController by lazy { PresenceController(mContext, presenceViewModel) }
+ companion object{
+ private val TAG = ChatContactListFragmentEvent::class.java.simpleName
+ }
+
+ override fun initView(savedInstanceState: Bundle?) {
+ super.initView(savedInstanceState)
+ binding?.titleContact?.let {
+ it.setTitle("")
+ it.setTitleEndDrawable(R.drawable.contact_title)
+ }
+ updateProfile()
+ }
+
+ override fun initData() {
+ super.initData()
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE + EaseEvent.TYPE.CONTACT + DemoConstant.EVENT_UPDATE_USER_SUFFIX).register(this) {
+ if (it.isContactChange && it.message.isNullOrEmpty().not()) {
+ binding?.listContact?.loadContactData(false)
+ }
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE + EaseEvent.TYPE.CONTACT).register(this) {
+ if (it.isContactChange && it.event == DemoConstant.EVENT_UPDATE_SELF) {
+ updateProfile()
+ }
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE.name).register(this) {
+ if (it.isPresenceChange && it.message.equals(EaseIM.getCurrentUser()?.id) ) {
+ updateProfile()
+ }
+ }
+ }
+
+ override fun initListener() {
+ super.initListener()
+ binding?.titleContact?.setLogoClickListener {
+ EaseIM.getCurrentUser()?.id?.let {
+ presenceController.showPresenceStatusDialog(PresenceCache.getUserPresence(it))
+ }
+ }
+ }
+
+ private fun updateProfile(){
+ binding?.titleContact?.let { titlebar->
+ EaseIM.getConfig()?.avatarConfig?.setAvatarStyle(titlebar.getLogoView())
+ EaseIM.getConfig()?.avatarConfig?.setStatusStyle(titlebar.getStatusView(),2.dpToPx(mContext),
+ ContextCompat.getColor(mContext, R.color.demo_background))
+
+ EaseIM.getCurrentUser()?.let { profile->
+ val presence = PresenceCache.getUserPresence(profile.id)
+ presence?.let {
+ val logoStatus = EasePresenceUtil.getPresenceIcon(mContext,it)
+ titlebar.setLogoStatusMargin(end = -1, bottom = -1)
+ titlebar.setLogoStatus(logoStatus)
+ titlebar.getStatusView().visibility = View.VISIBLE
+ titlebar.setLogoStatusSize(resources.getDimensionPixelSize(R.dimen.em_title_bar_status_icon_size))
+ }
+ ChatLog.e(TAG,"updateProfile ${profile.id} ${profile.name} ${profile.avatar}")
+ titlebar.setLogo(profile.avatar, io.agora.uikit.R.drawable.ease_default_avatar, 32.dpToPx(mContext))
+ val layoutParams = titlebar.getLogoView()?.layoutParams as? ViewGroup.MarginLayoutParams
+ layoutParams?.marginStart = 12.dpToPx(mContext)
+ titlebar.getTitleView().let { text ->
+ text.text = ""
+ }
+ }
+ }
+ }
+
+ override fun loadContactListSuccess(userList: MutableList) {
+ super.loadContactListSuccess(userList)
+ if (!isFirstLoadData){
+ fetchContactInfo(userList)
+ isFirstLoadData = true
+ }
+ }
+
+ override fun loadContactListFail(code: Int, error: String) {
+ super.loadContactListFail(code, error)
+ ChatLog.e(TAG,"loadContactListFail: $code $error")
+ }
+
+ class Builder:EaseContactsListFragment.Builder() {
+ override fun build(): EaseContactsListFragment {
+ if (customFragment == null) {
+ customFragment = ChatContactListFragmentEvent()
+ }
+ if (customFragment is ChatContactListFragmentEvent){
+
+ }
+ return super.build()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatNewRequestsActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatNewRequestsActivity.kt
new file mode 100644
index 00000000..f313b53f
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/contact/ChatNewRequestsActivity.kt
@@ -0,0 +1,23 @@
+package io.agora.chatdemo.page.contact
+
+import io.agora.chatdemo.common.helper.LocalNotifyHelper
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatMessage
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.common.extensions.mainScope
+import io.agora.uikit.feature.invitation.EaseNewRequestsActivity
+import io.agora.uikit.model.EaseEvent
+
+class ChatNewRequestsActivity:EaseNewRequestsActivity() {
+
+ override fun agreeInviteSuccess(userId: String, msg: ChatMessage) {
+ super.agreeInviteSuccess(userId, msg)
+ val notifyMsg = LocalNotifyHelper.createContactNotifyMessage(userId)
+ ChatClient.getInstance().chatManager().saveMessage(notifyMsg)
+ mContext.let {
+ EaseFlowBus.with(EaseEvent.EVENT.ADD.name)
+ .post(it.mainScope(), EaseEvent(EaseEvent.EVENT.ADD.name, EaseEvent.TYPE.CONTACT))
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/conversation/ConversationListFragment.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/conversation/ConversationListFragment.kt
new file mode 100644
index 00000000..23c18ed3
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/conversation/ConversationListFragment.kt
@@ -0,0 +1,136 @@
+package io.agora.chatdemo.page.conversation
+
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.R
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.PresenceCache
+import io.agora.chatdemo.feature.presence.controller.PresenceController
+import io.agora.chatdemo.feature.presence.utils.EasePresenceUtil
+import io.agora.chatdemo.feature.presence.viewmodel.PresenceViewModel
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.common.extensions.dpToPx
+import io.agora.uikit.configs.setAvatarStyle
+import io.agora.uikit.configs.setStatusStyle
+import io.agora.uikit.feature.conversation.EaseConversationListFragment
+import io.agora.uikit.model.EaseConversation
+import io.agora.uikit.model.EaseEvent
+
+class ConversationListFragment: EaseConversationListFragment() {
+
+ private var isFirstLoadData = false
+ private val presenceViewModel by lazy { ViewModelProvider(this)[PresenceViewModel::class.java] }
+ private val presenceController by lazy { PresenceController(mContext,presenceViewModel) }
+
+ override fun initData() {
+ super.initData()
+ initEventBus()
+ }
+
+ override fun initView(savedInstanceState: Bundle?) {
+ super.initView(savedInstanceState)
+
+ binding?.titleConversations?.let {
+ EaseIM.getConfig()?.avatarConfig?.setAvatarStyle(it.getLogoView())
+ EaseIM.getConfig()?.avatarConfig?.setStatusStyle(it.getStatusView(),2.dpToPx(mContext),
+ ContextCompat.getColor(mContext, R.color.demo_background))
+ updateProfile()
+ it.setTitleEndDrawable(R.drawable.conversation_title)
+ }
+ }
+
+ private fun initEventBus() {
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE + EaseEvent.TYPE.CONTACT).register(this) {
+ if (it.isContactChange && it.event == DemoConstant.EVENT_UPDATE_SELF) {
+ updateProfile()
+ }
+ }
+
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE.name).register(this) {
+ if (it.isPresenceChange && it.message.equals(EaseIM.getCurrentUser()?.id) ) {
+ updateProfile()
+ }
+ }
+
+ EaseFlowBus.with(EaseEvent.EVENT.UPDATE + EaseEvent.TYPE.CONTACT + DemoConstant.EVENT_UPDATE_USER_SUFFIX).register(this) {
+ if (it.isContactChange && it.message.isNullOrEmpty().not()) {
+ binding?.listConversation?.notifyDataSetChanged()
+ }
+ }
+ EaseFlowBus.with(EaseEvent.EVENT.ADD.name).register(viewLifecycleOwner) {
+ if (it.isContactChange) {
+ refreshData()
+ }
+ }
+ }
+
+ override fun initListener() {
+ super.initListener()
+ binding?.titleConversations?.setLogoClickListener {
+ EaseIM.getCurrentUser()?.id?.let {
+ presenceController.showPresenceStatusDialog(PresenceCache.getUserPresence(it))
+ }
+ }
+ }
+
+ private fun updateProfile(){
+ binding?.titleConversations?.let { titlebar->
+ EaseIM.getCurrentUser()?.let { profile->
+ val presence = PresenceCache.getUserPresence(profile.id)
+ presence?.let {
+ val logoStatus = EasePresenceUtil.getPresenceIcon(mContext,it)
+ ChatLog.d("ConversationListFragment","logoStatus $logoStatus")
+ titlebar.setLogoStatusMargin(end = -1, bottom = -1)
+ titlebar.setLogoStatus(logoStatus)
+ titlebar.getStatusView().visibility = View.VISIBLE
+ titlebar.setLogoStatusSize(resources.getDimensionPixelSize(R.dimen.em_title_bar_status_icon_size))
+ }
+ ChatLog.e("ConversationListFragment","updateProfile ${profile.id} ${profile.name} ${profile.avatar}")
+ titlebar.setLogo(profile.avatar, io.agora.uikit.R.drawable.ease_default_avatar, 32.dpToPx(mContext))
+ val layoutParams = titlebar.getLogoView()?.layoutParams as? ViewGroup.MarginLayoutParams
+ layoutParams?.marginStart = 12.dpToPx(mContext)
+ titlebar.getTitleView().let { text ->
+ text.text = ""
+ }
+ }
+ }
+ }
+
+
+ override fun loadConversationListSuccess(userList: List) {
+ if (!isFirstLoadData){
+ fetchFirstVisibleData()
+ isFirstLoadData = true
+ }
+ }
+
+ private fun fetchFirstVisibleData(){
+ binding?.listConversation?.let { layout->
+ (layout.conversationList.layoutManager as? LinearLayoutManager)?.let { manager->
+ layout.post {
+ val firstVisibleItemPosition = manager.findFirstVisibleItemPosition()
+ val lastVisibleItemPosition = manager.findLastVisibleItemPosition()
+ val visibleList = layout.getListAdapter()?.mData?.filterIndexed { index, _ ->
+ index in firstVisibleItemPosition..lastVisibleItemPosition
+ }
+ val fetchList = visibleList?.filter { conv ->
+ val u = DemoHelper.getInstance().getDataModel().getUser(conv.conversationId)
+ (u == null || u.updateTimes == 0) && (u?.name.isNullOrEmpty() || u?.avatar.isNullOrEmpty())
+ }
+ fetchList?.let {
+ layout.fetchConvUserInfo(it)
+ }
+ }
+ }
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/ChatCreateGroupActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/ChatCreateGroupActivity.kt
new file mode 100644
index 00000000..e24a1d46
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/ChatCreateGroupActivity.kt
@@ -0,0 +1,32 @@
+package io.agora.chatdemo.page.group
+
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import io.agora.chatdemo.utils.ToastUtils.showToast
+import io.agora.chatdemo.viewmodel.ProfileInfoViewModel
+import io.agora.uikit.common.ChatGroup
+import io.agora.uikit.common.extensions.catchChatException
+import io.agora.uikit.feature.group.EaseCreateGroupActivity
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+class ChatCreateGroupActivity: EaseCreateGroupActivity() {
+ private val profileViewModel by lazy { ViewModelProvider(this)[ProfileInfoViewModel::class.java] }
+
+ override fun createGroupSuccess(group: ChatGroup) {
+ lifecycleScope.launch {
+ profileViewModel.getGroupAvatar(group.groupId)
+ .catchChatException { e ->
+ showToast(e.description)
+ }
+ .onStart {
+ showLoading(true)
+ }
+ .onCompletion { dismissLoading() }
+ .collect {
+ super.createGroupSuccess(group)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/ChatGroupDetailActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/ChatGroupDetailActivity.kt
new file mode 100644
index 00000000..16ad33d4
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/ChatGroupDetailActivity.kt
@@ -0,0 +1,61 @@
+package io.agora.chatdemo.page.group
+
+import androidx.core.content.ContextCompat
+import io.agora.chat.callkit.general.EaseCallType
+import io.agora.chatdemo.R
+import io.agora.chatdemo.callkit.CallKitManager
+import io.agora.chatdemo.common.extensions.internal.parse
+import io.agora.uikit.EaseIM
+import io.agora.uikit.common.ChatGroup
+import io.agora.uikit.feature.group.EaseGroupDetailActivity
+import io.agora.uikit.model.EaseMenuItem
+
+class ChatGroupDetailActivity : EaseGroupDetailActivity(){
+
+ override fun getDetailItem(): MutableList? {
+ val list = super.getDetailItem()
+ val voiceItem = EaseMenuItem(
+ title = getString(R.string.menu_voice_call),
+ resourceId = io.agora.uikit.R.drawable.ease_phone_pick,
+ menuId = R.id.group_item_voice_call,
+ titleColor = ContextCompat.getColor(this, R.color.color_primary),
+ order = 2,
+ resourceTintColor = ContextCompat.getColor(this, R.color.color_primary)
+ )
+ val videoItem = EaseMenuItem(
+ title = getString(R.string.menu_video_call),
+ resourceId = io.agora.uikit.R.drawable.ease_video_camera,
+ menuId = R.id.group_item_video_call,
+ titleColor = ContextCompat.getColor(this, R.color.color_primary),
+ order = 2,
+ resourceTintColor = ContextCompat.getColor(this, R.color.color_primary)
+ )
+ list?.add(voiceItem)
+ list?.add(videoItem)
+ return list
+ }
+
+ override fun onMenuItemClick(item: EaseMenuItem?, position: Int): Boolean {
+ item?.let {menu->
+ return when(menu.menuId){
+ R.id.group_item_video_call -> {
+ CallKitManager.startConferenceCall(EaseCallType.CONFERENCE_VIDEO_CALL,this, groupId)
+ true
+ }
+ R.id.group_item_voice_call -> {
+ CallKitManager.startConferenceCall(EaseCallType.CONFERENCE_VOICE_CALL,this, groupId)
+ true
+ }
+ else -> {
+ super.onMenuItemClick(item, position)
+ }
+ }
+ }
+ return false
+ }
+
+ override fun fetchGroupDetailSuccess(group: ChatGroup) {
+ EaseIM.updateGroupInfo(listOf(group.parse()))
+ super.fetchGroupDetailSuccess(group)
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/repository/GroupRepository.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/repository/GroupRepository.kt
new file mode 100644
index 00000000..67b92797
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/group/repository/GroupRepository.kt
@@ -0,0 +1,32 @@
+package io.agora.chatdemo.page.group.repository
+
+import io.agora.chatdemo.base.BaseRepository
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatException
+import io.agora.uikit.common.ChatGroup
+import io.agora.uikit.common.ChatValueCallback
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+class GroupRepository: BaseRepository() {
+ val groupManager = ChatClient.getInstance().groupManager()
+
+ /**
+ * Suspend method for [ChatGroupManager.asyncGetJoinedGroupsFromServer()]
+ */
+ suspend fun asyncGetJoinedGroupsFromServer(): List {
+ return suspendCoroutine { continuation->
+ groupManager.asyncGetJoinedGroupsFromServer(object : ChatValueCallback> {
+ override fun onSuccess(value: MutableList) {
+ continuation.resume(value)
+ }
+ override fun onError(error: Int, errorMsg: String?) {
+ continuation.resumeWithException(ChatException(error, errorMsg))
+ }
+ })
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/LoginActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/LoginActivity.kt
new file mode 100644
index 00000000..64929908
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/LoginActivity.kt
@@ -0,0 +1,61 @@
+package io.agora.chatdemo.page.login
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import io.agora.chatdemo.R
+import io.agora.chatdemo.base.BaseInitActivity
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.databinding.DemoActivityLoginBinding
+import io.agora.chatdemo.page.login.fragment.LoginFragment
+import io.agora.chatdemo.page.login.fragment.ServerSetFragment
+import io.agora.uikit.common.bus.EaseFlowBus
+
+class LoginActivity : BaseInitActivity() {
+
+ override fun initView(savedInstanceState: Bundle?) {
+ super.initView(savedInstanceState)
+ supportFragmentManager.beginTransaction().replace(R.id.fl_fragment, LoginFragment())
+ .commit()
+ }
+
+ override fun getViewBinding(inflater: LayoutInflater): DemoActivityLoginBinding? {
+ return DemoActivityLoginBinding.inflate(inflater)
+ }
+
+ override fun setActivityTheme() {
+ setFitSystemForTheme(false, ContextCompat.getColor(this, R.color.transparent), true)
+ }
+
+ override fun initData() {
+ super.initData()
+ initEvent()
+ }
+
+ private fun initEvent() {
+ EaseFlowBus.with(DemoConstant.SKIP_DEVELOPER_CONFIG).register(this) {
+ if (it == LoginFragment::class.java.simpleName) {
+ replace(ServerSetFragment())
+ }
+ }
+ }
+
+ private fun replace(fragment: Fragment) {
+ supportFragmentManager.beginTransaction().setCustomAnimations(
+ R.anim.slide_in_from_right,
+ R.anim.slide_out_to_left,
+ R.anim.slide_in_from_left,
+ R.anim.slide_out_to_right
+ ).replace(R.id.fl_fragment, fragment).addToBackStack(null).commit()
+ }
+
+ companion object {
+ fun startAction(context: Context) {
+ val intent = Intent(context, LoginActivity::class.java)
+ context.startActivity(intent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/fragment/LoginFragment.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/fragment/LoginFragment.kt
new file mode 100644
index 00000000..024d677e
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/fragment/LoginFragment.kt
@@ -0,0 +1,281 @@
+package io.agora.chatdemo.page.login.fragment
+
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.os.Bundle
+import android.os.SystemClock
+import android.text.Editable
+import android.text.InputType
+import android.text.TextUtils
+import android.text.TextWatcher
+import android.text.method.LinkMovementMethod
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import android.widget.TextView
+import android.widget.TextView.OnEditorActionListener
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.MainActivity
+import io.agora.chatdemo.R
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.dialog.SimpleDialog
+import io.agora.chatdemo.common.extensions.internal.changePwdDrawable
+import io.agora.chatdemo.common.extensions.internal.clearEditTextListener
+import io.agora.chatdemo.common.extensions.internal.showRightDrawable
+import io.agora.chatdemo.common.helper.DeveloperModeHelper
+import io.agora.chatdemo.databinding.DemoFragmentLoginBinding
+import io.agora.chatdemo.page.login.viewModel.LoginViewModel
+import io.agora.chatdemo.utils.ToastUtils.showToast
+import io.agora.uikit.base.EaseBaseFragment
+import io.agora.uikit.common.ChatClient
+import io.agora.uikit.common.ChatError
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.common.extensions.catchChatException
+import io.agora.uikit.common.extensions.hideSoftKeyboard
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class LoginFragment : EaseBaseFragment(),
+ View.OnClickListener, TextWatcher,OnEditorActionListener {
+ private var mUserId: String? = null
+ private var mCode: String? = null
+ private lateinit var mFragmentViewModel: LoginViewModel
+ private var clear: Drawable? = null
+ private var eyeOpen: Drawable? = null
+ private var eyeClose: Drawable? = null
+ private val mHits = LongArray(COUNT)
+ private var isDeveloperMode = false
+ private var isShowingDialog = false
+
+ override fun getViewBinding(
+ inflater: LayoutInflater,
+ container: ViewGroup?
+ ): DemoFragmentLoginBinding {
+ return DemoFragmentLoginBinding.inflate(inflater)
+ }
+
+ override fun initListener() {
+ super.initListener()
+ binding?.run {
+ etLoginId.addTextChangedListener(this@LoginFragment)
+ etLoginCode.addTextChangedListener(this@LoginFragment)
+ tvLoginDeveloper.setOnClickListener(this@LoginFragment)
+ tvVersion.setOnClickListener(this@LoginFragment)
+ btnLogin.setOnClickListener(this@LoginFragment)
+ etLoginCode.setOnEditorActionListener(this@LoginFragment)
+ etLoginId.clearEditTextListener()
+ root.setOnClickListener {
+ mContext.hideSoftKeyboard()
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ retainInstance = true
+ mFragmentViewModel = ViewModelProvider(this)[LoginViewModel::class.java]
+ }
+
+ override fun initData() {
+ super.initData()
+ binding?.run {
+ etLoginId.setText(ChatClient.getInstance().currentUser)
+ tvVersion.text = "V${ChatClient.VERSION}"
+ tvAgreement.movementMethod = LinkMovementMethod.getInstance()
+ tvAgreement.setHintTextColor(Color.TRANSPARENT)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val drawableCursor = etLoginId.textCursorDrawable
+ drawableCursor?.colorFilter = PorterDuffColorFilter(mContext.getColor(R.color.color_primary), PorterDuff.Mode.SRC_IN)
+ etLoginId.textCursorDrawable = drawableCursor
+ etLoginCode.textCursorDrawable = drawableCursor
+ val idDrawableIndicator = etLoginId.textSelectHandle
+ val codeDrawableIndicator = etLoginCode.textSelectHandle
+ idDrawableIndicator?.colorFilter = PorterDuffColorFilter(mContext.getColor(R.color.color_primary), PorterDuff.Mode.SRC_IN)
+ codeDrawableIndicator?.colorFilter = PorterDuffColorFilter(mContext.getColor(R.color.color_primary), PorterDuff.Mode.SRC_IN)
+ }
+
+ }
+ eyeClose = ContextCompat.getDrawable(mContext, R.drawable.sign_eye_slash)
+ eyeOpen = ContextCompat.getDrawable(mContext, R.drawable.sign_eye)
+ clear = ContextCompat.getDrawable(mContext, R.drawable.sign_clear_icon)
+ binding?.etLoginId?.showRightDrawable(clear)
+ binding?.etLoginCode?.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
+ isDeveloperMode = DeveloperModeHelper.isDeveloperMode()
+ resetView(isDeveloperMode)
+ }
+
+ override fun onClick(v: View) {
+ when (v.id) {
+ R.id.tv_version -> {
+ System.arraycopy(mHits, 1, mHits, 0, mHits.size - 1)
+ mHits[mHits.size - 1] = SystemClock.uptimeMillis()
+ if (mHits[0] >= SystemClock.uptimeMillis() - DURATION && !isShowingDialog) {
+ isShowingDialog = true
+ showOpenDeveloperDialog()
+ }
+ }
+ R.id.btn_login -> {
+ mContext.hideSoftKeyboard()
+ loginToServer()
+ }
+ R.id.tv_login_developer -> {
+ EaseFlowBus.with(DemoConstant.SKIP_DEVELOPER_CONFIG).post(lifecycleScope, LoginFragment::class.java.simpleName)
+ }
+
+ }
+ }
+
+ private fun loginToServer() {
+ if (TextUtils.isEmpty(mUserId) || TextUtils.isEmpty(mCode)) {
+ showToast(mContext.getString(R.string.em_login_btn_info_incomplete))
+ return
+ }
+ lifecycleScope.launch {
+ if (isDeveloperMode){
+ mFragmentViewModel.login(mUserId!!, mCode!!)
+ .onStart { showLoading(true) }
+ .onCompletion { dismissLoading() }
+ .catchChatException { e ->
+ if (e.errorCode == ChatError.USER_AUTHENTICATION_FAILED) {
+ showToast(R.string.demo_error_user_authentication_failed)
+ } else {
+ showToast(e.description)
+ }
+ }
+ .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(stopTimeoutMillis), null)
+ .collect {
+ if (it != null) {
+ DemoHelper.getInstance().getDataModel().initDb()
+ startActivity(Intent(mContext, MainActivity::class.java))
+ mContext.finish()
+ }
+ }
+ }else{
+ lifecycleScope.launch {
+ mFragmentViewModel.loginFromAppServer(mUserId!!, mCode!!)
+ .onStart {
+ showLoading(true)
+ }
+ .onCompletion {
+ dismissLoading()
+ }
+ .catchChatException { e ->
+ if (e.errorCode == ChatError.USER_AUTHENTICATION_FAILED) {
+ showToast(R.string.demo_error_user_authentication_failed)
+ } else {
+ showToast(e.description)
+ }
+ }
+ .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(stopTimeoutMillis), null)
+ .collect {
+ if (it != null) {
+ DemoHelper.getInstance().getDataModel().initDb()
+ startActivity(Intent(mContext, MainActivity::class.java))
+ mContext.finish()
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ private fun showOpenDeveloperDialog() {
+ SimpleDialog.Builder(mContext)
+ .setTitle(
+ if (isDeveloperMode) getString(R.string.server_close_develop_mode) else getString(
+ R.string.server_open_develop_mode
+ )
+ )
+ .setPositiveButton{
+ isDeveloperMode = !isDeveloperMode
+ DeveloperModeHelper.setDeveloperMode(isDeveloperMode)
+ binding?.etLoginId?.setText("")
+ resetView(isDeveloperMode)
+ }
+ .setOnDismissListener {
+ isShowingDialog = false
+ }
+ .setCanceledOnTouchOutside(false)
+ .build()
+ .show()
+ }
+
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
+ override fun afterTextChanged(s: Editable) {
+ binding?.run {
+ mUserId = etLoginId.text.toString().trim { it <= ' ' }
+ mCode = etLoginCode.text.toString().trim { it <= ' ' }
+ etLoginId.showRightDrawable(clear)
+ etLoginCode.showRightDrawable(eyeClose)
+ setButtonEnable(!TextUtils.isEmpty(mUserId) && !TextUtils.isEmpty(mCode))
+ }
+ }
+
+ private fun setButtonEnable(enable: Boolean) {
+ binding?.run {
+ btnLogin.isEnabled = enable
+ if (etLoginCode.hasFocus()) {
+ etLoginCode.imeOptions =
+ if (enable) EditorInfo.IME_ACTION_DONE else EditorInfo.IME_ACTION_PREVIOUS
+ } else if (etLoginId.hasFocus()) {
+ etLoginCode.imeOptions =
+ if (enable) EditorInfo.IME_ACTION_DONE else EditorInfo.IME_ACTION_NEXT
+ }
+ }
+ }
+
+ override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent): Boolean {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ if (!TextUtils.isEmpty(mUserId) && !TextUtils.isEmpty(mCode)) {
+ mContext.hideSoftKeyboard()
+ loginToServer()
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun resetView(isDeveloperMode: Boolean) {
+ binding?.run {
+ etLoginCode.setText("")
+ etLoginCode.changePwdDrawable(
+ eyeOpen,
+ eyeClose,
+ null,
+ null,
+ null
+ )
+ etLoginCode.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
+ etLoginCode.showRightDrawable(null)
+ if (!isDeveloperMode) {
+ tvLoginDeveloper.visibility = View.GONE
+ DeveloperModeHelper.setEnableCustom(false)
+ DeveloperModeHelper.setDeveloperMode(false)
+ }else{
+ tvLoginDeveloper.visibility = View.VISIBLE
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "LoginFragment"
+ private const val COUNT: Int = 5
+ private const val DURATION = (3 * 1000).toLong()
+ private const val stopTimeoutMillis: Long = 5000
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/fragment/ServerSetFragment.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/fragment/ServerSetFragment.kt
new file mode 100644
index 00000000..55120056
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/fragment/ServerSetFragment.kt
@@ -0,0 +1,221 @@
+package io.agora.chatdemo.page.login.fragment
+
+import android.os.Bundle
+import android.text.Editable
+import android.text.InputType
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.R
+import io.agora.chatdemo.common.dialog.SimpleDialog
+import io.agora.chatdemo.common.extensions.internal.addDefaultTextChangedListener
+import io.agora.chatdemo.common.helper.DeveloperModeHelper
+import io.agora.chatdemo.databinding.DemoFragmentServerSetBinding
+import io.agora.uikit.base.EaseBaseFragment
+import kotlin.system.exitProcess
+
+class ServerSetFragment: EaseBaseFragment() {
+
+ private val changeArray = BooleanArray(4)
+ private var isEnableCustomServer = false
+ override fun getViewBinding(
+ inflater: LayoutInflater,
+ container: ViewGroup?
+ ): DemoFragmentServerSetBinding? {
+ return DemoFragmentServerSetBinding.inflate(inflater)
+ }
+
+ override fun initView(savedInstanceState: Bundle?) {
+ super.initView(savedInstanceState)
+ binding?.toolbarServer?.let {
+ it.inflateMenu(R.menu.demo_server_set_menu)
+ enableSaveMenu(false)
+ }
+ binding?.etAppkey?.inputType = InputType.TYPE_CLASS_TEXT
+ }
+
+ override fun initListener() {
+ super.initListener()
+ binding?.toolbarServer?.setNavigationOnClickListener {
+ activity?.onBackPressed()
+ }
+ binding?.toolbarServer?.setOnMenuItemClickListener {
+ when(it.itemId) {
+ R.id.action_server_set_save -> {
+ saveSettings()
+ true
+ }
+ else -> false
+ }
+ }
+ binding?.etAppkey?.addDefaultTextChangedListener {
+ it?.let { s ->
+ changeArray[0] = s.isNotEmpty()
+ changeSaveMenu(s)
+ }
+ }
+ binding?.etServerAddress?.addDefaultTextChangedListener {
+ it?.let { s ->
+ changeArray[1] = s.isNotEmpty()
+ changeSaveMenu(s)
+ }
+ }
+ binding?.etServerPort?.addDefaultTextChangedListener {
+ it?.let { s ->
+ changeArray[2] = s.isNotEmpty()
+ changeSaveMenu(s)
+ }
+ }
+ binding?.etServerRest?.addDefaultTextChangedListener {
+ it?.let { s ->
+ changeArray[3] = s.isNotEmpty()
+ changeSaveMenu(s)
+ }
+ }
+ binding?.switchSpecifyServer?.setOnCheckedChangeListener { _, isChecked ->
+ isEnableCustomServer = isChecked
+ makeCustomServerItemEnable(isChecked)
+ }
+ }
+
+ private fun saveSettings() {
+ if (checkChange()) {
+ DemoHelper.getInstance().getDataModel().enableCustomSet(true)
+ DemoHelper.getInstance().getDataModel().enableCustomServer(isEnableCustomServer)
+ binding?.etAppkey?.text?.let {
+ if (it.isNotEmpty()) {
+ val appkey = it.toString().trim()
+ if (appkey.contains("#")) {
+ DemoHelper.getInstance().getDataModel().setCustomAppKey(it.toString().trim())
+ } else {
+ checkAppkeyDialog()
+ return
+ }
+
+ }
+ }
+ binding?.etServerAddress?.text?.let {
+ if (it.isNotEmpty()) {
+ DemoHelper.getInstance().getDataModel().setIMServer(it.toString().trim())
+ }
+ }
+ binding?.etServerPort?.text?.let {
+ if (it.isNotEmpty()) {
+ DemoHelper.getInstance().getDataModel().setIMServerPort(it.toString().trim().toInt())
+ }
+ }
+ binding?.etServerRest?.text?.let {
+ if (it.isNotEmpty()) {
+ DemoHelper.getInstance().getDataModel().setRestServer(it.toString().trim())
+ }
+ }
+ if (isEnableCustomServer && checkServerSettingChange()) {
+ showAlertDialog()
+ } else {
+ mContext.onBackPressed()
+ }
+ } else {
+ DemoHelper.getInstance().getDataModel().enableCustomSet(false)
+ mContext.onBackPressed()
+ }
+ }
+
+ private fun showAlertDialog() {
+ SimpleDialog.Builder(mContext)
+ .setTitle(getString(R.string.server_set_dialog_title))
+ .setSubtitle(getString(R.string.server_set_dialog_content))
+ .setPositiveButton(getString(R.string.server_set_dialog_confirm_button_text)) {
+ exitProcess(1)
+ }
+ .build()
+ .show()
+ }
+
+ private fun checkAppkeyDialog() {
+ SimpleDialog.Builder(mContext)
+ .setTitle(getString(R.string.server_set_illegal_appkey))
+ .setPositiveButton {
+ // do nothing
+ }
+ .dismissNegativeButton()
+ .build()
+ .show()
+ }
+
+ override fun initData() {
+ super.initData()
+ DeveloperModeHelper.isCustomSetEnable().let {
+ if (it) {
+ DemoHelper.getInstance().getDataModel().getCustomAppKey()?.let { appKey ->
+ binding?.etAppkey?.setText(appKey)
+ }
+ DemoHelper.getInstance().getDataModel().isCustomServerEnable().let { enable ->
+ isEnableCustomServer = enable
+ binding?.switchSpecifyServer?.isChecked = enable
+ makeCustomServerItemEnable(enable)
+ }
+ DemoHelper.getInstance().getDataModel().getIMServer()?.let { server ->
+ if (server.isEmpty().not()) {
+ binding?.etServerAddress?.setText(server)
+ }
+
+ }
+ DemoHelper.getInstance().getDataModel().getIMServerPort().let { port ->
+ if (port != 0) {
+ binding?.etServerPort?.setText(port.toString())
+ }
+ }
+ DemoHelper.getInstance().getDataModel().getRestServer()?.let { rest ->
+ if (rest.isEmpty().not()) {
+ binding?.etServerRest?.setText(rest)
+ }
+ }
+ }
+ }
+ makeCustomServerItemEnable(binding?.switchSpecifyServer?.isChecked ?: false)
+ }
+
+ private fun makeCustomServerItemEnable(enable: Boolean) {
+ binding?.etServerAddress?.isEnabled = enable
+ binding?.etServerPort?.isEnabled = enable
+ binding?.etServerRest?.isEnabled = enable
+ }
+
+ private fun changeSaveMenu(s: Editable) {
+ if (s.isNotEmpty()) {
+ enableSaveMenu()
+ } else {
+ enableSaveMenu(checkChange())
+ }
+ }
+
+ private fun checkChange(): Boolean {
+ changeArray.forEach {
+ if (it) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun checkServerSettingChange(): Boolean {
+ changeArray.forEachIndexed { index, b ->
+ if (index > 0 && b) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun enableSaveMenu(enable: Boolean = true) {
+ binding?.toolbarServer?.let {
+ if (enable) {
+ it.setMenuTitleColor(ContextCompat.getColor(mContext, R.color.color_primary))
+ } else {
+ it.setMenuTitleColor(ContextCompat.getColor(mContext, R.color.demo_on_background_high))
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/viewModel/LoginViewModel.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/viewModel/LoginViewModel.kt
new file mode 100644
index 00000000..8011c76d
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/login/viewModel/LoginViewModel.kt
@@ -0,0 +1,46 @@
+package io.agora.chatdemo.page.login.viewModel
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import io.agora.chatdemo.page.splash.repository.ChatClientRepository
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flatMapConcat
+import kotlinx.coroutines.flow.flow
+
+class LoginViewModel(application: Application) : AndroidViewModel(application) {
+ private val mRepository: ChatClientRepository = ChatClientRepository()
+
+ /**
+ * Login to Chat Server.
+ * @param userName
+ * @param pwd
+ * @param isTokenFlag
+ */
+ fun login(userName: String, pwd: String,agoraUid:Int = 0,isTokenFlag: Boolean = false) =
+ flow {
+ emit(mRepository.loginToServer(userName, pwd, agoraUid, isTokenFlag))
+ }
+
+ /**
+ * Login from app server.
+ * @param userName
+ * @param userPassword
+ */
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun loginFromAppServer(userName: String, userPassword: String) =
+ flow {
+ emit(mRepository.loginFromServer(userName, userPassword))
+ } .flatMapConcat { result ->
+ flow { emit(mRepository.loginToServer(result.chatUserName!!, result.token!!,result.agoraUid, true)) }
+ }
+
+ /**
+ * Logout from Chat server.
+ */
+ fun logout() =
+ flow {
+ emit(mRepository.logout(true))
+ }
+
+
+}
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/CameraAndCroppingController.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/CameraAndCroppingController.kt
new file mode 100644
index 00000000..59353467
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/CameraAndCroppingController.kt
@@ -0,0 +1,145 @@
+package io.agora.chatdemo.page.me
+
+import android.app.Activity
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import androidx.activity.result.ActivityResultLauncher
+import androidx.core.content.FileProvider
+import com.yalantis.ucrop.UCrop
+import io.agora.chatdemo.BuildConfig
+import io.agora.chatdemo.utils.CameraAndCropFileUtils
+import io.agora.uikit.common.ChatImageUtils
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.extensions.isSdcardExist
+import java.io.File
+
+class CameraAndCroppingController(
+ var context: Context
+) {
+ companion object {
+ const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.fileProvider"
+ }
+ private var cameraFile: File? = null
+ private var imageCropFile:File? = null
+
+ private var resultImageUri:Uri? = null
+ private var cropImageUri:Uri? = null
+
+ fun selectPicFromCamera(launcher: ActivityResultLauncher?){
+ val takePicture = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2){
+ if (takePicture.resolveActivity(context.packageManager) != null) {
+ val values = ContentValues()
+ values.put(MediaStore.Images.Media.TITLE, "MyPic")
+ values.put(
+ MediaStore.Images.Media.DESCRIPTION,
+ "Photo taken on " + System.currentTimeMillis()
+ )
+ resultImageUri = context.contentResolver.insert(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ values
+ )
+ takePicture.putExtra(MediaStore.EXTRA_OUTPUT, resultImageUri)
+ }
+ }else{
+ if (!isSdcardExist()) {
+ return
+ }
+ cameraFile = CameraAndCropFileUtils.createImageFile( context, false)
+ cameraFile?.let {
+ takePicture.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
+ // 如果是 11 以上系统 通过MediaStore获取 uri
+ ChatLog.e("CameraAndCroppingController","selectPicFromCamera version >= 11 putExtra: ${CameraAndCropFileUtils.uri}")
+ takePicture.putExtra(MediaStore.EXTRA_OUTPUT,CameraAndCropFileUtils.uri)
+ }else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
+ // 如果是 7.0 且低于 11 系统 需要使用FileProvider
+ takePicture.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ val imgUri = FileProvider.getUriForFile(context, AUTHORITY, it)
+ ChatLog.e("CameraAndCroppingController","selectPicFromCamera 7 <= version < 11 putExtra: $imgUri")
+ takePicture.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
+ }else{
+ // 低于 7.0 系统
+ ChatLog.e("CameraAndCroppingController","selectPicFromCamera version < 7 putExtra: ${Uri.fromFile(it)}")
+ takePicture.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(it))
+ }
+ }
+ }
+ launcher?.launch(takePicture)
+ }
+
+ fun resultForCamera(data: Intent?):Uri?{
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2){
+ return resultImageUri
+ }else{
+ cameraFile?.let {
+ //判断文件是否存在
+ if (it.exists()){
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
+ // 如果是 11 以上系统 通过MediaStore获取 uri
+ resultImageUri = CameraAndCropFileUtils.uri
+ ChatLog.e("CameraAndCroppingController","resultForCamera version >= 11 putExtra: $resultImageUri")
+ }else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
+ // 如果是 7.0 且低于 11 系统 需要使用FileProvider 创建一个content类型的Uri
+ resultImageUri = FileProvider.getUriForFile(context, AUTHORITY, it)
+ ChatLog.e("CameraAndCroppingController","resultForCamera 7 <= version < 11 putExtra: $resultImageUri")
+ }else{
+ resultImageUri = Uri.fromFile(it)
+ ChatLog.e("CameraAndCroppingController","resultForCamera version < 7 putExtra: $resultImageUri")
+ }
+ }
+ }
+ }
+ return resultImageUri
+ }
+
+ fun gotoCrop(sourceUri: Uri){
+ val values = ContentValues()
+ values.put(MediaStore.Images.Media.TITLE, "MyCrop")
+ values.put(
+ MediaStore.Images.Media.DESCRIPTION,
+ "Crop taken on " + System.currentTimeMillis()
+ )
+ cropImageUri = context.contentResolver.insert(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ values
+ )
+ cropImageUri?.let {
+ UCrop.of(sourceUri, it)
+ .withAspectRatio(1f, 1f)
+ .withMaxResultSize(500, 500)
+ .start(context as Activity)
+ }
+ }
+
+ fun resultForCropFile(data: Intent?):File?{
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2){
+ CameraAndCropFileUtils.getCropFile(context,cropImageUri)
+ }else{
+ if (imageCropFile != null && imageCropFile?.absolutePath != null){
+ imageCropFile?.let {
+ return it
+ }
+ }
+ }
+ return null
+ }
+
+ fun getImageCropUri():Uri?{
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2){
+ return cropImageUri
+ }else{
+ imageCropFile?.let {
+ val uri = Uri.parse(it.absolutePath)
+ return ChatImageUtils.checkDegreeAndRestoreImage(context, uri)
+ }
+ }
+ return null
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/AboutActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/AboutActivity.kt
new file mode 100644
index 00000000..f53a746d
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/AboutActivity.kt
@@ -0,0 +1,65 @@
+package io.agora.chatdemo.page.me.activity
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import io.agora.chatdemo.R
+import io.agora.chatdemo.databinding.DemoActivityAboutBinding
+import io.agora.uikit.EaseIM
+import io.agora.uikit.base.EaseBaseActivity
+import io.agora.uikit.common.ChatClient
+
+class AboutActivity: EaseBaseActivity(), View.OnClickListener {
+ companion object{
+ const val Documentation = "https://"
+ const val Platform = "/overview/product-overview?platform=web"
+ const val BaseUrl = "https://www."
+ }
+ override fun getViewBinding(inflater: LayoutInflater): DemoActivityAboutBinding? {
+ return DemoActivityAboutBinding.inflate(inflater)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ initView()
+ initListener()
+ }
+
+ private fun initView(){
+ binding.let {
+ it.tvVersion.text = getString(R.string.about_version, ChatClient.VERSION)
+ it.tvKitVersion.text = getString(R.string.about_uikit_version,EaseIM.version)
+ }
+ }
+
+ private fun initListener(){
+ binding.let {
+ it.titleBar.setNavigationOnClickListener{
+ mContext.onBackPressed()
+ }
+ it.arrowItemDocumentation.setOnClickListener(this)
+ it.arrowItemSales.setOnClickListener(this)
+ it.arrowItemDemoRepo.setOnClickListener(this)
+ it.arrowItemMore.setOnClickListener(this)
+ }
+ }
+
+ override fun onClick(v: View?) {
+ when(v?.id){
+ R.id.arrow_item_documentation -> {
+ WebViewActivity.actionStart(this@AboutActivity,WebViewLoadType.RemoteUrl,"$Documentation${binding.arrowItemDocumentation.getSubTitle()}$Platform")
+ }
+ R.id.arrow_item_sales -> {
+ WebViewActivity.actionStart(this@AboutActivity,WebViewLoadType.RemoteUrl,"$BaseUrl${binding.arrowItemSales.getSubTitle()}")
+ }
+ R.id.arrow_item_demo_repo -> {
+ WebViewActivity.actionStart(this@AboutActivity,WebViewLoadType.RemoteUrl,"$BaseUrl${binding.arrowItemDemoRepo.getSubTitle()}")
+ }
+ R.id.arrow_item_more -> {
+ WebViewActivity.actionStart(this@AboutActivity,WebViewLoadType.RemoteUrl,"$BaseUrl${binding.arrowItemMore.getSubTitle()}")
+ }
+ else -> {}
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/CurrencyActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/CurrencyActivity.kt
new file mode 100644
index 00000000..71d505ad
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/CurrencyActivity.kt
@@ -0,0 +1,141 @@
+package io.agora.chatdemo.page.me.activity
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.View
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatDelegate
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.R
+import io.agora.chatdemo.bean.LanguageType
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.PreferenceManager
+import io.agora.chatdemo.common.extensions.internal.setSwitchDefaultStyle
+import io.agora.chatdemo.databinding.DemoActivityCurrencyBinding
+import io.agora.uikit.EaseIM
+import io.agora.uikit.base.EaseBaseActivity
+
+class CurrencyActivity: EaseBaseActivity(),View.OnClickListener {
+ private var targetLanguage:String? = ""
+ override fun getViewBinding(inflater: LayoutInflater): DemoActivityCurrencyBinding {
+ return DemoActivityCurrencyBinding.inflate(inflater)
+ }
+ companion object {
+ private const val LANGUAGE_TYPE_TARGET = 1
+ private const val RESULT_CHOICE_TARGET_LANGUAGE = 101
+ private const val RESULT_LANGUAGE_TAG = "language_tag"
+ private const val RESULT_LANGUAGE_CODE = "language_code"
+ private const val LANGUAGE_TYPE = "language_type"
+ }
+
+ private val launcherToTargetLanguage: ActivityResultLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result -> onActivityResult(result, RESULT_CHOICE_TARGET_LANGUAGE) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ initView()
+ initListener()
+ }
+
+ private fun initView(){
+ initSwitch()
+ initLanguage()
+ }
+
+ private fun initListener(){
+ binding.let {
+ it.switchItemDark.setOnClickListener(this)
+ it.arrowItemFeature.setOnClickListener(this)
+ it.arrowItemTargetLanguage.setOnClickListener(this)
+ it.titleBar.setNavigationOnClickListener{
+ mContext.onBackPressed()
+ }
+ }
+ }
+
+ private fun initSwitch(){
+ val isBlack = DemoHelper.getInstance().getDataModel().getBoolean(DemoConstant.IS_BLACK_THEME)
+
+ val handler = Looper.myLooper()?.let { Handler(it) }
+
+ handler?.postDelayed({
+ binding.switchItemDark.setChecked(isBlack)
+ binding.switchItemDark.setSwitchDefaultStyle()
+ }, 200)
+
+ }
+
+ private fun initLanguage(){
+ val spTagLanguage = PreferenceManager.getValue(DemoConstant.TARGET_LANGUAGE, LanguageType.EN.value)
+
+ if (spTagLanguage == LanguageType.ZH.value){
+ targetLanguage = getString(R.string.currency_language_zh_cn)
+ }else if (spTagLanguage == LanguageType.EN.value){
+ targetLanguage = getString(R.string.currency_language_en)
+ }
+ updateLanguage()
+ }
+
+ private fun onActivityResult(result: ActivityResult, requestCode: Int) {
+ if (result.resultCode == Activity.RESULT_OK) {
+ result.data?.let {
+ if (it.hasExtra(RESULT_LANGUAGE_TAG) && it.hasExtra(RESULT_LANGUAGE_CODE)){
+ val tag = it.getStringExtra(RESULT_LANGUAGE_TAG)
+ val code = it.getStringExtra(RESULT_LANGUAGE_CODE)
+ when (requestCode) {
+ RESULT_CHOICE_TARGET_LANGUAGE -> {
+ targetLanguage = tag
+ code?.let { languageCode->
+ PreferenceManager.putValue(DemoConstant.TARGET_LANGUAGE, languageCode)
+ EaseIM.getConfig()?.chatConfig?.targetTranslationLanguage = languageCode
+ }
+ }
+ else -> {}
+ }
+ }
+ updateLanguage()
+ }
+ }
+ }
+
+ private fun changeTheme(isChecked: Boolean){
+ DemoHelper.getInstance().getDataModel().putBoolean(DemoConstant.IS_BLACK_THEME, isChecked)
+ AppCompatDelegate.setDefaultNightMode(if (isChecked) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
+ }
+
+ private fun updateLanguage(){
+ binding.let {
+ it.arrowItemTargetLanguage.setContent(targetLanguage?:"")
+ }
+ }
+
+ override fun onClick(v: View?) {
+ when(v?.id){
+ R.id.arrow_item_feature -> {
+ startActivity(Intent(this@CurrencyActivity,FeaturesActivity::class.java))
+ }
+ R.id.arrow_item_target_language -> {
+ val intent = Intent(this@CurrencyActivity, LanguageSettingActivity::class.java)
+ intent.putExtra(LANGUAGE_TYPE, LANGUAGE_TYPE_TARGET)
+ launcherToTargetLanguage.launch(intent)
+ }
+ R.id.switch_item_dark -> {
+ binding.switchItemDark.switch?.let { switch ->
+ val isChecked = switch.isChecked.not()
+ binding.switchItemDark.setChecked(isChecked)
+ changeTheme(isChecked)
+ }
+ }
+ else -> {}
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/EditUserNicknameActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/EditUserNicknameActivity.kt
new file mode 100644
index 00000000..2b93c08a
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/EditUserNicknameActivity.kt
@@ -0,0 +1,101 @@
+package io.agora.chatdemo.page.me.activity
+
+import android.content.Intent
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import androidx.core.content.ContextCompat
+import io.agora.chatdemo.R
+import io.agora.chatdemo.databinding.DemoActivityMeInformationEditBinding
+import io.agora.uikit.EaseIM
+import io.agora.uikit.base.EaseBaseActivity
+import io.agora.uikit.model.EaseProfile
+
+open class EditUserNicknameActivity: EaseBaseActivity() {
+ var selfProfile: EaseProfile? = null
+ private var newName:String = ""
+
+ companion object{
+ private const val RESULT_REFRESH = "isRefresh"
+ }
+
+ override fun getViewBinding(inflater: LayoutInflater): DemoActivityMeInformationEditBinding {
+ return DemoActivityMeInformationEditBinding.inflate(inflater)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ selfProfile = EaseIM.getCurrentUser()
+ initTitle()
+ initListener()
+ showKeyboard(binding.etName)
+ }
+
+ open fun initTitle(){
+ binding.run {
+ titleBar.setTitle(getString(R.string.main_about_me_information_edit_nick_name))
+ selfProfile?.let {
+ etName.setText(it.name)
+ }
+ etName.requestFocus()
+ showKeyboard(etName)
+ }
+ binding.inputNameCount.text = resources.getString(
+ R.string.main_about_me_information_change_name_count
+ ,selfProfile?.name?.length ?: 0)
+ }
+
+ open fun initListener(){
+ binding.titleBar.setNavigationOnClickListener { mContext.onBackPressed() }
+ binding.etName.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
+
+ }
+
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+
+ }
+
+ override fun afterTextChanged(s: Editable) {
+ val length = s.toString().trim().length
+ if (length == 0){
+ binding.inputNameCount.text =
+ resources.getString(R.string.main_about_me_information_change_name_count, 0)
+ }else{
+ binding.inputNameCount.text =
+ resources.getString(R.string.main_about_me_information_change_name_count, length)
+
+ }
+ updateSaveView(binding.etName.text.length)
+ }
+ })
+ binding.titleBar.setOnMenuItemClickListener { item ->
+ when (item?.itemId) {
+ io.agora.uikit.R.id.action_save -> {
+ updateUserInfo()
+ }
+
+ else -> {}
+ }
+ true
+ }
+ }
+
+ open fun updateSaveView(length: Int){
+ binding.titleBar.setMenuTitleColor(
+ ContextCompat.getColor(mContext,
+ if (length != 0) R.color.color_primary
+ else R.color.demo_on_background_high))
+ }
+
+ private fun updateUserInfo(){
+ newName = binding.etName.text.trim().toString()
+ val resultIntent = Intent()
+ resultIntent.putExtra(RESULT_REFRESH, true)
+ resultIntent.putExtra("nickname", newName)
+ setResult(RESULT_OK,resultIntent)
+ finish()
+ }
+
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/FeaturesActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/FeaturesActivity.kt
new file mode 100644
index 00000000..2c155d11
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/FeaturesActivity.kt
@@ -0,0 +1,89 @@
+package io.agora.chatdemo.page.me.activity
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.R
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.extensions.internal.setSwitchDefaultStyle
+import io.agora.chatdemo.databinding.DemoActivityFeaturesBinding
+import io.agora.uikit.EaseIM
+import io.agora.uikit.base.EaseBaseActivity
+
+class FeaturesActivity: EaseBaseActivity(),View.OnClickListener {
+ override fun getViewBinding(inflater: LayoutInflater): DemoActivityFeaturesBinding {
+ return DemoActivityFeaturesBinding.inflate(inflater)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ initView()
+ initListener()
+ }
+
+ fun initView(){
+ val enableTranslation = DemoHelper.getInstance().getDataModel().getBoolean(DemoConstant.FEATURES_TRANSLATION,true)
+ val enableThread = DemoHelper.getInstance().getDataModel().getBoolean(DemoConstant.FEATURES_THREAD,true)
+ val enableReaction = DemoHelper.getInstance().getDataModel().getBoolean(DemoConstant.FEATURES_REACTION,true)
+ val isTyping = DemoHelper.getInstance().getDataModel().getBoolean(DemoConstant.IS_TYPING_ON,true)
+ binding.switchItemTranslation.setChecked(enableTranslation)
+ binding.switchItemTranslation.setSwitchDefaultStyle()
+ binding.switchItemTopic.setChecked(enableThread)
+ binding.switchItemTopic.setSwitchDefaultStyle()
+ binding.switchItemReaction.setChecked(enableReaction)
+ binding.switchItemReaction.setSwitchDefaultStyle()
+ binding.switchItemTyping.setChecked(isTyping)
+ binding.switchItemTyping.setSwitchDefaultStyle()
+ }
+
+ fun initListener(){
+ binding.let {
+ it.titleBar.setNavigationOnClickListener{
+ mContext.onBackPressed()
+ }
+ it.switchItemTranslation.setOnClickListener(this)
+ it.switchItemTopic.setOnClickListener(this)
+ it.switchItemReaction.setOnClickListener(this)
+ it.switchItemTyping.setOnClickListener(this)
+ }
+ }
+
+ override fun onClick(v: View?) {
+ when(v?.id){
+ R.id.switch_item_translation -> {
+ binding.switchItemTranslation.switch?.let { switch ->
+ val isChecked = switch.isChecked.not()
+ binding.switchItemTranslation.setChecked(isChecked)
+ EaseIM.getConfig()?.chatConfig?.enableTranslationMessage = isChecked
+ DemoHelper.getInstance().getDataModel().putBoolean(DemoConstant.FEATURES_TRANSLATION,isChecked)
+ }
+ }
+ R.id.switch_item_topic -> {
+ binding.switchItemTopic.switch?.let { switch ->
+ val isChecked = switch.isChecked.not()
+ binding.switchItemTopic.setChecked(isChecked)
+ EaseIM.getConfig()?.chatConfig?.enableChatThreadMessage = isChecked
+ DemoHelper.getInstance().getDataModel().putBoolean(DemoConstant.FEATURES_THREAD,isChecked)
+ }
+ }
+ R.id.switch_item_reaction -> {
+ binding.switchItemReaction.switch?.let { switch ->
+ val isChecked = switch.isChecked.not()
+ binding.switchItemReaction.setChecked(isChecked)
+ EaseIM.getConfig()?.chatConfig?.enableMessageReaction = isChecked
+ DemoHelper.getInstance().getDataModel().putBoolean(DemoConstant.FEATURES_REACTION,isChecked)
+ }
+ }
+ R.id.switch_item_typing -> {
+ binding.switchItemTyping.switch?.let { switch ->
+ val isChecked = switch.isChecked.not()
+ binding.switchItemTyping.setChecked(isChecked)
+ EaseIM.getConfig()?.chatConfig?.enableChatTyping = isChecked
+ DemoHelper.getInstance().getDataModel().putBoolean(DemoConstant.IS_TYPING_ON,isChecked)
+ }
+ }
+ else -> {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/LanguageSettingActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/LanguageSettingActivity.kt
new file mode 100644
index 00000000..9a37b869
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/LanguageSettingActivity.kt
@@ -0,0 +1,196 @@
+package io.agora.chatdemo.page.me.activity
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Bundle
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.style.ForegroundColorSpan
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.agora.chatdemo.R
+import io.agora.chatdemo.bean.Language
+import io.agora.chatdemo.bean.LanguageType
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.PreferenceManager
+import io.agora.chatdemo.databinding.DemoActivityLanguageBinding
+import io.agora.chatdemo.interfaces.LanguageListItemSelectListener
+import io.agora.uikit.base.EaseBaseActivity
+
+class LanguageSettingActivity: EaseBaseActivity() {
+ private var tagList:MutableList = mutableListOf()
+ private var languageAdapter:LanguageAdapter? = null
+ private var languageTag:String = ""
+ private var languageCode:String = ""
+ private var languageType:Int = 0
+ private var selectedPosition: Int = -1
+
+ override fun getViewBinding(inflater: LayoutInflater): DemoActivityLanguageBinding {
+ return DemoActivityLanguageBinding.inflate(inflater)
+ }
+
+ companion object {
+ private const val RESULT_LANGUAGE_TAG = "language_tag"
+ private const val RESULT_LANGUAGE_CODE = "language_code"
+ private const val LANGUAGE_TYPE = "language_type"
+ private const val LANGUAGE_TYPE_TARGET = 1
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ intent.hasExtra(LANGUAGE_TYPE).apply {
+ languageType = intent.getIntExtra(LANGUAGE_TYPE,0)
+ }
+ initView()
+ initListener()
+ }
+
+ fun initView(){
+ defaultLanguage()
+ binding.let {
+ languageAdapter = LanguageAdapter(tagList)
+ val layoutManager = LinearLayoutManager(mContext)
+ it.rlSheetList.layoutManager = layoutManager
+ it.rlSheetList.adapter = this.languageAdapter
+ when(languageType){
+ LANGUAGE_TYPE_TARGET -> {
+ binding.titleBar.setTitle(getString(R.string.currency_target_language))
+ PreferenceManager.getValue(DemoConstant.TARGET_LANGUAGE,
+ LanguageType.EN.value).let { tag->
+ val index = tagList.indexOfFirst { language-> language.type.value == tag }
+ languageCode = tag
+ selectedPosition = index
+ if (selectedPosition != -1){
+ languageAdapter?.setSelectPosition(selectedPosition)
+ }
+ }
+ }
+ else -> {}
+ }
+ updateConfirm()
+ }
+ }
+
+ fun initListener(){
+ languageAdapter?.setLanguageListItemClickListener(object : LanguageListItemSelectListener{
+ override fun onSelectListener(position: Int,language:Language) {
+ languageTag = language.tag
+ languageCode = language.type.value
+ selectedPosition = position
+ updateConfirm()
+ }
+ })
+ binding.titleBar.setOnMenuItemClickListener { item->
+ when (item.itemId){
+ R.id.action_language_confirm -> {
+ chengLanguage()
+ }
+ else -> {}
+ }
+ true
+ }
+ binding.titleBar.setNavigationOnClickListener{
+ mContext.onBackPressed()
+ }
+
+ }
+
+ fun updateConfirm(){
+ binding.let {
+ it.titleBar.getToolBar().let { tb ->
+ tb.menu.findItem(R.id.action_language_confirm)?.let { menuItem->
+ menuItem.isVisible = true
+ menuItem.title?.let { tl ->
+ val spannable = SpannableString(menuItem.title)
+ spannable.setSpan(
+ ForegroundColorSpan(ContextCompat.getColor(mContext,
+ if (selectedPosition != -1){
+ R.color.color_primary
+ }else{
+ R.color.demo_on_background_high
+ })
+ )
+ , 0, tl.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ menuItem.title = spannable
+ }
+ }
+ }
+ }
+ }
+
+ private fun chengLanguage(){
+ val resultIntent = Intent()
+ resultIntent.putExtra(RESULT_LANGUAGE_TAG,languageTag)
+ resultIntent.putExtra(RESULT_LANGUAGE_CODE,languageCode)
+ setResult(RESULT_OK,resultIntent)
+ finish()
+ }
+
+ private fun defaultLanguage(){
+ tagList = mutableListOf(
+ Language(LanguageType.ZH,getString(R.string.currency_language_zh_cn)),
+ Language(LanguageType.EN,getString(R.string.currency_language_en))
+ )
+ }
+
+ class LanguageAdapter(
+ private val languageList: MutableList?,
+ ) : RecyclerView.Adapter(){
+ private lateinit var listener: LanguageListItemSelectListener
+ private var selectPosition:Int = -1
+
+ inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val textView: TextView = itemView.findViewById(R.id.language_tag)
+ val tagCb: CheckBox = itemView.findViewById(R.id.language_cb)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.ease_layout_language_item, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun getItemCount(): Int {
+ return languageList?.size ?: 0
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, @SuppressLint("RecyclerView") position: Int) {
+ languageList?.let {
+ holder.textView.text = it[position].tag
+ holder.tagCb.isChecked = selectPosition == position
+
+ holder.tagCb.isClickable = false
+
+ holder.itemView.setOnClickListener {
+ if (position == selectPosition) {
+ holder.tagCb.isChecked = false
+ selectPosition = -1
+ }else{
+ holder.tagCb.isChecked = true
+ selectPosition = position
+ }
+ notifyDataSetChanged()
+
+ if (holder.tagCb.isChecked){
+ listener.onSelectListener(position,languageList[selectPosition])
+ }
+ }
+ }
+ }
+
+ fun setSelectPosition(selectPosition:Int){
+ this.selectPosition = selectPosition
+ notifyDataSetChanged()
+ }
+
+ fun setLanguageListItemClickListener(listener: LanguageListItemSelectListener){
+ this.listener = listener
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/NotifyActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/NotifyActivity.kt
new file mode 100644
index 00000000..75cf1be7
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/NotifyActivity.kt
@@ -0,0 +1,104 @@
+package io.agora.chatdemo.page.me.activity
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.databinding.DemoActivityNotifyBinding
+import io.agora.chatdemo.viewmodel.PushViewModel
+import io.agora.uikit.base.EaseBaseActivity
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.ChatPushRemindType
+import io.agora.uikit.common.extensions.catchChatException
+import kotlinx.coroutines.launch
+
+class NotifyActivity: EaseBaseActivity() {
+
+ private var pushViewModel: PushViewModel? = null
+
+ override fun getViewBinding(inflater: LayoutInflater): DemoActivityNotifyBinding? {
+ return DemoActivityNotifyBinding.inflate(inflater)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ initView()
+ initListener()
+ initData()
+ }
+
+ private fun initData() {
+ pushViewModel = ViewModelProvider(this)[PushViewModel::class.java]
+ pushViewModel?.let {
+ lifecycleScope.launch {
+ it.getSilentModeForApp()
+ .catchChatException {
+ ChatLog.e("notify", "initData: ${it.description}")
+ }
+ .collect {
+ it.remindType?.let { remindType ->
+ if (remindType == ChatPushRemindType.NONE) {
+ DemoHelper.getInstance().getDataModel().setAppPushSilent(true)
+ binding.switchItemNotify.setChecked(true)
+ } else {
+ DemoHelper.getInstance().getDataModel().setAppPushSilent(false)
+ binding.switchItemNotify.setChecked(false)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun initView(){
+ initSwitch()
+ }
+
+ private fun initSwitch(){
+// binding.switchItemNotify.setSwitchTarckDrawable(com.hyphenate.easeui.R.drawable.ease_switch_track_selector)
+// binding.switchItemNotify.setSwitchThumbDrawable(com.hyphenate.easeui.R.drawable.ease_switch_thumb_selector)
+ }
+
+ private fun initListener(){
+ binding.let {
+ it.titleBar.setNavigationOnClickListener{
+ mContext.onBackPressed()
+ }
+ it.switchItemNotify.setOnClickListener {
+ binding.switchItemNotify.switch?.let { switch ->
+ val isChecked = switch.isChecked
+ changeAppSilentModel(isChecked.not())
+ }
+ }
+ }
+ }
+
+ private fun changeAppSilentModel(checked: Boolean) {
+ lifecycleScope.launch {
+ if (checked) {
+ pushViewModel?.let {
+ it.setSilentModeForApp()
+ .catchChatException {
+ ChatLog.e("notify", "changeAppSilentModel: ${it.description}")
+ }
+ .collect {
+ DemoHelper.getInstance().getDataModel().setAppPushSilent(true)
+ binding.switchItemNotify.setChecked(true)
+ }
+ }
+ } else {
+ pushViewModel?.let {
+ it.clearSilentModeForApp()
+ .catchChatException {
+ ChatLog.e("notify", "changeAppSilentModel: ${it.description}")
+ }
+ .collect {
+ DemoHelper.getInstance().getDataModel().setAppPushSilent(false)
+ binding.switchItemNotify.setChecked(false)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/UserInformationActivity.kt b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/UserInformationActivity.kt
new file mode 100644
index 00000000..2693bb17
--- /dev/null
+++ b/app-kotlin/src/main/kotlin/io/agora/chatdemo/page/me/activity/UserInformationActivity.kt
@@ -0,0 +1,409 @@
+package io.agora.chatdemo.page.me.activity
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import coil.load
+import com.yalantis.ucrop.UCrop
+import io.agora.chatdemo.DemoHelper
+import io.agora.chatdemo.R
+import io.agora.chatdemo.common.DemoConstant
+import io.agora.chatdemo.common.helper.DeveloperModeHelper
+import io.agora.chatdemo.databinding.DemoActivityMeInformationBinding
+import io.agora.chatdemo.page.me.CameraAndCroppingController
+import io.agora.chatdemo.utils.CameraAndCropFileUtils
+import io.agora.chatdemo.viewmodel.ProfileInfoViewModel
+import io.agora.uikit.EaseIM
+import io.agora.uikit.base.EaseBaseActivity
+import io.agora.uikit.common.ChatImageUtils
+import io.agora.uikit.common.ChatLog
+import io.agora.uikit.common.bus.EaseFlowBus
+import io.agora.uikit.common.dialog.SimpleListSheetDialog
+import io.agora.uikit.common.extensions.catchChatException
+import io.agora.uikit.common.extensions.dpToPx
+import io.agora.uikit.common.extensions.mainScope
+import io.agora.uikit.common.extensions.showToast
+import io.agora.uikit.common.permission.PermissionCompat
+import io.agora.uikit.common.utils.EaseCompat
+import io.agora.uikit.common.utils.EaseFileUtils
+import io.agora.uikit.configs.setAvatarStyle
+import io.agora.uikit.interfaces.SimpleListSheetItemClickListener
+import io.agora.uikit.model.EaseEvent
+import io.agora.uikit.model.EaseMenuItem
+import io.agora.uikit.model.EaseProfile
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import java.io.File
+
+
+class UserInformationActivity: EaseBaseActivity(),
+ View.OnClickListener {
+
+ private val cameraAndCroppingController: CameraAndCroppingController by lazy {
+ CameraAndCroppingController(mContext)
+ }
+ private var selfProfile: EaseProfile? = null
+ private var showSelectDialog: SimpleListSheetDialog? = null
+ private var imageUri:Uri?= null
+ private lateinit var model: ProfileInfoViewModel
+
+ override fun getViewBinding(inflater: LayoutInflater): DemoActivityMeInformationBinding? {
+ return DemoActivityMeInformationBinding.inflate(inflater)
+ }
+ companion object {
+ private const val REQUEST_CODE_STORAGE_PICTURE = 111
+ private const val REQUEST_CODE_CAMERA = 112
+ private const val REQUEST_CODE_LOCAL_EDIT = 113
+ private const val RESULT_CODE_CAMERA = 114
+ private const val RESULT_CODE_LOCAL = 115
+ private const val RESULT_CODE_UPDATE_NAME = 116
+ private const val RESULT_REFRESH = "isRefresh"
+ }
+
+ private val requestCameraPermission: ActivityResultLauncher> =
+ registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { result ->
+ onRequestResult(
+ result,
+ REQUEST_CODE_CAMERA
+ )
+ }
+
+ private val requestImagePermission: ActivityResultLauncher> =
+ registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { result ->
+ onRequestResult(
+ result,
+ REQUEST_CODE_STORAGE_PICTURE
+ )
+ }
+
+ private val launcherToCamera: ActivityResultLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result -> onActivityResult(result, RESULT_CODE_CAMERA)
+ }
+ private val launcherToAlbum: ActivityResultLauncher