From 99269610e476d710a20130d1657592d3681e27cd Mon Sep 17 00:00:00 2001 From: Juntao Peng Date: Wed, 11 Nov 2020 16:17:40 +0800 Subject: [PATCH 1/8] Split master into two branches: MobileChatRoom (1/2) --- .../AndroidChatRoomClient/.gitignore | 16 + .../AndroidChatRoomClient/.idea/.gitignore | 2 + .../.idea/codeStyles/Project.xml | 125 +++++++ .../AndroidChatRoomClient/.idea/compiler.xml | 6 + .../AndroidChatRoomClient/.idea/gradle.xml | 22 ++ .../.idea/jarRepositories.xml | 30 ++ .../AndroidChatRoomClient/.idea/misc.xml | 9 + .../.idea/runConfigurations.xml | 12 + .../AndroidChatRoomClient/.idea/vcs.xml | 6 + .../New Text Document.txt | 0 .../AndroidChatRoomClient/app/.gitignore | 2 + .../AndroidChatRoomClient/app/build.gradle | 53 +++ .../app/proguard-rules.pro | 21 ++ .../app/src/main/AndroidManifest.xml | 47 +++ .../activity/MainActivity.java | 124 ++++++ .../fragment/ChatFragment.java | 352 ++++++++++++++++++ .../fragment/ChatUserInterface.java | 27 ++ .../fragment/LoginFragment.java | 52 +++ .../buttonhandler/ImageButtonHandler.java | 55 +++ .../buttonhandler/SendButtonHandler.java | 58 +++ .../chatrecyclerview/ChatContentAdapter.java | 205 ++++++++++ .../ChatContentViewHolder.java | 44 +++ .../RecyclerViewItemClickListener.java | 7 + .../androidchatroom/message/Message.java | 196 ++++++++++ .../message/MessageFactory.java | 197 ++++++++++ .../message/MessageTypeEnum.java | 29 ++ .../message/SimpleCallback.java | 5 + .../androidchatroom/service/ChatService.java | 21 ++ .../service/FirebaseService.java | 111 ++++++ .../service/NotificationService.java | 79 ++++ .../service/SignalRChatService.java | 349 +++++++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 30 ++ .../drawable/enter_leave_message_bubble.xml | 8 + .../res/drawable/ic_launcher_background.xml | 170 +++++++++ .../app/src/main/res/drawable/ic_pulling.xml | 12 + .../main/res/drawable/ic_ready_to_pull.xml | 9 + .../received_broadcast_message_bubble.xml | 8 + .../received_private_message_bubble.xml | 8 + .../main/res/drawable/self_message_bubble.xml | 8 + .../drawable/sent_private_message_bubble.xml | 8 + .../app/src/main/res/layout/activity_main.xml | 25 ++ .../app/src/main/res/layout/content_main.xml | 19 + .../app/src/main/res/layout/fragment_chat.xml | 99 +++++ .../src/main/res/layout/fragment_login.xml | 33 ++ .../item_received_image_broadcast_message.xml | 40 ++ .../item_received_image_private_message.xml | 41 ++ .../item_received_text_broadcast_message.xml | 42 +++ .../item_received_text_private_message.xml | 42 +++ .../item_self_image_broadcast_message.xml | 57 +++ .../item_self_image_private_message.xml | 57 +++ .../item_self_text_broadcast_message.xml | 59 +++ .../layout/item_self_text_private_message.xml | 59 +++ .../main/res/layout/item_system_message.xml | 30 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes .../app/src/main/res/navigation/nav_graph.xml | 28 ++ .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/dimens.xml | 3 + .../app/src/main/res/values/strings.xml | 33 ++ .../app/src/main/res/values/styles.xml | 19 + .../AndroidChatRoomClient/build.gradle | 24 ++ .../AndroidChatRoomClient/gradle.properties | 19 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../AndroidChatRoomClient/gradlew | 172 +++++++++ .../AndroidChatRoomClient/gradlew.bat | 84 +++++ .../AndroidChatRoomClient/settings.gradle | 2 + samples/MobileChatRoom/README.md | 181 +++++++++ .../MobileChatRoom/assets/1-open-project.png | Bin 0 -> 20562 bytes .../MobileChatRoom/assets/2-open-project.png | Bin 0 -> 28191 bytes samples/MobileChatRoom/assets/3-build.png | Bin 0 -> 7276 bytes samples/MobileChatRoom/assets/4-avd.png | Bin 0 -> 18736 bytes .../assets/4-different-usernames.png | Bin 0 -> 87103 bytes .../assets/4-start-chatting.png | Bin 0 -> 101264 bytes .../MobileChatRoom/assets/broadcast-image.png | Bin 0 -> 15986 bytes .../MobileChatRoom/assets/broadcast-text.png | Bin 0 -> 48559 bytes .../MobileChatRoom/assets/history-message.png | Bin 0 -> 90979 bytes .../MobileChatRoom/assets/image-loaded.png | Bin 0 -> 18711 bytes .../assets/overview-interface.png | Bin 0 -> 53419 bytes .../MobileChatRoom/assets/private-image.png | Bin 0 -> 13091 bytes .../MobileChatRoom/assets/private-text.png | Bin 0 -> 54909 bytes .../MobileChatRoom/assets/read-private.png | Bin 0 -> 3937 bytes samples/MobileChatRoom/assets/red-square.png | Bin 0 -> 5216 bytes .../assets/resend-broadcast.png | Bin 0 -> 4586 bytes .../MobileChatRoom/assets/sending-private.png | Bin 0 -> 4328 bytes .../MobileChatRoom/assets/sent-private.png | Bin 0 -> 3888 bytes .../MobileChatRoom/assets/white-square.png | Bin 0 -> 5148 bytes 97 files changed, 3713 insertions(+) create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/.gitignore create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/.idea/.gitignore create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/.idea/codeStyles/Project.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/.idea/compiler.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/.idea/gradle.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/.idea/jarRepositories.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/.idea/misc.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/.idea/runConfigurations.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/.idea/vcs.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/New Text Document.txt create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/.gitignore create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/build.gradle create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/proguard-rules.pro create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/AndroidManifest.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/activity/MainActivity.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/ChatFragment.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/ChatUserInterface.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/LoginFragment.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/buttonhandler/ImageButtonHandler.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/buttonhandler/SendButtonHandler.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/ChatContentAdapter.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/ChatContentViewHolder.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/RecyclerViewItemClickListener.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/Message.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/MessageFactory.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/MessageTypeEnum.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/SimpleCallback.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/ChatService.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/FirebaseService.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/NotificationService.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/SignalRChatService.java create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/enter_leave_message_bubble.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_pulling.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_ready_to_pull.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/received_broadcast_message_bubble.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/received_private_message_bubble.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/self_message_bubble.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/sent_private_message_bubble.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/activity_main.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/content_main.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/fragment_chat.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/fragment_login.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/item_received_image_broadcast_message.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/item_received_image_private_message.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/item_received_text_broadcast_message.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/item_received_text_private_message.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/item_self_image_broadcast_message.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/item_self_image_private_message.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/item_self_text_broadcast_message.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/item_self_text_private_message.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/item_system_message.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/navigation/nav_graph.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/values/colors.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/values/dimens.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/values/strings.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/values/styles.xml create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/build.gradle create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/gradle.properties create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/gradle/wrapper/gradle-wrapper.properties create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/gradlew create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/gradlew.bat create mode 100644 samples/MobileChatRoom/AndroidChatRoomClient/settings.gradle create mode 100644 samples/MobileChatRoom/README.md create mode 100644 samples/MobileChatRoom/assets/1-open-project.png create mode 100644 samples/MobileChatRoom/assets/2-open-project.png create mode 100644 samples/MobileChatRoom/assets/3-build.png create mode 100644 samples/MobileChatRoom/assets/4-avd.png create mode 100644 samples/MobileChatRoom/assets/4-different-usernames.png create mode 100644 samples/MobileChatRoom/assets/4-start-chatting.png create mode 100644 samples/MobileChatRoom/assets/broadcast-image.png create mode 100644 samples/MobileChatRoom/assets/broadcast-text.png create mode 100644 samples/MobileChatRoom/assets/history-message.png create mode 100644 samples/MobileChatRoom/assets/image-loaded.png create mode 100644 samples/MobileChatRoom/assets/overview-interface.png create mode 100644 samples/MobileChatRoom/assets/private-image.png create mode 100644 samples/MobileChatRoom/assets/private-text.png create mode 100644 samples/MobileChatRoom/assets/read-private.png create mode 100644 samples/MobileChatRoom/assets/red-square.png create mode 100644 samples/MobileChatRoom/assets/resend-broadcast.png create mode 100644 samples/MobileChatRoom/assets/sending-private.png create mode 100644 samples/MobileChatRoom/assets/sent-private.png create mode 100644 samples/MobileChatRoom/assets/white-square.png diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/.gitignore b/samples/MobileChatRoom/AndroidChatRoomClient/.gitignore new file mode 100644 index 00000000..42573888 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/app/google-services.json +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/.idea/.gitignore b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/.gitignore new file mode 100644 index 00000000..5c98b428 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/.idea/codeStyles/Project.xml b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..663459aa --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/codeStyles/Project.xml @@ -0,0 +1,125 @@ + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/.idea/compiler.xml b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/compiler.xml new file mode 100644 index 00000000..61a9130c --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/.idea/gradle.xml b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/gradle.xml new file mode 100644 index 00000000..23a89bbb --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/gradle.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/.idea/jarRepositories.xml b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/jarRepositories.xml new file mode 100644 index 00000000..ce3bfd62 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/.idea/misc.xml b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/misc.xml new file mode 100644 index 00000000..d5d35ec4 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/.idea/runConfigurations.xml b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/runConfigurations.xml new file mode 100644 index 00000000..7f68460d --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/.idea/vcs.xml b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/vcs.xml new file mode 100644 index 00000000..c2365ab1 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/New Text Document.txt b/samples/MobileChatRoom/AndroidChatRoomClient/New Text Document.txt new file mode 100644 index 00000000..e69de29b diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/.gitignore b/samples/MobileChatRoom/AndroidChatRoomClient/app/.gitignore new file mode 100644 index 00000000..2abde4aa --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/.gitignore @@ -0,0 +1,2 @@ +/build +/google-services.json diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/build.gradle b/samples/MobileChatRoom/AndroidChatRoomClient/app/build.gradle new file mode 100644 index 00000000..925ea997 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/build.gradle @@ -0,0 +1,53 @@ +apply plugin: 'com.android.application' +apply plugin: 'com.google.gms.google-services' + +android { + compileSdkVersion 30 + buildToolsVersion '30.0.2' + + defaultConfig { + applicationId 'com.microsoft.signalr.androidchatroom' + minSdkVersion 26 + targetSdkVersion 30 + versionCode 1 + versionName '1.0' + multiDexEnabled true + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'com.google.android.material:material:1.2.1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.2' + implementation 'androidx.navigation:navigation-fragment:2.3.1' + implementation 'androidx.navigation:navigation-ui:2.3.1' + implementation 'androidx.navigation:navigation-dynamic-features-fragment:2.3.1' + implementation 'com.microsoft.signalr:signalr:3.0.0' + implementation 'com.microsoft.azure:notification-hubs-android-sdk:1.1.2@aar' + implementation 'com.google.firebase:firebase-messaging:20.3.0' + implementation 'com.android.volley:volley:1.1.1' + implementation 'com.google.code.gson:gson:2.8.6' + testImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.29' +} + +repositories { + maven { + url "https://dl.bintray.com/microsoftazuremobile/SDK" + } +} \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/proguard-rules.pro b/samples/MobileChatRoom/AndroidChatRoomClient/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/AndroidManifest.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..02e4be0f --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/activity/MainActivity.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/activity/MainActivity.java new file mode 100644 index 00000000..6f741098 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/activity/MainActivity.java @@ -0,0 +1,124 @@ +package com.microsoft.signalr.androidchatroom.activity; + +import android.content.ComponentName; +import android.content.Context; +import android.content.ServiceConnection; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import com.microsoft.signalr.androidchatroom.R; +import com.microsoft.signalr.androidchatroom.service.ChatService; +import com.microsoft.signalr.androidchatroom.service.NotificationService; +import com.microsoft.signalr.androidchatroom.service.SignalRChatService; +import com.microsoft.signalr.androidchatroom.service.FirebaseService; + +import android.content.Intent; +import android.os.IBinder; +import android.widget.Toast; + +public class MainActivity extends AppCompatActivity { + private static final String TAG = "MainActivity"; + + public static MainActivity mainActivity; + public static Boolean isVisible = false; + + + private ChatService chatService; + private final ServiceConnection chatServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + SignalRChatService.ChatServiceBinder chatServiceBinder = (SignalRChatService.ChatServiceBinder) service; + chatService = chatServiceBinder.getService(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + chatService = null; + } + }; + + private NotificationService notificationService; + private final ServiceConnection notificationServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + NotificationService.NotificationServiceBinder notificationServiceBinder = (NotificationService.NotificationServiceBinder) service; + notificationService = notificationServiceBinder.getService(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + notificationService = null; + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mainActivity = this; + setContentView(R.layout.activity_main); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + bindChatService(); + bindNotificationService(); + FirebaseService.createChannelAndHandleNotifications(getApplicationContext()); + } + + public ChatService getChatService() { + return chatService; + } + + public NotificationService getNotificationService() { + return notificationService; + } + + public void bindChatService() { + Intent intent = new Intent(this, SignalRChatService.class); + bindService(intent, chatServiceConnection, Context.BIND_AUTO_CREATE); + } + + public void bindNotificationService() { + Intent intent = new Intent(this, NotificationService.class); + bindService(intent, notificationServiceConnection, Context.BIND_AUTO_CREATE); + } + + @Override + public void onBackPressed() { + chatService.expireSession(false); + super.onBackPressed(); + } + + @Override + protected void onStart() { + super.onStart(); + isVisible = true; + } + + @Override + protected void onPause() { + super.onPause(); + isVisible = false; + } + + @Override + protected void onResume() { + super.onResume(); + isVisible = true; + } + + @Override + protected void onStop() { + super.onStop(); + isVisible = false; + } + + public void ToastNotify(final String notificationMessage) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(MainActivity.this, notificationMessage, Toast.LENGTH_LONG).show(); + } + }); + } +} \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/ChatFragment.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/ChatFragment.java new file mode 100644 index 00000000..cc1a8709 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/ChatFragment.java @@ -0,0 +1,352 @@ +package com.microsoft.signalr.androidchatroom.fragment; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.microsoft.signalr.androidchatroom.R; +import com.microsoft.signalr.androidchatroom.activity.MainActivity; +import com.microsoft.signalr.androidchatroom.fragment.buttonhandler.ImageButtonHandler; +import com.microsoft.signalr.androidchatroom.fragment.buttonhandler.SendButtonHandler; +import com.microsoft.signalr.androidchatroom.fragment.chatrecyclerview.ChatContentAdapter; +import com.microsoft.signalr.androidchatroom.message.MessageFactory; +import com.microsoft.signalr.androidchatroom.message.Message; +import com.microsoft.signalr.androidchatroom.service.ChatService; +import com.microsoft.signalr.androidchatroom.service.NotificationService; + +import org.jetbrains.annotations.NotNull; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static android.app.Activity.RESULT_OK; + +public class ChatFragment extends Fragment implements ChatUserInterface { + private static final String TAG = "ChatFragment"; + public static final int RESULT_LOAD_IMAGE = 1; + + // Services + private ChatService chatService; + private NotificationService notificationService; + + // Messages + private final List messages = new ArrayList<>(); + private String username; + private String deviceUuid; + + // Click Listeners + private SendButtonHandler sendButtonHandler; + private ImageButtonHandler imageButtonHandler; + + // View elements and adapters + private EditText chatBoxReceiverEditText; + private EditText chatBoxMessageEditText; + private Button chatBoxSendButton; + private Button chatBoxImageButton; + private RecyclerView chatContentRecyclerView; + private ChatContentAdapter chatContentAdapter; + private LinearLayoutManager layoutManager; + + public EditText getChatBoxReceiverEditText() { + return chatBoxReceiverEditText; + } + + public EditText getChatBoxMessageEditText() { + return chatBoxMessageEditText; + } + + public String getUsername() { + return username; + } + + @Override + public void onAttach(@NotNull Context context) { + super.onAttach(context); + try { + chatService = ((MainActivity) context).getChatService(); + notificationService = ((MainActivity) context).getNotificationService(); + } catch (ClassCastException e) { + Log.e(TAG, e.getMessage()); + } + } + + @Override + public void onDetach() { + super.onDetach(); + + // Remove activity reference + chatService = null; + notificationService = null; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState + ) { + // Inflate the layout for this fragment + View view = inflater.inflate(R.layout.fragment_chat, container, false); + + // Get view element references + this.chatBoxReceiverEditText = view.findViewById(R.id.edit_chat_receiver); + this.chatBoxMessageEditText = view.findViewById(R.id.edit_chat_message); + this.chatBoxSendButton = view.findViewById(R.id.button_chatbox_send); + this.chatBoxImageButton = view.findViewById(R.id.button_chatbox_image); + this.chatContentRecyclerView = view.findViewById(R.id.recyclerview_chatcontent); + + // Create objects + this.chatContentAdapter = new ChatContentAdapter(messages, getContext(), this, chatService); + this.layoutManager = new LinearLayoutManager(this.getActivity()); + + // Configure RecyclerView + configureRecyclerView(); + + this.sendButtonHandler = new SendButtonHandler(this, chatService); + this.imageButtonHandler = new ImageButtonHandler(this, chatService); + + return view; + } + + private void configureRecyclerView() { + // Add append new messages to end (bottom) + layoutManager.setStackFromEnd(true); + + chatContentRecyclerView.setLayoutManager(layoutManager); + chatContentRecyclerView.setAdapter(chatContentAdapter); + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // Get passed username + if ((username = getArguments().getString("username")) == null) { + username = "EMPTY_PLACEHOLDER"; + } + + // Get deviceUuid + deviceUuid = notificationService.getDeviceUuid(); + + // Register user info into chat service + new Thread(() -> { + chatService.register(username, deviceUuid, this); + chatService.startSession(); + }).start(); + } + + @Override + public void activateClickEvent() { + chatBoxSendButton.setOnClickListener(sendButtonHandler); + chatBoxImageButton.setOnClickListener(imageButtonHandler); + chatContentRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + if (!recyclerView.canScrollVertically(-1)) { + Log.d(TAG, "OnScroll cannot scroll vertical -1"); + long untilTime = System.currentTimeMillis(); + for (Message message : messages) { + if (!message.getMessageType().name().contains("SYSTEM")) { + untilTime = message.getTime(); + break; + } + } + chatService.pullHistoryMessages(untilTime); + } + } + }); + } + + @Override + public void disableClickEvent() { + chatBoxSendButton.setOnClickListener(null); + chatBoxImageButton.setOnClickListener(null); + chatContentRecyclerView.addOnScrollListener(null); + } + + @Override + public void tryAddMessage(Message message, int direction) { + // Check for duplicated message + boolean isDuplicateMessage = checkForDuplicatedMessage(message.getMessageId()); + + // If not duplicated, create ChatMessage according to parameters + if (!isDuplicateMessage) { + messages.add(message); + + // Tell the server the message was read + setReceivedMessageRead(message); + } + + refreshUiThread(true, direction); + } + + @Override + public void tryAddAllMessages(List messages, int direction) { + // Record all messages for now + Set existedMessageIds = this.messages.stream().map(Message::getMessageId).collect(Collectors.toSet()); + + // Iterate through message list + for (Message message : messages) { + if (!existedMessageIds.contains(message.getMessageId())) { + // If found a new message, add it to message list + this.messages.add(message); + existedMessageIds.add(message.getMessageId()); + + // Tell the server the message was read + setReceivedMessageRead(message); + } + } + + refreshUiThread(true, direction); + } + + @Override + public void setSentMessageAck(String messageId, long receivedTimeInLong) { + for (Message message : messages) { + if (message.getMessageId().equals(messageId)) { + message.ack(receivedTimeInLong); + Log.d("setMessageAck", messageId); + break; + } + } + + refreshUiThread(true, 0); + } + + @Override + public void setSentMessageRead(String messageId) { + for (Message message : messages) { + if (message.getMessageId().equals(messageId)) { + message.read(); + break; + } + } + + refreshUiThread(true, 0); + } + + private void setReceivedMessageRead(Message message) { + if (isUnreadReceivedPrivateMessage(message)) { + chatService.sendMessageRead(message.getMessageId()); + } + } + + private boolean isUnreadReceivedPrivateMessage(Message message) { + return !message.isRead() && + message.getMessageType().name().contains("RECEIVED") + && message.getMessageType().name().contains("PRIVATE"); + } + + @Override + public void showSessionExpiredDialog() { + requireActivity().runOnUiThread(() -> { + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setMessage(R.string.alert_message) + .setTitle(R.string.alert_title) + .setCancelable(false); + builder.setPositiveButton(R.string.alert_ok, (dialog, id) -> { + Navigation.findNavController(requireView()).navigate(R.id.action_ChatFragment_to_LoginFragment); + requireActivity().recreate(); + }); + AlertDialog dialog = builder.create(); + dialog.show(); + }); + } + + @Override + public void onActivityResult(int reqCode, int resultCode, Intent data) { + super.onActivityResult(reqCode, resultCode, data); + if (resultCode == RESULT_OK) { + try { + final Uri imageUri = data.getData(); + final InputStream imageStream = requireActivity().getContentResolver().openInputStream(imageUri); + final Bitmap selectedImage = BitmapFactory.decodeStream(imageStream); + Message imageMessage = imageButtonHandler.createAndSendImageMessage(selectedImage); + messages.add(imageMessage); + refreshUiThread(false,1); + } catch (FileNotFoundException e) { + e.printStackTrace(); + Toast.makeText(getContext(), "Image picking failed.", Toast.LENGTH_LONG).show(); + } + } else { + Toast.makeText(getContext(), "You haven't picked Image.", Toast.LENGTH_LONG).show(); + } + } + + @Override + public void setImageContent(String messageId, String payload) { + Message message = getMessageWithId(messageId); + if (message != null) { + message.ackPullImage(); + message.setPayload(payload); + message.setBmp(MessageFactory.decodeToBitmap(payload)); + refreshUiThread(false,0); + } + } + + private boolean checkForDuplicatedMessage(String messageId) { + boolean isDuplicateMessage = false; + for (Message message : messages) { + if (message.getMessageId().equals(messageId)) { + isDuplicateMessage = true; + break; + } + } + return isDuplicateMessage; + } + + private Message getMessageWithId(String messageId) { + for (Message message : messages) { + if (message.getMessageId().equals(messageId)) { + return message; + } + } + + return null; + } + + @Override + public void refreshUiThread(boolean sortMessageList, int direction) { + // Sort by send time first + if (sortMessageList) { + messages.sort((m1, m2) -> (int) (m1.getTime() - m2.getTime())); + } + + // Then refresh the UiThread + requireActivity().runOnUiThread(() -> { + chatContentAdapter.notifyDataSetChanged(); + switch (direction) { + case 1: + Log.d(TAG, "Finger swipe up" + (messages.size() - 1)); + chatContentRecyclerView.scrollToPosition(messages.size() - 1); + break; + case -1: + Log.d(TAG, "Finger swipe down"); + chatContentRecyclerView.scrollToPosition(0); + break; + default: + } + }); + } +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/ChatUserInterface.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/ChatUserInterface.java new file mode 100644 index 00000000..ebf4c12b --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/ChatUserInterface.java @@ -0,0 +1,27 @@ +package com.microsoft.signalr.androidchatroom.fragment; + +import com.microsoft.signalr.androidchatroom.message.Message; + +import java.util.List; +import java.util.Set; + +public interface ChatUserInterface { + + void activateClickEvent(); + + void disableClickEvent(); + + void tryAddMessage(Message message, int direction); + + void tryAddAllMessages(List messages, int direction); + + void setSentMessageAck(String messageId, long receivedTimeInLong); + + void setSentMessageRead(String messageId); + + void setImageContent(String messageId, String payload); + + void showSessionExpiredDialog(); + + void refreshUiThread(boolean sortMessageList, int direction); +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/LoginFragment.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/LoginFragment.java new file mode 100644 index 00000000..77983892 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/LoginFragment.java @@ -0,0 +1,52 @@ +package com.microsoft.signalr.androidchatroom.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + +import com.microsoft.signalr.androidchatroom.R; + +public class LoginFragment extends Fragment { + private Button loginButton; + private TextView usernameTextView; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState + ) { + // Inflate the layout for this fragment + View view = inflater.inflate(R.layout.fragment_login, container, false); + + // Get element references + this.usernameTextView = view.findViewById(R.id.text_username_LoginFragment); + this.loginButton = view.findViewById(R.id.button_login_LoginFragment); + + return view; + } + + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (loginButton != null && usernameTextView != null) { + loginButton.setOnClickListener( + (v) -> { + if (usernameTextView.getText().toString().length() > 0) { + Bundle bundle = new Bundle(); + bundle.putString("username", usernameTextView.getText().toString()); + NavHostFragment.findNavController(LoginFragment.this) + .navigate(R.id.action_LoginFragment_to_ChatFragment, bundle); + } + } + ); + } + } + +} \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/buttonhandler/ImageButtonHandler.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/buttonhandler/ImageButtonHandler.java new file mode 100644 index 00000000..2a53cb9f --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/buttonhandler/ImageButtonHandler.java @@ -0,0 +1,55 @@ +package com.microsoft.signalr.androidchatroom.fragment.buttonhandler; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.view.View; + +import com.microsoft.signalr.androidchatroom.fragment.ChatFragment; +import com.microsoft.signalr.androidchatroom.message.Message; +import com.microsoft.signalr.androidchatroom.message.MessageFactory; +import com.microsoft.signalr.androidchatroom.service.ChatService; + +public class ImageButtonHandler implements View.OnClickListener { + private final ChatFragment chatFragment; + private final ChatService chatService; + + public ImageButtonHandler(ChatFragment chatFragment, ChatService chatService) { + this.chatFragment = chatFragment; + this.chatService = chatService; + } + + @Override + public void onClick(View v) { + Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); + photoPickerIntent.setType("image/*"); + chatFragment.startActivityForResult(photoPickerIntent, ChatFragment.RESULT_LOAD_IMAGE); + } + + + public Message createAndSendImageMessage(Bitmap bmp) { + // Receiver + String receiver = chatFragment.getChatBoxReceiverEditText().getText().toString(); + + // Payload + String messageContent = chatFragment.getChatBoxMessageEditText().getText().toString(); + chatFragment.getChatBoxMessageEditText().getText().clear(); + + // Receiver field length == 0 -> broadcast message + boolean isBroadcastMessage = receiver.length() == 0; + + Message chatMessage; + if (isBroadcastMessage) { + chatMessage = MessageFactory.createSendingImageBroadcastMessage(chatFragment.getUsername(), bmp, System.currentTimeMillis(), m -> { + m.startSendMessageTimer(chatFragment, r -> r.refreshUiThread(false, 0)); + chatService.sendMessage(m); + }); + } else { + chatMessage = MessageFactory.createSendingImagePrivateMessage(chatFragment.getUsername(), receiver, bmp, System.currentTimeMillis(), m -> { + m.startSendMessageTimer(chatFragment, r -> r.refreshUiThread(false,0)); + chatService.sendMessage(m); + }); + } + + return chatMessage; + } +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/buttonhandler/SendButtonHandler.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/buttonhandler/SendButtonHandler.java new file mode 100644 index 00000000..4f1277cc --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/buttonhandler/SendButtonHandler.java @@ -0,0 +1,58 @@ +package com.microsoft.signalr.androidchatroom.fragment.buttonhandler; + +import android.view.View; + +import com.microsoft.signalr.androidchatroom.fragment.ChatFragment; +import com.microsoft.signalr.androidchatroom.message.Message; +import com.microsoft.signalr.androidchatroom.message.MessageFactory; +import com.microsoft.signalr.androidchatroom.service.ChatService; + +public class SendButtonHandler implements View.OnClickListener { + + private final ChatFragment chatFragment; + private final ChatService chatService; + + public SendButtonHandler(ChatFragment chatFragment, ChatService chatService) { + this.chatFragment = chatFragment; + this.chatService = chatService; + } + + @Override + public void onClick(View v) { + if (chatFragment.getChatBoxMessageEditText().getText().length() > 0) { // Empty message not allowed + // Create and send message + Message chatMessage = createAndSendTextMessage(); + + // Add message into list + chatFragment.tryAddMessage(chatMessage, 0); + + // Refresh ui + chatFragment.refreshUiThread(false,1); + } + } + + private Message createAndSendTextMessage() { + // Receiver + String receiver = chatFragment.getChatBoxReceiverEditText().getText().toString(); + + // Payload + String messageContent = chatFragment.getChatBoxMessageEditText().getText().toString(); + chatFragment.getChatBoxMessageEditText().getText().clear(); + + // Receiver field length == 0 -> broadcast message + boolean isBroadcastMessage = receiver.length() == 0; + + Message chatMessage; + if (isBroadcastMessage) { + chatMessage = MessageFactory.createSendingTextBroadcastMessage(chatFragment.getUsername(), messageContent, System.currentTimeMillis()); + } else { + chatMessage = MessageFactory.createSendingTextPrivateMessage(chatFragment.getUsername(), receiver, messageContent, System.currentTimeMillis()); + } + + // If hubConnection is active then send message + chatMessage.startSendMessageTimer(chatFragment, r -> r.refreshUiThread(false,0)); + chatService.sendMessage(chatMessage); + + return chatMessage; + } +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/ChatContentAdapter.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/ChatContentAdapter.java new file mode 100644 index 00000000..5f324377 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/ChatContentAdapter.java @@ -0,0 +1,205 @@ +package com.microsoft.signalr.androidchatroom.fragment.chatrecyclerview; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.microsoft.signalr.androidchatroom.R; +import com.microsoft.signalr.androidchatroom.fragment.ChatUserInterface; +import com.microsoft.signalr.androidchatroom.message.Message; +import com.microsoft.signalr.androidchatroom.message.MessageTypeEnum; +import com.microsoft.signalr.androidchatroom.service.ChatService; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class ChatContentAdapter extends RecyclerView.Adapter { + private static final int READ_IMAGE_PRIVATE_MESSAGE_VIEW = 0; + private static final int READ_TEXT_PRIVATE_MESSAGE_VIEW = 1; + private static final int RECEIVED_IMAGE_BROADCAST_MESSAGE_VIEW = 2; + private static final int RECEIVED_IMAGE_PRIVATE_MESSAGE_VIEW = 3; + private static final int RECEIVED_TEXT_BROADCAST_MESSAGE_VIEW = 4; + private static final int RECEIVED_TEXT_PRIVATE_MESSAGE_VIEW = 5; + private static final int SYSTEM_MESSAGE_VIEW = 6; + private static final int SENDING_IMAGE_BROADCAST_MESSAGE_VIEW = 7; + private static final int SENDING_IMAGE_PRIVATE_MESSAGE_VIEW = 8; + private static final int SENDING_TEXT_BROADCAST_MESSAGE_VIEW = 9; + private static final int SENDING_TEXT_PRIVATE_MESSAGE_VIEW = 10; + private static final int SENT_IMAGE_BROADCAST_MESSAGE_VIEW = 11; + private static final int SENT_IMAGE_PRIVATE_MESSAGE_VIEW = 12; + private static final int SENT_TEXT_BROADCAST_MESSAGE_VIEW = 13; + private static final int SENT_TEXT_PRIVATE_MESSAGE_VIEW = 14; + + private final ChatUserInterface chatUserInterface; + private final ChatService chatService; + + + private final Context context; + private final List messages; + + + // Used for datetime formatting + private final SimpleDateFormat sdf = new SimpleDateFormat("MM/dd hh:mm:ss", Locale.US); + + public ChatContentAdapter(List messages, Context context, ChatUserInterface chatUserInterface, ChatService chatService) { + this.messages = messages; + this.context = context; + this.chatUserInterface = chatUserInterface; + this.chatService = chatService; + } + + @NonNull + @Override + public ChatContentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view; + + switch (viewType) { + case SENDING_TEXT_BROADCAST_MESSAGE_VIEW: + case SENT_TEXT_BROADCAST_MESSAGE_VIEW: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_self_text_broadcast_message, parent, false); + break; + case SENDING_TEXT_PRIVATE_MESSAGE_VIEW: + case SENT_TEXT_PRIVATE_MESSAGE_VIEW: + case READ_TEXT_PRIVATE_MESSAGE_VIEW: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_self_text_private_message, parent, false); + break; + case SENDING_IMAGE_BROADCAST_MESSAGE_VIEW: + case SENT_IMAGE_BROADCAST_MESSAGE_VIEW: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_self_image_broadcast_message, parent, false); + break; + case SENDING_IMAGE_PRIVATE_MESSAGE_VIEW: + case SENT_IMAGE_PRIVATE_MESSAGE_VIEW: + case READ_IMAGE_PRIVATE_MESSAGE_VIEW: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_self_image_private_message, parent, false); + break; + case RECEIVED_TEXT_BROADCAST_MESSAGE_VIEW: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_received_text_broadcast_message, parent, false); + break; + case RECEIVED_TEXT_PRIVATE_MESSAGE_VIEW: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_received_text_private_message, parent, false); + break; + case RECEIVED_IMAGE_BROADCAST_MESSAGE_VIEW: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_received_image_broadcast_message, parent, false); + break; + case RECEIVED_IMAGE_PRIVATE_MESSAGE_VIEW: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_received_image_private_message, parent, false); + break; + case SYSTEM_MESSAGE_VIEW: + default: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_system_message, parent, false); + break; + } + + return new ChatContentViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ChatContentViewHolder viewHolder, int position) { + Message message = messages.get(position); + + // System message directly set content and return + if (message.getMessageType() == MessageTypeEnum.SYSTEM_MESSAGE) { + viewHolder.systemMessageContent.setText(message.getPayload()); + return; + } + + // General chat message components + viewHolder.messageSender.setText(message.getSender()); + viewHolder.messageTime.setText(sdf.format(new Date(message.getTime()))); + + // Different types of message content + if (message.getMessageType().name().contains("TEXT")) { + // Normal text content + viewHolder.messageTextContent.setText(message.getPayload()); + } else if (message.getMessageType().name().contains("IMAGE") && message.getBmp() != null) { + // Loaded image content + Bitmap bmp = message.getBmp(); + int[] resized = resizeImage(bmp.getWidth(), bmp.getHeight(), 400); + viewHolder.messageImageContent.setImageBitmap(Bitmap.createScaledBitmap(bmp, resized[0], resized[1], false)); + } else if (message.getMessageType().name().contains("IMAGE") && message.getBmp() == null) { + viewHolder.messageImageContent.setImageResource(R.drawable.ic_ready_to_pull); + // Image content need to load + if (message.isPullImageTimeOut() && message.getBmp() == null) { + viewHolder.bindImageClick(message, v -> { + if (message.isPullImageTimeOut() && message.getBmp() == null) { + Log.d("Clicking image time=", new Date(message.getTime()).toString()); + message.startPullImageTimer(chatUserInterface, r -> r.refreshUiThread(false, 0)); + viewHolder.messageImageContent.setImageResource(R.drawable.ic_pulling); + chatService.pullImageContent(message.getMessageId()); + } + }); + } + } + + // Different types of message status + if (message.getMessageType().name().contains("SENDING")) { + if (message.isSendMessageTimeOut()) { + viewHolder.statusTextView.setText(R.string.message_resend); + viewHolder.bindStatusClick(message, v -> { + if (message.isSendMessageTimeOut()) { + viewHolder.statusTextView.setText(R.string.message_sending); + message.setTime(System.currentTimeMillis()); + message.startSendMessageTimer(chatUserInterface, r -> r.refreshUiThread(false, 0)); + chatService.sendMessage(message); + } + }); + } else { + viewHolder.statusTextView.setText(R.string.message_sending); + viewHolder.statusTextView.setOnClickListener(null); + } + } else if (message.getMessageType().name().contains("SENT")) { + if (message.getMessageType().name().contains("PRIVATE") && !message.isRead()) { + Log.d("unread sent private message:", message.getPayload()); + viewHolder.statusTextView.setText(R.string.message_sent); + } else { + viewHolder.statusTextView.setText(""); + } + } else if ( message.getMessageType().name().contains("READ") && + message.isRead()) { + if (isLastSelfMessage(message)) { + viewHolder.statusTextView.setText(R.string.message_read); + } else { + viewHolder.statusTextView.setText(R.string.message_empty); + } + } + } + + private boolean isLastSelfMessage(Message message) { + for (int index=messages.size() - 1; index>=0; index --) { + if (messages.get(index).getMessageType().name().contains("READ")) { + return messages.get(index).getMessageId().equals(message.getMessageId()); + } + } + return false; + } + + + @Override + public int getItemCount() { + return this.messages.size(); + } + + @Override + public int getItemViewType(int position) { + return messages.get(position).getMessageType().getValue(); + } + + private int[] resizeImage(int width, int height, int maxValue) { + if (width > height && width > maxValue) { + height = height * maxValue / width; + width = maxValue; + } else if (width < height && height > maxValue) { + width = width * maxValue / height; + height = maxValue; + } + return new int[]{width, height}; + } +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/ChatContentViewHolder.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/ChatContentViewHolder.java new file mode 100644 index 00000000..bf7ed638 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/ChatContentViewHolder.java @@ -0,0 +1,44 @@ +package com.microsoft.signalr.androidchatroom.fragment.chatrecyclerview; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import com.microsoft.signalr.androidchatroom.R; +import com.microsoft.signalr.androidchatroom.message.Message; + +public class ChatContentViewHolder extends RecyclerView.ViewHolder { + // For all non-system message + public final TextView messageSender; + public final TextView messageTime; + public final TextView statusTextView; + + // For Text Message + public final TextView messageTextContent; + + // For Image Message + public final ImageView messageImageContent; + + // For System Message + public final TextView systemMessageContent; + + ChatContentViewHolder(View view) { + super(view); + this.messageSender = view.findViewById(R.id.textview_message_sender); + this.messageTime = view.findViewById(R.id.textview_message_time); + this.statusTextView = view.findViewById(R.id.textview_message_status); + this.messageTextContent = view.findViewById(R.id.textview_message_content); + this.messageImageContent = view.findViewById(R.id.imageview_message_content); + this.systemMessageContent = view.findViewById(R.id.textview_enter_leave_content); + } + + public void bindImageClick(final Message message, final RecyclerViewItemClickListener listener) { + messageImageContent.setOnClickListener(v -> listener.onClickItem(message)); + } + + public void bindStatusClick(final Message message, final RecyclerViewItemClickListener listener) { + statusTextView.setOnClickListener(v -> listener.onClickItem(message)); + } +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/RecyclerViewItemClickListener.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/RecyclerViewItemClickListener.java new file mode 100644 index 00000000..9c4b84fe --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/fragment/chatrecyclerview/RecyclerViewItemClickListener.java @@ -0,0 +1,7 @@ +package com.microsoft.signalr.androidchatroom.fragment.chatrecyclerview; + +import com.microsoft.signalr.androidchatroom.message.Message; + +public interface RecyclerViewItemClickListener { + void onClickItem(Message message); +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/Message.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/Message.java new file mode 100644 index 00000000..febdcdc9 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/Message.java @@ -0,0 +1,196 @@ +package com.microsoft.signalr.androidchatroom.message; + +import android.graphics.Bitmap; + +import com.microsoft.signalr.androidchatroom.fragment.ChatUserInterface; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; + +public class Message { + public final static String BROADCAST_RECEIVER = "BCAST"; + public final static String SYSTEM_SENDER = "SYS"; + + private String messageId; + private MessageTypeEnum messageType; + private String sender; + private String receiver; + private String payload; + private long time; + + private boolean isRead = false; + + private Bitmap bmp; + + private Timer sendMessageTimer; + private boolean sendMessageTimeOut = false; + + private Timer pullImageTimer; + private boolean pullImageTimeOut = true; + + public Message(String messageId, MessageTypeEnum messageType) { + this.messageId = messageId; + this.messageType = messageType; + } + + public Message(MessageTypeEnum messageType) { + this.messageId = UUID.randomUUID().toString(); + this.messageType = messageType; + } + + public String getMessageId() { + return messageId; + } + + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + public MessageTypeEnum getMessageType() { + return messageType; + } + + public void setMessageType(MessageTypeEnum messageType) { + this.messageType = messageType; + } + + public String getSender() { + return sender; + } + + public void setSender(String sender) { + this.sender = sender; + } + + public String getReceiver() { + return receiver; + } + + public void setReceiver(String receiver) { + this.receiver = receiver; + } + + public boolean isImage() { + return this.messageType.name().contains("IMAGE"); + } + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + public long getTime() { + return time; + } + + public void setTime(long time) { + this.time = time; + } + + public Bitmap getBmp() { + return bmp; + } + + public void setBmp(Bitmap bmp) { + this.pullImageTimeOut = false; + this.bmp = bmp; + } + + public boolean isRead() { + return isRead; + } + + public void setRead(boolean read) { + isRead = read; + } + + public boolean isPullImageTimeOut() { + return this.pullImageTimeOut; + } + + public void startPullImageTimer(ChatUserInterface chatUserInterface, SimpleCallback callback) { + this.pullImageTimer = new Timer(); + this.pullImageTimeOut = false; + long localPullTime = System.currentTimeMillis(); + pullImageTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (!pullImageTimeOut && System.currentTimeMillis() - localPullTime > 5000) { + pullImageTimeOut = true; + cancel(); + callback.run(chatUserInterface); + } + } + }, 0, 200); + } + + public void ackPullImage() { + if (!pullImageTimeOut) { + if (pullImageTimer != null) { + pullImageTimer.cancel(); + } + } + } + + public boolean isSendMessageTimeOut() { + return this.sendMessageTimeOut; + } + + public void startSendMessageTimer(ChatUserInterface chatUserInterface, SimpleCallback callback) { + this.sendMessageTimer = new Timer(); + this.sendMessageTimeOut = false; + long localSendTime = this.time; + sendMessageTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (!sendMessageTimeOut && System.currentTimeMillis() - localSendTime > 5000) { + sendMessageTimeOut = true; + cancel(); + callback.run(chatUserInterface); + } + } + }, 0, 200); + } + + public void ack(long receivedTimeInLong) { + if (!sendMessageTimeOut) { + if (sendMessageTimer != null) { + sendMessageTimer.cancel(); + } + switch (messageType) { + case SENDING_TEXT_BROADCAST_MESSAGE: + messageType = MessageTypeEnum.SENT_TEXT_BROADCAST_MESSAGE; + break; + case SENDING_TEXT_PRIVATE_MESSAGE: + messageType = MessageTypeEnum.SENT_TEXT_PRIVATE_MESSAGE; + break; + case SENDING_IMAGE_BROADCAST_MESSAGE: + messageType = MessageTypeEnum.SENT_IMAGE_BROADCAST_MESSAGE; + break; + case SENDING_IMAGE_PRIVATE_MESSAGE: + messageType = MessageTypeEnum.SENT_IMAGE_PRIVATE_MESSAGE; + break; + default: + } + setTime(receivedTimeInLong); + } + } + + public void read() { + switch (messageType) { + case SENT_TEXT_PRIVATE_MESSAGE: + isRead = true; + messageType = MessageTypeEnum.READ_TEXT_PRIVATE_MESSAGE; + break; + case SENT_IMAGE_PRIVATE_MESSAGE: + isRead = true; + messageType = MessageTypeEnum.READ_IMAGE_PRIVATE_MESSAGE; + break; + default: + } + } +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/MessageFactory.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/MessageFactory.java new file mode 100644 index 00000000..5cbf7c74 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/MessageFactory.java @@ -0,0 +1,197 @@ +package com.microsoft.signalr.androidchatroom.message; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Locale; + +public class MessageFactory { + private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'", Locale.US); + private final static long utcOffset = 1000 * 3600 * 8; + private final static Gson gson = new Gson(); + + public static Message createReceivedTextBroadcastMessage(String messageId, String sender, String payload, long time) { + Message message = new Message(messageId, MessageTypeEnum.RECEIVED_TEXT_BROADCAST_MESSAGE); + message.setSender(sender); + message.setReceiver(Message.BROADCAST_RECEIVER); + message.setPayload(payload); + message.setTime(time); + return message; + } + + public static Message createReceivedImageBroadcastMessage(String messageId, String sender, String payload, long time) { + Message message = new Message(messageId, MessageTypeEnum.RECEIVED_IMAGE_BROADCAST_MESSAGE); + message.setSender(sender); + message.setReceiver(Message.BROADCAST_RECEIVER); + message.setPayload(payload); + message.setTime(time); + return message; + } + + public static Message createSendingTextBroadcastMessage(String sender, String payload, long time) { + Message message = new Message(MessageTypeEnum.SENDING_TEXT_BROADCAST_MESSAGE); + message.setSender(sender); + message.setReceiver(Message.BROADCAST_RECEIVER); + message.setPayload(payload); + message.setTime(time); + return message; + } + + public static Message createSendingImageBroadcastMessage(String sender, Bitmap bmp, long time, SimpleCallback callback) { + Message message = new Message(MessageTypeEnum.SENDING_IMAGE_BROADCAST_MESSAGE); + message.setSender(sender); + message.setReceiver(Message.BROADCAST_RECEIVER); + message.setBmp(bmp); + new Thread(() -> { + String payload = encodeToBase64(bmp); + message.setPayload(payload); + callback.run(message); + }).start(); + message.setTime(time); + return message; + } + + public static Message createReceivedTextPrivateMessage(String messageId, String sender, String receiver, String payload, long time) { + Message message = new Message(messageId, MessageTypeEnum.RECEIVED_TEXT_PRIVATE_MESSAGE); + message.setSender(sender); + message.setReceiver(receiver); + message.setPayload(payload); + message.setTime(time); + return message; + } + + public static Message createReceivedImagePrivateMessage(String messageId, String sender, String receiver, String payload, long time) { + Message message = new Message(messageId, MessageTypeEnum.RECEIVED_IMAGE_PRIVATE_MESSAGE); + message.setSender(sender); + message.setReceiver(receiver); + message.setPayload(payload); + message.setTime(time); + return message; + } + + public static Message createSendingTextPrivateMessage(String sender, String receiver, String payload, long time) { + Message message = new Message(MessageTypeEnum.SENDING_TEXT_PRIVATE_MESSAGE); + message.setSender(sender); + message.setReceiver(receiver); + message.setPayload(payload); + message.setTime(time); + return message; + } + + public static Message createSendingImagePrivateMessage(String sender, String receiver, Bitmap bmp, long time, SimpleCallback callback) { + Message message = new Message(MessageTypeEnum.SENDING_IMAGE_PRIVATE_MESSAGE); + message.setSender(sender); + message.setReceiver(receiver); + message.setBmp(bmp); + new Thread(() -> { + String payload = encodeToBase64(bmp); + message.setPayload(payload); + callback.run(message); + }).start(); + message.setTime(time); + return message; + } + + public static Message createReceivedSystemMessage(String messageId, String payload, long time) { + Message message = new Message(messageId, MessageTypeEnum.SYSTEM_MESSAGE); + message.setSender(Message.SYSTEM_SENDER); + message.setReceiver(Message.BROADCAST_RECEIVER); + message.setPayload(payload); + message.setTime(time); + return message; + } + + public static Message fromJsonObject(JsonObject jsonObject, String sessionUser) { + String messageId = jsonObject.get("MessageId").getAsString(); + String sender = jsonObject.get("Sender").getAsString(); + String receiver = jsonObject.get("Receiver").getAsString(); + String payload = jsonObject.get("Payload").getAsString(); + boolean isImage = jsonObject.get("IsImage").getAsBoolean(); + boolean isRead = jsonObject.get("IsRead").getAsBoolean(); + long time; + try { + time = sdf.parse(jsonObject.get("SendTime").getAsString()).getTime() + utcOffset; + } catch (Exception e) { + time = 0; + } + MessageTypeEnum type; + int rawType = jsonObject.get("Type").getAsInt(); + if (isImage) { + if (rawType == 0) { + if (sender.equals(sessionUser)) { + type = MessageTypeEnum.SENT_IMAGE_PRIVATE_MESSAGE; + } else { + type = MessageTypeEnum.RECEIVED_IMAGE_PRIVATE_MESSAGE; + } + } else { + if (sender.equals(sessionUser)) { + type = MessageTypeEnum.SENT_IMAGE_BROADCAST_MESSAGE; + } else { + type = MessageTypeEnum.RECEIVED_IMAGE_BROADCAST_MESSAGE; + } + } + } else { + if (rawType == 0) { + if (sender.equals(sessionUser)) { + type = MessageTypeEnum.SENT_TEXT_PRIVATE_MESSAGE; + } else { + type = MessageTypeEnum.RECEIVED_TEXT_PRIVATE_MESSAGE; + } + } else { + if (sender.equals(sessionUser)) { + type = MessageTypeEnum.SENT_TEXT_BROADCAST_MESSAGE; + } else { + type = MessageTypeEnum.RECEIVED_TEXT_BROADCAST_MESSAGE; + } + } + } + Message message = new Message(messageId, type); + message.setSender(sender); + message.setReceiver(receiver); + message.setPayload(payload); + message.setTime(time); + message.setRead(isRead); + return message; + } + + public static List parseHistoryMessages(String serializedString, String username) { + List historyMessages = new ArrayList<>(); + JsonArray jsonArray = gson.fromJson(serializedString, JsonArray.class); + for (JsonElement jsonElement : jsonArray) { + Message chatMessage = fromJsonObject(jsonElement.getAsJsonObject(), username); + historyMessages.add(chatMessage); + } + return historyMessages; + } + + public static String encodeToBase64(Bitmap bmp) { + // Encoding image into base64 string + String messageImageContent = ""; // Empty by default + try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { + bmp.compress(Bitmap.CompressFormat.JPEG, 25, stream); + byte[] byteArray = stream.toByteArray(); + messageImageContent = Base64.getEncoder().encodeToString(byteArray); + } catch (IOException ioe) { + Log.e("createImageMessage", ioe.getLocalizedMessage()); + } + return messageImageContent; + } + + public static Bitmap decodeToBitmap(String payload) { + byte[] byteArray = Base64.getDecoder().decode(payload); + Bitmap bmp = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); + return bmp; + } +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/MessageTypeEnum.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/MessageTypeEnum.java new file mode 100644 index 00000000..1a3d6807 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/MessageTypeEnum.java @@ -0,0 +1,29 @@ +package com.microsoft.signalr.androidchatroom.message; + +public enum MessageTypeEnum { + READ_IMAGE_PRIVATE_MESSAGE(0), + READ_TEXT_PRIVATE_MESSAGE(1), + RECEIVED_IMAGE_BROADCAST_MESSAGE(2), + RECEIVED_IMAGE_PRIVATE_MESSAGE(3), + RECEIVED_TEXT_BROADCAST_MESSAGE(4), + RECEIVED_TEXT_PRIVATE_MESSAGE(5), + SYSTEM_MESSAGE(6), + SENDING_IMAGE_BROADCAST_MESSAGE(7), + SENDING_IMAGE_PRIVATE_MESSAGE(8), + SENDING_TEXT_BROADCAST_MESSAGE(9), + SENDING_TEXT_PRIVATE_MESSAGE(10), + SENT_IMAGE_BROADCAST_MESSAGE(11), + SENT_IMAGE_PRIVATE_MESSAGE(12), + SENT_TEXT_BROADCAST_MESSAGE(13), + SENT_TEXT_PRIVATE_MESSAGE(14); + + private final int value; + + MessageTypeEnum(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/SimpleCallback.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/SimpleCallback.java new file mode 100644 index 00000000..80b88c00 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/message/SimpleCallback.java @@ -0,0 +1,5 @@ +package com.microsoft.signalr.androidchatroom.message; + +public interface SimpleCallback { + void run(T t); +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/ChatService.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/ChatService.java new file mode 100644 index 00000000..d0859d4a --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/ChatService.java @@ -0,0 +1,21 @@ +package com.microsoft.signalr.androidchatroom.service; + +import com.microsoft.signalr.androidchatroom.fragment.ChatUserInterface; +import com.microsoft.signalr.androidchatroom.message.Message; + +public interface ChatService { + //// Message sending methods + void sendMessage(Message chatMessage); + void sendMessageRead(String messageId); + + //// Session management methods + void startSession(); + void expireSession(boolean showAlert); + + //// register methods + void register(String username, String deviceUuid, ChatUserInterface chatUserInterface); + + //// Pulling methods + void pullHistoryMessages(long untilTimeInLong); + void pullImageContent(String messageId); +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/FirebaseService.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/FirebaseService.java new file mode 100644 index 00000000..7cdc989b --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/FirebaseService.java @@ -0,0 +1,111 @@ +package com.microsoft.signalr.androidchatroom.service; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat.Builder; +import androidx.core.app.NotificationCompat; + +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; +import com.microsoft.signalr.androidchatroom.activity.MainActivity; + +import java.util.Iterator; + + +// See https://docs.microsoft.com/en-us/azure/notification-hubs/notification-hubs-android-push-notification-google-fcm-get-started#test-send-notification-from-the-notification-hub +public class FirebaseService extends FirebaseMessagingService { + private static final String TAG = "FirebaseService"; + public static final String NOTIFICATION_CHANNEL_ID = "id_chatroom"; + public static final String NOTIFICATION_CHANNEL_NAME = "ChatRoom Channel"; + public static final String NOTIFICATION_CHANNEL_DESCRIPTION = "ChatRoom Channel Description"; + public static final int NOTIFICATION_ID = 1; + + private NotificationManager notificationManager; + private Builder builder; + + @Override + public void onNewToken(@NonNull String s) { + super.onNewToken(s); + } + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + // ... + + // TODO(developer): Handle FCM messages here. + + + // Not getting messages here? See why this may be: https://goo.gl/39bRNJ + Log.d(TAG, "From: " + remoteMessage.getFrom()); + + String title = null, text; + // Check if message contains a notification payload. + if (remoteMessage.getNotification() != null) { + Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody()); + text = remoteMessage.getNotification().getBody(); + } else { + Iterator dataPayloadIterator = remoteMessage.getData().values().iterator(); + title = dataPayloadIterator.next(); + text = dataPayloadIterator.next(); + } + + // Also if you intend on generating your own notifications as a result of a received FCM + // message, here is where that should be initiated. See sendNotification method below. + if (!MainActivity.isVisible) { + sendNotification(title, text); + } + + } + + private void sendNotification(String title, String text) { + + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + + notificationManager = (NotificationManager) + this.getSystemService(Context.NOTIFICATION_SERVICE); + + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, + intent, PendingIntent.FLAG_ONE_SHOT); + + Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( + this, + NOTIFICATION_CHANNEL_ID) + .setContentText(text) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setSmallIcon(android.R.drawable.ic_popup_reminder) + .setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL) + .setAutoCancel(true); + if (title != null) { + notificationBuilder.setContentTitle(title); + } + notificationBuilder.setContentIntent(contentIntent); + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } + + public static void createChannelAndHandleNotifications(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH); + channel.setDescription(NOTIFICATION_CHANNEL_DESCRIPTION); + channel.setShowBadge(true); + + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } +} \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/NotificationService.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/NotificationService.java new file mode 100644 index 00000000..1c6efc7f --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/NotificationService.java @@ -0,0 +1,79 @@ +package com.microsoft.signalr.androidchatroom.service; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.google.android.gms.tasks.OnCanceledListener; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; +import com.microsoft.signalr.androidchatroom.R; +import com.microsoft.windowsazure.messaging.NotificationHub; + +import java.util.Arrays; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + + +// See https://docs.microsoft.com/en-us/azure/notification-hubs/notification-hubs-android-push-notification-google-fcm-get-started#test-send-notification-from-the-notification-hub +public class NotificationService extends Service { + + private static final String TAG = "NotificationService"; + private final String deviceUuid = UUID.randomUUID().toString(); + private String deviceToken; + private String registrationId; + private NotificationHub notificationHub; + + + // Service binder + private final IBinder notificationServiceBinder = new NotificationService.NotificationServiceBinder(); + public class NotificationServiceBinder extends Binder { + public NotificationService getService() { + return NotificationService.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(InstanceIdResult instanceIdResult) { + deviceToken = instanceIdResult.getToken(); + notificationHub = new NotificationHub(getString(R.string.azure_notification_hub_name), + getString(R.string.azure_notification_hub_connection_string), NotificationService.this); + new Thread() { + @Override + public void run() { + try { + Log.d(TAG, String.format("Register with deviceToken: %s; tag: %s", deviceToken, deviceUuid)); + registrationId = notificationHub.register(deviceToken, deviceUuid).getRegistrationId(); + } catch (Exception e) { + Log.d(TAG, Arrays.toString(e.getStackTrace())); + } + } + }.start(); + } + }); + return notificationServiceBinder; + } + + public String getDeviceToken() { + return deviceToken; + } + + public String getRegistrationId() { + return registrationId; + } + + public String getDeviceUuid() { + return deviceUuid; + } +} \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/SignalRChatService.java b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/SignalRChatService.java new file mode 100644 index 00000000..313af770 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/java/com/microsoft/signalr/androidchatroom/service/SignalRChatService.java @@ -0,0 +1,349 @@ +package com.microsoft.signalr.androidchatroom.service; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.microsoft.signalr.HubConnection; +import com.microsoft.signalr.HubConnectionBuilder; +import com.microsoft.signalr.HubConnectionState; +import com.microsoft.signalr.androidchatroom.R; +import com.microsoft.signalr.androidchatroom.fragment.ChatUserInterface; +import com.microsoft.signalr.androidchatroom.message.Message; +import com.microsoft.signalr.androidchatroom.message.MessageFactory; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.CompletableObserver; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; + +public class SignalRChatService extends Service implements ChatService { + // SignalR HubConnection + private HubConnection hubConnection; + private ChatUserInterface chatUserInterface; + + // User info + private String username; + private String deviceUuid; + + // Reconnect timer + private boolean firstPull = true; + private boolean activePull = false; + private final AtomicBoolean sessionStarted = new AtomicBoolean(false); + private final int reconnectDelay = 0; // immediate connect to server when enter the chat room + private final int reconnectInterval = 5000; + private Timer reconnectTimer; + + // Service binder + private final IBinder chatServiceBinder = new ChatServiceBinder(); + + public class ChatServiceBinder extends Binder { + public SignalRChatService getService() { + return SignalRChatService.this; + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return chatServiceBinder; + } + + ///////// Session managers + + @Override + public void register(String username, String deviceUuid, ChatUserInterface chatUserInterface) { + this.username = username; + this.deviceUuid = deviceUuid; + this.chatUserInterface = chatUserInterface; + } + + @Override + public void startSession() { + if (!sessionStarted.get()) { + synchronized (sessionStarted) { + if (!sessionStarted.get()) { + // Create hub connection + this.hubConnection = HubConnectionBuilder.create(getString(R.string.app_server_url)).build(); + + // Start hub connection + this.hubConnection.start().subscribe(new CompletableObserver() { + @Override + public void onSubscribe(@NonNull Disposable d) { + + } + + @Override + public void onComplete() { + // When completed start process, register methods and start guard thread + onSessionStart(); + } + + @Override + public void onError(@NonNull Throwable e) { + + } + }); + } + } + } + } + + private void onSessionStart() { + // Set atomic boolean status + sessionStarted.set(true); + + // Register message receivers + hubConnection.on("receiveSystemMessage", this::receiveSystemMessage, + String.class, String.class, Long.class); + hubConnection.on("receiveBroadcastMessage", this::receiveBroadcastMessage, + String.class, String.class, String.class, String.class, Boolean.class, Long.class, String.class); + hubConnection.on("receivePrivateMessage", this::receivePrivateMessage, + String.class, String.class, String.class, String.class, Boolean.class, Long.class, String.class); + + // Register rich content receivers + hubConnection.on("receiveHistoryMessages", this::receiveHistoryMessages, String.class); + hubConnection.on("receiveImageContent", this::receiveImageContent, String.class, String.class); + + // Register operation receivers + hubConnection.on("serverAck", this::serverAck, String.class, Long.class); + hubConnection.on("clientRead", this::clientRead, String.class, String.class); + hubConnection.on("expireSession", this::expireSession, Boolean.class); + + // Activate UI click listeners + chatUserInterface.activateClickEvent(); + + // Broadcast enter chat room + hubConnection.send("EnterChatRoom", deviceUuid, username); + + // start guard thread for reconnecting + reconnectTimer = new Timer(); + reconnectTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + connectToServer(); + } + }, reconnectDelay, reconnectInterval); + + } + + @Override + public void expireSession(boolean showAlert) { + if (sessionStarted.get()) { + synchronized (sessionStarted) { + if (sessionStarted.get() && hubConnection.getConnectionState() == HubConnectionState.CONNECTED) { + reconnectTimer.cancel(); + + hubConnection.invoke("LeaveChatRoom", deviceUuid, username).subscribe(new CompletableObserver() { + @Override + public void onSubscribe(@NonNull Disposable d) { + + } + + @Override + public void onComplete() { + hubConnection.stop(); + sessionStarted.set(false); + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e("HubConnection", e.toString()); + hubConnection.stop(); + sessionStarted.set(false); + } + }); + } + } + } + if (showAlert) { + Log.d("expireSession", "Server expired the session"); + chatUserInterface.showSessionExpiredDialog(); + } else { + Log.d("expireSession", "Manually quited the session"); + } + } + + ///////// Receivers + + /// Message receivers + private void receiveSystemMessage(String messageId, String payload, long sendTime) { + Log.d("receiveSystemMessage", payload); + + // Create message + Message systemMessage = MessageFactory.createReceivedSystemMessage(messageId, payload, sendTime); + + // Try to add message to fragment + chatUserInterface.tryAddMessage(systemMessage, 1); + } + + private void receiveBroadcastMessage(String messageId, String sender, String receiver, String payload, boolean isImage, long sendTime, String ackId) { + Log.d("receiveBroadcastMessage", sender); + + // Send back ack + hubConnection.send("OnAckResponseReceived", ackId, username); + + // Create message + Message chatMessage; + if (isImage) { + chatMessage = MessageFactory.createReceivedImageBroadcastMessage(messageId, sender, payload,sendTime); + } else { + chatMessage = MessageFactory.createReceivedTextBroadcastMessage(messageId, sender, payload, sendTime); + } + + // Try to add message to fragment + chatUserInterface.tryAddMessage(chatMessage, 1); + } + + private void receivePrivateMessage(String messageId, String sender, String receiver, String payload, boolean isImage, long sendTime, String ackId) { + Log.d("receivePrivateMessage", sender); + + // Send back ack + hubConnection.send("OnAckResponseReceived", ackId, username); + + // Create message + Message chatMessage; + if (isImage) { + chatMessage = MessageFactory.createReceivedImagePrivateMessage(messageId, sender, receiver, payload,sendTime); + } else { + chatMessage = MessageFactory.createReceivedTextPrivateMessage(messageId, sender, receiver, payload, sendTime); + } + + // Try to add message to fragment + chatUserInterface.tryAddMessage(chatMessage, 1); + } + + /// Rich content receivers + private void receiveImageContent(String messageId, String payload) { + chatUserInterface.setImageContent(messageId, payload); + } + + private void receiveHistoryMessages(String serializedString) { + Log.d("receiveHistoryMessages", serializedString); + List historyMessages = MessageFactory.parseHistoryMessages(serializedString, username); + + if (firstPull) { + chatUserInterface.tryAddAllMessages(historyMessages, 1); + firstPull = false; + } else { + chatUserInterface.tryAddAllMessages(historyMessages, 0); + } + + activePull = false; + } + + private void serverAck(String messageId, long receivedTimeInLong) { + chatUserInterface.setSentMessageAck(messageId, receivedTimeInLong); + } + + private void clientRead(String messageId, String username) { + chatUserInterface.setSentMessageRead(messageId); + } + + ///////// Senders + + /// Message sending methods + @Override + public void sendMessage(Message chatMessage) { + synchronized (chatMessage) { + switch (chatMessage.getMessageType()) { + case SENDING_TEXT_BROADCAST_MESSAGE: + case SENDING_IMAGE_BROADCAST_MESSAGE: + sendBroadcastMessage(chatMessage); + break; + case SENDING_TEXT_PRIVATE_MESSAGE: + case SENDING_IMAGE_PRIVATE_MESSAGE: + sendPrivateMessage(chatMessage); + break; + default: + } + } + } + + @Override + public void sendMessageRead(String messageId) { + hubConnection.send("OnReadResponseReceived", messageId, username); + } + + private void sendBroadcastMessage(Message broadcastMessage) { + Log.d("SEND BCAST MESSAGE", broadcastMessage.toString()); + if (hubConnection.getConnectionState() == HubConnectionState.CONNECTED) { + if (broadcastMessage.isImage() && broadcastMessage.getBmp()!=null || + !broadcastMessage.isImage()) { + hubConnection.send("OnBroadcastMessageReceived", + broadcastMessage.getMessageId(), + broadcastMessage.getSender(), + broadcastMessage.getPayload(), + broadcastMessage.isImage()); + } + } + } + + private void sendPrivateMessage(Message privateMessage) { + Log.d("SEND PRIVATE MESSAGE", privateMessage.toString()); + if (hubConnection.getConnectionState() == HubConnectionState.CONNECTED) { + if (privateMessage.isImage() && privateMessage.getBmp()!=null || + !privateMessage.isImage()) { + hubConnection.send("OnPrivateMessageReceived", + privateMessage.getMessageId(), + privateMessage.getSender(), + privateMessage.getReceiver(), + privateMessage.getPayload(), + privateMessage.isImage()); + } + } + } + + //// Pulling message methods + @Override + public void pullHistoryMessages(long untilTimeInLong) { + if (hubConnection.getConnectionState() == HubConnectionState.CONNECTED && !activePull) { + activePull = true; + Log.d("pullHistoryMessages", "Called with long time: " + untilTimeInLong); + hubConnection.send("OnPullHistoryMessagesReceived", username, untilTimeInLong); + } + } + + @Override + public void pullImageContent(String messageId) { + if (hubConnection.getConnectionState() == HubConnectionState.CONNECTED) { + Log.d("pullImageContent", "message id=" + messageId); + hubConnection.send("OnPullImageContentReceived", username, messageId); + } + } + + // Guard thread method + private void connectToServer() { + if (hubConnection.getConnectionState() != HubConnectionState.CONNECTED) { + hubConnection.start().subscribe(new CompletableObserver() { + @Override + public void onSubscribe(@NotNull Disposable d) { + + } + + @Override + public void onComplete() { + hubConnection.send("TouchServer", deviceUuid, username); + } + + @Override + public void onError(@NotNull Throwable e) { + Log.e("HubConnection", e.toString()); + } + }); + } else { + // If connected, must be in an active session. Directly call TouchServer + hubConnection.send("TouchServer", deviceUuid, username); + } + } +} diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/enter_leave_message_bubble.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/enter_leave_message_bubble.xml new file mode 100644 index 00000000..e261f82d --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/enter_leave_message_bubble.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_launcher_background.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_pulling.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_pulling.xml new file mode 100644 index 00000000..408387a5 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_pulling.xml @@ -0,0 +1,12 @@ + + + + diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_ready_to_pull.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_ready_to_pull.xml new file mode 100644 index 00000000..cadc070f --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/ic_ready_to_pull.xml @@ -0,0 +1,9 @@ + + + diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/received_broadcast_message_bubble.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/received_broadcast_message_bubble.xml new file mode 100644 index 00000000..4aa93c0b --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/received_broadcast_message_bubble.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/received_private_message_bubble.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/received_private_message_bubble.xml new file mode 100644 index 00000000..e261f82d --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/received_private_message_bubble.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/self_message_bubble.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/self_message_bubble.xml new file mode 100644 index 00000000..42a9e217 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/self_message_bubble.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/sent_private_message_bubble.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/sent_private_message_bubble.xml new file mode 100644 index 00000000..e261f82d --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/drawable/sent_private_message_bubble.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/activity_main.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..f042781f --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/content_main.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/content_main.xml new file mode 100644 index 00000000..c4e7db67 --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/content_main.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/fragment_chat.xml b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/fragment_chat.xml new file mode 100644 index 00000000..337a8aac --- /dev/null +++ b/samples/MobileChatRoom/AndroidChatRoomClient/app/src/main/res/layout/fragment_chat.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + +