├── .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 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Server.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Start_all.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/app1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/app2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
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 |
47 |
48 |
56 |
57 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main_mvvm.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
25 |
33 |
34 |
35 |
36 |
45 |
46 |
49 |
50 |
55 |
56 |
57 |
65 |
66 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main_mvvm2.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
25 |
34 |
35 |
36 |
37 |
46 |
47 |
50 |
51 |
56 |
57 |
58 |
66 |
67 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main_mvvm3.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
25 |
33 |
34 |
35 |
36 |
45 |
46 |
49 |
50 |
55 |
56 |
57 |
65 |
66 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main_with_backup_btn.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
19 |
24 |
25 |
26 |
35 |
36 |
41 |
48 |
55 |
56 |
64 |
65 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/backup.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
21 |
22 |
28 |
29 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/header_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_text_mvvm.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
19 |
20 |
28 |
29 |
30 |
36 |
37 |
42 |
43 |
51 |
52 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_text_mvvm2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
19 |
20 |
21 |
28 |
29 |
37 |
38 |
39 |
45 |
46 |
51 |
52 |
60 |
61 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_text_mvvm3.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
19 |
20 |
28 |
29 |
30 |
36 |
37 |
42 |
43 |
51 |
52 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_text_receive.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
20 |
26 |
27 |
33 |
34 |
42 |
43 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_text_send.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
25 |
26 |
32 |
33 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/login_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
14 |
15 |
23 |
24 |
31 |
32 |
37 |
38 |
48 |
49 |
50 |
55 |
56 |
61 |
62 |
75 |
76 |
77 |
78 |
84 |
85 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/activity_main_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_head_default_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-xhdpi/ic_head_default_left.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_head_default_right.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-xhdpi/ic_head_default_right.jpg
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 8dp
6 | 176dp
7 | 16dp
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AndroidChatIn4Patterns
3 | Open navigation drawer
4 | Close navigation drawer
5 | Android Studio
6 | android.studio@android.com
7 | Navigation header
8 | Settings
9 |
10 | Home
11 | Gallery
12 | Slideshow
13 | Tools
14 | Share
15 | Send
16 |
17 | 连接到服务器
18 | 服务器IP
19 | 用户名
20 | 连接
21 | 发送
22 | 进入备份界面
23 | (已撤回)
24 | 撤回消息提示
25 | 确定撤回消息吗
26 | 确定
27 | 取消
28 | 撤回消息成功
29 | 已取消撤回操作
30 | 不是您发出的消息,不能撤回
31 | 您发送的消息包含脏话,已屏蔽
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/test/java/nju/androidchat/client/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.client;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | jcenter()
7 |
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.4.1'
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 |
22 | }
23 |
24 | tasks.withType(JavaCompile) {
25 | configure(options) {
26 | options.encoding = 'UTF-8'
27 | }
28 | }
29 | }
30 |
31 | task clean(type: Delete) {
32 | delete rootProject.buildDir
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/docs/img/client-chat-main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/client-chat-main.png
--------------------------------------------------------------------------------
/docs/img/client-input-username.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/client-input-username.png
--------------------------------------------------------------------------------
/docs/img/differences.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/differences.png
--------------------------------------------------------------------------------
/docs/img/extfunc1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/extfunc1.gif
--------------------------------------------------------------------------------
/docs/img/extfunc2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/extfunc2.gif
--------------------------------------------------------------------------------
/docs/img/extfunc3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/extfunc3.gif
--------------------------------------------------------------------------------
/docs/img/extfunc4.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/extfunc4.gif
--------------------------------------------------------------------------------
/docs/img/frp-badwords.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/frp-badwords.png
--------------------------------------------------------------------------------
/docs/img/frp-message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/frp-message.png
--------------------------------------------------------------------------------
/docs/img/frp-throttle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/frp-throttle.png
--------------------------------------------------------------------------------
/docs/img/frp1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/frp1.png
--------------------------------------------------------------------------------
/docs/img/mvc-backup-dataflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/mvc-backup-dataflow.png
--------------------------------------------------------------------------------
/docs/img/mvc-send-dataflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/mvc-send-dataflow.png
--------------------------------------------------------------------------------
/docs/img/mvc1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/mvc1.png
--------------------------------------------------------------------------------
/docs/img/mvp-backup-dataflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/mvp-backup-dataflow.png
--------------------------------------------------------------------------------
/docs/img/mvp-return-dataflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/mvp-return-dataflow.png
--------------------------------------------------------------------------------
/docs/img/mvp1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/mvp1.png
--------------------------------------------------------------------------------
/docs/img/mvvm-return-dataflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/mvvm-return-dataflow.png
--------------------------------------------------------------------------------
/docs/img/mvvm1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/mvvm1.png
--------------------------------------------------------------------------------
/docs/img/server-launched.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/docs/img/server-launched.png
--------------------------------------------------------------------------------
/docs/合作规范.md:
--------------------------------------------------------------------------------
1 | # 合作规范
2 |
3 | 每个人在master目前的最新之下创建自己的分支,在自己的分支中工作,工作完成后将分支合并到master中去。
--------------------------------------------------------------------------------
/docs/客户端和服务器端通信.md:
--------------------------------------------------------------------------------
1 | # 客户端和服务器端通信
2 |
3 | 客户端和服务器端通过Socket建立连接,通过序列化传输Shared项目中定义的**Message对象**进行通信。
4 |
5 | 要定义自己的Message类,需要继承Message基类,并保证所有字段都是可序列化的。
6 |
7 | # 通信机制(非常重要!)
8 |
9 | 不同于HTTP连接的**一请求对应一响应**,在Socket连接中通信是**双向**的。这也是说,**请求和响应没有对应关系**!请求流和响应流是两个单独的流,在代码中体现为`ObjectInputStream`为请求流(即发送到本端的流),`ObjectOutputStream`为相应流(即本端用于发送信息给对方的流)。
10 |
11 | 所以通信机制也有所变化。例如说,在网站的登录中,一般是客户端向服务器端发送登录请求,然后**等待**服务器端的响应。但是在Socket中,由于请求和响应没有对应关系,所以**登录请求操作**和**登录完成操作**也是分开的:在按下按钮后,客户端直接向自己的ObjectOutputStream流中写入一个LoginRequestMessage,然后流程即结束。那什么时候登录完成呢?当客户端接受到一个LoginResponseMessage,即说明登录完成。
12 |
13 | 切记在Socket的通信中,没有等待请求的概念。
14 |
15 | # 已有的Message类的说明
16 |
17 | | Message | 方向 | 说明 |
18 | | --- | -- | -- |
19 | | `LoginRequestMessage` | 客户端到服务器 | 客户端登录请求,包含username字段。客户端在建立与服务器的Socket连接后,此连接的**第一条消息必须是这个类型的消息**,以使服务器记录本客户端的消息。若服务器接受到某个socket的第一条消息不是这个类型,将会报错并断开连接 |
20 | | `LoginResponseMessage` | 服务器到客户端 | 服务器在接受到`LoginRequestMessage`后,将会对客户端发送此类型消息,表示此客户端登录成功。此消息中包含了username,以降低编程难度。|
21 | | `ClientSendMessage` | 客户端到服务器 | 客户端发送信息。服务器接受到此消息后,将会向所有其他客户端发送·ServerSendMessage·消息 |
22 | | `ServerSendMessage` | 服务器到客户端 | 见`ClientSendMessage` |
23 | | `RecallRequestMessage` | 客户端到服务器 | 客户端要求撤回某个消息,包含消息的ID。服务器接受到此消息后,将会向所有其他客户端发送`RecallMessage`消息。懒得做检查是不是发出者了。 |
24 | | `RecallMessage` | 服务器到客户端 | 见`RecallRequestMessage` |
25 | | `DisconnectMessage` | 客户端到服务器 | 客户端断开连接时,应该调用这个方法告诉服务器自己已经断开连接。|
26 | | `ErrorMessage` | 服务器到客户端 | 服务器发生错误时将给客户端发送此消息,包含错误信息,目前包括**登录名称冲突**、**第一条消息不是LoginRequestMessage**。 |
27 |
--------------------------------------------------------------------------------
/docs/扩展需求开发文档.md:
--------------------------------------------------------------------------------
1 | # 扩展需求开发说明
2 |
3 | ## 通用
4 |
5 | - 复制已有的代码到另外一个包,并以文档中的序号命名包和类。例如,实现功能4的FRP代码,应该存在放`frp4`包中,并命名为`Frp4TalkActivity`。
6 |
7 | - 要修改登录后进入哪个窗口,修改app/assets/config.properties文件中chat_activity为对应的类名。
8 |
9 | ## 公用部分
10 |
11 | 扩展需求中,需求1,2,3需要有公用部分,需求4请培林自己设计即可。我已经完成了这些公用部分,具体说明如下:
12 |
13 | ### 需求1:消息记录云备份(MVC-1, MVP-1注意)
14 |
15 | #### 消息记录备份界面
16 |
17 | 消息记录备份功能是一个单独的activity,需要有自己的M,V,C或者M,V,P。
18 |
19 | 消息记录云备份的功能应该为
20 | 1. 初始界面:上次备份时间显示从未更新
21 | 2. 点击界面中的按钮,调用`Thread.sleep(3000);`模仿耗时的网络请求,之后,修改备份时间为当前时间。
22 |
23 | MVC-1中的问题为:点击按钮后,界面无任何响应(因为此时Model并未通知UI改变);直到3000ms后,Model处理完成,才通知界面刷新。
24 |
25 | MVP-1应该改进的地方为:点击按钮后,Presenter修改界面的按钮的属性,使其**无法点击,并显示文本“正在备份”**;Model处理完成后,Presenter将其改为正常状态,并修改界面的上次备份时间。
26 |
27 | MVC-1的实现参考mvc1包里的Backup的三个文件。
28 |
29 | #### 切换到消息记录备份界面
30 |
31 | 在`activity_main.xml`的基础上,复制了一份`activity_main_with_backup_btn.xml`文件,其区别是在发送按钮上面增加了一个备份按钮。
32 |
33 | 按下此按钮,应该切换到Backup对应的Activity。
34 |
35 | 使用`activity_main_with_backup_btn.xml`实现mvp1的Activity。
36 | ```java
37 | public void onBtnToBackupClicked(View view) {
38 | Utils.jumpTo(this, Mvc1BackupActivity.class);
39 | }
40 | ```
41 | ### 需求2:撤回消息(MVP-2, MVVM-2注意)
42 |
43 | #### 用户如何撤回消息
44 |
45 | 我已重新设计ItemTextSend,主要有以下变化:
46 | 1. 构造时需要再传入**消息ID(messageId)**和**长按事件处理函数(OnRecallMessageRequested)**
47 | 2. 用户**长按**某条消息,系统弹出是否撤回本条消息的提示。当用户提示后,调用构造时传入的长按事件处理器进行处理。
48 |
49 | MVVM自己实现的所有Activity也应该符合此操作标准。
50 |
51 | 注意,ItemTextReceive没有处理长按事件,不能撤回接收到的其他人的消息。
52 |
53 | #### 消息撤回处理方法
54 |
55 | 在长按事件处理函数中,向服务器端写一个**RecallRequestMessage**,包含要撤回的消息的ID。
56 |
57 | 当应用接受到类型为**RecallMessage**的消息时,将其messageId的消息的**文本**修改为**(已撤回)**。
58 |
59 | 对于MVP,这些事情(向Model要求发送撤回请求,处理接收到的撤回消息,处理界面变化等)都应该由Presenter负责;对于MVVM,在VM中,修改应该只需要调用`ClientMessageObservable`中`setState`即可,不应该做修改用户界面内容的实际操作。
60 |
61 | ### 需求3:过滤脏话
62 |
63 | #### 如何过滤脏话
64 |
65 | 将文本信息传入`Utils.containsBadWords`方法,返回true/false分别代表存在和不存在脏话。存不存在脏话的判断标准目前仅为**有无fuck**字样。
66 |
67 | 当脏话存在时,**不显示此内容**(文本有错,已经更改)。
68 |
69 | #### 实现说明
70 |
71 | 对于MVVM,由于消息列表(`ObservableList`)已经和列表界面绑定,所以要使**不显示此内容**,应该在`updateList`方法中增加if判断,如果存在脏话,就不增加进入ObservableList。
72 |
73 | 对于FRP,增加filter即可。
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddadaal/android-chat-in-4-patterns/04391b5d9ad1a96d573048ee1605282af2b8bfe1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed May 08 14:11:32 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/server/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 |
3 | dependencies {
4 | compileOnly 'org.projectlombok:lombok:1.18.8'
5 | annotationProcessor 'org.projectlombok:lombok:1.18.8'
6 |
7 | implementation project(":shared")
8 | implementation fileTree(dir: 'libs', include: ['*.jar'])
9 | }
10 |
11 | sourceCompatibility = '1.8'
12 | targetCompatibility = '1.8'
13 |
--------------------------------------------------------------------------------
/server/src/main/java/nju/androidchat/server/ChatServer.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.server;
2 |
3 | import java.io.IOException;
4 | import java.io.ObjectInputStream;
5 | import java.io.ObjectOutputStream;
6 | import java.net.ServerSocket;
7 | import java.net.Socket;
8 | import java.util.HashMap;
9 | import java.util.Map;
10 | import java.util.concurrent.ConcurrentHashMap;
11 |
12 | import lombok.extern.java.Log;
13 | import nju.androidchat.shared.message.ErrorMessage;
14 | import nju.androidchat.shared.message.LoginRequestMessage;
15 | import nju.androidchat.shared.message.LoginResponseMessage;
16 | import nju.androidchat.shared.message.Message;
17 |
18 | import static nju.androidchat.shared.Shared.SERVER_PORT;
19 |
20 | @Log
21 | public class ChatServer {
22 | public static void main(String[] args) throws IOException {
23 | ChatServer server = new ChatServer();
24 |
25 | server.startServer();
26 |
27 | }
28 |
29 | // Record all connected clients
30 | public static Map connectionMap = new ConcurrentHashMap<>();
31 |
32 | public void startServer() throws IOException {
33 |
34 | ServerSocket server = new ServerSocket(SERVER_PORT);
35 | log.info(String.format("[Server] Server started on %s. Awaiting incoming connection.", SERVER_PORT));
36 | while (true) {
37 | Socket client = server.accept();
38 |
39 | log.info("Received connection from " + client.getRemoteSocketAddress().toString());
40 |
41 | // get first message and see if it is login request
42 |
43 | new Thread(new ConnectionHandlerImpl(client)).start();
44 |
45 | }
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/server/src/main/java/nju/androidchat/server/ConnectionHandler.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.server;
2 |
3 | import nju.androidchat.shared.message.Message;
4 |
5 | public interface ConnectionHandler {
6 | void sendToAllOtherClients(Message message);
7 |
8 | void setTerminate(boolean terminate);
9 |
10 | String getUsername();
11 |
12 | void log(String content);
13 | }
14 |
--------------------------------------------------------------------------------
/server/src/main/java/nju/androidchat/server/handlers/ClientSendMessageHandler.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.server.handlers;
2 |
3 | import lombok.extern.java.Log;
4 | import nju.androidchat.server.ConnectionHandler;
5 | import nju.androidchat.shared.message.ClientSendMessage;
6 | import nju.androidchat.shared.message.ServerSendMessage;
7 |
8 | @Log
9 | public class ClientSendMessageHandler implements MessageHandler {
10 | @Override
11 | public void handle(ClientSendMessage message, ConnectionHandler connectionHandler) {
12 | String messageContent = message.getMessage();
13 |
14 | log.info("有客户端发来消息" + messageContent);
15 | // received a message, send it to all clients except sender
16 | connectionHandler.sendToAllOtherClients(new ServerSendMessage(
17 | message.getMessageId(),
18 | message.getTime(),
19 | connectionHandler.getUsername(),
20 | messageContent
21 | ));
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server/src/main/java/nju/androidchat/server/handlers/DisconnectMessageHandler.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.server.handlers;
2 |
3 | import lombok.extern.java.Log;
4 | import nju.androidchat.server.ChatServer;
5 | import nju.androidchat.server.ConnectionHandler;
6 | import nju.androidchat.shared.message.DisconnectMessage;
7 |
8 | @Log
9 | public class DisconnectMessageHandler implements MessageHandler {
10 | @Override
11 | public void handle(DisconnectMessage message, ConnectionHandler connectionHandler) {
12 | connectionHandler.log("Disconnect request received. Terminating connection of " + connectionHandler.getUsername());
13 | connectionHandler.setTerminate(true);
14 | ChatServer.connectionMap.remove(connectionHandler.getUsername());
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/server/src/main/java/nju/androidchat/server/handlers/MessageHandler.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.server.handlers;
2 |
3 | import nju.androidchat.server.ConnectionHandler;
4 | import nju.androidchat.shared.message.Message;
5 |
6 | public interface MessageHandler {
7 | void handle(T message, ConnectionHandler connectionHandler);
8 | }
9 |
--------------------------------------------------------------------------------
/server/src/main/java/nju/androidchat/server/handlers/RecallRequestMessageHandler.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.server.handlers;
2 |
3 | import java.util.UUID;
4 |
5 | import nju.androidchat.server.ConnectionHandler;
6 | import nju.androidchat.shared.message.RecallMessage;
7 | import nju.androidchat.shared.message.RecallRequestMessage;
8 |
9 | public class RecallRequestMessageHandler implements MessageHandler {
10 | @Override
11 | public void handle(RecallRequestMessage message, ConnectionHandler connectionHandler) {
12 | UUID messageId = message.getMessageId();
13 | connectionHandler.log("Recall request received: " + messageId);
14 |
15 | // no sender validation
16 | connectionHandler.sendToAllOtherClients(new RecallMessage(messageId));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':server', ':shared'
2 |
--------------------------------------------------------------------------------
/shared/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/shared/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 |
3 | dependencies {
4 |
5 | compileOnly 'org.projectlombok:lombok:1.18.8'
6 | annotationProcessor 'org.projectlombok:lombok:1.18.8'
7 |
8 | implementation fileTree(dir: 'libs', include: ['*.jar'])
9 | }
10 |
11 | sourceCompatibility = '1.8'
12 | targetCompatibility = '1.8'
13 |
--------------------------------------------------------------------------------
/shared/src/main/java/nju/androidchat/shared/Shared.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.shared;
2 |
3 |
4 | import lombok.Getter;
5 |
6 | public class Shared {
7 |
8 | public static final int SERVER_PORT = 16492;
9 | }
10 |
--------------------------------------------------------------------------------
/shared/src/main/java/nju/androidchat/shared/message/ClientSendMessage.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.shared.message;
2 |
3 | import java.time.LocalDateTime;
4 | import java.util.UUID;
5 |
6 | import lombok.AllArgsConstructor;
7 | import lombok.Getter;
8 | import lombok.Setter;
9 | import lombok.ToString;
10 |
11 |
12 | /**
13 | * 客户端到服务器:发送信息
14 | */
15 | @ToString
16 | @AllArgsConstructor
17 | public class ClientSendMessage extends Message {
18 |
19 | @Getter
20 | private UUID messageId;
21 |
22 | @Getter
23 | private LocalDateTime time;
24 |
25 | @Getter
26 | private String message;
27 | }
28 |
--------------------------------------------------------------------------------
/shared/src/main/java/nju/androidchat/shared/message/DisconnectMessage.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.shared.message;
2 |
3 | /**
4 | * 客户端到服务器:断开连接
5 | */
6 | public class DisconnectMessage extends Message {
7 | }
8 |
--------------------------------------------------------------------------------
/shared/src/main/java/nju/androidchat/shared/message/ErrorMessage.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.shared.message;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.ToString;
6 |
7 | /**
8 | * 服务器到客户端:发生错误
9 | */
10 | @ToString
11 | @AllArgsConstructor
12 | public class ErrorMessage extends Message {
13 | @Getter
14 | private String errorMessage;
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/main/java/nju/androidchat/shared/message/LoginRequestMessage.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.shared.message;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 | import lombok.ToString;
7 |
8 | /**
9 | * 客户端到服务器:登录请求
10 | * 登录写法:并不是像HTTP一样一个request一个response!
11 | * 客户端发送流和响应流是分开的!
12 | * 所以在登录按钮按下前,就初始化客户端的SocketClient建立Socket连接
13 | * 然后在这个连接上调用writeToServer,发出LoginRequestMessage信息
14 | * 然后当SocketClient接受到LoginResponseMessage信息时说明登录成功
15 | *
16 | */
17 | @ToString
18 | @AllArgsConstructor
19 | public class LoginRequestMessage extends Message {
20 | @Getter
21 | private String username;
22 | }
23 |
--------------------------------------------------------------------------------
/shared/src/main/java/nju/androidchat/shared/message/LoginResponseMessage.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.shared.message;
2 | import lombok.AllArgsConstructor;
3 | import lombok.Getter;
4 | import lombok.ToString;
5 |
6 | /**
7 | * 服务器到客户端:登录响应
8 | * 登录写法:并不是像HTTP一样一个request一个response!
9 | * 客户端发送流和响应流是分开的!
10 | * 所以在登录按钮按下前,就初始化客户端的SocketClient建立Socket连接
11 | * 然后在这个连接上调用writeToServer,发出LoginRequestMessage信息
12 | * 然后当SocketClient接受到LoginResponseMessage信息时说明登录成功
13 | */
14 | @ToString
15 | @AllArgsConstructor
16 | public class LoginResponseMessage extends Message {
17 | @Getter
18 | private String loggedInUsername;
19 | }
20 |
--------------------------------------------------------------------------------
/shared/src/main/java/nju/androidchat/shared/message/Message.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.shared.message;
2 |
3 | import java.io.Serializable;
4 |
5 | public abstract class Message implements Serializable {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/shared/src/main/java/nju/androidchat/shared/message/RecallMessage.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.shared.message;
2 |
3 | import java.util.UUID;
4 |
5 | import lombok.AllArgsConstructor;
6 | import lombok.Getter;
7 | import lombok.ToString;
8 |
9 | @ToString
10 | @AllArgsConstructor
11 | public class RecallMessage extends Message {
12 | @Getter
13 | private UUID messageId;
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/main/java/nju/androidchat/shared/message/RecallRequestMessage.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.shared.message;
2 |
3 | import java.util.UUID;
4 |
5 | import lombok.AllArgsConstructor;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 | import lombok.ToString;
9 |
10 | @AllArgsConstructor
11 | @ToString
12 | public class RecallRequestMessage extends Message {
13 | @Getter
14 | private UUID messageId;
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/main/java/nju/androidchat/shared/message/ServerSendMessage.java:
--------------------------------------------------------------------------------
1 | package nju.androidchat.shared.message;
2 |
3 | import java.time.LocalDateTime;
4 | import java.util.UUID;
5 |
6 | import lombok.AllArgsConstructor;
7 | import lombok.Getter;
8 | import lombok.Setter;
9 | import lombok.ToString;
10 |
11 | @ToString
12 | @AllArgsConstructor
13 | public class ServerSendMessage extends Message {
14 | @Getter
15 | private UUID messageId;
16 |
17 | @Getter
18 | private LocalDateTime time;
19 |
20 | @Getter
21 | private String senderUsername;
22 |
23 | @Getter
24 | private String message;
25 | }
26 |
--------------------------------------------------------------------------------
/作业信息.txt:
--------------------------------------------------------------------------------
1 | 学号:
2 | 作业选项:
--------------------------------------------------------------------------------
/作业提交说明.md:
--------------------------------------------------------------------------------
1 | # 作业提交说明
2 |
3 | 请所有同学遵照以下流程做本作业:
4 |
5 | - fork本项目,并保持你的项目是public的
6 | - 在**作业信息.txt**里写上自己的学号和作业选项(作业选项用1、2或者3表示)
7 | - 使用Android Studio最新版(3.4.1)打开本项目
8 | - **复制**一遍作业基础代码所在的包,并改名为**hw{作业编号1,2,3}**,在新包里完成工作
9 | - 例如:如果你想做第一个作业,就复制一遍包`mvp0`到`nju.androidchat.client`包里,并新包改名为`hw1`。
10 | - 在提交前,确保将`assets/config.properties`中的`chat_activity`改为你的作业的主Activity
11 |
12 | # 注意
13 |
14 | - 你的包里的Activity是否改名可选,建议改名(即改成例如`Hw1TalkActivity`)
15 | - 提交到自己的仓库里即可,不要给原repo提交PR
16 | - 请确保使用Android 9.0的模拟器能够运行你的项目
17 |
18 | # 作业澄清
19 |
20 | - 作业1:URL为网络地址,不需要处理本地地址
--------------------------------------------------------------------------------