From 226c00e5e1f866893bd423b7a0d81a113505b9e7 Mon Sep 17 00:00:00 2001 From: Adit Modhvadia Date: Tue, 10 Mar 2020 22:38:06 -0400 Subject: [PATCH] Release v1.0.0 (#8) * Initial project setup * - project structure set up * - add BaseActivity and BaseFragment * - set up FireBase library module * - add apiManager instance to BaseActivity and BaseFragment - add Crashlytics to app * - add Timber for logging - add version tag display to SplashActivity - add flow for Login/Landing redirection - add Application class with Timber * - add test case for SplashActivity - add RegistrationActivity * - registered and create user with email and password on successfully entering details - add test cases for AppUtils and RegistrationActivity * - add LandingActivity * - add LoginActivity and complete it's flow - add tests for Login and Registration flow * - add Chat room display to LandingActivity * - add ChatActivity with different views for sent and received messages * - setup dummy data for messages - add method to add message to RecyclerView and scroll to it - pass ChatRoom to ChatActivity from LandingActivity * - add room dependencies - add boilerplate code for room - add messages to room and retrieve from it as well - add ability to delete chat room messages from options * - add NetworkManager to application - add QueryResponseMessage POJO - successfully call API from application * - fix LoginActivity and RegistrationActivity flow * - add API call for querying backend - integrate api call flow and display of query response to user - add static id and Names for ChatRoom * - change GET request to POST and send message query with it * - add method to store new sent messages and query response messages to FireStore * - add IntentService to store messages in FireBase and sync messages from FireBase - on logout all local messages are deleted - on login all messages are synced from FireBase * - add static method to Message.java to convert given message into a HashMap for storing in FireStore - add WorkManager for Syncing local and FireStore messages once daily * Ui/ui updates refinement (#7) * - add new color palette for application * - add logo to all the chat rooms * - add app name to SplashActivity - add fade in and fade out transition to SplashActivity - remove windowPreview from app * - fix margin issues in chat activity * - add first name and last name in registration - display first name of user instead of email address in LandingActivity * - add standard button style - * - upgrade gradle build tools version * - add material chips to show query message hints * - final commit * - update develop branch * - update documentation * Update README.md --- .idea/.name | 1 + .idea/codeStyles/Project.xml | 113 ++++ .idea/misc.xml | 6 + .idea/modules.xml | 10 + .idea/runConfigurations.xml | 12 + .idea/vcs.xml | 6 + README.md | 69 ++- app/.gitignore | 1 + app/build.gradle | 63 +++ app/proguard-rules.pro | 21 + .../ExampleInstrumentedTest.java | 27 + .../ui/login/LoginActivityTest.java | 41 ++ .../RegistrationActivityTest.java | 54 ++ .../ui/splash/SplashActivityTest.java | 31 ++ app/src/main/AndroidManifest.xml | 42 ++ .../chatbotmetcs622/ChatBotApp.java | 46 ++ .../chatbotmetcs622/database/BaseDao.java | 32 ++ .../database/ChatBotDatabase.java | 32 ++ .../database/messages/Message.java | 100 ++++ .../database/messages/MessageDao.java | 36 ++ .../intentservice/FireBaseIntentService.java | 144 +++++ .../chatbotmetcs622/models/ChatRoom.java | 48 ++ .../chatbotmetcs622/network/ApiManager.java | 112 ++++ .../network/NetworkManager.java | 498 ++++++++++++++++++ .../network/handlers/NetworkCallback.java | 25 + .../network/handlers/NetworkWrapper.java | 152 ++++++ .../network/models/ChatBotError.java | 60 +++ .../network/models/NetCompoundRes.java | 37 ++ .../network/models/NetError.java | 114 ++++ .../network/models/NetResponse.java | 20 + .../request/MessageQueryRequestModel.java | 22 + .../models/response/QueryResponseMessage.java | 36 ++ .../network/utils/CoreUtils.java | 43 ++ .../repositories/MessageRepository.java | 427 +++++++++++++++ .../chatbotmetcs622/ui/base/BaseActivity.java | 127 +++++ .../chatbotmetcs622/ui/base/BaseFragment.java | 79 +++ .../chatbotmetcs622/ui/chat/ChatActivity.java | 193 +++++++ .../ui/chat/ChatListAdapter.java | 128 +++++ .../ui/landing/ChatSelectionListAdapter.java | 92 ++++ .../ui/landing/LandingActivity.java | 99 ++++ .../ui/login/LoginActivity.java | 143 +++++ .../ui/registration/RegistrationActivity.java | 150 ++++++ .../ui/splash/SplashActivity.java | 149 ++++++ .../chatbotmetcs622/utils/AppUtils.java | 48 ++ .../workers/FireBaseSyncWorker.java | 30 ++ app/src/main/res/anim/fade_in.xml | 8 + app/src/main/res/anim/fade_out.xml | 8 + app/src/main/res/drawable-anydpi/ic_send.xml | 11 + app/src/main/res/drawable-hdpi/ic_send.png | Bin 0 -> 272 bytes app/src/main/res/drawable-mdpi/ic_send.png | Bin 0 -> 202 bytes .../drawable-v24/ic_launcher_foreground.xml | 34 ++ app/src/main/res/drawable-xhdpi/ic_send.png | Bin 0 -> 315 bytes app/src/main/res/drawable-xxhdpi/ic_send.png | Bin 0 -> 422 bytes .../main/res/drawable/brute_force_logo.jpg | Bin 0 -> 20189 bytes .../res/drawable/button_round_primary.xml | 6 + .../res/drawable/ic_launcher_background.xml | 170 ++++++ app/src/main/res/drawable/lucene_logo.png | Bin 0 -> 82426 bytes app/src/main/res/drawable/mongodb_logo.png | Bin 0 -> 54883 bytes app/src/main/res/drawable/mysql_logo.jpg | Bin 0 -> 65346 bytes .../main/res/drawable/receiver_message_bg.xml | 8 + .../main/res/drawable/sender_message_bg.xml | 6 + app/src/main/res/font/aclonica.xml | 7 + app/src/main/res/layout/activity_chat.xml | 72 +++ app/src/main/res/layout/activity_landing.xml | 30 ++ app/src/main/res/layout/activity_login.xml | 112 ++++ .../main/res/layout/activity_registration.xml | 193 +++++++ app/src/main/res/layout/activity_splash.xml | 38 ++ .../layout/chat_room_display_view_item.xml | 57 ++ .../receiver_message_display_view_item.xml | 40 ++ .../sender_message_display_view_item.xml | 45 ++ app/src/main/res/menu/menu_chat.xml | 8 + app/src/main/res/menu/menu_landing.xml | 9 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes app/src/main/res/values/colors.xml | 8 + app/src/main/res/values/dimens.xml | 10 + app/src/main/res/values/font_certs.xml | 17 + app/src/main/res/values/preloaded_fonts.xml | 6 + app/src/main/res/values/strings.xml | 42 ++ app/src/main/res/values/styles.xml | 19 + .../chatbotmetcs622/ExampleUnitTest.java | 17 + .../chatbotmetcs622/utils/AppUtilsTest.java | 6 + build.gradle | 31 ++ firebase-api-library/.gitignore | 1 + firebase-api-library/build.gradle | 41 ++ firebase-api-library/consumer-rules.pro | 0 firebase-api-library/proguard-rules.pro | 21 + .../ExampleInstrumentedTest.java | 27 + .../src/main/AndroidManifest.xml | 2 + .../api/FireBaseApiManager.java | 227 ++++++++ .../api/FireBaseApiWrapper.java | 110 ++++ .../api/FireBaseApiWrapperInterface.java | 27 + .../listeners/DBValueListener.java | 8 + .../listeners/OnTaskCompleteListener.java | 10 + .../src/main/res/values/strings.xml | 3 + .../firebase_api_library/ExampleUnitTest.java | 17 + gradle.properties | 20 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++++ gradlew.bat | 84 +++ settings.gradle | 2 + 112 files changed, 5523 insertions(+), 2 deletions(-) create mode 100644 .idea/.name create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ExampleInstrumentedTest.java create mode 100644 app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/login/LoginActivityTest.java create mode 100644 app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/registration/RegistrationActivityTest.java create mode 100644 app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/splash/SplashActivityTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/ChatBotApp.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/database/BaseDao.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/database/ChatBotDatabase.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/database/messages/Message.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/database/messages/MessageDao.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/intentservice/FireBaseIntentService.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/models/ChatRoom.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/ApiManager.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/NetworkManager.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/handlers/NetworkCallback.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/handlers/NetworkWrapper.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/ChatBotError.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetCompoundRes.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetError.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetResponse.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/request/MessageQueryRequestModel.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/response/QueryResponseMessage.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/network/utils/CoreUtils.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/repositories/MessageRepository.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/ui/base/BaseActivity.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/ui/base/BaseFragment.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/ui/chat/ChatActivity.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/ui/chat/ChatListAdapter.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/ui/landing/ChatSelectionListAdapter.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/ui/landing/LandingActivity.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/ui/login/LoginActivity.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/ui/registration/RegistrationActivity.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/ui/splash/SplashActivity.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/utils/AppUtils.java create mode 100644 app/src/main/java/com/fazemeright/chatbotmetcs622/workers/FireBaseSyncWorker.java create mode 100644 app/src/main/res/anim/fade_in.xml create mode 100644 app/src/main/res/anim/fade_out.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_send.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_send.png create mode 100644 app/src/main/res/drawable-mdpi/ic_send.png create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable-xhdpi/ic_send.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_send.png create mode 100644 app/src/main/res/drawable/brute_force_logo.jpg create mode 100644 app/src/main/res/drawable/button_round_primary.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/lucene_logo.png create mode 100644 app/src/main/res/drawable/mongodb_logo.png create mode 100644 app/src/main/res/drawable/mysql_logo.jpg create mode 100644 app/src/main/res/drawable/receiver_message_bg.xml create mode 100644 app/src/main/res/drawable/sender_message_bg.xml create mode 100644 app/src/main/res/font/aclonica.xml create mode 100644 app/src/main/res/layout/activity_chat.xml create mode 100644 app/src/main/res/layout/activity_landing.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/activity_registration.xml create mode 100644 app/src/main/res/layout/activity_splash.xml create mode 100644 app/src/main/res/layout/chat_room_display_view_item.xml create mode 100644 app/src/main/res/layout/receiver_message_display_view_item.xml create mode 100644 app/src/main/res/layout/sender_message_display_view_item.xml create mode 100644 app/src/main/res/menu/menu_chat.xml create mode 100644 app/src/main/res/menu/menu_landing.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/font_certs.xml create mode 100644 app/src/main/res/values/preloaded_fonts.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/com/fazemeright/chatbotmetcs622/ExampleUnitTest.java create mode 100644 app/src/test/java/com/fazemeright/chatbotmetcs622/utils/AppUtilsTest.java create mode 100644 build.gradle create mode 100644 firebase-api-library/.gitignore create mode 100644 firebase-api-library/build.gradle create mode 100644 firebase-api-library/consumer-rules.pro create mode 100644 firebase-api-library/proguard-rules.pro create mode 100644 firebase-api-library/src/androidTest/java/com/fazemeright/firebase_api_library/ExampleInstrumentedTest.java create mode 100644 firebase-api-library/src/main/AndroidManifest.xml create mode 100644 firebase-api-library/src/main/java/com/fazemeright/firebase_api_library/api/FireBaseApiManager.java create mode 100644 firebase-api-library/src/main/java/com/fazemeright/firebase_api_library/api/FireBaseApiWrapper.java create mode 100644 firebase-api-library/src/main/java/com/fazemeright/firebase_api_library/api/FireBaseApiWrapperInterface.java create mode 100644 firebase-api-library/src/main/java/com/fazemeright/firebase_api_library/listeners/DBValueListener.java create mode 100644 firebase-api-library/src/main/java/com/fazemeright/firebase_api_library/listeners/OnTaskCompleteListener.java create mode 100644 firebase-api-library/src/main/res/values/strings.xml create mode 100644 firebase-api-library/src/test/java/com/fazemeright/firebase_api_library/ExampleUnitTest.java create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..53671a8 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +ChatBot MET CS622 \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..ae78c11 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,113 @@ + + + + + +
+ + + + 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/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..21e588e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..b165b06 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 95da3c7..17e4a5f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,67 @@ -# MET-CS622-ChatBot-Project -An Android chat bot for MET CS622 final Project +# MET-CS622-ChatBot-Project (Chat Bot) +An Android chat bot for MET CS622 final Project which queries smartwatch database to give user insights on their activity. + +## Getting Started + +These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. + +### Prerequisites + +* Please fork the repository and work from it so that I can incoorporate the changes you make or if you add any new features. +* You need to have the googleservices.json file to link to firebase, you can set it up using [Set up Firebase](https://firebase.google.com/docs/android/setup) or email me on (adit.modhvadia@gmail.com) for further instructions. +* You need to clone the backend for this project as well which you can find here [MET-CS622-ChatBot-Backend](https://github.com/aditmodhvadia/MET-CS622-ChatBot-Project-Backend). Instructions on how to clone it are given below. + +### Installing + +* Fork and clone both the front end Android repo and the back end Java server. + * [MET-CS622-ChatBot-Android-App](https://github.com/aditmodhvadia/MET-CS622-ChatBot-Project.git) + * [MET-CS622-ChatBot-Backend](https://github.com/aditmodhvadia/MET-CS622-ChatBot-Project-Backend) +* Simply run the following command from your terminal to get the source code on your system in the desired directory. + +``` +git clone https://github.com//MET-CS622-ChatBot-Project.git +``` +``` +git clone https://github.com//MET-CS622-ChatBot-Project-Backend.git +``` + +* Or you can fork this repository and then create a pull request for implementing the changes + +* If you want to install the release apk on your android device then go to the release tab of the repo, and download the apk from the latest release. + +## Deployment + +* All releases would be ready to be isntalled on supported android devices +* This app is for education/research purpose only and hence won't be released on the playstore as of yet, prior announcement will be made. + +## Features +* Run queries to database via chat messages. +* Messages are stored both locally and on cloud. +* User authentication allows any user to log in and retrieve previously typed messages and responses. +* Built with MVVM hence easy to maintain, test and easy to pickup. + +## Built With + +* [Firebase](https://firebase.google.com/) - A comprehensive mobile development platform, go serverless with firebase +* [Maven](https://maven.apache.org/) - Dependency Management +* [Git](https://git-scm.com/downloads) - Used for version control +* [FastAndroidNetworking](https://github.com/amitshekhariitbhu/Fast-Android-Networking) - Used FAN API to call REST APIs +* [Timber](https://github.com/JakeWharton/timber) - Used for logging + +## Contributing + +Will update the requirements and code of conduct soon. + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/aditmodhvadia/MET-CS622-ChatBot-Project/tags). + +## Authors + +* **Adit Modhvadia** - *Initial work* - [aditmodhvadia](https://github.com/aditmodhvadia/) + +See also the list of [contributors](https://github.com/aditmodhvadia/MET-CS622-ChatBot-Project/contributors) who participated in this project. + +## License + +This project is licensed under the GPL License - see the [LICENSE](https://github.com/aditmodhvadia/Canteen_App/blob/master/LICENSE) file for details diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..e4abdd2 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,63 @@ +apply plugin: 'com.android.application' +apply plugin: 'com.google.gms.google-services' +apply plugin: 'io.fabric' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + defaultConfig { + applicationId "com.fazemeright.chatbotmetcs622" + minSdkVersion 22 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.google.android.material:material:1.0.0' + implementation 'com.jakewharton.timber:timber:4.7.1' // logging library + +// testing libraries + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test:core:1.2.0' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + // Testing-only dependencies +/* testImplementation 'androidx.test:core:1.2.0' + testImplementation 'androidx.test.ext:junit:1.1.1' + testImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation 'androidx.test.espresso:espresso-intents:3.2.0'*/ + + // Room and Lifecycle dependencies + implementation "androidx.room:room-runtime:2.2.1" + annotationProcessor "androidx.room:room-compiler:2.2.1" + implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" + +// WorkManager Library + implementation ("androidx.work:work-runtime:2.2.0") { + exclude group: 'com.google.guava', module: 'listenablefuture' + } + implementation 'com.google.guava:guava:27.0.1-android' + + +// Networking libraries + implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.amitshekhar.android:android-networking:1.0.2' + + + implementation project(':firebase-api-library') +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/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 diff --git a/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ExampleInstrumentedTest.java new file mode 100644 index 0000000..fcb058d --- /dev/null +++ b/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package com.fazemeright.chatbotmetcs622; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.fazemeright.chatbotmetcs622", appContext.getPackageName()); + } +} diff --git a/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/login/LoginActivityTest.java b/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/login/LoginActivityTest.java new file mode 100644 index 0000000..db3d688 --- /dev/null +++ b/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/login/LoginActivityTest.java @@ -0,0 +1,41 @@ +package com.fazemeright.chatbotmetcs622.ui.login; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; + +import com.fazemeright.chatbotmetcs622.R; +import com.fazemeright.chatbotmetcs622.ui.registration.RegistrationActivityTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; +import static androidx.test.espresso.action.ViewActions.typeText; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static org.hamcrest.core.IsNot.not; + +@RunWith(AndroidJUnit4.class) +public class LoginActivityTest { + + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>(LoginActivity.class); + + @Test + public void login_with_email_password_isCorrect() { +// onView(withId(R.id.tvHaveAccount)).check(matches(isDisplayed())); +// onView(withId(R.id.tvHaveAccount)).perform(click()); + onView(withId(R.id.btnLogin)).check(matches(isDisplayed())); + onView(withId(R.id.userLoginEmailEditText)).perform(typeText(RegistrationActivityTest.CORRECT_EMAIL_ADDRESS)); + onView(withId(R.id.userPasswordEditText)).perform(typeText(RegistrationActivityTest.CORRECT_PASSWORD), closeSoftKeyboard()); + onView(withId(R.id.btnLogin)).perform(click()); +// TODO: Add IdlingResources to successfully run these tests + onView(withId(R.id.btnLogin)).check(matches(not(isDisplayed()))); +// openActionBarOverflowOrOptionsMenu(ApplicationProvider.getApplicationContext()); +// onView(withId(R.id.action_logout)).check(matches(withText("Logout"))); + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/registration/RegistrationActivityTest.java b/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/registration/RegistrationActivityTest.java new file mode 100644 index 0000000..1a9b4b6 --- /dev/null +++ b/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/registration/RegistrationActivityTest.java @@ -0,0 +1,54 @@ +package com.fazemeright.chatbotmetcs622.ui.registration; + +import android.widget.EditText; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; + +import com.fazemeright.chatbotmetcs622.R; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; +import static androidx.test.espresso.action.ViewActions.typeText; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.assertEquals; + + +@RunWith(AndroidJUnit4.class) +public class RegistrationActivityTest { + + public static final String INCORRECT_EMAIL_ADDRESS = "abcd@gmail"; + public static final String CORRECT_EMAIL_ADDRESS = "unittesting@gmail.com"; + public static final String CORRECT_PASSWORD = "12345678"; + + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>(RegistrationActivity.class); + + @Test + public void incorrect_email_address_error() throws Exception { + String expected = mActivityRule.getActivity().getString(R.string.incorrect_email_err_msg); + onView(withId(R.id.userLoginEmailEditText)).perform(typeText(INCORRECT_EMAIL_ADDRESS)); + onView(withId(R.id.btnRegister)).perform(click()); + EditText etEmail = mActivityRule.getActivity().findViewById(R.id.userLoginEmailEditText); + assertEquals(etEmail.getError().toString(), expected); + } + + @Test + public void registration_flow_isCorrect() throws Exception { +// onView(withId(R.id.btnRegister)).check(matches(isDisplayed())); + onView(withId(R.id.userLoginEmailEditText)).perform(typeText(CORRECT_EMAIL_ADDRESS)); + onView(withId(R.id.userPasswordEditText)).perform(typeText(CORRECT_PASSWORD)); + onView(withId(R.id.userConPasswordEditText)).perform(typeText(CORRECT_PASSWORD), closeSoftKeyboard()); + onView(withId(R.id.btnRegister)).perform(click()); + onView(withId(R.id.btnRegister)).check(matches(not(isDisplayed()))); + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/splash/SplashActivityTest.java b/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/splash/SplashActivityTest.java new file mode 100644 index 0000000..f788332 --- /dev/null +++ b/app/src/androidTest/java/com/fazemeright/chatbotmetcs622/ui/splash/SplashActivityTest.java @@ -0,0 +1,31 @@ +package com.fazemeright.chatbotmetcs622.ui.splash; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; + +import com.fazemeright.chatbotmetcs622.BuildConfig; +import com.fazemeright.chatbotmetcs622.R; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +@RunWith(AndroidJUnit4.class) +public class SplashActivityTest { + + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>(SplashActivity.class); + + @Test + public void display_app_version() throws Exception { + String expected = BuildConfig.VERSION_NAME; + onView(withId(R.id.tvAppVersion)).check(matches(isDisplayed())); + onView(withId(R.id.tvAppVersion)).check(matches(withText(expected))); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d40a3c1 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/ChatBotApp.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/ChatBotApp.java new file mode 100644 index 0000000..236c612 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/ChatBotApp.java @@ -0,0 +1,46 @@ +package com.fazemeright.chatbotmetcs622; + +import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.os.Build; + +import com.fazemeright.chatbotmetcs622.network.ApiManager; +import com.fazemeright.chatbotmetcs622.network.NetworkManager; + +import timber.log.Timber; + +public class ChatBotApp extends Application { + + public static final String CHANNEL_ID = "FireBaseSyncChannel"; + + @Override + public void onCreate() { + super.onCreate(); + if (BuildConfig.DEBUG) { + Timber.plant(new Timber.DebugTree()); + } + NetworkManager.getInstance().init(getApplicationContext(), 300); + + ApiManager.BaseUrl.setLocalIP("http://192.168.43.28:8080"); + + createNotificationChannel(); + } + + /** + * Call to create a notification channel for OS greater than OREO + */ + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel serviceChannel = new NotificationChannel( + CHANNEL_ID, + "FireBase Sync channel", + NotificationManager.IMPORTANCE_DEFAULT + ); + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(serviceChannel); + } + } + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/database/BaseDao.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/database/BaseDao.java new file mode 100644 index 0000000..79b8ba2 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/database/BaseDao.java @@ -0,0 +1,32 @@ +package com.fazemeright.chatbotmetcs622.database; + +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.Update; + +public interface BaseDao { + + /** + * Insert an object in the database. + * + * @param element the object to be inserted. + */ + @Insert + void insert(T element); + + /** + * Update an object from the database. + * + * @param element the object to be updated + */ + @Update + void update(T element); + + /** + * Delete an object from the database + * + * @param element the object to be deleted + */ + @Delete + void deleteItem(T element); +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/database/ChatBotDatabase.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/database/ChatBotDatabase.java new file mode 100644 index 0000000..ce199ad --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/database/ChatBotDatabase.java @@ -0,0 +1,32 @@ +package com.fazemeright.chatbotmetcs622.database; + +import android.content.Context; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import com.fazemeright.chatbotmetcs622.database.messages.Message; +import com.fazemeright.chatbotmetcs622.database.messages.MessageDao; + + +@Database(entities = {Message.class}, version = 1, exportSchema = false) +public abstract class ChatBotDatabase extends RoomDatabase { + + // singleton instance of Database + private static ChatBotDatabase INSTANCE; + + public static synchronized ChatBotDatabase getInstance(Context context) { + if (INSTANCE == null) { + INSTANCE = Room.databaseBuilder(context, ChatBotDatabase.class, "chat_bot_database") + // Wipes and rebuilds instead of migrating if no Migration object. + .fallbackToDestructiveMigration() + .build(); + } + + return INSTANCE; + } + + // List all daos here + public abstract MessageDao messageDao(); +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/database/messages/Message.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/database/messages/Message.java new file mode 100644 index 0000000..7f6ac31 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/database/messages/Message.java @@ -0,0 +1,100 @@ +package com.fazemeright.chatbotmetcs622.database.messages; + +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * POJO for a message + */ +@Entity(tableName = "my_messages_table") +public class Message implements Serializable { + public static final String SENDER_USER = "User"; + /** + * mid of message + */ + @PrimaryKey(autoGenerate = true) + private long mid; + /** + * text of message + */ + private String msg; + /** + * sender of the message + */ + private String sender; + /** + * receiver of the message + */ + private String receiver; + /** + * mid of the chat room where message was sent + */ + private long chatRoomId; + /** + * timestamp of the message + */ + private long timestamp; + + public Message(long mid, String msg, String sender, String receiver, long chatRoomId, long timestamp) { + this.mid = mid; + this.msg = msg; + this.sender = sender; + this.receiver = receiver; + this.chatRoomId = chatRoomId; + this.timestamp = timestamp; + } + + public static Message newMessage(String msg, String sender, String receiver, long chatRoomId) { + return new Message(0, msg, sender, receiver, chatRoomId, System.currentTimeMillis()); + } + + public static Map getHashMap(Message message) { + Map messageHashMap = new HashMap<>(); + messageHashMap.put("mid", message.getMid()); + messageHashMap.put("msg", message.getMsg()); + messageHashMap.put("sender", message.getSender()); + messageHashMap.put("receiver", message.getReceiver()); + messageHashMap.put("chatRoomId", message.getChatRoomId()); + messageHashMap.put("timestamp", message.getTimestamp()); + return messageHashMap; + } + + public long getMid() { + return mid; + } + + public String getMsg() { + return msg; + } + + public String getSender() { + return sender; + } + + public String getReceiver() { + return receiver; + } + + public long getChatRoomId() { + return chatRoomId; + } + + public long getTimestamp() { + return timestamp; + } + + public String getFormattedTime() { + String pattern = "MM/dd HH:mm a"; + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern, Locale.US); + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(timestamp); + return simpleDateFormat.format(cal.getTime()); + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/database/messages/MessageDao.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/database/messages/MessageDao.java new file mode 100644 index 0000000..6d7e319 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/database/messages/MessageDao.java @@ -0,0 +1,36 @@ +package com.fazemeright.chatbotmetcs622.database.messages; + +import androidx.room.Dao; +import androidx.room.FtsOptions; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.fazemeright.chatbotmetcs622.database.BaseDao; + +import java.util.List; + +@Dao +public interface MessageDao extends BaseDao { + + @Query("SELECT * from my_messages_table WHERE mid = :key") + Message get(long key); + + @Query("DELETE FROM my_messages_table") + void clear(); + + @Query("SELECT * FROM my_messages_table ORDER BY timestamp DESC") + List getAllMessages(); + + @Query("SELECT * FROM my_messages_table WHERE chatRoomId = :chatRoomId ORDER BY timestamp DESC") + List getAllMessagesFromChatRoom(long chatRoomId); + + @Query("DELETE from my_messages_table WHERE chatRoomId = :chatRoomId") + void clearChatRoomMessages(long chatRoomId); + + @Query("SELECT * FROM my_messages_table WHERE chatRoomId = :chatRoomId ORDER BY timestamp DESC LIMIT 1") + Message getLatestMessage(long chatRoomId); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertAllMessages(List order); +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/intentservice/FireBaseIntentService.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/intentservice/FireBaseIntentService.java new file mode 100644 index 0000000..269e23d --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/intentservice/FireBaseIntentService.java @@ -0,0 +1,144 @@ +package com.fazemeright.chatbotmetcs622.intentservice; + +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Intent; +import android.os.Build; +import android.os.PowerManager; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import com.fazemeright.chatbotmetcs622.ChatBotApp; +import com.fazemeright.chatbotmetcs622.R; +import com.fazemeright.chatbotmetcs622.database.ChatBotDatabase; +import com.fazemeright.chatbotmetcs622.database.messages.Message; +import com.fazemeright.chatbotmetcs622.repositories.MessageRepository; +import com.fazemeright.firebase_api_library.api.FireBaseApiManager; +import com.fazemeright.firebase_api_library.listeners.DBValueListener; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import timber.log.Timber; + +public class FireBaseIntentService extends IntentService { + + public static final String ACTION_ADD_MESSAGE = "AddMessage"; + public static final String ACTION_SYNC_MESSAGES = "SyncMessages"; + /** + * TAG for logs + */ + private static final String TAG = "FireBaseIntentService"; + /** + * Use to send data with intent + */ + public static final String ACTION = "IntentAction"; + public static final String MESSAGE = "Message"; + public static final String RESULT_RECEIVER = "ResultReceiver"; + + protected ChatBotDatabase database; + private PowerManager.WakeLock wakeLock; + private FireBaseApiManager fireBaseApiManager; + private MessageRepository messageRepository; + + /** + * Creates an IntentService. Invoked by your subclass's constructor. + *

+ * TAG Used to name the worker thread, important only for debugging. + */ + public FireBaseIntentService() { + super(TAG); + } + + @Override + public void onCreate() { + super.onCreate(); + Timber.i("onCreate"); + database = ChatBotDatabase.getInstance(getApplicationContext()); + PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); + if (powerManager != null) { + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ChatBot:WakeLockTag"); + wakeLock.acquire(60 * 1000); // acquire CPU + Timber.i("onCreate: Wake Lock acquired"); + } + showForegroundServiceNotification("Chat Bot", "Syncing Messages..."); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Timber.i("onDestroy"); + wakeLock.release(); + Timber.i("onDestroy: Wake Lock released"); + + } + + @Override + protected void onHandleIntent(@Nullable Intent intent) { + Timber.d("onHandleIntent"); + if (intent != null) { + switch (intent.getStringExtra(ACTION)) { + case ACTION_ADD_MESSAGE: + addMessageToFireStore((Message) intent.getSerializableExtra(MESSAGE)); + break; + case ACTION_SYNC_MESSAGES: + syncMessages(); + break; + } + } + + } + + /** + * Call to sync messages from FireStore to Room for the logged in user + */ + private void syncMessages() { + /*try { + Thread.sleep(5000); // intentionally kept delay to show in presentation TODO: Remove afterwards + } catch (InterruptedException e) { + e.printStackTrace(); + }*/ + messageRepository.syncMessagesFromFireStoreToRoom(); + } + + /** + * Call to add given message to FireStore + * + * @param message given message + */ + private void addMessageToFireStore(Message message) { + messageRepository.addMessageToFireBase(Message.getHashMap(message)); + } + + /** + * Call to display foreground running notification to notify user of a background operation running + * + * @param title title to display in notification + * @param text text to display in notification + */ + private void showForegroundServiceNotification(String title, String text) { + Timber.i("Show notification called"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + + Notification notification = new NotificationCompat.Builder(this, ChatBotApp.CHANNEL_ID) + .setContentTitle(title) + .setContentText(text) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setPriority(NotificationManager.IMPORTANCE_DEFAULT) + .build(); + + startForeground(1, notification); + } + } + + @Override + public void onStart(@Nullable Intent intent, int startId) { + super.onStart(intent, startId); + fireBaseApiManager = FireBaseApiManager.getInstance(); + messageRepository = MessageRepository.getInstance(getApplicationContext()); + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/models/ChatRoom.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/models/ChatRoom.java new file mode 100644 index 0000000..45e002d --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/models/ChatRoom.java @@ -0,0 +1,48 @@ +package com.fazemeright.chatbotmetcs622.models; + +import java.io.Serializable; + +/** + * POJO to hold Chat room + */ +public class ChatRoom implements Serializable { + public static final int BRUTE_FORCE_ID = 0; + public static final int LUCENE_ID = 1; + public static final int MONGO_DB_ID = 2; + public static final int MY_SQL_ID = 3; + + public static final String MONGO_DB = "MongoDB"; + public static final String MY_SQL = "MySQL"; + public static final String LUCENE = "Lucene"; + public static final String BRUTE_FORCE = "Brute Force"; + /** + * is of the chat room + */ + private long id; + /** + * Name of the chat room + */ + private String name; + /** + * Id of resource file associated with the ChatRoom + */ + private int logoId; + + public ChatRoom(int id, String name, int logoId) { + this.id = id; + this.name = name; + this.logoId = logoId; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public int getLogoId() { + return logoId; + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/ApiManager.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/ApiManager.java new file mode 100644 index 0000000..e18117e --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/ApiManager.java @@ -0,0 +1,112 @@ +package com.fazemeright.chatbotmetcs622.network; + +import android.content.Context; + +import com.fazemeright.chatbotmetcs622.database.messages.Message; +import com.fazemeright.chatbotmetcs622.models.ChatRoom; +import com.fazemeright.chatbotmetcs622.network.handlers.NetworkCallback; +import com.fazemeright.chatbotmetcs622.network.handlers.NetworkWrapper; +import com.fazemeright.chatbotmetcs622.network.models.NetError; +import com.fazemeright.chatbotmetcs622.network.models.NetResponse; +import com.fazemeright.chatbotmetcs622.network.models.request.MessageQueryRequestModel; +import com.fazemeright.chatbotmetcs622.network.models.response.QueryResponseMessage; +import com.google.gson.reflect.TypeToken; + +public class ApiManager { + + private static ApiManager apiManager = null; + private NetworkManager networkManager; + + public static ApiManager getInstance() { + if (apiManager == null) { + apiManager = new ApiManager(); + } + return apiManager; + } + + /** + * Initialize base url,alias key and {@link NetworkWrapper} from application side, + * so that no need to pass base url in every user related network call. + * + * @param networkManager + */ + public void init(NetworkManager networkManager) { + this.networkManager = networkManager; + + } + + + /** + * Call Query API to backend to fetch results for the given Message query + * + * @param context context + * @param newMessage given message query + * @param networkCallback callback to listen to response + */ + public void queryDatabase(Context context, Message newMessage, + final NetworkCallback networkCallback) { + String serverEndPoint; + switch ((int) newMessage.getChatRoomId()) { + case ChatRoom.BRUTE_FORCE_ID: + serverEndPoint = DatabaseUrl.BRUTE_FORCE; + break; + case ChatRoom.LUCENE_ID: + serverEndPoint = DatabaseUrl.LUCENE; + break; + case ChatRoom.MONGO_DB_ID: + serverEndPoint = DatabaseUrl.MONGO_DB; + break; + default: + serverEndPoint = DatabaseUrl.MY_SQL; + break; + } + String url = BaseUrl.BASE_URL.concat(BaseUrl.BASE_APP_NAME).concat(serverEndPoint); + + MessageQueryRequestModel messageQuery = new MessageQueryRequestModel(newMessage.getMsg()); + + TypeToken typeToken = new TypeToken() { + }; + networkManager.makePostRequest(context, url, messageQuery, typeToken, "", new NetworkCallback() { + @Override + public void onSuccess(NetResponse response) { + networkCallback.onSuccess(response); + } + + @Override + public void onError(NetError error) { + networkCallback.onError(error); + } + }); + + } + + /** + * DatabaseUrl module Api sub url + */ + static class DatabaseUrl { + final static String MONGO_DB = "/mongodb"; + final static String LUCENE = "/lucene"; + final static String MY_SQL = "/mysql"; + final static String BRUTE_FORCE = "/bruteforce"; + + } + + /** + * baseURL model for all the BASE URL addresses + */ + public static class BaseUrl { + static String BASE_URL = "http://192.168.43.28:8080"; // Update the url as per your local ip address + final static String BASE_APP_NAME = "/MET_CS622_ChatBot_Backend_war_exploded"; + + /** + * Set the local IP address or the base url address for the server where the database back end is hosted. + * Also include the port. + * + * @param hostAddress ip address + */ + public static void setLocalIP(String hostAddress) { + BASE_URL = hostAddress; + } + + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/NetworkManager.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/NetworkManager.java new file mode 100644 index 0000000..6921e00 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/NetworkManager.java @@ -0,0 +1,498 @@ +package com.fazemeright.chatbotmetcs622.network; + + +import android.content.Context; + +import androidx.annotation.Nullable; + +import com.androidnetworking.AndroidNetworking; +import com.androidnetworking.common.Priority; +import com.androidnetworking.error.ANError; +import com.androidnetworking.interceptors.HttpLoggingInterceptor; +import com.androidnetworking.interfaces.ParsedRequestListener; +import com.fazemeright.chatbotmetcs622.R; +import com.fazemeright.chatbotmetcs622.network.handlers.NetworkCallback; +import com.fazemeright.chatbotmetcs622.network.handlers.NetworkWrapper; +import com.fazemeright.chatbotmetcs622.network.models.ChatBotError; +import com.fazemeright.chatbotmetcs622.network.models.NetCompoundRes; +import com.fazemeright.chatbotmetcs622.network.models.NetError; +import com.fazemeright.chatbotmetcs622.network.models.NetResponse; +import com.fazemeright.chatbotmetcs622.network.utils.CoreUtils; +import com.google.gson.reflect.TypeToken; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; +import timber.log.Timber; + +public class NetworkManager implements NetworkWrapper { + + private final static String TAG = NetworkManager.class.getSimpleName(); + private static final String CONTENT_TYPE = "application/json; charset=utf-8"; + private static NetworkManager networkManager = null; + + public static NetworkManager getInstance() { + if (networkManager == null) { + networkManager = new NetworkManager(); + } + return networkManager; + } + + /** + * To get Http client + * + * @param requestTimeOut Network Request timeout in millisecond it's configurable from backend + * @return OkHttpClient + */ + public static OkHttpClient getHttpClient(int requestTimeOut) { + //if set < 2 second then we put our default timeout + + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + builder.connectTimeout(requestTimeOut, TimeUnit.SECONDS); + builder.readTimeout(requestTimeOut, TimeUnit.SECONDS); + builder.writeTimeout(requestTimeOut, TimeUnit.SECONDS); + + return builder.build(); + } + + /** + * Initializing at the very first time + * Set Request Timeout + * Enabling network logging + * + * @param requestTimeOut Network Request timeout in millisecond it's configurable from backend + * @param context + */ + public void init(Context context, int requestTimeOut) { + initSecureClient(context, requestTimeOut); + } + + /** + * To initialize network manager + * + * @param context App context + * @param requestTimeOut Network Request timeout in millisecond it's configurable from backend + */ + private void initSecureClient(Context context, int requestTimeOut) { + AndroidNetworking.initialize(context, getHttpClient(requestTimeOut)); + } + + @Override + public void makeGetRequest(Context context, String url, TypeToken typeToken, String tag, + final NetworkCallback networkCallback) { + if (CoreUtils.isValidUrl(url)) { + if (CoreUtils.isNetworkAvailable(context)) { + printURLAndRequestParameters(url, null); + AndroidNetworking.get(url) + .setTag(tag) + .setPriority(Priority.MEDIUM) + .build() + .getAsParsed(typeToken, new ParsedRequestListener() { + @Override + public void onResponse(T response) { + Timber.i("onResponse :: %s", CoreUtils.getStringFromObject(response)); + NetResponse netResponse = new NetResponse(); + netResponse.setResponse(response); + networkCallback.onSuccess(netResponse); + } + + @Override + public void onError(ANError anError) { + Timber.e("onError :: %s", CoreUtils.getStringFromObject(anError)); + networkCallback.onError(getNetError(anError, null)); + } + }); + } else { + networkCallback.onError(getNetErrorConnectivityError(getConnectivityError(context), null)); + } + } else { + networkCallback.onError(getInvalidUrlError(url)); + } + } + + @Override + public void makePostRequest(Context context, String url, TypeToken typeToken, + HashMap hashMapHeader, String tag, + final NetworkCallback networkCallback) { + if (CoreUtils.isValidUrl(url)) { + if (CoreUtils.isNetworkAvailable(context)) { + AndroidNetworking.post(url) + .setContentType("application/x-www-form-urlencoded") // custom ContentType + .setTag(tag) + .addHeaders(hashMapHeader) + .setPriority(Priority.MEDIUM) + .build() + .getAsParsed(typeToken, new ParsedRequestListener() { + @Override + public void onResponse(T response) { + NetResponse netResponse = new NetResponse(); + netResponse.setResponse(response); + networkCallback.onSuccess(netResponse); + } + + @Override + public void onError(ANError anError) { + networkCallback.onError(getNetError(anError, null)); + } + }); + } else { + networkCallback.onError(getNetErrorConnectivityError(getConnectivityError(context), + null)); + } + } else { + networkCallback.onError(getInvalidUrlError(url)); + } + } + + + @Override + public void makeGetRequestHeader(Context context, String url, TypeToken typeToken, + HashMap hashMapHeader, String tag, + final NetworkCallback networkCallback) { + if (CoreUtils.isValidUrl(url)) { + if (CoreUtils.isNetworkAvailable(context)) { + printURLAndRequestParameters(url, null); + AndroidNetworking.get(url) + .setTag(tag) + .addHeaders(hashMapHeader) + .setPriority(Priority.MEDIUM) + .build() + .getAsParsed(typeToken, new ParsedRequestListener() { + @Override + public void onResponse(T response) { + Timber.i("onResponse :: %s", CoreUtils.getStringFromObject(response)); + NetResponse netResponse = new NetResponse(); + netResponse.setResponse(response); + networkCallback.onSuccess(netResponse); + } + + @Override + public void onError(ANError anError) { + Timber.e("onError :: %s", CoreUtils.getStringFromObject(anError)); + networkCallback.onError(getNetError(anError, null)); + } + }); + } else { + networkCallback.onError(getNetErrorConnectivityError(getConnectivityError(context), null)); + } + } else { + networkCallback.onError(getInvalidUrlError(url)); + } + } + + @Override + public void makePutRequestHeader(Context context, String url, final Object dataObject, + TypeToken typeToken, HashMap hashMapHeader, + String tag, final NetworkCallback networkCallback) { + if (CoreUtils.isValidUrl(url)) { + if (CoreUtils.isNetworkAvailable(context)) { + printURLAndRequestParameters(url, dataObject); + //.addBodyParameter(dataObject) + //.addStringBody(NetworkUtility.getStringFromObject(dataObject)) + AndroidNetworking.put(url) + .addApplicationJsonBody(dataObject) + .addHeaders(hashMapHeader) + .setContentType(CONTENT_TYPE) // custom ContentType + .setTag(tag) + .setPriority(Priority.MEDIUM) + .build() + .getAsParsed(typeToken, new ParsedRequestListener() { + @Override + public void onResponse(T response) { + // LogUtils.getInstance().printLog(TAG, "onResponse :: " + // + CoreUtils.getStringFromObject(response)); + NetResponse netResponse = new NetResponse(); + netResponse.setResponse(response); + networkCallback.onSuccess(netResponse); + } + + @Override + public void onError(ANError anError) { + // LogUtils.getInstance().printLog(TAG, "onError :: " + // + CoreUtils.getStringFromObject(anError)); + networkCallback.onError(getNetError(anError, dataObject)); + } + }); + } else { + networkCallback.onError(getNetErrorConnectivityError(getConnectivityError(context), dataObject)); + } + } else { + networkCallback.onError(getInvalidUrlError(url)); + } + } + + + @Override + public void makePostRequest(Context context, String url, final Object dataObject, TypeToken typeToken, + String tag, final NetworkCallback networkCallback) { + if (CoreUtils.isValidUrl(url)) { + if (CoreUtils.isNetworkAvailable(context)) { + printURLAndRequestParameters(url, dataObject); +//.addBodyParameter(dataObject) +//.addStringBody(NetworkUtility.getStringFromObject(dataObject)) + AndroidNetworking.post(url) + .addApplicationJsonBody(dataObject) +// .addHeaders(hashMapHeader) + .setContentType(CONTENT_TYPE) // custom ContentType + .setTag(tag) + .setPriority(Priority.MEDIUM) + .build() + .getAsParsed(typeToken, new ParsedRequestListener() { + @Override + public void onResponse(T response) { + // LogUtils.getInstance().printLog(TAG, "onResponse :: " + // + CoreUtils.getStringFromObject(response)); + NetResponse netResponse = new NetResponse(); + netResponse.setResponse(response); + networkCallback.onSuccess(netResponse); + } + + @Override + public void onError(ANError anError) { + // LogUtils.getInstance().printLog(TAG, "onError :: " + // + CoreUtils.getStringFromObject(anError)); + networkCallback.onError(getNetError(anError, dataObject)); + } + }); + } else { + networkCallback.onError(getNetErrorConnectivityError(getConnectivityError(context), dataObject)); + } + } else { + networkCallback.onError(getInvalidUrlError(url)); + } + } + + @Override + public void makePostStringRequest(Context context, String url, String data, TypeToken typeToken, + String tag, NetworkCallback networkCallback) { + + } + + @Override + public NetCompoundRes makePostRequestSync(Context context, String url, Object dataObject, + TypeToken typeToken, String tag) { + return null; + } + + @Override + public void makePostRequestHeader(Context context, String url, final Object dataObject, + TypeToken typeToken, HashMap hashMapHeader, + String tag, final NetworkCallback networkCallback) { + + if (CoreUtils.isValidUrl(url)) { + if (CoreUtils.isNetworkAvailable(context)) { + printURLAndRequestParameters(url, dataObject); +//.addBodyParameter(dataObject) +//.addStringBody(NetworkUtility.getStringFromObject(dataObject)) + AndroidNetworking.post(url) + .addApplicationJsonBody(dataObject) + .addHeaders(hashMapHeader) + .setContentType(CONTENT_TYPE) // custom ContentType + .setTag(tag) + .setPriority(Priority.MEDIUM) + .build() + .getAsParsed(typeToken, new ParsedRequestListener() { + @Override + public void onResponse(T response) { + // LogUtils.getInstance().printLog(TAG, "onResponse :: " + // + CoreUtils.getStringFromObject(response)); + NetResponse netResponse = new NetResponse(); + netResponse.setResponse(response); + networkCallback.onSuccess(netResponse); + } + + @Override + public void onError(ANError anError) { + // LogUtils.getInstance().printLog(TAG, "onError :: " + // + CoreUtils.getStringFromObject(anError)); + networkCallback.onError(getNetError(anError, dataObject)); + } + }); + } else { + networkCallback.onError(getNetErrorConnectivityError(getConnectivityError(context), dataObject)); + } + } else { + networkCallback.onError(getInvalidUrlError(url)); + } + } + + @Override + public void makeDeleteRequestHeader(Context context, String url, /*final Object dataObject,*/ + TypeToken typeToken, HashMap hashMapHeader, + String tag, final NetworkCallback networkCallback) { + + if (CoreUtils.isValidUrl(url)) { + if (CoreUtils.isNetworkAvailable(context)) { +// printURLAndRequestParameters(url, dataObject); +//.addBodyParameter(dataObject) +//.addStringBody(NetworkUtility.getStringFromObject(dataObject)) + AndroidNetworking.delete(url) +// .addApplicationJsonBody(dataObject) + .addHeaders(hashMapHeader) + .setContentType(CONTENT_TYPE) // custom ContentType + .setTag(tag) + .setPriority(Priority.MEDIUM) + .build() + .getAsParsed(typeToken, new ParsedRequestListener() { + @Override + public void onResponse(T response) { + // LogUtils.getInstance().printLog(TAG, "onResponse :: " + // + CoreUtils.getStringFromObject(response)); + NetResponse netResponse = new NetResponse(); + netResponse.setResponse(response); + networkCallback.onSuccess(netResponse); + } + + @Override + public void onError(ANError anError) { + // LogUtils.getInstance().printLog(TAG, "onError :: " + // + CoreUtils.getStringFromObject(anError)); + networkCallback.onError(getNetError(anError, null)); + } + }); + } else { + networkCallback.onError(getNetErrorConnectivityError(getConnectivityError(context), null)); + } + } else { + networkCallback.onError(getInvalidUrlError(url)); + } + } + + @Override + public void cancelRequest(String tag) { + + } + + /** + * To use only for get id_token,access_token and refresh_token when login with Social + * + * @param context + * @param url + * @param dataObject + * @param typeToken + * @param hashMapHeader + * @param tag + * @param networkCallback + * @param + */ + @Override + public void makeCustomPostForSocialRequest(Context context, String url, final String dataObject, + TypeToken typeToken, + HashMap hashMapHeader, String tag, + final NetworkCallback networkCallback) { + + String stringURL = url.concat("?grant_type=authorization_code&redirect_uri=rosedaleapp://login&client_id=2idjeho7u7717nur0uhb6kmuhj&") + .concat("code=").concat(dataObject).concat("&scope=email openid profile"); + + if (CoreUtils.isValidUrl(url)) { + if (CoreUtils.isNetworkAvailable(context)) { + printURLAndRequestParameters(stringURL, dataObject); + AndroidNetworking.post(stringURL) + .setContentType("application/x-www-form-urlencoded") // custom ContentType + .setTag(tag) + .setPriority(Priority.MEDIUM) + .build() + .getAsParsed(typeToken, new ParsedRequestListener() { + @Override + public void onResponse(T response) { + NetResponse netResponse = new NetResponse(); + netResponse.setResponse(response); + networkCallback.onSuccess(netResponse); + } + + @Override + public void onError(ANError anError) { + networkCallback.onError(getNetError(anError, dataObject)); + } + }); + } else { + networkCallback.onError(getNetErrorConnectivityError(getConnectivityError(context), dataObject)); + } + } else { + networkCallback.onError(getInvalidUrlError(url)); + } + } + + /** + * To create class for error from network/api + * + * @param anError + * @param requestObject + */ + private NetError getNetError(ANError anError, @Nullable Object requestObject) { + NetError netError = new NetError(anError.getMessage()); + netError.setErrorBody(anError.getErrorBody()); + netError.setErrorCode(anError.getErrorCode()); + netError.setErrorDetail(anError.getErrorDetail()); + netError.setErrorLocalizeMessage(anError.getLocalizedMessage()); + netError.setApiRequest(requestObject); + netError.setResponseErrorMessage(anError.getErrorBody()); + return netError; + } + + /** + * To create class for error from network/api + * + * @param anError + * @param requestObject + */ + private NetError getNetErrorConnectivityError(ANError anError, @Nullable Object requestObject) { + NetError netError = new NetError(anError.getMessage()); + netError.setErrorBody(anError.getErrorBody()); + netError.setErrorCode(anError.getErrorCode()); + netError.setErrorDetail(anError.getErrorDetail()); + netError.setErrorLocalizeMessage(anError.getLocalizedMessage()); + netError.setApiRequest(requestObject); + netError.setResponseErrorMessage(anError.getErrorBody()); + return netError; + } + + /** + * To get Connectivity Error + * + * @param context + * @return + */ + private ANError getConnectivityError(Context context) { + ANError anError = new ANError(context.getString(R.string.no_internet_connection_available)); + anError.setErrorCode(ChatBotError.ChatBotErrorCodes.INTERNET_NOT_AVAILABLE); + anError.setErrorBody(context.getString(R.string.no_internet_connection_available)); + return anError; + } + + /** + * To Print Request + * + * @param url + * @param data + */ + private void printURLAndRequestParameters(String url, Object data) { + Timber.i("url :: %s", url); + Timber.i("requestParameters :: %s", + CoreUtils.getStringFromObject(data)); + } + + + /** + * To get Invalid Url Error + * + * @param url + * @return + */ + private NetError getInvalidUrlError(String url) { + NetError anError = new NetError("Invalid url: " + url); + anError.setErrorCode(ChatBotError.ChatBotErrorCodes.INVALID_URL); + return anError; + } + + + private void enableAndroidNetworkingLogging(boolean enable) { + if (enable) { + AndroidNetworking.enableLogging(HttpLoggingInterceptor.Level.BODY); + } else { + AndroidNetworking.enableLogging(HttpLoggingInterceptor.Level.NONE); + } + } + +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/handlers/NetworkCallback.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/handlers/NetworkCallback.java new file mode 100644 index 0000000..dac9c4e --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/handlers/NetworkCallback.java @@ -0,0 +1,25 @@ +package com.fazemeright.chatbotmetcs622.network.handlers; + + +import com.fazemeright.chatbotmetcs622.network.models.NetError; +import com.fazemeright.chatbotmetcs622.network.models.NetResponse; + +/* + * Interface for handling API response + * */ +public interface NetworkCallback { + + /** + * Interface method called on success of api call + * + * @param response {@link NetResponse} + */ + void onSuccess(NetResponse response); + + /** + * Interface method called on api/network failure + * + * @param error Error obj with error detail + */ + void onError(NetError error); +} \ No newline at end of file diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/handlers/NetworkWrapper.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/handlers/NetworkWrapper.java new file mode 100644 index 0000000..bb2ea71 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/handlers/NetworkWrapper.java @@ -0,0 +1,152 @@ +package com.fazemeright.chatbotmetcs622.network.handlers; + + +import android.content.Context; + +import com.fazemeright.chatbotmetcs622.network.models.NetCompoundRes; +import com.google.gson.reflect.TypeToken; + +import java.util.HashMap; + +/* + * Interface for calling API + * */ +public interface NetworkWrapper { + + /** + * API GET method Request + * + * @param context App context + * @param url Request URL + * @param typeToken {@link TypeToken} of the expected parsed object + * @param tag request tag + * @param networkCallback Network callback + */ + void makeGetRequest(Context context, String url, TypeToken typeToken, String tag, + NetworkCallback networkCallback); + + /** + * API POST method Request + * + * @param context App context + * @param url Request URL + * @param typeToken {@link TypeToken} of the expected parsed object + * @param tag request tag + * @param networkCallback Network callback + */ + void makePostRequest(Context context, String url, TypeToken typeToken, + HashMap hashMapHeader, String tag, + NetworkCallback networkCallback); + + /** + * API POST method Request + * + * @param context App context + * @param url Request URL + * @param dataObject Request body + * @param typeToken {@link TypeToken} of the expected parsed object + * @param tag request tag + * @param networkCallback Network callback + */ + void makePostRequest(Context context, String url, Object dataObject, TypeToken typeToken, + String tag, NetworkCallback networkCallback); + + /** + * API POST method Request + * + * @param context App context + * @param url Request URL + * @param data Request body + * @param typeToken {@link TypeToken} of the expected parsed object + * @param tag request tag + * @param networkCallback Network callback + */ + void makePostStringRequest(Context context, String url, String data, TypeToken typeToken, + String tag, NetworkCallback networkCallback); + + /** + * API sync POST method Request. Expect a response/error in {@link NetCompoundRes} + * + * @param context App context + * @param url Request URL + * @param dataObject Request body + * @param typeToken {@link TypeToken} of the expected parsed object + * @param tag request tag + * @return Compound response ( It contains success / error ). First check it using method + * isSuccess() to find whether response contains error or not + */ + NetCompoundRes makePostRequestSync(Context context, String url, Object dataObject, + TypeToken typeToken, String tag); + + /** + * API POST method Request with header + * + * @param context App context + * @param url Request URL + * @param dataObject Request body + * @param typeToken {@link TypeToken} of the expected parsed object + * @param hashMapHeader HashMap of request header + * @param tag request tag + * @param networkCallback Network callback + */ + void makePostRequestHeader(Context context, String url, Object dataObject, TypeToken typeToken, + HashMap hashMapHeader, String tag, + NetworkCallback networkCallback); + + /** + * API POST method Request with header + * + * @param context App context + * @param url Request URL + * @param typeToken {@link TypeToken} of the expected parsed object + * @param hashMapHeader HashMap of request header + * @param tag request tag + * @param networkCallback Network callback + */ + void makeDeleteRequestHeader(Context context, String url/*, Object dataObject*/, TypeToken typeToken, + HashMap hashMapHeader, String tag, + NetworkCallback networkCallback); + + + /** + * API Get method Request with header + * + * @param context App context + * @param url Request URL + * @param typeToken {@link TypeToken} of the expected parsed object + * @param hashMapHeader HashMap of request header + * @param tag request tag + * @param networkCallback Network callback + */ + void makeGetRequestHeader(Context context, String url, TypeToken typeToken, + HashMap hashMapHeader, String tag, + NetworkCallback networkCallback); + + /** + * API PUT Method Request with header + * + * @param context + * @param url + * @param dataObject + * @param typeToken + * @param hashMapHeader + * @param tag + * @param networkCallback + * @param + */ + void makePutRequestHeader(Context context, String url, Object dataObject, TypeToken typeToken, + HashMap hashMapHeader, String tag, + NetworkCallback networkCallback); + + /** + * Cancel api request by tag + * + * @param tag request tag + */ + void cancelRequest(String tag); + + void makeCustomPostForSocialRequest(Context context, String url, + String dataObject, TypeToken typeToken, + HashMap hashMapAuthenticate, String s, + NetworkCallback networkCallback); +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/ChatBotError.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/ChatBotError.java new file mode 100644 index 0000000..4e525c6 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/ChatBotError.java @@ -0,0 +1,60 @@ +package com.fazemeright.chatbotmetcs622.network.models; + + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + + + +public class ChatBotError { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ChatBotErrorCodes.INTERNET_NOT_AVAILABLE, /*SOMETHING_WENT_WRONG,*/ ChatBotErrorCodes.INVALID_URL, ChatBotErrorCodes.DATA_NOT_FOUND, ChatBotErrorCodes.BAD_REQUEST, ChatBotErrorCodes.UN_AUTHORIZED, ChatBotErrorCodes.UN_EXPECTED_SERVER_ERROR}) + public @interface ChatBotErrorCodes { + + /** + * Internet is not available + */ + public static final int INTERNET_NOT_AVAILABLE = 1001; + + + /** + * Invalid URL + */ + public static final int INVALID_URL = 1002; + + + /** + * Used for unknown error + */ + + public static final int SOMETHING_WENT_WRONG = -123456; + + /** + * Used for data not found error + */ + public static final int DATA_NOT_FOUND = 204; + + + /** + * Used for Bad request (Malformed Parameters or parameters missing) + */ + public static final int BAD_REQUEST = 400; + + + /** + * Used for Unauthorized (Authorization header is incorrec, log-in user again) + */ + public static final int UN_AUTHORIZED = 401; + + + /** + * Used for Unexpected server error (Error) + */ + public static final int UN_EXPECTED_SERVER_ERROR = 500; + + } + +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetCompoundRes.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetCompoundRes.java new file mode 100644 index 0000000..33867a1 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetCompoundRes.java @@ -0,0 +1,37 @@ +package com.fazemeright.chatbotmetcs622.network.models; + +/** + * To use when we want to have sync response from network manager + * + * @param + */ +public class NetCompoundRes { + + private boolean isSuccess; + private NetResponse netResponse; + private NetError netError; + + public void setSuccess(boolean success) { + isSuccess = success; + } + + public void setNetResponse(NetResponse netResponse) { + this.netResponse = netResponse; + } + + public void setNetError(NetError netError) { + this.netError = netError; + } + + public boolean isSuccess() { + return isSuccess; + } + + public NetResponse getNetResponse() { + return netResponse; + } + + public NetError getNetError() { + return netError; + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetError.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetError.java new file mode 100644 index 0000000..7e00820 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetError.java @@ -0,0 +1,114 @@ +package com.fazemeright.chatbotmetcs622.network.models; + + +import android.text.TextUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +/* + * Class for handling Network/API related errors + * */ +public class NetError extends Exception { + + private String errorBody = ""; + private int errorCode = ChatBotError.ChatBotErrorCodes.SOMETHING_WENT_WRONG; + private String errorDetail = ""; + private String errorLocalizeMessage = ""; + private Object apiRequest; + private String requestName = ""; + private String responseErrorMessage; + + public String getResponseErrorMessage() { + return responseErrorMessage; + } + + public void setResponseErrorMessage(String responseErrorMessage) { + this.responseErrorMessage = parseJson(responseErrorMessage); + } + + private String parseJson(String responseErrorMessage) { + String errorMessage = "Something went wrong!"; + if (!TextUtils.isEmpty(responseErrorMessage)) { + try { + JSONObject jsonObject = new JSONObject(responseErrorMessage); + if (jsonObject.has("responseMessage")) { + errorMessage = jsonObject.getString("responseMessage"); + } else { + return responseErrorMessage; + } + } catch (JSONException e) { + e.printStackTrace(); + return responseErrorMessage; + } + } + return errorMessage; + + } + + public String getRequestName() { + return requestName; + } + + public void setRequestName(String requestName) { + if (requestName != null) { + this.requestName = requestName; + } + } + + public Object getApiRequest() { + return apiRequest; + } + + public void setApiRequest(Object apiRequest) { + this.apiRequest = apiRequest; + } + + public String getErrorLocalizeMessage() { + return errorLocalizeMessage; + } + + public void setErrorLocalizeMessage(String errorLocalizeMessage) { + if (errorLocalizeMessage != null) { + this.errorLocalizeMessage = errorLocalizeMessage; + } + } + + public NetError(String message) { + super(message); + if (message == null) { + message = "Getting null error message."; + } + setErrorLocalizeMessage(message); + setErrorBody(message); + setErrorDetail(message); + } + + public void setErrorDetail(String errorDetail) { + if (errorDetail != null) { + this.errorDetail = errorDetail; + } + } + + public String getErrorDetail() { + return this.errorDetail; + } + + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } + + public int getErrorCode() { + return this.errorCode; + } + + public String getErrorBody() { + return errorBody; + } + + public void setErrorBody(String errorBody) { + if (errorBody != null) { + this.errorBody = errorBody; + } + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetResponse.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetResponse.java new file mode 100644 index 0000000..3ad6b07 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/NetResponse.java @@ -0,0 +1,20 @@ +package com.fazemeright.chatbotmetcs622.network.models; + +import com.google.gson.annotations.SerializedName; + +/** + * Class for handling generic response + */ +public class NetResponse { + + @SerializedName("response") + private T response; + + public T getResponse() { + return response; + } + + public void setResponse(T response) { + this.response = response; + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/request/MessageQueryRequestModel.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/request/MessageQueryRequestModel.java new file mode 100644 index 0000000..85d8bb9 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/request/MessageQueryRequestModel.java @@ -0,0 +1,22 @@ +package com.fazemeright.chatbotmetcs622.network.models.request; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class MessageQueryRequestModel { + @SerializedName("query") + @Expose + private String query; + + public MessageQueryRequestModel(String query) { + this.query = query; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/response/QueryResponseMessage.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/response/QueryResponseMessage.java new file mode 100644 index 0000000..93077ad --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/models/response/QueryResponseMessage.java @@ -0,0 +1,36 @@ +package com.fazemeright.chatbotmetcs622.network.models.response; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class QueryResponseMessage { + + @SerializedName("data") + @Expose + private Data data; + + public Data getData() { + return data; + } + + public void setData(Data data) { + this.data = data; + } + + public class Data { + + + @SerializedName("responseMsg") + @Expose + private String responseMsg; + + public String getResponseMsg() { + return responseMsg; + } + + public void setResponseMsg(String responseMsg) { + this.responseMsg = responseMsg; + } + } +} + diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/network/utils/CoreUtils.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/utils/CoreUtils.java new file mode 100644 index 0000000..c34cf18 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/network/utils/CoreUtils.java @@ -0,0 +1,43 @@ +package com.fazemeright.chatbotmetcs622.network.utils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.webkit.URLUtil; + +import com.google.gson.Gson; + + +public class CoreUtils { + + /** + * To check internet connection + * + * @param mContext App context + * @return true if available else false + */ + public static boolean isNetworkAvailable(Context mContext) { + ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = null; + if (cm != null) { + networkInfo = cm.getActiveNetworkInfo(); + } + return networkInfo != null && networkInfo.isConnected(); + } + + public static String getStringFromObject(Object data) { + Gson gson = new Gson(); + return gson.toJson(data); + } + + /** + * Check whether URL is valid or not + * + * @param url url + * @return true if valid else false + */ + public static boolean isValidUrl(String url) { + return URLUtil.isValidUrl(url); + } + +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/repositories/MessageRepository.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/repositories/MessageRepository.java new file mode 100644 index 0000000..4961091 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/repositories/MessageRepository.java @@ -0,0 +1,427 @@ +package com.fazemeright.chatbotmetcs622.repositories; + +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Build; + +import androidx.core.content.ContextCompat; + +import com.fazemeright.chatbotmetcs622.database.ChatBotDatabase; +import com.fazemeright.chatbotmetcs622.database.messages.Message; +import com.fazemeright.chatbotmetcs622.database.messages.MessageDao; +import com.fazemeright.chatbotmetcs622.intentservice.FireBaseIntentService; +import com.fazemeright.chatbotmetcs622.models.ChatRoom; +import com.fazemeright.chatbotmetcs622.network.ApiManager; +import com.fazemeright.chatbotmetcs622.network.NetworkManager; +import com.fazemeright.chatbotmetcs622.network.handlers.NetworkCallback; +import com.fazemeright.chatbotmetcs622.network.models.NetError; +import com.fazemeright.chatbotmetcs622.network.models.NetResponse; +import com.fazemeright.chatbotmetcs622.network.models.response.QueryResponseMessage; +import com.fazemeright.firebase_api_library.api.FireBaseApiManager; +import com.fazemeright.firebase_api_library.listeners.DBValueListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import timber.log.Timber; + + +public class MessageRepository { + + private static MessageRepository repository; + private ChatBotDatabase database; + private ApiManager apiManager; + private FireBaseApiManager fireBaseApiManager; + + private MessageRepository(ChatBotDatabase database, ApiManager apiManager, FireBaseApiManager fireBaseApiManager) { + this.database = database; + this.apiManager = apiManager; + this.fireBaseApiManager = fireBaseApiManager; +// messageList = this.database.messageDao().getAllMessages(); + } + + /** + * Call to get instance of MessageRepository with the given context + * + * @param context given context + * @return synchronized call to get Instance of MessageRepository class + */ + public static MessageRepository getInstance(Context context) { + if (repository == null) { + synchronized (MessageRepository.class) { +// get instance of database + ChatBotDatabase database = ChatBotDatabase.getInstance(context); + ApiManager apiManager = ApiManager.getInstance(); + FireBaseApiManager fireBaseApiManager = FireBaseApiManager.getInstance(); + apiManager.init(NetworkManager.getInstance()); + repository = new MessageRepository(database, apiManager, fireBaseApiManager); + } + } + return repository; + } + + /** + * Call to insert given project into database with thread safety + * + * @param newMessage given project + * @return + */ + private Message insertMessageInRoom(Message newMessage) { +// insert into Room using AsyncTask + Timber.i("Insert message in Room called%s", newMessage.getMsg()); + try { + return new InsertAsyncTask(database.messageDao()).execute(newMessage).get(); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + return newMessage; + } + + /** + * Call to update given project into database with thread safety + * + * @param oldMessage given project + */ + private void updateMessage(Message oldMessage) { + // insert into Room using AsyncTask + new UpdateAsyncTask(database.messageDao()).execute(oldMessage); + } + + /** + * Call to get Message with given Message ID + * + * @param mid given Message ID + * @return Message with given ID + */ + public Message getMessage(long mid) { + return fetchMessage(mid); + } + + /** + * Call to get Message with given Message ID with thread safety + * + * @param pid given Message ID + * @return Message with given ID + */ + private Message fetchMessage(long pid) { + try { + return new FetchMessageAsyncTask(database.messageDao()).execute(pid).get(); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Call to delete project with given project + * + * @param project given Message + * @return Deleted Message + */ + public void deleteMessage(Message project) { + deleteMessageFromRoom(project); + } + + /** + * Delete given Message from Room with Thread Safety + * + * @param project given Message + */ + private void deleteMessageFromRoom(Message project) { + try { + new DeleteMessageAsyncTask(database.messageDao()).execute(project).get(); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + } + + public ArrayList getMessagesForChatRoom(ChatRoom chatRoom) { + return getChatRoomMessagesFromDatabase(chatRoom); + } + + private ArrayList getChatRoomMessagesFromDatabase(ChatRoom chatRoom) { + try { + return (ArrayList) new FetchChatRoomMessagesAsyncTask(database.messageDao()).execute(chatRoom).get(); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + return new ArrayList<>(); + } + } + + /** + * Called when user sends a given new message in the ChatRoom + * - Add new Message to Room + * - Call API to fetch answer for new message + * - Sync message with FireStore + * + * @param newMessage given new message + */ + public void newMessageSent(final Context context, final Message newMessage, final OnMessageResponseReceivedListener listener) { + final Message roomLastMessage = insertMessageInRoom(newMessage); + insertMessageInFireBase(context, roomLastMessage); + apiManager.queryDatabase(context, newMessage, new NetworkCallback() { + @Override + public void onSuccess(NetResponse response) { + Message queryResponseMessage = Message.newMessage(response.getResponse().getData().getResponseMsg(), + newMessage.getReceiver(), newMessage.getSender(), newMessage.getChatRoomId()); + + Message roomLastInsertedMessage = insertMessageInRoom(queryResponseMessage); + insertMessageInFireBase(context, roomLastInsertedMessage); + listener.onMessageResponseReceived(queryResponseMessage); + } + + @Override + public void onError(NetError error) { + listener.onNoResponseReceived(new Error(error.getErrorLocalizeMessage())); + } + }); + +// TODO: Finish the remaining cart + } + + /** + * Call to insert the given new message to FireStore database + * + * @param context context + * @param newMessage given new message + */ + private void insertMessageInFireBase(Context context, Message newMessage) { + Intent intent = new Intent(context, FireBaseIntentService.class); + intent.putExtra(FireBaseIntentService.ACTION, FireBaseIntentService.ACTION_ADD_MESSAGE); + intent.putExtra(FireBaseIntentService.MESSAGE, newMessage); + context.startService(intent); + } + + public ArrayList getAllMessages() { + return (ArrayList) database.messageDao().getAllMessages(); + } + + /** + * Clear all given chat room messages + * - From Room + * - From FireStore + * + * @param chatRoom given chat room + */ + public void clearAllChatRoomMessages(ChatRoom chatRoom) { + clearAllChatRoomMessagesFromRoom(chatRoom); + } + + private void clearAllChatRoomMessagesFromRoom(ChatRoom chatRoom) { + new ClearAllMessagesInChatRoomAsyncTask(database.messageDao()).execute(chatRoom); + } + + /** + * Call to logout user and clear all messages from Room + */ + public void logOutUser() { + fireBaseApiManager.logOutUser(); + clearAllMessages(); + } + + /** + * Call to clear all messages from Room + */ + private void clearAllMessages() { + new ClearAllMessagesAsyncTask(database.messageDao()).execute(); + } + + /** + * Add given list of messages to Room + * + * @param messages + */ + public void addMessages(List messages) { + new AddAllMessagesAsyncTask(database.messageDao()).execute(messages); + } + + /** + * Call to add the given message to FireStore + * + * @param messageHashMap given message converted into HashMap + */ + public void addMessageToFireBase(Map messageHashMap) { + fireBaseApiManager.addMessageToUserDatabase(messageHashMap); + } + + /** + * Call to sync messages from FireStore to Room for the logged in user + */ + public void syncMessagesFromFireStoreToRoom() { + fireBaseApiManager.syncMessages(new DBValueListener>>() { + @Override + public void onDataReceived(List> data) { +// This code runs on the UI thread + List messages = new ArrayList<>(); + for (Map object : + data) { + Timber.i(String.valueOf(object.get("mid"))); + Message newMessage = new Message((long) object.get("mid"), String.valueOf(object.get("msg")), + String.valueOf(object.get("sender")), String.valueOf(object.get("receiver")), + (long) object.get("chatRoomId"), (long) object.get("timestamp")); + Timber.i(newMessage.toString()); + messages.add(newMessage); + } + addMessages(messages); + } + + @Override + public void onCancelled(Error error) { + + } + }); + } + + /** + * Fetch all chat room messages for the given ChatRoom through AsyncTask from Room + */ + private static class FetchChatRoomMessagesAsyncTask extends AsyncTask> { + + private MessageDao mAsyncTaskDao; + + FetchChatRoomMessagesAsyncTask(MessageDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected List doInBackground(ChatRoom... params) { + return mAsyncTaskDao.getAllMessagesFromChatRoom(params[0].getId()); + } + } + + /** + * Fetch all chat room messages for the given ChatRoom through AsyncTask from Room + */ + private static class ClearAllMessagesInChatRoomAsyncTask extends AsyncTask { + + private MessageDao mAsyncTaskDao; + + ClearAllMessagesInChatRoomAsyncTask(MessageDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected Void doInBackground(ChatRoom... params) { + mAsyncTaskDao.clearChatRoomMessages(params[0].getId()); + return null; + } + } + + /** + * Fetch all messages through AsyncTask from Room + */ + private static class AddAllMessagesAsyncTask extends AsyncTask, Void, Void> { + + private MessageDao mAsyncTaskDao; + + AddAllMessagesAsyncTask(MessageDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected Void doInBackground(List... lists) { + mAsyncTaskDao.insertAllMessages(lists[0]); + return null; + } + } + + /** + * Fetch all messages through AsyncTask from Room + */ + private static class ClearAllMessagesAsyncTask extends AsyncTask { + + private MessageDao mAsyncTaskDao; + + ClearAllMessagesAsyncTask(MessageDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected Void doInBackground(Void... params) { + mAsyncTaskDao.clear(); + return null; + } + } + + /** + * Call to get favorite projects from Room through AsyncTask + */ + private static class DeleteMessageAsyncTask extends AsyncTask { + + private MessageDao dao; + + DeleteMessageAsyncTask(MessageDao mDao) { + dao = mDao; + } + + + @Override + protected Message doInBackground(Message... params) { + dao.deleteItem(params[0]); + return params[0]; + } + } + + /** + * Fetch a specific project for the given Message ID through AsyncTask + */ + private static class FetchMessageAsyncTask extends AsyncTask { + + private MessageDao mAsyncTaskDao; + + FetchMessageAsyncTask(MessageDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected Message doInBackground(Long... params) { + return mAsyncTaskDao.get(params[0]); + } + } + + /** + * AsyncTask which makes insert operation thread safe and does not block the main thread for a long time + */ + private static class InsertAsyncTask extends AsyncTask { + + private MessageDao mAsyncTaskDao; + + InsertAsyncTask(MessageDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected Message doInBackground(final Message... params) { + mAsyncTaskDao.insert(params[0]); + Timber.i("Inside AsyncTask to insert message in Room %s", params[0].getMsg()); + return mAsyncTaskDao.getLatestMessage(params[0].getChatRoomId()); + } + } + + /** + * AsyncTask which makes insert operation thread safe and does not block the main thread for a long time + */ + private static class UpdateAsyncTask extends AsyncTask { + + private MessageDao mAsyncTaskDao; + + UpdateAsyncTask(MessageDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected Void doInBackground(final Message... params) { + mAsyncTaskDao.update(params[0]); + return null; + } + } + + public interface OnMessageResponseReceivedListener { + void onMessageResponseReceived(Message response); + + void onNoResponseReceived(Error error); + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/base/BaseActivity.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/base/BaseActivity.java new file mode 100644 index 0000000..86edcfc --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/base/BaseActivity.java @@ -0,0 +1,127 @@ +package com.fazemeright.chatbotmetcs622.ui.base; + +import android.app.Activity; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Bundle; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; + +import androidx.annotation.LayoutRes; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.fazemeright.chatbotmetcs622.network.ApiManager; +import com.fazemeright.chatbotmetcs622.network.NetworkManager; +import com.fazemeright.chatbotmetcs622.repositories.MessageRepository; +import com.fazemeright.firebase_api_library.api.FireBaseApiManager; + +public abstract class BaseActivity extends AppCompatActivity { + + public Context mContext; + protected FireBaseApiManager fireBaseApiManager; + protected MessageRepository messageRepository; + protected ApiManager apiManager; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = this; + fireBaseApiManager = FireBaseApiManager.getInstance(); + messageRepository = MessageRepository.getInstance(mContext); + apiManager = ApiManager.getInstance(); + apiManager.init(NetworkManager.getInstance()); + setContentView(getLayoutResId()); + } + + @Override + public void setContentView(@LayoutRes int layoutResID) { + super.setContentView(layoutResID); + initViews(); + setListeners(); + } + + /** + * Call to hide soft keyboard + * + * @param activity + */ + public void hideKeyboard(Activity activity) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE); + //Find the currently focused view, so we can grab the correct window token from it. + View view = activity.getCurrentFocus(); + //If no view currently has focus, create a new one, just so we can grab a window token from it + if (view == null) { + view = new View(activity); + } + if (imm != null) { + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + public void showKeyBoard(EditText yourEditText) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(yourEditText, InputMethodManager.SHOW_IMPLICIT); + } + } + + /** + * Call to disable the given button + * + * @param button given button + */ + protected void disableButton(Button button) { + button.setEnabled(false); + } + + /** + * Call to enable the given button + * + * @param button given button + */ + protected void enableButton(Button button) { + button.setEnabled(true); + } + + /** + * To initialize views of activity + */ + public abstract void initViews(); + + /** + * To set listeners of view or callback + */ + public abstract void setListeners(); + + /** + * To get layout resource id + */ + public abstract int getLayoutResId(); + + public boolean isNetworkConnected() { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = null; + if (cm != null) { + networkInfo = cm.getActiveNetworkInfo(); + } + return networkInfo != null && networkInfo.isConnected(); + } + + public Context getContext() { + return mContext; + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/base/BaseFragment.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/base/BaseFragment.java new file mode 100644 index 0000000..90812a4 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/base/BaseFragment.java @@ -0,0 +1,79 @@ +package com.fazemeright.chatbotmetcs622.ui.base; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.fazemeright.chatbotmetcs622.network.ApiManager; +import com.fazemeright.chatbotmetcs622.network.NetworkManager; +import com.fazemeright.chatbotmetcs622.repositories.MessageRepository; +import com.fazemeright.firebase_api_library.api.FireBaseApiManager; + +public abstract class BaseFragment extends Fragment { + + public Context mContext; + public FireBaseApiManager fireBaseApiManager; + protected MessageRepository messageRepository; + protected ApiManager apiManager; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = getActivity(); + fireBaseApiManager = FireBaseApiManager.getInstance(); + messageRepository = MessageRepository.getInstance(mContext); + apiManager = ApiManager.getInstance(); + apiManager.init(NetworkManager.getInstance()); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(getLayoutResId(), container, false); + initViews(view); + setListeners(view); + return view; + } + + /** + * To get layout resource id + */ + public abstract @LayoutRes + int getLayoutResId(); + + /** + * To initialize views of activity + */ + public abstract void initViews(View view); + + /** + * To set listeners of view or callback + * + * @param view + */ + public abstract void setListeners(View view); + + public boolean isNetworkConnected() { + ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = null; + if (cm != null) { + networkInfo = cm.getActiveNetworkInfo(); + } + return networkInfo != null && networkInfo.isConnected(); + } + + @Nullable + @Override + public Context getContext() { + return mContext; + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/chat/ChatActivity.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/chat/ChatActivity.java new file mode 100644 index 0000000..b04986a --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/chat/ChatActivity.java @@ -0,0 +1,193 @@ +package com.fazemeright.chatbotmetcs622.ui.chat; + +import android.text.TextUtils; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.fazemeright.chatbotmetcs622.R; +import com.fazemeright.chatbotmetcs622.database.messages.Message; +import com.fazemeright.chatbotmetcs622.models.ChatRoom; +import com.fazemeright.chatbotmetcs622.repositories.MessageRepository; +import com.fazemeright.chatbotmetcs622.ui.base.BaseActivity; +import com.fazemeright.chatbotmetcs622.ui.landing.LandingActivity; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; + +import java.util.ArrayList; +import java.util.Objects; + +public class ChatActivity extends BaseActivity implements View.OnClickListener { + + private RecyclerView rvChatList; + private ChatListAdapter adapter; + private ArrayList messages; + private EditText etMsg; + private ImageView ivSendMsg; + private ChatRoom chatRoom; + private ChipGroup dataFilterChipGroup; + + @Override + public void initViews() { + + etMsg = findViewById(R.id.etMsg); + ivSendMsg = findViewById(R.id.ivSendMsg); + rvChatList = findViewById(R.id.rvChatList); + dataFilterChipGroup = findViewById(R.id.dataFilterChipGroup); + + if (getIntent() != null) { + chatRoom = (ChatRoom) getIntent().getSerializableExtra(LandingActivity.SELECTED_CHAT_ROOM); + if (getSupportActionBar() != null) { + getSupportActionBar().setTitle(chatRoom.getName()); + getSupportActionBar().setHomeButtonEnabled(true); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + } + + String[] dataFilters = getResources().getStringArray(R.array.query_sample_selection); + setupFilterKeywords(dataFilters); + + rvChatList.setHasFixedSize(true); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(mContext); + linearLayoutManager.setReverseLayout(true); + linearLayoutManager.setStackFromEnd(true); + rvChatList.setLayoutManager(linearLayoutManager); + + ArrayList messages = messageRepository.getMessagesForChatRoom(chatRoom); + adapter = new ChatListAdapter(messages, mContext); + + rvChatList.setAdapter(adapter); +// Show user the most recent messages, hence scroll to the top + rvChatList.scrollToPosition(ChatListAdapter.MOST_RECENT_MSG_POSITION); + } + + /** + * Use to setup chips for the given list of data filter for device usage + * + * @param dataFilters given array of data filter + */ + private void setupFilterKeywords(String[] dataFilters) { +// remove all views from ChipGroup if any + dataFilterChipGroup.removeAllViews(); + if (dataFilters != null) { + for (final String dataFilter : + dataFilters) { +// create new chip and apply attributes + Chip chip = new Chip(Objects.requireNonNull(mContext)) {{ + setText(dataFilter); // set text + setClickable(true); + setCloseIconVisible(false); // no need for close icon in our scenario + setCheckable(true); // set checkable to be true, hence allow check changes + }}; + dataFilterChipGroup.addView(chip); // add chip to ChipGroup + chip.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + etMsg.requestFocus(); + etMsg.setText(buttonView.getText().toString()); + etMsg.setSelection(buttonView.getText().toString().length()); + showKeyBoard(etMsg); + } + } + }); + + } +// show ChipGroup if list is not empty + dataFilterChipGroup.setVisibility(View.VISIBLE); + } else { +// hide ChipGroup if list is empty + dataFilterChipGroup.setVisibility(View.INVISIBLE); + } + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + break; + case R.id.action_clear: + clearChatRoomMessagesClicked(chatRoom); + break; + } + return super.onOptionsItemSelected(item); + } + + /** + * Call to clear all message for the given ChatRoom + * + * @param chatRoom given ChatRoom + */ + private void clearChatRoomMessagesClicked(ChatRoom chatRoom) { + messageRepository.clearAllChatRoomMessages(chatRoom); + adapter.clearAllMessages(); + } + + @Override + public void setListeners() { + ivSendMsg.setOnClickListener(this); + } + + @Override + public int getLayoutResId() { + return R.layout.activity_chat; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.ivSendMsg) { + sendMessageClicked(); + } + } + + /** + * User clicked send message. Show new message to user and pass it to repository + */ + private void sendMessageClicked() { + String msg = etMsg.getText().toString().trim(); + if (TextUtils.isEmpty(msg)) { + return; + } + etMsg.setText(""); + Message newMessage = Message.newMessage(msg, Message.SENDER_USER, chatRoom.getName(), chatRoom.getId()); + addMessageToAdapter(newMessage); +// send new message to repository + messageRepository.newMessageSent(mContext, newMessage, new MessageRepository.OnMessageResponseReceivedListener() { + @Override + public void onMessageResponseReceived(Message response) { + addMessageToAdapter(response); + } + + @Override + public void onNoResponseReceived(Error error) { + Toast.makeText(mContext, error.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); +// TODO: Show error to the user + } + }); + } + + /** + * Call to add given new message to the Adapter and display it to the user and scroll to it + * + * @param newMessage given new message to be displayed + */ + private void addMessageToAdapter(Message newMessage) { + adapter.addMessage(newMessage); + rvChatList.scrollToPosition(ChatListAdapter.MOST_RECENT_MSG_POSITION); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_chat, menu); + return super.onCreateOptionsMenu(menu); + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/chat/ChatListAdapter.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/chat/ChatListAdapter.java new file mode 100644 index 0000000..992968b --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/chat/ChatListAdapter.java @@ -0,0 +1,128 @@ +package com.fazemeright.chatbotmetcs622.ui.chat; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.fazemeright.chatbotmetcs622.R; +import com.fazemeright.chatbotmetcs622.database.messages.Message; + +import java.util.ArrayList; + +/** + * RecyclerView Adapter to display Chat + * + * @see ChatActivity for use + */ +public class ChatListAdapter extends RecyclerView.Adapter { + + static final int MOST_RECENT_MSG_POSITION = 0; + private static final int TYPE_SENT = 0; + private static final int TYPE_RECEIVED = 1; + private ArrayList messages; + private Context context; + + + ChatListAdapter(ArrayList messages, Context context) { + this.messages = messages; + this.context = context; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + View view; + if (viewType == TYPE_SENT) { // for sent message layout + view = LayoutInflater.from(context).inflate(R.layout.sender_message_display_view_item, viewGroup, false); + return new SentViewHolder(view); + + } else { // for received message layout + view = LayoutInflater.from(context).inflate(R.layout.receiver_message_display_view_item, viewGroup, false); + return new ReceivedViewHolder(view); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (getItemViewType(position) == TYPE_SENT) { + ((SentViewHolder) holder).bind(messages.get(position)); + } else { + ((ReceivedViewHolder) holder).bind(messages.get(position)); + } + } + + @Override + public int getItemViewType(int position) { + if (messages.get(position).getSender().equals(Message.SENDER_USER)) { + return TYPE_SENT; + } else { + return TYPE_RECEIVED; + } + } + + @Override + public int getItemCount() { + return messages == null ? 0 : messages.size(); + } + + /** + * Call to add given new message to ArrayList at the bottom of the list and notify it was inserted + * + * @param newMessage given new message + */ + void addMessage(Message newMessage) { + if (messages == null) { + messages = new ArrayList<>(); + } + messages.add(MOST_RECENT_MSG_POSITION, newMessage); + notifyItemInserted(MOST_RECENT_MSG_POSITION); + } + + /** + * Call to remove all messages from the Data List and notify data set changed + */ + void clearAllMessages() { + messages.clear(); + notifyDataSetChanged(); + } + + public interface ChatMessageInteractionListener { + } + + public class SentViewHolder extends RecyclerView.ViewHolder { + + TextView tvMsg, tvTimestamp; + + SentViewHolder(@NonNull View itemView) { + super(itemView); + tvMsg = itemView.findViewById(R.id.tvMsg); + tvTimestamp = itemView.findViewById(R.id.tvTimestamp); + } + + void bind(Message item) { + tvMsg.setText(item.getMsg()); + tvTimestamp.setText(item.getFormattedTime()); + } + } + + public class ReceivedViewHolder extends RecyclerView.ViewHolder { + + TextView tvMsg, tvTimestamp; + + ReceivedViewHolder(@NonNull View itemView) { + super(itemView); + tvMsg = itemView.findViewById(R.id.tvMsg); + tvTimestamp = itemView.findViewById(R.id.tvTimestamp); + } + + void bind(Message item) { + tvMsg.setText(item.getMsg()); + tvTimestamp.setText(item.getFormattedTime()); + } + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/landing/ChatSelectionListAdapter.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/landing/ChatSelectionListAdapter.java new file mode 100644 index 0000000..e56df88 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/landing/ChatSelectionListAdapter.java @@ -0,0 +1,92 @@ +package com.fazemeright.chatbotmetcs622.ui.landing; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import com.fazemeright.chatbotmetcs622.R; +import com.fazemeright.chatbotmetcs622.models.ChatRoom; + +import java.util.ArrayList; + +/** + * RecyclerView Adapter to show Chat Rooms + * + * @see LandingActivity for use + */ +public class ChatSelectionListAdapter extends ListAdapter { + + private ChatListInteractionListener listener; + + protected ChatSelectionListAdapter(ChatListInteractionListener listener) { + super(new ChatRoomDiffCallBack()); + this.listener = listener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.chat_room_display_view_item, parent, false); + + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.bind(getItem(position)); + } + + public void submitDataList(ArrayList dataList) { + submitList(dataList); + } + + public interface ChatListInteractionListener { + void onChatRoomClicked(ChatRoom chatRoom); + } + + static class ChatRoomDiffCallBack extends DiffUtil.ItemCallback { + + @Override + public boolean areItemsTheSame(@NonNull ChatRoom oldItem, @NonNull ChatRoom newItem) { + return oldItem.getId() == newItem.getId(); + } + + @Override + public boolean areContentsTheSame(@NonNull ChatRoom oldItem, @NonNull ChatRoom newItem) { + return oldItem.equals(newItem); + } + } + + public class ViewHolder extends RecyclerView.ViewHolder { + + TextView tvChatRoomName; + ImageView ivChatRoom; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + tvChatRoomName = itemView.findViewById(R.id.tvChatRoomName); + ivChatRoom = itemView.findViewById(R.id.ivChatRoom); + } + + public void bind(ChatRoom item) { + tvChatRoomName.setText(item.getName()); + + ivChatRoom.setBackgroundResource(item.getLogoId()); + + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onChatRoomClicked(getItem(getAdapterPosition())); + } + }); + } + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/landing/LandingActivity.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/landing/LandingActivity.java new file mode 100644 index 0000000..159a450 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/landing/LandingActivity.java @@ -0,0 +1,99 @@ +package com.fazemeright.chatbotmetcs622.ui.landing; + +import android.content.Intent; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.fazemeright.chatbotmetcs622.R; +import com.fazemeright.chatbotmetcs622.models.ChatRoom; +import com.fazemeright.chatbotmetcs622.ui.base.BaseActivity; +import com.fazemeright.chatbotmetcs622.ui.chat.ChatActivity; +import com.fazemeright.chatbotmetcs622.ui.login.LoginActivity; +import com.fazemeright.chatbotmetcs622.ui.registration.RegistrationActivity; + +import java.util.ArrayList; + +public class LandingActivity extends BaseActivity implements ChatSelectionListAdapter.ChatListInteractionListener { + + public static final String SELECTED_CHAT_ROOM = "chatRoomSelected"; + private RecyclerView rvChatRoomList; + private ChatSelectionListAdapter adapter; + + @Override + public void initViews() { + if (getSupportActionBar() != null) { + String firstName = fireBaseApiManager.getCurrentUserFirstName(); + if (firstName == null) { + firstName = "Adit"; + } + getSupportActionBar().setTitle(getString(R.string.welcome_title) + " " + firstName); + } + + rvChatRoomList = findViewById(R.id.rvChatRoomList); + rvChatRoomList.setHasFixedSize(true); + rvChatRoomList.setLayoutManager(new LinearLayoutManager(mContext)); + rvChatRoomList.addItemDecoration(new DividerItemDecoration(rvChatRoomList.getContext(), LinearLayoutManager.VERTICAL)); + adapter = new ChatSelectionListAdapter(this); + rvChatRoomList.setAdapter(adapter); + + adapter.submitDataList(getChatRoomList()); + } + + private ArrayList getChatRoomList() { + ArrayList chatRooms = new ArrayList<>(); + chatRooms.add(new ChatRoom(ChatRoom.BRUTE_FORCE_ID, ChatRoom.BRUTE_FORCE, R.drawable.brute_force_logo)); + chatRooms.add(new ChatRoom(ChatRoom.LUCENE_ID, ChatRoom.LUCENE, R.drawable.lucene_logo)); + chatRooms.add(new ChatRoom(ChatRoom.MONGO_DB_ID, ChatRoom.MONGO_DB, R.drawable.mongodb_logo)); + chatRooms.add(new ChatRoom(ChatRoom.MY_SQL_ID, ChatRoom.MY_SQL, R.drawable.mysql_logo)); + return chatRooms; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_landing, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.action_logout: + logoutUser(); + openRegistrationActivity(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + private void openRegistrationActivity() { + startActivity(new Intent(LandingActivity.this, RegistrationActivity.class)); + finish(); + } + + private void logoutUser() { + messageRepository.logOutUser(); + } + + @Override + public void setListeners() { + + } + + @Override + public int getLayoutResId() { + return R.layout.activity_landing; + } + + @Override + public void onChatRoomClicked(ChatRoom chatRoom) { + Intent intent = new Intent(LandingActivity.this, ChatActivity.class); + intent.putExtra(SELECTED_CHAT_ROOM, chatRoom); + startActivity(intent); + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/login/LoginActivity.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/login/LoginActivity.java new file mode 100644 index 0000000..f803fbc --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/login/LoginActivity.java @@ -0,0 +1,143 @@ +package com.fazemeright.chatbotmetcs622.ui.login; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import com.fazemeright.chatbotmetcs622.R; +import com.fazemeright.chatbotmetcs622.intentservice.FireBaseIntentService; +import com.fazemeright.chatbotmetcs622.ui.base.BaseActivity; +import com.fazemeright.chatbotmetcs622.ui.landing.LandingActivity; +import com.fazemeright.chatbotmetcs622.utils.AppUtils; +import com.fazemeright.firebase_api_library.listeners.OnTaskCompleteListener; + +import timber.log.Timber; + +public class LoginActivity extends BaseActivity implements View.OnClickListener { + + private EditText userEmailEditText, userPasswordEditText; + private TextView tvDontHaveAccount; + private Button btnLogin; + + @Override + public void initViews() { +// set title for activity + if (getSupportActionBar() != null) { + getSupportActionBar().setTitle(getString(R.string.login_title)); + getSupportActionBar().setHomeButtonEnabled(true); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + + userEmailEditText = findViewById(R.id.userLoginEmailEditText); + userPasswordEditText = findViewById(R.id.userPasswordEditText); + tvDontHaveAccount = findViewById(R.id.tvDontHaveAccount); + btnLogin = findViewById(R.id.btnLogin); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + } + return super.onOptionsItemSelected(item); + } + + @Override + public void setListeners() { + btnLogin.setOnClickListener(this); + tvDontHaveAccount.setOnClickListener(this); + } + + @Override + public int getLayoutResId() { + return R.layout.activity_login; + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.tvDontHaveAccount: + openRegistrationActivity(); + break; + case R.id.btnLogin: +// TODO: Disable and re-enable the button after performing registration and validation + disableButton(btnLogin); + String email = userEmailEditText.getText().toString(); + String password = userPasswordEditText.getText().toString(); + performLogin(email, password); + enableButton(btnLogin); + break; + } + } + + /** + * Perform login with the given credentials + * + * @param email user email address + * @param password user password + */ + private void performLogin(String email, String password) { + if (!AppUtils.isValidEmail(email)) { + userEmailEditText.setError(mContext.getString(R.string.incorrect_email_err_msg)); + userEmailEditText.requestFocus(); + return; + } + + if (!AppUtils.isValidPassword(password)) { + userPasswordEditText.setError(mContext.getString(R.string.incorrect_pass_err_msg)); + userPasswordEditText.requestFocus(); + return; + } + + Timber.i("Login clicked"); + fireBaseApiManager.logInWithEmailAndPassword(email, password, new OnTaskCompleteListener() { + @Override + public void onTaskSuccessful() { + Timber.i("User logged in successfully %s", fireBaseApiManager.getCurrentLoggedInUserEmail()); + btnLogin.setText(getString(R.string.login_success_msg)); + Intent intent = new Intent(mContext, FireBaseIntentService.class); + intent.putExtra(FireBaseIntentService.ACTION, FireBaseIntentService.ACTION_SYNC_MESSAGES); +// ContextCompat.startForegroundService(LoginActivity.this, intent); +// ContextCompat.startForegroundService(mContext, intent); + ContextCompat.startForegroundService(mContext, intent); + openLandingActivity(); + } + + @Override + public void onTaskCompleteButFailed(String errMsg) { + Timber.e(errMsg); +// TODO: Show error to user + } + + @Override + public void onTaskFailed(Exception e) { + Timber.e(e); +// TODO: Show error to user + } + }); + } + + /** + * Open LandingActivity and finish this one + */ + private void openLandingActivity() { + startActivity(new Intent(LoginActivity.this, LandingActivity.class)); + finishAffinity(); + } + + /** + * Open Registration Activity + */ + private void openRegistrationActivity() { + finish(); + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/registration/RegistrationActivity.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/registration/RegistrationActivity.java new file mode 100644 index 0000000..0541698 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/registration/RegistrationActivity.java @@ -0,0 +1,150 @@ +package com.fazemeright.chatbotmetcs622.ui.registration; + +import android.content.Intent; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import com.fazemeright.chatbotmetcs622.ui.landing.LandingActivity; +import com.fazemeright.chatbotmetcs622.R; +import com.fazemeright.chatbotmetcs622.ui.base.BaseActivity; +import com.fazemeright.chatbotmetcs622.ui.login.LoginActivity; +import com.fazemeright.chatbotmetcs622.utils.AppUtils; +import com.fazemeright.firebase_api_library.listeners.OnTaskCompleteListener; + +import timber.log.Timber; + +public class RegistrationActivity extends BaseActivity implements View.OnClickListener { + + private EditText userEmailEditText, userPasswordEditText, userConPasswordEditText, etFirstName, etLastName; + private TextView tvHaveAccount; + private Button btnRegister; + + @Override + public void initViews() { +// set title for activity + if (getSupportActionBar() != null) { + getSupportActionBar().setTitle(getString(R.string.registration)); + } + + userEmailEditText = findViewById(R.id.userLoginEmailEditText); + etFirstName = findViewById(R.id.etFirstName); + etLastName = findViewById(R.id.etLastName); + userPasswordEditText = findViewById(R.id.userPasswordEditText); + userConPasswordEditText = findViewById(R.id.userConPasswordEditText); + tvHaveAccount = findViewById(R.id.tvHaveAccount); + btnRegister = findViewById(R.id.btnRegister); + + } + + @Override + public void setListeners() { + btnRegister.setOnClickListener(this); + tvHaveAccount.setOnClickListener(this); + } + + @Override + public int getLayoutResId() { + return R.layout.activity_registration; + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.tvHaveAccount: + openLoginActivity(); + break; + case R.id.btnRegister: + disableButton(btnRegister); + String email = userEmailEditText.getText().toString(); + String firstName = etFirstName.getText().toString(); + String lastName = etLastName.getText().toString(); + String password = userPasswordEditText.getText().toString(); + String conPassword = userConPasswordEditText.getText().toString(); + performRegistration(email, firstName, lastName, password, conPassword); + enableButton(btnRegister); + break; + } + } + + /** + * Open Login Activity + */ + private void openLoginActivity() { + startActivity(new Intent(RegistrationActivity.this, LoginActivity.class)); + } + + /** + * Call to perform validation on the input parameters and then perform registration + * + * @param email user email address + * @param firstName first name of user + * @param lastName last name of user + * @param password user selected password + * @param conPassword user selected confirmation password + */ + private void performRegistration(final String email, String firstName, String lastName, final String password, String conPassword) { + if (!AppUtils.isValidEmail(email)) { + userEmailEditText.setError(mContext.getString(R.string.incorrect_email_err_msg)); + userEmailEditText.requestFocus(); + return; + } + + if (!AppUtils.isValidName(firstName)) { + etFirstName.setError(mContext.getString(R.string.incorrect_first_name)); + etFirstName.requestFocus(); + return; + } + + if (!AppUtils.isValidName(lastName)) { + etLastName.setError(mContext.getString(R.string.incorrect_last_name)); + etLastName.requestFocus(); + return; + } + + if (!AppUtils.isValidPassword(password)) { + userPasswordEditText.setError(mContext.getString(R.string.incorrect_pass_err_msg)); + userPasswordEditText.requestFocus(); + return; + } + + if (!AppUtils.arePasswordsValid(password, conPassword)) { + userPasswordEditText.setError(mContext.getString(R.string.passwords_dont_match_err_msg)); + userPasswordEditText.requestFocus(); + userPasswordEditText.setText(""); + userConPasswordEditText.setText(""); + return; + } + + fireBaseApiManager.registerNewUserWithEmailPassword(email, password, firstName, lastName, new OnTaskCompleteListener() { + @Override + public void onTaskSuccessful() { + Timber.i("New user registered successfully %s", fireBaseApiManager.getCurrentLoggedInUserEmail()); + btnRegister.setText(getString(R.string.registration_success_msg)); + openLandingActivity(); + } + + @Override + public void onTaskCompleteButFailed(String errMsg) { + Timber.e(errMsg); +// TODO: Show error to user + } + + @Override + public void onTaskFailed(Exception e) { + Timber.e(e); +// TODO: Show error to user + } + }); + + } + + /** + * Open LandingActivity and finish this one + */ + private void openLandingActivity() { + startActivity(new Intent(RegistrationActivity.this, LandingActivity.class)); + finish(); + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/splash/SplashActivity.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/splash/SplashActivity.java new file mode 100644 index 0000000..2619afc --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/ui/splash/SplashActivity.java @@ -0,0 +1,149 @@ +package com.fazemeright.chatbotmetcs622.ui.splash; + +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.view.WindowManager; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.TextView; + +import androidx.work.Constraints; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; + +import com.fazemeright.chatbotmetcs622.ui.landing.LandingActivity; +import com.fazemeright.chatbotmetcs622.R; +import com.fazemeright.chatbotmetcs622.ui.base.BaseActivity; +import com.fazemeright.chatbotmetcs622.ui.registration.RegistrationActivity; +import com.fazemeright.chatbotmetcs622.workers.FireBaseSyncWorker; +import com.fazemeright.firebase_api_library.listeners.OnTaskCompleteListener; + +import java.util.concurrent.TimeUnit; + +import timber.log.Timber; + +public class SplashActivity extends BaseActivity { + + private TextView tvAppVersion, tvAppTitle; + + @Override + public void initViews() { + hideSystemUI(); + tvAppVersion = findViewById(R.id.tvAppVersion); + tvAppTitle = findViewById(R.id.tvAppTitle); + + tvAppVersion.setText(getAppVersion()); + + fadeInViews(); + + final Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + determineIfUserIsLoggedIn(); + } + }, 800); + } + + private void fadeInViews() { + Animation aniFade = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.fade_in); + tvAppTitle.startAnimation(aniFade); + tvAppVersion.startAnimation(aniFade); + } + + private void determineIfUserIsLoggedIn() { + fireBaseApiManager.reloadUserAuthState(new OnTaskCompleteListener() { + @Override + public void onTaskSuccessful() { +// user is logged in, open landing activity + Constraints constraints = new Constraints.Builder() + .setRequiresCharging(true) + .build(); + + PeriodicWorkRequest saveRequest = + new PeriodicWorkRequest.Builder(FireBaseSyncWorker.class, 1, TimeUnit.DAYS) + .setConstraints(constraints) + .build(); + + WorkManager.getInstance(mContext) + .enqueue(saveRequest); + + Timber.i("Open Landing Activity"); + openLandingActivity(); + } + + @Override + public void onTaskCompleteButFailed(String errMsg) { + // user not logged in, open registration activity + Timber.i("Open Registration Activity"); + openRegistrationActivity(); + } + + @Override + public void onTaskFailed(Exception e) { + // user not logged in or could not perform check, open registration activity + Timber.i("Open Registration Activity"); + openRegistrationActivity(); + } + }); + } + + /** + * Call to open RegistrationActivity from the current activity + */ + private void openRegistrationActivity() { + Animation animFadeOut = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.fade_out); + tvAppVersion.startAnimation(animFadeOut); + tvAppTitle.startAnimation(animFadeOut); + + startActivity(new Intent(SplashActivity.this, RegistrationActivity.class)); + finish(); + } + + /** + * Open LandingActivity and finish this one + */ + private void openLandingActivity() { + Animation animFadeOut = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.fade_out); + tvAppVersion.startAnimation(animFadeOut); + tvAppTitle.startAnimation(animFadeOut); + + startActivity(new Intent(SplashActivity.this, LandingActivity.class)); + finish(); + } + + /** + * Call to get the version of the Application + * + * @return version name of the application + */ + private String getAppVersion() { + PackageInfo pInfo; + try { + pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + return pInfo.versionName; + } catch (PackageManager.NameNotFoundException e) { + return "beta-testing"; + } + } + + /** + * Makes the screen layout to cover the full display of the device + */ + private void hideSystemUI() { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + + @Override + public void setListeners() { + + } + + @Override + public int getLayoutResId() { + return R.layout.activity_splash; + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/utils/AppUtils.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/utils/AppUtils.java new file mode 100644 index 0000000..76eb38a --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/utils/AppUtils.java @@ -0,0 +1,48 @@ +package com.fazemeright.chatbotmetcs622.utils; + +import android.text.TextUtils; +import android.util.Patterns; + +public class AppUtils { + + /** + * Call to check validity of given email address + * + * @param email given email address + * @return true if given email is valid, else false + */ + public static boolean isValidEmail(String email) { + return !TextUtils.isEmpty(email) && Patterns.EMAIL_ADDRESS.matcher(email).matches(); + } + + /** + * Call to check if given password is valid i.e. it is longer than 7 characters + * + * @param password given password + * @return true if given password is valid, else false + */ + public static boolean isValidPassword(String password) { + return !TextUtils.isEmpty(password) && password.length() > 7; + } + + /** + * Call to check if given password match or not + * + * @param password given password + * @param conPassword given confirm password + * @return true if both passwords match, else false + */ + public static boolean arePasswordsValid(String password, String conPassword) { + return password.equals(conPassword); + } + + /** + * Call to check if given name is valid or not. It should not be empty + * + * @param name given name + * @return true if given name is valid, else false + */ + public static boolean isValidName(String name) { + return !TextUtils.isEmpty(name); + } +} diff --git a/app/src/main/java/com/fazemeright/chatbotmetcs622/workers/FireBaseSyncWorker.java b/app/src/main/java/com/fazemeright/chatbotmetcs622/workers/FireBaseSyncWorker.java new file mode 100644 index 0000000..0febbf8 --- /dev/null +++ b/app/src/main/java/com/fazemeright/chatbotmetcs622/workers/FireBaseSyncWorker.java @@ -0,0 +1,30 @@ +package com.fazemeright.chatbotmetcs622.workers; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import com.fazemeright.chatbotmetcs622.database.messages.Message; +import com.fazemeright.chatbotmetcs622.repositories.MessageRepository; + +public class FireBaseSyncWorker extends Worker { + public FireBaseSyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { +// add all messages from Room to FireBase + MessageRepository messageRepository = MessageRepository.getInstance(getApplicationContext()); + for (Message message : messageRepository.getAllMessages()) { + messageRepository.addMessageToFireBase(Message.getHashMap(message)); + } +// get all messages from FireBase to Room + messageRepository.syncMessagesFromFireStoreToRoom(); + + return Result.success(); + } +} diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml new file mode 100644 index 0000000..306b605 --- /dev/null +++ b/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml new file mode 100644 index 0000000..d7841b9 --- /dev/null +++ b/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_send.xml b/app/src/main/res/drawable-anydpi/ic_send.xml new file mode 100644 index 0000000..8b51306 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_send.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-hdpi/ic_send.png b/app/src/main/res/drawable-hdpi/ic_send.png new file mode 100644 index 0000000000000000000000000000000000000000..fb9e7ed4e3aec0d642f77adf9cb4bbf06e5e8f7f GIT binary patch literal 272 zcmV+r0q_2aP)Z1(>s z3JEbxp64Tol?o+rh6S`03Z;-0++YKJl|s)PU{brzFNI1TI_I_N{GrgR&Ynu4lC;n{ z_M+3F&?lWOl|uG9jS9W#tWqfRgquzt=)eG`u!Jof;bN$BZKX4YE(~D?E7-vat`0i) zm2`T1yY{EEkwT3qll!O@kG}1CrGd!Y5M>Fzd6H3 zQGv}DPBpnY?2+hL&QK^I@cHs7CW(3cM=rQPDTJ{k87q8ad!(YE$Nng;ibY;opqp`{nSeE8p@qP6C*^uJ)299+?hHAX zFC8hf)Gsrh%`nl_$_Tr0am|ipxvj=r1&j + + + + + + + + + + diff --git a/app/src/main/res/drawable-xhdpi/ic_send.png b/app/src/main/res/drawable-xhdpi/ic_send.png new file mode 100644 index 0000000000000000000000000000000000000000..205c7a33cd974ff88d7eaca295c1e65cb17e3830 GIT binary patch literal 315 zcmV-B0mS}^P)J_knuh%q+UP1*ba&f(c3;22sSqsSN6?yUD^}+=@U;rjy z0XEcQiGd7nNzg4m+;-o8~r#2ynAwbB~d`VLKF`%{cALZ4;lSd z!Zvd$Zl@^^G_*%b+Cx3%SViSvi+;42+;Ep}yiWgvgZmeg#bU8U%^NvV104rD7ajlr N002ovPDHLkV1h4AenS8N literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_send.png b/app/src/main/res/drawable-xxhdpi/ic_send.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf52592ef6304729a9dbc86615c703d16a4fa32 GIT binary patch literal 422 zcmV;X0a^ZuP)Ms7|hKw$R;Caqd=V_ih!OjG8|*eW3A<-6ST!n>P? zd(_;0*Xa;K2qAVx{MPSv^kq5h!XX?R_=uRf_0^<7;ZKhP$4{XAZMZuD!(>icR$KMz-* zE&U9yKKuFs2*vo4=4YNiio9y@vg%4BwmaA`W6PCIdjN6l!@(g(&!G-4F#ggYAO@&H z7{OzRvt~ey0f#rmx@xKMW{Ag*MV0OC?FA%qY@2q9(p0})2XEV>xR Qg#Z8m07*qoM6N<$g3^D*asU7T literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/brute_force_logo.jpg b/app/src/main/res/drawable/brute_force_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6bf050f9b68013e6a43f3ea429fa157c7a80e174 GIT binary patch literal 20189 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 + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/lucene_logo.png b/app/src/main/res/drawable/lucene_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a714c72e5ca59ce289dae42c7f0d39917d126fd2 GIT binary patch literal 82426 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

>_t?DO;w&E&6POpeQsMs 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