├── .gitignore ├── .idea ├── compiler.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── runConfigurations │ ├── Server.xml │ ├── Start_all.xml │ ├── app1.xml │ └── app2.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── nju │ │ └── androidchat │ │ └── client │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── config.properties │ ├── java │ │ └── nju │ │ │ └── androidchat │ │ │ └── client │ │ │ ├── ClientMessage.java │ │ │ ├── MainActivity.java │ │ │ ├── Utils.java │ │ │ ├── component │ │ │ ├── ItemTextReceive.java │ │ │ ├── ItemTextSend.java │ │ │ └── OnRecallMessageRequested.java │ │ │ ├── frp0 │ │ │ └── Frp0TalkActivity.java │ │ │ ├── frp3 │ │ │ └── Frp3TalkActivity.java │ │ │ ├── frp4 │ │ │ └── Frp4TalkActivity.java │ │ │ ├── mvc0 │ │ │ ├── Mvc0TalkActivity.java │ │ │ ├── Mvc0TalkController.java │ │ │ └── Mvc0TalkModel.java │ │ │ ├── mvc1 │ │ │ ├── Mvc1BackupActivity.java │ │ │ ├── Mvc1BackupController.java │ │ │ ├── Mvc1BackupModel.java │ │ │ ├── Mvc1TalkActivity.java │ │ │ ├── Mvc1TalkController.java │ │ │ └── Mvc1TalkModel.java │ │ │ ├── mvp0 │ │ │ ├── BasePresenter.java │ │ │ ├── BaseView.java │ │ │ ├── Mvp0Contract.java │ │ │ ├── Mvp0TalkActivity.java │ │ │ ├── Mvp0TalkModel.java │ │ │ └── Mvp0TalkPresenter.java │ │ │ ├── mvp1 │ │ │ ├── BasePresenter.java │ │ │ ├── BaseView.java │ │ │ ├── Mvp1BackupActivity.java │ │ │ ├── Mvp1BackupModel.java │ │ │ ├── Mvp1BackupPresenter.java │ │ │ ├── Mvp1Contract.java │ │ │ ├── Mvp1TalkActivity.java │ │ │ ├── Mvp1TalkModel.java │ │ │ └── Mvp1TalkPresenter.java │ │ │ ├── mvp2 │ │ │ ├── BasePresenter.java │ │ │ ├── BaseView.java │ │ │ ├── Mvp2Contract.java │ │ │ ├── Mvp2TalkActivity.java │ │ │ ├── Mvp2TalkModel.java │ │ │ └── Mvp2TalkPresenter.java │ │ │ ├── mvvm0 │ │ │ ├── Mvvm0TalkActivity.java │ │ │ ├── model │ │ │ │ ├── ClientMessageObservable.java │ │ │ │ ├── Direction.java │ │ │ │ └── State.java │ │ │ └── viewmodel │ │ │ │ ├── ItemTextAdapters.java │ │ │ │ ├── ListBindingAdapters.java │ │ │ │ ├── Mvvm0ViewModel.java │ │ │ │ └── UiOperator.java │ │ │ ├── mvvm2 │ │ │ ├── Mvvm2TalkActivity.java │ │ │ ├── model │ │ │ │ ├── ClientMessageObservable.java │ │ │ │ ├── Direction.java │ │ │ │ └── State.java │ │ │ └── viewmodel │ │ │ │ ├── ItemTextAdapters.java │ │ │ │ ├── ListBindingAdapters.java │ │ │ │ ├── LongClickListener.java │ │ │ │ ├── Mvvm2ViewModel.java │ │ │ │ ├── RecallHandler.java │ │ │ │ └── UiOperator.java │ │ │ ├── mvvm3 │ │ │ ├── Mvvm3TalkActivity.java │ │ │ ├── model │ │ │ │ ├── ClientMessageObservable.java │ │ │ │ ├── Direction.java │ │ │ │ └── State.java │ │ │ └── viewmodel │ │ │ │ ├── CustomizedAdapters.java │ │ │ │ ├── ListBindingAdapters.java │ │ │ │ ├── Mvvm3ViewModel.java │ │ │ │ └── UiOperator.java │ │ │ └── socket │ │ │ ├── MessageListener.java │ │ │ └── SocketClient.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ ├── message_text_receive.9.png │ │ └── message_text_send.9.png │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── ic_menu_camera.xml │ │ ├── ic_menu_gallery.xml │ │ ├── ic_menu_manage.xml │ │ ├── ic_menu_send.xml │ │ ├── ic_menu_share.xml │ │ ├── ic_menu_slideshow.xml │ │ ├── message_shap_chat_bg.xml │ │ └── side_nav_bar.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_main_mvvm.xml │ │ ├── activity_main_mvvm2.xml │ │ ├── activity_main_mvvm3.xml │ │ ├── activity_main_with_backup_btn.xml │ │ ├── backup.xml │ │ ├── header_main.xml │ │ ├── item_text_mvvm.xml │ │ ├── item_text_mvvm2.xml │ │ ├── item_text_mvvm3.xml │ │ ├── item_text_receive.xml │ │ ├── item_text_send.xml │ │ └── login_main.xml │ │ ├── menu │ │ ├── activity_main_drawer.xml │ │ └── main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_head_default_left.png │ │ ├── ic_head_default_right.jpg │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-v21 │ │ └── styles.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── nju │ └── androidchat │ └── client │ └── ExampleUnitTest.java ├── build.gradle ├── docs ├── img │ ├── client-chat-main.png │ ├── client-input-username.png │ ├── differences.png │ ├── extfunc1.gif │ ├── extfunc2.gif │ ├── extfunc3.gif │ ├── extfunc4.gif │ ├── frp-badwords.png │ ├── frp-message.png │ ├── frp-throttle.png │ ├── frp1.png │ ├── mvc-backup-dataflow.png │ ├── mvc-send-dataflow.png │ ├── mvc1.png │ ├── mvp-backup-dataflow.png │ ├── mvp-return-dataflow.png │ ├── mvp1.png │ ├── mvvm-return-dataflow.png │ ├── mvvm1.png │ └── server-launched.png ├── 合作规范.md ├── 客户端和服务器端通信.md ├── 扩展需求开发文档.md └── 报告.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── server ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── nju │ └── androidchat │ └── server │ ├── ChatServer.java │ ├── ConnectionHandler.java │ ├── ConnectionHandlerImpl.java │ └── handlers │ ├── ClientSendMessageHandler.java │ ├── DisconnectMessageHandler.java │ ├── MessageHandler.java │ └── RecallRequestMessageHandler.java ├── settings.gradle ├── shared ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── nju │ └── androidchat │ └── shared │ ├── Shared.java │ └── message │ ├── ClientSendMessage.java │ ├── DisconnectMessage.java │ ├── ErrorMessage.java │ ├── LoginRequestMessage.java │ ├── LoginResponseMessage.java │ ├── Message.java │ ├── RecallMessage.java │ ├── RecallRequestMessage.java │ └── ServerSendMessage.java ├── 作业信息.txt └── 作业提交说明.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | ### Android template 15 | # Built application files 16 | *.apk 17 | *.ap_ 18 | 19 | # Files for the ART/Dalvik VM 20 | *.dex 21 | 22 | # Java class files 23 | *.class 24 | 25 | # Generated files 26 | bin/ 27 | gen/ 28 | out/ 29 | 30 | # Gradle files 31 | .gradle/ 32 | build/ 33 | 34 | # Local configuration file (sdk path, etc) 35 | local.properties 36 | 37 | # Proguard folder generated by Eclipse 38 | proguard/ 39 | 40 | # Log Files 41 | *.log 42 | 43 | # Android Studio Navigation editor temp files 44 | .navigation/ 45 | 46 | # Android Studio captures folder 47 | captures/ 48 | 49 | # IntelliJ 50 | .idea/workspace.xml 51 | .idea/tasks.xml 52 | .idea/gradle.xml 53 | .idea/assetWizardSettings.xml 54 | .idea/dictionaries 55 | .idea/libraries 56 | .idea/caches 57 | 58 | # Keystore files 59 | # Uncomment the following line if you do not want to check your keystore files in. 60 | #*.jks 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Ignore Gradle GUI config 78 | gradle-app.setting 79 | 80 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 81 | !gradle-wrapper.jar 82 | 83 | # Cache of project 84 | .gradletasknamecache 85 | 86 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 87 | # gradle/wrapper/gradle-wrapper.properties 88 | ### JetBrains template 89 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 90 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 91 | 92 | # User-specific stuff 93 | .idea/**/workspace.xml 94 | .idea/**/tasks.xml 95 | .idea/**/dictionaries 96 | .idea/**/shelf 97 | 98 | # Sensitive or high-churn files 99 | .idea/**/dataSources/ 100 | .idea/**/dataSources.ids 101 | .idea/**/dataSources.local.xml 102 | .idea/**/sqlDataSources.xml 103 | .idea/**/dynamic.xml 104 | .idea/**/uiDesigner.xml 105 | .idea/**/dbnavigator.xml 106 | 107 | # Gradle 108 | .idea/**/gradle.xml 109 | .idea/**/libraries 110 | 111 | # CMake 112 | cmake-build-debug/ 113 | cmake-build-release/ 114 | 115 | # Mongo Explorer plugin 116 | .idea/**/mongoSettings.xml 117 | 118 | # File-based project format 119 | *.iws 120 | # mpeltonen/sbt-idea plugin 121 | .idea_modules/ 122 | 123 | # JIRA plugin 124 | atlassian-ide-plugin.xml 125 | 126 | # Cursive Clojure plugin 127 | .idea/replstate.xml 128 | 129 | # Crashlytics plugin (for Android Studio and IntelliJ) 130 | com_crashlytics_export_strings.xml 131 | crashlytics.properties 132 | crashlytics-build.properties 133 | fabric.properties 134 | 135 | # Editor-based Rest Client 136 | .idea/httpRequests 137 | 138 | .idea/runConfigurations.xml 139 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Server.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Start_all.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/app1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 55 | -------------------------------------------------------------------------------- /.idea/runConfigurations/app2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 55 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "nju.androidchat" 7 | minSdkVersion 26 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | compileOptions { 20 | sourceCompatibility = '1.8' 21 | targetCompatibility = '1.8' 22 | 23 | } 24 | buildToolsVersion = '28.0.3' 25 | dataBinding { 26 | enabled = true 27 | } 28 | } 29 | 30 | dependencies { 31 | compileOnly 'org.jetbrains:annotations:13.0' 32 | compileOnly 'org.projectlombok:lombok:1.18.8' 33 | annotationProcessor 'org.projectlombok:lombok:1.18.8' 34 | 35 | implementation project(":shared") 36 | implementation fileTree(dir: 'libs', include: ['*.jar']) 37 | implementation 'androidx.appcompat:appcompat:1.0.2' 38 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 39 | implementation 'com.google.android.material:material:1.0.0' 40 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 41 | 42 | implementation "io.reactivex.rxjava2:rxjava:2.2.8" 43 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' 44 | implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha2' 45 | 46 | testImplementation 'junit:junit:4.12' 47 | androidTestImplementation 'androidx.test:runner:1.1.1' 48 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 49 | implementation 'de.hdodenhof:circleimageview:2.0.0' 50 | } 51 | 52 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/nju/androidchat/client/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.InstrumentationRegistry; 6 | import androidx.test.runner.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("nju.androidchat.client", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/assets/config.properties: -------------------------------------------------------------------------------- 1 | chat_activity=nju.androidchat.client.frp4.Frp4TalkActivity -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/ClientMessage.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.UUID; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import nju.androidchat.shared.message.Message; 9 | import nju.androidchat.shared.message.ServerSendMessage; 10 | 11 | @AllArgsConstructor 12 | public class ClientMessage extends Message { 13 | @Getter 14 | private UUID messageId; 15 | 16 | @Getter 17 | private LocalDateTime time; 18 | 19 | @Getter 20 | private String senderUsername; 21 | 22 | @Getter 23 | private String message; 24 | 25 | public ClientMessage(ServerSendMessage message) { 26 | this.messageId = message.getMessageId(); 27 | this.time = message.getTime(); 28 | this.senderUsername = message.getSenderUsername(); 29 | this.message = message.getMessage(); 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/MainActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.AsyncTask; 5 | import android.os.Bundle; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | import android.os.Handler; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | import android.view.inputmethod.InputMethodManager; 13 | import android.widget.EditText; 14 | import android.widget.Toast; 15 | 16 | import lombok.extern.java.Log; 17 | import nju.androidchat.client.socket.SocketClient; 18 | import nju.androidchat.shared.Shared; 19 | 20 | import java.io.IOException; 21 | import java.util.Objects; 22 | 23 | @Log 24 | public class MainActivity extends AppCompatActivity { 25 | 26 | EditText inputIp; 27 | 28 | @SuppressLint("SetTextI18n") 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) { 31 | try { 32 | Utils.props.load(getResources().getAssets().open("config.properties")); 33 | String chatActivityClassName = Utils.props.getProperty(Utils.CHAT_ACTIVITY_KEY); 34 | if (chatActivityClassName != null) { 35 | Class classRead = Class.forName(chatActivityClassName); 36 | log.info("Current chatActivity is: " + chatActivityClassName); 37 | Utils.CHAT_ACTIVITY = classRead; 38 | } 39 | } catch (IOException e) { 40 | e.printStackTrace(); 41 | } catch (ClassNotFoundException e) { 42 | e.printStackTrace(); 43 | } 44 | 45 | super.onCreate(savedInstanceState); 46 | setContentView(R.layout.login_main); 47 | 48 | inputIp = findViewById(R.id.ip_input); 49 | 50 | 51 | inputIp.setText(SocketClient.SERVER_ADDRESS + ":" + Shared.SERVER_PORT); 52 | } 53 | 54 | public void onBtnConnectClicked(View view) { 55 | Handler handler = new Handler(); 56 | EditText editText2 = findViewById(R.id.username_input); 57 | 58 | String ip = inputIp.getText().toString(); 59 | String username = editText2.getText().toString(); 60 | 61 | if (!(ip.equals("") || username.equals(""))) { 62 | 63 | 64 | AsyncTask.execute(() -> { 65 | String result = SocketClient.connect(username, ip); 66 | if (result.equals("SUCCESS")) { 67 | handler.post(() -> { 68 | Toast.makeText(this, "登录成功!", Toast.LENGTH_SHORT).show(); 69 | 70 | Utils.jumpToChat(this); 71 | }); 72 | } else { 73 | handler.post(() -> { 74 | Toast.makeText(this, "连接失败!原因:" + result, Toast.LENGTH_SHORT).show(); 75 | }); 76 | } 77 | }); 78 | 79 | } else { 80 | Toast.makeText(this, "请输入信息!", Toast.LENGTH_SHORT).show(); 81 | } 82 | } 83 | 84 | @Override 85 | // 点击空白位置 隐藏软键盘 86 | public boolean onTouchEvent(MotionEvent event) { 87 | if (null != this.getCurrentFocus()) { 88 | return hideKeyboard(); 89 | } 90 | return super.onTouchEvent(event); 91 | } 92 | 93 | //隐藏软键盘 94 | private boolean hideKeyboard() { 95 | InputMethodManager mInputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 96 | return mInputMethodManager.hideSoftInputFromWindow(Objects.requireNonNull(this.getCurrentFocus()).getWindowToken(), 0); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/Utils.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client; 2 | 3 | import android.content.Intent; 4 | import android.view.KeyEvent; 5 | import android.view.inputmethod.EditorInfo; 6 | import android.view.inputmethod.InputMethodManager; 7 | import android.widget.ScrollView; 8 | 9 | import androidx.appcompat.app.AppCompatActivity; 10 | 11 | import java.util.Objects; 12 | import java.util.Properties; 13 | 14 | import lombok.experimental.UtilityClass; 15 | import lombok.extern.java.Log; 16 | import nju.androidchat.client.mvvm0.Mvvm0TalkActivity; 17 | 18 | import static android.content.Context.INPUT_METHOD_SERVICE; 19 | 20 | @Log 21 | @UtilityClass 22 | public class Utils { 23 | Properties props = new Properties(); 24 | String CHAT_ACTIVITY_KEY = "chat_activity"; 25 | Class CHAT_ACTIVITY = Mvvm0TalkActivity.class; 26 | 27 | public void jumpTo(AppCompatActivity activity, Class clazz) { 28 | Intent intent = new Intent(activity.getBaseContext(), clazz); 29 | activity.startActivity(intent); 30 | } 31 | 32 | public void jumpToHome(AppCompatActivity activity) { 33 | jumpTo(activity, MainActivity.class); 34 | } 35 | 36 | public void jumpToChat(AppCompatActivity activity) { 37 | jumpTo(activity, CHAT_ACTIVITY); 38 | } 39 | 40 | 41 | public boolean hideKeyboard(AppCompatActivity activity) { 42 | InputMethodManager mInputMethodManager = (InputMethodManager) activity.getSystemService(INPUT_METHOD_SERVICE); 43 | return mInputMethodManager.hideSoftInputFromWindow(Objects.requireNonNull(activity.getCurrentFocus()).getWindowToken(), 0); 44 | } 45 | 46 | public boolean send(int actionId, KeyEvent event) { 47 | return actionId == EditorInfo.IME_ACTION_SEND 48 | || actionId == EditorInfo.IME_ACTION_DONE 49 | || (event != null && KeyEvent.KEYCODE_ENTER == event.getKeyCode() && KeyEvent.ACTION_DOWN == event.getAction()); 50 | } 51 | 52 | public void scrollListToBottom(AppCompatActivity activity) { 53 | ScrollView scrollView = activity.findViewById(R.id.content_scroll_view); 54 | scrollView.post(() -> { 55 | scrollView.fullScroll(ScrollView.FOCUS_DOWN); 56 | }); 57 | } 58 | 59 | // 简单地判断一个内容里有没有脏话 60 | public boolean containsBadWords(String content) { 61 | return content.contains("fuck"); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/component/ItemTextReceive.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.component; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | import android.widget.LinearLayout; 9 | import android.widget.TextView; 10 | 11 | import androidx.annotation.StyleableRes; 12 | 13 | import java.util.UUID; 14 | 15 | import nju.androidchat.client.R; 16 | 17 | public class ItemTextReceive extends LinearLayout { 18 | 19 | 20 | @StyleableRes 21 | int index0 = 0; 22 | 23 | private TextView textView; 24 | private Context context; 25 | private UUID messageId; 26 | private OnRecallMessageRequested onRecallMessageRequested; 27 | 28 | 29 | public ItemTextReceive(Context context, String text, UUID messageId) { 30 | super(context); 31 | this.context = context; 32 | inflate(context, R.layout.item_text_receive, this); 33 | this.textView = findViewById(R.id.chat_item_content_text); 34 | this.messageId = messageId; 35 | setText(text); 36 | } 37 | 38 | public void init(Context context) { 39 | 40 | } 41 | 42 | public String getText() { 43 | return textView.getText().toString(); 44 | } 45 | 46 | public void setText(String text) { 47 | textView.setText(text); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/component/ItemTextSend.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.component; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | import android.widget.LinearLayout; 9 | import android.widget.TextView; 10 | 11 | import androidx.annotation.StyleableRes; 12 | 13 | import java.util.UUID; 14 | 15 | import lombok.Setter; 16 | import nju.androidchat.client.R; 17 | 18 | public class ItemTextSend extends LinearLayout implements View.OnLongClickListener { 19 | @StyleableRes 20 | int index0 = 0; 21 | 22 | private TextView textView; 23 | private Context context; 24 | private UUID messageId; 25 | @Setter private OnRecallMessageRequested onRecallMessageRequested; 26 | 27 | public ItemTextSend(Context context, String text, UUID messageId, OnRecallMessageRequested onRecallMessageRequested) { 28 | super(context); 29 | this.context = context; 30 | inflate(context, R.layout.item_text_send, this); 31 | this.textView = findViewById(R.id.chat_item_content_text); 32 | this.messageId = messageId; 33 | this.onRecallMessageRequested = onRecallMessageRequested; 34 | 35 | this.setOnLongClickListener(this); 36 | setText(text); 37 | } 38 | 39 | public String getText() { 40 | return textView.getText().toString(); 41 | } 42 | 43 | public void setText(String text) { 44 | textView.setText(text); 45 | } 46 | 47 | @Override 48 | public boolean onLongClick(View v) { 49 | AlertDialog.Builder builder = new AlertDialog.Builder(context); 50 | builder.setMessage("确定要撤回这条消息吗?") 51 | .setPositiveButton("是", (dialog, which) -> { 52 | if (onRecallMessageRequested != null) { 53 | onRecallMessageRequested.onRecallMessageRequested(this.messageId); 54 | } 55 | }) 56 | .setNegativeButton("否", ((dialog, which) -> { 57 | })) 58 | .create() 59 | .show(); 60 | 61 | return true; 62 | 63 | 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/component/OnRecallMessageRequested.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.component; 2 | 3 | import java.util.UUID; 4 | 5 | public interface OnRecallMessageRequested { 6 | void onRecallMessageRequested(UUID messageId); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvc0/Mvc0TalkActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvc0; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.view.KeyEvent; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | import android.widget.Button; 9 | import android.widget.EditText; 10 | import android.widget.LinearLayout; 11 | import android.widget.ScrollView; 12 | import android.widget.TextView; 13 | 14 | import androidx.appcompat.app.AppCompatActivity; 15 | 16 | import java.util.List; 17 | import java.util.UUID; 18 | 19 | import lombok.extern.java.Log; 20 | import nju.androidchat.client.ClientMessage; 21 | import nju.androidchat.client.R; 22 | import nju.androidchat.client.Utils; 23 | import nju.androidchat.client.component.ItemTextReceive; 24 | import nju.androidchat.client.component.ItemTextSend; 25 | import nju.androidchat.client.component.OnRecallMessageRequested; 26 | 27 | @Log 28 | public class Mvc0TalkActivity extends AppCompatActivity 29 | implements Mvc0TalkModel.MessageListUpdateListener, 30 | TextView.OnEditorActionListener, 31 | OnRecallMessageRequested 32 | { 33 | 34 | private Mvc0TalkModel model = new Mvc0TalkModel(); 35 | private Mvc0TalkController controller = new Mvc0TalkController(model, this); 36 | 37 | @Override 38 | protected void onCreate(Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | setContentView(R.layout.activity_main); 41 | 42 | 43 | // Input事件处理 44 | EditText editText = findViewById(R.id.et_content); 45 | editText.setOnEditorActionListener(this); 46 | 47 | // View向Model注册事件并开始监听 48 | model.setMessageListener(this); 49 | model.startListening(); 50 | } 51 | 52 | // 处理Model更新事件,更新UI。注意这是在另外一个线程,所以不能直接操作 53 | // 这里的处理事件的方法比较暴力,就是删除到 54 | @Override 55 | public void onListUpdate(List messages) { 56 | runOnUiThread(() -> { 57 | LinearLayout content = findViewById(R.id.chat_content); 58 | 59 | // 删除所有已有的ItemText 60 | content.removeAllViews(); 61 | 62 | // 增加ItemText 63 | for (ClientMessage message: messages) { 64 | String text = String.format("%s", message.getMessage()); 65 | // 如果是自己发的,增加ItemTextSend,并传入撤回请求事件处理 66 | if (message.getSenderUsername().equals(model.getUsername())) { 67 | content.addView(new ItemTextSend(this, text, message.getMessageId(), this)); 68 | } else { 69 | content.addView(new ItemTextReceive(this, text, message.getMessageId())); 70 | } 71 | } 72 | 73 | // scroll to bottom 74 | Utils.scrollListToBottom(this); 75 | 76 | }); 77 | 78 | } 79 | 80 | @Override 81 | public void onBackPressed() { 82 | controller.jumpBackToHome(); 83 | } 84 | 85 | @Override 86 | public boolean onTouchEvent(MotionEvent event) { 87 | if (null != this.getCurrentFocus()) { 88 | return hideKeyboard(); 89 | } 90 | return super.onTouchEvent(event); 91 | } 92 | 93 | private boolean hideKeyboard() { 94 | return Utils.hideKeyboard(this); 95 | } 96 | 97 | 98 | @Override 99 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 100 | if (Utils.send(actionId, event)) { 101 | hideKeyboard(); 102 | // 异步地让Controller处理事件 103 | sendText(); 104 | } 105 | return false; 106 | } 107 | 108 | private void sendText() { 109 | EditText text = findViewById(R.id.et_content); 110 | AsyncTask.execute(() -> { 111 | controller.sendInformation(text.getText().toString()); 112 | }); 113 | } 114 | 115 | public void onBtnSendClicked(View v) { 116 | hideKeyboard(); 117 | sendText(); 118 | } 119 | 120 | // 当用户长按消息,并选择撤回消息时做什么,MVC-0不实现 121 | @Override 122 | public void onRecallMessageRequested(UUID messageId) { 123 | 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvc0/Mvc0TalkController.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvc0; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.experimental.ExtensionMethod; 7 | import nju.androidchat.client.Utils; 8 | 9 | @AllArgsConstructor 10 | public class Mvc0TalkController { 11 | 12 | private Mvc0TalkModel model; 13 | private Mvc0TalkActivity activity; 14 | 15 | // Controller将View传来的请求转发给Model进行处理 16 | public void sendInformation(String message) { 17 | model.sendInformation(message); 18 | } 19 | 20 | 21 | public void jumpBackToHome() { 22 | AsyncTask.execute(() -> { 23 | model.disconnect(); 24 | }); 25 | 26 | Utils.jumpToHome(activity); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvc0/Mvc0TalkModel.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvc0; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | import lombok.extern.java.Log; 9 | import nju.androidchat.client.ClientMessage; 10 | import nju.androidchat.client.socket.MessageListener; 11 | import nju.androidchat.client.socket.SocketClient; 12 | import nju.androidchat.shared.message.ClientSendMessage; 13 | import nju.androidchat.shared.message.ErrorMessage; 14 | import nju.androidchat.shared.message.Message; 15 | import nju.androidchat.shared.message.RecallMessage; 16 | import nju.androidchat.shared.message.ServerSendMessage; 17 | 18 | @Log 19 | public class Mvc0TalkModel implements MessageListener { 20 | 21 | // Model需要Activity注册的监听器 22 | public interface MessageListUpdateListener { 23 | void onListUpdate(List messages); 24 | } 25 | 26 | private SocketClient client; 27 | private MessageListUpdateListener listener; 28 | private List messageList; 29 | 30 | public Mvc0TalkModel() { 31 | this.client = SocketClient.getClient(); 32 | this.messageList = new ArrayList<>(); 33 | 34 | // Model本身去注册Socket的消息接受事件 35 | client.setMessageListener(this); 36 | } 37 | 38 | // 处理从服务器接收到的消息 39 | @Override 40 | public void onMessageReceived(Message message) { 41 | if (message instanceof ServerSendMessage) { 42 | // 接受到其他设备发来的消息 43 | // 增加到自己的消息列表里,并通知UI修改 44 | ServerSendMessage serverSendMessage = (ServerSendMessage) message; 45 | log.info(String.format("%s sent a message: %s", 46 | serverSendMessage.getSenderUsername(), 47 | serverSendMessage.getMessage() 48 | )); 49 | messageList.add(new ClientMessage(serverSendMessage)); 50 | if (listener != null) { 51 | listener.onListUpdate(messageList); 52 | } 53 | } else if (message instanceof ErrorMessage) { 54 | // 接收到服务器的错误消息 55 | log.severe("Server error: " + ((ErrorMessage) message).getErrorMessage()); 56 | 57 | } else if (message instanceof RecallMessage) { 58 | // 接受到服务器的撤回消息,MVC-0不实现 59 | } else { 60 | // 不认识的消息 61 | log.severe("Unsupported message received: " + message.toString()); 62 | 63 | } 64 | } 65 | 66 | public String getUsername() { 67 | return client.getUsername(); 68 | } 69 | 70 | public void setMessageListener(MessageListUpdateListener listener) { 71 | this.listener = listener; 72 | } 73 | 74 | public void startListening() { 75 | client.startListening(); 76 | } 77 | 78 | public void disconnect() { 79 | client.disconnect(); 80 | } 81 | 82 | // 接受Controller的Delegate,实际进行处理 83 | public void sendInformation(String message) { 84 | //处理事件 85 | LocalDateTime now = LocalDateTime.now(); 86 | UUID uuid = UUID.randomUUID(); 87 | 88 | // 可选:乐观更新,发送信息之前就增加到自己的消息列表里,并通知View更新UI 89 | messageList.add(new ClientMessage(uuid, now, getUsername(), message)); 90 | if (listener != null) { 91 | listener.onListUpdate(messageList); 92 | } 93 | 94 | // 阻塞地把信息发送到服务器 95 | client.writeToServer(new ClientSendMessage(uuid, now, message)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvc1/Mvc1BackupActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvc1; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.Button; 7 | import android.widget.TextView; 8 | import android.widget.Toast; 9 | 10 | import androidx.annotation.Nullable; 11 | import androidx.appcompat.app.AppCompatActivity; 12 | 13 | import nju.androidchat.client.R; 14 | 15 | public class Mvc1BackupActivity extends AppCompatActivity { 16 | 17 | private Mvc1BackupController controller; 18 | private Mvc1BackupModel model; 19 | private Button btnBackup; 20 | 21 | @Override 22 | protected void onCreate(@Nullable Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.backup); 25 | 26 | model = new Mvc1BackupModel(); 27 | controller = new Mvc1BackupController(model); 28 | 29 | TextView txLastUpdated = findViewById(R.id.last_updated_lb); 30 | 31 | Button btnBackup = findViewById(R.id.backup_btn); 32 | 33 | 34 | // 订阅备份开始事件 35 | model.setBackupStartListener(() -> { 36 | runOnUiThread(() -> { 37 | btnBackup.setText("正在备份"); 38 | btnBackup.setEnabled(false); 39 | }); 40 | }); 41 | 42 | // 订阅备份完成事件 43 | model.setBackupCompleteListener(() -> { 44 | runOnUiThread(() -> { 45 | if (model.getLastUpdated() == null){ 46 | txLastUpdated.setText("正在备份"); 47 | } else { 48 | Toast.makeText(this, "备份成功!", Toast.LENGTH_SHORT).show(); 49 | txLastUpdated.setText(model.getLastUpdated().toString()); 50 | } 51 | btnBackup.setText("备份"); 52 | btnBackup.setEnabled(true); 53 | }); 54 | }); 55 | 56 | 57 | txLastUpdated.setText("从未更新"); 58 | 59 | } 60 | 61 | public void onBtnBackupClicked(View view) { 62 | AsyncTask.execute(() -> { 63 | controller.backup(); 64 | }); 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvc1/Mvc1BackupController.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvc1; 2 | 3 | import lombok.AllArgsConstructor; 4 | 5 | @AllArgsConstructor 6 | public class Mvc1BackupController { 7 | 8 | public Mvc1BackupModel model; 9 | 10 | public void backup() { 11 | model.backup(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvc1/Mvc1BackupModel.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvc1; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.SneakyThrows; 8 | 9 | public class Mvc1BackupModel { 10 | 11 | public interface BackupCompleteListener { 12 | void onBackupComplete(); 13 | } 14 | 15 | public interface BackupStartListener { 16 | void onBackupStart(); 17 | } 18 | 19 | @Getter 20 | private boolean backingUp = false; 21 | 22 | @Getter 23 | private LocalDateTime lastUpdated = null; 24 | 25 | @Getter @Setter 26 | private BackupCompleteListener backupCompleteListener; 27 | 28 | @Getter @Setter 29 | private BackupStartListener backupStartListener; 30 | 31 | @SneakyThrows 32 | public void backup() { 33 | 34 | // 修改正在备份状态数据 35 | backingUp = true; 36 | if (backupStartListener != null) { 37 | backupStartListener.onBackupStart(); 38 | } 39 | 40 | // 发送HTTP请求(使用Thread.sleep替代) 41 | Thread.sleep(3000); 42 | 43 | // 备份完成,修改上次更新时间,修改正在备份状态 44 | lastUpdated = LocalDateTime.now(); 45 | backingUp = true; 46 | 47 | if (backupCompleteListener != null) { 48 | backupCompleteListener.onBackupComplete(); 49 | } 50 | 51 | 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvc1/Mvc1TalkActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvc1; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.view.KeyEvent; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | import android.widget.EditText; 9 | import android.widget.LinearLayout; 10 | import android.widget.TextView; 11 | 12 | import androidx.appcompat.app.AppCompatActivity; 13 | 14 | import java.util.List; 15 | import java.util.UUID; 16 | 17 | import lombok.extern.java.Log; 18 | import nju.androidchat.client.ClientMessage; 19 | import nju.androidchat.client.R; 20 | import nju.androidchat.client.Utils; 21 | import nju.androidchat.client.component.ItemTextReceive; 22 | import nju.androidchat.client.component.ItemTextSend; 23 | import nju.androidchat.client.component.OnRecallMessageRequested; 24 | 25 | @Log 26 | public class Mvc1TalkActivity extends AppCompatActivity implements Mvc1TalkModel.MessageListUpdateListener, TextView.OnEditorActionListener, OnRecallMessageRequested 27 | { 28 | 29 | private Mvc1TalkModel model = new Mvc1TalkModel(); 30 | private Mvc1TalkController controller = new Mvc1TalkController(model, this); 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_main_with_backup_btn); 36 | 37 | 38 | // Input事件处理 39 | EditText editText = findViewById(R.id.et_content); 40 | editText.setOnEditorActionListener(this); 41 | 42 | // View向Model注册事件并开始监听 43 | model.setMessageListener(this); 44 | model.startListening(); 45 | } 46 | 47 | // 处理Model更新事件,更新UI。注意这是在另外一个线程,所以不能直接操作 48 | // 这里的处理事件的方法比较暴力,就是删除到 49 | @Override 50 | public void onListUpdate(List messages) { 51 | runOnUiThread(() -> { 52 | LinearLayout content = findViewById(R.id.chat_content); 53 | 54 | // 删除所有已有的ItemText 55 | content.removeAllViews(); 56 | 57 | // 增加ItemText 58 | for (ClientMessage message: messages) { 59 | String text = String.format("%s", message.getMessage()); 60 | // 如果是自己发的,增加ItemTextSend 61 | if (message.getSenderUsername().equals(model.getUsername())) { 62 | content.addView(new ItemTextSend(this, text, message.getMessageId(), this)); 63 | } else { 64 | content.addView(new ItemTextReceive(this, text, message.getMessageId())); 65 | } 66 | } 67 | 68 | // scroll to bottom 69 | Utils.scrollListToBottom(this); 70 | 71 | }); 72 | 73 | } 74 | 75 | @Override 76 | public void onBackPressed() { 77 | controller.jumpBackToHome(); 78 | } 79 | 80 | @Override 81 | public boolean onTouchEvent(MotionEvent event) { 82 | if (null != this.getCurrentFocus()) { 83 | return hideKeyboard(); 84 | } 85 | return super.onTouchEvent(event); 86 | } 87 | 88 | private boolean hideKeyboard() { 89 | return Utils.hideKeyboard(this); 90 | } 91 | 92 | 93 | @Override 94 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 95 | if (Utils.send(actionId, event)) { 96 | hideKeyboard(); 97 | // 异步地让Controller处理事件 98 | sendText(); 99 | } 100 | return false; 101 | } 102 | 103 | private void sendText() { 104 | EditText text = findViewById(R.id.et_content); 105 | AsyncTask.execute(() -> { 106 | controller.sendInformation(text.getText().toString()); 107 | }); 108 | } 109 | 110 | public void onBtnSendClicked(View v) { 111 | hideKeyboard(); 112 | sendText(); 113 | } 114 | 115 | public void onBtnToBackupClicked(View view) { 116 | Utils.jumpTo(this, Mvc1BackupActivity.class); 117 | } 118 | 119 | // 当用户长按消息,并选择撤回消息时做什么,MVC-1不实现\ 120 | @Override 121 | public void onRecallMessageRequested(UUID messageId) { 122 | 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvc1/Mvc1TalkController.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvc1; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import lombok.AllArgsConstructor; 6 | import nju.androidchat.client.Utils; 7 | 8 | @AllArgsConstructor 9 | public class Mvc1TalkController { 10 | 11 | private Mvc1TalkModel model; 12 | private Mvc1TalkActivity activity; 13 | 14 | // Controller将View传来的请求转发给Model进行处理 15 | public void sendInformation(String message) { 16 | model.sendInformation(message); 17 | } 18 | 19 | 20 | public void jumpBackToHome() { 21 | AsyncTask.execute(() -> { 22 | model.disconnect(); 23 | }); 24 | 25 | Utils.jumpToHome(activity); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvc1/Mvc1TalkModel.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvc1; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | import lombok.extern.java.Log; 9 | import nju.androidchat.client.ClientMessage; 10 | import nju.androidchat.client.socket.MessageListener; 11 | import nju.androidchat.client.socket.SocketClient; 12 | import nju.androidchat.shared.message.ClientSendMessage; 13 | import nju.androidchat.shared.message.ErrorMessage; 14 | import nju.androidchat.shared.message.Message; 15 | import nju.androidchat.shared.message.RecallMessage; 16 | import nju.androidchat.shared.message.ServerSendMessage; 17 | 18 | @Log 19 | public class Mvc1TalkModel implements MessageListener { 20 | 21 | // Model需要Activity注册的监听器 22 | public interface MessageListUpdateListener { 23 | void onListUpdate(List messages); 24 | } 25 | 26 | private SocketClient client; 27 | private MessageListUpdateListener listener; 28 | private List messageList; 29 | 30 | public Mvc1TalkModel() { 31 | this.client = SocketClient.getClient(); 32 | this.messageList = new ArrayList<>(); 33 | 34 | // Model本身去注册Socket的消息接受事件 35 | client.setMessageListener(this); 36 | } 37 | 38 | // 处理从服务器接收到的消息 39 | @Override 40 | public void onMessageReceived(Message message) { 41 | if (message instanceof ServerSendMessage) { 42 | // 接受到其他设备发来的消息 43 | // 增加到自己的消息列表里,并通知UI修改 44 | ServerSendMessage serverSendMessage = (ServerSendMessage) message; 45 | log.info(String.format("%s sent a message: %s", 46 | serverSendMessage.getSenderUsername(), 47 | serverSendMessage.getMessage() 48 | )); 49 | messageList.add(new ClientMessage(serverSendMessage)); 50 | if (listener != null) { 51 | listener.onListUpdate(messageList); 52 | } 53 | } else if (message instanceof ErrorMessage) { 54 | // 接收到服务器的错误消息 55 | log.severe("Server error: " + ((ErrorMessage) message).getErrorMessage()); 56 | 57 | } else if (message instanceof RecallMessage) { 58 | // 接受到服务器的撤回消息,MVC-0不实现 59 | } else { 60 | // 不认识的消息 61 | log.severe("Unsupported message received: " + message.toString()); 62 | 63 | } 64 | } 65 | 66 | public String getUsername() { 67 | return client.getUsername(); 68 | } 69 | 70 | public void setMessageListener(MessageListUpdateListener listener) { 71 | this.listener = listener; 72 | } 73 | 74 | public void startListening() { 75 | client.startListening(); 76 | } 77 | 78 | public void disconnect() { 79 | client.disconnect(); 80 | } 81 | 82 | // 接受Controller的Delegate,实际进行处理 83 | public void sendInformation(String message) { 84 | //处理事件 85 | LocalDateTime now = LocalDateTime.now(); 86 | UUID uuid = UUID.randomUUID(); 87 | 88 | // 可选:乐观更新,发送信息之前就增加到自己的消息列表里,并通知View更新UI 89 | messageList.add(new ClientMessage(uuid, now, getUsername(), message)); 90 | if (listener != null) { 91 | listener.onListUpdate(messageList); 92 | } 93 | 94 | // 阻塞地把信息发送到服务器 95 | client.writeToServer(new ClientSendMessage(uuid, now, message)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp0/BasePresenter.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp0; 2 | 3 | public interface BasePresenter { 4 | 5 | void start(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp0/BaseView.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp0; 2 | 3 | public interface BaseView { 4 | 5 | void setPresenter(T presenter); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp0/Mvp0Contract.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp0; 2 | 3 | import java.util.List; 4 | 5 | import nju.androidchat.client.ClientMessage; 6 | 7 | public interface Mvp0Contract { 8 | interface View extends BaseView { 9 | void showMessageList(List messages); 10 | } 11 | 12 | interface Presenter extends BasePresenter { 13 | void sendMessage(String content); 14 | 15 | void receiveMessage(ClientMessage content); 16 | 17 | String getUsername(); 18 | 19 | //撤回消息mvp0不实现 20 | void recallMessage(int index0); 21 | } 22 | 23 | interface Model { 24 | ClientMessage sendInformation(String message); 25 | 26 | String getUsername(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp0/Mvp0TalkActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp0; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.view.KeyEvent; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | import android.widget.EditText; 9 | import android.widget.LinearLayout; 10 | import android.widget.ScrollView; 11 | import android.widget.TextView; 12 | 13 | import androidx.appcompat.app.AppCompatActivity; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.UUID; 18 | 19 | import lombok.extern.java.Log; 20 | import nju.androidchat.client.ClientMessage; 21 | import nju.androidchat.client.R; 22 | import nju.androidchat.client.Utils; 23 | import nju.androidchat.client.component.ItemTextReceive; 24 | import nju.androidchat.client.component.ItemTextSend; 25 | import nju.androidchat.client.component.OnRecallMessageRequested; 26 | 27 | @Log 28 | public class Mvp0TalkActivity extends AppCompatActivity implements Mvp0Contract.View, TextView.OnEditorActionListener, OnRecallMessageRequested { 29 | private Mvp0Contract.Presenter presenter; 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_main); 35 | 36 | Mvp0TalkModel mvp0TalkModel = new Mvp0TalkModel(); 37 | 38 | // Create the presenter 39 | this.presenter = new Mvp0TalkPresenter(mvp0TalkModel, this, new ArrayList<>()); 40 | mvp0TalkModel.setIMvp0TalkPresenter(this.presenter); 41 | } 42 | 43 | @Override 44 | public void onResume() { 45 | super.onResume(); 46 | presenter.start(); 47 | } 48 | 49 | @Override 50 | public void showMessageList(List messages) { 51 | runOnUiThread(() -> { 52 | LinearLayout content = findViewById(R.id.chat_content); 53 | 54 | // 删除所有已有的ItemText 55 | content.removeAllViews(); 56 | 57 | // 增加ItemText 58 | for (ClientMessage message : messages) { 59 | String text = String.format("%s", message.getMessage()); 60 | // 如果是自己发的,增加ItemTextSend 61 | if (message.getSenderUsername().equals(this.presenter.getUsername())) { 62 | content.addView(new ItemTextSend(this, text, message.getMessageId(), this)); 63 | } else { 64 | content.addView(new ItemTextReceive(this, text, message.getMessageId())); 65 | } 66 | } 67 | 68 | Utils.scrollListToBottom(this); 69 | } 70 | ); 71 | } 72 | 73 | @Override 74 | public void setPresenter(Mvp0Contract.Presenter presenter) { 75 | this.presenter = presenter; 76 | } 77 | 78 | @Override 79 | public boolean onTouchEvent(MotionEvent event) { 80 | if (null != this.getCurrentFocus()) { 81 | return hideKeyboard(); 82 | } 83 | return super.onTouchEvent(event); 84 | } 85 | 86 | private boolean hideKeyboard() { 87 | return Utils.hideKeyboard(this); 88 | } 89 | 90 | 91 | @Override 92 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 93 | if (Utils.send(actionId, event)) { 94 | hideKeyboard(); 95 | // 异步地让Controller处理事件 96 | sendText(); 97 | } 98 | return false; 99 | } 100 | 101 | private void sendText() { 102 | EditText text = findViewById(R.id.et_content); 103 | AsyncTask.execute(() -> { 104 | this.presenter.sendMessage(text.getText().toString()); 105 | }); 106 | } 107 | 108 | public void onBtnSendClicked(View v) { 109 | hideKeyboard(); 110 | sendText(); 111 | } 112 | 113 | // 当用户长按消息,并选择撤回消息时做什么,MVP-0不实现 114 | @Override 115 | public void onRecallMessageRequested(UUID messageId) { 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp0/Mvp0TalkModel.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp0; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | import lombok.Setter; 9 | import lombok.extern.java.Log; 10 | import nju.androidchat.client.ClientMessage; 11 | import nju.androidchat.client.mvc0.Mvc0TalkModel; 12 | import nju.androidchat.client.socket.MessageListener; 13 | import nju.androidchat.client.socket.SocketClient; 14 | import nju.androidchat.shared.message.ClientSendMessage; 15 | import nju.androidchat.shared.message.ErrorMessage; 16 | import nju.androidchat.shared.message.Message; 17 | import nju.androidchat.shared.message.RecallMessage; 18 | import nju.androidchat.shared.message.ServerSendMessage; 19 | 20 | @Log 21 | public class Mvp0TalkModel implements MessageListener, Mvp0Contract.Model { 22 | 23 | private SocketClient client; 24 | 25 | @Setter 26 | private Mvp0Contract.Presenter iMvp0TalkPresenter; 27 | 28 | public Mvp0TalkModel() { 29 | this.client = SocketClient.getClient(); 30 | // Model本身去注册Socket的消息接受事件 31 | client.setMessageListener(this); 32 | client.startListening(); 33 | } 34 | 35 | @Override 36 | public String getUsername() { 37 | return client.getUsername(); 38 | } 39 | 40 | @Override 41 | public ClientMessage sendInformation(String message) { 42 | //处理事件 43 | LocalDateTime now = LocalDateTime.now(); 44 | UUID uuid = UUID.randomUUID(); 45 | ClientMessage clientMessage = new ClientMessage(uuid, now, getUsername(), message); 46 | // 阻塞地把信息发送到服务器 47 | client.writeToServer(new ClientSendMessage(uuid, now, message)); 48 | return clientMessage; 49 | } 50 | 51 | @Override 52 | public void onMessageReceived(Message message) { 53 | if (message instanceof ServerSendMessage) { 54 | // 接受到其他设备发来的消息 55 | ServerSendMessage serverSendMessage = (ServerSendMessage) message; 56 | log.info(String.format("%s sent a message: %s", 57 | serverSendMessage.getSenderUsername(), 58 | serverSendMessage.getMessage() 59 | )); 60 | iMvp0TalkPresenter.receiveMessage(new ClientMessage(serverSendMessage)); 61 | } else if (message instanceof ErrorMessage) { 62 | // 接收到服务器的错误消息 63 | log.severe("Server error: " + ((ErrorMessage) message).getErrorMessage()); 64 | } else if (message instanceof RecallMessage) { 65 | // 接受到服务器的撤回消息,MVC-0不实现 66 | } else { 67 | // 不认识的消息 68 | log.severe("Unsupported message received: " + message.toString()); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp0/Mvp0TalkPresenter.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp0; 2 | 3 | import java.util.List; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import nju.androidchat.client.ClientMessage; 8 | 9 | @AllArgsConstructor 10 | public class Mvp0TalkPresenter implements Mvp0Contract.Presenter { 11 | 12 | private Mvp0Contract.Model mvp0TalkModel; 13 | private Mvp0Contract.View iMvp0TalkView; 14 | 15 | @Getter 16 | private List clientMessages; 17 | 18 | @Override 19 | public void sendMessage(String content) { 20 | ClientMessage clientMessage = mvp0TalkModel.sendInformation(content); 21 | refreshMessageList(clientMessage); 22 | } 23 | 24 | @Override 25 | public void receiveMessage(ClientMessage clientMessage) { 26 | refreshMessageList(clientMessage); 27 | } 28 | 29 | @Override 30 | public String getUsername() { 31 | return mvp0TalkModel.getUsername(); 32 | } 33 | 34 | private void refreshMessageList(ClientMessage clientMessage) { 35 | clientMessages.add(clientMessage); 36 | iMvp0TalkView.showMessageList(clientMessages); 37 | } 38 | 39 | //撤回消息,Mvp0暂不实现 40 | @Override 41 | public void recallMessage(int index0) { 42 | 43 | } 44 | 45 | @Override 46 | public void start() { 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp1/BasePresenter.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp1; 2 | 3 | public interface BasePresenter { 4 | 5 | void start(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp1/BaseView.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp1; 2 | 3 | public interface BaseView { 4 | 5 | void setPresenter(T presenter); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp1/Mvp1BackupActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp1; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.Button; 7 | import android.widget.TextView; 8 | 9 | import androidx.annotation.Nullable; 10 | import androidx.appcompat.app.AppCompatActivity; 11 | 12 | import lombok.extern.java.Log; 13 | import nju.androidchat.client.R; 14 | 15 | @Log 16 | public class Mvp1BackupActivity extends AppCompatActivity implements Mvp1Contract.BackupView { 17 | 18 | private Mvp1Contract.BackupPresenter presenter; 19 | private TextView txLastUpdated; 20 | private Button backupBtn; 21 | 22 | @Override 23 | protected void onCreate(@Nullable Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | setContentView(R.layout.backup); 26 | 27 | txLastUpdated = findViewById(R.id.last_updated_lb); 28 | backupBtn = findViewById(R.id.backup_btn); 29 | 30 | Mvp1BackupModel mvp1BackupModel = new Mvp1BackupModel(); 31 | 32 | // Create the presenter 33 | this.presenter = new Mvp1BackupPresenter(mvp1BackupModel, this); 34 | mvp1BackupModel.setIMvp1BackupPresenter(this.presenter); 35 | } 36 | 37 | public void onBtnBackupClicked(View view) { 38 | AsyncTask.execute(() -> { 39 | presenter.backup(); 40 | }); 41 | } 42 | 43 | @Override 44 | public void editBtnStatusAndText(boolean canEdit, String text) { 45 | runOnUiThread(() -> { 46 | backupBtn.setText(text); 47 | backupBtn.setEnabled(canEdit); 48 | }); 49 | } 50 | 51 | @Override 52 | public void editTextView(String text) { 53 | runOnUiThread(() -> txLastUpdated.setText(text)); 54 | } 55 | 56 | @Override 57 | public void setPresenter(Mvp1Contract.BackupPresenter presenter) { 58 | this.presenter = presenter; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp1/Mvp1BackupModel.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp1; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.extern.java.Log; 8 | 9 | @Log 10 | public class Mvp1BackupModel implements Mvp1Contract.BackupModel { 11 | 12 | @Setter 13 | private Mvp1Contract.BackupPresenter iMvp1BackupPresenter; 14 | 15 | @Getter 16 | private LocalDateTime lastUpdated = null; 17 | 18 | @Override 19 | public void backup() { 20 | // Simulate a HTTP request 21 | try { 22 | Thread.sleep(3000); 23 | lastUpdated = LocalDateTime.now(); 24 | } catch (InterruptedException e) { 25 | e.printStackTrace(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp1/Mvp1BackupPresenter.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp1; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Setter; 5 | 6 | @AllArgsConstructor 7 | public class Mvp1BackupPresenter implements Mvp1Contract.BackupPresenter { 8 | 9 | private Mvp1Contract.BackupModel backupModel; 10 | private Mvp1Contract.BackupView backupView; 11 | 12 | @Override 13 | public void backup() { 14 | // Presenter首先修改界面显示 15 | this.backupView.editBtnStatusAndText(false, "正在备份"); 16 | 17 | // 再进行数据操作 18 | this.backupModel.backup(); 19 | 20 | // 数据操作结束后,将界面改回来 21 | this.backupView.editBtnStatusAndText(true, "备份"); 22 | this.backupView.editTextView(this.backupModel.getLastUpdated().toString()); 23 | } 24 | 25 | @Override 26 | public void start() { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp1/Mvp1Contract.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp1; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | 6 | import nju.androidchat.client.ClientMessage; 7 | 8 | public interface Mvp1Contract { 9 | interface TalkView extends BaseView { 10 | void showMessageList(List messages); 11 | } 12 | 13 | interface TalkPresenter extends BasePresenter { 14 | void sendMessage(String content); 15 | 16 | void receiveMessage(ClientMessage content); 17 | 18 | String getUsername(); 19 | 20 | //撤回消息mvp0不实现 21 | void recallMessage(int index0); 22 | } 23 | 24 | interface TalkModel { 25 | ClientMessage sendInformation(String message); 26 | 27 | String getUsername(); 28 | } 29 | 30 | interface BackupView extends BaseView { 31 | void editBtnStatusAndText(boolean canEdit, String text); 32 | 33 | void editTextView(String text); 34 | } 35 | 36 | interface BackupPresenter extends BasePresenter { 37 | void backup(); 38 | } 39 | 40 | interface BackupModel { 41 | void backup(); 42 | 43 | LocalDateTime getLastUpdated(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp1/Mvp1TalkActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp1; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.view.KeyEvent; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | import android.widget.EditText; 9 | import android.widget.LinearLayout; 10 | import android.widget.TextView; 11 | 12 | import androidx.appcompat.app.AppCompatActivity; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.UUID; 17 | 18 | import lombok.extern.java.Log; 19 | import nju.androidchat.client.ClientMessage; 20 | import nju.androidchat.client.R; 21 | import nju.androidchat.client.Utils; 22 | import nju.androidchat.client.component.ItemTextReceive; 23 | import nju.androidchat.client.component.ItemTextSend; 24 | import nju.androidchat.client.component.OnRecallMessageRequested; 25 | import nju.androidchat.client.mvc1.Mvc1BackupActivity; 26 | 27 | @Log 28 | public class Mvp1TalkActivity extends AppCompatActivity implements Mvp1Contract.TalkView, TextView.OnEditorActionListener, OnRecallMessageRequested { 29 | private Mvp1Contract.TalkPresenter presenter; 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_main_with_backup_btn); 35 | 36 | Mvp1TalkModel mvp1TalkModel = new Mvp1TalkModel(); 37 | 38 | // Create the presenter 39 | this.presenter = new Mvp1TalkPresenter(mvp1TalkModel, this, new ArrayList<>()); 40 | mvp1TalkModel.setIMvp0TalkPresenter(this.presenter); 41 | } 42 | 43 | @Override 44 | public void onResume() { 45 | super.onResume(); 46 | presenter.start(); 47 | } 48 | 49 | @Override 50 | public void showMessageList(List messages) { 51 | runOnUiThread(() -> { 52 | LinearLayout content = findViewById(R.id.chat_content); 53 | 54 | // 删除所有已有的ItemText 55 | content.removeAllViews(); 56 | 57 | // 增加ItemText 58 | for (ClientMessage message : messages) { 59 | String text = String.format("%s", message.getMessage()); 60 | // 如果是自己发的,增加ItemTextSend 61 | if (message.getSenderUsername().equals(this.presenter.getUsername())) { 62 | content.addView(new ItemTextSend(this, text, message.getMessageId(), this)); 63 | } else { 64 | content.addView(new ItemTextReceive(this, text, message.getMessageId())); 65 | } 66 | } 67 | 68 | Utils.scrollListToBottom(this); 69 | } 70 | ); 71 | } 72 | 73 | @Override 74 | public void setPresenter(Mvp1Contract.TalkPresenter presenter) { 75 | this.presenter = presenter; 76 | } 77 | 78 | @Override 79 | public boolean onTouchEvent(MotionEvent event) { 80 | if (null != this.getCurrentFocus()) { 81 | return hideKeyboard(); 82 | } 83 | return super.onTouchEvent(event); 84 | } 85 | 86 | private boolean hideKeyboard() { 87 | return Utils.hideKeyboard(this); 88 | } 89 | 90 | 91 | @Override 92 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 93 | if (Utils.send(actionId, event)) { 94 | hideKeyboard(); 95 | // 异步地让Controller处理事件 96 | sendText(); 97 | } 98 | return false; 99 | } 100 | 101 | private void sendText() { 102 | EditText text = findViewById(R.id.et_content); 103 | AsyncTask.execute(() -> { 104 | this.presenter.sendMessage(text.getText().toString()); 105 | }); 106 | } 107 | 108 | public void onBtnSendClicked(View v) { 109 | hideKeyboard(); 110 | sendText(); 111 | } 112 | 113 | public void onBtnToBackupClicked(View view) { 114 | Utils.jumpTo(this, Mvp1BackupActivity.class); 115 | } 116 | 117 | // 当用户长按消息,并选择撤回消息时做什么,MVP-0不实现 118 | @Override 119 | public void onRecallMessageRequested(UUID messageId) { 120 | 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp1/Mvp1TalkModel.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp1; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.UUID; 5 | 6 | import lombok.Setter; 7 | import lombok.extern.java.Log; 8 | import nju.androidchat.client.ClientMessage; 9 | import nju.androidchat.client.socket.MessageListener; 10 | import nju.androidchat.client.socket.SocketClient; 11 | import nju.androidchat.shared.message.ClientSendMessage; 12 | import nju.androidchat.shared.message.ErrorMessage; 13 | import nju.androidchat.shared.message.Message; 14 | import nju.androidchat.shared.message.RecallMessage; 15 | import nju.androidchat.shared.message.ServerSendMessage; 16 | 17 | @Log 18 | public class Mvp1TalkModel implements MessageListener, Mvp1Contract.TalkModel { 19 | 20 | private SocketClient client; 21 | 22 | @Setter 23 | private Mvp1Contract.TalkPresenter iMvp0TalkPresenter; 24 | 25 | public Mvp1TalkModel() { 26 | this.client = SocketClient.getClient(); 27 | // Model本身去注册Socket的消息接受事件 28 | client.setMessageListener(this); 29 | client.startListening(); 30 | } 31 | 32 | @Override 33 | public String getUsername() { 34 | return client.getUsername(); 35 | } 36 | 37 | @Override 38 | public ClientMessage sendInformation(String message) { 39 | //处理事件 40 | LocalDateTime now = LocalDateTime.now(); 41 | UUID uuid = UUID.randomUUID(); 42 | ClientMessage clientMessage = new ClientMessage(uuid, now, getUsername(), message); 43 | // 阻塞地把信息发送到服务器 44 | client.writeToServer(new ClientSendMessage(uuid, now, message)); 45 | return clientMessage; 46 | } 47 | 48 | @Override 49 | public void onMessageReceived(Message message) { 50 | if (message instanceof ServerSendMessage) { 51 | // 接受到其他设备发来的消息 52 | ServerSendMessage serverSendMessage = (ServerSendMessage) message; 53 | log.info(String.format("%s sent a message: %s", 54 | serverSendMessage.getSenderUsername(), 55 | serverSendMessage.getMessage() 56 | )); 57 | iMvp0TalkPresenter.receiveMessage(new ClientMessage(serverSendMessage)); 58 | } else if (message instanceof ErrorMessage) { 59 | // 接收到服务器的错误消息 60 | log.severe("Server error: " + ((ErrorMessage) message).getErrorMessage()); 61 | } else if (message instanceof RecallMessage) { 62 | // 接受到服务器的撤回消息,MVC-0不实现 63 | } else { 64 | // 不认识的消息 65 | log.severe("Unsupported message received: " + message.toString()); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp1/Mvp1TalkPresenter.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp1; 2 | 3 | import java.util.List; 4 | 5 | import lombok.AllArgsConstructor; 6 | import nju.androidchat.client.ClientMessage; 7 | 8 | @AllArgsConstructor 9 | public class Mvp1TalkPresenter implements Mvp1Contract.TalkPresenter { 10 | 11 | private Mvp1Contract.TalkModel mvp0TalkModel; 12 | private Mvp1Contract.TalkView iMvp0TalkView; 13 | 14 | private List clientMessages; 15 | 16 | @Override 17 | public void sendMessage(String content) { 18 | ClientMessage clientMessage = mvp0TalkModel.sendInformation(content); 19 | refreshMessageList(clientMessage); 20 | } 21 | 22 | @Override 23 | public void receiveMessage(ClientMessage clientMessage) { 24 | refreshMessageList(clientMessage); 25 | } 26 | 27 | @Override 28 | public String getUsername() { 29 | return mvp0TalkModel.getUsername(); 30 | } 31 | 32 | private void refreshMessageList(ClientMessage clientMessage) { 33 | clientMessages.add(clientMessage); 34 | iMvp0TalkView.showMessageList(clientMessages); 35 | } 36 | 37 | //撤回消息,Mvp1暂不实现 38 | @Override 39 | public void recallMessage(int index0) { 40 | 41 | } 42 | 43 | @Override 44 | public void start() { 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp2/BasePresenter.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp2; 2 | 3 | public interface BasePresenter { 4 | 5 | void start(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp2/BaseView.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp2; 2 | 3 | public interface BaseView { 4 | 5 | void setPresenter(T presenter); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp2/Mvp2Contract.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp2; 2 | 3 | import java.util.List; 4 | import java.util.UUID; 5 | 6 | import nju.androidchat.client.ClientMessage; 7 | 8 | public interface Mvp2Contract { 9 | interface TalkView extends BaseView { 10 | void showMessageList(List messages); 11 | } 12 | 13 | interface TalkPresenter extends BasePresenter { 14 | void sendMessage(String content); 15 | 16 | void receiveMessage(ClientMessage content); 17 | 18 | String getUsername(); 19 | 20 | void recallMessage(UUID messageId); 21 | } 22 | 23 | interface TalkModel { 24 | ClientMessage sendInformation(String message); 25 | 26 | String getUsername(); 27 | 28 | void recallMessage(UUID messageId); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp2/Mvp2TalkActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp2; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.view.KeyEvent; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | import android.widget.EditText; 9 | import android.widget.LinearLayout; 10 | import android.widget.TextView; 11 | import android.widget.Toast; 12 | 13 | import androidx.appcompat.app.AlertDialog; 14 | import androidx.appcompat.app.AppCompatActivity; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | import lombok.extern.java.Log; 21 | import nju.androidchat.client.ClientMessage; 22 | import nju.androidchat.client.R; 23 | import nju.androidchat.client.Utils; 24 | import nju.androidchat.client.component.ItemTextReceive; 25 | import nju.androidchat.client.component.ItemTextSend; 26 | import nju.androidchat.client.component.OnRecallMessageRequested; 27 | 28 | @Log 29 | public class Mvp2TalkActivity extends AppCompatActivity implements Mvp2Contract.TalkView, TextView.OnEditorActionListener, OnRecallMessageRequested { 30 | private Mvp2Contract.TalkPresenter presenter; 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_main); 36 | 37 | Mvp2TalkModel mvp2TalkModel = new Mvp2TalkModel(); 38 | 39 | // Create the presenter 40 | this.presenter = new Mvp2TalkPresenter(mvp2TalkModel, this, new ArrayList<>()); 41 | mvp2TalkModel.setIMvp0TalkPresenter(this.presenter); 42 | } 43 | 44 | @Override 45 | public void onResume() { 46 | super.onResume(); 47 | presenter.start(); 48 | } 49 | 50 | @Override 51 | public void showMessageList(List messages) { 52 | runOnUiThread(() -> { 53 | LinearLayout content = findViewById(R.id.chat_content); 54 | 55 | // 删除所有已有的ItemText 56 | content.removeAllViews(); 57 | 58 | // 增加ItemText 59 | for (ClientMessage message : messages) { 60 | String text = String.format("%s", message.getMessage()); 61 | // 如果是自己发的,增加ItemTextSend 62 | if (message.getSenderUsername().equals(this.presenter.getUsername())) { 63 | content.addView(new ItemTextSend(this, text, message.getMessageId(), this)); 64 | } else { 65 | content.addView(new ItemTextReceive(this, text, message.getMessageId())); 66 | } 67 | } 68 | 69 | Utils.scrollListToBottom(this); 70 | } 71 | ); 72 | } 73 | 74 | @Override 75 | public void setPresenter(Mvp2Contract.TalkPresenter presenter) { 76 | this.presenter = presenter; 77 | } 78 | 79 | @Override 80 | public boolean onTouchEvent(MotionEvent event) { 81 | if (null != this.getCurrentFocus()) { 82 | return hideKeyboard(); 83 | } 84 | return super.onTouchEvent(event); 85 | } 86 | 87 | private boolean hideKeyboard() { 88 | return Utils.hideKeyboard(this); 89 | } 90 | 91 | 92 | @Override 93 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 94 | if (Utils.send(actionId, event)) { 95 | hideKeyboard(); 96 | // 异步地让Controller处理事件 97 | sendText(); 98 | } 99 | return false; 100 | } 101 | 102 | private void sendText() { 103 | EditText text = findViewById(R.id.et_content); 104 | AsyncTask.execute(() -> { 105 | this.presenter.sendMessage(text.getText().toString()); 106 | }); 107 | } 108 | 109 | public void onBtnSendClicked(View v) { 110 | hideKeyboard(); 111 | sendText(); 112 | } 113 | 114 | // 当用户长按消息,并选择撤回消息时做什么 115 | @Override 116 | public void onRecallMessageRequested(UUID messageId) { 117 | this.presenter.recallMessage(messageId); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp2/Mvp2TalkModel.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp2; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import java.time.LocalDateTime; 6 | import java.util.UUID; 7 | 8 | import lombok.Setter; 9 | import lombok.extern.java.Log; 10 | import nju.androidchat.client.ClientMessage; 11 | import nju.androidchat.client.socket.MessageListener; 12 | import nju.androidchat.client.socket.SocketClient; 13 | import nju.androidchat.shared.message.ClientSendMessage; 14 | import nju.androidchat.shared.message.ErrorMessage; 15 | import nju.androidchat.shared.message.Message; 16 | import nju.androidchat.shared.message.RecallMessage; 17 | import nju.androidchat.shared.message.RecallRequestMessage; 18 | import nju.androidchat.shared.message.ServerSendMessage; 19 | 20 | @Log 21 | public class Mvp2TalkModel implements MessageListener, Mvp2Contract.TalkModel { 22 | 23 | private SocketClient client; 24 | 25 | @Setter 26 | private Mvp2Contract.TalkPresenter iMvp0TalkPresenter; 27 | 28 | public Mvp2TalkModel() { 29 | this.client = SocketClient.getClient(); 30 | // Model本身去注册Socket的消息接受事件 31 | client.setMessageListener(this); 32 | client.startListening(); 33 | } 34 | 35 | @Override 36 | public String getUsername() { 37 | return client.getUsername(); 38 | } 39 | 40 | @Override 41 | public void recallMessage(UUID messageId) { 42 | RecallRequestMessage recallRequestMessage = new RecallRequestMessage(messageId); 43 | AsyncTask.execute(() -> client.writeToServer(recallRequestMessage)); 44 | } 45 | 46 | @Override 47 | public ClientMessage sendInformation(String message) { 48 | //处理事件 49 | LocalDateTime now = LocalDateTime.now(); 50 | UUID uuid = UUID.randomUUID(); 51 | ClientMessage clientMessage = new ClientMessage(uuid, now, getUsername(), message); 52 | // 阻塞地把信息发送到服务器 53 | client.writeToServer(new ClientSendMessage(uuid, now, message)); 54 | return clientMessage; 55 | } 56 | 57 | @Override 58 | public void onMessageReceived(Message message) { 59 | if (message instanceof ServerSendMessage) { 60 | // 接受到其他设备发来的消息 61 | ServerSendMessage serverSendMessage = (ServerSendMessage) message; 62 | log.info(String.format("%s sent a message: %s", 63 | serverSendMessage.getSenderUsername(), 64 | serverSendMessage.getMessage() 65 | )); 66 | iMvp0TalkPresenter.receiveMessage(new ClientMessage(serverSendMessage)); 67 | } else if (message instanceof ErrorMessage) { 68 | // 接收到服务器的错误消息 69 | log.severe("Server error: " + ((ErrorMessage) message).getErrorMessage()); 70 | } else if (message instanceof RecallMessage) { 71 | // 接受到服务器的撤回消息,MVC-0不实现 72 | iMvp0TalkPresenter.recallMessage(((RecallMessage) message).getMessageId()); 73 | } else { 74 | // 不认识的消息 75 | log.severe("Unsupported message received: " + message.toString()); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvp2/Mvp2TalkPresenter.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvp2; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.UUID; 6 | 7 | import lombok.AllArgsConstructor; 8 | import nju.androidchat.client.ClientMessage; 9 | 10 | @AllArgsConstructor 11 | public class Mvp2TalkPresenter implements Mvp2Contract.TalkPresenter { 12 | 13 | private Mvp2Contract.TalkModel mvp2TalkModel; 14 | private Mvp2Contract.TalkView iMvp2TalkView; 15 | 16 | private List clientMessages; 17 | 18 | @Override 19 | public void sendMessage(String content) { 20 | ClientMessage clientMessage = mvp2TalkModel.sendInformation(content); 21 | refreshMessageList(clientMessage); 22 | } 23 | 24 | @Override 25 | public void receiveMessage(ClientMessage clientMessage) { 26 | refreshMessageList(clientMessage); 27 | } 28 | 29 | @Override 30 | public String getUsername() { 31 | return mvp2TalkModel.getUsername(); 32 | } 33 | 34 | private void refreshMessageList(ClientMessage clientMessage) { 35 | clientMessages.add(clientMessage); 36 | iMvp2TalkView.showMessageList(clientMessages); 37 | } 38 | 39 | //撤回消息,Mvp0暂不实现 40 | @Override 41 | public void recallMessage(UUID messageId) { 42 | // 操作界面 43 | List newMessages = new ArrayList<>(); 44 | for (ClientMessage clientMessage : clientMessages) { 45 | if (clientMessage.getMessageId().equals(messageId)) { 46 | newMessages.add(new ClientMessage(clientMessage.getMessageId(), clientMessage.getTime(), clientMessage.getSenderUsername(), "(已撤回)")); 47 | } else { 48 | newMessages.add(clientMessage); 49 | } 50 | } 51 | this.clientMessages = newMessages; 52 | this.iMvp2TalkView.showMessageList(newMessages); 53 | 54 | // 操作数据 55 | this.mvp2TalkModel.recallMessage(messageId); 56 | 57 | } 58 | 59 | @Override 60 | public void start() { 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm0/Mvvm0TalkActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm0; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.view.KeyEvent; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | import android.widget.TextView; 9 | 10 | import androidx.appcompat.app.AppCompatActivity; 11 | import androidx.databinding.DataBindingUtil; 12 | import androidx.databinding.ViewDataBinding; 13 | 14 | import lombok.extern.java.Log; 15 | import nju.androidchat.client.BR; 16 | import nju.androidchat.client.R; 17 | import nju.androidchat.client.Utils; 18 | import nju.androidchat.client.mvvm0.viewmodel.Mvvm0ViewModel; 19 | import nju.androidchat.client.mvvm0.viewmodel.UiOperator; 20 | 21 | @Log 22 | public class Mvvm0TalkActivity extends AppCompatActivity implements TextView.OnEditorActionListener, UiOperator { 23 | private Mvvm0ViewModel viewModel; 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | viewModel = new Mvvm0ViewModel(this); 29 | ViewDataBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main_mvvm); 30 | binding.setVariable(BR.viewModel, viewModel); 31 | } 32 | 33 | 34 | @Override 35 | public void onBackPressed() { 36 | AsyncTask.execute(() -> { 37 | viewModel.disconnect(); 38 | }); 39 | 40 | Utils.jumpToHome(this); 41 | } 42 | 43 | @Override 44 | public boolean onTouchEvent(MotionEvent event) { 45 | if (null != this.getCurrentFocus()) { 46 | log.info("not on focus"); 47 | return hideKeyboard(); 48 | } 49 | return super.onTouchEvent(event); 50 | } 51 | 52 | private boolean hideKeyboard() { 53 | return Utils.hideKeyboard(this); 54 | } 55 | 56 | 57 | private void sendText() { 58 | viewModel.sendMessage(); 59 | } 60 | 61 | public void onBtnSendClicked(View v) { 62 | hideKeyboard(); 63 | sendText(); 64 | } 65 | 66 | @Override 67 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 68 | if (Utils.send(actionId, event)) { 69 | hideKeyboard(); 70 | // 异步地让Controller处理事件 71 | sendText(); 72 | } 73 | return false; 74 | } 75 | 76 | @Override 77 | public void scrollListToBottom() { 78 | Utils.scrollListToBottom(this); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm0/model/ClientMessageObservable.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm0.model; 2 | 3 | import androidx.databinding.BaseObservable; 4 | import androidx.databinding.Bindable; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.UUID; 8 | 9 | import lombok.AllArgsConstructor; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import nju.androidchat.client.BR; 13 | import nju.androidchat.shared.message.ClientSendMessage; 14 | import nju.androidchat.shared.message.ServerSendMessage; 15 | 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | public class ClientMessageObservable extends BaseObservable { 19 | @Getter 20 | private UUID messageId; 21 | 22 | @Getter 23 | private LocalDateTime time; 24 | 25 | @Getter 26 | private String senderUsername; 27 | 28 | @Getter 29 | @Bindable 30 | private String message; 31 | 32 | @Getter 33 | private Direction direction; 34 | 35 | @Getter 36 | @Bindable 37 | private State state; 38 | 39 | @Getter 40 | private final String withdrawnMessage = "(已撤回)"; 41 | 42 | public void setMessage(String message) { 43 | this.message = message; 44 | notifyPropertyChanged(BR.message); 45 | } 46 | 47 | public void setState(State state) { 48 | this.state = state; 49 | notifyPropertyChanged(BR.state); 50 | } 51 | 52 | public ClientMessageObservable(ClientSendMessage clientSendMessage, String username) { 53 | direction = Direction.SEND; 54 | messageId = clientSendMessage.getMessageId(); 55 | time = clientSendMessage.getTime(); 56 | message = clientSendMessage.getMessage(); 57 | senderUsername = username; 58 | state = State.SENT; 59 | } 60 | 61 | public ClientMessageObservable(ServerSendMessage serverSendMessage) { 62 | direction = Direction.RECEIVE; 63 | messageId = serverSendMessage.getMessageId(); 64 | time = serverSendMessage.getTime(); 65 | message = serverSendMessage.getMessage(); 66 | senderUsername = serverSendMessage.getSenderUsername(); 67 | state = State.SENT; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm0/model/Direction.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm0.model; 2 | 3 | public enum Direction { 4 | SEND, RECEIVE 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm0/model/State.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm0.model; 2 | 3 | public enum State { 4 | SENT,WITHDRAWN,FAILED 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm0/viewmodel/ItemTextAdapters.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm0.viewmodel; 2 | 3 | import android.widget.LinearLayout; 4 | import android.widget.RelativeLayout; 5 | 6 | import androidx.databinding.BindingAdapter; 7 | 8 | import de.hdodenhof.circleimageview.CircleImageView; 9 | import nju.androidchat.client.R; 10 | import nju.androidchat.client.mvvm0.model.Direction; 11 | 12 | public class ItemTextAdapters { 13 | @BindingAdapter({"message_type"}) 14 | public static void setLayout(CircleImageView circleImageView, Direction direction) { 15 | if (direction.equals(Direction.SEND)) { 16 | circleImageView.setImageResource(R.mipmap.ic_head_default_right); 17 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) circleImageView.getLayoutParams(); 18 | params.addRule(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE); 19 | circleImageView.setLayoutParams(params); 20 | } else { 21 | circleImageView.setImageResource(R.mipmap.ic_head_default_left); 22 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) circleImageView.getLayoutParams(); 23 | params.addRule(RelativeLayout.ALIGN_PARENT_START, RelativeLayout.TRUE); 24 | circleImageView.setLayoutParams(params); 25 | } 26 | } 27 | 28 | @BindingAdapter({"layout_type"}) 29 | public static void setLayoutLinear(LinearLayout linearLayout, Direction direction) { 30 | if (direction.equals(Direction.SEND)) { 31 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) linearLayout.getLayoutParams(); 32 | params.addRule(RelativeLayout.START_OF, R.id.chat_item_header); 33 | params.addRule(RelativeLayout.ALIGN_PARENT_START); 34 | linearLayout.setLayoutParams(params); 35 | } else { 36 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) linearLayout.getLayoutParams(); 37 | params.addRule(RelativeLayout.END_OF, R.id.chat_item_header); 38 | linearLayout.setLayoutParams(params); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm0/viewmodel/Mvvm0ViewModel.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm0.viewmodel; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import androidx.databinding.BaseObservable; 6 | import androidx.databinding.Bindable; 7 | import androidx.databinding.ObservableArrayList; 8 | import androidx.databinding.ObservableList; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.UUID; 12 | 13 | import lombok.Getter; 14 | import lombok.extern.java.Log; 15 | import nju.androidchat.client.BR; 16 | import nju.androidchat.client.mvvm0.model.ClientMessageObservable; 17 | import nju.androidchat.client.socket.MessageListener; 18 | import nju.androidchat.client.socket.SocketClient; 19 | import nju.androidchat.shared.message.ClientSendMessage; 20 | import nju.androidchat.shared.message.ErrorMessage; 21 | import nju.androidchat.shared.message.Message; 22 | import nju.androidchat.shared.message.RecallMessage; 23 | import nju.androidchat.shared.message.ServerSendMessage; 24 | 25 | @Log 26 | public class Mvvm0ViewModel extends BaseObservable implements MessageListener { 27 | 28 | @Bindable 29 | @Getter 30 | private String messageToSend; 31 | @Getter 32 | private ObservableList messageObservableList; 33 | @Getter 34 | private SocketClient client; 35 | 36 | private UiOperator uiOperator; 37 | 38 | public void setMessageToSend(String messageToSend) { 39 | this.messageToSend = messageToSend; 40 | notifyPropertyChanged(BR.messageToSend); 41 | } 42 | 43 | public Mvvm0ViewModel(UiOperator uiOperator) { 44 | this.uiOperator = uiOperator; 45 | 46 | messageToSend = ""; 47 | messageObservableList = new ObservableArrayList<>(); 48 | client = SocketClient.getClient(); 49 | client.setMessageListener(this); 50 | client.startListening(); 51 | } 52 | 53 | private void updateList(ClientMessageObservable clientMessage) { 54 | uiOperator.runOnUiThread(() -> { 55 | messageObservableList.add(clientMessage); 56 | uiOperator.scrollListToBottom(); 57 | }); 58 | } 59 | 60 | public void sendMessage() { 61 | LocalDateTime now = LocalDateTime.now(); 62 | UUID uuid = UUID.randomUUID(); 63 | String senderUsername = client.getUsername(); 64 | ClientSendMessage clientSendMessage = new ClientSendMessage(uuid, now, messageToSend); 65 | ClientMessageObservable clientMessageObservable = new ClientMessageObservable(clientSendMessage, senderUsername); 66 | updateList(clientMessageObservable); 67 | 68 | AsyncTask.execute(() -> client.writeToServer(clientSendMessage)); 69 | } 70 | 71 | @Override 72 | public void onMessageReceived(Message message) { 73 | if (message instanceof ServerSendMessage) { 74 | // 接受到其他设备发来的消息 75 | // 增加到自己的消息列表里,并通知UI修改 76 | ServerSendMessage serverSendMessage = (ServerSendMessage) message; 77 | log.info(String.format("%s sent a messageToSend: %s", 78 | serverSendMessage.getSenderUsername(), 79 | serverSendMessage.getMessage() 80 | )); 81 | ClientMessageObservable clientMessage = new ClientMessageObservable(serverSendMessage); 82 | updateList(clientMessage); 83 | } else if (message instanceof ErrorMessage) { 84 | // 接收到服务器的错误消息 85 | log.severe("Server error: " + ((ErrorMessage) message).getErrorMessage()); 86 | 87 | } else if (message instanceof RecallMessage) { 88 | // 接受到服务器的撤回消息,MVVM-0不实现 89 | } else { 90 | // 不认识的消息 91 | log.severe("Unsupported messageToSend received: " + message.toString()); 92 | 93 | } 94 | } 95 | 96 | public void disconnect() { 97 | AsyncTask.execute(() -> client.disconnect()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm0/viewmodel/UiOperator.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm0.viewmodel; 2 | 3 | public interface UiOperator { 4 | void runOnUiThread(Runnable action); 5 | void scrollListToBottom(); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm2/Mvvm2TalkActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm2; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.view.KeyEvent; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | import android.widget.TextView; 9 | import android.widget.Toast; 10 | 11 | import androidx.appcompat.app.AlertDialog; 12 | import androidx.appcompat.app.AppCompatActivity; 13 | import androidx.databinding.DataBindingUtil; 14 | import androidx.databinding.ViewDataBinding; 15 | 16 | import lombok.extern.java.Log; 17 | import nju.androidchat.client.BR; 18 | import nju.androidchat.client.R; 19 | import nju.androidchat.client.Utils; 20 | import nju.androidchat.client.mvvm2.model.ClientMessageObservable; 21 | import nju.androidchat.client.mvvm2.model.Direction; 22 | import nju.androidchat.client.mvvm2.viewmodel.RecallHandler; 23 | import nju.androidchat.client.mvvm2.viewmodel.Mvvm2ViewModel; 24 | import nju.androidchat.client.mvvm2.viewmodel.UiOperator; 25 | 26 | @Log 27 | public class Mvvm2TalkActivity extends AppCompatActivity implements TextView.OnEditorActionListener, UiOperator { 28 | private Mvvm2ViewModel viewModel; 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | viewModel = new Mvvm2ViewModel(this); 34 | ViewDataBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main_mvvm2); 35 | binding.setVariable(BR.viewModel, viewModel); 36 | } 37 | 38 | 39 | @Override 40 | public void onBackPressed() { 41 | AsyncTask.execute(() -> { 42 | viewModel.disconnect(); 43 | }); 44 | 45 | Utils.jumpToHome(this); 46 | } 47 | 48 | @Override 49 | public boolean onTouchEvent(MotionEvent event) { 50 | if (null != this.getCurrentFocus()) { 51 | log.info("not on focus"); 52 | return hideKeyboard(); 53 | } 54 | return super.onTouchEvent(event); 55 | } 56 | 57 | private boolean hideKeyboard() { 58 | return Utils.hideKeyboard(this); 59 | } 60 | 61 | 62 | private void sendText() { 63 | viewModel.sendMessage(); 64 | } 65 | 66 | public void onBtnSendClicked(View v) { 67 | hideKeyboard(); 68 | sendText(); 69 | } 70 | 71 | @Override 72 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 73 | if (Utils.send(actionId, event)) { 74 | hideKeyboard(); 75 | // 异步地让Controller处理事件 76 | sendText(); 77 | } 78 | return false; 79 | } 80 | 81 | @Override 82 | public void scrollListToBottom() { 83 | Utils.scrollListToBottom(this); 84 | } 85 | 86 | @Override 87 | public void showRecallUi(ClientMessageObservable messageObservable, RecallHandler recallHandler) { 88 | if (messageObservable.getDirection().equals(Direction.SEND)) { 89 | // 通过AlertDialog.Builder这个类来实例化我们的一个AlertDialog的对象 90 | AlertDialog.Builder builder = new AlertDialog.Builder(this); 91 | // 设置Title的图标 92 | builder.setIcon(R.drawable.ic_launcher_background); 93 | // 设置Title的内容 94 | builder.setTitle(getString(R.string.recall_message_title)); 95 | // 设置Content来显示一个信息 96 | builder.setMessage(getString(R.string.recall_message_question)); 97 | // 设置一个PositiveButton 98 | builder.setPositiveButton(getString(R.string.recall_message_confirm), (dialog, which) -> { 99 | recallHandler.handleRecall(messageObservable); 100 | Toast.makeText(this, getString(R.string.after_recall_success), Toast.LENGTH_SHORT).show(); 101 | }); 102 | // 设置一个NegativeButton 103 | builder.setNegativeButton(getString(R.string.recall_message_cancel), (dialog, which) -> Toast.makeText(this, getString(R.string.after_recall_cancel), Toast.LENGTH_SHORT).show()); 104 | // 显示出该对话框 105 | builder.show(); 106 | } else { 107 | Toast.makeText(this, getString(R.string.can_not_recall), Toast.LENGTH_SHORT).show(); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm2/model/ClientMessageObservable.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm2.model; 2 | 3 | import androidx.databinding.BaseObservable; 4 | import androidx.databinding.Bindable; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.UUID; 8 | 9 | import lombok.AllArgsConstructor; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.extern.java.Log; 13 | import nju.androidchat.client.BR; 14 | import nju.androidchat.client.Utils; 15 | import nju.androidchat.client.mvvm2.viewmodel.LongClickListener; 16 | import nju.androidchat.shared.message.ClientSendMessage; 17 | import nju.androidchat.shared.message.ServerSendMessage; 18 | 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | @Log 22 | public class ClientMessageObservable extends BaseObservable { 23 | @Getter 24 | private UUID messageId; 25 | 26 | @Getter 27 | private LocalDateTime time; 28 | 29 | @Getter 30 | private String senderUsername; 31 | 32 | @Getter 33 | @Bindable 34 | private String message; 35 | 36 | @Getter 37 | private Direction direction; 38 | 39 | @Getter 40 | @Bindable 41 | private State state; 42 | 43 | public void setMessage(String message) { 44 | this.message = message; 45 | notifyPropertyChanged(BR.message); 46 | } 47 | 48 | public void setState(State state) { 49 | this.state = state; 50 | notifyPropertyChanged(BR.state); 51 | } 52 | 53 | public ClientMessageObservable(ClientSendMessage clientSendMessage, String username) { 54 | direction = Direction.SEND; 55 | messageId = clientSendMessage.getMessageId(); 56 | time = clientSendMessage.getTime(); 57 | message = clientSendMessage.getMessage(); 58 | senderUsername = username; 59 | state = State.SENT; 60 | } 61 | 62 | public ClientMessageObservable(ServerSendMessage serverSendMessage) { 63 | direction = Direction.RECEIVE; 64 | messageId = serverSendMessage.getMessageId(); 65 | time = serverSendMessage.getTime(); 66 | message = serverSendMessage.getMessage(); 67 | senderUsername = serverSendMessage.getSenderUsername(); 68 | state = State.SENT; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm2/model/Direction.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm2.model; 2 | 3 | public enum Direction { 4 | SEND, RECEIVE 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm2/model/State.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm2.model; 2 | 3 | public enum State { 4 | SENT,WITHDRAWN,FAILED 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm2/viewmodel/ItemTextAdapters.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm2.viewmodel; 2 | 3 | import android.widget.LinearLayout; 4 | import android.widget.RelativeLayout; 5 | 6 | import androidx.databinding.BindingAdapter; 7 | 8 | import de.hdodenhof.circleimageview.CircleImageView; 9 | import nju.androidchat.client.R; 10 | import nju.androidchat.client.mvvm2.model.Direction; 11 | 12 | public class ItemTextAdapters { 13 | @BindingAdapter({"message_type"}) 14 | public static void setLayout(CircleImageView circleImageView, Direction direction) { 15 | if (direction.equals(Direction.SEND)) { 16 | circleImageView.setImageResource(R.mipmap.ic_head_default_right); 17 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) circleImageView.getLayoutParams(); 18 | params.addRule(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE); 19 | circleImageView.setLayoutParams(params); 20 | } else { 21 | circleImageView.setImageResource(R.mipmap.ic_head_default_left); 22 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) circleImageView.getLayoutParams(); 23 | params.addRule(RelativeLayout.ALIGN_PARENT_START, RelativeLayout.TRUE); 24 | circleImageView.setLayoutParams(params); 25 | } 26 | } 27 | 28 | @BindingAdapter({"layout_type"}) 29 | public static void setLayoutLinear(LinearLayout linearLayout, Direction direction) { 30 | if (direction.equals(Direction.SEND)) { 31 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) linearLayout.getLayoutParams(); 32 | params.addRule(RelativeLayout.START_OF, R.id.chat_item_header); 33 | params.addRule(RelativeLayout.ALIGN_PARENT_START); 34 | linearLayout.setLayoutParams(params); 35 | } else { 36 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) linearLayout.getLayoutParams(); 37 | params.addRule(RelativeLayout.END_OF, R.id.chat_item_header); 38 | linearLayout.setLayoutParams(params); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm2/viewmodel/LongClickListener.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm2.viewmodel; 2 | 3 | import nju.androidchat.client.mvvm2.model.ClientMessageObservable; 4 | 5 | public interface LongClickListener { 6 | boolean onLongClick(ClientMessageObservable messageObservable); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm2/viewmodel/Mvvm2ViewModel.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm2.viewmodel; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import androidx.databinding.BaseObservable; 6 | import androidx.databinding.Bindable; 7 | import androidx.databinding.ObservableArrayList; 8 | import androidx.databinding.ObservableList; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.UUID; 12 | 13 | import lombok.Getter; 14 | import lombok.extern.java.Log; 15 | import nju.androidchat.client.BR; 16 | import nju.androidchat.client.R; 17 | import nju.androidchat.client.mvvm2.model.ClientMessageObservable; 18 | import nju.androidchat.client.mvvm2.model.State; 19 | import nju.androidchat.client.socket.MessageListener; 20 | import nju.androidchat.client.socket.SocketClient; 21 | import nju.androidchat.shared.message.ClientSendMessage; 22 | import nju.androidchat.shared.message.ErrorMessage; 23 | import nju.androidchat.shared.message.Message; 24 | import nju.androidchat.shared.message.RecallMessage; 25 | import nju.androidchat.shared.message.RecallRequestMessage; 26 | import nju.androidchat.shared.message.ServerSendMessage; 27 | 28 | @Log 29 | public class Mvvm2ViewModel extends BaseObservable implements MessageListener, RecallHandler, LongClickListener { 30 | 31 | @Bindable 32 | @Getter 33 | private String messageToSend; 34 | @Getter 35 | private ObservableList messageObservableList; 36 | @Getter 37 | private SocketClient client; 38 | 39 | private UiOperator uiOperator; 40 | 41 | public void setMessageToSend(String messageToSend) { 42 | this.messageToSend = messageToSend; 43 | notifyPropertyChanged(BR.messageToSend); 44 | } 45 | 46 | public Mvvm2ViewModel(UiOperator uiOperator) { 47 | this.uiOperator = uiOperator; 48 | messageToSend = ""; 49 | messageObservableList = new ObservableArrayList<>(); 50 | client = SocketClient.getClient(); 51 | client.setMessageListener(this); 52 | client.startListening(); 53 | } 54 | 55 | private void updateList(ClientMessageObservable clientMessage) { 56 | uiOperator.runOnUiThread(() -> { 57 | messageObservableList.add(clientMessage); 58 | uiOperator.scrollListToBottom(); 59 | }); 60 | } 61 | 62 | private void recallMessage(UUID uuid) { 63 | uiOperator.runOnUiThread(() -> { 64 | messageObservableList.stream() 65 | .filter(message -> message.getMessageId().equals(uuid)) 66 | .findAny() 67 | .ifPresent(message -> { 68 | // 修改数据的状态即可 69 | message.setState(State.WITHDRAWN); 70 | }); 71 | }); 72 | } 73 | 74 | 75 | public void sendMessage() { 76 | LocalDateTime now = LocalDateTime.now(); 77 | UUID uuid = UUID.randomUUID(); 78 | String senderUsername = client.getUsername(); 79 | ClientSendMessage clientSendMessage = new ClientSendMessage(uuid, now, messageToSend); 80 | ClientMessageObservable clientMessageObservable = new ClientMessageObservable(clientSendMessage, senderUsername); 81 | updateList(clientMessageObservable); 82 | 83 | AsyncTask.execute(() -> client.writeToServer(clientSendMessage)); 84 | } 85 | 86 | private void sendRecallRequest(UUID uuid) { 87 | RecallRequestMessage recallRequestMessage = new RecallRequestMessage(uuid); 88 | AsyncTask.execute(() -> client.writeToServer(recallRequestMessage)); 89 | } 90 | 91 | @Override 92 | public void onMessageReceived(Message message) { 93 | if (message instanceof ServerSendMessage) { 94 | // 接受到其他设备发来的消息 95 | // 增加到自己的消息列表里,并通知UI修改 96 | ServerSendMessage serverSendMessage = (ServerSendMessage) message; 97 | log.info(String.format("%s sent a messageToSend: %s", 98 | serverSendMessage.getSenderUsername(), 99 | serverSendMessage.getMessage() 100 | )); 101 | ClientMessageObservable clientMessage = new ClientMessageObservable(serverSendMessage); 102 | updateList(clientMessage); 103 | } else if (message instanceof ErrorMessage) { 104 | // 接收到服务器的错误消息 105 | log.severe("Server error: " + ((ErrorMessage) message).getErrorMessage()); 106 | 107 | } else if (message instanceof RecallMessage) { 108 | RecallMessage recallMessage = (RecallMessage) message; 109 | UUID uuid = recallMessage.getMessageId(); 110 | log.info(String.format("A recallMessage received: %s", 111 | uuid 112 | )); 113 | recallMessage(uuid); 114 | } else { 115 | // 不认识的消息 116 | log.severe("Unsupported messageToSend received: " + message.toString()); 117 | 118 | } 119 | } 120 | 121 | @Override 122 | public boolean onLongClick(ClientMessageObservable messageObservable) { 123 | uiOperator.showRecallUi(messageObservable, this); 124 | return true; 125 | } 126 | 127 | @Override 128 | public void handleRecall(ClientMessageObservable messageObservable) { 129 | messageObservable.setState(State.WITHDRAWN); 130 | UUID uuid = messageObservable.getMessageId(); 131 | sendRecallRequest(uuid); 132 | } 133 | 134 | public void disconnect() { 135 | AsyncTask.execute(() -> client.disconnect()); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm2/viewmodel/RecallHandler.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm2.viewmodel; 2 | 3 | import nju.androidchat.client.mvvm2.model.ClientMessageObservable; 4 | 5 | public interface RecallHandler { 6 | void handleRecall(ClientMessageObservable messageObservable); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm2/viewmodel/UiOperator.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm2.viewmodel; 2 | 3 | 4 | import nju.androidchat.client.mvvm2.model.ClientMessageObservable; 5 | 6 | public interface UiOperator{ 7 | void runOnUiThread(Runnable action); 8 | void scrollListToBottom(); 9 | void showRecallUi(ClientMessageObservable messageObservable, RecallHandler recallHandler); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm3/Mvvm3TalkActivity.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm3; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import android.view.KeyEvent; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | import android.widget.TextView; 9 | import android.widget.Toast; 10 | 11 | import androidx.appcompat.app.AppCompatActivity; 12 | import androidx.databinding.DataBindingUtil; 13 | import androidx.databinding.ViewDataBinding; 14 | 15 | import lombok.extern.java.Log; 16 | import nju.androidchat.client.BR; 17 | import nju.androidchat.client.R; 18 | import nju.androidchat.client.Utils; 19 | import nju.androidchat.client.mvvm3.viewmodel.Mvvm3ViewModel; 20 | import nju.androidchat.client.mvvm3.viewmodel.UiOperator; 21 | 22 | @Log 23 | public class Mvvm3TalkActivity extends AppCompatActivity implements TextView.OnEditorActionListener, UiOperator { 24 | private Mvvm3ViewModel viewModel; 25 | 26 | @Override 27 | protected void onCreate(Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | setContentView(R.layout.activity_main_mvvm3); 30 | 31 | viewModel = new Mvvm3ViewModel(this); 32 | 33 | ViewDataBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main_mvvm3); 34 | binding.setVariable(BR.viewModel, viewModel); 35 | } 36 | 37 | 38 | @Override 39 | public void onBackPressed() { 40 | AsyncTask.execute(() -> { 41 | viewModel.disconnect(); 42 | }); 43 | 44 | Utils.jumpToHome(this); 45 | } 46 | 47 | @Override 48 | public boolean onTouchEvent(MotionEvent event) { 49 | if (null != this.getCurrentFocus()) { 50 | log.info("not on focus"); 51 | return hideKeyboard(); 52 | } 53 | return super.onTouchEvent(event); 54 | } 55 | 56 | private boolean hideKeyboard() { 57 | return Utils.hideKeyboard(this); 58 | } 59 | 60 | 61 | private void sendText() { 62 | viewModel.sendMessage(); 63 | } 64 | 65 | public void onBtnSendClicked(View v) { 66 | hideKeyboard(); 67 | sendText(); 68 | } 69 | 70 | @Override 71 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 72 | if (Utils.send(actionId, event)) { 73 | hideKeyboard(); 74 | // 异步地让Controller处理事件 75 | sendText(); 76 | } 77 | return false; 78 | } 79 | 80 | @Override 81 | public void scrollListToBottom() { 82 | Utils.scrollListToBottom(this); 83 | } 84 | 85 | @Override 86 | public void sendBadWordNotice() { 87 | Toast.makeText(this, getString(R.string.bad_word_notice), Toast.LENGTH_SHORT).show(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm3/model/ClientMessageObservable.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm3.model; 2 | 3 | import androidx.databinding.BaseObservable; 4 | import androidx.databinding.Bindable; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.UUID; 8 | 9 | import lombok.AllArgsConstructor; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import nju.androidchat.client.BR; 13 | import nju.androidchat.shared.message.ClientSendMessage; 14 | import nju.androidchat.shared.message.ServerSendMessage; 15 | 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | public class ClientMessageObservable extends BaseObservable { 19 | @Getter 20 | private UUID messageId; 21 | 22 | @Getter 23 | private LocalDateTime time; 24 | 25 | @Getter 26 | private String senderUsername; 27 | 28 | @Getter 29 | @Bindable 30 | private String message; 31 | 32 | @Getter 33 | private Direction direction; 34 | 35 | @Getter 36 | @Bindable 37 | private State state; 38 | 39 | @Getter 40 | private final String withdrawnMessage = "(已撤回)"; 41 | 42 | public void setMessage(String message) { 43 | this.message = message; 44 | notifyPropertyChanged(BR.message); 45 | } 46 | 47 | public void setState(State state) { 48 | this.state = state; 49 | notifyPropertyChanged(BR.state); 50 | } 51 | 52 | public boolean isSend() { 53 | return direction.equals(Direction.SEND); 54 | } 55 | 56 | public boolean isWithdrawn() { 57 | return state.equals(State.WITHDRAWN); 58 | } 59 | 60 | public ClientMessageObservable(ClientSendMessage clientSendMessage, String username) { 61 | direction = Direction.SEND; 62 | messageId = clientSendMessage.getMessageId(); 63 | time = clientSendMessage.getTime(); 64 | message = clientSendMessage.getMessage(); 65 | senderUsername = username; 66 | state = State.SENT; 67 | } 68 | 69 | public ClientMessageObservable(ServerSendMessage serverSendMessage) { 70 | direction = Direction.RECEIVE; 71 | messageId = serverSendMessage.getMessageId(); 72 | time = serverSendMessage.getTime(); 73 | message = serverSendMessage.getMessage(); 74 | senderUsername = serverSendMessage.getSenderUsername(); 75 | state = State.SENT; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm3/model/Direction.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm3.model; 2 | 3 | public enum Direction { 4 | SEND, RECEIVE 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm3/model/State.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm3.model; 2 | 3 | public enum State { 4 | SENT,WITHDRAWN,FAILED 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm3/viewmodel/CustomizedAdapters.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm3.viewmodel; 2 | 3 | import android.widget.LinearLayout; 4 | import android.widget.RelativeLayout; 5 | 6 | import androidx.databinding.BindingAdapter; 7 | 8 | import de.hdodenhof.circleimageview.CircleImageView; 9 | import nju.androidchat.client.R; 10 | import nju.androidchat.client.mvvm3.model.Direction; 11 | 12 | public class CustomizedAdapters { 13 | @BindingAdapter({"message_type"}) 14 | public static void setLayout(CircleImageView circleImageView, Direction direction) { 15 | if (direction.equals(Direction.SEND)) { 16 | circleImageView.setImageResource(R.mipmap.ic_head_default_right); 17 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) circleImageView.getLayoutParams(); 18 | params.addRule(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE); 19 | circleImageView.setLayoutParams(params); 20 | } else { 21 | circleImageView.setImageResource(R.mipmap.ic_head_default_left); 22 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) circleImageView.getLayoutParams(); 23 | params.addRule(RelativeLayout.ALIGN_PARENT_START, RelativeLayout.TRUE); 24 | circleImageView.setLayoutParams(params); 25 | } 26 | } 27 | 28 | @BindingAdapter({"layout_type"}) 29 | public static void setLayoutLinear(LinearLayout linearLayout, Direction direction) { 30 | if (direction.equals(Direction.SEND)) { 31 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) linearLayout.getLayoutParams(); 32 | params.addRule(RelativeLayout.START_OF, R.id.chat_item_header); 33 | params.addRule(RelativeLayout.ALIGN_PARENT_START); 34 | linearLayout.setLayoutParams(params); 35 | } else { 36 | RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) linearLayout.getLayoutParams(); 37 | params.addRule(RelativeLayout.END_OF, R.id.chat_item_header); 38 | linearLayout.setLayoutParams(params); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm3/viewmodel/Mvvm3ViewModel.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm3.viewmodel; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import androidx.databinding.BaseObservable; 6 | import androidx.databinding.Bindable; 7 | import androidx.databinding.ObservableArrayList; 8 | import androidx.databinding.ObservableList; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.UUID; 12 | 13 | import lombok.Getter; 14 | import lombok.extern.java.Log; 15 | import nju.androidchat.client.BR; 16 | import nju.androidchat.client.Utils; 17 | import nju.androidchat.client.mvvm3.model.ClientMessageObservable; 18 | import nju.androidchat.client.socket.MessageListener; 19 | import nju.androidchat.client.socket.SocketClient; 20 | import nju.androidchat.shared.message.ClientSendMessage; 21 | import nju.androidchat.shared.message.ErrorMessage; 22 | import nju.androidchat.shared.message.Message; 23 | import nju.androidchat.shared.message.RecallMessage; 24 | import nju.androidchat.shared.message.ServerSendMessage; 25 | 26 | @Log 27 | public class Mvvm3ViewModel extends BaseObservable implements MessageListener { 28 | 29 | @Bindable 30 | @Getter 31 | private String messageToSend; 32 | @Getter 33 | private ObservableList messageObservableList; 34 | @Getter 35 | private SocketClient client; 36 | private UiOperator uiOperator; 37 | 38 | public void setMessageToSend(String messageToSend) { 39 | this.messageToSend = messageToSend; 40 | notifyPropertyChanged(BR.messageToSend); 41 | } 42 | 43 | public Mvvm3ViewModel(UiOperator uiOperator) { 44 | this.uiOperator = uiOperator; 45 | messageToSend = ""; 46 | messageObservableList = new ObservableArrayList<>(); 47 | client = SocketClient.getClient(); 48 | client.setMessageListener(this); 49 | client.startListening(); 50 | } 51 | 52 | private void updateList(ClientMessageObservable clientMessage) { 53 | uiOperator.runOnUiThread(() -> { 54 | messageObservableList.add(clientMessage); 55 | uiOperator.scrollListToBottom(); 56 | }); 57 | } 58 | 59 | public void sendMessage() { 60 | if (Utils.containsBadWords(messageToSend)) { 61 | uiOperator.sendBadWordNotice(); 62 | } else { 63 | LocalDateTime now = LocalDateTime.now(); 64 | UUID uuid = UUID.randomUUID(); 65 | String senderUsername = client.getUsername(); 66 | ClientSendMessage clientSendMessage = new ClientSendMessage(uuid, now, messageToSend); 67 | ClientMessageObservable clientMessageObservable = new ClientMessageObservable(clientSendMessage, senderUsername); 68 | updateList(clientMessageObservable); 69 | AsyncTask.execute(() -> client.writeToServer(clientSendMessage)); 70 | } 71 | } 72 | 73 | @Override 74 | public void onMessageReceived(Message message) { 75 | if (message instanceof ServerSendMessage) { 76 | // 接受到其他设备发来的消息 77 | // 增加到自己的消息列表里,并通知UI修改 78 | ServerSendMessage serverSendMessage = (ServerSendMessage) message; 79 | log.info(String.format("%s sent a messageToSend: %s", 80 | serverSendMessage.getSenderUsername(), 81 | serverSendMessage.getMessage() 82 | )); 83 | ClientMessageObservable clientMessage = new ClientMessageObservable(serverSendMessage); 84 | updateList(clientMessage); 85 | } else if (message instanceof ErrorMessage) { 86 | // 接收到服务器的错误消息 87 | log.severe("Server error: " + ((ErrorMessage) message).getErrorMessage()); 88 | 89 | } else if (message instanceof RecallMessage) { 90 | // 接受到服务器的撤回消息,MVVM-0不实现 91 | } else { 92 | // 不认识的消息 93 | log.severe("Unsupported messageToSend received: " + message.toString()); 94 | 95 | } 96 | } 97 | 98 | public void disconnect() { 99 | AsyncTask.execute(() -> client.disconnect()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/mvvm3/viewmodel/UiOperator.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.mvvm3.viewmodel; 2 | 3 | public interface UiOperator { 4 | void runOnUiThread(Runnable action); 5 | void scrollListToBottom(); 6 | void sendBadWordNotice(); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/socket/MessageListener.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.socket; 2 | 3 | import nju.androidchat.shared.message.Message; 4 | 5 | public interface MessageListener { 6 | void onMessageReceived(Message message); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/nju/androidchat/client/socket/SocketClient.java: -------------------------------------------------------------------------------- 1 | package nju.androidchat.client.socket; 2 | 3 | 4 | import java.io.Closeable; 5 | import java.io.IOException; 6 | import java.io.ObjectInputStream; 7 | import java.io.ObjectOutputStream; 8 | import java.net.Socket; 9 | 10 | import lombok.AllArgsConstructor; 11 | import lombok.Getter; 12 | import lombok.Setter; 13 | import lombok.SneakyThrows; 14 | import lombok.extern.java.Log; 15 | import nju.androidchat.client.MainActivity; 16 | import nju.androidchat.shared.Shared; 17 | import nju.androidchat.shared.message.DisconnectMessage; 18 | import nju.androidchat.shared.message.ErrorMessage; 19 | import nju.androidchat.shared.message.LoginRequestMessage; 20 | import nju.androidchat.shared.message.LoginResponseMessage; 21 | import nju.androidchat.shared.message.Message; 22 | 23 | /** 24 | * 看一下服务器的ConnectionHandler的写法 25 | */ 26 | @Log 27 | public class SocketClient implements Closeable, Runnable { 28 | 29 | private Thread thread; 30 | 31 | @Getter 32 | private String username; 33 | 34 | private Socket socket; 35 | 36 | @Getter 37 | private volatile boolean terminate = false; 38 | 39 | private @Setter 40 | MessageListener messageListener; 41 | 42 | public final static String SERVER_ADDRESS = "10.0.2.2"; 43 | 44 | // socket的输入和输出流 45 | // 在开始操作之前初始化这两个field 46 | /* 47 | this.in = new ObjectInputStream(socket.getInputStream()); 48 | this.out = new ObjectOutputStream(socket.getOutputStream()); 49 | */ 50 | private ObjectInputStream in; 51 | private ObjectOutputStream out; 52 | 53 | private @Getter 54 | static SocketClient client; 55 | 56 | 57 | private SocketClient(String username, Socket socket) throws IOException { 58 | this.username = username; 59 | this.socket = socket; 60 | 61 | this.out = new ObjectOutputStream(socket.getOutputStream()); 62 | this.in = new ObjectInputStream(socket.getInputStream()); 63 | } 64 | 65 | @SneakyThrows 66 | public static String connect(String username, String target) { 67 | 68 | String[] splitted = target.split(":"); 69 | 70 | try { 71 | if (client != null) { 72 | client.close(); 73 | } 74 | Socket socket = new Socket(splitted[0], Integer.parseInt(splitted[1])); 75 | SocketClient client = new SocketClient(username, socket); 76 | log.info("Socket connection established."); 77 | 78 | // send login request 79 | log.info("Sending LoginRequestMessage"); 80 | client.writeToServer(new LoginRequestMessage(username)); 81 | Message message = client.readFromServer(); 82 | if (message instanceof LoginResponseMessage) { 83 | LoginResponseMessage loginResponseMessage = (LoginResponseMessage) message; 84 | log.info("LoginResponseMessage received. Login successful"); 85 | if (loginResponseMessage.getLoggedInUsername().equals(username)) { 86 | SocketClient.client = client; 87 | return "SUCCESS"; 88 | } 89 | } else if (message instanceof ErrorMessage) { 90 | return ((ErrorMessage) message).getErrorMessage(); 91 | } 92 | } catch (Exception e) { 93 | e.printStackTrace(); 94 | log.severe(e.toString()); 95 | if (client != null) { 96 | client.close(); 97 | } 98 | return e.toString(); 99 | } 100 | return ""; 101 | } 102 | 103 | @SneakyThrows 104 | public static String connect(String username) { 105 | 106 | return connect(username, SocketClient.SERVER_ADDRESS + ":" + Shared.SERVER_PORT); 107 | } 108 | 109 | 110 | @SneakyThrows 111 | public static void disconnectCurrent() { 112 | client.disconnect(); 113 | } 114 | 115 | public void startListening() { 116 | client.thread = new Thread(client); 117 | client.thread.start(); 118 | } 119 | 120 | @Override 121 | public void run() { 122 | try { 123 | while (!terminate) { 124 | Message message = readFromServer(); 125 | if (messageListener != null) { 126 | messageListener.onMessageReceived(message); 127 | } 128 | } 129 | } catch (Exception e) { 130 | log.severe("[Client] Exception occurred: " + e.toString()); 131 | } 132 | 133 | } 134 | 135 | /** 136 | * 向服务器发送Message 137 | */ 138 | @SneakyThrows 139 | public void writeToServer(Message message) { 140 | out.writeObject(message); 141 | } 142 | 143 | 144 | @SneakyThrows 145 | // 从服务端读取信息,当没有读取到消息的时候阻塞 146 | // 直接开个线程循环调用这个方法 147 | // 看server.ConnectionHandler.run 148 | public Message readFromServer() throws IOException { 149 | return (Message) in.readObject(); 150 | } 151 | 152 | /** 153 | * 通知服务器关掉连接 154 | * 不需要等待回复! 155 | */ 156 | public void disconnect() { 157 | writeToServer(new DisconnectMessage()); 158 | } 159 | 160 | // 结束的时候记得调用这个方法 161 | // 通知服务器关掉连接并关掉资源 162 | @Override 163 | public void close() throws IOException { 164 | this.terminate = true; 165 | disconnect(); 166 | in.close(); 167 | out.close(); 168 | socket.close(); 169 | } 170 | 171 | 172 | } 173 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/message_text_receive.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/drawable-xhdpi/message_text_receive.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/message_text_send.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/drawable-xhdpi/message_text_send.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_camera.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_gallery.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_manage.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_send.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_slideshow.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/message_shap_chat_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 19 | 24 | 25 | 26 | 35 | 36 | 39 | 40 |