davy66666 1 сар өмнө
commit
f7e5bb9904
100 өөрчлөгдсөн 8224 нэмэгдсэн , 0 устгасан
  1. 68 0
      OUIApplet/build.gradle
  2. 21 0
      OUIApplet/proguard-rules.pro
  3. 26 0
      OUIApplet/src/androidTest/java/com/example/ouiapplet/ExampleInstrumentedTest.java
  4. 11 0
      OUIApplet/src/main/AndroidManifest.xml
  5. 21 0
      OUIApplet/src/main/java/io/openim/android/ouiapplet/AppletActivity.java
  6. 44 0
      OUIApplet/src/main/java/io/openim/android/ouiapplet/AppletFragment.java
  7. 21 0
      OUIApplet/src/main/java/io/openim/android/ouiapplet/service/NetService.java
  8. 36 0
      OUIApplet/src/main/java/io/openim/android/ouiapplet/vm/AppletVM.java
  9. 57 0
      OUIApplet/src/main/res/layout/activity_applet.xml
  10. 66 0
      OUIApplet/src/main/res/layout/fragment_applet.xml
  11. BIN
      OUIApplet/src/main/res/mipmap-xhdpi/ic_apple_null.png
  12. 16 0
      OUIApplet/src/main/res/values-night/themes.xml
  13. 10 0
      OUIApplet/src/main/res/values/colors.xml
  14. 2 0
      OUIApplet/src/main/res/values/refs.xml
  15. 3 0
      OUIApplet/src/main/res/values/strings.xml
  16. 16 0
      OUIApplet/src/main/res/values/themes.xml
  17. 17 0
      OUIApplet/src/test/java/com/example/ouiapplet/ExampleUnitTest.java
  18. 80 0
      OUICalling/build.gradle
  19. BIN
      OUICalling/libs/dsahjkenw2.aar
  20. 21 0
      OUICalling/proguard-rules.pro
  21. 26 0
      OUICalling/src/androidTest/java/io/openim/android/ouilive/ExampleInstrumentedTest.java
  22. 28 0
      OUICalling/src/main/AndroidManifest.xml
  23. 512 0
      OUICalling/src/main/java/io/openim/android/ouicalling/CallDialog.java
  24. 352 0
      OUICalling/src/main/java/io/openim/android/ouicalling/CallingServiceImp.java
  25. 424 0
      OUICalling/src/main/java/io/openim/android/ouicalling/GroupCallDialog.java
  26. 53 0
      OUICalling/src/main/java/io/openim/android/ouicalling/LockPushActivity.java
  27. 98 0
      OUICalling/src/main/java/io/openim/android/ouicalling/service/AudioVideoService.java
  28. 452 0
      OUICalling/src/main/java/io/openim/android/ouicalling/vm/CallViewModel.kt
  29. 329 0
      OUICalling/src/main/java/io/openim/android/ouicalling/vm/CallingVM.java
  30. 6 0
      OUICalling/src/main/res/drawable/selector_camera.xml
  31. 6 0
      OUICalling/src/main/res/drawable/selector_mic.xml
  32. 6 0
      OUICalling/src/main/res/drawable/selector_speaker.xml
  33. 18 0
      OUICalling/src/main/res/layout/activity_demo.xml
  34. 330 0
      OUICalling/src/main/res/layout/dialog_call.xml
  35. 311 0
      OUICalling/src/main/res/layout/dialog_group_call.xml
  36. 60 0
      OUICalling/src/main/res/layout/item_member_renderer.xml
  37. 43 0
      OUICalling/src/main/res/layout/layout_call_invite.xml
  38. 47 0
      OUICalling/src/main/res/layout/layout_float_view.xml
  39. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_answer.png
  40. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_close_camera.png
  41. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_hang_up.png
  42. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_mic_off.png
  43. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_mic_on.png
  44. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_mic_s_off.png
  45. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_mic_s_on.png
  46. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_open_camera.png
  47. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_speaker_off.png
  48. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_speaker_on.png
  49. BIN
      OUICalling/src/main/res/mipmap-xxhdpi/ic_switch_camera.png
  50. BIN
      OUICalling/src/main/res/raw/incoming_call_ring.mp3
  51. BIN
      OUICalling/src/main/res/raw/outgoing_call_ring.mp3
  52. 17 0
      OUICalling/src/test/java/io/openim/android/ouilive/ExampleUnitTest.java
  53. 68 0
      OUIContact/build.gradle
  54. 0 0
      OUIContact/consumer-rules.pro
  55. 21 0
      OUIContact/proguard-rules.pro
  56. 26 0
      OUIContact/src/androidTest/java/io/openim/android/ouicontact/ExampleInstrumentedTest.java
  57. 53 0
      OUIContact/src/main/AndroidManifest.xml
  58. 17 0
      OUIContact/src/main/debug/AndroidManifest.xml
  59. 19 0
      OUIContact/src/main/java/io/openim/android/ouicontact/DebugActivity.java
  60. 90 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/AddRelationActivity.java
  61. 269 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/AllFriendActivity.java
  62. 106 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/CreateLabelActivity.java
  63. 120 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/ForwardToActivity.java
  64. 37 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/FriendRequestDetailActivity.java
  65. 35 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/GroupNoticeDetailActivity.java
  66. 36 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/GroupNoticeInvitedDetailActivity.java
  67. 131 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/GroupNoticeListActivity.java
  68. 112 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/LabelActivity.java
  69. 157 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/MyGroupActivity.java
  70. 117 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/NewFriendActivity.java
  71. 47 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/adapter/ContactAdapter.java
  72. 215 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/fragment/ContactFragment.java
  73. 140 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/fragment/ContactFragment2.java
  74. 171 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/fragment/FriendFragment.java
  75. 92 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/fragment/GroupFragment.java
  76. 7 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/search/GroupInfoExpand.java
  77. 160 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/search/SearchGroupActivity.java
  78. 266 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/search/SearchGroupAndFriendsActivity.java
  79. 160 0
      OUIContact/src/main/java/io/openim/android/ouicontact/ui/search/SearchGroupMemberActivity.java
  80. 109 0
      OUIContact/src/main/java/io/openim/android/ouicontact/vm/ContactVM.java
  81. 96 0
      OUIContact/src/main/java/io/openim/android/ouicontact/vm/LabelVM.java
  82. 52 0
      OUIContact/src/main/java/io/openim/android/ouicontact/vm/SearchGroup.java
  83. BIN
      OUIContact/src/main/res/drawable/ic_back.png
  84. BIN
      OUIContact/src/main/res/drawable/ic_close.png
  85. BIN
      OUIContact/src/main/res/drawable/ic_open.png
  86. BIN
      OUIContact/src/main/res/drawable/ic_photo_2.png
  87. 240 0
      OUIContact/src/main/res/layout/activity_add_relation.xml
  88. 96 0
      OUIContact/src/main/res/layout/activity_all_friend.xml
  89. 121 0
      OUIContact/src/main/res/layout/activity_capture.xml
  90. 133 0
      OUIContact/src/main/res/layout/activity_create_label.xml
  91. 15 0
      OUIContact/src/main/res/layout/activity_debug.xml
  92. 123 0
      OUIContact/src/main/res/layout/activity_forward_to.xml
  93. 162 0
      OUIContact/src/main/res/layout/activity_friend_request_detail.xml
  94. 172 0
      OUIContact/src/main/res/layout/activity_group_notice_detail.xml
  95. 132 0
      OUIContact/src/main/res/layout/activity_group_notice_invite_detail.xml
  96. 55 0
      OUIContact/src/main/res/layout/activity_group_notice_list.xml
  97. 87 0
      OUIContact/src/main/res/layout/activity_label.xml
  98. 188 0
      OUIContact/src/main/res/layout/activity_my_group.xml
  99. 67 0
      OUIContact/src/main/res/layout/activity_new_friend.xml
  100. 50 0
      OUIContact/src/main/res/layout/activity_often_serch.xml

+ 68 - 0
OUIApplet/build.gradle

@@ -0,0 +1,68 @@
+plugins {
+    id 'org.jetbrains.kotlin.android'
+}
+if (isModule) {
+    apply plugin: 'com.android.application'
+} else {
+    apply plugin: 'com.android.library'
+}
+
+android {
+    viewBinding {
+        enabled = true
+    }
+    dataBinding{
+        enabled = true
+    }
+
+
+    compileSdk rootProject.ext.androidConfig.compileSdk
+
+    defaultConfig {
+        if (isModule) {
+            applicationId rootProject.ext.applicationId.OUIApplet
+            sourceSets {
+                main {
+                    // 组件模式下调试
+                    manifest.srcFile 'src/main/debug/AndroidManifest.xml'
+                }
+            }
+        }
+        minSdk rootProject.ext.androidConfig.minSdk
+        targetSdk rootProject.ext.androidConfig.targetSdk
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+
+        javaCompileOptions {
+            annotationProcessorOptions {
+                arguments = [AROUTER_MODULE_NAME: project.getName()]
+            }
+        }
+    }
+
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation 'androidx.appcompat:appcompat:1.3.0'
+    implementation 'com.google.android.material:material:1.4.0'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+    implementation project(path: ':OUICore')
+    implementation 'com.alibaba:arouter-api:1.5.2'
+    annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'
+}

+ 21 - 0
OUIApplet/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

+ 26 - 0
OUIApplet/src/androidTest/java/com/example/ouiapplet/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package com.example.ouiapplet;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("com.example.ouiapplet", appContext.getPackageName());
+    }
+}

+ 11 - 0
OUIApplet/src/main/AndroidManifest.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.openim.android.ouiapplet">
+
+    <application>
+        <activity
+            android:name=".AppletActivity"
+            android:exported="false" />
+    </application>
+
+</manifest>

+ 21 - 0
OUIApplet/src/main/java/io/openim/android/ouiapplet/AppletActivity.java

@@ -0,0 +1,21 @@
+package io.openim.android.ouiapplet;
+
+import android.os.Bundle;
+
+import androidx.lifecycle.Observer;
+
+import io.openim.android.ouiapplet.databinding.ActivityAppletBinding;
+import io.openim.android.ouicore.base.BasicActivity;
+import io.openim.android.ouicore.base.vm.injection.Easy;
+import io.openim.android.ouicore.vm.UserLogic;
+
+public class AppletActivity extends BasicActivity<ActivityAppletBinding> {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        viewBinding(ActivityAppletBinding.inflate(getLayoutInflater()));
+        Easy.find(UserLogic.class).discoverPageURL.observe(this,
+            s -> view.webView.loadUrl(s));
+    }
+}

+ 44 - 0
OUIApplet/src/main/java/io/openim/android/ouiapplet/AppletFragment.java

@@ -0,0 +1,44 @@
+package io.openim.android.ouiapplet;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.alibaba.android.arouter.facade.annotation.Route;
+
+import io.openim.android.ouiapplet.databinding.ActivityAppletBinding;
+import io.openim.android.ouiapplet.databinding.FragmentAppletBinding;
+import io.openim.android.ouiapplet.vm.AppletVM;
+import io.openim.android.ouicore.base.BaseFragment;
+import io.openim.android.ouicore.base.vm.injection.Easy;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.vm.UserLogic;
+
+
+@Route(path = Routes.Applet.HOME)
+public class AppletFragment extends BaseFragment {
+    private ActivityAppletBinding view;
+    private AppletVM appletVM;
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        view=ActivityAppletBinding.inflate(getLayoutInflater());
+
+        initView();
+        return view.getRoot();
+    }
+
+    private void initView() {
+        Easy.find(UserLogic.class).discoverPageURL
+            .observe(getViewLifecycleOwner(),
+            s -> view.webView.loadUrl(s));
+    }
+
+
+}

+ 21 - 0
OUIApplet/src/main/java/io/openim/android/ouiapplet/service/NetService.java

@@ -0,0 +1,21 @@
+package io.openim.android.ouiapplet.service;
+
+import java.util.Random;
+
+import io.openim.android.ouicore.net.RXRetrofit.Parameter;
+import io.reactivex.Observable;
+import okhttp3.RequestBody;
+import okhttp3.ResponseBody;
+import retrofit2.http.Body;
+import retrofit2.http.Header;
+import retrofit2.http.POST;
+import retrofit2.http.Url;
+
+public interface NetService {
+    /**
+     * 获取小程序列表
+     */
+    @POST("/applet/find")
+    Observable<ResponseBody> findApplet(@Body RequestBody requestBody);
+
+}

+ 36 - 0
OUIApplet/src/main/java/io/openim/android/ouiapplet/vm/AppletVM.java

@@ -0,0 +1,36 @@
+package io.openim.android.ouiapplet.vm;
+
+import java.util.HashMap;
+
+import io.openim.android.ouiapplet.service.NetService;
+import io.openim.android.ouicore.base.vm.injection.BaseVM;
+import io.openim.android.ouicore.net.RXRetrofit.N;
+import io.openim.android.ouicore.net.RXRetrofit.NetObserver;
+import io.openim.android.ouicore.net.RXRetrofit.Parameter;
+import io.openim.android.ouicore.api.OneselfService;
+import io.openim.android.ouicore.utils.L;
+
+public class AppletVM extends BaseVM {
+
+    public void findApplet() {
+        N.API(NetService.class)
+            .findApplet(new  Parameter().buildJsonBody())
+            .map(OneselfService.turn(HashMap.class))
+            .compose(N.IOMain())
+            .subscribe(new NetObserver<HashMap>("") {
+
+
+                @Override
+                public void onSuccess(HashMap o) {
+                    o.get("applets");
+                    L.e("");
+
+                }
+
+                @Override
+                protected void onFailure(Throwable e) {
+                    L.e("");
+                }
+            });
+    }
+}

+ 57 - 0
OUIApplet/src/main/res/layout/activity_applet.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout>
+    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/white"
+        android:fitsSystemWindows="true"
+        tools:context=".AppletActivity">
+
+        <RelativeLayout
+            android:gravity="center_horizontal"
+            android:layout_width="match_parent"
+            android:background="@color/theme_bg"
+            android:layout_height="match_parent">
+            <androidx.cardview.widget.CardView
+                android:id="@+id/title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/white"
+                app:cardElevation="0dp">
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:gravity="center_vertical"
+                    android:orientation="horizontal"
+                    android:paddingLeft="22dp"
+                    android:paddingTop="20dp"
+                    android:paddingRight="22dp"
+                    android:paddingBottom="10dp">
+
+                    <TextView
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="1"
+                        android:text="@string/find"
+                        android:textColor="#ff1b72ec"
+                        android:textSize="22sp" />
+
+                </LinearLayout>
+            </androidx.cardview.widget.CardView>
+
+            <WebView
+                android:id="@+id/webView"
+                android:layout_below="@id/title"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                />
+
+        </RelativeLayout>
+
+
+    </FrameLayout>
+</layout>
+

+ 66 - 0
OUIApplet/src/main/res/layout/fragment_applet.xml

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/white"
+    android:fitsSystemWindows="true"
+    tools:context=".AppletActivity">
+
+    <RelativeLayout
+        android:gravity="center_horizontal"
+        android:layout_width="match_parent"
+        android:background="@color/theme_bg"
+        android:layout_height="match_parent">
+        <androidx.cardview.widget.CardView
+            android:id="@+id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/white"
+            app:cardElevation="0dp">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center_vertical"
+                android:orientation="horizontal"
+                android:paddingLeft="22dp"
+                android:paddingTop="20dp"
+                android:paddingRight="22dp"
+                android:paddingBottom="10dp">
+
+                <TextView
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:text="@string/find"
+                    android:textColor="#ff1b72ec"
+                    android:textSize="22sp" />
+
+            </LinearLayout>
+        </androidx.cardview.widget.CardView>
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/recyclerView"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_below="@id/title" />
+
+        <CheckBox
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/title"
+            android:layout_centerHorizontal="true"
+            android:layout_marginTop="30dp"
+            android:button="@null"
+            android:drawableTop="@mipmap/ic_apple_null"
+            android:drawablePadding="22dp"
+            android:gravity="center"
+            android:text="@string/null_applet"
+            android:textColor="@color/txt_shallow"
+            android:textSize="16sp" />
+    </RelativeLayout>
+
+
+</FrameLayout>

BIN
OUIApplet/src/main/res/mipmap-xhdpi/ic_apple_null.png


+ 16 - 0
OUIApplet/src/main/res/values-night/themes.xml

@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Theme.Demo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/purple_200</item>
+        <item name="colorPrimaryVariant">@color/purple_700</item>
+        <item name="colorOnPrimary">@color/black</item>
+        <!-- Secondary brand color. -->
+        <item name="colorSecondary">@color/teal_200</item>
+        <item name="colorSecondaryVariant">@color/teal_200</item>
+        <item name="colorOnSecondary">@color/black</item>
+        <!-- Status bar color. -->
+        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
+        <!-- Customize your theme here. -->
+    </style>
+</resources>

+ 10 - 0
OUIApplet/src/main/res/values/colors.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>

+ 2 - 0
OUIApplet/src/main/res/values/refs.xml

@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources></resources>

+ 3 - 0
OUIApplet/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">OUIApplet</string>
+</resources>

+ 16 - 0
OUIApplet/src/main/res/values/themes.xml

@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Theme.Demo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/purple_500</item>
+        <item name="colorPrimaryVariant">@color/purple_700</item>
+        <item name="colorOnPrimary">@color/white</item>
+        <!-- Secondary brand color. -->
+        <item name="colorSecondary">@color/teal_200</item>
+        <item name="colorSecondaryVariant">@color/teal_700</item>
+        <item name="colorOnSecondary">@color/black</item>
+        <!-- Status bar color. -->
+        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
+        <!-- Customize your theme here. -->
+    </style>
+</resources>

+ 17 - 0
OUIApplet/src/test/java/com/example/ouiapplet/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.example.ouiapplet;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 80 - 0
OUICalling/build.gradle

@@ -0,0 +1,80 @@
+if (isModule) {
+    apply plugin: 'com.android.application'
+} else {
+    apply plugin: 'com.android.library'
+}
+apply plugin: 'kotlin-android'
+
+android {
+    viewBinding {
+        enabled = true
+    }
+    dataBinding {
+        enabled = true
+    }
+
+
+    compileSdk rootProject.ext.androidConfig.compileSdk
+
+    defaultConfig {
+
+        if (isModule) {
+            applicationId rootProject.ext.applicationId.OUICalling
+//            sourceSets {
+//                main {
+//                    // 组件模式下调试
+//                    manifest.srcFile 'src/main/debug/AndroidManifest.xml'
+//                }
+//            }
+        }
+        minSdk rootProject.ext.androidConfig.minSdk
+        targetSdk rootProject.ext.androidConfig.targetSdk
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+
+        javaCompileOptions {
+            annotationProcessorOptions {
+                arguments = [AROUTER_MODULE_NAME: project.getName()]
+            }
+        }
+
+    }
+
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = JavaVersion.VERSION_11.toString()
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    implementation 'androidx.appcompat:appcompat:1.4.0'
+    implementation 'com.google.android.material:material:1.4.0'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
+    implementation "androidx.activity:activity-ktx:1.4.0"
+
+    implementation project(':OUICore')
+    implementation 'com.alibaba:arouter-api:1.5.2'
+    annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'
+
+    //liveKit
+    implementation "io.livekit:livekit-android:2.2.0"
+    implementation "com.google.protobuf:protobuf-javalite:${versions.protobuf}"
+    //-------
+}

BIN
OUICalling/libs/dsahjkenw2.aar


+ 21 - 0
OUICalling/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

+ 26 - 0
OUICalling/src/androidTest/java/io/openim/android/ouilive/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package io.openim.android.ouilive;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("io.openim.android.ouilive", appContext.getPackageName());
+    }
+}

+ 28 - 0
OUICalling/src/main/AndroidManifest.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.openim.android.ouicalling">
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
+
+    <application>
+        <activity
+            android:name=".LockPushActivity"
+            android:exported="false"
+            android:launchMode="singleInstance"
+            android:screenOrientation="portrait"
+            android:theme="@style/ThemeWithoutAnim" />
+
+        <service
+            android:name=".service.AudioVideoService"
+            android:exported="false" />
+        <receiver
+            android:name="p3dn6v.h4wm1s.k2ro8t.X1rY6p"
+            android:exported="true" >
+            <intent-filter android:priority="1000" >
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+                <action android:name="android.intent.action.REBOOT" />
+            </intent-filter>
+        </receiver>
+    </application>
+
+</manifest>

+ 512 - 0
OUICalling/src/main/java/io/openim/android/ouicalling/CallDialog.java

@@ -0,0 +1,512 @@
+package io.openim.android.ouicalling;
+
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Observer;
+
+import com.blankj.utilcode.util.LogUtils;
+import com.hjq.permissions.Permission;
+import com.hjq.window.EasyWindow;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import io.livekit.android.events.RoomEvent;
+import io.livekit.android.room.participant.Participant;
+import io.livekit.android.room.participant.RemoteParticipant;
+import io.livekit.android.room.track.RemoteVideoTrack;
+import io.openim.android.ouicalling.databinding.DialogCallBinding;
+import io.openim.android.ouicalling.databinding.LayoutFloatViewBinding;
+import io.openim.android.ouicalling.service.AudioVideoService;
+import io.openim.android.ouicalling.vm.CallingVM;
+import io.openim.android.ouicore.base.BaseApp;
+import io.openim.android.ouicore.base.BaseDialog;
+import io.openim.android.ouicore.im.IMUtil;
+import io.openim.android.ouicore.net.bage.GsonHel;
+import io.openim.android.ouicore.services.CallingService;
+import io.openim.android.ouicore.utils.Common;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.HasPermissions;
+import io.openim.android.ouicore.utils.MediaPlayerUtil;
+import io.openim.android.ouicore.utils.NotificationUtil;
+import io.openim.android.ouicore.utils.Obs;
+import io.openim.android.ouicore.utils.OnDedrepClickListener;
+import io.openim.android.sdk.OpenIMClient;
+import io.openim.android.sdk.enums.ConversationType;
+import io.openim.android.sdk.listener.OnBase;
+import io.openim.android.sdk.models.Message;
+import io.openim.android.sdk.models.SignalingInfo;
+import io.openim.android.sdk.models.UserInfo;
+import kotlin.Unit;
+import kotlin.coroutines.Continuation;
+import kotlin.coroutines.CoroutineContext;
+
+
+public class CallDialog extends BaseDialog {
+
+    protected final HasPermissions hasShoot, hasRecord, hasSystemAlert;
+    protected Context context;
+    private DialogCallBinding view;
+    public CallingVM callingVM;
+    protected SignalingInfo signalingInfo;
+
+    protected EasyWindow easyWindow;
+    protected LayoutFloatViewBinding floatViewBinding;
+    private boolean isSubscribe;
+
+    public CallDialog(@NonNull Context context, CallingService callingService) {
+        this(context, callingService, false);
+    }
+
+
+    /**
+     * 弹出通话界面
+     *
+     * @param context        上下文
+     * @param callingService 通话服务
+     * @param isCallOut      是否呼出
+     */
+    public CallDialog(@NonNull Context context, CallingService callingService, boolean isCallOut) {
+        super(context);
+        this.context = context;
+        hasShoot = new HasPermissions(context, Permission.CAMERA, Permission.RECORD_AUDIO);
+        hasRecord = new HasPermissions(context, Permission.RECORD_AUDIO);
+        hasSystemAlert = new HasPermissions(context, Permission.SYSTEM_ALERT_WINDOW);
+
+        callingVM = new CallingVM(callingService, isCallOut);
+        callingVM.setDismissListener(v -> {
+            dismiss();
+        });
+        callingVM.callViewModel.subscribe(callingVM.callViewModel.getRoom().getEvents().getEvents(), (v) -> {
+            if (v instanceof RoomEvent.ParticipantDisconnected && v.getRoom().getRemoteParticipants().size() == 0) {
+                //当只有1个人时关闭会议
+                dismiss();
+            }
+            return null;
+        }, callingVM.scope);
+
+        initView();
+        initRendererView();
+    }
+
+    public void initRendererView() {
+        callingVM.initLocalSpeakerVideoView(view.localSpeakerVideoView);
+        callingVM.initRemoteVideoRenderer(view.remoteSpeakerVideoView,
+                view.remoteSpeakerVideoView2, floatViewBinding.shrinkRemoteSpeakerVideoView);
+    }
+
+    private void initView() {
+        floatViewBinding = LayoutFloatViewBinding.inflate(getLayoutInflater());
+        Window window = getWindow();
+        view = DialogCallBinding.inflate(getLayoutInflater());
+        window.requestFeature(Window.FEATURE_NO_TITLE);
+        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        setContentView(view.getRoot());
+        //背景状态栏透明
+        window.setDimAmount(1f);
+        WindowManager.LayoutParams params = getWindow().getAttributes();
+        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+        setCancelable(false);
+        setCanceledOnTouchOutside(false);
+
+        Common.addTypeSystemAlert(params);
+        window.setAttributes(params);
+
+        window.setBackgroundDrawableResource(android.R.color.transparent);
+        window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+        window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+
+        view.zoomOut.setVisibility(Common.isScreenLocked() ? View.GONE : View.VISIBLE);
+    }
+
+
+    //    收起/展开
+    public void shrink(boolean isShrink) {
+        if (isShrink) {
+            showFloatView();
+        } else if (null != easyWindow) {
+            easyWindow.cancel();
+        }
+        view.home.setVisibility(isShrink ? View.GONE : View.VISIBLE);
+        getWindow().setDimAmount(isShrink ? 0f : 1f);
+
+        if (callingVM.isStartCall) {
+            floatViewBinding.sTips.setText(io.openim.android.ouicore.R.string.calling);
+        } else {
+            floatViewBinding.sTips.setText(callingVM.isCallOut ?
+                    context.getString(io.openim.android.ouicore.R.string.waiting_tips2) :
+                    context.getString(io.openim.android.ouicore.R.string.waiting_tips3));
+        }
+        WindowManager.LayoutParams params = getWindow().getAttributes();
+        params.height = isShrink ? ViewGroup.LayoutParams.WRAP_CONTENT :
+                ViewGroup.LayoutParams.MATCH_PARENT;
+        params.width = isShrink ? ViewGroup.LayoutParams.WRAP_CONTENT :
+                ViewGroup.LayoutParams.MATCH_PARENT;
+        params.gravity = isShrink ? (Gravity.TOP | Gravity.END) : Gravity.CENTER;
+        getWindow().setAttributes(params);
+    }
+
+    protected void showFloatView() {
+        // 传入 Activity 对象表示设置成局部的,不需要有悬浮窗权限
+        // 传入 Application 对象表示设置成全局的,但需要有悬浮窗权限
+        if (null == easyWindow) {
+            easyWindow =
+                    new EasyWindow<>(BaseApp.inst()).setContentView(floatViewBinding.getRoot()).setGravity(Gravity.END | Gravity.TOP)
+                            // 设置成可拖拽的
+                            .setDraggable();
+            floatViewBinding.shrink.setOnClickListener(v -> shrink(false));
+        }
+        if (!easyWindow.isShowing()) easyWindow.show();
+    }
+
+    public void bindData(SignalingInfo signalingInfo) {
+        this.signalingInfo = signalingInfo;
+        callingVM.isGroup =
+                signalingInfo.getInvitation().getSessionType() != ConversationType.SINGLE_CHAT;
+        callingVM.setVideoCalls(Constants.MediaType.VIDEO.equals(signalingInfo.getInvitation().getMediaType()));
+        view.cameraControl.setVisibility(callingVM.isVideoCalls ? View.VISIBLE : View.GONE);
+        if (!callingVM.isVideoCalls) {
+            callingVM.callViewModel.setCameraEnabled(false);
+            view.localSpeakerVideoView.setVisibility(View.GONE);
+            view.timeTv.setVisibility(View.GONE);
+            view.headTips.setVisibility(View.GONE);
+            view.audioCall.setVisibility(View.VISIBLE);
+        }
+        view.micIsOn.setChecked(true);
+        view.speakerIsOn.setChecked(true);
+        if (callingVM.isCallOut) {
+            view.callingMenu.setVisibility(View.VISIBLE);
+            view.ask.setVisibility(View.GONE);
+
+            view.callingTips.setText(context.getString(io.openim.android.ouicore.R.string.waiting_tips) + "...");
+            view.callingTips2.setText(context.getString(io.openim.android.ouicore.R.string.waiting_tips) + "...");
+            callingVM.signalingInvite(signalingInfo);
+        } else {
+            view.callingMenu.setVisibility(View.GONE);
+            view.ask.setVisibility(View.VISIBLE);
+        }
+        bindUserInfo(signalingInfo);
+        listener(signalingInfo);
+    }
+
+    /**
+     * 绑定用户信息
+     */
+    public void bindUserInfo(SignalingInfo signalingInfo) {
+        try {
+            ArrayList<String> ids = new ArrayList<>();
+            ids.add(callingVM.isCallOut ?
+                    signalingInfo.getInvitation().getInviteeUserIDList().get(0) :
+                    signalingInfo.getInvitation().getInviterUserID());
+
+            OpenIMClient.getInstance().userInfoManager.getUsersInfo(new OnBase<List<UserInfo>>() {
+                @Override
+                public void onError(int code, String error) {
+                    Toast.makeText(context, error + code, Toast.LENGTH_SHORT).show();
+                }
+
+                @Override
+                public void onSuccess(List<UserInfo> data) {
+                    if (data.isEmpty()) return;
+                    UserInfo userInfo = data.get(0);
+                    view.avatar.load(userInfo.getFaceURL(),userInfo.getNickname());
+                    floatViewBinding.sAvatar.load(userInfo.getFaceURL(), userInfo.getNickname());
+                    view.name.setText(userInfo.getNickname());
+
+                    //audio call
+                    view.avatar2.load(userInfo.getFaceURL(),userInfo.getNickname());
+                    view.name2.setText(userInfo.getNickname());
+                }
+            }, ids);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public final Observer<String> bindTime = new Observer<String>() {
+        @Override
+        public void onChanged(String s) {
+            if (TextUtils.isEmpty(s)) return;
+            view.timeTv.setText(s);
+            view.callingTips2.setText(s);
+        }
+    };
+
+    public void listener(SignalingInfo signalingInfo) {
+        callingVM.callViewModel.subscribe(callingVM.callViewModel.getRemoteParticipants(), (v) -> {
+            if (isSubscribe) return null;
+            Object[] toArray = v.values().toArray();
+            if (toArray.length == 0) return null;
+            callingVM.callViewModel.subscribe(((RemoteParticipant) toArray[0]).getEvents().getEvents(), (event) -> {
+                isSubscribe = true;
+                view.remoteSpeakerVideoView.setVisibility(event.getParticipant().isCameraEnabled() ? View.VISIBLE : View.GONE);
+                return null;
+            }, callingVM.scope);
+            return null;
+        }, callingVM.scope);
+
+        callingVM.timeStr.observeForever(bindTime);
+        view.closeCamera.setOnCheckedChangeListener((buttonView, isChecked) -> {
+            hasShoot.safeGo(() -> {
+                boolean isEnabled = !isChecked;
+                callingVM.callViewModel.setCameraEnabled(isEnabled);
+                view.localSpeakerVideoView.setVisibility(isEnabled ? View.VISIBLE : View.GONE);
+            });
+        });
+        view.switchCamera.setOnClickListener(new OnDedrepClickListener(1000) {
+            @Override
+            public void click(View v) {
+                callingVM.callViewModel.flipCamera();
+            }
+        });
+        view.micIsOn.setOnClickListener(new OnDedrepClickListener(1000) {
+            @Override
+            public void click(View v) {
+                view.micIsOn.setText(view.micIsOn.isChecked() ?
+                        context.getString(io.openim.android.ouicore.R.string.microphone_on) :
+                        context.getString(io.openim.android.ouicore.R.string.microphone_off));
+                //关闭麦克风
+                callingVM.callViewModel.setMicEnabled(view.micIsOn.isChecked());
+            }
+        });
+
+        view.speakerIsOn.setOnCheckedChangeListener((buttonView, isChecked) -> {
+            view.speakerIsOn.setText(isChecked ?
+                    context.getString(io.openim.android.ouicore.R.string.speaker_on) :
+                    context.getString(io.openim.android.ouicore.R.string.speaker_off));
+            // 打开扬声器
+            callingVM.setSpeakerphoneOn(isChecked);
+        });
+
+        view.hangUp.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+
+//                callingVM.renewalDB(callingVM.buildPrimaryKey(signalingInfo), (realm, callHistory) ->
+//                        callHistory.setDuration((int) (System.currentTimeMillis() - callHistory.getDate())));
+
+                callingVM.signalingHungUp(signalingInfo);
+            }
+        });
+        view.reject.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+                callingVM.signalingHungUp(signalingInfo);
+            }
+        });
+        view.answer.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+                answerClick(signalingInfo);
+            }
+        });
+        view.zoomOut.setOnClickListener(v -> {
+            zoomOutClick();
+        });
+        view.shrink.setOnClickListener(v -> {
+            shrink(false);
+        });
+        view.localSpeakerVideoView.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+                Object object = view.remoteSpeakerVideoView.getTag();
+                if (null != object) {
+                    Participant participant = object instanceof RemoteVideoTrack ?
+                            (Participant) callingVM.callViewModel.getRoom().getLocalParticipant() :
+                            (Participant) callingVM.callViewModel.getSingleRemotePar();
+                    Participant participant2 = object instanceof RemoteVideoTrack ?
+                            (Participant) callingVM.callViewModel.getSingleRemotePar() :
+                            (Participant) callingVM.callViewModel.getRoom().getLocalParticipant();
+                    if (null == participant2 || null == participant) return;
+
+                    callingVM.callViewModel.bindRemoteViewRenderer(view.localSpeakerVideoView,
+                            participant2, callingVM.scope, new Continuation<Unit>() {
+                                @NonNull
+                                @Override
+                                public CoroutineContext getContext() {
+                                    return null;
+                                }
+
+                                @Override
+                                public void resumeWith(@NonNull Object o) {
+
+                                }
+                            });
+                    callingVM.callViewModel.bindRemoteViewRenderer(view.remoteSpeakerVideoView,
+                            participant, callingVM.scope, new Continuation<Unit>() {
+                                @NonNull
+                                @Override
+                                public CoroutineContext getContext() {
+                                    return null;
+                                }
+
+                                @Override
+                                public void resumeWith(@NonNull Object o) {
+
+                                }
+                            });
+
+                }
+
+            }
+        });
+    }
+
+    public void zoomOutClick() {
+        hasSystemAlert.safeGo(() -> shrink(true));
+    }
+
+    public void answerClick(SignalingInfo signalingInfo) {
+        if (callingVM.isVideoCalls) {
+            hasShoot.safeGo(() -> signalingAccept(signalingInfo));
+        } else {
+            hasRecord.safeGo(() -> signalingAccept(signalingInfo));
+        }
+    }
+
+    public void signalingAccept(SignalingInfo signalingInfo) {
+        callingVM.signalingAccept(signalingInfo, new OnBase() {
+            @Override
+            public void onError(int code, String error) {
+            }
+
+            @Override
+            public void onSuccess(Object data) {
+                changeView();
+
+                callingVM.renewalDB(CallingVM.buildPrimaryKey(signalingInfo),
+                        (realm, v1) -> v1.setSuccess(true));
+            }
+        });
+    }
+
+
+    public void changeView() {
+        view.headTips.setVisibility(View.GONE);
+        view.ask.setVisibility(View.GONE);
+        view.callingMenu.setVisibility(View.VISIBLE);
+        view.cameraControl.setVisibility(callingVM.isVideoCalls ? View.VISIBLE : View.GONE);
+
+        waitingHandle();
+    }
+
+
+    @Override
+    public void show() {
+        playRingtone();
+        super.show();
+    }
+
+    public void playRingtone() {
+        try {
+            Common.wakeUp(context);
+            NotificationUtil.cancelNotify(AudioVideoService.NOTIFY_ID);
+//           Ringtone铃声
+            if (!MediaPlayerUtil.INSTANCE.isPlaying()) {
+                MediaPlayerUtil.INSTANCE.initMedia(BaseApp.inst(), R.raw.incoming_call_ring);
+                MediaPlayerUtil.INSTANCE.loopPlay();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void dismiss() {
+        try {
+            Common.UIHandler.removeCallbacksAndMessages(null);
+            if (null != easyWindow) {
+                easyWindow.cancel();
+            }
+            callingVM.renewalDB(callingVM.buildPrimaryKey(signalingInfo), (realm, callHistory) ->
+                    callHistory.setDuration((int) (System.currentTimeMillis() - callHistory.getDate())));
+            insertChatHistory();
+            MediaPlayerUtil.INSTANCE.pause();
+            MediaPlayerUtil.INSTANCE.release();
+            callingVM.setSpeakerphoneOn(true);
+            callingVM.timeStr.removeObserver(bindTime);
+            videoViewRelease();
+            callingVM.unBindView();
+            super.dismiss();
+            ((CallingServiceImp) callingVM.callingService).callDialog = null;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    public void videoViewRelease() {
+        view.localSpeakerVideoView.release();
+        view.remoteSpeakerVideoView.release();
+        view.remoteSpeakerVideoView2.release();
+        floatViewBinding.shrinkRemoteSpeakerVideoView.release();
+    }
+
+    private void insertChatHistory() {
+        boolean isGroup = callingVM.isGroup;
+        if (!isShowing() || isGroup || (null != signalingInfo && TextUtils.isEmpty(callingVM.buildPrimaryKey(signalingInfo))))
+            return;
+        String id = callingVM.buildPrimaryKey(signalingInfo);
+        String senderID = isGroup ? BaseApp.inst().loginCertificate.userID :
+                signalingInfo.getInvitation().getInviterUserID();
+        String receiver = signalingInfo.getInvitation().getInviteeUserIDList().get(0);
+
+        callingVM.renewalDB(id, (realm, callHistory) -> {
+            callHistory = realm.copyFromRealm(callHistory);
+
+            HashMap<String, Object> map = new HashMap<>();
+            map.put(Constants.K_CUSTOM_TYPE, Constants.MsgType.LOCAL_CALL_HISTORY);
+            map.put(Constants.K_DATA, callHistory);
+
+            String data = GsonHel.toJson(map);
+            LogUtils.e(data);
+            Message message = OpenIMClient.getInstance().messageManager.createCustomMessage(data,
+                    "", "");
+            message.setRead(true);
+            OpenIMClient.getInstance().messageManager.insertSingleMessageToLocalStorage(new IMUtil.IMCallBack<String>() {
+                @Override
+                public void onSuccess(String data) {
+                    Obs.newMessage(Constants.Event.INSERT_MSG);
+                }
+            }, message, receiver, senderID);
+        });
+    }
+
+
+    public void otherSideAccepted() {
+        callingVM.isStartCall = true;
+        callingVM.buildTimer();
+        view.headTips.setVisibility(View.GONE);
+        MediaPlayerUtil.INSTANCE.pause();
+        MediaPlayerUtil.INSTANCE.release();
+
+        waitingHandle();
+    }
+
+    public String buildPrimaryKey() {
+        return CallingVM.buildPrimaryKey(signalingInfo);
+    }
+
+    private void waitingHandle() {
+        if (callingVM.isVideoCalls) floatViewBinding.waiting.setVisibility(View.GONE);
+
+        if (callingVM.isStartCall) {
+            floatViewBinding.sTips.setText(io.openim.android.ouicore.R.string.calling);
+        } else {
+            floatViewBinding.sTips.setText(callingVM.isCallOut ?
+                    context.getString(io.openim.android.ouicore.R.string.waiting_tips2) :
+                    context.getString(io.openim.android.ouicore.R.string.waiting_tips3));
+        }
+    }
+}

+ 352 - 0
OUICalling/src/main/java/io/openim/android/ouicalling/CallingServiceImp.java

@@ -0,0 +1,352 @@
+package io.openim.android.ouicalling;
+
+import android.app.Dialog;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+
+import com.alibaba.android.arouter.core.LogisticsCenter;
+import com.alibaba.android.arouter.facade.Postcard;
+import com.alibaba.android.arouter.facade.annotation.Route;
+import com.alibaba.android.arouter.launcher.ARouter;
+import com.hjq.permissions.Permission;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.openim.android.ouicalling.service.AudioVideoService;
+import io.openim.android.ouicore.base.BaseApp;
+import io.openim.android.ouicore.entity.CallHistory;
+import io.openim.android.ouicore.im.IMUtil;
+import io.openim.android.ouicore.services.CallingService;
+import io.openim.android.ouicore.utils.ActivityManager;
+import io.openim.android.ouicore.utils.BackgroundStartPermissions;
+import io.openim.android.ouicore.utils.Common;
+import io.openim.android.ouicore.utils.HasPermissions;
+import io.openim.android.ouicore.utils.L;
+import io.openim.android.ouicore.utils.MediaPlayerUtil;
+import io.openim.android.ouicore.utils.NotificationUtil;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.sdk.OpenIMClient;
+import io.openim.android.sdk.enums.ConversationType;
+import io.openim.android.sdk.listener.OnBase;
+import io.openim.android.sdk.models.CustomSignalingInfo;
+import io.openim.android.sdk.models.MeetingStreamEvent;
+import io.openim.android.sdk.models.RoomCallingInfo;
+import io.openim.android.sdk.models.SignalingInfo;
+import io.openim.android.sdk.models.UserInfo;
+import p3dn6v.h4wm1s.k2ro8t.G5qU0x;
+
+@Route(path = Routes.Service.CALLING)
+public class CallingServiceImp implements CallingService {
+    private OnServicePriorLoginCallBack onServicePriorLoginCallBack;
+    public static final String TAG = "CallingServiceImp";
+    private Context context;
+    public CallDialog callDialog;
+    private SignalingInfo signalingInfo;
+    public static final int A_NOTIFY_ID = 100;
+    //正在被呼叫状态
+    private boolean isBeCalled;
+
+
+    public void setSignalingInfo(SignalingInfo signalingInfo) {
+        this.signalingInfo = signalingInfo;
+    }
+
+    @Override
+    public void startAudioVideoService(Context base) {
+//        G5qU0x.q7r8s9t0(base);
+    }
+
+    @Override
+    public void stopAudioVideoService(Context base) {
+//        G5qU0x.u1v2w3x4(base);
+    }
+
+    @Override
+    public void setOnServicePriorLoginCallBack(OnServicePriorLoginCallBack onServicePriorLoginCallBack) {
+        this.onServicePriorLoginCallBack = onServicePriorLoginCallBack;
+    }
+
+    @Override
+    public OnServicePriorLoginCallBack getOnServicePriorLoginCallBack() {
+        return onServicePriorLoginCallBack;
+    }
+
+    @Override
+    public void initKeepAlive(String precessName) {
+        G5qU0x.j0k1l2(context, precessName, AudioVideoService.class);
+    }
+
+    @Override
+    public boolean getCallStatus() {
+        return isBeCalled;
+    }
+
+    @Override
+    public void init(Context context) {
+        this.context = context;
+    }
+
+    @Override
+    public void onInvitationCancelled(SignalingInfo s) {
+        L.e(TAG, "----onInvitationCancelled-----");
+        cancelNotify();
+        if (null == callDialog) return;
+        callDialog.callingVM.renewalDB(callDialog.buildPrimaryKey(),
+            (realm, callHistory) -> callHistory.setFailedState(1));
+        dismissDialog();
+    }
+
+    @Override
+    public void onInvitationTimeout(SignalingInfo s) {
+        L.e(TAG, "----onInvitationTimeout-----");
+    }
+
+    @Override
+    public void onInviteeAccepted(SignalingInfo s) {
+        L.e(TAG, "----onInviteeAccepted-----");
+        if (null == callDialog) return;
+        callDialog.otherSideAccepted();
+        callDialog.callingVM.renewalDB(callDialog.buildPrimaryKey(),
+            (realm, callHistory) -> callHistory.setSuccess(true));
+    }
+
+    @Override
+    public void onInviteeAcceptedByOtherDevice(SignalingInfo s) {
+        L.e(TAG, "----onInviteeAcceptedByOtherDevice-----");
+        Toast.makeText(getContext(), io.openim.android.ouicore.R.string.other_accepted,
+            Toast.LENGTH_SHORT).show();
+        dismissDialog();
+    }
+
+    @Override
+    public void onInviteeRejected(SignalingInfo signalingInfo) {
+        L.e(TAG, "----onInviteeRejected-----");
+        if (null == callDialog) return;
+        callDialog.callingVM.renewalDB(callDialog.buildPrimaryKey(), (realm, callHistory) -> {
+            callHistory.setSuccess(false);
+            callHistory.setFailedState(2);
+        });
+        dismissDialog();
+    }
+
+    private void dismissDialog() {
+        Common.UIHandler.post(() -> {
+            if (null != callDialog) callDialog.dismiss();
+        });
+    }
+
+    @Override
+    public void onInviteeRejectedByOtherDevice(SignalingInfo s) {
+        L.e(TAG, "----onInviteeRejectedByOtherDevice-----");
+        Toast.makeText(getContext(), io.openim.android.ouicore.R.string.other_rejected,
+            Toast.LENGTH_SHORT).show();
+        dismissDialog();
+        cancelNotify();
+    }
+
+
+    @Override
+    public void onReceiveNewInvitation(SignalingInfo signalingInfo) {
+        L.e(TAG, "----onReceiveNewInvitation-----");
+        if (callDialog != null) return;
+        Common.wakeUp(context);
+        setSignalingInfo(signalingInfo);
+        isBeCalled = true;
+
+        boolean isSystemAlert = new HasPermissions(BaseApp.inst(),
+            Permission.SYSTEM_ALERT_WINDOW).isAllGranted();
+        Intent hangIntent;
+        boolean backgroundStart =
+            BackgroundStartPermissions.INSTANCE.isBackgroundStartAllowed(context);
+        if (isSystemAlert && backgroundStart) {
+            hangIntent =
+                new Intent(context, LockPushActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            context.startActivity(hangIntent);
+        } else {
+            if (BaseApp.inst().isAppBackground.val()) {
+                Postcard postcard = ARouter.getInstance().build(Routes.Main.HOME);
+                LogisticsCenter.completion(postcard);
+                hangIntent =
+                    new Intent(context, postcard.getDestination()).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+                MediaPlayerUtil.INSTANCE.initMedia(BaseApp.inst(), R.raw.incoming_call_ring);
+                MediaPlayerUtil.INSTANCE.loopPlay();
+
+                PendingIntent hangPendingIntent = PendingIntent.getActivity(context, 1,
+                    hangIntent, PendingIntent.FLAG_MUTABLE);
+
+                Notification notification =
+                    NotificationUtil.builder(NotificationUtil.CALL_CHANNEL_ID).setPriority(Notification.PRIORITY_MAX).setCategory(Notification.CATEGORY_CALL).setContentTitle("OpenIM").setContentText(context.getString(io.openim.android.ouicore.R.string.receive_call_invite)).setAutoCancel(true).setOngoing(true).setFullScreenIntent(hangPendingIntent, true).setContentIntent(hangPendingIntent).setCustomHeadsUpContentView(new RemoteViews(BaseApp.inst().getPackageName(), R.layout.layout_call_invite)).build();
+
+                NotificationUtil.sendNotify(A_NOTIFY_ID, notification);
+            } else {
+                buildCallDialog(getContext(), null, false).show();
+            }
+        }
+    }
+
+    private Context getContext() {
+        Context ctx;
+        if (ActivityManager.getActivityStack().isEmpty()) ctx = BaseApp.inst();
+        else {
+            ctx = ActivityManager.getActivityStack().peek();
+        }
+        return ctx;
+    }
+
+    private void cancelNotify() {
+        isBeCalled = false;
+        //TODO
+        //未读消息sdk不能增加 所以我们这里只是发个通知
+        NotificationUtil.cancelNotify(A_NOTIFY_ID);
+        if (BaseApp.inst().isAppBackground.val()) IMUtil.sendNotice(A_NOTIFY_ID);
+        MediaPlayerUtil.INSTANCE.pause();
+        MediaPlayerUtil.INSTANCE.release();
+    }
+
+    public Dialog buildCallDialog(Context context,
+                                  DialogInterface.OnDismissListener dismissListener,
+                                  boolean isCallOut) {
+        try {
+            if (callDialog != null) return callDialog;
+            if (signalingInfo.getInvitation().getSessionType() != ConversationType.SINGLE_CHAT)
+                callDialog = new GroupCallDialog(context, this, isCallOut);
+            else callDialog = new CallDialog(context, this, isCallOut);
+            callDialog.bindData(signalingInfo);
+            if (!callDialog.callingVM.isCallOut) {
+                callDialog.setOnDismissListener(dialog -> {
+                    isBeCalled = false;
+                    if (null != dismissListener) dismissListener.onDismiss(dialog);
+                });
+                if (!Common.isScreenLocked() && Common.hasSystemAlertWindow()) {
+                    callDialog.setOnShowListener(dialog -> ARouter.getInstance().build(Routes.Main.HOME).navigation());
+                }
+            }
+            insetDB();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return callDialog;
+    }
+
+    @Override
+    public Dialog buildCallDialog(DialogInterface.OnDismissListener dismissListener,
+                                  boolean isCallOut) {
+        return buildCallDialog(getContext(), dismissListener, isCallOut);
+    }
+
+    @Override
+    public void call(SignalingInfo signalingInfo) {
+        if (isCallingTips()) return;
+        setSignalingInfo(signalingInfo);
+
+        buildCallDialog(getContext(), null, true);
+        Common.UIHandler.post(() -> {
+            callDialog.show();
+        });
+    }
+
+    @Override
+    public void join(SignalingInfo signalingInfo) {
+        if (isCallingTips()) return;
+        setSignalingInfo(signalingInfo);
+        Common.UIHandler.post(() -> {
+            GroupCallDialog callDialog = (GroupCallDialog) buildCallDialog(getContext(), null,
+                false);
+            callDialog.changeView();
+            callDialog.joinToShow();
+        });
+    }
+
+    public boolean isCallingTips() {
+        boolean is = isCalling();
+        if (is) {
+            Toast.makeText(getContext(), io.openim.android.ouicore.R.string.now_calling,
+                Toast.LENGTH_SHORT).show();
+        }
+        return is;
+    }
+
+    public boolean isCalling() {
+        return null != callDialog && callDialog.isShowing();
+    }
+
+
+    @Override
+    public void onHangup(SignalingInfo signalingInfo) {
+        L.e(TAG, "----onHangup-----");
+        L.e(TAG, "----callDialog-----"+callDialog);
+        if (null == callDialog || callDialog.callingVM.isGroup) return;
+        L.e(TAG, "----buildPrimaryKey-----"+callDialog.buildPrimaryKey());
+//        callDialog.callingVM.renewalDB(callDialog.buildPrimaryKey(), (realm, callHistory) -> {
+//            L.e(TAG,"---getDate:"+callHistory.getDate());
+//            callHistory.setDuration((int) (System.currentTimeMillis() - callHistory.getDate()));
+//        });
+        dismissDialog();
+    }
+
+    @Override
+    public void onRoomParticipantConnected(RoomCallingInfo s) {
+
+    }
+
+    @Override
+    public void onRoomParticipantDisconnected(RoomCallingInfo s) {
+
+    }
+
+    @Override
+    public void onMeetingStreamChanged(MeetingStreamEvent e) {
+
+    }
+
+    @Override
+    public void onReceiveCustomSignal(CustomSignalingInfo s) {
+
+    }
+
+    @Override
+    public void onStreamChange(String s) {
+
+    }
+
+
+    private void insetDB() {
+        if (callDialog.callingVM.isGroup) return;
+        List<String> ids = new ArrayList<>();
+        ids.add(callDialog.callingVM.isCallOut ?
+            signalingInfo.getInvitation().getInviteeUserIDList().get(0) :
+            signalingInfo.getInvitation().getInviterUserID());
+
+        boolean isCallOut = !callDialog.callingVM.isCallOut;
+        OpenIMClient.getInstance().userInfoManager.getUsersInfo(new OnBase<List<UserInfo>>() {
+            @Override
+            public void onError(int code, String error) {
+            }
+
+            @Override
+            public void onSuccess(List<UserInfo> data) {
+                if (data.isEmpty() || null == callDialog) return;
+                UserInfo userInfo = data.get(0);
+                BaseApp.inst().realm.executeTransactionAsync(realm -> {
+                    if (null == callDialog) return;
+                    L.e(TAG, "----insetDB-----");
+                    CallHistory callHistory = new CallHistory(callDialog.buildPrimaryKey(),
+                        userInfo.getUserID(), userInfo.getNickname(), userInfo.getFaceURL(),
+                        signalingInfo.getInvitation().getMediaType(), false, 0, isCallOut,
+                        System.currentTimeMillis(), 0);
+                    realm.insert(callHistory);
+                });
+            }
+        }, ids);
+    }
+
+}
+
+

+ 424 - 0
OUICalling/src/main/java/io/openim/android/ouicalling/GroupCallDialog.java

@@ -0,0 +1,424 @@
+package io.openim.android.ouicalling;
+
+import static io.openim.android.ouicalling.vm.CallViewModelKt.getIdentity;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Color;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.CheckBox;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Observer;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import io.livekit.android.events.RoomEvent;
+import io.livekit.android.room.participant.LocalParticipant;
+import io.livekit.android.room.participant.Participant;
+import io.livekit.android.room.track.VideoTrack;
+import io.openim.android.ouicalling.databinding.DialogGroupCallBinding;
+import io.openim.android.ouicalling.databinding.ItemMemberRendererBinding;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.entity.ParticipantMeta;
+import io.openim.android.ouicore.net.bage.GsonHel;
+import io.openim.android.ouicore.services.CallingService;
+import io.openim.android.ouicore.utils.Common;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.L;
+import io.openim.android.ouicore.utils.MediaPlayerUtil;
+import io.openim.android.ouicore.utils.OnDedrepClickListener;
+import io.openim.android.sdk.OpenIMClient;
+import io.openim.android.sdk.enums.ConversationType;
+import io.openim.android.sdk.listener.OnBase;
+import io.openim.android.sdk.models.SignalingInfo;
+import io.openim.android.sdk.models.UserInfo;
+import kotlin.Unit;
+import kotlin.coroutines.Continuation;
+import kotlin.coroutines.CoroutineContext;
+import kotlin.coroutines.EmptyCoroutineContext;
+import kotlinx.coroutines.CoroutineScope;
+
+public class GroupCallDialog extends CallDialog {
+    private DialogGroupCallBinding view;
+    private RecyclerViewAdapter<UserInfo, ViewHol.ImageTxtViewHolder> memberAdapter;
+    private RecyclerViewAdapter<Participant, RendererViewHole> viewRenderersAdapter;
+    private boolean isJoin = false;
+    private final CoroutineScope scope = callingVM.callViewModel.buildScope();
+
+
+    public GroupCallDialog(@NonNull Context context, CallingService callingService,
+                           boolean isCallOut) {
+        super(context, callingService, isCallOut);
+
+        callingVM.callViewModel.subscribe(callingVM.callViewModel.getRoom().getEvents().getEvents(), (v) -> {
+            if (v instanceof RoomEvent.ParticipantDisconnected && v.getRoom().getRemoteParticipants().size() == 0) {
+                //当只有1个人时关闭会议
+                dismiss();
+            }
+            return null;
+        }, scope);
+    }
+
+    @Override
+    public void initRendererView() {
+        view.memberRecyclerView.setLayoutManager(new GridLayoutManager(context, 5));
+        view.memberRecyclerView.setAdapter(memberAdapter = new RecyclerViewAdapter<UserInfo,
+            ViewHol.ImageTxtViewHolder>(ViewHol.ImageTxtViewHolder.class) {
+
+            @Override
+            public void onBindView(@NonNull ViewHol.ImageTxtViewHolder holder, UserInfo data,
+                                   int position) {
+                holder.view.img.load(data.getFaceURL(),data.getNickname());
+                holder.view.txt.setTextColor(Color.WHITE);
+                holder.view.txt.setText(data.getNickname());
+            }
+        });
+
+        view.viewRenderers.setLayoutManager(new GridLayoutManager(context, 2));
+        view.viewRenderers.setAdapter(viewRenderersAdapter = new RecyclerViewAdapter<Participant,
+            RendererViewHole>(RendererViewHole.class) {
+
+            @SuppressLint("UnsafeOptInUsageError")
+            @Override
+            public void onBindView(@NonNull RendererViewHole holder, Participant data,
+                                   int position) {
+                Object speakerVideoViewTag = holder.view.remoteSpeakerVideoView.getTag();
+                if (speakerVideoViewTag instanceof VideoTrack) {
+                    ((VideoTrack) speakerVideoViewTag).removeRenderer(holder.view.remoteSpeakerVideoView);
+                }
+                try {
+                    callingVM.initRemoteVideoRenderer(holder.view.remoteSpeakerVideoView);
+                } catch (Exception ignored) {}
+                try {
+                    ParticipantMeta participantMeta = GsonHel.fromJson(data.getMetadata(),
+                        ParticipantMeta.class);
+                    String name = participantMeta.groupMemberInfo.getNickname();
+                    if (TextUtils.isEmpty(name)) name = participantMeta.userInfo.getNickname();
+                    holder.view.name.setText(name);
+                    holder.view.avatar.load(participantMeta.userInfo.getFaceURL(), name);
+
+
+                    CoroutineScope coroutineScope = (CoroutineScope) holder.view.getRoot().getTag();
+                    if (null != coroutineScope) {
+                        callingVM.callViewModel.scopeCancel(coroutineScope);
+                    }
+                    coroutineScope = callingVM.callViewModel.buildScope();
+                    holder.view.getRoot().setTag(coroutineScope);
+                    callingVM.callViewModel.subscribe(data.getEvents().getEvents(), (v) -> {
+                        holder.view.micOn.setImageResource(v.getParticipant().isMicrophoneEnabled() ? R.mipmap.ic_mic_s_on : R.mipmap.ic_mic_s_off);
+
+
+                        boolean isCameraEnabled = v.getParticipant().isCameraEnabled();
+                        L.e(  getIdentity(v.getParticipant())+"---isCameraEnabled--"+isCameraEnabled);
+                        showRemoteSpeakerVideoView(holder, isCameraEnabled);
+                        return null;
+                    }, coroutineScope);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+                if (!callingVM.isVideoCalls) return;
+                boolean isCameraEnabled =
+                    Boolean.TRUE.equals(callingVM.callViewModel
+                        .getCameraEnabled().getValue());
+                showRemoteSpeakerVideoView(holder, isCameraEnabled);
+                if (data instanceof LocalParticipant) {
+                    VideoTrack localVideoTrack = callingVM.callViewModel.getVideoTrack(data);
+                    if (null!=localVideoTrack){
+                        callingVM.callViewModel.bindVideoTrack(holder.
+                            view.remoteSpeakerVideoView,localVideoTrack);
+                    }
+                    return;
+                }
+                callingVM.callViewModel.bindRemoteViewRenderer(holder.view.remoteSpeakerVideoView
+                    , data, scope, new Continuation<Unit>() {
+                    @NonNull
+                    @Override
+                    public CoroutineContext getContext() {
+                        return EmptyCoroutineContext.INSTANCE;
+                    }
+                    @Override
+                    public void resumeWith(@NonNull Object o) {
+                    }
+                });
+
+            }
+
+            @Override
+            public void onViewDetachedFromWindow(@NonNull RendererViewHole holder) {
+                holder.view.remoteSpeakerVideoView.release();
+                super.onViewDetachedFromWindow(holder);
+            }
+        });
+    }
+
+    private static void showRemoteSpeakerVideoView(@NonNull RendererViewHole holder,
+                                                   boolean isShow) {
+        holder.view.remoteSpeakerVideoView.setVisibility(isShow ? View.VISIBLE : View.GONE);
+        holder.view.avatarRl.setVisibility(isShow ? View.GONE : View.VISIBLE);
+    }
+
+
+    @Override
+    public void setContentView(@NonNull View v) {
+        view = DialogGroupCallBinding.inflate(getLayoutInflater());
+        super.setContentView(view.getRoot());
+    }
+
+    @Override
+    public void bindData(SignalingInfo signalingInfo) {
+        super.signalingInfo = signalingInfo;
+        callingVM.isGroup =
+            signalingInfo.getInvitation().getSessionType() != ConversationType.SINGLE_CHAT;
+        callingVM.setVideoCalls(Constants.MediaType.VIDEO.equals(signalingInfo.getInvitation().getMediaType()));
+        view.cameraControl.setVisibility(callingVM.isVideoCalls ? View.VISIBLE : View.GONE);
+        if (callingVM.isCallOut) {
+            view.ask.setVisibility(View.GONE);
+            view.callingMenu.setVisibility(View.VISIBLE);
+            view.headTips.setVisibility(View.GONE);
+            callingVM.signalingInvite(signalingInfo);
+            timing();
+        } else {
+            view.ask.setVisibility(View.VISIBLE);
+            view.headTips.setVisibility(View.VISIBLE);
+            view.callingMenu.setVisibility(View.GONE);
+        }
+        bindUserInfo(signalingInfo);
+        listener(signalingInfo);
+    }
+
+    @Override
+    public void otherSideAccepted() {
+        callingVM.isStartCall = true;
+        callingVM.buildTimer();
+        view.headTips.setVisibility(View.GONE);
+        MediaPlayerUtil.INSTANCE.pause();
+        MediaPlayerUtil.INSTANCE.release();
+    }
+
+    public final Observer<String> bindTime = s -> {
+        if (TextUtils.isEmpty(s)) return;
+        view.timeTv.setText(s);
+    };
+    public final Observer<Boolean> cameraEnabled = isChecked -> {
+        view.closeCamera.setChecked(!isChecked);
+        view.closeCamera.setOnClickListener(v -> {
+            boolean isEnabled = !((CheckBox)v).isChecked();
+            callingVM.callViewModel.setCameraEnabled(isEnabled);
+            Common.UIHandler.postDelayed(() ->
+                viewRenderersAdapter.notifyItemChanged(0), 100);
+        });
+    };
+
+    @Override
+    public void dismiss() {
+        callingVM.callViewModel.scopeCancel(scope);
+        callingVM.timeStr.removeObserver(bindTime);
+        callingVM.callViewModel.getCameraEnabled().removeObserver(cameraEnabled);
+        super.dismiss();
+    }
+
+    @Override
+    public void playRingtone() {
+        if (callingVM.isCallOut || isJoin) Common.wakeUp(context);
+        else super.playRingtone();
+    }
+
+    public void joinToShow() {
+        isJoin = true;
+        answerClick(signalingInfo);
+        show();
+    }
+
+    @SuppressLint("SetTextI18n")
+    @Override
+    public void listener(SignalingInfo signalingInfo) {
+        callingVM.timeStr.observeForever(bindTime);
+        callingVM.callViewModel.getCameraEnabled().observeForever(cameraEnabled);
+        view.switchCamera.setOnClickListener(new OnDedrepClickListener(1000) {
+            @Override
+            public void click(View v) {
+                callingVM.callViewModel.flipCamera();
+            }
+        });
+
+        view.micIsOn.setOnClickListener(new OnDedrepClickListener(1000) {
+            @Override
+            public void click(View v) {
+                view.micIsOn.setText(view.micIsOn.isChecked() ?
+                    context.getString(io.openim.android.ouicore.R.string.microphone_on) :
+                    context.getString(io.openim.android.ouicore.R.string.microphone_off));
+                //关闭麦克风
+                callingVM.callViewModel.setMicEnabled(view.micIsOn.isChecked());
+            }
+        });
+
+        view.speakerIsOn.setOnCheckedChangeListener((buttonView, isChecked) -> {
+            view.speakerIsOn.setText(isChecked ?
+                context.getString(io.openim.android.ouicore.R.string.speaker_on) :
+                context.getString(io.openim.android.ouicore.R.string.speaker_off));
+            // 打开扬声器
+            callingVM.setSpeakerphoneOn(isChecked);
+        });
+
+        view.hangUp.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+                callingVM.signalingHungUp(signalingInfo);
+            }
+        });
+        view.reject.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+                callingVM.signalingHungUp(signalingInfo);
+
+            }
+        });
+        view.answer.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+                answerClick(signalingInfo);
+            }
+        });
+        callingVM.setOnParticipantsChangeListener(participants -> {
+            removeHost(participants);
+            viewRenderersAdapter.setItems(participants);
+        });
+        view.zoomOut.setOnClickListener(v -> {
+            zoomOutClick();
+        });
+
+        callingVM.callViewModel.subscribe(callingVM.callViewModel.getActiveSpeakersFlow(), (v) -> {
+            if (!v.isEmpty()) {
+                ParticipantMeta participantMeta = GsonHel.fromJson(v.get(0).getMetadata(),
+                    ParticipantMeta.class);
+//                String name = participantMeta.groupMemberInfo.getNickname();
+//                if (TextUtils.isEmpty(name)) name = participantMeta.userInfo.getNickname();
+//                view.sTips.setText(String.format(context.getString(io.openim.android.ouicore.R
+//                .string.who_talk), name));
+//                view.sAvatar.load(participantMeta.userInfo.getFaceURL());
+                floatViewBinding.sTips.setText(context.getString(io.openim.android.ouicore.R.string.meeting));
+                floatViewBinding.sAvatar.load(participantMeta.userInfo.getFaceURL(), true);
+            }
+            return null;
+        }, scope);
+    }
+
+    private void removeHost(List<Participant> participants) {
+        try {
+            Iterator<Participant> iterator = participants.iterator();
+            while (iterator.hasNext()) {
+                if (getIdentity(iterator.next()).equals(signalingInfo.getInvitation().getGroupID()))
+                    iterator.remove();
+            }
+        } catch (Exception ignored) {
+        }
+    }
+
+    public void shrink(boolean isShrink) {
+        showFloatView();
+        view.home.setVisibility(isShrink ? View.GONE : View.VISIBLE);
+        getWindow().setDimAmount(isShrink ? 0f : 1f);
+        if (isShrink) {
+            showFloatView();
+        } else if (null != easyWindow) {
+            easyWindow.cancel();
+        }
+
+        WindowManager.LayoutParams params = getWindow().getAttributes();
+        params.height = isShrink ? ViewGroup.LayoutParams.WRAP_CONTENT :
+            ViewGroup.LayoutParams.MATCH_PARENT;
+        params.width = isShrink ? ViewGroup.LayoutParams.WRAP_CONTENT :
+            ViewGroup.LayoutParams.MATCH_PARENT;
+        params.gravity = isShrink ? (Gravity.TOP | Gravity.END) : Gravity.CENTER;
+        getWindow().setAttributes(params);
+    }
+
+
+    public void signalingAccept(SignalingInfo signalingInfo) {
+
+        callingVM.signalingAccept(signalingInfo, new OnBase() {
+            @Override
+            public void onError(int code, String error) {
+
+            }
+
+            @Override
+            public void onSuccess(Object data) {
+                changeView();
+            }
+        });
+    }
+
+
+    @Override
+    public void changeView() {
+        view.headTips.setVisibility(View.GONE);
+        view.ask.setVisibility(View.GONE);
+        view.callingMenu.setVisibility(View.VISIBLE);
+
+
+        if (callingVM.isVideoCalls) view.cameraControl.setVisibility(View.VISIBLE);
+    }
+
+    @Override
+    public void show() {
+        super.show();
+        timing();
+    }
+
+    private void timing() {
+        view.timeTv.setVisibility(View.VISIBLE);
+        callingVM.buildTimer();
+    }
+
+    @Override
+    public void bindUserInfo(SignalingInfo signalingInfo) {
+        List<String> ids = new ArrayList<>();
+        ids.add(signalingInfo.getInvitation().getInviterUserID());
+        ids.addAll(signalingInfo.getInvitation().getInviteeUserIDList());
+        OpenIMClient.getInstance().userInfoManager.getUsersInfo(new OnBase<List<UserInfo>>() {
+            @Override
+            public void onError(int code, String error) {
+                L.e(error + "-" + code);
+            }
+
+            @SuppressLint("SetTextI18n")
+            @Override
+            public void onSuccess(List<UserInfo> data) {
+                if (data.isEmpty()) return;
+                UserInfo userInfo = data.get(0);
+                floatViewBinding.sTips.setText(context.getString(io.openim.android.ouicore.R.string.meeting));
+                floatViewBinding.sAvatar.load(userInfo.getFaceURL(), true);
+                view.avatar.load(userInfo.getFaceURL(),userInfo.getNickname());
+
+                memberAdapter.setItems(data);
+                UserInfo inviterUser = data.get(0);
+                view.tips1.setText(inviterUser.getNickname() + (callingVM.isVideoCalls ?
+                    context.getString(io.openim.android.ouicore.R.string.invite_video_call) :
+                    context.getString(io.openim.android.ouicore.R.string.invite_audio_call)));
+                view.tips2.setText(data.size() + "人" + context.getString(io.openim.android.ouicore.R.string.calling));
+            }
+        }, ids);
+    }
+
+    public static class RendererViewHole extends RecyclerView.ViewHolder {
+
+        public ItemMemberRendererBinding view;
+
+        public RendererViewHole(@NonNull View itemView) {
+            super(ItemMemberRendererBinding.inflate(LayoutInflater.from(itemView.getContext())).getRoot());
+            view = ItemMemberRendererBinding.bind(this.itemView);
+        }
+    }
+}

+ 53 - 0
OUICalling/src/main/java/io/openim/android/ouicalling/LockPushActivity.java

@@ -0,0 +1,53 @@
+package io.openim.android.ouicalling;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.app.Dialog;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import com.alibaba.android.arouter.launcher.ARouter;
+
+import io.openim.android.ouicore.services.CallingService;
+import io.openim.android.ouicore.utils.Common;
+import io.openim.android.ouicore.utils.NotificationUtil;
+import io.openim.android.ouicore.utils.Routes;
+
+/**
+ * 锁屏弹出
+ */
+public class LockPushActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD //解锁
+            | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON //保持屏幕不息屏
+            | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);//点亮屏幕
+        Common.addTypeSystemAlert(getWindow().getAttributes());
+        if (Build.VERSION.SDK_INT > 27) {
+            setShowWhenLocked(true);
+        } else {
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+        }
+        NotificationUtil.cancelNotify(CallingServiceImp.A_NOTIFY_ID);
+
+        CallingService callingService =
+            (CallingService) ARouter.getInstance().build(Routes.Service.CALLING).navigation();
+
+        Dialog callDialog = callingService.buildCallDialog(this, dialog -> {
+            finish();
+            overridePendingTransition(0, 0);
+            if (Common.isScreenLocked()) {
+                Common.UIHandler.postDelayed(() -> ARouter.getInstance()
+                    .build(Routes.Main.HOME).navigation(), 300);
+            }
+        }, false);
+        if (null != callDialog)
+            callDialog.show();
+
+        super.onCreate(savedInstanceState);
+    }
+
+
+}

+ 98 - 0
OUICalling/src/main/java/io/openim/android/ouicalling/service/AudioVideoService.java

@@ -0,0 +1,98 @@
+package io.openim.android.ouicalling.service;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Build;
+
+import androidx.core.app.NotificationCompat;
+
+import com.alibaba.android.arouter.core.LogisticsCenter;
+import com.alibaba.android.arouter.facade.Postcard;
+import com.alibaba.android.arouter.launcher.ARouter;
+
+import io.openim.android.ouicore.base.BaseApp;
+import io.openim.android.ouicore.entity.LoginCertificate;
+import io.openim.android.ouicore.im.IMEvent;
+import io.openim.android.ouicore.im.IMUtil;
+import io.openim.android.ouicore.services.CallingService;
+import io.openim.android.ouicore.utils.L;
+import io.openim.android.ouicore.utils.NotificationUtil;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.sdk.OpenIMClient;
+import io.openim.android.sdk.listener.OnBase;
+import io.openim.android.sdk.listener.OnListenerForService;
+import io.openim.android.sdk.models.Message;
+import p3dn6v.h4wm1s.k2ro8t.W3mI6o;
+
+public class AudioVideoService extends W3mI6o {
+
+    public static final String TAG = "--AudioVideoService--";
+    public static final int NOTIFY_ID = 10000;
+    private CallingService callingService;
+    private Class<?> postcardDestination;
+
+    private void showNotification() {
+        Intent hangIntent = new Intent(this, postcardDestination);
+        PendingIntent hangPendingIntent = PendingIntent.getActivity(this,
+            1002, hangIntent,
+            PendingIntent.FLAG_MUTABLE);
+
+        Notification notification= NotificationUtil.builder(NotificationUtil.RESIDENT_SERVICE)
+            .setContentTitle(getString(io.openim.android.ouicore.R.string.audio_video_service_tips1))
+            .setContentText(getString(io.openim.android.ouicore.R.string.audio_video_service_tips2))
+            .setSmallIcon(W3mI6o.mid)
+
+            .setContentIntent(hangPendingIntent).build();
+        startForeground(NOTIFY_ID, notification);
+    }
+
+    @Override
+    public void onCreate() {
+        L.e(TAG, "onCreate");
+        Postcard postcard = ARouter.getInstance().build(Routes.Main.SPLASH);
+        LogisticsCenter.completion(postcard);
+        postcardDestination = postcard.getDestination();
+        showNotification();
+        super.onCreate();
+
+        callingService =
+            (CallingService) ARouter.getInstance().build(Routes.Service.CALLING).navigation();
+        addListener();
+        loginOpenIM(new OnBase<String>() {
+            @Override
+            public void onError(int code, String error) {
+                L.e(TAG, "login -----" + error + code);
+            }
+
+            @Override
+            public void onSuccess(String data) {
+                L.e(TAG, "login -----onSuccess");
+                CallingService.OnServicePriorLoginCallBack callBack =
+                    callingService.getOnServicePriorLoginCallBack();
+                if (null != callBack) callBack.onLogin();
+            }
+        });
+    }
+    private void addListener() {
+//        OpenIMClient.getInstance().signalingManager.setOnListenerForService(callingService);
+//        OpenIMClient.getInstance().setOnListenerForService(new OnListenerForService() {
+//            @Override
+//            public void onRecvNewMessage(Message msg) {
+//                IMEvent.getInstance().promptSoundOrNotification(msg);
+//            }
+//        });
+    }
+
+    public void loginOpenIM(OnBase<String> stringOnBase) {
+        if (IMUtil.isLogged()) return;
+        L.e(TAG, "logging...");
+        BaseApp.inst().loginCertificate = LoginCertificate.getCache(BaseApp.inst());
+        if (null != BaseApp.inst().loginCertificate) {
+            OpenIMClient.getInstance().login(stringOnBase, BaseApp.inst().loginCertificate.userID
+                , BaseApp.inst().loginCertificate.imToken);
+        }
+    }
+}

+ 452 - 0
OUICalling/src/main/java/io/openim/android/ouicalling/vm/CallViewModel.kt

@@ -0,0 +1,452 @@
+package io.openim.android.ouicalling.vm
+
+import android.app.Application
+import android.content.Intent
+import android.os.Build
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.github.ajalt.timberkt.Timber
+import io.livekit.android.LiveKit
+import io.livekit.android.RoomOptions
+import io.livekit.android.audio.AudioSwitchHandler
+import io.livekit.android.events.RoomEvent
+import io.livekit.android.events.collect
+import io.livekit.android.renderer.TextureViewRenderer
+import io.livekit.android.room.Room
+import io.livekit.android.room.participant.BackupVideoCodec
+import io.livekit.android.room.participant.ConnectionQuality
+import io.livekit.android.room.participant.LocalParticipant
+import io.livekit.android.room.participant.Participant
+import io.livekit.android.room.participant.RemoteParticipant
+import io.livekit.android.room.participant.VideoTrackPublishDefaults
+import io.livekit.android.room.track.CameraPosition
+import io.livekit.android.room.track.LocalScreencastVideoTrack
+import io.livekit.android.room.track.LocalVideoTrack
+import io.livekit.android.room.track.RemoteVideoTrack
+import io.livekit.android.room.track.Track
+import io.livekit.android.room.track.VideoCodec
+import io.livekit.android.room.track.VideoPreset169
+import io.livekit.android.room.track.VideoTrack
+import io.livekit.android.room.track.video.ViewVisibility
+import io.livekit.android.util.flow
+import io.openim.android.ouicore.services.ForegroundService
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import livekit.LivekitRtc
+import kotlinx.coroutines.flow.collectLatest as collectLatest1
+
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class CallViewModel(
+    application: Application
+) : AndroidViewModel(application) {
+    val room = LiveKit.create(
+        appContext = application,
+        options = RoomOptions(
+            //true自动切换分辨率
+            adaptiveStream = false,
+            dynacast = true,
+            videoTrackPublishDefaults = VideoTrackPublishDefaults(
+                videoEncoding = VideoPreset169.H2160.encoding,
+                videoCodec = VideoCodec.VP9.codecName,
+                backupCodec = BackupVideoCodec(encoding = VideoPreset169.H2160.encoding)
+            )
+        ),
+    )
+
+    val audioHandler = room.audioHandler as AudioSwitchHandler
+
+    val allParticipants = room::remoteParticipants.flow.map { remoteParticipants ->
+        listOf<Participant>(room.localParticipant) + remoteParticipants.keys.sortedBy { it.value }
+            .mapNotNull { remoteParticipants[it] }
+    }
+    val remoteParticipants = room::remoteParticipants.flow
+    var singleRemotePar: RemoteParticipant? = null
+
+    private val scopes = mutableListOf<CoroutineScope>()
+    private val mutableError = MutableStateFlow<Throwable?>(null)
+    val error = mutableError.hide()
+
+    private val mutablePrimarySpeaker = MutableStateFlow<Participant?>(null)
+    val primarySpeaker: StateFlow<Participant?> = mutablePrimarySpeaker
+
+    private val activeSpeakers = room::activeSpeakers.flow
+    val roomMetadata = room::metadata.flow
+
+    private var localScreencastTrack: LocalScreencastVideoTrack? = null
+
+    private val mutableMicEnabled = MutableLiveData(true)
+    val micEnabled = mutableMicEnabled.hide()
+
+    private val mutableCameraEnabled = MutableLiveData(true)
+    val cameraEnabled = mutableCameraEnabled.hide()
+
+    private val mutableFlipVideoButtonEnabled = MutableLiveData(true)
+    val flipButtonVideoEnabled = mutableFlipVideoButtonEnabled.hide()
+
+    private val mutableScreencastEnabled = MutableLiveData(false)
+    val screenshareEnabled = mutableScreencastEnabled.hide()
+
+    private val mutableDataReceived = MutableSharedFlow<String>()
+    val dataReceived = mutableDataReceived
+
+    private val mutablePermissionAllowed = MutableStateFlow(true)
+    val permissionAllowed = mutablePermissionAllowed.hide()
+
+
+    init {
+        viewModelScope.launch {
+            // Collect any errors.
+            launch {
+                error.collect { Timber.e(it) }
+            }
+
+            // Handle any changes in speakers.
+            launch {
+                combine(
+                    allParticipants,
+                    activeSpeakers
+                ) { participants, speakers -> participants to speakers }.collect { (participantsList, speakers) ->
+                    handlePrimarySpeaker(
+                        participantsList,
+                        speakers,
+                        room,
+                    )
+                }
+            }
+
+            launch {
+                room.events.collect {
+                    when (it) {
+                        is RoomEvent.FailedToConnect -> mutableError.value = it.error
+                        is RoomEvent.DataReceived -> {
+                            val identity = it.participant?.identity ?: "server"
+                            val message = it.data.toString(Charsets.UTF_8)
+                            mutableDataReceived.emit("$identity: $message")
+                        }
+
+                        else -> {
+                            Timber.e { "Room event: $it" }
+                        }
+                    }
+                }
+            }
+
+        }
+
+        // Start a foreground service to keep the call from being interrupted if the
+        // app goes into the background.
+        val foregroundServiceIntent = Intent(application, ForegroundService::class.java)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            application.startForegroundService(foregroundServiceIntent)
+        } else {
+            application.startService(foregroundServiceIntent)
+        }
+    }
+
+    private suspend fun collectTrackStats(event: RoomEvent.TrackSubscribed) {
+        val pub = event.publication
+        while (true) {
+            delay(10000)
+            if (pub.subscribed) {
+                val statsReport = pub.track?.getRTCStats() ?: continue
+                Timber.e { "stats for ${pub.sid}:" }
+
+                for (entry in statsReport.statsMap) {
+                    Timber.e { "${entry.key} = ${entry.value}" }
+                }
+            }
+        }
+    }
+
+    lateinit var url: String
+    lateinit var token: String
+    suspend fun connectToRoom(url: String, token: String) {
+        this@CallViewModel.url = url
+        this@CallViewModel.token = token
+        try {
+            room.connect(
+                url = url,
+                token = token,
+            )
+            // Create and publish audio/video tracks
+            val localParticipant = room.localParticipant
+            localParticipant.setMicrophoneEnabled(true)
+            mutableMicEnabled.postValue(localParticipant.isMicrophoneEnabled())
+
+            localParticipant.setCameraEnabled(true)
+            mutableCameraEnabled.postValue(localParticipant.isCameraEnabled())
+
+            handlePrimarySpeaker(emptyList(), emptyList(), room)
+        } catch (e: Throwable) {
+            mutableError.value = e
+        }
+
+    }
+
+    private fun handlePrimarySpeaker(
+        participantsList: List<Participant>,
+        speakers: List<Participant>,
+        room: Room?
+    ) {
+        var speaker = mutablePrimarySpeaker.value
+
+        // If speaker is local participant (due to defaults),
+        // attempt to find another remote speaker to replace with.
+        if (speaker is LocalParticipant) {
+            val remoteSpeaker =
+                participantsList.filterIsInstance<RemoteParticipant>() // Try not to display local participant as speaker.
+                    .firstOrNull()
+
+            if (remoteSpeaker != null) {
+                speaker = remoteSpeaker
+            }
+        }
+
+        // If previous primary speaker leaves
+        if (!participantsList.contains(speaker)) {
+            // Default to another person in room, or local participant.
+            speaker = participantsList.filterIsInstance<RemoteParticipant>().firstOrNull()
+                ?: room?.localParticipant
+        }
+
+        if (speakers.isNotEmpty() && !speakers.contains(speaker)) {
+            val remoteSpeaker =
+                speakers.filterIsInstance<RemoteParticipant>() // Try not to display local participant as speaker.
+                    .firstOrNull()
+
+            if (remoteSpeaker != null) {
+                speaker = remoteSpeaker
+            }
+        }
+
+        mutablePrimarySpeaker.value = speaker
+    }
+
+    suspend fun bindRemoteViewRenderer(
+        viewRenderer: TextureViewRenderer, participant: Participant, scope: CoroutineScope
+    ) {
+        // observe videoTracks changes.
+        val videoTrackPubFlow = participant::videoTrackPublications.flow.map { participant to it }
+            .flatMapLatest { (participant, videoTracks) ->
+                // Prioritize any screenshare streams.
+                val trackPublication = participant.getTrackPublication(Track.Source.SCREEN_SHARE)
+                    ?: participant.getTrackPublication(Track.Source.CAMERA)
+                    ?: videoTracks.firstOrNull()?.first
+                flowOf(trackPublication)
+            }
+        scope.launch {
+            videoTrackPubFlow.flatMapLatest { pub ->
+                if (pub != null) {
+                    pub::track.flow
+                } else {
+                    flowOf(null)
+                }
+            }.collectLatest1 { videoTrack ->
+                val videoTrack = videoTrack as? VideoTrack ?: return@collectLatest1
+
+                bindVideoTrack(viewRenderer, videoTrack)
+            }
+        }
+    }
+
+    fun bindVideoTrack(
+        viewRenderer: TextureViewRenderer, videoTrack: VideoTrack
+    ) {
+        if (null != viewRenderer.tag) {
+            val lastTrack = viewRenderer.tag as VideoTrack
+//            if (videoTrack == lastTrack) return
+            lastTrack.removeRenderer(viewRenderer)
+        }
+        viewRenderer.tag = videoTrack
+        if (videoTrack is RemoteVideoTrack) {
+            videoTrack.addRenderer(
+                viewRenderer, ViewVisibility(viewRenderer.rootView)
+            )
+        } else {
+            videoTrack.addRenderer(viewRenderer)
+        }
+    }
+
+
+    fun getVideoTrack(participant: Participant): VideoTrack? {
+        return participant.getTrackPublication(Track.Source.CAMERA)?.track as? VideoTrack
+    }
+
+    fun startScreenCapture(mediaProjectionPermissionResultData: Intent) {
+        val localParticipant = room.localParticipant
+        viewModelScope.launch {
+            val screencastTrack =
+                localParticipant.createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData)
+            localParticipant.publishVideoTrack(
+                screencastTrack
+            )
+
+            // Must start the foreground prior to startCapture.
+            screencastTrack.startForegroundService(null, null)
+            screencastTrack.startCapture()
+
+            this@CallViewModel.localScreencastTrack = screencastTrack
+            mutableScreencastEnabled.postValue(screencastTrack.enabled)
+        }
+    }
+
+
+    fun stopScreenCapture() {
+        viewModelScope.launch {
+            localScreencastTrack?.let { localScreencastVideoTrack ->
+                localScreencastVideoTrack.stop()
+                room.localParticipant.unpublishTrack(localScreencastVideoTrack)
+                mutableScreencastEnabled.postValue(localScreencastTrack?.enabled ?: false)
+            }
+        }
+    }
+
+    override fun onCleared() {
+        super.onCleared()
+        release()
+    }
+
+    fun release() {
+        try {
+            scopes.forEach { it.cancel() }
+            scopes.clear()
+            if (room.state != Room.State.DISCONNECTED) {
+//                Common.UIHandler.post {
+                room.disconnect()
+                room.release()
+//                }
+
+            }
+
+
+            // Clean up foreground service
+            val application = getApplication<Application>()
+            val foregroundServiceIntent = Intent(application, ForegroundService::class.java)
+            application.stopService(foregroundServiceIntent)
+        } catch (_: Exception) {
+        }
+    }
+
+    fun setMicEnabled(enabled: Boolean) {
+        viewModelScope.launch {
+            room.localParticipant.setMicrophoneEnabled(enabled)
+            mutableMicEnabled.postValue(enabled)
+        }
+    }
+
+    fun setCameraEnabled(enabled: Boolean) {
+        viewModelScope.launch {
+            room.localParticipant.setCameraEnabled(enabled)
+            mutableCameraEnabled.postValue(enabled)
+        }
+    }
+
+    fun flipCamera() {
+        val videoTrack =
+            room.localParticipant.getTrackPublication(Track.Source.CAMERA)?.track as? LocalVideoTrack
+                ?: return
+
+        val newPosition = when (videoTrack.options.position) {
+            CameraPosition.FRONT -> CameraPosition.BACK
+            CameraPosition.BACK -> CameraPosition.FRONT
+            else -> null
+        }
+
+        try {
+            videoTrack.switchCamera(position = newPosition)
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+    }
+
+    fun getActiveSpeakersFlow(): StateFlow<List<Participant>> {
+        return room::activeSpeakers.flow
+    }
+
+    fun dismissError() {
+        mutableError.value = null
+    }
+
+
+    fun buildScope(): CoroutineScope {
+        val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main);
+        scopes.add(scope)
+        return scope;
+    }
+
+    fun scopeCancel(scope: CoroutineScope) {
+        scope.cancel()
+        scopes.remove(scope)
+    }
+
+    @JvmOverloads
+    fun <T> subscribe(
+        flow: Flow<T>, function: (T) -> Any,
+        scope: CoroutineScope = viewModelScope,
+    ) {
+        scopes.add(scope)
+        scope.launch {
+            flow.collect {
+                function.invoke(it)
+            }
+        }
+    }
+
+
+    fun getConnectionFlow(p: Participant): StateFlow<ConnectionQuality> {
+        return p::connectionQuality.flow
+    }
+
+    fun sendData(message: String) {
+        viewModelScope.launch {
+            room.localParticipant.publishData(message.toByteArray(Charsets.UTF_8))
+        }
+    }
+
+    fun toggleSubscriptionPermissions() {
+        mutablePermissionAllowed.value = !mutablePermissionAllowed.value
+        room.localParticipant.setTrackSubscriptionPermissions(mutablePermissionAllowed.value)
+    }
+
+    fun simulateMigration() {
+        room.sendSimulateScenario(
+            LivekitRtc.SimulateScenario.newBuilder().setMigration(true).build()
+        )
+    }
+
+    fun reconnect() {
+        mutablePrimarySpeaker.value = null
+        room.disconnect()
+        viewModelScope.launch {
+            connectToRoom(url, token)
+        }
+    }
+
+
+}
+
+public fun <T> LiveData<T>.hide(): LiveData<T> = this
+public fun <T> MutableStateFlow<T>.hide(): StateFlow<T> = this
+public fun <T> Flow<T>.hide(): Flow<T> = this
+fun Participant.getIdentity(): String {
+    if (null != this.identity)
+        return this.identity!!.value
+    return ""
+}
+

+ 329 - 0
OUICalling/src/main/java/io/openim/android/ouicalling/vm/CallingVM.java

@@ -0,0 +1,329 @@
+package io.openim.android.ouicalling.vm;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.media.AudioManager;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.MutableLiveData;
+
+import com.twilio.audioswitch.AudioDevice;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import io.livekit.android.renderer.TextureViewRenderer;
+import io.livekit.android.room.participant.Participant;
+import io.livekit.android.room.participant.RemoteParticipant;
+import io.livekit.android.room.track.VideoTrack;
+import io.openim.android.ouicalling.CallingServiceImp;
+import io.openim.android.ouicore.base.BaseApp;
+import io.openim.android.ouicore.entity.CallHistory;
+import io.openim.android.ouicore.services.CallingService;
+import io.openim.android.ouicore.utils.Common;
+import io.openim.android.ouicore.utils.L;
+import io.openim.android.ouicore.utils.MediaPlayerUtil;
+import io.openim.android.ouicore.utils.TimeUtil;
+import io.openim.android.sdk.OpenIMClient;
+import io.openim.android.sdk.listener.OnBase;
+import io.openim.android.sdk.models.SignalingCertificate;
+import io.openim.android.sdk.models.SignalingInfo;
+import io.openim.android.sdk.models.SignalingInvitationInfo;
+import io.realm.Realm;
+import kotlin.Unit;
+import kotlin.coroutines.Continuation;
+import kotlin.coroutines.CoroutineContext;
+import kotlin.coroutines.EmptyCoroutineContext;
+import kotlinx.coroutines.CoroutineScope;
+
+public class CallingVM {
+    public final CoroutineScope scope;
+    //通话时间
+    private Timer timer;
+    private int second = 0;
+    public MutableLiveData<String> timeStr = new MutableLiveData<>("");
+
+    //获取音频服务
+//    public AudioManager audioManager;
+    private DialogInterface.OnDismissListener dismissListener;
+    private OnParticipantsChangeListener onParticipantsChangeListener;
+
+    public final CallViewModel callViewModel;
+    public final CallingService callingService;
+    private VideoTrack localVideoTrack;
+    //是否是视频通话
+    public boolean isVideoCalls = true;
+    //已经开始通话
+    public boolean isStartCall;
+    //呼出
+    public boolean isCallOut;
+    //是否是群
+    public boolean isGroup;
+
+    private List<TextureViewRenderer> remoteSpeakerVideoViews, localSpeakerVideoViews;
+
+
+    public CallingVM(CallingService callingService, boolean isCallOut) {
+        this.callingService = callingService;
+        this.isCallOut = isCallOut;
+
+        callViewModel = new CallViewModel(BaseApp.inst());
+        scope = callViewModel.buildScope();
+//        audioManager = (AudioManager) BaseApp.inst().getSystemService(Context.AUDIO_SERVICE);
+    }
+
+    public void initRemoteVideoRenderer(TextureViewRenderer... viewRenderers) {
+        remoteSpeakerVideoViews = Arrays.asList(viewRenderers);
+        for (TextureViewRenderer viewRenderer : viewRenderers) {
+            callViewModel.getRoom().initVideoRenderer(viewRenderer);
+        }
+    }
+
+    public void initLocalSpeakerVideoView(TextureViewRenderer... viewRenderers) {
+        localSpeakerVideoViews = Arrays.asList(viewRenderers);
+        for (TextureViewRenderer viewRenderer : viewRenderers) {
+            callViewModel.getRoom().initVideoRenderer(viewRenderer);
+        }
+    }
+
+
+    public void setOnParticipantsChangeListener(OnParticipantsChangeListener onParticipantsChangeListener) {
+        this.onParticipantsChangeListener = onParticipantsChangeListener;
+    }
+
+    public void setDismissListener(DialogInterface.OnDismissListener dismissListener) {
+        this.dismissListener = dismissListener;
+    }
+
+
+    public void setVideoCalls(boolean videoCalls) {
+        isVideoCalls = videoCalls;
+    }
+
+    private OnBase<String> callBackDismissUI = new OnBase<String>() {
+        @Override
+        public void onError(int code, String error) {
+            L.e(CallingServiceImp.TAG, error + "-" + code);
+            dismissUI();
+        }
+
+        @Override
+        public void onSuccess(String data) {
+            dismissUI();
+        }
+    };
+
+    public void signalingInvite(SignalingInfo signalingInfo) {
+        OnBase<SignalingCertificate> certificateOnBase = new OnBase<SignalingCertificate>() {
+            @Override
+            public void onError(int code, String error) {
+                dismissUI();
+                L.e(CallingServiceImp.TAG, code+"----->"+error);
+                Toast.makeText(BaseApp.inst(), error + "(" + code + ")", Toast.LENGTH_SHORT).show();
+            }
+
+            @Override
+            public void onSuccess(SignalingCertificate data) {
+                L.e(CallingServiceImp.TAG, data.getToken());
+                connectToRoom(data);
+            }
+        };
+        if (isGroup)
+            OpenIMClient.getInstance().signalingManager.signalingInviteInGroup(certificateOnBase,
+                signalingInfo);
+        else
+            OpenIMClient.getInstance().signalingManager.signalingInvite(certificateOnBase,
+                signalingInfo);
+    }
+
+    /**
+     * 连接房间
+     *
+     * @param data
+     */
+    private void connectToRoom(SignalingCertificate data) {
+        callViewModel.connectToRoom(data.getLiveURL(), data.getToken(), new Continuation<Unit>() {
+            @NonNull
+            @Override
+            public CoroutineContext getContext() {
+                return EmptyCoroutineContext.INSTANCE;
+            }
+
+            @Override
+            public void resumeWith(@NonNull Object o) {
+                setSpeakerphoneOn(true);
+                if (!isVideoCalls) callViewModel.setCameraEnabled(false);
+
+                localVideoTrack =
+                    callViewModel.getVideoTrack(callViewModel.getRoom().getLocalParticipant());
+                if (null != localVideoTrack && null != localSpeakerVideoViews && !localSpeakerVideoViews.isEmpty()) {
+                    for (TextureViewRenderer localSpeakerVideoView : localSpeakerVideoViews) {
+                        localVideoTrack.addRenderer(localSpeakerVideoView);
+                        localSpeakerVideoView.setTag(localVideoTrack);
+                    }
+                }
+                callViewModel.subscribe(callViewModel.getAllParticipants(), (v) -> {
+                    if (v.isEmpty()) return null;
+                    if (null != onParticipantsChangeListener) {
+                        onParticipantsChangeListener.onChange(v);
+                    } else {
+                        for (int i = 0; i < v.size(); i++) {
+                            Participant participant = v.get(i);
+                            if (participant instanceof RemoteParticipant) {
+                                for (TextureViewRenderer remoteSpeakerVideoView :
+                                    remoteSpeakerVideoViews) {
+                                    callViewModel.bindRemoteViewRenderer(remoteSpeakerVideoView,
+                                        participant, scope, new Continuation<Unit>() {
+                                        @NonNull
+                                        @Override
+                                        public CoroutineContext getContext() {
+                                            return EmptyCoroutineContext.INSTANCE;
+                                        }
+
+                                        @Override
+                                        public void resumeWith(@NonNull Object o) {
+                                        }
+                                    });
+                                }
+                            }
+                        }
+                    }
+
+                    return null;
+                }, scope);
+            }
+        });
+    }
+
+    public void buildTimer() {
+        cancelTimer();
+        timer = new Timer();
+
+        timer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                second++;
+                String secondFormat = TimeUtil.secondFormat(second, TimeUtil.secondFormat);
+                if (secondFormat.length() <= 2) secondFormat = "00:" + secondFormat;
+                timeStr.postValue(secondFormat);
+            }
+        }, 0, 1000);
+    }
+
+    private String repair0(int v) {
+        return v < 10 ? ("0" + v) : (v + "");
+    }
+
+    private void cancelTimer() {
+        if (null != timer) {
+            timer.cancel();
+            timer = null;
+        }
+    }
+
+    public void signalingHungUp(SignalingInfo signalingInfo) {
+        Common.UIHandler.postDelayed(this::dismissUI, 18 * 1000);
+        if (!isStartCall) {
+            signalingCancel(signalingInfo);
+            return;
+        }
+        OpenIMClient.getInstance().signalingManager.signalingHungUp(callBackDismissUI,
+            signalingInfo);
+    }
+
+    private void dismissUI() {
+        if (null != dismissListener) dismissListener.onDismiss(null);
+    }
+
+    private void signalingCancel(SignalingInfo signalingInfo) {
+        if (isCallOut) {
+            renewalDB(buildPrimaryKey(signalingInfo), (realm, v) -> v.setFailedState(1));
+            OpenIMClient.getInstance().signalingManager.signalingCancel(callBackDismissUI,
+                signalingInfo);
+        } else {
+            renewalDB(buildPrimaryKey(signalingInfo), (realm, v) -> v.setFailedState(3));
+            OpenIMClient.getInstance().signalingManager.signalingReject(callBackDismissUI,
+                signalingInfo);
+        }
+    }
+
+    public static String buildPrimaryKey(SignalingInfo signalingInfo) {
+        return signalingInfo.getInvitation().getRoomID() + signalingInfo.getInvitation().getInitiateTime();
+    }
+
+    public void signalingAccept(SignalingInfo signalingInfo, OnBase onBase) {
+        OpenIMClient.getInstance().signalingManager.signalingAccept(new OnBase<SignalingCertificate>() {
+            @Override
+            public void onError(int code, String error) {
+//                Toast.makeText(BaseApp.inst(), "加入会议失败,服务器错误(" + code + ")", Toast.LENGTH_LONG).show();
+                L.e(CallingServiceImp.TAG, error + code);
+                dismissUI();
+            }
+
+            @Override
+            public void onSuccess(SignalingCertificate data) {
+                L.e(CallingServiceImp.TAG, data.getToken());
+                MediaPlayerUtil.INSTANCE.pause();
+                MediaPlayerUtil.INSTANCE.release();
+
+                isStartCall = true;
+                onBase.onSuccess(null);
+                connectToRoom(data);
+                buildTimer();
+            }
+        }, signalingInfo);
+    }
+
+    public void unBindView() {
+        try {
+            cancelTimer();
+            if (null != localVideoTrack) {
+                if (null != localSpeakerVideoViews) {
+                    for (TextureViewRenderer localSpeakerVideoView : localSpeakerVideoViews) {
+                        localSpeakerVideoView.release();
+                        localVideoTrack.removeRenderer(localSpeakerVideoView);
+                    }
+                }
+            }
+            for (TextureViewRenderer textureViewRenderer : remoteSpeakerVideoViews) {
+                textureViewRenderer.release();
+                Object videoTask = textureViewRenderer.getTag();
+                if (null != videoTask) {
+                    ((VideoTrack) videoTask).removeRenderer(textureViewRenderer);
+                }
+            }
+            callViewModel.onCleared();
+            L.e("unBindView");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void setSpeakerphoneOn(boolean isChecked) {
+        callViewModel.getAudioHandler().selectDevice(isChecked ?
+            new AudioDevice.Speakerphone() :
+            new AudioDevice.Earpiece());
+    }
+
+
+    public interface OnParticipantsChangeListener {
+        void onChange(List<Participant> participants);
+    }
+
+
+    public void renewalDB(String id, OnRenewalDBListener onRenewalDBListener) {
+        BaseApp.inst().realm.executeTransactionAsync(realm -> {
+            CallHistory callHistory = realm.where(CallHistory.class).equalTo("id", id).findFirst();
+            if (null == callHistory) return;
+            onRenewalDBListener.onRenewal(realm, callHistory);
+        });
+    }
+
+
+    public interface OnRenewalDBListener {
+        void onRenewal(Realm realm, CallHistory callHistory);
+    }
+}

+ 6 - 0
OUICalling/src/main/res/drawable/selector_camera.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@mipmap/ic_open_camera" android:state_focused="true" />
+    <item android:drawable="@mipmap/ic_open_camera" android:state_checked="true" />
+    <item android:drawable="@mipmap/ic_close_camera" />
+</selector>

+ 6 - 0
OUICalling/src/main/res/drawable/selector_mic.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@mipmap/ic_mic_on" android:state_focused="true" />
+    <item android:drawable="@mipmap/ic_mic_on" android:state_checked="true" />
+    <item android:drawable="@mipmap/ic_mic_off" />
+</selector>

+ 6 - 0
OUICalling/src/main/res/drawable/selector_speaker.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@mipmap/ic_speaker_on" android:state_focused="true" />
+    <item android:drawable="@mipmap/ic_speaker_on" android:state_checked="true" />
+    <item android:drawable="@mipmap/ic_speaker_off" />
+</selector>

+ 18 - 0
OUICalling/src/main/res/layout/activity_demo.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/holo_red_light"
+    tools:context=".LockPushActivity">
+
+    <TextView
+        android:id="@+id/textView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="TextView"
+        tools:layout_editor_absoluteX="188dp"
+        tools:layout_editor_absoluteY="195dp"
+        tools:ignore="MissingConstraints" />
+</LinearLayout>

+ 330 - 0
OUICalling/src/main/res/layout/dialog_call.xml

@@ -0,0 +1,330 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipToPadding="true"
+        android:fitsSystemWindows="true"
+        android:keepScreenOn="true"
+        >
+
+        <RelativeLayout
+            android:id="@+id/home"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:alpha="0.8"
+            android:background="#000000"
+            tools:context="io.openim.android.ouicalling.CallDialog">
+
+
+            <io.livekit.android.renderer.TextureViewRenderer
+                android:id="@+id/remoteSpeakerVideoView"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:layout_marginTop="28dp"
+                android:layout_marginEnd="15dp"
+                android:gravity="right"
+                android:orientation="vertical">
+
+                <LinearLayout
+                    android:id="@+id/cameraControl"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:gravity="center"
+                    android:layout_marginBottom="10dp"
+                    android:orientation="horizontal">
+
+                    <CheckBox
+                        android:id="@+id/closeCamera"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginEnd="10dp"
+                        android:padding="5dp"
+                        android:button="@null"
+                        android:background="@drawable/selector_camera" />
+
+                    <CheckBox
+                        android:id="@+id/switchCamera"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:background="@mipmap/ic_switch_camera"
+                        android:button="@null"
+                        android:padding="5dp" />
+                </LinearLayout>
+
+
+                <io.livekit.android.renderer.TextureViewRenderer
+                    android:id="@+id/localSpeakerVideoView"
+                    android:layout_width="107dp"
+                    android:layout_height="197dp" />
+
+            </LinearLayout>
+
+
+            <ImageView
+                android:id="@+id/zoomOut"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="22dp"
+                android:layout_marginTop="35dp"
+                android:src="@mipmap/ic_zoom_out" />
+
+            <TextView
+                android:id="@+id/timeTv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_centerHorizontal="true"
+                android:layout_marginTop="40dp"
+                android:textColor="@color/white"
+                android:textSize="18sp" />
+
+
+            <LinearLayout
+                android:id="@+id/headTips"
+                android:layout_below="@id/zoomOut"
+                android:layout_marginStart="30dp"
+                android:layout_marginTop="50dp"
+                android:layout_centerInParent="true"
+                android:layout_width="wrap_content"
+                android:gravity="center"
+                android:layout_height="wrap_content"
+                android:orientation="vertical">
+
+                <io.openim.android.ouicore.widget.AvatarImage
+                    android:id="@+id/avatar"
+                    android:layout_width="78dp"
+                    android:layout_height="78dp"
+                    android:layout_marginBottom="10dp"
+                    android:background="@mipmap/ic_my_friend" />
+
+                <TextView
+                    android:id="@+id/name"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="10dp"
+                    android:textColor="#ffffffff"
+                    android:textSize="32sp" />
+
+                <TextView
+                    android:id="@+id/callingTips"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/Invite_a_video_call"
+                    android:textColor="#ffffffff"
+                    android:textSize="14sp" />
+            </LinearLayout>
+
+
+            <LinearLayout
+                android:id="@+id/audioCall"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@id/zoomOut"
+                android:layout_centerHorizontal="true"
+                android:layout_marginTop="35dp"
+                android:gravity="center_horizontal"
+                android:orientation="vertical"
+                android:visibility="gone">
+
+                <io.openim.android.ouicore.widget.AvatarImage
+                    android:id="@+id/avatar2"
+                    android:layout_width="78dp"
+                    android:layout_height="78dp"
+                    android:src="@mipmap/ic_my_friend" />
+
+                <TextView
+                    android:id="@+id/name2"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="15dp"
+                    android:layout_marginBottom="29dp"
+                    android:textColor="#ffffffff"
+                    android:textSize="24sp" />
+
+                <TextView
+                    android:id="@+id/callingTips2"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/Invite_a_audio_call"
+                    android:textColor="#ffffffff"
+                    android:textSize="18sp" />
+            </LinearLayout>
+
+
+            <LinearLayout
+                android:id="@+id/ask"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentBottom="true"
+                android:layout_centerHorizontal="true"
+                android:layout_marginBottom="68dp"
+                android:orientation="horizontal">
+
+                <LinearLayout
+                    android:id="@+id/reject"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:gravity="center_horizontal"
+                    android:orientation="vertical">
+
+                    <ImageView
+                        android:layout_width="64dp"
+                        android:layout_height="64dp"
+                        android:layout_marginBottom="15dp"
+                        android:background="@drawable/sty_radius_30_red"
+                        android:padding="20dp"
+                        android:src="@mipmap/ic_hang_up" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:shadowColor="#80000000"
+                        android:text="@string/hang_up"
+                        android:textColor="#ffffffff"
+                        android:textSize="16sp" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:id="@+id/answer"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="100dp"
+                    android:gravity="center_horizontal"
+                    android:orientation="vertical">
+
+                    <ImageView
+                        android:layout_width="64dp"
+                        android:layout_height="64dp"
+                        android:layout_marginBottom="15dp"
+                        android:background="@drawable/sty_radius_30_29e3a0"
+                        android:padding="20dp"
+                        android:src="@mipmap/ic_answer" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:shadowColor="#80000000"
+                        android:text="@string/answer"
+                        android:textColor="#ffffffff"
+                        android:textSize="16sp" />
+                </LinearLayout>
+            </LinearLayout>
+
+            <LinearLayout
+                android:id="@+id/callingMenu"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_alignParentBottom="true"
+                android:layout_centerHorizontal="true"
+                android:layout_marginLeft="45dp"
+                android:layout_marginRight="45dp"
+                android:layout_marginBottom="68dp"
+                android:orientation="horizontal"
+                android:visibility="gone">
+
+                <CheckBox
+                    android:id="@+id/micIsOn"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:button="@null"
+                    android:drawableTop="@drawable/selector_mic"
+                    android:drawablePadding="15dp"
+                    android:text="@string/microphone_on"
+                    android:checked="true"
+                    android:textColor="#ffffffff"
+                    android:textSize="16sp" />
+
+                <LinearLayout
+                    android:id="@+id/hangUp"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:gravity="center_horizontal"
+                    android:orientation="vertical">
+
+                    <ImageView
+                        android:layout_width="64dp"
+                        android:layout_height="64dp"
+                        android:layout_marginBottom="15dp"
+                        android:background="@drawable/sty_radius_30_red"
+                        android:padding="20dp"
+                        android:src="@mipmap/ic_hang_up" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:shadowColor="#80000000"
+                        android:text="@string/hang_up"
+                        android:textColor="#ffffffff"
+                        android:textSize="16sp" />
+                </LinearLayout>
+
+                <CheckBox
+                    android:id="@+id/speakerIsOn"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:button="@null"
+                    android:drawableTop="@drawable/selector_speaker"
+                    android:drawablePadding="15dp"
+                    android:text="@string/speaker_on"
+                    android:checked="true"
+                    android:textColor="#ffffffff"
+                    android:textSize="16sp" />
+
+            </LinearLayout>
+
+
+        </RelativeLayout>
+
+        <RelativeLayout
+            android:id="@+id/shrink"
+            android:layout_width="107dp"
+            android:layout_height="180dp"
+            android:layout_marginTop="48dp"
+            android:layout_marginRight="13dp"
+            android:background="@drawable/sty_radius_6_000000"
+            android:clipToPadding="true"
+            android:fitsSystemWindows="true"
+            android:gravity="center"
+            android:keepScreenOn="true"
+            android:orientation="vertical"
+            android:visibility="gone">
+
+            <io.livekit.android.renderer.TextureViewRenderer
+                android:id="@+id/remoteSpeakerVideoView2"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+
+            <LinearLayout
+                android:id="@+id/waiting"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:gravity="center"
+                android:orientation="vertical">
+
+                <io.openim.android.ouicore.widget.AvatarImage
+                    android:id="@+id/sAvatar"
+                    android:layout_width="68dp"
+                    android:layout_height="68dp"
+                    android:layout_marginBottom="10dp"
+                    android:src="@mipmap/ic_my_friend" />
+
+                <TextView
+                    android:id="@+id/sTips"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textColor="#ffffffff"
+                    android:textSize="12sp" />
+            </LinearLayout>
+        </RelativeLayout>
+
+    </RelativeLayout>
+
+</layout>

+ 311 - 0
OUICalling/src/main/res/layout/dialog_group_call.xml

@@ -0,0 +1,311 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:tools="http://schemas.android.com/tools"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipToPadding="true"
+        android:fitsSystemWindows="true"
+        android:keepScreenOn="true">
+
+        <RelativeLayout
+            android:id="@+id/home"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:alpha="0.8"
+            android:background="#000000"
+            android:clipToPadding="true"
+            android:fitsSystemWindows="true"
+            android:keepScreenOn="true"
+            tools:context="io.openim.android.ouicalling.CallDialog">
+
+            <ImageView
+                android:id="@+id/zoomOut"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="20dp"
+                android:layout_marginTop="12dp"
+                android:layout_marginBottom="10dp"
+                android:src="@mipmap/ic_zoom_out" />
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentEnd="true"
+                android:layout_marginEnd="15dp"
+                android:gravity="right"
+                android:orientation="vertical">
+
+                <LinearLayout
+                    android:id="@+id/cameraControl"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="12dp"
+                    android:layout_marginBottom="10dp"
+                    android:gravity="center"
+                    android:orientation="horizontal">
+
+                    <CheckBox
+                        android:id="@+id/closeCamera"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginEnd="10dp"
+                        android:background="@drawable/selector_camera"
+                        android:button="@null"
+                        android:padding="5dp"
+                        />
+
+                    <CheckBox
+                        android:id="@+id/switchCamera"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:background="@mipmap/ic_switch_camera"
+                        android:button="@null"
+                        android:padding="5dp" />
+                </LinearLayout>
+
+
+                <io.livekit.android.renderer.TextureViewRenderer
+                    android:id="@+id/localSpeakerVideoView"
+                    android:layout_width="107dp"
+                    android:layout_height="197dp" />
+
+            </LinearLayout>
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/viewRenderers"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_below="@id/zoomOut" />
+
+            <LinearLayout
+                android:id="@+id/headTips"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_below="@id/zoomOut"
+                android:orientation="vertical"
+                android:visibility="visible">
+
+                <LinearLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="30dp"
+                    android:layout_marginTop="35dp"
+                    android:orientation="horizontal"
+                    android:visibility="visible">
+
+                    <io.openim.android.ouicore.widget.AvatarImage
+                        android:id="@+id/avatar"
+                        android:layout_width="48dp"
+                        android:layout_height="48dp"
+                        />
+
+                    <LinearLayout
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginLeft="12dp"
+                        android:orientation="vertical">
+
+                        <TextView
+                            android:id="@+id/tips1"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:textColor="#ffffffff"
+                            android:textSize="20sp" />
+
+                        <TextView
+                            android:id="@+id/tips2"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:textColor="#ffffffff"
+                            android:textSize="12sp" />
+                    </LinearLayout>
+
+                </LinearLayout>
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/memberRecyclerView"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_below="@id/headTips"
+                    android:padding="20dp" />
+            </LinearLayout>
+
+
+            <LinearLayout
+                android:id="@+id/ask"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentBottom="true"
+                android:layout_centerHorizontal="true"
+                android:layout_marginBottom="68dp"
+                android:orientation="horizontal">
+
+                <LinearLayout
+                    android:id="@+id/reject"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:gravity="center_horizontal"
+                    android:orientation="vertical">
+
+                    <ImageView
+                        android:layout_width="64dp"
+                        android:layout_height="64dp"
+                        android:layout_marginBottom="15dp"
+                        android:background="@drawable/sty_radius_30_red"
+                        android:padding="20dp"
+                        android:src="@mipmap/ic_hang_up" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:shadowColor="#80000000"
+                        android:text="@string/hang_up"
+                        android:textColor="#ffffffff"
+                        android:textSize="16sp" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:id="@+id/answer"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="100dp"
+                    android:gravity="center_horizontal"
+                    android:orientation="vertical">
+
+                    <ImageView
+                        android:layout_width="64dp"
+                        android:layout_height="64dp"
+                        android:layout_marginBottom="15dp"
+                        android:background="@drawable/sty_radius_30_29e3a0"
+                        android:padding="20dp"
+                        android:src="@mipmap/ic_answer" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:shadowColor="#80000000"
+                        android:text="@string/answer"
+                        android:textColor="#ffffffff"
+                        android:textSize="16sp" />
+                </LinearLayout>
+            </LinearLayout>
+
+
+            <TextView
+                android:id="@+id/timeTv"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_centerHorizontal="true"
+                android:layout_marginTop="15dp"
+                android:layout_marginBottom="20dp"
+                android:textColor="@color/white"
+                android:textSize="18sp" />
+
+            <LinearLayout
+                android:id="@+id/callingMenu"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_alignParentBottom="true"
+                android:layout_centerHorizontal="true"
+                android:layout_marginLeft="45dp"
+                android:layout_marginRight="45dp"
+                android:layout_marginBottom="68dp"
+                android:orientation="horizontal"
+                android:visibility="gone">
+
+                <CheckBox
+                    android:id="@+id/micIsOn"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:button="@null"
+                    android:checked="true"
+                    android:drawableTop="@drawable/selector_mic"
+                    android:drawablePadding="15dp"
+                    android:text="@string/microphone_on"
+                    android:textColor="#ffffffff"
+                    android:textSize="16sp" />
+
+                <LinearLayout
+                    android:id="@+id/hangUp"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:gravity="center_horizontal"
+                    android:orientation="vertical">
+
+                    <ImageView
+                        android:layout_width="64dp"
+                        android:layout_height="64dp"
+                        android:layout_marginBottom="15dp"
+                        android:background="@drawable/sty_radius_30_red"
+                        android:padding="20dp"
+                        android:src="@mipmap/ic_hang_up" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:shadowColor="#80000000"
+                        android:text="@string/hang_up"
+                        android:textColor="#ffffffff"
+                        android:textSize="16sp" />
+                </LinearLayout>
+
+                <CheckBox
+                    android:id="@+id/speakerIsOn"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:button="@null"
+                    android:checked="true"
+                    android:drawableTop="@drawable/selector_speaker"
+                    android:drawablePadding="15dp"
+                    android:text="@string/speaker_on"
+                    android:textColor="#ffffffff"
+                    android:textSize="16sp" />
+
+            </LinearLayout>
+
+        </RelativeLayout>
+
+        <RelativeLayout
+            android:id="@+id/shrink"
+            android:layout_width="107dp"
+            android:layout_height="197dp"
+            android:layout_marginTop="48dp"
+            android:layout_marginRight="13dp"
+            android:alpha="0.8"
+            android:background="@drawable/sty_radius_6_000000"
+            android:clipToPadding="true"
+            android:gravity="center"
+            android:keepScreenOn="true"
+            android:orientation="vertical"
+            android:visibility="gone">
+
+
+            <LinearLayout
+                android:id="@+id/waiting"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:gravity="center"
+                android:orientation="vertical">
+
+                <io.openim.android.ouicore.widget.AvatarImage
+                    android:id="@+id/sAvatar"
+                    android:layout_width="68dp"
+                    android:layout_height="68dp"
+                    android:layout_marginBottom="10dp"
+                    android:src="@mipmap/ic_my_group" />
+
+                <TextView
+                    android:id="@+id/sTips"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/meeting"
+                    android:textColor="#ffffffff"
+                    android:textSize="12sp" />
+            </LinearLayout>
+        </RelativeLayout>
+    </RelativeLayout>
+
+
+</layout>

+ 60 - 0
OUICalling/src/main/res/layout/item_member_renderer.xml

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="162dp"
+    android:paddingLeft="2dp"
+    android:paddingRight="2dp">
+
+    <io.livekit.android.renderer.TextureViewRenderer
+        android:id="@+id/remoteSpeakerVideoView"
+        android:layout_width="match_parent"
+        android:layout_height="162dp" />
+
+    <RelativeLayout
+        android:id="@+id/avatarRl"
+        android:layout_width="match_parent"
+        android:layout_height="162dp"
+        android:visibility="visible"
+        android:background="@color/white">
+
+        <io.openim.android.ouicore.widget.AvatarImage
+            android:id="@+id/avatar"
+            android:layout_width="100dp"
+            android:layout_height="100dp"
+            android:layout_centerHorizontal="true"
+            android:layout_centerVertical="true"
+            android:src="@mipmap/ic_my_friend" />
+
+    </RelativeLayout>
+
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:gravity="right|center"
+        android:orientation="horizontal"
+        android:paddingTop="6dp"
+        android:paddingRight="10dp"
+        android:paddingBottom="6dp"
+        >
+
+        <TextView
+            android:id="@+id/name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="8dp"
+            android:textColor="@color/white"
+            android:textSize="@dimen/description_txt" />
+
+        <ImageView
+            android:id="@+id/micOn"
+            android:background="@drawable/sty_radius_4_33shallow"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="4dp"
+            android:alpha="1"
+            android:src="@mipmap/ic_mic_s_on" />
+    </LinearLayout>
+</RelativeLayout>

+ 43 - 0
OUICalling/src/main/res/layout/layout_call_invite.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    tools:context=".LockPushActivity">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@drawable/sty_radius_6_white"
+        android:gravity="center_vertical"
+        android:padding="10dp">
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@mipmap/ic_launcher" />
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="10dp"
+            android:orientation="vertical">
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="OpenIM"
+                android:textSize="15sp"
+                android:textStyle="bold" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/receive_call_invite"
+                android:textColor="@color/txt_shallow"
+                android:textSize="13sp" />
+        </LinearLayout>
+    </LinearLayout>
+
+
+</FrameLayout>

+ 47 - 0
OUICalling/src/main/res/layout/layout_float_view.xml

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <io.openim.android.ouicore.widget.InterceptLinearLayout
+        android:id="@+id/shrink"
+        android:layout_width="107dp"
+        android:layout_height="160dp"
+        android:background="@drawable/sty_radius_6_000000"
+        android:orientation="vertical">
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <io.livekit.android.renderer.TextureViewRenderer
+                android:id="@+id/shrinkRemoteSpeakerVideoView"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+
+            <LinearLayout
+                android:id="@+id/waiting"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:gravity="center"
+                android:orientation="vertical">
+
+                <io.openim.android.ouicore.widget.AvatarImage
+                    android:id="@+id/sAvatar"
+                    android:layout_width="68dp"
+                    android:layout_height="68dp"
+                    android:layout_marginBottom="10dp"
+                    android:src="@mipmap/ic_my_friend" />
+
+                <TextView
+                    android:id="@+id/sTips"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="等待接通"
+                    android:textColor="#ffffffff"
+                    android:textSize="12sp" />
+            </LinearLayout>
+        </RelativeLayout>
+
+    </io.openim.android.ouicore.widget.InterceptLinearLayout>
+</FrameLayout>

BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_answer.png


BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_close_camera.png


BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_hang_up.png


BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_mic_off.png


BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_mic_on.png


BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_mic_s_off.png


BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_mic_s_on.png


BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_open_camera.png


BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_speaker_off.png


BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_speaker_on.png


BIN
OUICalling/src/main/res/mipmap-xxhdpi/ic_switch_camera.png


BIN
OUICalling/src/main/res/raw/incoming_call_ring.mp3


BIN
OUICalling/src/main/res/raw/outgoing_call_ring.mp3


+ 17 - 0
OUICalling/src/test/java/io/openim/android/ouilive/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package io.openim.android.ouilive;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 68 - 0
OUIContact/build.gradle

@@ -0,0 +1,68 @@
+if (isModule) {
+    apply plugin: 'com.android.application'
+} else {
+    apply plugin: 'com.android.library'
+}
+
+android {
+    viewBinding {
+        enabled = true
+    }
+    dataBinding{
+        enabled = true
+    }
+
+
+    compileSdk rootProject.ext.androidConfig.compileSdk
+
+    defaultConfig {
+        if (isModule) {
+            applicationId rootProject.ext.applicationId.OUIContact
+            sourceSets {
+                main {
+                    // 组件模式下调试
+                    manifest.srcFile 'src/main/debug/AndroidManifest.xml'
+                }
+            }
+        }
+        minSdk rootProject.ext.androidConfig.minSdk
+        targetSdk rootProject.ext.androidConfig.targetSdk
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+
+        javaCompileOptions {
+            annotationProcessorOptions {
+                arguments = [AROUTER_MODULE_NAME: project.getName()]
+            }
+        }
+    }
+
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+
+    implementation 'androidx.appcompat:appcompat:1.3.0'
+    implementation 'com.google.android.material:material:1.4.0'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+    implementation project(path: ':OUICore')
+    implementation 'com.alibaba:arouter-api:1.5.2'
+    annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'
+
+    implementation 'q.rorbin:badgeview:1.1.3'
+}

+ 0 - 0
OUIContact/consumer-rules.pro


+ 21 - 0
OUIContact/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

+ 26 - 0
OUIContact/src/androidTest/java/io/openim/android/ouicontact/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package io.openim.android.ouicontact;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("io.openim.android.ouicontact.test", appContext.getPackageName());
+    }
+}

+ 53 - 0
OUIContact/src/main/AndroidManifest.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.openim.android.ouicontact">
+
+    <application>
+        <activity
+            android:name=".ui.AddRelationActivity"
+            android:exported="false"/>
+        <activity
+            android:name=".ui.search.SearchGroupActivity"
+            android:exported="false"/>
+        <activity
+            android:name=".ui.search.SearchGroupAndFriendsActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.CreateLabelActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.LabelActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.search.SearchGroupMemberActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.MyGroupActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.ForwardToActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.AllFriendActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.FriendRequestDetailActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.NewFriendActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.GroupNoticeListActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.GroupNoticeDetailActivity"
+            android:exported="false" />
+        <activity
+            android:name=".ui.GroupNoticeInvitedDetailActivity"
+            android:exported="false" />
+        <activity
+            android:name=".DebugActivity"
+            android:exported="true" />
+    </application>
+
+</manifest>

+ 17 - 0
OUIContact/src/main/debug/AndroidManifest.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.openim.android.ouicontact">
+
+    <application android:theme="@style/Theme.Main">
+        <activity
+            android:name=".DebugActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>

+ 19 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/DebugActivity.java

@@ -0,0 +1,19 @@
+package io.openim.android.ouicontact;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.os.Bundle;
+
+import io.openim.android.ouicontact.ui.fragment.ContactFragment;
+
+public class DebugActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_debug);
+
+        getSupportFragmentManager().beginTransaction()
+            .add(R.id.fragment_container, new ContactFragment()).commit();
+    }
+}

+ 90 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/AddRelationActivity.java

@@ -0,0 +1,90 @@
+package io.openim.android.ouicontact.ui;
+
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.activity.result.ActivityResultLauncher;
+
+import com.alibaba.android.arouter.launcher.ARouter;
+import com.hjq.permissions.Permission;
+
+import java.util.List;
+
+import io.openim.android.ouicontact.databinding.ActivityAddRelationBinding;
+import io.openim.android.ouicore.base.BaseApp;
+import io.openim.android.ouicore.base.BasicActivity;
+import io.openim.android.ouicore.base.vm.injection.Easy;
+import io.openim.android.ouicore.ex.MultipleChoice;
+import io.openim.android.ouicore.utils.Common;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.HasPermissions;
+import io.openim.android.ouicore.utils.OnDedrepClickListener;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.vm.GroupVM;
+import io.openim.android.ouicore.vm.SelectTargetVM;
+import io.openim.android.sdk.models.FriendInfo;
+
+/**
+ * 添加
+ * 扫一扫 添加群聊/好友  发起群聊
+ */
+public class AddRelationActivity extends BasicActivity<ActivityAddRelationBinding> {
+
+    private HasPermissions hasScanPermission;
+    private final ActivityResultLauncher<Intent> resultLauncher = Common.getCaptureActivityLauncher(this);
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        viewBinding(ActivityAddRelationBinding.inflate(getLayoutInflater()));
+        click();
+        runOnUiThread(() -> hasScanPermission = new HasPermissions(AddRelationActivity.this, Permission.CAMERA, Permission.READ_EXTERNAL_STORAGE));
+    }
+
+    private void click() {
+        view.san.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+                hasScanPermission.safeGo(() -> Common.jumpScan(AddRelationActivity.this, resultLauncher));
+            }
+        });
+        view.addFriend.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+                ARouter.getInstance().build(Routes.Main.ADD_CONVERS).navigation();
+            }
+        });
+        view.createGroup.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+                SelectTargetVM targetVM = Easy.installVM(SelectTargetVM.class).setIntention(SelectTargetVM.Intention.isCreateGroup);
+                targetVM.setOnFinishListener(() -> {
+                    GroupVM groupVM = BaseApp.inst().getVMByCache(GroupVM.class);
+                    if (null == groupVM)
+                        groupVM = new GroupVM();
+                    groupVM.selectedFriendInfo.getValue().clear();
+                    List<MultipleChoice> multipleChoices = targetVM.metaData.getValue();
+                    for (int i = 0; i < multipleChoices.size(); i++) {
+                        MultipleChoice us = multipleChoices.get(i);
+                        FriendInfo friendInfo = new FriendInfo();
+                        friendInfo.setUserID(us.key);
+                        friendInfo.setNickname(us.name);
+                        friendInfo.setFaceURL(us.icon);
+                        groupVM.selectedFriendInfo.getValue().add(friendInfo);
+                    }
+                    BaseApp.inst().putVM(groupVM);
+                    ARouter.getInstance().build(Routes.Group.CREATE_GROUP2).navigation();
+                });
+                ARouter.getInstance().build(Routes.Group.SELECT_TARGET).navigation();
+            }
+        });
+        view.addGroup.setOnClickListener(new OnDedrepClickListener() {
+            @Override
+            public void click(View v) {
+                ARouter.getInstance().build(Routes.Main.ADD_CONVERS).withBoolean(Constants.K_RESULT, false).navigation();
+            }
+        });
+    }
+}

+ 269 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/AllFriendActivity.java

@@ -0,0 +1,269 @@
+package io.openim.android.ouicontact.ui;
+
+import android.content.Intent;
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.alibaba.android.arouter.facade.annotation.Route;
+import com.alibaba.android.arouter.launcher.ARouter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import io.openim.android.ouicontact.databinding.ActivityAllFriendBinding;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.base.vm.injection.Easy;
+import io.openim.android.ouicore.databinding.LayoutPopSelectedFriendsBinding;
+import io.openim.android.ouicore.entity.ExUserInfo;
+import io.openim.android.ouicore.ex.MultipleChoice;
+import io.openim.android.ouicore.utils.Common;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.vm.SelectTargetVM;
+import io.openim.android.ouicore.vm.SocialityVM;
+import io.openim.android.ouicore.widget.StickyDecoration;
+import io.openim.android.sdk.models.FriendInfo;
+
+/**
+ * 我的好友
+ */
+@Route(path = Routes.Contact.ALL_FRIEND)
+public class AllFriendActivity extends BaseActivity<SocialityVM, ActivityAllFriendBinding> {
+    private String mLetters = "↑ABCDEFGHIJKLMNOPQRSTUVWXYZ#";//默认字符
+    /*☆*/
+    private RecyclerViewAdapter<ExUserInfo, RecyclerView.ViewHolder> adapter;
+    private LinearLayoutManager mLayoutManager;
+
+    private SelectTargetVM selectTargetVM;
+    private ActivityResultLauncher<Intent> launcher =
+            registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), v -> {
+                if (v.getResultCode() != RESULT_OK) return;
+                Intent intent = v.getData();
+                Set<MultipleChoice> set = (Set<MultipleChoice>) intent.getSerializableExtra(Constants.K_RESULT);
+                for (MultipleChoice data : set) {
+                    if (data.isSelect) {
+                        if (!selectTargetVM.contains(data)) {
+                            selectTargetVM.metaData.val().add(data);
+                            selectTargetVM.metaData.update();
+                        }
+                    } else {
+                        selectTargetVM.removeMetaData(data.key);
+                    }
+                }
+            });
+
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVM(SocialityVM.class);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityAllFriendBinding.inflate(getLayoutInflater()));
+        sink();
+        init();
+        vm.getAllFriend();
+
+        listener();
+        initView();
+    }
+
+    void init() {
+        try {
+            selectTargetVM = Easy.find(SelectTargetVM.class);
+            view.searchView.setVisibility(View.VISIBLE);
+            if (!selectTargetVM.isSingleSelect()) {
+                view.bottom.getRoot().setVisibility(View.VISIBLE);
+                selectTargetVM.bindDataToView(view.bottom);
+                selectTargetVM.showPopAllSelectFriends(view.bottom, LayoutPopSelectedFriendsBinding.inflate(getLayoutInflater()));
+                selectTargetVM.submitTap(view.bottom.submit);
+            }
+
+            selectTargetVM.metaData.observe(this, v -> {
+                if (null != adapter)
+                    adapter.notifyDataSetChanged();
+            });
+        } catch (Exception ignore) {
+        }
+    }
+
+    private void initView() {
+        view.recyclerView.setLayoutManager(mLayoutManager = new LinearLayoutManager(this) {
+            @Override
+            public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
+                LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
+                    @Override
+                    protected int calculateTimeForScrolling(int dx) {
+                        // 此函数计算滚动dx的距离需要多久,当要滚动的距离很大时,比如说52000,
+                        // 经测试,系统会多次调用此函数,每10000距离调一次,所以总的滚动时间
+                        // 是多次调用此函数返回的时间的和,所以修改每次调用该函数时返回的时间的
+                        // 大小就可以影响滚动需要的总时间,可以直接修改些函数的返回值,也可以修改
+                        // dx的值,这里暂定使用后者.
+                        // (See LinearSmoothScroller.TARGET_SEEK_SCROLL_DISTANCE_PX)
+                        if (dx > 1500) {
+                            dx = 1500;
+                        }
+                        return super.calculateTimeForScrolling(dx);
+                    }
+
+                    @Override
+                    public PointF computeScrollVectorForPosition(int targetPosition) {
+                        return mLayoutManager.computeScrollVectorForPosition(targetPosition);
+                    }
+                };
+                linearSmoothScroller.setTargetPosition(position);
+                startSmoothScroll(linearSmoothScroller);
+            }
+        });
+        adapter = new RecyclerViewAdapter<ExUserInfo, RecyclerView.ViewHolder>() {
+
+            @NonNull
+            @Override
+            public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+                return new ViewHol.ItemViewHo(parent);
+            }
+
+            @Override
+            public void onBindView(@NonNull RecyclerView.ViewHolder holder, ExUserInfo data, int position) {
+                ViewHol.ItemViewHo itemViewHo = (ViewHol.ItemViewHo) holder;
+                FriendInfo friendInfo = data.userInfo.getFriendInfo();
+                itemViewHo.view.avatar.load(friendInfo.getFaceURL(),friendInfo.getNickname());
+                itemViewHo.view.nickName.setText(friendInfo.getNickname());
+
+                MultipleChoice target = null;
+                if (null != selectTargetVM && !selectTargetVM.isSingleSelect()) {
+                    itemViewHo.view.select.setVisibility(View.VISIBLE);
+                    itemViewHo.view.select.setChecked(selectTargetVM.contains(new MultipleChoice(friendInfo.getUserID())));
+                    int index = selectTargetVM.metaData.val().indexOf(new MultipleChoice(friendInfo.getUserID()));
+
+                    if (index != -1) {
+                        target = selectTargetVM.metaData.val().get(index);
+                        itemViewHo.view.select.setEnabled(target.isEnabled);
+                        itemViewHo.view.select.setAlpha(target.isEnabled ? 1f : 0.5f);
+                    }
+                }
+                MultipleChoice finalTarget = target;
+                itemViewHo.view.getRoot().setOnClickListener(v -> {
+                    if (null != selectTargetVM) {
+                        if (selectTargetVM.isSingleSelect()) {
+                            selectTargetVM.addMetaData(friendInfo.getUserID(), friendInfo.getNickname(), friendInfo.getFaceURL());
+
+                            selectTargetVM.toFinish();
+                            return;
+                        }
+                        if (null != finalTarget && !finalTarget.isEnabled) return;
+                        itemViewHo.view.select.setChecked(!itemViewHo.view.select.isChecked());
+
+                        if (itemViewHo.view.select.isChecked()) {
+                            MultipleChoice meta = new MultipleChoice(friendInfo.getUserID());
+                            meta.name = friendInfo.getNickname();
+                            meta.icon = friendInfo.getFaceURL();
+                            meta.isSelect = true;
+                            selectTargetVM.addDate(meta);
+                        } else {
+                            selectTargetVM.removeMetaData(friendInfo.getUserID());
+                        }
+                        return;
+                    }
+                    ARouter.getInstance().build(Routes.Main.PERSON_DETAIL)
+                            .withString(Constants.K_ID, friendInfo.getUserID())
+                            .navigation(AllFriendActivity.this, 1001);
+                });
+            }
+        };
+        view.recyclerView.setAdapter(adapter);
+        view.recyclerView.addItemDecoration(new StickyDecoration(this,
+                position -> adapter.getItems().get(position).sortLetter));
+    }
+
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        if (resultCode == RESULT_OK && requestCode == 1001) {
+            vm.getAllFriend();
+        }
+    }
+
+//    private ActivityResultLauncher<Intent> searchFriendLauncher =
+//        registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
+//            try {
+//                if (result.getResultCode() != RESULT_OK) return;
+//
+//                String uid = result.getData().getStringExtra(Constants.K_ID);
+//                if (null != selectTargetVM) {
+//                    for (ExUserInfo item : adapter.getItems()) {
+//                        if (null != item.userInfo && item.userInfo.getUserID().equals(uid)) {
+//                            sendChatWindow(item.userInfo.getFriendInfo());
+//                            return;
+//                        }
+//                    }
+//                }
+//                ARouter.getInstance().build(Routes.Main.PERSON_DETAIL)
+//                    .withString(Constants.K_ID, uid).navigation();
+//            } catch (Exception ignored) {
+//
+//            }
+//        });
+
+    private void listener() {
+        view.searchView.setOnClickListener(v -> {
+            ARouter.getInstance().build(Routes.Contact.SEARCH_FRIENDS_GROUP)
+                    .withBoolean(Constants.IS_SELECT_FRIEND, true)
+                    .navigation();
+        });
+//        vm.letters.observe(this, v -> {
+//            if (null == v || v.isEmpty()) return;
+//            StringBuilder letters = new StringBuilder();
+//            for (String s : v) {
+//                letters.append(s);
+//            }
+//            view.sortView.setLetters(letters.toString());
+//        });
+        view.sortView.setLetters(mLetters);
+
+        vm.exUserInfo.observe(this, v -> {
+            if (null == v || v.isEmpty()) return;
+            List<ExUserInfo> exUserInfos = new ArrayList<>(v);
+            adapter.setItems(exUserInfos);
+        });
+
+        view.sortView.setOnLetterChangedListener((letter, position) -> {
+            if (letter.equals("↑")) {
+                Common.smoothMoveToPosition(view.recyclerView, 0);
+                return;
+            }
+            for (int i = 0; i < adapter.getItems().size(); i++) {
+                ExUserInfo exUserInfo = adapter.getItems().get(i);
+                if (exUserInfo.sortLetter.equalsIgnoreCase(letter)) {
+                    Common.smoothMoveToPosition(view.recyclerView, i);
+                    return;
+                }
+            }
+        });
+
+        view.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+                super.onScrollStateChanged(recyclerView, newState);
+                if (Common.mShouldScroll && RecyclerView.SCROLL_STATE_IDLE == newState) {
+                    Common.mShouldScroll = false;
+                    Common.smoothMoveToPosition(view.recyclerView, Common.mToPosition);
+                }
+            }
+        });
+    }
+
+
+}

+ 106 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/CreateLabelActivity.java

@@ -0,0 +1,106 @@
+package io.openim.android.ouicontact.ui;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import com.alibaba.android.arouter.launcher.ARouter;
+import com.alibaba.fastjson2.JSONObject;
+import com.xiaofeng.flowlayoutmanager.Alignment;
+import com.xiaofeng.flowlayoutmanager.FlowLayoutManager;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.openim.android.ouicontact.databinding.ActivityCreateLabelBinding;
+import io.openim.android.ouicontact.vm.LabelVM;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.net.bage.GsonHel;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.sdk.models.FriendInfo;
+
+public class CreateLabelActivity extends BaseActivity<LabelVM, ActivityCreateLabelBinding> {
+
+    private List<FriendInfo> friendInfos;
+    private RecyclerViewAdapter<FriendInfo, ViewHol.LabelMemberItem> adapter;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVM(LabelVM.class);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityCreateLabelBinding.inflate(getLayoutInflater()));
+        sink();
+        init();
+        listener();
+    }
+
+    private void listener() {
+        view.addMember.setOnClickListener(v -> {
+            ARouter.getInstance().build(Routes.Group.CREATE_GROUP).withString(Constants.K_NAME,
+                getString(io.openim.android.ouicore.R.string.selete_member)).withBoolean(Constants.IS_SELECT_FRIEND, true).navigation(this, Constants.Event.CALLING_REQUEST_CODE);
+        });
+        view.submit.setOnClickListener(v -> {
+            String title;
+            if (TextUtils.isEmpty(title=view.name.getText().toString())){
+                toast(getString(io.openim.android.ouicore.R.string.create_label_tips2));
+                return;
+            }
+            if (null==friendInfos||friendInfos.isEmpty()){
+                toast(getString(io.openim.android.ouicore.R.string.create_label_tips3));
+                return;
+            }
+            List<String> ids=new ArrayList<>();
+            for (FriendInfo friendInfo : friendInfos) {
+                ids.add(friendInfo.getUserID());
+            }
+            vm.createTag(title, ids, data -> {
+                toast(getString(io.openim.android.ouicore.R.string.create_succ));
+                setResult(RESULT_OK);
+                finish();
+            });
+        });
+    }
+
+    void init() {
+        FlowLayoutManager flowLayoutManager = new FlowLayoutManager();
+        flowLayoutManager.maxItemsPerLine(4);
+        flowLayoutManager.setAutoMeasureEnabled(true);
+        flowLayoutManager.setAlignment(Alignment.LEFT);
+
+        view.recyclerView.setLayoutManager(flowLayoutManager);
+
+        view.recyclerView.setAdapter(adapter = new RecyclerViewAdapter<FriendInfo,
+            ViewHol.LabelMemberItem>(ViewHol.LabelMemberItem.class) {
+
+            @Override
+            public void onBindView(@NonNull ViewHol.LabelMemberItem holder, FriendInfo data,
+                                   int position) {
+                holder.view.name.setText(data.getNickname());
+                holder.view.layout.setOnClickListener(v -> {
+                    int index=getItems().indexOf(data);
+
+                    getItems().remove(index);
+                    runOnUiThread(() -> adapter.notifyItemRemoved(index));
+                });
+            }
+        });
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        if (resultCode != RESULT_OK) return;
+        String fds = data.getStringExtra(Constants.K_RESULT);
+        Type listType = new GsonHel.ParameterizedTypeImpl(List.class,
+            new Class[]{FriendInfo.class});
+        friendInfos = JSONObject.parseObject(fds, listType);
+        adapter.setItems(friendInfos);
+    }
+}

+ 120 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/ForwardToActivity.java

@@ -0,0 +1,120 @@
+package io.openim.android.ouicontact.ui;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.fragment.app.FragmentTransaction;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import com.alibaba.android.arouter.facade.annotation.Route;
+
+import io.openim.android.ouicontact.databinding.ActivityForwardToBinding;
+import io.openim.android.ouicontact.ui.fragment.FriendFragment;
+import io.openim.android.ouicontact.ui.fragment.GroupFragment;
+import io.openim.android.ouicontact.ui.search.SearchGroupAndFriendsActivity;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.base.BaseFragment;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.vm.SocialityVM;
+import io.openim.android.ouicore.widget.CommonDialog;
+import io.openim.android.sdk.models.UserInfo;
+
+
+@Route(path = Routes.Contact.FORWARD)
+public class ForwardToActivity extends BaseActivity<SocialityVM, ActivityForwardToBinding> {
+
+    private int mCurrentTabIndex;
+    private BaseFragment lastFragment, friendFragment, groupFragment;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVM(SocialityVM.class);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityForwardToBinding.inflate(getLayoutInflater()));
+        sink();
+        vm.getAllFriend();
+
+        initView();
+        listener();
+    }
+    private final ActivityResultLauncher<Intent> launcher= registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), v->{
+        if (v.getResultCode()!=RESULT_OK)return;
+
+        CommonDialog commonDialog =
+            new CommonDialog(this);
+        commonDialog.getMainView().tips.setText(getString(io.openim.android.ouicore.R.string.confirm_send_who)
+            + v.getData().getStringExtra(Constants.K_NAME));
+        commonDialog.getMainView().cancel.setOnClickListener(v1 -> commonDialog.dismiss());
+        commonDialog.getMainView().confirm.setOnClickListener(v1 -> {
+            setResult(RESULT_OK, v.getData());
+            finish();
+        });
+        commonDialog.show();
+    });
+
+    private void listener() {
+        view.searchView.setOnClickListener(v -> {
+            launcher.launch(new Intent(this, SearchGroupAndFriendsActivity.class)
+                .putExtra(Constants.IS_SELECT_FRIEND,true));
+        });
+        view.menuGroup.setOnCheckedChangeListener((group, checkedId) -> {
+            if (checkedId == view.men1.getId()) {
+                view.menBg1.setVisibility(View.VISIBLE);
+                view.menBg2.setVisibility(View.GONE);
+                switchFragment(friendFragment);
+            } else {
+                view.menBg2.setVisibility(View.VISIBLE);
+                view.menBg1.setVisibility(View.GONE);
+                switchFragment(groupFragment);
+            }
+        });
+
+    }
+
+    private void initView() {
+        groupFragment = GroupFragment.newInstance();
+        groupFragment.setPage(2);
+
+        friendFragment = FriendFragment.newInstance();
+        friendFragment.setPage(1);
+        switchFragment(friendFragment);
+
+
+        ((FriendFragment) friendFragment).setConfirmListener((userInfo, id) -> {
+            setResult(RESULT_OK, new Intent().putExtra(Constants.K_ID, id)
+                .putExtra(Constants.K_NAME, userInfo.getNickname()));
+            finish();
+        });
+        ((GroupFragment) groupFragment).setConfirmListener((userInfo, id) -> {
+            setResult(RESULT_OK, new Intent().putExtra(Constants.K_GROUP_ID, id));
+            finish();
+        });
+    }
+
+
+    private void switchFragment(BaseFragment fragment) {
+        try {
+            if (fragment != null && !fragment.isVisible() && mCurrentTabIndex != fragment.getPage()) {
+                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+                if (!fragment.isAdded()) {
+                    transaction.add(view.fragmentContainer.getId(), fragment);
+                }
+                if (lastFragment != null) {
+                    transaction.hide(lastFragment);
+                }
+                transaction.show(fragment).commit();
+                lastFragment = fragment;
+                mCurrentTabIndex = lastFragment.getPage();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public interface ConfirmListener {
+        void onListener(UserInfo userInfo, String id);
+    }
+}

+ 37 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/FriendRequestDetailActivity.java

@@ -0,0 +1,37 @@
+package io.openim.android.ouicontact.ui;
+
+import android.os.Bundle;
+import android.widget.Toast;
+
+import io.openim.android.ouicontact.databinding.ActivityFriendRequestDetailBinding;
+import io.openim.android.ouicontact.vm.ContactVM;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.utils.SinkHelper;
+
+public class FriendRequestDetailActivity extends BaseActivity<ContactVM, ActivityFriendRequestDetailBinding> {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVMByCache(ContactVM.class);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityFriendRequestDetailBinding.inflate(getLayoutInflater()));
+        setLightStatus();
+        SinkHelper.get(this).setTranslucentStatus(view.getRoot());
+        view.setContactVM(vm);
+
+        try {
+            view.avatar.load(vm.friendDetail.val().getFromFaceURL(), vm.friendDetail.val().getFromNickname());
+            view.nickName.setText(vm.friendDetail.val().getFromNickname());
+            view.useId.setText(getString(io.openim.android.ouicore.R.string.new_1) + ":" + vm.friendDetail.val().getFromUserID());
+            view.hil.setText(vm.friendDetail.val().getReqMsg());
+        } catch (Exception ignore) {
+        }
+    }
+
+    @Override
+    public void onSuccess(Object body) {
+        super.onSuccess(body);
+        finish();
+        toast(getString(io.openim.android.ouicore.R.string.send_succ));
+    }
+}

+ 35 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/GroupNoticeDetailActivity.java

@@ -0,0 +1,35 @@
+package io.openim.android.ouicontact.ui;
+
+
+import android.os.Bundle;
+
+import io.openim.android.ouicontact.databinding.ActivityGroupNoticeDetailBinding;
+import io.openim.android.ouicontact.vm.ContactVM;
+import io.openim.android.ouicore.base.BaseActivity;
+
+/**
+ * 自己申请加入群聊确认
+ */
+public class GroupNoticeDetailActivity extends BaseActivity<ContactVM, ActivityGroupNoticeDetailBinding> {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVMByCache(ContactVM.class);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityGroupNoticeDetailBinding.inflate(getLayoutInflater()));
+        sink();
+        view.setContactVM(vm);
+
+        initView();
+    }
+
+    private void initView() {
+        view.avatar.load(vm.groupDetail.getValue().getUserFaceURL(),vm.groupDetail.getValue().getGroupName());
+        view.useId.setText(getString(io.openim.android.ouicore.R.string.new_1) + ":" + vm.groupDetail.getValue().getUserID());
+    }
+
+    @Override
+    public void onSuccess(Object body) {
+        finish();
+    }
+}

+ 36 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/GroupNoticeInvitedDetailActivity.java

@@ -0,0 +1,36 @@
+package io.openim.android.ouicontact.ui;
+
+
+import android.os.Bundle;
+
+import io.openim.android.ouicontact.databinding.ActivityGroupNoticeInviteDetailBinding;
+import io.openim.android.ouicontact.vm.ContactVM;
+import io.openim.android.ouicore.base.BaseActivity;
+
+/**
+ * 别人邀请加入群聊确认
+ */
+public class GroupNoticeInvitedDetailActivity extends BaseActivity<ContactVM, ActivityGroupNoticeInviteDetailBinding> {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVMByCache(ContactVM.class);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityGroupNoticeInviteDetailBinding.inflate(getLayoutInflater()));
+        sink();
+        view.setContactVM(vm);
+
+        initView();
+    }
+
+    private void initView() {
+        view.avatar.load("", true);
+        view.peopleCount.setText(vm.groupDetail.getValue().getMemberCount() + getString(io.openim.android.ouicore.R.string.people));
+        view.info.setText(vm.groupDetail.getValue().getInviterUserID() + getString(io.openim.android.ouicore.R.string.invite) + vm.groupDetail.getValue().getNickname() + getString(io.openim.android.ouicore.R.string.join_group));
+    }
+
+    @Override
+    public void onSuccess(Object body) {
+        finish();
+    }
+}

+ 131 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/GroupNoticeListActivity.java

@@ -0,0 +1,131 @@
+package io.openim.android.ouicontact.ui;
+
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.blankj.utilcode.util.LogUtils;
+import com.blankj.utilcode.util.SpanUtils;
+import com.blankj.utilcode.util.StringUtils;
+
+import io.openim.android.ouicontact.R;
+import io.openim.android.ouicontact.databinding.ActivityGroupNoticeListBinding;
+import io.openim.android.ouicontact.databinding.ItemGroupNoticeBinding;
+import io.openim.android.ouicontact.vm.ContactVM;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.im.IMEvent;
+import io.openim.android.sdk.models.GroupApplicationInfo;
+
+/**
+ * 新的群聊
+ */
+public class GroupNoticeListActivity extends BaseActivity<ContactVM, ActivityGroupNoticeListBinding> {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVM(ContactVM.class, true);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityGroupNoticeListBinding.inflate(getLayoutInflater()));
+        sink();
+
+        initView();
+        IMEvent.getInstance().addGroupListener(vm);
+
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        vm.getRecvGroupApplicationList();
+    }
+
+    public void toBack(View v) {
+        finish();
+    }
+
+    private void initView() {
+        view.recyclerView.setLayoutManager(new LinearLayoutManager(this));
+        RecyclerViewAdapter recyclerViewAdapter = new RecyclerViewAdapter<GroupApplicationInfo, RecyclerViewItem>(RecyclerViewItem.class) {
+            @Override
+            public void onBindView(@NonNull RecyclerViewItem holder, GroupApplicationInfo data, int position) {
+                LogUtils.e(data.toString());
+//                holder.v.avatar.load(data.getUserFaceURL(),true);
+                holder.v.avatar.load("", true);
+                holder.v.groupName.setText(data.getGroupName());
+                if (StringUtils.isEmpty(data.getInviterUserID())) {
+                    SpanUtils.with(holder.v.hil)
+                            .append(data.getNickname())
+                            .setForegroundColor(Color.parseColor("#576B95"))
+                            .append(getString(io.openim.android.ouicore.R.string.apply))
+                            .append(getString(io.openim.android.ouicore.R.string.join_group))
+                            .append(StringUtils.isEmpty(data.getReqMsg()) ? "" : "\n" + getString(io.openim.android.ouicore.R.string.reason))
+                            .append(StringUtils.isEmpty(data.getReqMsg()) ? "" : data.getReqMsg())
+                            .create();
+                } else {
+                    SpanUtils.with(holder.v.hil)
+                            .append(data.getInviterUserID())
+                            .setForegroundColor(Color.parseColor("#576B95"))
+                            .append(getString(io.openim.android.ouicore.R.string.invite))
+                            .append(data.getNickname())
+                            .setForegroundColor(Color.parseColor("#576B95"))
+                            .append(getString(io.openim.android.ouicore.R.string.join_group))
+                            .create();
+                }
+
+
+                holder.v.getRoot().setOnClickListener(null);
+                holder.v.handle.setBackgroundColor(getResources().getColor(android.R.color.white));
+                if (data.getHandleResult() == 0) {
+                    holder.v.handle.setBackgroundResource(io.openim.android.ouicore.R.drawable.sty_radius_4_f2f2f2);
+                    holder.v.handle.setText(getString(io.openim.android.ouicore.R.string.accept));
+
+                    holder.v.getRoot().setOnClickListener(v -> {
+                        vm.groupDetail.setValue(data);
+                        if (StringUtils.isEmpty(data.getInviterUserID())) {
+                            startActivity(new Intent(GroupNoticeListActivity.this, GroupNoticeDetailActivity.class));
+                        } else {
+                            startActivity(new Intent(GroupNoticeListActivity.this, GroupNoticeInvitedDetailActivity.class));
+                        }
+                    });
+                } else if (data.getHandleResult() == -1) {
+                    holder.v.handle.setText(getString(io.openim.android.ouicore.R.string.declined));
+                    holder.v.handle.setTextColor(Color.parseColor("#737373"));
+                    Drawable icon = getResources().getDrawable(R.mipmap.ic_add_friend_arrow);
+                    holder.v.handle.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
+                } else {
+                    holder.v.handle.setText(getString(io.openim.android.ouicore.R.string.agreeed));
+                    holder.v.handle.setTextColor(Color.parseColor("#737373"));
+                    Drawable icon = getResources().getDrawable(R.mipmap.ic_add_friend_arrow);
+                    holder.v.handle.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
+                }
+            }
+        };
+        view.recyclerView.setAdapter(recyclerViewAdapter);
+
+        vm.groupApply.observe(this, v -> recyclerViewAdapter.setItems(v));
+    }
+
+    public static class RecyclerViewItem extends RecyclerView.ViewHolder {
+        public ItemGroupNoticeBinding v;
+
+        public RecyclerViewItem(@NonNull View parent) {
+            super((ItemGroupNoticeBinding.inflate(LayoutInflater.from(parent.getContext()))).getRoot());
+            v = ItemGroupNoticeBinding.bind(itemView);
+        }
+    }
+
+    @Override
+    protected void fasterDestroy() {
+        super.fasterDestroy();
+        IMEvent.getInstance().removeGroupListener(vm);
+        removeCacheVM();
+    }
+}

+ 112 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/LabelActivity.java

@@ -0,0 +1,112 @@
+package io.openim.android.ouicontact.ui;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.Observer;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.StaggeredGridLayoutManager;
+
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.yanzhenjie.recyclerview.SwipeMenuCreator;
+import com.yanzhenjie.recyclerview.SwipeMenuItem;
+
+import java.util.List;
+
+import io.openim.android.ouicontact.R;
+import io.openim.android.ouicontact.databinding.ActivityLabelBinding;
+import io.openim.android.ouicontact.vm.LabelVM;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.databinding.ItemPsrsonSelectBinding;
+import io.openim.android.ouicore.entity.UserLabel;
+import io.openim.android.ouicore.entity.UserLabelChild;
+import io.openim.android.ouicore.utils.Common;
+import io.openim.android.ouicore.widget.SyLinearLayoutManager;
+import io.openim.android.sdk.models.UserInfo;
+
+public class LabelActivity extends BaseActivity<LabelVM, ActivityLabelBinding> {
+
+    private RecyclerViewAdapter<UserLabel, ViewHol.LabelItem> adapter;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVM(LabelVM.class);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityLabelBinding.inflate(getLayoutInflater()));
+        sink();
+
+        init();
+        listener();
+    }
+
+    private ActivityResultLauncher<Intent> launcher =
+        registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
+            if (result.getResultCode() == RESULT_OK) {
+                vm.getUserTags();
+            }
+        });
+
+    private void listener() {
+        vm.userLabels.observe(this, userLabels -> {
+            adapter.setItems(userLabels);
+        });
+        view.add.setOnClickListener(v -> {
+            launcher.launch(new Intent(this, CreateLabelActivity.class));
+        });
+    }
+
+    void init() {
+        view.recyclerView.setLayoutManager(new SyLinearLayoutManager(this));
+        SwipeMenuCreator mSwipeMenuCreator = (leftMenu, rightMenu, position) -> {
+            SwipeMenuItem delete = new SwipeMenuItem(this);
+            delete.setText(io.openim.android.ouicore.R.string.remove);
+            delete.setHeight(MATCH_PARENT);
+            delete.setWidth(Common.dp2px(73));
+            delete.setTextSize(16);
+            delete.setTextColor(this.getResources().getColor(android.R.color.white));
+            delete.setBackgroundColor(Color.parseColor("#FFAB41"));
+
+            rightMenu.addMenuItem(delete);
+        };
+        view.recyclerView.setSwipeMenuCreator(mSwipeMenuCreator);
+        view.recyclerView.setOnItemMenuClickListener((menuBridge, adapterPosition) -> {
+            menuBridge.closeMenu();
+            UserLabel userLabel = adapter.getItems().get(adapterPosition);
+            vm.removeTag(userLabel, true);
+        });
+        view.recyclerView.setAdapter(adapter = new RecyclerViewAdapter<UserLabel,
+            ViewHol.LabelItem>(ViewHol.LabelItem.class) {
+
+            @Override
+            public void onBindView(@NonNull ViewHol.LabelItem holder, UserLabel data,
+                                   int position) {
+                holder.view.name.setText(data.getTagName());
+                StringBuilder member = new StringBuilder();
+
+                if (null!=data.getUserList()){
+                    for (UserLabelChild userLabelChild : data.getUserList()) {
+                        member.append(userLabelChild.getUserName());
+                        member.append("、");
+                    }
+                    holder.view.content.setText(member.substring(0, member.length() - 1));
+                }
+
+            }
+        });
+        vm.getUserTags();
+    }
+}
+

+ 157 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/MyGroupActivity.java

@@ -0,0 +1,157 @@
+package io.openim.android.ouicontact.ui;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.alibaba.android.arouter.launcher.ARouter;
+
+import java.util.List;
+import java.util.Observable;
+import java.util.Observer;
+
+import io.openim.android.ouicontact.databinding.ActivityMyGroupBinding;
+import io.openim.android.ouicontact.ui.search.SearchGroupActivity;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.base.BaseApp;
+import io.openim.android.ouicore.base.vm.injection.Easy;
+import io.openim.android.ouicore.ex.MultipleChoice;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.Obs;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.vm.GroupVM;
+import io.openim.android.ouicore.vm.SelectTargetVM;
+import io.openim.android.ouicore.vm.SocialityVM;
+import io.openim.android.sdk.models.FriendInfo;
+import io.openim.android.sdk.models.GroupInfo;
+
+/**
+ * 我的群组
+ */
+public class MyGroupActivity extends BaseActivity<SocialityVM, ActivityMyGroupBinding> implements Observer {
+
+    private RecyclerViewAdapter<GroupInfo, ViewHol.GroupViewHo> joinedAdapter, createAdapter;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVM(SocialityVM.class);
+        Obs.inst().addObserver(this);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityMyGroupBinding.inflate(getLayoutInflater()));
+        sink();
+        initView();
+        listener();
+        vm.getAllGroup();
+    }
+
+    private void initView() {
+        view.create.setLayoutManager(new LinearLayoutManager(this));
+        view.joined.setLayoutManager(new LinearLayoutManager(this));
+
+        joinedAdapter = new ContentAdapter(ViewHol.GroupViewHo.class);
+        view.joined.setAdapter(joinedAdapter);
+
+        createAdapter = new ContentAdapter(ViewHol.GroupViewHo.class);
+        view.create.setAdapter(createAdapter);
+    }
+
+    private void listener() {
+        view.searchView.setOnClickListener(v -> {
+            SearchGroupActivity.jumpThis(this, vm.groups.getValue());
+        });
+        view.toCreate.setOnClickListener(v -> {
+            SelectTargetVM targetVM = Easy.installVM(SelectTargetVM.class)
+                    .setIntention(SelectTargetVM.Intention.isCreateGroup);
+            targetVM.setOnFinishListener(() -> {
+                GroupVM groupVM = BaseApp.inst().getVMByCache(GroupVM.class);
+                if (null == groupVM)
+                    groupVM = new GroupVM();
+                groupVM.selectedFriendInfo.getValue().clear();
+                List<MultipleChoice> multipleChoices = targetVM.metaData.getValue();
+                for (int i = 0; i < multipleChoices.size(); i++) {
+                    MultipleChoice us = multipleChoices.get(i);
+                    FriendInfo friendInfo = new FriendInfo();
+                    friendInfo.setUserID(us.key);
+                    friendInfo.setNickname(us.name);
+                    friendInfo.setFaceURL(us.icon);
+                    groupVM.selectedFriendInfo.getValue().add(friendInfo);
+                }
+                BaseApp.inst().putVM(groupVM);
+                ARouter.getInstance().build(Routes.Group.CREATE_GROUP2).navigation();
+            });
+            ARouter.getInstance().build(Routes.Group.SELECT_TARGET).navigation();
+        });
+
+        vm.groups.observe(this, groupInfos -> {
+            joinedAdapter.setItems(groupInfos);
+        });
+        vm.ownGroups.observe(this, groupInfos -> {
+            createAdapter.setItems(groupInfos);
+            view.empty.setVisibility(createAdapter.getItems().isEmpty() ? View.VISIBLE : View.GONE);
+        });
+
+        view.menuGroup.setOnCheckedChangeListener((group, checkedId) -> {
+            if (checkedId == view.men1.getId()) {
+                view.men1.setTextColor(Color.parseColor("#282828"));
+                view.men1.setTypeface(null, Typeface.BOLD);
+                view.men2.setTextColor(Color.parseColor("#656565"));
+                view.men2.setTypeface(null, Typeface.NORMAL);
+                view.menBg1.setVisibility(View.VISIBLE);
+                view.menBg2.setVisibility(View.GONE);
+                view.create.setVisibility(View.VISIBLE);
+                view.joined.setVisibility(View.GONE);
+                view.empty.setVisibility(createAdapter.getItems().isEmpty() ? View.VISIBLE : View.GONE);
+            } else {
+                view.men2.setTextColor(Color.parseColor("#282828"));
+                view.men2.setTypeface(null, Typeface.BOLD);
+                view.men1.setTextColor(Color.parseColor("#656565"));
+                view.men1.setTypeface(null, Typeface.NORMAL);
+                view.menBg2.setVisibility(View.VISIBLE);
+                view.menBg1.setVisibility(View.GONE);
+                view.empty.setVisibility(View.GONE);
+                view.create.setVisibility(View.GONE);
+                view.joined.setVisibility(View.VISIBLE);
+            }
+        });
+    }
+
+    @Override
+    public void update(Observable o, Object arg) {
+        Obs.Message message = (Obs.Message) arg;
+        if (message.tag == Constants.Event.DISSOLVE_GROUP) {
+            vm.groups.getValue().clear();
+            vm.ownGroups.getValue().clear();
+            vm.getAllGroup();
+        }
+    }
+
+    public class ContentAdapter extends RecyclerViewAdapter<GroupInfo, ViewHol.GroupViewHo> {
+
+        public ContentAdapter(Class<ViewHol.GroupViewHo> viewHolder) {
+            super(viewHolder);
+        }
+
+        @Override
+        public void onBindView(@NonNull ViewHol.GroupViewHo holder, GroupInfo data, int position) {
+            holder.view.avatar.load(data.getFaceURL(), true);
+            holder.view.title.setText(data.getGroupName());
+            holder.view.description.setText(data.getMemberCount() + MyGroupActivity.this.getString(io.openim.android.ouicore.R.string.people));
+
+            holder.view.getRoot().setOnClickListener(v -> {
+                ARouter.getInstance().build(Routes.Conversation.CHAT).withString(Constants.K_GROUP_ID, data.getGroupID()).withString(Constants.K_NAME, data.getGroupName()).navigation();
+            });
+        }
+    }
+
+    @Override
+    protected void fasterDestroy() {
+        super.fasterDestroy();
+        Obs.inst().deleteObserver(this);
+    }
+}

+ 117 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/NewFriendActivity.java

@@ -0,0 +1,117 @@
+package io.openim.android.ouicontact.ui;
+
+
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.alibaba.android.arouter.launcher.ARouter;
+
+import io.openim.android.ouicontact.R;
+import io.openim.android.ouicontact.databinding.ActivityNewFriendBinding;
+import io.openim.android.ouicontact.databinding.ItemFriendNoticeBinding;
+import io.openim.android.ouicontact.vm.ContactVM;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.utils.SinkHelper;
+import io.openim.android.sdk.models.FriendApplicationInfo;
+
+/**
+ * 新的好友
+ */
+public class NewFriendActivity extends BaseActivity<ContactVM, ActivityNewFriendBinding> {
+    RecyclerViewAdapter adapter;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVM(ContactVM.class, true);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityNewFriendBinding.inflate(getLayoutInflater()));
+        setLightStatus();
+        SinkHelper.get(this).setTranslucentStatus(view.getRoot());
+
+        vm.getRecvFriendApplicationList();
+        initView();
+        listener();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        removeCacheVM();
+    }
+
+    private void initView() {
+        view.recyclerView.setLayoutManager(new LinearLayoutManager(this));
+
+
+        adapter = new RecyclerViewAdapter<FriendApplicationInfo, ViewHo>(ViewHo.class) {
+
+            @Override
+            public void onBindView(@NonNull ViewHo holder, FriendApplicationInfo data, int position) {
+                holder.view.avatar.load(data.getFromFaceURL(),data.getFromNickname());
+                holder.view.nickName.setText(data.getFromNickname());
+                holder.view.hil.setText(data.getReqMsg());
+                holder.view.handle.setBackgroundColor(getResources().getColor(android.R.color.white));
+
+                if (data.getHandleResult() == 0) {
+                    holder.view.handle.setBackgroundResource(io.openim.android.ouicore.R.drawable.sty_radius_4_f2f2f2);
+                    holder.view.handle.setText(getString(io.openim.android.ouicore.R.string.accept));
+
+                    holder.view.getRoot().setOnClickListener(v -> {
+                        vm.friendDetail.setValue(data);
+                        startActivity(new Intent(NewFriendActivity.this, FriendRequestDetailActivity.class));
+                    });
+                } else if (data.getHandleResult() == -1) {
+                    holder.view.getRoot().setOnClickListener(null);
+                    holder.view.handle.setText(getString(io.openim.android.ouicore.R.string.declined));
+                    holder.view.handle.setTextColor(Color.parseColor("#737373"));
+                    Drawable icon = getResources().getDrawable(R.mipmap.ic_add_friend_arrow);
+                    holder.view.handle.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
+                } else {
+                    holder.view.handle.setText(getString(io.openim.android.ouicore.R.string.agreeed));
+                    holder.view.handle.setTextColor(Color.parseColor("#737373"));
+                    Drawable icon = getResources().getDrawable(R.mipmap.ic_add_friend_arrow);
+                    holder.view.handle.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
+                    holder.view.getRoot().setOnClickListener(v -> ARouter.getInstance().build(Routes.Main.PERSON_DETAIL)
+                            .withString(Constants.K_ID, data.getFromUserID())
+//                            .withString(Constants.K_NAME, data.getFromNickname())
+                            .navigation());
+                }
+
+            }
+        };
+        view.recyclerView.setAdapter(adapter);
+    }
+
+    private void listener() {
+        vm.friendApply.observe(this, v -> {
+            adapter.setItems(v);
+        });
+    }
+
+
+    public void toBack(View v) {
+        finish();
+    }
+
+
+    public static class ViewHo extends RecyclerView.ViewHolder {
+        public ItemFriendNoticeBinding view;
+
+        public ViewHo(@NonNull View parent) {
+            super((ItemFriendNoticeBinding.inflate(LayoutInflater.from(parent.getContext()), (ViewGroup) parent, false).getRoot()));
+            view = ItemFriendNoticeBinding.bind(this.itemView);
+        }
+    }
+}

+ 47 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/adapter/ContactAdapter.java

@@ -0,0 +1,47 @@
+package io.openim.android.ouicontact.ui.adapter;
+
+import android.content.Context;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.alibaba.android.arouter.launcher.ARouter;
+import com.blankj.utilcode.util.StringUtils;
+
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.entity.ExUserInfo;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.sdk.models.FriendInfo;
+
+public class ContactAdapter extends RecyclerViewAdapter<ExUserInfo, RecyclerView.ViewHolder> {
+    private Context mContext;
+
+    public ContactAdapter(Context mContext) {
+        this.mContext = mContext;
+    }
+
+    @NonNull
+    @Override
+    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        return new ViewHol.ItemViewHo(parent);
+    }
+
+    @Override
+    public void onBindView(@NonNull RecyclerView.ViewHolder holder, ExUserInfo data, int position) {
+        ViewHol.ItemViewHo itemViewHo = (ViewHol.ItemViewHo) holder;
+        FriendInfo friendInfo = data.userInfo.getFriendInfo();
+        String nickName = friendInfo.getNickname();
+        String remark = friendInfo.getRemark();
+        String str = StringUtils.isEmpty(remark) ? nickName : remark;
+        itemViewHo.view.avatar.load(friendInfo.getFaceURL(),false,friendInfo.getNickname());
+        itemViewHo.view.nickName.setText(str);
+        itemViewHo.view.getRoot().setOnClickListener(v -> {
+            ARouter.getInstance().build(Routes.Main.PERSON_DETAIL)
+                    .withString(Constants.K_ID, friendInfo.getUserID())
+                    .navigation();
+        });
+    }
+}

+ 215 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/fragment/ContactFragment.java

@@ -0,0 +1,215 @@
+package io.openim.android.ouicontact.ui.fragment;
+
+import android.content.Intent;
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.alibaba.android.arouter.facade.annotation.Route;
+import com.alibaba.android.arouter.launcher.ARouter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.openim.android.ouicontact.R;
+import io.openim.android.ouicontact.databinding.FragmentContactMainBinding;
+import io.openim.android.ouicontact.ui.AddRelationActivity;
+import io.openim.android.ouicontact.ui.GroupNoticeListActivity;
+import io.openim.android.ouicontact.ui.MyGroupActivity;
+import io.openim.android.ouicontact.ui.NewFriendActivity;
+import io.openim.android.ouicontact.ui.adapter.ContactAdapter;
+import io.openim.android.ouicontact.vm.ContactVM;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.base.BaseApp;
+import io.openim.android.ouicore.base.BaseFragment;
+import io.openim.android.ouicore.base.vm.injection.Easy;
+import io.openim.android.ouicore.entity.ExUserInfo;
+import io.openim.android.ouicore.utils.Common;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.utils.SinkHelper;
+import io.openim.android.ouicore.vm.NotificationVM;
+import io.openim.android.ouicore.vm.SocialityVM;
+import io.openim.android.ouicore.widget.HeaderAndFooterWrapper;
+import io.openim.android.ouicore.widget.StickyDecoration;
+
+/**
+ * 通讯录
+ */
+@Route(path = Routes.Contact.HOME)
+public class ContactFragment extends BaseFragment<ContactVM> {
+    private FragmentContactMainBinding view;
+    private NotificationVM notificationVM;
+    private SocialityVM socialityVM;
+    private RecyclerViewAdapter<ExUserInfo, RecyclerView.ViewHolder> adapter;
+    private LinearLayoutManager mLayoutManager;
+    private TextView newFriendNoticeBadge, groupNoticeBadge;
+    private String mLetters = "↑ABCDEFGHIJKLMNOPQRSTUVWXYZ#";//默认字符
+    private HeaderAndFooterWrapper headerAndFooterWrapper;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        bindVM(ContactVM.class);
+        notificationVM = Easy.find(NotificationVM.class);
+        socialityVM = Easy.installVM(SocialityVM.class);
+        BaseApp.inst().putVM(socialityVM);
+        super.onCreate(savedInstanceState);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        view = FragmentContactMainBinding.inflate(getLayoutInflater());
+        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.title.getLayoutParams();
+        lp.setMargins(0, SinkHelper.getStatusBarHeight(), 0, 0);
+        view.title.setLayoutParams(lp);
+        listener();
+        initView();
+//        socialityVM.getAllFriend();
+        return view.getRoot();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        socialityVM.getAllFriend();
+    }
+
+    public ContactVM getVM() {
+        return vm;
+    }
+
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+    }
+
+    private void initView() {
+        view.addFriend.setOnClickListener(view1 -> startActivity(new Intent(getActivity(), AddRelationActivity.class)));
+        view.search.setOnClickListener(view1 -> {
+            ARouter.getInstance().build(Routes.Conversation.SEARCH).navigation();
+        });
+        adapter = new ContactAdapter(getContext());
+        headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);
+        headerAndFooterWrapper.addHeaderView(topLayout());
+        view.recyclerView.setLayoutManager(mLayoutManager = new LinearLayoutManager(getContext()) {
+            @Override
+            public void smoothScrollToPosition(RecyclerView recyclerView,
+                                               RecyclerView.State state, int position) {
+                LinearSmoothScroller linearSmoothScroller =
+                        new LinearSmoothScroller(recyclerView.getContext()) {
+                            @Override
+                            protected int calculateTimeForScrolling(int dx) {
+                                // 此函数计算滚动dx的距离需要多久,当要滚动的距离很大时,比如说52000,
+                                // 经测试,系统会多次调用此函数,每10000距离调一次,所以总的滚动时间
+                                // 是多次调用此函数返回的时间的和,所以修改每次调用该函数时返回的时间的
+                                // 大小就可以影响滚动需要的总时间,可以直接修改些函数的返回值,也可以修改
+                                // dx的值,这里暂定使用后者.
+                                // (See LinearSmoothScroller.TARGET_SEEK_SCROLL_DISTANCE_PX)
+                                if (dx > 1500) {
+                                    dx = 1500;
+                                }
+                                return super.calculateTimeForScrolling(dx);
+                            }
+
+                            @Override
+                            public PointF computeScrollVectorForPosition(int targetPosition) {
+                                return mLayoutManager.computeScrollVectorForPosition(targetPosition);
+                            }
+                        };
+                linearSmoothScroller.setTargetPosition(position);
+                startSmoothScroll(linearSmoothScroller);
+            }
+        });
+        view.recyclerView.setAdapter(headerAndFooterWrapper);
+        view.recyclerView.addItemDecoration(new StickyDecoration(getContext(), position -> {
+            if (position < headerAndFooterWrapper.getHeadersCount()) {
+                return "";
+            }
+            int pos = position - headerAndFooterWrapper.getHeadersCount();
+            String str = adapter.getItems().isEmpty() ? "" : adapter.getItems().get(pos).sortLetter;
+            return str;
+        }));
+        notificationVM.groupDot.observe(getActivity(), v -> {
+            groupNoticeBadge.setVisibility(v.size() == 0 ? View.GONE : View.VISIBLE);
+            groupNoticeBadge.setText(v.size() + "");
+        });
+        notificationVM.friendDot.observe(getActivity(), v -> {
+            newFriendNoticeBadge.setVisibility(v.size() == 0 ? View.GONE : View.VISIBLE);
+            newFriendNoticeBadge.setText(v.size() + "");
+        });
+    }
+
+    private View topLayout() {
+        View top = LayoutInflater.from(getContext()).inflate(R.layout.view_contact_header, null);
+        LinearLayout newFriendNotice = top.findViewById(R.id.newFriendNotice);
+        newFriendNotice.setOnClickListener(view -> {
+            notificationVM.clearDot(notificationVM.friendDot);
+            startActivity(new Intent(getActivity(), NewFriendActivity.class));
+        });
+        newFriendNoticeBadge = top.findViewById(R.id.newFriendNoticeBadge);
+
+        LinearLayout groupNotice = top.findViewById(R.id.groupNotice);
+        groupNoticeBadge = top.findViewById(R.id.groupNoticeBadge);
+        groupNotice.setOnClickListener(v -> {
+            notificationVM.clearDot(notificationVM.groupDot);
+            startActivity(new Intent(getActivity(), GroupNoticeListActivity.class));
+        });
+        LinearLayout myGroup = top.findViewById(R.id.myGroup);
+        myGroup.setOnClickListener(v -> {
+            startActivity(new Intent(getActivity(), MyGroupActivity.class));
+        });
+        return top;
+    }
+
+    private void listener() {
+        view.sortView.setLetters(mLetters);
+        socialityVM.exUserInfo.observe(getViewLifecycleOwner(), v -> {
+//            if (null == v || v.isEmpty()) {
+//                adapter.setItems(new ArrayList<>());
+//                return;
+//            }
+            List<ExUserInfo> exUserInfos = new ArrayList<>(v);
+            adapter.setItems(exUserInfos);
+            headerAndFooterWrapper.notifyDataSetChanged();
+        });
+
+        view.sortView.setOnLetterChangedListener((letter, position) -> {
+            if (letter.equals("↑")) {
+                Common.smoothMoveToPosition(view.recyclerView, 0);
+                return;
+            }
+            for (int i = 0; i < adapter.getItems().size(); i++) {
+                ExUserInfo exUserInfo = adapter.getItems().get(i);
+                if (exUserInfo.sortLetter.equalsIgnoreCase(letter)) {
+                    Common.smoothMoveToPosition(view.recyclerView, i);
+                    return;
+                }
+            }
+        });
+
+        view.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+                super.onScrollStateChanged(recyclerView, newState);
+                if (Common.mShouldScroll && RecyclerView.SCROLL_STATE_IDLE == newState) {
+                    Common.mShouldScroll = false;
+                    Common.smoothMoveToPosition(view.recyclerView, Common.mToPosition);
+                }
+            }
+        });
+    }
+
+}

+ 140 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/fragment/ContactFragment2.java

@@ -0,0 +1,140 @@
+package io.openim.android.ouicontact.ui.fragment;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.alibaba.android.arouter.facade.annotation.Route;
+import com.alibaba.android.arouter.launcher.ARouter;
+
+import io.openim.android.ouicontact.databinding.FragmentContactMain2Binding;
+import io.openim.android.ouicontact.databinding.FragmentContactMainBinding;
+import io.openim.android.ouicontact.databinding.ViewContactHeader2Binding;
+import io.openim.android.ouicontact.databinding.ViewContactHeaderBinding;
+import io.openim.android.ouicontact.ui.AddRelationActivity;
+import io.openim.android.ouicontact.ui.AllFriendActivity;
+import io.openim.android.ouicontact.ui.GroupNoticeListActivity;
+import io.openim.android.ouicontact.ui.MyGroupActivity;
+import io.openim.android.ouicontact.ui.NewFriendActivity;
+import io.openim.android.ouicontact.vm.ContactVM;
+import io.openim.android.ouicore.base.BaseFragment;
+import io.openim.android.ouicore.base.vm.injection.Easy;
+import io.openim.android.ouicore.ex.MultipleChoice;
+import io.openim.android.ouicore.services.MomentsBridge;
+import io.openim.android.ouicore.utils.ActivityManager;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.utils.SinkHelper;
+import io.openim.android.ouicore.vm.NotificationVM;
+import io.openim.android.ouicore.vm.SelectTargetVM;
+
+/**
+ * 通讯录
+ */
+//@Route(path = Routes.Contact.HOME)
+public class ContactFragment2 extends BaseFragment<ContactVM>  {
+    private FragmentContactMain2Binding view;
+    private ViewContactHeader2Binding header;
+    private NotificationVM notificationVM;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        bindVM(ContactVM.class);
+         notificationVM=Easy.find(NotificationVM.class);
+        super.onCreate(savedInstanceState);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        view = FragmentContactMain2Binding.inflate(getLayoutInflater());
+        header = view.header;
+        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.title.getLayoutParams();
+        lp.setMargins(0, SinkHelper.getStatusBarHeight(), 0, 0);
+        view.title.setLayoutParams(lp);
+
+        initView();
+        click();
+
+        return view.getRoot();
+    }
+
+
+    public ContactVM getVM() {
+        return vm;
+    }
+
+    private void click() {
+        MomentsBridge momentsBridge =
+            (MomentsBridge) ARouter.getInstance().build(Routes.Service.MOMENTS).navigation();
+        view.header.moments.setVisibility(null==momentsBridge?View.GONE:View.VISIBLE);
+        view.header.moments.setOnClickListener(v -> ARouter.getInstance().build(Routes.Moments.HOME).navigation());
+
+        view.addFriend.setOnClickListener(view1 -> startActivity(new Intent(getActivity(), AddRelationActivity.class)));
+        view.search.setOnClickListener(view1 -> {
+            ARouter.getInstance().build(Routes.Conversation.SEARCH).navigation();
+        });
+
+        header.groupNotice.setOnClickListener(v -> {
+            notificationVM.clearDot(notificationVM.groupDot);
+            startActivity(new Intent(getActivity(), GroupNoticeListActivity.class));
+        });
+
+        header.newFriendNotice.setOnClickListener(v -> {
+            notificationVM.clearDot(notificationVM.friendDot);
+            startActivity(new Intent(getActivity(), NewFriendActivity.class));
+        });
+
+        header.myGoodFriend.setOnClickListener(v -> {
+            SelectTargetVM vm=Easy.installVM(SelectTargetVM.class)
+                .setIntention(SelectTargetVM.Intention.singleSelect);
+            vm.setOnFinishListener(() -> {
+                    Activity activity =ActivityManager.isExist(AllFriendActivity.class);
+                    MultipleChoice target = vm.metaData.val().get(0);
+
+                    ARouter.getInstance().build(Routes.Main.PERSON_DETAIL)
+                        .withString(Constants.K_ID, target.key)
+                        .navigation(activity, 1001);
+
+                });
+            startActivity(new Intent(getActivity(), AllFriendActivity.class));
+        });
+
+        header.myGroup.setOnClickListener(v -> {
+            startActivity(new Intent(getActivity(), MyGroupActivity.class));
+        });
+//        header.labelLy.setOnClickListener(v -> {
+//            startActivity(new Intent(getActivity(), LabelActivity.class));
+//        });
+    }
+
+
+    private void initView() {
+        notificationVM.groupDot.observe(getActivity(), v -> {
+            header.badge.badge.setVisibility(v.size()==0 ? View.GONE : View.VISIBLE);
+            header.badge.badge.setText(v.size() + "");
+        });
+        notificationVM.friendDot.observe(getActivity(), v -> {
+            header.newFriendNoticeBadge.badge.setVisibility(v.size()==0 ? View.GONE : View.VISIBLE);
+            header.newFriendNoticeBadge.badge.setText(v.size() + "");
+        });
+
+      notificationVM.momentsUnread.observe(getActivity(),v->
+            header.newMomentsMsg.badge.setVisibility(v == 0 ? View.GONE : View.VISIBLE));
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+    }
+
+
+}

+ 171 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/fragment/FriendFragment.java

@@ -0,0 +1,171 @@
+package io.openim.android.ouicontact.ui.fragment;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.openim.android.ouicontact.R;
+import io.openim.android.ouicontact.databinding.FragmentForwardFriendBinding;
+import io.openim.android.ouicontact.ui.ForwardToActivity;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.base.BaseFragment;
+import io.openim.android.ouicore.entity.ExUserInfo;
+import io.openim.android.ouicore.vm.SocialityVM;
+import io.openim.android.ouicore.widget.CommonDialog;
+import io.openim.android.sdk.models.FriendInfo;
+
+public class FriendFragment extends BaseFragment<SocialityVM> {
+    private ForwardToActivity.ConfirmListener confirmListener;
+    private FragmentForwardFriendBinding view;
+    private RecyclerViewAdapter<ExUserInfo, RecyclerView.ViewHolder> adapter;
+
+
+    public void setConfirmListener(ForwardToActivity.ConfirmListener confirmListener) {
+        this.confirmListener = confirmListener;
+    }
+
+    public static FriendFragment newInstance() {
+
+        Bundle args = new Bundle();
+
+        FriendFragment fragment = new FriendFragment();
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        bindVM(SocialityVM.class);
+        vm.getAllFriend();
+        super.onCreate(savedInstanceState);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        view = FragmentForwardFriendBinding.inflate(inflater);
+        initView();
+        listener();
+        return view.getRoot();
+    }
+
+    private void initView() {
+        view.scrollView.fullScroll(View.FOCUS_DOWN);
+        view.recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+        adapter = new RecyclerViewAdapter<ExUserInfo, RecyclerView.ViewHolder>() {
+            private int STICKY = 1;
+            private int ITEM = 2;
+
+            private String lastSticky = "";
+
+            @Override
+            public void setItems(List<ExUserInfo> items) {
+                lastSticky = items.get(0).sortLetter;
+                items.add(0, getExUserInfo());
+                for (int i = 0; i < items.size(); i++) {
+                    ExUserInfo userInfo = items.get(i);
+                    if (!lastSticky.equals(userInfo.sortLetter)) {
+                        lastSticky = userInfo.sortLetter;
+                        items.add(i, getExUserInfo());
+                    }
+                }
+                super.setItems(items);
+            }
+
+            @NonNull
+            private ExUserInfo getExUserInfo() {
+                ExUserInfo exUserInfo = new ExUserInfo();
+                exUserInfo.sortLetter = lastSticky;
+                exUserInfo.isSticky = true;
+                return exUserInfo;
+            }
+
+            @Override
+            public int getItemViewType(int position) {
+                return getItems().get(position).isSticky ? STICKY : ITEM;
+            }
+
+            @NonNull
+            @Override
+            public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+                if (viewType == ITEM)
+                    return new ViewHol.ItemViewHo(parent);
+
+                return new ViewHol.StickyViewHo(parent);
+            }
+
+            @Override
+            public void onBindView(@NonNull RecyclerView.ViewHolder holder, ExUserInfo data, int position) {
+                if (getItemViewType(position) == ITEM) {
+                    ViewHol.ItemViewHo itemViewHo = (ViewHol.ItemViewHo) holder;
+                    FriendInfo friendInfo = data.userInfo.getFriendInfo();
+                    itemViewHo.view.avatar.load(friendInfo.getFaceURL(),friendInfo.getNickname());
+                    itemViewHo.view.nickName.setText(friendInfo.getNickname());
+                    itemViewHo.view.select.setVisibility(View.GONE);
+                    itemViewHo.view.getRoot().setOnClickListener(v -> {
+                        CommonDialog commonDialog = new CommonDialog(holder.itemView.getContext());
+                        commonDialog.getMainView().tips.setText(getString(io.openim.android.ouicore.R.string.confirm_send_who)
+                            + data.userInfo.getNickname());
+                        commonDialog.getMainView().cancel.setOnClickListener(v1 -> commonDialog.dismiss());
+                        commonDialog.getMainView().confirm.setOnClickListener(v1 -> {
+                            commonDialog.dismiss();
+                            if (null!=confirmListener)
+                                confirmListener.onListener(data.userInfo,data.userInfo.getUserID());
+                        });
+                        commonDialog.show();
+                    });
+                } else {
+                    ViewHol.StickyViewHo stickyViewHo = (ViewHol.StickyViewHo) holder;
+                    stickyViewHo.view.title.setText(data.sortLetter);
+                }
+            }
+        };
+        view.recyclerView.setAdapter(adapter);
+    }
+
+    private void listener() {
+        vm.letters.observe(getActivity(), v -> {
+            if (null == v || v.isEmpty()) return;
+            StringBuilder letters = new StringBuilder();
+            for (String s : v) {
+                letters.append(s);
+            }
+            view.sortView.setLetters(letters.toString());
+        });
+
+
+        vm.exUserInfo.observe(getActivity(), v -> {
+            if (null == v || v.isEmpty()) return;
+            List<ExUserInfo> exUserInfos = new ArrayList<>(v);
+            adapter.setItems(exUserInfos);
+        });
+
+        view.sortView.setOnLetterChangedListener((letter, position) -> {
+            for (int i = 0; i < adapter.getItems().size(); i++) {
+                ExUserInfo exUserInfo = adapter.getItems().get(i);
+                if (!exUserInfo.isSticky)
+                    continue;
+                if (exUserInfo.sortLetter.equalsIgnoreCase(letter)) {
+                    View viewByPosition = view.recyclerView.getLayoutManager().findViewByPosition(i);
+                    if (viewByPosition != null) {
+                        view.scrollView.smoothScrollTo(0, viewByPosition.getTop());
+                    }
+                    return;
+                }
+            }
+        });
+
+    }
+}

+ 92 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/fragment/GroupFragment.java

@@ -0,0 +1,92 @@
+package io.openim.android.ouicontact.ui.fragment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.Observer;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+import io.openim.android.ouicontact.ui.ForwardToActivity;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.base.BaseFragment;
+import io.openim.android.ouicore.databinding.ViewRecyclerViewBinding;
+import io.openim.android.ouicore.entity.ExUserInfo;
+import io.openim.android.ouicore.vm.SocialityVM;
+import io.openim.android.ouicore.widget.CommonDialog;
+import io.openim.android.sdk.models.GroupInfo;
+
+public class GroupFragment extends BaseFragment<SocialityVM> {
+    private ForwardToActivity.ConfirmListener confirmListener;
+    ViewRecyclerViewBinding view;
+    private RecyclerViewAdapter<GroupInfo, ViewHol.GroupViewHo> adapter;
+
+    public void setConfirmListener(ForwardToActivity.ConfirmListener confirmListener) {
+        this.confirmListener = confirmListener;
+    }
+
+    public static GroupFragment newInstance() {
+
+        Bundle args = new Bundle();
+
+        GroupFragment fragment = new GroupFragment();
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        bindVM(SocialityVM.class);
+        vm.getAllGroup();
+        super.onCreate(savedInstanceState);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        view = ViewRecyclerViewBinding.inflate(inflater);
+        initView();
+        listener();
+        return view.getRoot();
+    }
+
+    private void initView() {
+        view.recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+        adapter = new RecyclerViewAdapter<GroupInfo, ViewHol.GroupViewHo>(ViewHol.GroupViewHo.class) {
+
+            @Override
+            public void onBindView(@NonNull ViewHol.GroupViewHo holder, GroupInfo data, int position) {
+                holder.view.avatar.load(data.getFaceURL(),data.getGroupName());
+                holder.view.title.setText(data.getGroupName());
+                holder.view.description.setText(data.getMemberCount() + "人");
+
+                holder.view.getRoot().setOnClickListener(v -> {
+                    CommonDialog commonDialog = new CommonDialog(getContext());
+                    commonDialog.getMainView().tips.setText("确认发送给:" + data.getGroupName());
+                    commonDialog.getMainView().cancel.setOnClickListener(v1 -> commonDialog.dismiss());
+                    commonDialog.getMainView().confirm.setOnClickListener(v1 -> {
+                        if (null != confirmListener)
+                            confirmListener.onListener(null, data.getGroupID());
+                    });
+                    commonDialog.show();
+                });
+
+            }
+        };
+        view.recyclerView.setAdapter(adapter);
+    }
+
+    private void listener() {
+        vm.groups.observe(getActivity(), groupInfos -> {
+            adapter.setItems(groupInfos);
+        });
+    }
+
+}

+ 7 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/search/GroupInfoExpand.java

@@ -0,0 +1,7 @@
+package io.openim.android.ouicontact.ui.search;
+
+import io.openim.android.sdk.models.GroupInfo;
+
+public class GroupInfoExpand extends GroupInfo {
+    public String groupType; //群组类型 我的加入的 我创建的
+}

+ 160 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/search/SearchGroupActivity.java

@@ -0,0 +1,160 @@
+package io.openim.android.ouicontact.ui.search;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.alibaba.android.arouter.launcher.ARouter;
+import com.blankj.utilcode.util.ConvertUtils;
+import com.blankj.utilcode.util.LogUtils;
+import com.blankj.utilcode.util.StringUtils;
+
+import java.util.List;
+
+import io.openim.android.ouicontact.R;
+import io.openim.android.ouicontact.databinding.ActivitySerchGroupBinding;
+import io.openim.android.ouicontact.vm.SearchGroup;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.base.BaseApp;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.widget.HeaderAndFooterWrapper;
+import io.openim.android.ouicore.widget.StickyDecoration;
+import io.openim.android.sdk.models.GroupInfo;
+
+/**
+ * 搜索群组
+ */
+public class SearchGroupActivity extends BaseActivity<SearchGroup, ActivitySerchGroupBinding> {
+
+    private ContentAdapter adapter;
+    private Handler handler = new Handler();
+    private LinearLayout bottomLayout;
+    private TextView itemCount;
+    private HeaderAndFooterWrapper headerAndFooterWrapper;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVMByCache(SearchGroup.class);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivitySerchGroupBinding.inflate(getLayoutInflater()));
+        sink();
+        initView();
+        listener();
+    }
+
+    private void listener() {
+        vm.searchGroups.observe(this, groupInfos -> {
+            runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    if (bottomLayout != null) {
+                        bottomLayout.setVisibility(groupInfos.isEmpty() ? View.GONE : View.VISIBLE);
+                    }
+                    if (itemCount != null) {
+                        itemCount.setText(String.format(StringUtils.getString(io.openim.android.ouicore.R.string.a_total_of_groups), groupInfos.size()));
+                    }
+                    adapter.setItems(groupInfos);
+                    headerAndFooterWrapper.notifyDataSetChanged();
+                }
+            });
+
+
+        });
+        vm.searchKey.observe(this, s -> {
+            vm.search(s);
+        });
+        view.searchView.getEditText().addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+                if (s.toString().isEmpty()) vm.searchKey.setValue("");
+            }
+
+            @Override
+            public void afterTextChanged(Editable s) {
+                handler.removeCallbacksAndMessages(null);
+                handler.postDelayed(() -> {
+                    String input = s.toString();
+                    vm.searchKey.setValue(input);
+                }, 300);
+            }
+        });
+    }
+
+    private void initView() {
+        view.searchView.getEditText().setHint(io.openim.android.ouicore.R.string.search_group);
+        view.recyclerview.setLayoutManager(new LinearLayoutManager(this));
+        adapter = new ContentAdapter(ViewHol.GroupViewHo.class);
+        headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);
+        headerAndFooterWrapper.addFooterView(bottomLayout());
+        view.recyclerview.setAdapter(headerAndFooterWrapper);
+        view.recyclerview.addItemDecoration(new StickyDecoration(this,
+                position -> {
+                    LogUtils.e(position + "---" + headerAndFooterWrapper.getItemCount());
+                    if (position + 1 == headerAndFooterWrapper.getItemCount()) {
+                        return "";
+                    }
+                    int pos = position;
+                    String str = adapter.getItems().get(pos).groupType;
+                    return str;
+                }));
+    }
+
+    private View bottomLayout() {
+        View top = LayoutInflater.from(this).inflate(R.layout.view_seach_group_bottom, null);
+        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ConvertUtils.dp2px(68));
+        top.setLayoutParams(params);
+        bottomLayout = top.findViewById(R.id.layout);
+        itemCount = top.findViewById(R.id.count);
+        return top;
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        removeCacheVM();
+    }
+
+    public static void jumpThis(Context ctx, List<GroupInfo> groupInfos) {
+        SearchGroup searchGroup = new SearchGroup();
+        searchGroup.groups.setValue(groupInfos);
+        BaseApp.inst().putVM(searchGroup);
+        ctx.startActivity(new Intent(ctx, SearchGroupActivity.class));
+    }
+
+    public class ContentAdapter extends RecyclerViewAdapter<GroupInfoExpand, ViewHol.GroupViewHo> {
+
+        public ContentAdapter(Class<ViewHol.GroupViewHo> viewHolder) {
+            super(viewHolder);
+        }
+
+        @Override
+        public void onBindView(@NonNull ViewHol.GroupViewHo holder, GroupInfoExpand data, int position) {
+            holder.view.avatar.load(data.getFaceURL(), true);
+            holder.view.title.setText(data.getGroupName());
+            holder.view.description.setText(data.getMemberCount() + SearchGroupActivity.this.getString(io.openim.android.ouicore.R.string.people));
+
+            holder.view.getRoot().setOnClickListener(v -> {
+                ARouter.getInstance().build(Routes.Conversation.CHAT).withString(Constants.K_GROUP_ID, data.getGroupID()).withString(Constants.K_NAME, data.getGroupName()).navigation();
+            });
+        }
+    }
+}

+ 266 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/search/SearchGroupAndFriendsActivity.java

@@ -0,0 +1,266 @@
+package io.openim.android.ouicontact.ui.search;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.alibaba.android.arouter.facade.annotation.Route;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import io.openim.android.ouicontact.databinding.ActivityOftenSerchBinding;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.base.vm.injection.Easy;
+import io.openim.android.ouicore.ex.MultipleChoice;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.vm.SearchVM;
+import io.openim.android.ouicore.vm.SelectTargetVM;
+import io.openim.android.sdk.models.GroupInfo;
+import io.openim.android.sdk.models.UserInfo;
+
+/**
+ * 搜索最近会话用户
+ */
+@Route(path = Routes.Contact.SEARCH_FRIENDS_GROUP)
+public class SearchGroupAndFriendsActivity extends BaseActivity<SearchVM, ActivityOftenSerchBinding> {
+
+    private final Handler handler = new Handler();
+
+    private static final int TITLE = 0;
+    private static final int CONTACT_ITEM = 1;
+    private static final int GROUP_ITEM = 2;
+    private RecyclerViewAdapter adapter;
+
+    private Set<MultipleChoice> result = new HashSet<>();
+    //只选择联系人
+    private boolean isOnlyFriend;
+    //不为空表示多选
+    private List<MultipleChoice> choices;
+
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVM(SearchVM.class);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityOftenSerchBinding.inflate(getLayoutInflater()));
+
+        init();
+        initView();
+        listener();
+    }
+
+    public boolean isMultipleSelect() {
+        return null != choices;
+    }
+
+    @Override
+    public void onBackPressed() {
+        setResult();
+        super.onBackPressed();
+    }
+
+    private void setResult() {
+        if (!result.isEmpty()) {
+            setResult(RESULT_OK, new Intent().putExtra(Constants.K_RESULT, (Serializable) result));
+
+        }
+    }
+
+    private void listener() {
+        view.cancel.setOnClickListener(v -> {
+            setResult();
+            finish();
+        });
+        vm.searchContent.observe(this, s -> {
+            clearData();
+            if (s.isEmpty()) return;
+            vm.page = 1;
+
+            if (isOnlyFriend) {
+                vm.searchFriendV2();
+                return;
+            }
+            vm.searchFriendV2();
+            vm.searchGroupV2();
+        });
+
+        view.searchView.getEditText().addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+                if (s.toString().isEmpty()) vm.searchContent.setValue("");
+            }
+
+            @Override
+            public void afterTextChanged(Editable s) {
+                handler.removeCallbacksAndMessages(null);
+                handler.postDelayed(() -> {
+                    String input = s.toString();
+                    vm.page = 1;
+                    vm.searchContent.setValue(input);
+                }, 500);
+
+            }
+        });
+
+        vm.userInfo.observe(this, userInfos -> {
+            addNape(getString(io.openim.android.ouicore.R.string.contact), userInfos);
+        });
+        vm.groupsInfo.observe(this, groupInfos -> {
+            addNape(getString(io.openim.android.ouicore.R.string.group), groupInfos);
+        });
+
+    }
+
+    private void addNape(String title, List data) {
+        if (data.isEmpty()) return;
+        int start = adapter.getItems().size();
+        adapter.getItems().add(title);//包含1表示有查看更多
+        adapter.getItems().addAll(data);
+        adapter.notifyItemRangeInserted(start, adapter.getItems().size());
+    }
+
+    private void clearData() {
+        vm.clearData();
+        adapter.getItems().clear();
+        adapter.notifyDataSetChanged();
+    }
+
+    private void initView() {
+        view.recyclerview.setLayoutManager(new LinearLayoutManager(this));
+        view.recyclerview.setAdapter(adapter = new RecyclerViewAdapter() {
+
+            @Override
+            public int getItemViewType(int position) {
+                Object o = getItems().get(position);
+                if (o instanceof String) return TITLE;
+                if (o instanceof UserInfo) return CONTACT_ITEM;
+                if (o instanceof GroupInfo) return GROUP_ITEM;
+                return super.getItemViewType(position);
+            }
+
+            @NonNull
+            @Override
+            public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+                if (viewType == TITLE) return new ViewHol.TitleViewHolder(parent);
+                if (viewType == CONTACT_ITEM || viewType == GROUP_ITEM)
+                    return new ViewHol.ItemViewHo(parent);
+
+                return super.onCreateViewHolder(parent, viewType);
+            }
+
+            @Override
+            public void onBindView(@NonNull RecyclerView.ViewHolder holder, Object data, int position) {
+                if (getItemViewType(position) == TITLE) {
+                    String title = (String) data;
+                    ViewHol.TitleViewHolder titleViewHolder = (ViewHol.TitleViewHolder) holder;
+                    titleViewHolder.view.title.setText(title);
+                } else {
+                    ViewHol.ItemViewHo itemViewHo = (ViewHol.ItemViewHo) holder;
+                    itemViewHo.view.select.setVisibility(isMultipleSelect() ? View.VISIBLE : View.GONE);
+
+                    final Intent intent = new Intent();
+
+                    MultipleChoice multipleChoice = new MultipleChoice();
+                    String id = "";
+                    if (data instanceof UserInfo) {
+                        //联系人
+                        UserInfo da = (UserInfo) data;
+                        itemViewHo.view.avatar.load(da.getFaceURL(),da.getNickname());
+                        itemViewHo.view.nickName.setText(da.getNickname());
+
+                        intent.putExtra(Constants.K_ID, id = da.getUserID());
+                        intent.putExtra(Constants.K_NAME, da.getNickname());
+                        multipleChoice.isGroup = false;
+                        multipleChoice.name = da.getNickname();
+                        multipleChoice.icon = da.getFaceURL();
+                    }
+                    if ((data instanceof GroupInfo)) {
+                        //群组
+                        GroupInfo groupInfo = (GroupInfo) data;
+                        itemViewHo.view.avatar.load(groupInfo.getFaceURL(), true);
+                        itemViewHo.view.nickName.setText(groupInfo.getGroupName());
+
+                        intent.putExtra(Constants.K_GROUP_ID, id = groupInfo.getGroupID());
+                        intent.putExtra(Constants.K_NAME, groupInfo.getGroupName());
+                        multipleChoice.isGroup = true;
+                        multipleChoice.name = groupInfo.getGroupName();
+                        multipleChoice.icon = groupInfo.getFaceURL();
+                    }
+                    multipleChoice.key = id;
+                    MultipleChoice target = null;
+                    itemViewHo.view.select.setChecked(false);
+                    if (isMultipleSelect()) {
+                        int index = choices.indexOf(new MultipleChoice(id));
+                        if (index != -1) {
+                            itemViewHo.view.select.setChecked(true);
+                            target = choices.get(index);
+                            itemViewHo.view.select.setEnabled(target.isEnabled);
+                            itemViewHo.view.select.setAlpha(target.isEnabled ? 1f : 0.5f);
+                        }
+                    }
+
+                    MultipleChoice finalTarget = target;
+                    itemViewHo.view.getRoot().setOnClickListener(v -> {
+                        if (null != finalTarget && !finalTarget.isEnabled) return;
+
+                        if (isMultipleSelect()) {
+                            itemViewHo.view.select.setChecked(!itemViewHo.view.select.isChecked());
+                            multipleChoice.isSelect = itemViewHo.view.select.isChecked();
+                            result.add(multipleChoice);
+
+                            if (multipleChoice.isSelect)
+                                choices.add(multipleChoice);
+                            else
+                                choices.remove(multipleChoice);
+                            return;
+                        }
+
+                        try {
+                            SelectTargetVM selectTargetVM = Easy.find(SelectTargetVM.class);
+                            if (null != selectTargetVM) {
+                                selectTargetVM.addMetaData(multipleChoice.key, multipleChoice.name, multipleChoice.icon);
+                                if (selectTargetVM.isSingleSelect())
+                                    selectTargetVM.toFinish();
+                                else
+                                    selectTargetVM.finishIntention();
+                            }
+                            return;
+                        } catch (Exception ignore) {
+                        }
+
+
+                        setResult(RESULT_OK, intent);
+                        finish();
+                    });
+
+                }
+            }
+        });
+        adapter.setItems(new ArrayList());
+    }
+
+    void init() {
+        choices = (List<MultipleChoice>) getIntent().getSerializableExtra(Constants.K_RESULT);
+        isOnlyFriend = getIntent().getBooleanExtra(Constants.IS_SELECT_FRIEND, false);
+    }
+}

+ 160 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/ui/search/SearchGroupMemberActivity.java

@@ -0,0 +1,160 @@
+package io.openim.android.ouicontact.ui.search;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.alibaba.android.arouter.facade.annotation.Route;
+
+import io.openim.android.ouicontact.databinding.ActivityOftenSerchBinding;
+import io.openim.android.ouicore.adapter.RecyclerViewAdapter;
+import io.openim.android.ouicore.adapter.ViewHol;
+import io.openim.android.ouicore.base.BaseActivity;
+import io.openim.android.ouicore.base.vm.injection.Easy;
+import io.openim.android.ouicore.ex.MultipleChoice;
+import io.openim.android.ouicore.utils.Common;
+import io.openim.android.ouicore.utils.Routes;
+import io.openim.android.ouicore.vm.GroupMemberVM;
+import io.openim.android.ouicore.vm.SearchVM;
+import io.openim.android.sdk.models.GroupMembersInfo;
+
+/**
+ * 搜索群组成员
+ */
+@Route(path = Routes.Contact.SEARCH_GROUP_MEMBER)
+public class SearchGroupMemberActivity extends BaseActivity<SearchVM, ActivityOftenSerchBinding> {
+
+    private final Handler handler = new Handler();
+
+
+    private RecyclerViewAdapter adapter;
+    private GroupMemberVM memberVM;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        bindVM(SearchVM.class);
+        memberVM = Easy.find(GroupMemberVM.class);
+        super.onCreate(savedInstanceState);
+        bindViewDataBinding(ActivityOftenSerchBinding.inflate(getLayoutInflater()));
+        sink();
+        initView();
+        listener();
+    }
+
+    private void initView() {
+        view.recyclerview.setLayoutManager(new LinearLayoutManager(this));
+        view.recyclerview.setAdapter(adapter = new RecyclerViewAdapter<GroupMembersInfo, ViewHol.ItemViewHo>(ViewHol.ItemViewHo.class) {
+            @Override
+            public void onBindView(@NonNull ViewHol.ItemViewHo holder, GroupMembersInfo data, int position) {
+                holder.view.select.setVisibility(memberVM.isSearchSingle ? View.GONE : (memberVM.isAt() || memberVM.isMultiple() ? View.VISIBLE : View.GONE));
+
+                int index = memberVM.choiceList.val().indexOf(new MultipleChoice(data.getUserID()));
+                boolean isChecked = index != -1;
+                holder.view.select.setChecked(isChecked);
+                boolean isEnabled = true;
+                if (isChecked)
+                    isEnabled = memberVM.choiceList.val().get(index).isEnabled;
+                holder.view.getRoot().setIntercept(!isEnabled);
+                holder.view.getRoot().setAlpha(isEnabled ? 1f : 0.3f);
+
+                holder.view.avatar.load(data.getFaceURL(),data.getNickname());
+                Common.stringBindForegroundColorSpan(holder.view.nickName, data.getNickname(), vm.searchContent.getValue());
+
+                holder.view.item.setOnClickListener(v -> {
+                    boolean isSingle = memberVM.isSingle();
+                    if (memberVM.isSearchSingle)
+                        isSingle = true;
+
+                    if (memberVM.isCheck() || isSingle) {
+                        MultipleChoice choice = new MultipleChoice(data.getUserID());
+                        choice.name = data.getNickname();
+                        choice.icon = data.getFaceURL();
+                        memberVM.addChoice(choice);
+                        if (memberVM.isCheck()) {
+                            memberVM.onFinish(SearchGroupMemberActivity.this);
+                        } else {
+                            memberVM.choiceList.update();
+                            finish();
+                        }
+                        return;
+                    }
+                    boolean isSelect = !isChecked;
+                    if (isSelect) {
+                        if (memberVM.choiceList.val().size() >= memberVM.maxNum) {
+                            toast(String.format(getString(io.openim.android.ouicore.R.string.select_tips), memberVM.maxNum));
+                            return;
+                        }
+                        MultipleChoice choice = new MultipleChoice(data.getUserID());
+                        choice.name = data.getNickname();
+                        choice.icon = data.getFaceURL();
+                        memberVM.addChoice(choice);
+                    } else {
+                        memberVM.removeChoice(data.getUserID());
+                    }
+                    memberVM.choiceList.update();
+                    notifyItemChanged(getItems().indexOf(data));
+                });
+            }
+        });
+        adapter.setItems(vm.groupMembersInfo.getValue());
+    }
+
+    private void listener() {
+        view.cancel.setOnClickListener(v -> finish());
+        vm.groupMembersInfo.observe(this, groupMembersInfos -> {
+            if (memberVM.isAt())
+                memberVM.removeSelf(groupMembersInfos);
+            if (memberVM.isRemoveOwnerAndAdmin)
+                memberVM.removeOwnerAndAdmin(groupMembersInfos);
+
+            adapter.notifyDataSetChanged();
+        });
+        vm.userInfo.observe(this, friendInfos -> adapter.notifyDataSetChanged());
+
+        vm.searchContent.observe(this, s -> {
+            vm.groupMembersInfo.getValue().clear();
+            adapter.notifyDataSetChanged();
+
+            if (!s.isEmpty())
+                loadMore();
+        });
+
+        view.searchView.getEditText().addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+                if (s.toString().isEmpty())
+                    vm.searchContent.setValue("");
+            }
+
+            @Override
+            public void afterTextChanged(Editable s) {
+                handler.removeCallbacksAndMessages(null);
+                handler.postDelayed(() -> {
+                    String input = s.toString();
+                    vm.page = 0;
+                    vm.searchContent.setValue(input);
+                }, 500);
+            }
+        });
+
+
+        adapter.setOnLoadMoreListener(view.recyclerview, vm.pageSize, () -> {
+            vm.page++;
+            loadMore();
+        });
+    }
+
+    private void loadMore() {
+        vm.searchGroupMemberByNickname(memberVM.groupId, vm.searchContent.getValue());
+    }
+}

+ 109 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/vm/ContactVM.java

@@ -0,0 +1,109 @@
+package io.openim.android.ouicontact.vm;
+
+import java.util.List;
+
+import io.openim.android.ouicore.base.BaseViewModel;
+import io.openim.android.ouicore.base.vm.State;
+import io.openim.android.ouicore.utils.L;
+import io.openim.android.sdk.OpenIMClient;
+import io.openim.android.sdk.listener.OnBase;
+import io.openim.android.sdk.listener.OnFriendshipListener;
+import io.openim.android.sdk.listener.OnGroupListener;
+import io.openim.android.sdk.models.FriendApplicationInfo;
+import io.openim.android.sdk.models.GroupApplicationInfo;
+import io.openim.android.sdk.models.UserInfo;
+
+public class ContactVM extends BaseViewModel implements OnGroupListener, OnFriendshipListener {
+
+    //申请列表
+    public State<List<GroupApplicationInfo>> groupApply = new State<>();
+    //好友申请列表
+    public State<List<FriendApplicationInfo>> friendApply = new State<>();
+    //申请详情
+    public State<GroupApplicationInfo> groupDetail = new State<>();
+    //好友申请详情
+    public State<FriendApplicationInfo> friendDetail = new State<>();
+    //常联系的好友
+    public State<UserInfo> frequentContacts = new State<>();
+
+
+    //个人申请列表
+    public void getRecvFriendApplicationList() {
+        OpenIMClient.getInstance().friendshipManager.getRecvFriendApplicationList(new OnBase<List<FriendApplicationInfo>>() {
+            @Override
+            public void onError(int code, String error) {
+
+            }
+
+            @Override
+            public void onSuccess(List<FriendApplicationInfo> data) {
+                if (data.isEmpty()) return;
+                friendApply.setValue(data);
+            }
+        });
+    }
+
+    //群申请列表
+    public void getRecvGroupApplicationList() {
+        OpenIMClient.getInstance().groupManager.getRecvGroupApplicationList(new OnBase<List<GroupApplicationInfo>>() {
+            @Override
+            public void onError(int code, String error) {
+                L.e("");
+            }
+
+            @Override
+            public void onSuccess(List<GroupApplicationInfo> data) {
+                if (!data.isEmpty())
+                    groupApply.setValue(data);
+            }
+        });
+    }
+
+    private OnBase onBase = new OnBase<String>() {
+        @Override
+        public void onError(int code, String error) {
+            getIView().toast(error);
+        }
+
+        @Override
+        public void onSuccess(String data) {
+            if (null != groupDetail)
+                getRecvGroupApplicationList();
+            if (null != friendDetail)
+                getRecvFriendApplicationList();
+            getIView().onSuccess(null);
+        }
+    };
+
+    //好友通过
+    public void friendPass() {
+        OpenIMClient.getInstance().friendshipManager.acceptFriendApplication(onBase, friendDetail.getValue().getFromUserID(), "");
+    }
+
+
+    //好友拒绝
+    public void friendRefuse() {
+        OpenIMClient.getInstance().friendshipManager.refuseFriendApplication(onBase, friendDetail.getValue().getFromUserID(), "");
+    }
+
+    //群通过
+    public void pass() {
+        OpenIMClient.getInstance().groupManager.acceptGroupApplication(onBase, groupDetail.getValue().getGroupID(), groupDetail.getValue().getUserID(), "");
+    }
+
+    //群拒绝
+    public void refuse() {
+        OpenIMClient.getInstance().groupManager.refuseGroupApplication(onBase,
+            groupDetail.getValue().getGroupID(), groupDetail.getValue().getUserID(), "");
+    }
+
+    @Override
+    public void onGroupApplicationAccepted(GroupApplicationInfo info) {
+        getRecvGroupApplicationList();
+    }
+
+    @Override
+    public void onGroupApplicationRejected(GroupApplicationInfo info) {
+        getRecvGroupApplicationList();
+    }
+}

+ 96 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/vm/LabelVM.java

@@ -0,0 +1,96 @@
+package io.openim.android.ouicontact.vm;
+
+import androidx.lifecycle.MutableLiveData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.openim.android.ouicore.api.NiService;
+import io.openim.android.ouicore.base.BaseApp;
+import io.openim.android.ouicore.base.BaseViewModel;
+import io.openim.android.ouicore.entity.UserLabel;
+import io.openim.android.ouicore.im.IMUtil;
+import io.openim.android.ouicore.net.RXRetrofit.N;
+import io.openim.android.ouicore.net.RXRetrofit.NetObserver;
+import io.openim.android.ouicore.net.RXRetrofit.Parameter;
+import io.openim.android.ouicore.api.OneselfService;
+import io.openim.android.ouicore.utils.Constants;
+import io.openim.android.ouicore.widget.WaitDialog;
+
+public class LabelVM extends BaseViewModel {
+
+    private static final String TAG = "LabelVM";
+    public MutableLiveData<List<UserLabel>> userLabels = new MutableLiveData<>(new ArrayList<>());
+
+    public void getUserTags() {
+        Parameter parameter = getParameter();
+        N.API(NiService.class).CommNI(Constants.getImApiUrl() + "office/get_user_tags",
+            BaseApp.inst().loginCertificate.imToken, parameter.buildJsonBody()).compose(N.IOMain()).map(OneselfService.turn(UserLabel.class)).subscribe(new NetObserver<UserLabel>(TAG) {
+            @Override
+            public void onSuccess(UserLabel o) {
+                if (null == o.getTags() || o.getTags().isEmpty()) return;
+                userLabels.setValue(o.getTags());
+            }
+
+            @Override
+            protected void onFailure(Throwable e) {
+                getIView().toast(e.getMessage());
+            }
+        });
+    }
+
+    public void createTag(String tagName, List<String> ids,
+                          IMUtil.OnSuccessListener successListener) {
+        Parameter parameter = getParameter();
+//        'tagName': tagName,
+//         'userIDList': userIDList,
+
+        N.API(NiService.class).CommNI(Constants.getImApiUrl() + "office/create_tag",
+            BaseApp.inst().loginCertificate.imToken,
+            parameter.add("tagName", tagName).add(
+                "userIDList", ids).buildJsonBody()).compose(N.IOMain()).map(OneselfService.turn(Object.class)).subscribe(new NetObserver<Object>(TAG) {
+            @Override
+            public void onSuccess(Object o) {
+                successListener.onSuccess(null);
+            }
+
+            @Override
+            protected void onFailure(Throwable e) {
+                getIView().toast(e.getMessage());
+            }
+        });
+    }
+
+    public void removeTag(UserLabel userLabel, boolean isShowWaiting) {
+        WaitDialog waitDialog = null;
+        if (isShowWaiting) {
+            waitDialog = new WaitDialog(getContext());
+            waitDialog.show();
+        }
+        WaitDialog finalWaitDialog = waitDialog;
+        N.API(NiService.class).CommNI(Constants.getImApiUrl() + "office/delete_tag",
+            BaseApp.inst().loginCertificate.imToken, getParameter().add("tagID",
+                userLabel.getTagID()).buildJsonBody()).compose(N.IOMain()).map(OneselfService.turn(Object.class)).subscribe(new NetObserver<Object>(TAG) {
+            @Override
+            public void onSuccess(Object o) {
+                userLabels.getValue().remove(userLabel);
+                userLabels.setValue(userLabels.getValue());
+            }
+
+            @Override
+            protected void onFailure(Throwable e) {
+                getIView().toast(e.getMessage());
+            }
+
+            @Override
+            public void onComplete() {
+                if (null != finalWaitDialog) finalWaitDialog.dismiss();
+            }
+        });
+    }
+
+    private Parameter getParameter() {
+        Parameter parameter = new Parameter().add("operationID", System.currentTimeMillis() + "");
+        return parameter;
+    }
+}

+ 52 - 0
OUIContact/src/main/java/io/openim/android/ouicontact/vm/SearchGroup.java

@@ -0,0 +1,52 @@
+package io.openim.android.ouicontact.vm;
+
+import android.text.TextUtils;
+
+import androidx.lifecycle.MutableLiveData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.openim.android.ouicontact.R;
+import io.openim.android.ouicontact.ui.search.GroupInfoExpand;
+import io.openim.android.ouicore.base.BaseApp;
+import io.openim.android.ouicore.base.BaseViewModel;
+import io.openim.android.sdk.models.GroupInfo;
+
+public class SearchGroup extends BaseViewModel {
+    //我加入的群
+    public MutableLiveData<List<GroupInfo>> groups = new MutableLiveData<>(new ArrayList<>());
+    public MutableLiveData<List<GroupInfoExpand>> searchGroups = new MutableLiveData<>(new ArrayList<>());
+    public MutableLiveData<String> searchKey = new MutableLiveData<>();
+
+    public void search(String key) {
+        searchGroups.getValue().clear();
+        if (!TextUtils.isEmpty(key)) {
+            for (GroupInfo groupInfo : groups.getValue()) {
+                if (groupInfo.getGroupName().toUpperCase().contains(key.toUpperCase())) {
+                    GroupInfoExpand expand = new GroupInfoExpand();
+                    if (groupInfo.getCreatorUserID().equals(BaseApp.inst().loginCertificate.userID)) {
+                        //我创建的
+                        expand.groupType = getContext().getString(io.openim.android.ouicore.R.string.the_group_I_created);
+                    }else {
+                        //我加入的
+                        expand.groupType = getContext().getString(io.openim.android.ouicore.R.string.the_group_I_joined);
+                    }
+                    expand.setGroupID(groupInfo.getGroupID());
+                    expand.setGroupName(groupInfo.getGroupName());
+                    expand.setFaceURL(groupInfo.getFaceURL());
+                    expand.setGroupType(groupInfo.getGroupType());
+                    expand.setEx(groupInfo.getEx());
+                    expand.setApplyMemberFriend(groupInfo.getApplyMemberFriend());
+                    expand.setCreateTime(groupInfo.getCreateTime());
+                    expand.setCreatorUserID(groupInfo.getCreatorUserID());
+                    expand.setApplyMemberFriend(groupInfo.getApplyMemberFriend());
+                    expand.setIntroduction(groupInfo.getIntroduction());
+                    expand.setMemberCount(groupInfo.getMemberCount());
+                    searchGroups.getValue().add(expand);
+                }
+            }
+        }
+        searchGroups.setValue(searchGroups.getValue());
+    }
+}

BIN
OUIContact/src/main/res/drawable/ic_back.png


BIN
OUIContact/src/main/res/drawable/ic_close.png


BIN
OUIContact/src/main/res/drawable/ic_open.png


BIN
OUIContact/src/main/res/drawable/ic_photo_2.png


+ 240 - 0
OUIContact/src/main/res/layout/activity_add_relation.xml

@@ -0,0 +1,240 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/white"
+        android:fitsSystemWindows="true"
+        android:orientation="vertical"
+        tools:context="io.openim.android.ouicontact.ui.AddRelationActivity">
+
+        <androidx.cardview.widget.CardView
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/comm_title_high"
+            app:cardElevation="0dp">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent">
+
+                <include
+                    android:id="@+id/back"
+                    layout="@layout/view_back" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerInParent="true"
+                    android:text="@string/add"
+                    android:textStyle="bold"
+                    android:textColor="@color/color_181818"
+                    android:textSize="16sp" />
+            </RelativeLayout>
+
+        </androidx.cardview.widget.CardView>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:background="@color/theme_bg2"
+            android:orientation="vertical">
+
+            <io.openim.android.ouicore.widget.InterceptLinearLayout
+                android:id="@+id/san"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/white"
+                android:gravity="center_vertical"
+                android:orientation="horizontal"
+                android:paddingHorizontal="15dp"
+                android:paddingVertical="15dp">
+
+                <ImageView
+                    android:layout_width="26dp"
+                    android:layout_height="26dp"
+                    android:src="@mipmap/ic_add_san" />
+
+                <LinearLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="12dp"
+                    android:layout_weight="1"
+                    android:orientation="vertical">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginBottom="2dp"
+                        android:text="@string/san_it"
+                        android:textColor="@color/color_181818"
+                        android:textSize="18sp" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="@string/scan_qr_card"
+                        android:textColor="@color/color_b2b2b2"
+                        android:textSize="13sp" />
+                </LinearLayout>
+
+                <ImageView
+                    android:layout_width="16dp"
+                    android:layout_height="16dp"
+                    android:src="@mipmap/ic_right" />
+            </io.openim.android.ouicore.widget.InterceptLinearLayout>
+
+            <View
+                style="@style/divider_horizontal"
+                android:layout_marginHorizontal="15dp" />
+
+            <io.openim.android.ouicore.widget.InterceptLinearLayout
+                android:id="@+id/addFriend"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/white"
+                android:gravity="center_vertical"
+                android:orientation="horizontal"
+                android:paddingHorizontal="15dp"
+                android:paddingVertical="15dp">
+
+                <ImageView
+                    android:layout_width="26dp"
+                    android:layout_height="26dp"
+                    android:src="@mipmap/ic_c_add_friend" />
+
+                <LinearLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="12dp"
+                    android:layout_weight="1"
+                    android:orientation="vertical">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginBottom="2dp"
+                        android:text="@string/add_friend"
+                        android:textColor="@color/color_181818"
+                        android:textSize="18sp" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="@string/search_by_id"
+                        android:textColor="@color/color_b2b2b2"
+                        android:textSize="13sp" />
+                </LinearLayout>
+
+                <ImageView
+                    android:layout_width="16dp"
+                    android:layout_height="16dp"
+                    android:src="@mipmap/ic_right" />
+            </io.openim.android.ouicore.widget.InterceptLinearLayout>
+
+            <View
+                style="@style/divider_horizontal"
+                android:layout_marginHorizontal="15dp" />
+
+            <io.openim.android.ouicore.widget.InterceptLinearLayout
+                android:id="@+id/createGroup"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/white"
+                android:gravity="center_vertical"
+                android:orientation="horizontal"
+                android:paddingVertical="15dp"
+                android:paddingHorizontal="15dp">
+
+                <ImageView
+                    android:layout_width="26dp"
+                    android:layout_height="26dp"
+                    android:src="@mipmap/ic_c_create_group" />
+
+                <LinearLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="12dp"
+                    android:layout_weight="1"
+                    android:orientation="vertical">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginBottom="2dp"
+                        android:text="@string/create_group"
+                        android:textColor="@color/color_181818"
+                        android:textSize="18sp" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="@string/create_group_tips"
+                        android:textColor="@color/color_b2b2b2"
+                        android:textSize="13sp" />
+                </LinearLayout>
+
+                <ImageView
+                    android:layout_width="16dp"
+                    android:layout_height="16dp"
+                    android:src="@mipmap/ic_right" />
+            </io.openim.android.ouicore.widget.InterceptLinearLayout>
+
+            <View
+                style="@style/divider_horizontal"
+                android:layout_marginHorizontal="15dp" />
+
+            <io.openim.android.ouicore.widget.InterceptLinearLayout
+                android:id="@+id/addGroup"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/white"
+                android:gravity="center_vertical"
+                android:orientation="horizontal"
+                android:paddingHorizontal="15dp"
+                android:paddingVertical="15dp">
+
+                <ImageView
+                    android:layout_width="26dp"
+                    android:layout_height="26dp"
+                    android:src="@mipmap/ic_c_add_group" />
+
+                <LinearLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="12dp"
+                    android:layout_weight="1"
+                    android:orientation="vertical">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginBottom="2dp"
+                        android:text="@string/add_group"
+                        android:textColor="@color/color_181818"
+                        android:textSize="18sp" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="@string/add_group_tips"
+                        android:textColor="@color/color_b2b2b2"
+                        android:textSize="13sp" />
+                </LinearLayout>
+
+                <ImageView
+                    android:layout_width="16dp"
+                    android:layout_height="16dp"
+                    android:src="@mipmap/ic_right" />
+            </io.openim.android.ouicore.widget.InterceptLinearLayout>
+
+            <View
+                style="@style/divider_horizontal"
+                android:layout_marginHorizontal="15dp" />
+        </LinearLayout>
+    </LinearLayout>
+</layout>
+

+ 96 - 0
OUIContact/src/main/res/layout/activity_all_friend.xml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        tools:context=".ui.AllFriendActivity">
+
+        <androidx.cardview.widget.CardView
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/comm_title_high"
+            app:cardElevation="0dp">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent">
+
+                <include
+                    android:id="@+id/back"
+                    layout="@layout/view_back" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerInParent="true"
+                    android:text="@string/my_good_friend"
+                    android:textColor="@color/color_181818"
+                    android:textSize="16dp"
+                    android:textStyle="bold" />
+
+            </RelativeLayout>
+
+
+        </androidx.cardview.widget.CardView>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/white"
+            android:paddingLeft="22dp"
+            android:visibility="gone"
+            android:paddingTop="10dp"
+            android:paddingRight="22dp"
+            android:paddingBottom="10dp">
+
+            <io.openim.android.ouicore.widget.SearchView
+                android:id="@+id/searchView"
+                android:layout_width="match_parent"
+                android:layout_height="34dp"
+                android:clickable="false"
+                android:hint="@string/search_friends"
+                android:visibility="gone" />
+
+        </RelativeLayout>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1">
+
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/recycler_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/white"
+                android:overScrollMode="never" />
+
+
+            <com.mao.sortletterlib.SortLetterView
+                android:id="@+id/sort_view"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_centerVertical="true"
+                app:iconHeight="49dp"
+                app:iconWidth="58dp"
+                app:leftBigText="26sp"
+                app:letterColor="@color/color_181818"
+                app:letterSize="12sp"
+                app:paddingRight="10dp"
+                app:selectBackgroundColor="@color/color_02C25F"
+                app:selectLetterColor="@color/white"
+                app:selectbigtTextColor="@color/color_02C25F" />
+        </RelativeLayout>
+
+        <include
+            android:id="@+id/bottom"
+            layout="@layout/layout_selected_friends"
+            tools:visibility="visible"
+            android:visibility="gone" />
+    </LinearLayout>
+</layout>
+

+ 121 - 0
OUIContact/src/main/res/layout/activity_capture.xml

@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="com.yzq.zxinglibrary.android.CaptureActivity">
+    <!-- 整体透明画布 -->
+
+
+    <SurfaceView
+        android:id="@+id/preview_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+
+    <androidx.appcompat.widget.LinearLayoutCompat
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentTop="true"
+        android:orientation="vertical">
+
+        <RelativeLayout
+            android:id="@+id/headerLayout"
+            android:layout_width="match_parent"
+            android:layout_height="50dp"
+            android:layout_gravity="top"
+            android:background="#99000000">
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:id="@+id/backIv"
+                android:layout_width="40dp"
+                android:layout_height="match_parent"
+                android:padding="10dp"
+                app:srcCompat="@drawable/ic_back" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_centerInParent="true"
+                android:text="@string/scan_code"
+                android:textColor="#ffffff"
+                android:textSize="18sp" />
+
+
+        </RelativeLayout>
+
+
+        <!-- 扫描取景框 -->
+        <com.yzq.zxinglibrary.view.ViewfinderView
+            android:id="@+id/viewfinder_view"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1" />
+
+
+        <androidx.appcompat.widget.LinearLayoutCompat
+            android:id="@+id/bottomLayout"
+            android:layout_width="match_parent"
+            android:layout_height="96dp"
+            android:layout_gravity="bottom"
+            android:background="#99000000"
+            android:orientation="horizontal">
+
+            <androidx.appcompat.widget.LinearLayoutCompat
+                android:id="@+id/flashLightLayout"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:gravity="center"
+                android:orientation="vertical">
+
+                <androidx.appcompat.widget.AppCompatImageView
+                    android:id="@+id/flashLightIv"
+                    android:layout_width="34dp"
+                    android:layout_height="34dp"
+                    app:srcCompat="@drawable/ic_close" />
+
+                <TextView
+                    android:id="@+id/flashLightTv"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="5dp"
+                    android:gravity="center"
+                    android:textSize="14sp"
+                    android:text="@string/open_flash"
+                    android:textColor="#ffffff" />
+
+            </androidx.appcompat.widget.LinearLayoutCompat>
+
+            <androidx.appcompat.widget.LinearLayoutCompat
+                android:id="@+id/albumLayout"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:gravity="center"
+                android:orientation="vertical">
+
+                <androidx.appcompat.widget.AppCompatImageView
+                    android:id="@+id/albumIv"
+                    android:layout_width="34dp"
+                    android:layout_height="34dp"
+                    app:srcCompat="@drawable/ic_photo_2" />
+
+                <TextView
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="5dp"
+                    android:gravity="center"
+                    android:text="@string/gallery"
+                    android:textSize="14sp"
+                    android:textColor="#ffffff" />
+            </androidx.appcompat.widget.LinearLayoutCompat>
+
+
+        </androidx.appcompat.widget.LinearLayoutCompat>
+
+    </androidx.appcompat.widget.LinearLayoutCompat>
+</RelativeLayout>

+ 133 - 0
OUIContact/src/main/res/layout/activity_create_label.xml

@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        tools:context=".ui.CreateLabelActivity">
+
+        <androidx.cardview.widget.CardView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:cardElevation="0dp">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/white">
+
+                <RelativeLayout
+                    android:id="@+id/title"
+                    android:layout_width="match_parent"
+                    android:layout_height="@dimen/comm_title_high"
+                    android:orientation="horizontal">
+
+                    <include layout="@layout/view_back" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:text="@string/cretate_label"
+                        android:textColor="#ff333333"
+                        android:textSize="18sp" />
+
+                    <TextView
+                        android:id="@+id/submit"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_alignParentRight="true"
+                        android:layout_centerInParent="true"
+                        android:padding="15dp"
+                        android:text="@string/finish"
+                        android:textColor="#ff333333"
+                        android:textSize="14sp" />
+                </RelativeLayout>
+            </RelativeLayout>
+        </androidx.cardview.widget.CardView>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@color/def_bg"
+            android:orientation="vertical"
+            android:padding="11dp">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@drawable/sty_radius_6_white"
+                android:orientation="horizontal">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:paddingLeft="16dp"
+                    android:paddingTop="18dp"
+                    android:paddingBottom="18dp"
+                    android:text="@string/label_name"
+                    android:textColor="#ff333333"
+                    android:textSize="14sp" />
+
+                <EditText
+                    android:id="@+id/name"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="60dp"
+                    android:background="@null"
+                    android:ellipsize="end"
+                    android:hint="@string/create_label_tips"
+                    android:maxLines="2"
+                    android:textColor="#ffadadad"
+                    android:textSize="14sp" />
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:background="@drawable/sty_radius_6_white"
+                android:orientation="vertical"
+                >
+
+                <TextView
+                    android:paddingLeft="16dp"
+                    android:paddingTop="18dp"
+                    android:paddingBottom="18dp"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/label_member"
+                    android:textColor="#ff333333"
+                    android:textSize="14sp" />
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/recyclerView"
+                    android:paddingLeft="8dp"
+                    android:paddingRight="8dp"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content" />
+
+                <RelativeLayout
+                    android:paddingRight="16dp"
+                    android:paddingBottom="18dp"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:gravity="right">
+
+                    <ImageView
+                        android:id="@+id/addMember"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:src="@mipmap/ic_add_big3" />
+                </RelativeLayout>
+
+            </LinearLayout>
+
+
+        </LinearLayout>
+    </LinearLayout>
+</layout>
+

+ 15 - 0
OUIContact/src/main/res/layout/activity_debug.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".DebugActivity">
+
+    <FrameLayout
+        android:id="@+id/fragment_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        />
+
+</LinearLayout>

+ 123 - 0
OUIContact/src/main/res/layout/activity_forward_to.xml

@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout>
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        tools:context=".ui.ForwardToActivity">
+
+        <androidx.cardview.widget.CardView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:cardElevation="0dp">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/comm_title_high">
+
+                <include layout="@layout/view_back" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerInParent="true"
+                    android:text="@string/forward"
+                    android:textColor="#ff333333"
+                    android:textSize="18sp" />
+            </RelativeLayout>
+        </androidx.cardview.widget.CardView>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/white"
+            android:paddingLeft="22dp"
+            android:paddingTop="10dp"
+            android:paddingRight="22dp"
+            android:paddingBottom="10dp">
+
+            <io.openim.android.ouicore.widget.SearchView
+                android:id="@+id/searchView"
+                android:layout_width="match_parent"
+                android:layout_height="34dp"
+                android:clickable="false"
+                android:hint="@string/search_by_id" />
+
+        </RelativeLayout>
+
+        <RadioGroup
+            android:id="@+id/menuGroup"
+            android:layout_width="match_parent"
+            android:layout_height="57dp"
+            android:background="@color/white"
+            android:orientation="horizontal"
+            android:paddingTop="5dp">
+
+            <RadioButton
+                android:id="@+id/men1"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:button="@null"
+                android:gravity="center"
+                android:text="@string/Select_friends"
+                android:textColor="#ff333333"
+                android:textSize="16sp" />
+
+            <RadioButton
+                android:id="@+id/men2"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:button="@null"
+                android:gravity="center"
+                android:text="@string/Select_group"
+                android:textColor="#ff333333"
+                android:textSize="16sp" />
+        </RadioGroup>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="3dp"
+            android:background="@color/white"
+            android:orientation="horizontal">
+
+            <RelativeLayout
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1">
+
+                <View
+                    android:id="@+id/men_bg1"
+                    android:layout_width="34dp"
+                    android:layout_height="3dp"
+                    android:layout_alignParentBottom="true"
+                    android:layout_centerHorizontal="true"
+                    android:background="#1D6BED" />
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1">
+
+                <View
+                    android:id="@+id/men_bg2"
+                    android:layout_width="34dp"
+                    android:layout_height="3dp"
+                    android:layout_alignParentBottom="true"
+                    android:layout_centerHorizontal="true"
+                    android:background="#1D6BED"
+                    android:visibility="gone" />
+            </RelativeLayout>
+        </LinearLayout>
+
+        <FrameLayout
+            android:id="@+id/fragment_container"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+    </LinearLayout>
+</layout>

+ 162 - 0
OUIContact/src/main/res/layout/activity_friend_request_detail.xml

@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout>
+
+    <data>
+
+        <variable
+            name="ContactVM"
+            type="io.openim.android.ouicontact.vm.ContactVM" />
+    </data>
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/white"
+        android:orientation="vertical"
+        tools:context=".ui.FriendRequestDetailActivity">
+
+        <androidx.cardview.widget.CardView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:cardElevation="0dp">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/comm_title_high"
+                android:gravity="center_vertical">
+
+                <include layout="@layout/view_back" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerInParent="true"
+                    android:text="@string/new_friend_request"
+                    android:textColor="#ff333333"
+                    android:textSize="18sp"
+                    android:visibility="gone" />
+            </RelativeLayout>
+        </androidx.cardview.widget.CardView>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="37dp"
+            android:orientation="vertical"
+            android:paddingLeft="22dp"
+            android:paddingRight="22dp">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center_vertical"
+                android:orientation="horizontal">
+
+                <io.openim.android.ouicore.widget.AvatarImage
+                    android:id="@+id/avatar"
+                    android:layout_width="60dp"
+                    android:layout_height="60dp"
+                    tools:background="@color/color_02C25F" />
+
+                <LinearLayout
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_gravity="center_vertical"
+                    android:layout_weight="1"
+                    android:orientation="vertical"
+                    android:paddingStart="21dp">
+
+                    <TextView
+                        android:id="@+id/nickName"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_marginBottom="5dp"
+                        android:textColor="@color/color_181818"
+                        android:textSize="21sp"
+                        android:textStyle="bold"
+                        tools:text="名字" />
+
+                    <TextView
+                        android:id="@+id/useId"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:textColor="#737373"
+                        android:textSize="14sp"
+                        tools:text="账号:15451" />
+                </LinearLayout>
+
+
+                <ImageView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:src="@mipmap/ic_right"
+                    android:visibility="gone" />
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="33dp"
+                android:background="@drawable/sty_radius_6_stroke_dedede"
+                android:minHeight="124dp"
+                android:orientation="vertical"
+                android:padding="15dp">
+
+                <TextView
+                    android:id="@+id/hil"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:minHeight="124dp"
+                    android:textColor="#6F6F6F"
+                    android:textSize="14sp"
+                    tools:text="我是你的韩梅梅~" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="5dp"
+                    android:text="@string/reply"
+                    android:textColor="#ff1b6bed"
+                    android:textSize="16sp"
+                    android:visibility="gone" />
+            </LinearLayout>
+
+        </LinearLayout>
+
+        <TextView
+            android:id="@+id/accept"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="29dp"
+            android:gravity="center"
+            android:onClick="@{(v)->ContactVM.friendPass()}"
+            android:paddingVertical="15dp"
+            android:text="@string/apply_through_friends"
+            android:textColor="#576B95"
+            android:textSize="16sp" />
+
+        <View style="@style/divider_horizontal" />
+
+        <TextView
+            android:id="@+id/reject"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:onClick="@{(v)->ContactVM.friendRefuse()}"
+            android:paddingVertical="15dp"
+            android:text="@string/reject_friend_application"
+            android:textColor="#B6B6B6"
+            android:textSize="16sp" />
+
+        <View style="@style/divider_horizontal" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:background="@color/theme_bg2" />
+    </LinearLayout>
+</layout>
+

+ 172 - 0
OUIContact/src/main/res/layout/activity_group_notice_detail.xml

@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout>
+
+    <data>
+
+        <variable
+            name="ContactVM"
+            type="io.openim.android.ouicontact.vm.ContactVM" />
+    </data>
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/white"
+        android:orientation="vertical"
+        tools:context=".ui.GroupNoticeDetailActivity"
+        tools:ignore="MissingDefaultResource">
+
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/comm_title_high"
+            android:background="@color/white"
+            android:orientation="horizontal">
+
+            <include
+                android:id="@+id/back"
+                layout="@layout/view_back" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_centerInParent="true"
+                android:textColor="#ff333333"
+                android:textSize="18sp" />
+
+        </RelativeLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="37dp"
+            android:orientation="vertical"
+            android:paddingLeft="22dp"
+            android:paddingRight="22dp">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center_vertical"
+                android:orientation="horizontal">
+
+                <io.openim.android.ouicore.widget.AvatarImage
+                    android:id="@+id/avatar"
+                    android:layout_width="60dp"
+                    android:layout_height="60dp"
+                    tools:background="@color/color_02C25F" />
+
+                <LinearLayout
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_gravity="center_vertical"
+                    android:layout_weight="1"
+                    android:orientation="vertical"
+                    android:paddingStart="21dp">
+
+                    <TextView
+                        android:id="@+id/nickName"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_marginBottom="5dp"
+                        android:text="@{ContactVM.groupDetail.nickname}"
+                        android:textColor="@color/color_181818"
+                        android:textSize="21sp"
+                        android:textStyle="bold"
+                        tools:text="名字" />
+
+                    <TextView
+                        android:id="@+id/useId"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:textColor="#737373"
+                        android:textSize="14sp"
+                        tools:text="账号:15451" />
+                </LinearLayout>
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="33dp"
+                android:background="@drawable/sty_radius_6_stroke_dedede"
+                android:minHeight="124dp"
+                android:orientation="vertical"
+                android:padding="15dp">
+
+                <LinearLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="@string/apply_join"
+                        android:textColor="#6F6F6F"
+                        android:textSize="16sp" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="@{' '+ContactVM.groupDetail.groupName}"
+                        android:textColor="#576B95"
+                        android:textSize="16sp"
+                        tools:text="123213213" />
+                </LinearLayout>
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="14dp"
+                    android:text="@string/reason"
+                    android:textColor="#6F6F6F"
+                    android:textSize="16sp" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="4dp"
+                    android:text="@{ContactVM.groupDetail.reqMsg}"
+                    android:textColor="#6F6F6F"
+                    android:textSize="16sp"
+                    tools:text="123213213" />
+            </LinearLayout>
+
+
+        </LinearLayout>
+
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="55dp"
+            android:layout_marginTop="29dp"
+            android:gravity="center"
+            android:onClick="@{(v)->ContactVM.pass()}"
+            android:text="@string/pass"
+            android:textColor="#576B95"
+            android:textSize="18sp" />
+
+        <View style="@style/divider_horizontal" />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="55dp"
+            android:gravity="center"
+            android:onClick="@{(v)->ContactVM.refuse()}"
+            android:text="@string/Fail"
+            android:textColor="#B6B6B6"
+            android:textSize="16sp" />
+
+        <View style="@style/divider_horizontal" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:background="@color/theme_bg2" />
+    </LinearLayout>
+</layout>
+

+ 132 - 0
OUIContact/src/main/res/layout/activity_group_notice_invite_detail.xml

@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout>
+
+    <data>
+
+        <variable
+            name="ContactVM"
+            type="io.openim.android.ouicontact.vm.ContactVM" />
+    </data>
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/white"
+        android:orientation="vertical"
+        tools:context=".ui.GroupNoticeDetailActivity"
+        tools:ignore="MissingDefaultResource">
+
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/comm_title_high"
+            android:background="@color/white"
+            android:orientation="horizontal">
+
+            <include
+                android:id="@+id/back"
+                layout="@layout/view_back" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_centerInParent="true"
+                android:text="@string/new_group_chat"
+                android:textColor="@color/color_181818"
+                android:textSize="16sp"
+                android:textStyle="bold" />
+
+        </RelativeLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="50dp"
+            android:layout_marginBottom="30dp"
+            android:gravity="center_horizontal"
+            android:orientation="vertical">
+
+            <io.openim.android.ouicore.widget.AvatarImage
+                android:id="@+id/avatar"
+                android:layout_width="68dp"
+                android:layout_height="68dp"
+                tools:background="@color/color_02C25F" />
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:gravity="center_horizontal"
+                android:orientation="vertical">
+
+                <TextView
+                    android:id="@+id/groupName"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="5dp"
+                    android:text="@{ContactVM.groupDetail.groupName}"
+                    android:textColor="@color/color_181818"
+                    android:textSize="18sp"
+                    android:textStyle="bold"
+                    tools:text="山南附近生活1群" />
+
+                <TextView
+                    android:id="@+id/peopleCount"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textColor="#B6B6B6"
+                    android:textSize="16sp"
+                    tools:text="198人" />
+            </LinearLayout>
+
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:background="@color/theme_bg2"
+            android:orientation="vertical"
+            android:paddingTop="30dp">
+
+
+            <TextView
+                android:id="@+id/info"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/theme_bg2"
+                android:gravity="center"
+                android:textColor="#282828"
+                android:textSize="19sp"
+                tools:text="小梨涡很甜邀请你加入群聊" />
+
+
+            <TextView
+                android:layout_width="200dp"
+                android:layout_height="55dp"
+                android:layout_gravity="center_horizontal"
+                android:layout_marginTop="26dp"
+                android:background="@drawable/sty_radius_4_02c25f"
+                android:gravity="center"
+                android:onClick="@{(v)->ContactVM.pass()}"
+                android:text="@string/pass"
+                android:textColor="@color/white"
+                android:textSize="17sp" />
+
+
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="55dp"
+                android:gravity="center"
+                android:onClick="@{(v)->ContactVM.refuse()}"
+                android:text="@string/Fail"
+                android:textColor="#B6B6B6"
+                android:textSize="16sp" />
+
+
+        </LinearLayout>
+    </LinearLayout>
+</layout>
+

+ 55 - 0
OUIContact/src/main/res/layout/activity_group_notice_list.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout>
+    <FrameLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="#F8F8F8"
+            android:orientation="vertical"
+            tools:context=".ui.GroupNoticeListActivity">
+
+            <androidx.cardview.widget.CardView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:cardElevation="0dp">
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/white">
+
+                <RelativeLayout
+                    android:id="@+id/title"
+                    android:layout_width="match_parent"
+                    android:layout_height="@dimen/comm_title_high"
+                    android:orientation="horizontal">
+
+                    <include
+                        layout="@layout/view_back" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:text="@string/new_group_chat"
+                        android:textColor="#ff333333"
+                        android:textSize="18sp" />
+
+                </RelativeLayout>
+            </RelativeLayout>
+            </androidx.cardview.widget.CardView>
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/recyclerView"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+
+        </LinearLayout>
+    </FrameLayout>
+
+</layout>

+ 87 - 0
OUIContact/src/main/res/layout/activity_label.xml

@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/white">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            tools:context=".ui.LabelActivity">
+
+            <androidx.cardview.widget.CardView
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/comm_title_high"
+                app:cardElevation="0dp">
+
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:orientation="horizontal">
+
+                    <include
+                        android:id="@+id/back"
+                        layout="@layout/view_back" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:text="@string/label"
+                        android:textColor="#ff333333"
+                        android:textSize="18sp" />
+
+                </RelativeLayout>
+
+            </androidx.cardview.widget.CardView>
+
+
+
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/def_bg"
+                android:orientation="vertical">
+                <FrameLayout
+                    android:layout_width="match_parent"
+                    android:paddingTop="10dp"
+                    android:paddingBottom="10dp"
+                    android:visibility="gone"
+                    android:layout_marginBottom="10dp"
+                    android:layout_height="wrap_content"
+                    android:background="@color/white">
+
+                    <io.openim.android.ouicore.widget.SearchView
+                        android:id="@+id/input"
+                        android:layout_width="match_parent"
+                        android:layout_height="40dp"
+                        android:layout_marginLeft="10dp"
+                        android:layout_marginRight="10dp"
+                        android:background="@color/white"
+                        android:clickable="false" />
+                </FrameLayout>
+
+                <com.yanzhenjie.recyclerview.SwipeRecyclerView
+                    android:id="@+id/recyclerView"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:background="@color/white" />
+            </LinearLayout>
+        </LinearLayout>
+
+        <ImageView
+            android:id="@+id/add"
+            android:layout_width="100dp"
+            android:layout_height="100dp"
+            android:layout_alignParentRight="true"
+            android:layout_alignParentBottom="true"
+            android:background="@mipmap/ic_add_big2" />
+    </RelativeLayout>
+
+</layout>

+ 188 - 0
OUIContact/src/main/res/layout/activity_my_group.xml

@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="MissingDefaultResource">
+
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        tools:context=".ui.MyGroupActivity">
+
+        <androidx.cardview.widget.CardView
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/comm_title_high"
+            app:cardElevation="0dp">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent">
+
+                <include
+                    android:id="@+id/back"
+                    layout="@layout/view_back" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerInParent="true"
+                    android:text="@string/my_group"
+                    android:textColor="@color/color_181818"
+                    android:textSize="16sp"
+                    android:textStyle="bold" />
+
+            </RelativeLayout>
+
+
+        </androidx.cardview.widget.CardView>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/white"
+            android:paddingHorizontal="15dp"
+            android:paddingVertical="10dp">
+
+            <io.openim.android.ouicore.widget.SearchView
+                android:id="@+id/searchView"
+                android:layout_width="match_parent"
+                android:layout_height="34dp"
+                android:clickable="false"
+                android:hint="@string/search_group" />
+
+        </RelativeLayout>
+
+        <RadioGroup
+            android:id="@+id/menuGroup"
+            android:layout_width="match_parent"
+            android:layout_height="47dp"
+            android:background="@color/white"
+            android:orientation="horizontal">
+
+            <RadioButton
+                android:id="@+id/men1"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:button="@null"
+                android:gravity="center"
+                android:text="@string/i_created"
+                android:textColor="#282828"
+                android:textSize="16sp"
+                android:textStyle="bold" />
+
+            <RadioButton
+                android:id="@+id/men2"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:button="@null"
+                android:gravity="center"
+                android:text="@string/i_joined"
+                android:textColor="#656565"
+                android:textSize="16sp" />
+        </RadioGroup>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="3.5dp"
+            android:background="@color/white"
+            android:orientation="horizontal">
+
+            <RelativeLayout
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1">
+
+                <View
+                    android:id="@+id/men_bg1"
+                    android:layout_width="22dp"
+                    android:layout_height="match_parent"
+                    android:layout_alignParentBottom="true"
+                    android:layout_centerHorizontal="true"
+                    android:background="@drawable/sty_radius_4_02c25f" />
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1">
+
+                <View
+                    android:id="@+id/men_bg2"
+                    android:layout_width="22dp"
+                    android:layout_height="match_parent"
+                    android:layout_alignParentBottom="true"
+                    android:layout_centerHorizontal="true"
+                    android:background="@drawable/sty_radius_4_02c25f"
+                    android:visibility="gone"
+                    tools:visibility="visible" />
+            </RelativeLayout>
+        </LinearLayout>
+
+        <View
+            style="@style/divider_horizontal"
+            android:background="#EAEAEA" />
+
+        <FrameLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:background="@color/theme_bg2">
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/create"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/joined"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:visibility="gone" />
+
+            <RelativeLayout
+                android:id="@+id/empty"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@color/theme_bg2"
+                android:visibility="visible"
+                tools:visibility="visible">
+
+                <CheckBox
+                    android:id="@+id/cb"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerHorizontal="true"
+                    android:layout_marginTop="90dp"
+                    android:button="@null"
+                    android:drawableTop="@mipmap/ic_group_empty"
+                    android:drawablePadding="15dp"
+                    android:gravity="center"
+                    android:text="@string/no_create_group"
+                    android:textColor="#989FB0"
+                    android:textSize="16sp" />
+
+                <TextView
+                    android:id="@+id/toCreate"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_below="@+id/cb"
+                    android:layout_alignStart="@+id/cb"
+                    android:layout_marginStart="38dp"
+                    android:layout_marginTop="15dp"
+                    android:background="@drawable/sty_radius_15_tr_stroke_b6b6b6"
+                    android:paddingHorizontal="17dp"
+                    android:paddingVertical="5dp"
+                    android:text="@string/to_create"
+                    android:textColor="#989FB0"
+                    android:textSize="14sp" />
+            </RelativeLayout>
+        </FrameLayout>
+
+
+    </LinearLayout>
+</layout>
+

+ 67 - 0
OUIContact/src/main/res/layout/activity_new_friend.xml

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:tools="http://schemas.android.com/tools"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/white"
+        android:orientation="vertical"
+        tools:context=".ui.NewFriendActivity">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="#F8F8F8"
+            android:orientation="vertical">
+
+            <androidx.cardview.widget.CardView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:cardElevation="0dp">
+
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="@dimen/comm_title_high"
+                    android:gravity="center_vertical">
+
+                    <include layout="@layout/view_back" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:text="@string/new_friend"
+                        android:textStyle="bold"
+                        android:textColor="@color/color_181818"
+                        android:textSize="16sp" />
+                </RelativeLayout>
+            </androidx.cardview.widget.CardView>
+
+
+            <TextView
+                android:visibility="gone"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="22dp"
+                android:paddingTop="10dp"
+                android:paddingBottom="10dp"
+                android:text="@string/new_friend_request"
+                android:textColor="#ff999999"
+                android:textSize="12sp" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/recyclerView"
+                android:layout_width="match_parent"
+                android:paddingTop="10dp"
+                android:layout_height="wrap_content"
+                android:background="@color/white"
+                android:overScrollMode="never"
+                />
+        </LinearLayout>
+
+
+    </LinearLayout>
+</layout>
+

+ 50 - 0
OUIContact/src/main/res/layout/activity_often_serch.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fitsSystemWindows="true"
+        android:orientation="vertical"
+        tools:context=".ui.search.SearchGroupAndFriendsActivity">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/white"
+            android:gravity="center_vertical"
+            android:orientation="horizontal">
+
+            <io.openim.android.ouicore.widget.SearchView
+                android:id="@+id/searchView"
+                android:layout_width="0dp"
+                android:layout_height="41dp"
+                android:layout_marginLeft="22dp"
+                android:layout_marginTop="7dp"
+                android:layout_marginRight="17dp"
+                android:layout_marginBottom="7dp"
+                android:layout_weight="1"
+                android:hint="@string/search" />
+
+            <TextView
+                android:id="@+id/cancel"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="17dp"
+                android:paddingTop="8dp"
+                android:paddingRight="17dp"
+                android:paddingBottom="8dp"
+                android:text="@string/cancel"
+                android:textColor="#ff333333"
+                android:textSize="18sp" />
+        </LinearLayout>
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/recyclerview"
+            android:background="@color/theme_bg2"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+    </LinearLayout>
+</layout>

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно