├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .idea
├── .gitignore
├── .name
├── androidTestResultsUserPreferences.xml
├── compiler.xml
├── deploymentTargetDropDown.xml
├── gradle.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
└── vcs.xml
├── LICENSE.md
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── google-services.json
├── proguard-rules.pro
├── schemas
│ └── com.quickblox.qb_qmunicate.data.repository.db.Database
│ │ └── 1.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ ├── HiltAndroidJUnitRunner.kt
│ │ └── quickblox
│ │ └── qb_qmunicate
│ │ ├── UserDaoTest.kt
│ │ ├── UserRepositoryTest.kt
│ │ ├── source
│ │ ├── FirebaseSourceTest.kt
│ │ └── QuickBloxSourceTest.kt
│ │ └── use_case
│ │ ├── CheckUserExistUseCaseTest.kt
│ │ ├── GetLoggedUserExistUseCaseTest.kt
│ │ ├── SessionUpdateUseCaseTest.kt
│ │ ├── SignInUserUseCaseTest.kt
│ │ ├── SignOutUserUseCaseTest.kt
│ │ └── UpdateUserUseCaseTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── quickblox
│ │ │ └── qb_qmunicate
│ │ │ ├── App.kt
│ │ │ ├── data
│ │ │ ├── dependency
│ │ │ │ ├── DBModule.kt
│ │ │ │ ├── RepositoryModule.kt
│ │ │ │ └── SourceModule.kt
│ │ │ └── repository
│ │ │ │ ├── AuthRepositoryImpl.kt
│ │ │ │ ├── UserRepositoryImpl.kt
│ │ │ │ ├── db
│ │ │ │ ├── DaoMapper.kt
│ │ │ │ ├── Database.kt
│ │ │ │ ├── UserDao.kt
│ │ │ │ └── UserDbModel.kt
│ │ │ │ ├── firebase
│ │ │ │ ├── FirebaseMapper.kt
│ │ │ │ └── FirebaseSource.kt
│ │ │ │ └── quickblox
│ │ │ │ ├── QuickBloxMapper.kt
│ │ │ │ └── QuickBloxSource.kt
│ │ │ ├── domain
│ │ │ ├── dependency
│ │ │ │ └── UseCaseModule.kt
│ │ │ ├── entity
│ │ │ │ ├── UserEntity.kt
│ │ │ │ └── UserEntityImpl.kt
│ │ │ ├── exception
│ │ │ │ ├── DomainException.kt
│ │ │ │ └── RepositoryException.kt
│ │ │ ├── repository
│ │ │ │ ├── AuthRepository.kt
│ │ │ │ └── UserRepository.kt
│ │ │ └── use_case
│ │ │ │ ├── auth
│ │ │ │ └── SessionUpdateUseCase.kt
│ │ │ │ ├── base
│ │ │ │ ├── BaseUseCase.kt
│ │ │ │ ├── FlowUseCase.kt
│ │ │ │ └── UseCase.kt
│ │ │ │ └── user
│ │ │ │ ├── CheckUserExistUseCase.kt
│ │ │ │ ├── GetUserUseCase.kt
│ │ │ │ ├── SignInUserUseCase.kt
│ │ │ │ ├── SignOutUserUseCase.kt
│ │ │ │ └── UpdateUserUseCase.kt
│ │ │ └── presentation
│ │ │ ├── base
│ │ │ ├── BaseActivity.kt
│ │ │ ├── BaseFragment.kt
│ │ │ └── BaseViewModel.kt
│ │ │ ├── dialog
│ │ │ └── AvatarDialog.kt
│ │ │ ├── main
│ │ │ ├── MainActivity.kt
│ │ │ └── MainViewModel.kt
│ │ │ ├── profile
│ │ │ ├── create
│ │ │ │ ├── CreateProfileActivity.kt
│ │ │ │ └── CreateProfileViewModel.kt
│ │ │ └── settings
│ │ │ │ ├── SettingsFragment.kt
│ │ │ │ └── SettingsViewModel.kt
│ │ │ ├── start
│ │ │ ├── StartActivity.kt
│ │ │ └── StartViewModel.kt
│ │ │ └── theme_manager
│ │ │ └── ThemeManager.kt
│ └── res
│ │ ├── anim
│ │ ├── slide_in_left.xml
│ │ ├── slide_in_right.xml
│ │ ├── slide_out_left.xml
│ │ └── slide_out_right.xml
│ │ ├── color
│ │ ├── box_stroke_states.xml
│ │ ├── navigation_selector.xml
│ │ └── text_color_selector.xml
│ │ ├── drawable
│ │ ├── clear_text_icon.xml
│ │ ├── dialogs.xml
│ │ ├── qmunicate_logo.xml
│ │ └── settings.xml
│ │ ├── layout
│ │ ├── create_profile_layout.xml
│ │ ├── main_layout.xml
│ │ ├── settings_layout.xml
│ │ └── start_layout.xml
│ │ ├── menu
│ │ └── bottom_navigation_menu.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── values-night
│ │ └── themes.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── quickblox
│ └── qb_qmunicate
│ ├── DaoMapperUnitTest.kt
│ ├── FirebaseMapperUnitTest.kt
│ └── QuickBloxMapperUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshots
├── firebase-settings.png
├── google-json.png
└── run.png
└── settings.gradle.kts
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Release notifier
2 |
3 | on:
4 | release:
5 | types: [ published ]
6 |
7 | jobs:
8 | after-release:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Notify Slack
12 | run: |
13 | curl -X POST \
14 | --data-urlencode "payload={
15 | \"channel\": \"${{ secrets.SLACK_CHANNEL }}\",
16 | \"username\": \"${{ secrets.QB_PROJECT }} release bot\",
17 | \"text\": \"
18 | The ${{ secrets.QB_PROJECT }} version *${{ github.event.release.name }}* was released.
19 | >*Changelog:*
20 | \`\`\`${{ github.event.release.body }}\`\`\`
21 | >*Link:*
22 | ${{ github.event.release.html_url }}\",
23 | \"icon_emoji\": \":rocket:\"}" \
24 | --url ${{ secrets.SLACK_WEBHOOK_URL }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | keystore
17 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | q_municate
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2023 QuickBlox
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | # Before you begin
10 |
11 | Register a new account following [this link](https://admin.quickblox.com/signup). Type in your email and password to
12 | sign in. You can also sign in with your Google or Github accounts.
13 | Create the app clicking New app button.
14 | Configure the app. Type in the information about your organization into corresponding fields and click Add button.
15 | Go to Dashboard => YOUR_APP => Overview section and copy your Application ID, Authorization Key, Authorization Secret,
16 | and Account Key .
17 |
18 | # To launch th Q-municate
19 |
20 | To launch the project you need to take a few simple steps
21 |
22 | ## Step 1
23 |
24 | 1. Clone the repository using the link below:
25 |
26 | ```
27 | git clone https://github.com/QuickBlox/q-municate-android.git
28 | ```
29 |
30 | 2. Go to menu **File => Open Project**. (Or "Open an existing Project" if Android Studio is just opened)
31 | 3. Select a path to the Q-municate project.
32 |
33 | ## Step 2
34 |
35 | Add the following properties to the **project-level local.properties** file
36 |
37 | ### 1. Keystore properties
38 |
39 | These properties are needed so that you can sign your builds depending on the types you need to **debug** or **release**
40 |
41 | ```
42 | #
43 | ### Debug keystore properties
44 | debug_store_file=../keystore/debug.jks // the path where is the your debug key
45 | debug_store_password= // debug store password for your key
46 | debug_key_alias= // debug key alias for your key
47 | debug_key_password= // debug key password for your key
48 | #
49 | ### Release keystore properties
50 | release_store_file=../keystore/release.jks // the path where is the your release key
51 | release_store_password= // release store password for your key
52 | release_key_alias= // release key alias for your key
53 | release_key_password= // release key password for your key
54 |
55 | ```
56 |
57 | ### 2. Firebase properties
58 |
59 | This application uses Firebase for authorization by phone number, therefore you need to specify the parameters required
60 | for it. These properties can be found in **Project settings** particular project in the FireBase console
61 | at [this link](https://console.firebase.google.com/). If you haven't already created a FireBase
62 | project, [add Firebase to your Android project](https://firebase.google.com/docs/android/setup).
63 | Information on how to enable phone number sign-in in Firebase you can find
64 | by [this link](https://firebase.google.com/docs/auth/android/phone-auth#enable-phone-number-sign-in-for-your-firebase-project).
65 |
66 |
67 |
68 |
69 |
70 | ```
71 | #
72 | ### Firebase properties
73 | firebase_app_id="" // firebase app id
74 | ```
75 |
76 | ### 3. QuickBlox properties
77 |
78 | These properties you need to init QuickBlox SDK.
79 | How to get credentials is described in the [Before you begin](#before-you-begin) section.
80 |
81 | ```
82 | #
83 | ### QuickBlox properties
84 | quickblox_app_id="" // QuickBlox app_id your application
85 | quickblox_auth_key="" // QuickBlox auth_key your application
86 | quickblox_auth_secret="" // QuickBlox auth_secret your application
87 | quickblox_account_key="" // QuickBlox account_key your application
88 | quickblox_api_endpoint="" // QuickBlox api_endpoint your application
89 | quickblox_chat_endpoint="" // QuickBlox chat_endpoint your application
90 | ```
91 |
92 | ### 4. QuickBlox AI properties
93 |
94 | These properties need to use QuickBlox AI libraries. The QuickBlox AI libraries can be used with two different
95 | approaches. Either directly, using the libraries with raw Open AI token, or with a QuickBlox session token via a proxy
96 | server. We recommended using the proxy server method. Please ensure you use the latest method which you can check in
97 | our [repository](https://github.com/QuickBlox/qb-ai-assistant-proxy-server).
98 |
99 | ```
100 | #
101 | ### QuickBlox AI
102 | quickblox_open_ai_token="" // your open ai token
103 | quickblox_ai_proxy_server_url="" // proxy server URL
104 | ```
105 |
106 | ### Common example of completed properties
107 |
108 | ```
109 | #
110 | ### Debug keystore properties
111 | debug_store_file=../keystore/my-debug-key.jks
112 | debug_store_password=my-debug-store-password
113 | debug_key_alias=my-debug-alias
114 | debug_key_password=my-debug-key-password
115 | #
116 | ### Release keystore properties
117 | release_store_file=../keystore/my-release-key.jks
118 | release_store_password=my-release-store-password
119 | release_key_alias=my-release-alias
120 | release_key_password=my-release-key-password
121 | #
122 | ### Firebase properties
123 | firebase_app_id="my-firebase-app-123"
124 | #
125 | ### QuickBlox properties
126 | quickblox_app_id="67895"
127 | quickblox_auth_key="lkjdueksu7392kj"
128 | quickblox_auth_secret="BTFsj7Rtt27DAmT"
129 | quickblox_account_key="9yvTe17TmjNPqDoYtfqp"
130 | quickblox_api_endpoint="api.endpoint.com"
131 | quickblox_chat_endpoint="chat.endpoint.com"
132 | #
133 | ### QuickBlox AI
134 | quickblox_open_ai_token="qwerty123qwedsfjHDSJHdjhqkldsqjdsaklja"
135 | quickblox_ai_proxy_server_url="https://proxy.url"
136 | #
137 | ### Terms of Service and Privacy Policy
138 | tos_url="https://tos.url"
139 | privacy_policy_url="https://privacy_policy.url"
140 | ```
141 |
142 | ## Step 3
143 |
144 | For authorization to work correctly, you will need to add a configuration file for FireBase. Download the **google-services.json** file from **Project settings** your project in
145 | the [FireBase console](https://console.firebase.google.com/) then add this file to the module (app-level) root directory
146 | of your app. Detailed information can be found
147 | at [this link](https://firebase.google.com/docs/android/setup#add-config-file).
148 |
149 |
150 |
151 |
152 |
153 | ## Step 4
154 |
155 | After setting up all the properties and configurations described in the steps above, you can run the project. To do
156 | this, you will need to connect a real Android device or emulator to Android Studio and then click the **Run** button.
157 |
158 |
159 |
160 |
161 |
162 |
163 | ## License
164 |
165 | AIAnswerAssistant is released under the [MIT License](LICENSE.md).
166 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
2 |
3 | plugins {
4 | id("com.android.application")
5 | id("org.jetbrains.kotlin.android")
6 | id("com.google.gms.google-services")
7 | id("com.google.devtools.ksp")
8 | id("com.google.dagger.hilt.android")
9 | }
10 |
11 | android {
12 | namespace = "com.quickblox.qb_qmunicate"
13 | compileSdk = 34
14 |
15 | defaultConfig {
16 | applicationId = "com.quickblox.qb_qmunicate"
17 | minSdk = 24
18 | targetSdk = 33
19 | versionCode = 300018
20 | versionName = "3.0.2"
21 |
22 | resourceConfigurations.addAll(listOf("en"))
23 |
24 | testInstrumentationRunner = "com.HiltAndroidJUnitRunner"
25 |
26 | ksp {
27 | arg("room.schemaLocation", "$projectDir/schemas")
28 | }
29 | }
30 |
31 | fun getLocalProperty(propertyName: String): String {
32 | return gradleLocalProperties(rootDir).getProperty(propertyName)
33 | }
34 |
35 | signingConfigs {
36 | getByName("debug") {
37 | storeFile = file(getLocalProperty("debug_store_file"))
38 | storePassword = getLocalProperty("debug_store_password")
39 | keyAlias = getLocalProperty("debug_key_alias")
40 | keyPassword = getLocalProperty("debug_key_password")
41 | }
42 |
43 | create("release") {
44 | storeFile = file(getLocalProperty("release_store_file"))
45 | storePassword = getLocalProperty("release_store_password")
46 | keyAlias = getLocalProperty("release_key_alias")
47 | keyPassword = getLocalProperty("release_key_password")
48 | }
49 | }
50 |
51 | buildTypes {
52 | buildTypes.onEach { type ->
53 | type.buildConfigField("String", "QB_APPLICATION_ID", getLocalProperty("quickblox_app_id"))
54 | type.buildConfigField("String", "QB_AUTH_KEY", getLocalProperty("quickblox_auth_key"))
55 | type.buildConfigField("String", "QB_AUTH_SECRET", getLocalProperty("quickblox_auth_secret"))
56 | type.buildConfigField("String", "QB_ACCOUNT_KEY", getLocalProperty("quickblox_account_key"))
57 | type.buildConfigField("String", "QB_API_DOMAIN", getLocalProperty("quickblox_api_endpoint"))
58 | type.buildConfigField("String", "QB_CHAT_DOMAIN", getLocalProperty("quickblox_chat_endpoint"))
59 | type.buildConfigField("String", "QB_OPEN_AI_TOKEN", getLocalProperty("quickblox_open_ai_token"))
60 | type.buildConfigField("String", "QB_AI_PROXY_SERVER_URL", getLocalProperty("quickblox_ai_proxy_server_url"))
61 | type.buildConfigField("String", "FIREBASE_APP_ID", getLocalProperty("firebase_app_id"))
62 | type.buildConfigField("String", "TOS_URL", getLocalProperty("tos_url"))
63 | type.buildConfigField("String", "PRIVACY_POLICY_URL", getLocalProperty("privacy_policy_url"))
64 | }
65 |
66 | debug {
67 | isMinifyEnabled = false
68 | isDebuggable = true
69 | }
70 |
71 | release {
72 | isMinifyEnabled = true
73 | signingConfig = signingConfigs.getByName("release")
74 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
75 | }
76 | }
77 |
78 | compileOptions {
79 | sourceCompatibility = JavaVersion.VERSION_1_8
80 | targetCompatibility = JavaVersion.VERSION_1_8
81 | }
82 |
83 | kotlinOptions {
84 | jvmTarget = "1.8"
85 | }
86 |
87 | testOptions {
88 | unitTests {
89 | isIncludeAndroidResources = true
90 | }
91 | }
92 |
93 | buildFeatures {
94 | buildConfig = true
95 | viewBinding = true
96 | }
97 | }
98 |
99 | dependencies {
100 | implementation("androidx.core:core-ktx:1.12.0")
101 | implementation("androidx.appcompat:appcompat:1.6.1")
102 | implementation("com.google.android.material:material:1.11.0")
103 | implementation("androidx.constraintlayout:constraintlayout:2.1.4")
104 | implementation("androidx.activity:activity-ktx:1.8.1")
105 | implementation("androidx.fragment:fragment-ktx:1.6.2")
106 |
107 | // QuickBlox UI-Kit
108 | implementation("com.quickblox:android-ui-kit:0.9.0")
109 |
110 | // FireBase
111 | implementation(platform("com.google.firebase:firebase-bom:32.6.0"))
112 | implementation("com.firebaseui:firebase-ui-auth:8.0.2")
113 | implementation("com.google.firebase:firebase-appcheck-playintegrity")
114 | implementation("com.google.firebase:firebase-iid:21.1.0")
115 |
116 | // Coroutines and Kotlin Flow
117 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
118 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
119 |
120 | // Room
121 | val roomVersion = "2.6.1"
122 | implementation("androidx.room:room-runtime:$roomVersion")
123 | annotationProcessor("androidx.room:room-compiler:$roomVersion")
124 | ksp("androidx.room:room-compiler:$roomVersion")
125 | implementation("androidx.room:room-ktx:$roomVersion")
126 | implementation("androidx.room:room-guava:$roomVersion")
127 | implementation("androidx.room:room-paging:$roomVersion")
128 |
129 | // DI
130 | val daggerVersion = "2.49"
131 | implementation("com.google.dagger:hilt-android:$daggerVersion")
132 | ksp("com.google.dagger:hilt-android-compiler:$daggerVersion")
133 |
134 | // In-app Update
135 | implementation("com.google.android.play:app-update:2.1.0")
136 | implementation("com.google.android.play:app-update-ktx:2.1.0")
137 |
138 | // Tests
139 | androidTestImplementation("androidx.test:core:1.5.0")
140 | androidTestImplementation("androidx.test:runner:1.5.2")
141 | androidTestImplementation("androidx.test:rules:1.5.0")
142 | androidTestImplementation("androidx.test.ext:junit:1.1.5")
143 |
144 | testImplementation("junit:junit:4.13.2")
145 |
146 | androidTestImplementation("com.google.dagger:hilt-android-testing:$daggerVersion")
147 | kspAndroidTest("com.google.dagger:hilt-compiler:$daggerVersion")
148 | testImplementation("com.google.dagger:hilt-android-testing:$daggerVersion")
149 | kspTest("com.google.dagger:hilt-compiler:$daggerVersion")
150 |
151 | testImplementation("androidx.room:room-testing:2.6.1")
152 | testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0")
153 | testImplementation("org.mockito:mockito-core:5.4.0")
154 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")
155 | }
--------------------------------------------------------------------------------
/app/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "925980545669",
4 | "firebase_url": "https://q-municate-d1527.firebaseio.com",
5 | "project_id": "q-municate-d1527",
6 | "storage_bucket": "q-municate-d1527.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:925980545669:android:723c711fb0c8c800",
12 | "android_client_info": {
13 | "package_name": "com.quickblox.qb_qmunicate"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "925980545669-6kectvd4hbjgas65tevab6up1m42niti.apps.googleusercontent.com",
19 | "client_type": 1,
20 | "android_info": {
21 | "package_name": "com.quickblox.qb_qmunicate",
22 | "certificate_hash": "bc08f97df4bf6324f71b0d08bca035dbef667871"
23 | }
24 | },
25 | {
26 | "client_id": "925980545669-9bjj4ml9gji62n4ectcldqbg2rm4u104.apps.googleusercontent.com",
27 | "client_type": 1,
28 | "android_info": {
29 | "package_name": "com.quickblox.qb_qmunicate",
30 | "certificate_hash": "fdf0a69f35aa302165291e989563cd74699c2ceb"
31 | }
32 | },
33 | {
34 | "client_id": "925980545669-b6ni663i52mfn7127lqn059uaiih23k6.apps.googleusercontent.com",
35 | "client_type": 1,
36 | "android_info": {
37 | "package_name": "com.quickblox.qb_qmunicate",
38 | "certificate_hash": "2767502a86f19c1b4c16e441101d98944c78a4d4"
39 | }
40 | },
41 | {
42 | "client_id": "925980545669-bsihced5k2rijf50l2od2pkl6gcl4cn7.apps.googleusercontent.com",
43 | "client_type": 1,
44 | "android_info": {
45 | "package_name": "com.quickblox.qb_qmunicate",
46 | "certificate_hash": "4068d2abf43e5a7ba9e695bbcc8320ba21d9b0af"
47 | }
48 | },
49 | {
50 | "client_id": "925980545669-rkdb562eld1eket4o7a0ld3esmss8gqs.apps.googleusercontent.com",
51 | "client_type": 1,
52 | "android_info": {
53 | "package_name": "com.quickblox.qb_qmunicate",
54 | "certificate_hash": "884dda6e3c43132823dbc3d029d37e1fd8c0de3e"
55 | }
56 | },
57 | {
58 | "client_id": "925980545669-7oru770avf2erti4kb041didj21tmeov.apps.googleusercontent.com",
59 | "client_type": 3
60 | }
61 | ],
62 | "api_key": [
63 | {
64 | "current_key": "AIzaSyBgYI8FDSKQq1Z0c8yhy_N2GN__GLyyMtU"
65 | }
66 | ],
67 | "services": {
68 | "appinvite_service": {
69 | "other_platform_oauth_client": [
70 | {
71 | "client_id": "925980545669-7oru770avf2erti4kb041didj21tmeov.apps.googleusercontent.com",
72 | "client_type": 3
73 | },
74 | {
75 | "client_id": "925980545669-85hofki94agos76vjmbcq6cail05rkib.apps.googleusercontent.com",
76 | "client_type": 2,
77 | "ios_info": {
78 | "bundle_id": "com.quickblox.qmunicate"
79 | }
80 | }
81 | ]
82 | }
83 | }
84 | }
85 | ],
86 | "configuration_version": "1"
87 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 | -keepattributes EnclosingMethod
23 | -keepattributes InnerClasses
24 | -keepattributes Signature
25 | -keepattributes Exceptions
26 |
27 | # GSON @Expose annotation
28 | -keepattributes *Annotation*
29 |
30 | # Quickblox sdk
31 | -keep class com.quickblox.** { *; }
32 |
33 | # Smack xmpp library
34 | -keep class org.jxmpp.** { *; }
35 | -keep class org.jivesoftware.** { *; }
36 | -dontwarn org.jivesoftware.**
37 |
38 | # Glide
39 | -keep class com.bumptech.** { *; }
40 |
41 | # Google gms
42 | -keep class com.google.android.gms.** { *; }
43 |
44 | # Json
45 | -keep class org.json.** { *; }
--------------------------------------------------------------------------------
/app/schemas/com.quickblox.qb_qmunicate.data.repository.db.Database/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 1,
5 | "identityHash": "dc2c0d89afd5cfe34a7533badc450971",
6 | "entities": [
7 | {
8 | "tableName": "logged_user_table",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `login` TEXT NOT NULL, `full_name` TEXT, `avatar_file_id` INTEGER, `avatar_file_url` TEXT, PRIMARY KEY(`id`))",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "INTEGER",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "login",
19 | "columnName": "login",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "fullName",
25 | "columnName": "full_name",
26 | "affinity": "TEXT",
27 | "notNull": false
28 | },
29 | {
30 | "fieldPath": "avatarFileId",
31 | "columnName": "avatar_file_id",
32 | "affinity": "INTEGER",
33 | "notNull": false
34 | },
35 | {
36 | "fieldPath": "avatarFileUrl",
37 | "columnName": "avatar_file_url",
38 | "affinity": "TEXT",
39 | "notNull": false
40 | }
41 | ],
42 | "primaryKey": {
43 | "autoGenerate": false,
44 | "columnNames": [
45 | "id"
46 | ]
47 | },
48 | "indices": [],
49 | "foreignKeys": []
50 | }
51 | ],
52 | "views": [],
53 | "setupQueries": [
54 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
55 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dc2c0d89afd5cfe34a7533badc450971')"
56 | ]
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/HiltAndroidJUnitRunner.kt:
--------------------------------------------------------------------------------
1 | package com
2 |
3 | import android.content.Context
4 | import androidx.test.runner.AndroidJUnitRunner
5 | import dagger.hilt.android.testing.HiltTestApplication
6 | import android.app.Application as Application1
7 |
8 | class HiltAndroidJUnitRunner : AndroidJUnitRunner() {
9 | override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application1 {
10 | return super.newApplication(cl, HiltTestApplication::class.java.name, context)
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/quickblox/qb_qmunicate/UserDaoTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import androidx.test.core.app.ApplicationProvider
6 | import androidx.test.ext.junit.runners.AndroidJUnit4
7 | import com.quickblox.qb_qmunicate.data.repository.db.Database
8 | import com.quickblox.qb_qmunicate.data.repository.db.UserDao
9 | import com.quickblox.qb_qmunicate.data.repository.db.UserDbModel
10 | import org.junit.After
11 | import org.junit.Assert.*
12 | import org.junit.Before
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 | import kotlin.random.Random
16 |
17 | @RunWith(AndroidJUnit4::class)
18 | class UserDaoTest {
19 | private lateinit var userDao: UserDao
20 | private lateinit var db: Database
21 |
22 | @Before
23 | fun createDb() {
24 | val context = ApplicationProvider.getApplicationContext()
25 | db = Room.inMemoryDatabaseBuilder(
26 | context, Database::class.java
27 | ).build()
28 | userDao = db.getUserDao()
29 | }
30 |
31 | @After
32 | fun closeDb() {
33 | userDao.clear()
34 | db.close()
35 | }
36 |
37 | @Test
38 | fun createUser_saveUserAndLoadUser_createdAndLoadedUsersAreSame() {
39 | val createdUser = createUser()
40 | userDao.insert(createdUser)
41 |
42 | val loadedUser = userDao.getUser()
43 | assertEquals(createdUser, loadedUser)
44 | }
45 |
46 | @Test
47 | fun createUser_saveUserAndCleanDb_loadUserIsNull() {
48 | val createdUser = createUser()
49 | userDao.insert(createdUser)
50 |
51 | userDao.clear()
52 |
53 | val loadedUser = userDao.getUser()
54 | assertNull(loadedUser)
55 | }
56 |
57 | @Test
58 | fun createUser_updateUserAndLoadUser_updatedAndLoadedUsersAreSame() {
59 | userDao.insert(createUser())
60 | val loadedUser = userDao.getUser()
61 |
62 | val updatedUser = updateUser(loadedUser)
63 |
64 | userDao.update(
65 | updatedUser.id,
66 | updatedUser.login,
67 | updatedUser.fullName!!,
68 | updatedUser.avatarFileId!!,
69 | updatedUser.avatarFileUrl!!
70 | )
71 |
72 | val loadedUpdatedUser = userDao.getUser()
73 | assertEquals(updatedUser, loadedUpdatedUser)
74 | }
75 |
76 | @Test
77 | fun createUser_deleteUser_loadUserIsNull() {
78 | val createdUser = createUser()
79 | userDao.insert(createdUser)
80 |
81 | userDao.delete(createdUser)
82 |
83 | val loadedUser = userDao.getUser()
84 | assertNull(loadedUser)
85 | }
86 |
87 | @Test
88 | fun create2Users_loadUser_loadedCreatedUserWithHighestId() {
89 | val createdUserA = createUser(100)
90 | userDao.insert(createdUserA)
91 |
92 | val createdUserB = createUser(101)
93 | userDao.insert(createdUserB)
94 |
95 | val loadedUser = userDao.getUser()
96 |
97 | assertEquals(createdUserB, loadedUser)
98 | assertNotEquals(createdUserA, loadedUser)
99 | }
100 |
101 | private fun createUser(userId: Int = Random.nextInt()): UserDbModel {
102 | val timeStamp = System.currentTimeMillis()
103 | return UserDbModel(
104 | userId,
105 | "test_login_$timeStamp",
106 | "test_full_name_$timeStamp",
107 | Random.nextInt(),
108 | "test_avatar_url_$timeStamp"
109 | )
110 | }
111 |
112 | private fun updateUser(user: UserDbModel): UserDbModel {
113 | return UserDbModel(
114 | user.id,
115 | user.login + "_updated",
116 | user.fullName + "_updated",
117 | user.avatarFileId?.plus(Random.nextInt()),
118 | user.avatarFileUrl + "_updated"
119 | )
120 | }
121 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/quickblox/qb_qmunicate/UserRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
5 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
6 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
7 | import dagger.hilt.android.testing.HiltAndroidRule
8 | import dagger.hilt.android.testing.HiltAndroidTest
9 | import org.junit.After
10 | import org.junit.Assert.*
11 | import org.junit.Before
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 | import javax.inject.Inject
16 | import kotlin.random.Random
17 |
18 | @HiltAndroidTest
19 | @RunWith(AndroidJUnit4::class)
20 | class UserRepositoryTest {
21 | @get:Rule
22 | var hiltRule = HiltAndroidRule(this)
23 |
24 | @Inject
25 | lateinit var userRepository: UserRepository
26 |
27 | @Before
28 | fun initHilt() {
29 | hiltRule.inject()
30 | }
31 |
32 | @After
33 | fun cleanDb() {
34 | userRepository.deleteLocalUser()
35 | }
36 |
37 | @Test
38 | fun createUser_saveUserAndLoadUser_createdAndLoadedUsersAreSame() {
39 | val createdUser = createUser()
40 | userRepository.saveLocalUser(createdUser)
41 |
42 | val loadedUser = userRepository.getLocalUser()
43 | assertEquals(createdUser, loadedUser)
44 | }
45 |
46 | @Test
47 | fun createUser_updateUser_createdAndUpdatedAreNotSame() {
48 | val createdUser = createUser()
49 | userRepository.saveLocalUser(createdUser)
50 |
51 | userRepository.updateLocalUser(updateUser(createdUser))
52 |
53 | val loadedUser = userRepository.getLocalUser()
54 | assertNotEquals(createdUser, loadedUser)
55 | }
56 |
57 | @Test
58 | fun createUser_deleteUser_loadedUserIsNull() {
59 | userRepository.saveLocalUser(createUser())
60 | userRepository.deleteLocalUser()
61 |
62 | val loadedUser = userRepository.getLocalUser()
63 | assertNull(loadedUser)
64 | }
65 |
66 | private fun createUser(): UserEntity {
67 | return UserEntityImpl(
68 | id = Random.nextInt(),
69 | login = "test ${System.currentTimeMillis()}",
70 | fullName = "test${System.currentTimeMillis()}",
71 | avatarFileId = Random.nextInt(),
72 | avatarFileUrl = "avatar_file_url"
73 | )
74 | }
75 |
76 | private fun updateUser(user: UserEntity): UserEntity {
77 | return UserEntityImpl(
78 | id = user.id,
79 | login = user.login,
80 | fullName = "test${System.currentTimeMillis()}",
81 | avatarFileId = user.avatarFileId?.plus(Random.nextInt()),
82 | avatarFileUrl = "avatar_file_url"
83 | )
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/quickblox/qb_qmunicate/source/FirebaseSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.source
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import com.quickblox.qb_qmunicate.data.repository.firebase.FirebaseSource
5 | import dagger.hilt.android.testing.HiltAndroidTest
6 | import org.junit.Assert.*
7 | import org.junit.Test
8 | import org.junit.runner.RunWith
9 |
10 |
11 | @HiltAndroidTest
12 | @RunWith(AndroidJUnit4::class)
13 | class FirebaseSourceTest {
14 | @Test(expected = IllegalArgumentException::class)
15 | fun userNotExist_createToken_receivedException() {
16 | FirebaseSource().getToken()
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/quickblox/qb_qmunicate/source/QuickBloxSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.source
2 |
3 | import android.app.Activity
4 | import androidx.test.core.app.ActivityScenario
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import androidx.test.platform.app.InstrumentationRegistry
7 | import com.google.firebase.FirebaseException
8 | import com.google.firebase.auth.FirebaseAuth
9 | import com.google.firebase.auth.PhoneAuthCredential
10 | import com.google.firebase.auth.PhoneAuthOptions
11 | import com.google.firebase.auth.PhoneAuthProvider
12 | import com.google.firebase.auth.PhoneAuthProvider.OnVerificationStateChangedCallbacks
13 | import com.quickblox.auth.session.QBSessionManager
14 | import com.quickblox.auth.session.QBSettings
15 | import com.quickblox.qb_qmunicate.BuildConfig.FIREBASE_APP_ID
16 | import com.quickblox.qb_qmunicate.BuildConfig.QB_ACCOUNT_KEY
17 | import com.quickblox.qb_qmunicate.BuildConfig.QB_APPLICATION_ID
18 | import com.quickblox.qb_qmunicate.BuildConfig.QB_AUTH_KEY
19 | import com.quickblox.qb_qmunicate.BuildConfig.QB_AUTH_SECRET
20 | import com.quickblox.qb_qmunicate.data.repository.firebase.FirebaseSource
21 | import com.quickblox.qb_qmunicate.data.repository.quickblox.QuickBloxSource
22 | import com.quickblox.qb_qmunicate.presentation.profile.create.CreateProfileActivity
23 | import com.quickblox.users.QBUsers
24 | import com.quickblox.users.model.QBUser
25 | import dagger.hilt.android.testing.HiltAndroidTest
26 | import kotlinx.coroutines.Dispatchers
27 | import kotlinx.coroutines.runBlocking
28 | import kotlinx.coroutines.withContext
29 | import org.junit.After
30 | import org.junit.Assert.*
31 | import org.junit.Before
32 | import org.junit.Ignore
33 | import org.junit.Test
34 | import org.junit.runner.RunWith
35 | import java.util.concurrent.CountDownLatch
36 | import java.util.concurrent.TimeUnit
37 |
38 |
39 | @HiltAndroidTest
40 | @RunWith(AndroidJUnit4::class)
41 | class QuickBloxSourceTest {
42 | private val quickBloxSource: QuickBloxSource =
43 | QuickBloxSource(InstrumentationRegistry.getInstrumentation().targetContext)
44 |
45 | @Before
46 | fun init() {
47 |
48 | initQuickBloxSDK()
49 | }
50 |
51 | @After
52 | fun release() {
53 | quickBloxSource.clearSession()
54 | }
55 |
56 | private fun initQuickBloxSDK() {
57 | val context = InstrumentationRegistry.getInstrumentation().targetContext
58 | QBSettings.getInstance().init(context, QB_APPLICATION_ID, QB_AUTH_KEY, QB_AUTH_SECRET)
59 | QBSettings.getInstance().accountKey = QB_ACCOUNT_KEY
60 | }
61 |
62 | @Test
63 | fun userExist_logout_noErrors() = runBlocking {
64 | loginQBUser(buildQBUser())
65 | quickBloxSource.signOut()
66 |
67 | assertFalse(QBSessionManager.getInstance().isValidActiveSession)
68 | assertNull(QBSessionManager.getInstance().activeSession)
69 | }
70 |
71 | private fun buildQBUser(): QBUser {
72 | return QBUser().apply {
73 | login = "qwe11"
74 | password = "quickblox"
75 | }
76 | }
77 |
78 | private fun loginQBUser(qbUser: QBUser): QBUser = runBlocking {
79 | withContext(Dispatchers.IO) {
80 | QBUsers.signIn(qbUser).perform()
81 | }
82 | }
83 |
84 | @Test
85 | fun userNotExist_logout_noErrors() {
86 | quickBloxSource.signOut()
87 |
88 | assertFalse(QBSessionManager.getInstance().isValidActiveSession)
89 | assertNull(QBSessionManager.getInstance().activeSession)
90 | }
91 |
92 | @Test
93 | fun userExist_updateUser_userIsUpdated() = runBlocking {
94 | val loggedUser = loginQBUser(buildQBUser())
95 | loggedUser.fullName = "updated_full_name_${System.currentTimeMillis()}"
96 |
97 | val updatedUser = quickBloxSource.updateUser(loggedUser)
98 |
99 | assertEquals(loggedUser.fullName, updatedUser.fullName)
100 | }
101 |
102 | @Test(expected = IllegalArgumentException::class)
103 | fun userNotExist_createToken_receivedException() {
104 | FirebaseSource().getToken()
105 | }
106 |
107 | @Test
108 | @Ignore("Need to fix. After sign in by phone, the firebase user is not exist")
109 | fun signInByFirebasePhone_signIn_u(): Unit = runBlocking {
110 | val phoneSignInCountDown = CountDownLatch(1)
111 |
112 | var firebaseToken = ""
113 | val verificationCallback = object : OnVerificationStateChangedCallbacks() {
114 | override fun onVerificationCompleted(credential: PhoneAuthCredential) {
115 | println(credential)
116 |
117 | firebaseToken = credential.zzc() ?: ""
118 | assertTrue(firebaseToken.isNotEmpty())
119 |
120 | phoneSignInCountDown.countDown()
121 | }
122 |
123 | override fun onVerificationFailed(exception: FirebaseException) {
124 | println(exception)
125 | }
126 | }
127 |
128 | signInByPhone(verificationCallback)
129 |
130 | phoneSignInCountDown.await(20, TimeUnit.SECONDS)
131 | assertEquals(0, phoneSignInCountDown.count)
132 |
133 | val qbUser = quickBloxSource.signIn(FIREBASE_APP_ID, firebaseToken)
134 | assertNotNull(qbUser)
135 | }
136 |
137 | private fun signInByPhone(verificationCallback: OnVerificationStateChangedCallbacks) {
138 | ActivityScenario.launch(CreateProfileActivity::class.java).use { scenario ->
139 | scenario.onActivity { activity ->
140 | val firebaseAuth = FirebaseAuth.getInstance()
141 | val firebaseAuthSettings = firebaseAuth.firebaseAuthSettings
142 |
143 | val phoneNumber = "+380507777777"
144 | val smsCode = "111111"
145 | firebaseAuthSettings.setAutoRetrievedSmsCodeForPhoneNumber(phoneNumber, smsCode)
146 |
147 | val options = buildPhoneAuthOptions(phoneNumber, firebaseAuth, activity, verificationCallback)
148 | PhoneAuthProvider.verifyPhoneNumber(options)
149 | }
150 | }
151 | }
152 |
153 | private fun buildPhoneAuthOptions(
154 | phoneNumber: String,
155 | firebaseAuth: FirebaseAuth,
156 | activity: Activity,
157 | callback: OnVerificationStateChangedCallbacks,
158 | ): PhoneAuthOptions {
159 | return PhoneAuthOptions.newBuilder(firebaseAuth).setPhoneNumber(phoneNumber).setTimeout(20, TimeUnit.SECONDS)
160 | .setActivity(activity).setCallbacks(callback).build()
161 | }
162 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/quickblox/qb_qmunicate/use_case/CheckUserExistUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.use_case
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
5 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
6 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
7 | import com.quickblox.qb_qmunicate.domain.use_case.user.CheckUserExistUseCase
8 | import dagger.hilt.android.testing.HiltAndroidRule
9 | import dagger.hilt.android.testing.HiltAndroidTest
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.runBlocking
12 | import kotlinx.coroutines.withContext
13 | import org.junit.After
14 | import org.junit.Assert.*
15 | import org.junit.Before
16 | import org.junit.Rule
17 | import org.junit.Test
18 | import org.junit.runner.RunWith
19 | import javax.inject.Inject
20 | import kotlin.random.Random
21 |
22 | @HiltAndroidTest
23 | @RunWith(AndroidJUnit4::class)
24 | class CheckUserExistUseCaseTest {
25 | @get:Rule
26 | var hiltRule = HiltAndroidRule(this)
27 |
28 | @Inject
29 | lateinit var userRepository: UserRepository
30 |
31 | @Inject
32 | lateinit var useCase: CheckUserExistUseCase
33 |
34 | @Before
35 | fun initHilt() {
36 | hiltRule.inject()
37 | }
38 |
39 | @After
40 | fun cleanDb() {
41 | userRepository.deleteLocalUser()
42 | }
43 |
44 | @Test
45 | fun userAlreadyExist_execute_true() = runBlocking {
46 | val createdUser = createUser()
47 | userRepository.saveLocalUser(createdUser)
48 |
49 | var isUserExist: Boolean
50 | withContext(Dispatchers.Main) {
51 | isUserExist = useCase.execute(Unit)
52 | }
53 | assertTrue(isUserExist)
54 | }
55 |
56 | @Test
57 | fun userNotExist_execute_false() = runBlocking {
58 | var isUserExist: Boolean
59 | withContext(Dispatchers.Main) {
60 | isUserExist = useCase.execute(Unit)
61 | }
62 | assertFalse(isUserExist)
63 | }
64 |
65 | private fun createUser(): UserEntity {
66 | return UserEntityImpl(
67 | id = Random.nextInt(),
68 | login = "test ${System.currentTimeMillis()}",
69 | fullName = "test${System.currentTimeMillis()}",
70 | avatarFileId = Random.nextInt(),
71 | avatarFileUrl = "avatar_file_url"
72 | )
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/quickblox/qb_qmunicate/use_case/GetLoggedUserExistUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.use_case
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
5 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
6 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
7 | import com.quickblox.qb_qmunicate.domain.use_case.user.GetUserUseCase
8 | import dagger.hilt.android.testing.HiltAndroidRule
9 | import dagger.hilt.android.testing.HiltAndroidTest
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.runBlocking
12 | import kotlinx.coroutines.withContext
13 | import org.junit.After
14 | import org.junit.Assert.*
15 | import org.junit.Before
16 | import org.junit.Rule
17 | import org.junit.Test
18 | import org.junit.runner.RunWith
19 | import javax.inject.Inject
20 | import kotlin.random.Random
21 |
22 | @HiltAndroidTest
23 | @RunWith(AndroidJUnit4::class)
24 | class GetLoggedUserExistUseCaseTest {
25 | @get:Rule
26 | var hiltRule = HiltAndroidRule(this)
27 |
28 | @Inject
29 | lateinit var userRepository: UserRepository
30 |
31 | @Inject
32 | lateinit var useCase: GetUserUseCase
33 |
34 | @Before
35 | fun initHilt() {
36 | hiltRule.inject()
37 | }
38 |
39 | @After
40 | fun cleanDb() {
41 | userRepository.deleteLocalUser()
42 | }
43 |
44 | @Test
45 | fun userAlreadyExist_execute_true() = runBlocking {
46 | val createdUser = createUser()
47 | userRepository.saveLocalUser(createdUser)
48 |
49 | var loggedUser: UserEntity?
50 | withContext(Dispatchers.Main) {
51 | loggedUser = useCase.execute(Unit)
52 | }
53 | assertNotNull(loggedUser)
54 | }
55 |
56 | @Test
57 | fun userNotExist_execute_false() = runBlocking {
58 | var loggedUser: UserEntity?
59 | withContext(Dispatchers.Main) {
60 | loggedUser = useCase.execute(Unit)
61 | }
62 | assertNull(loggedUser)
63 | }
64 |
65 | private fun createUser(): UserEntity {
66 | return UserEntityImpl(
67 | id = Random.nextInt(),
68 | login = "test ${System.currentTimeMillis()}",
69 | fullName = "test${System.currentTimeMillis()}",
70 | avatarFileId = Random.nextInt(),
71 | avatarFileUrl = "avatar_file_url"
72 | )
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/quickblox/qb_qmunicate/use_case/SessionUpdateUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.use_case
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import com.quickblox.auth.model.QBProvider
5 | import com.quickblox.auth.session.QBSessionManager
6 | import com.quickblox.qb_qmunicate.domain.use_case.auth.SessionUpdateUseCase
7 | import dagger.hilt.android.testing.HiltAndroidRule
8 | import dagger.hilt.android.testing.HiltAndroidTest
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.runBlocking
12 | import org.junit.Assert.*
13 | import org.junit.Before
14 | import org.junit.Rule
15 | import org.junit.Test
16 | import org.junit.runner.RunWith
17 | import java.util.concurrent.CountDownLatch
18 | import java.util.concurrent.TimeUnit
19 | import javax.inject.Inject
20 |
21 | @HiltAndroidTest
22 | @RunWith(AndroidJUnit4::class)
23 | class SessionUpdateUseCaseTest {
24 | @get:Rule
25 | var hiltRule = HiltAndroidRule(this)
26 |
27 | @Inject
28 | lateinit var useCase: SessionUpdateUseCase
29 |
30 | @Before
31 | fun initHilt() {
32 | hiltRule.inject()
33 | }
34 |
35 | @Test
36 | fun sessionExist_execute_sessionUpdateNotified() = runBlocking {
37 | val sessionUpdateCountDown = CountDownLatch(1)
38 |
39 | val sessionUseCaseJob = launch(Dispatchers.Main) {
40 | useCase.execute(Unit).collect {
41 | sessionUpdateCountDown.countDown()
42 | }
43 | }
44 |
45 | notifySessionExpired()
46 |
47 | sessionUpdateCountDown.await(10, TimeUnit.SECONDS)
48 |
49 | sessionUseCaseJob.cancel()
50 |
51 | assertEquals(0, sessionUpdateCountDown.count)
52 | }
53 |
54 | private fun notifySessionExpired() {
55 | QBSessionManager.getInstance().listeners.forEach { listener ->
56 | listener.onProviderSessionExpired(QBProvider.FIREBASE_PHONE)
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/quickblox/qb_qmunicate/use_case/SignInUserUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.use_case
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
5 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
6 | import com.quickblox.qb_qmunicate.domain.repository.AuthRepository
7 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
8 | import com.quickblox.qb_qmunicate.domain.use_case.user.SignInUserUseCase
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.runBlocking
13 | import kotlinx.coroutines.withContext
14 | import org.junit.Assert.*
15 | import org.junit.Test
16 | import org.junit.runner.RunWith
17 | import kotlin.random.Random
18 |
19 | @RunWith(AndroidJUnit4::class)
20 | class SignInUserUseCaseTest {
21 | // TODO: need to fix FirebaseSourceTest and add real firebase token and project id like in test FirebaseSourceTest
22 |
23 | @Test
24 | fun repositoriesHaveData_execute_userCreated() = runBlocking {
25 | var createdUser: UserEntity?
26 | withContext(Dispatchers.Main) {
27 | createdUser = SignInUserUseCase(buildSpyUserRepository(), buildSpyAuthRepository()).execute(Unit)
28 | }
29 |
30 | assertNotNull(createdUser)
31 | }
32 |
33 | private fun createUser(): UserEntity {
34 | return UserEntityImpl(
35 | id = Random.nextInt(),
36 | login = "test_login_${System.currentTimeMillis()}",
37 | fullName = "test_full_name_${System.currentTimeMillis()}",
38 | avatarFileId = Random.nextInt(),
39 | avatarFileUrl = "test_avatar_url_${System.currentTimeMillis()}"
40 | )
41 | }
42 |
43 | private fun updateUser(user: UserEntity): UserEntity {
44 | return UserEntityImpl(
45 | id = user.id,
46 | login = user.login,
47 | fullName = user.fullName + "_updated",
48 | avatarFileId = user.avatarFileId?.plus(Random.nextInt()),
49 | avatarFileUrl = user.avatarFileUrl + "_updated"
50 | )
51 | }
52 |
53 | private fun buildSpyAuthRepository(): AuthRepository {
54 | // TODO: need to change using Mockito library
55 | return object : AuthRepository {
56 | override fun getFirebaseProjectId(): String {
57 | return "dummy_project_id_${System.currentTimeMillis()}"
58 | }
59 |
60 | override fun subscribeSessionExpiredFlow(): Flow {
61 | return MutableStateFlow(false)
62 | }
63 |
64 | override fun getFirebaseToken(): String {
65 | return "dummy_token_${System.currentTimeMillis()}"
66 | }
67 | }
68 | }
69 |
70 | private fun buildSpyUserRepository(): UserRepository {
71 | // TODO: need to change using Mockito library
72 | return object : UserRepository {
73 | override fun signInRemoteUser(firebaseProjectId: String, firebaseToken: String): UserEntity? {
74 | return createUser()
75 | }
76 |
77 | override fun getLocalUser(): UserEntity? {
78 | return createUser()
79 | }
80 |
81 | override fun saveLocalUser(userEntity: UserEntity) {
82 | // do nothing
83 | }
84 |
85 | override fun updateLocalUser(userEntity: UserEntity) {
86 | // do nothing
87 | }
88 |
89 | override fun deleteLocalUser() {
90 | // do nothing
91 | }
92 |
93 | override fun updateRemoteUser(userEntity: UserEntity): UserEntity? {
94 | return updateUser(userEntity)
95 | }
96 |
97 | override fun logoutRemoteUser() {
98 | // do nothing
99 | }
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/quickblox/qb_qmunicate/use_case/SignOutUserUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.use_case
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
5 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
6 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
7 | import com.quickblox.qb_qmunicate.domain.use_case.user.SignOutUserUseCase
8 | import dagger.hilt.android.testing.HiltAndroidRule
9 | import dagger.hilt.android.testing.HiltAndroidTest
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.runBlocking
12 | import kotlinx.coroutines.withContext
13 | import org.junit.After
14 | import org.junit.Assert.*
15 | import org.junit.Before
16 | import org.junit.Rule
17 | import org.junit.Test
18 | import org.junit.runner.RunWith
19 | import javax.inject.Inject
20 | import kotlin.random.Random
21 |
22 | @HiltAndroidTest
23 | @RunWith(AndroidJUnit4::class)
24 | class SignOutUserUseCaseTest {
25 | @get:Rule
26 | var hiltRule = HiltAndroidRule(this)
27 |
28 | @Inject
29 | lateinit var userRepository: UserRepository
30 |
31 | @Inject
32 | lateinit var useCase: SignOutUserUseCase
33 |
34 | @Before
35 | fun initHilt() {
36 | hiltRule.inject()
37 | }
38 |
39 | @After
40 | fun cleanDb() {
41 | userRepository.deleteLocalUser()
42 | }
43 |
44 | @Test
45 | fun createUser_execute_userDeleted() = runBlocking {
46 | val createdUser = createUser()
47 | userRepository.saveLocalUser(createdUser)
48 |
49 | withContext(Dispatchers.Main) {
50 | useCase.execute(Unit)
51 | }
52 |
53 | val loadedUser = userRepository.getLocalUser()
54 | assertNull(loadedUser)
55 | }
56 |
57 | @Test
58 | fun userNotExist_execute_userDeleted() = runBlocking {
59 | withContext(Dispatchers.Main) {
60 | useCase.execute(Unit)
61 | }
62 |
63 | val loadedUser = userRepository.getLocalUser()
64 | assertNull(loadedUser)
65 | }
66 |
67 | private fun createUser(): UserEntity {
68 | return UserEntityImpl(
69 | id = Random.nextInt(),
70 | login = "test ${System.currentTimeMillis()}",
71 | fullName = "test${System.currentTimeMillis()}",
72 | avatarFileId = Random.nextInt(),
73 | avatarFileUrl = "test${System.currentTimeMillis()}"
74 | )
75 | }
76 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/quickblox/qb_qmunicate/use_case/UpdateUserUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.use_case
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import com.quickblox.auth.session.QBSettings
6 | import com.quickblox.qb_qmunicate.BuildConfig.QB_ACCOUNT_KEY
7 | import com.quickblox.qb_qmunicate.BuildConfig.QB_APPLICATION_ID
8 | import com.quickblox.qb_qmunicate.BuildConfig.QB_AUTH_KEY
9 | import com.quickblox.qb_qmunicate.BuildConfig.QB_AUTH_SECRET
10 | import com.quickblox.qb_qmunicate.data.repository.quickblox.QuickBloxMapper
11 | import com.quickblox.qb_qmunicate.data.repository.quickblox.QuickBloxSource
12 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
13 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
14 | import com.quickblox.qb_qmunicate.domain.exception.DomainException
15 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
16 | import com.quickblox.qb_qmunicate.domain.use_case.user.UpdateUserUseCase
17 | import com.quickblox.users.QBUsers
18 | import com.quickblox.users.model.QBUser
19 | import dagger.hilt.android.testing.HiltAndroidRule
20 | import dagger.hilt.android.testing.HiltAndroidTest
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.runBlocking
23 | import kotlinx.coroutines.withContext
24 | import org.junit.After
25 | import org.junit.Assert.*
26 | import org.junit.Before
27 | import org.junit.Rule
28 | import org.junit.Test
29 | import org.junit.runner.RunWith
30 | import javax.inject.Inject
31 | import kotlin.random.Random
32 |
33 | @HiltAndroidTest
34 | @RunWith(AndroidJUnit4::class)
35 | class UpdateUserUseCaseTest {
36 | @get:Rule
37 | var hiltRule = HiltAndroidRule(this)
38 |
39 | @Inject
40 | lateinit var quickBloxSource: QuickBloxSource
41 |
42 | @Inject
43 | lateinit var userRepository: UserRepository
44 |
45 | @Inject
46 | lateinit var useCase: UpdateUserUseCase
47 |
48 | @Before
49 | fun init() {
50 | hiltRule.inject()
51 | initQuickBloxSDK()
52 | }
53 |
54 | @After
55 | fun release() {
56 | userRepository.deleteLocalUser()
57 | quickBloxSource.clearSession()
58 | }
59 |
60 | private fun initQuickBloxSDK() {
61 | val context = InstrumentationRegistry.getInstrumentation().targetContext
62 | QBSettings.getInstance().init(context, QB_APPLICATION_ID, QB_AUTH_KEY, QB_AUTH_SECRET)
63 | QBSettings.getInstance().accountKey = QB_ACCOUNT_KEY
64 | }
65 |
66 | @Test
67 | fun createAndLoginUser_execute_updatedUserExist() = runBlocking {
68 | val loggedQBUser = loginQBUser(buildQBUser())
69 | val loggedUserEntity = QuickBloxMapper.mapQBUserToUserEntity(loggedQBUser)
70 | userRepository.saveLocalUser(loggedUserEntity!!)
71 |
72 | val userWithUpdatedFullName = changedUserFullName(loggedUserEntity)
73 |
74 | var updatedUser: UserEntity?
75 | withContext(Dispatchers.Main) {
76 | updatedUser = useCase.execute(userWithUpdatedFullName)
77 | }
78 |
79 | assertEquals(userWithUpdatedFullName, updatedUser)
80 | }
81 |
82 | private fun buildQBUser(): QBUser {
83 | return QBUser().apply {
84 | login = "qwe11"
85 | password = "quickblox"
86 | }
87 | }
88 |
89 | private fun loginQBUser(qbUser: QBUser): QBUser = runBlocking {
90 | withContext(Dispatchers.IO) {
91 | QBUsers.signIn(qbUser).perform()
92 | }
93 | }
94 |
95 | @Test(expected = DomainException::class)
96 | fun userNotExist_execute_updatedUserExist(): Unit = runBlocking {
97 | val updatedUser = changedUserFullName(createUser())
98 | useCase.execute(updatedUser)
99 | }
100 |
101 | private fun createUser(): UserEntity {
102 | return UserEntityImpl(
103 | id = Random.nextInt(),
104 | login = "test ${System.currentTimeMillis()}",
105 | fullName = "test${System.currentTimeMillis()}",
106 | avatarFileId = Random.nextInt(),
107 | avatarFileUrl = "avatar_file_url"
108 | )
109 | }
110 |
111 | private fun changedUserFullName(user: UserEntity): UserEntity {
112 | return UserEntityImpl(
113 | id = user.id,
114 | login = user.login,
115 | fullName = "updated_fullName_${System.currentTimeMillis()}",
116 | avatarFileId = user.avatarFileId,
117 | avatarFileUrl = user.avatarFileUrl
118 | )
119 | }
120 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/App.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate
2 |
3 | import android.app.Application
4 | import com.quickblox.qb_qmunicate.domain.use_case.auth.SessionUpdateUseCase
5 | import com.quickblox.qb_qmunicate.presentation.start.StartActivity
6 | import dagger.hilt.android.HiltAndroidApp
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.launch
10 | import javax.inject.Inject
11 |
12 | @HiltAndroidApp
13 | class App : Application() {
14 | @Inject
15 | lateinit var sessionUpdateUseCase: SessionUpdateUseCase
16 |
17 | override fun onCreate() {
18 | super.onCreate()
19 | CoroutineScope(Dispatchers.Main).launch {
20 | sessionUpdateUseCase.execute(Unit).collect {
21 | StartActivity.show(applicationContext)
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/dependency/DBModule.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.dependency
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.quickblox.qb_qmunicate.data.repository.db.DB_NAME
6 | import com.quickblox.qb_qmunicate.data.repository.db.Database
7 | import com.quickblox.qb_qmunicate.data.repository.db.UserDao
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.hilt.InstallIn
11 | import dagger.hilt.android.qualifiers.ApplicationContext
12 | import dagger.hilt.components.SingletonComponent
13 | import javax.inject.Singleton
14 |
15 | @Module
16 | @InstallIn(SingletonComponent::class)
17 | object DBModule {
18 |
19 | @Singleton
20 | @Provides
21 | fun provideDatabase(@ApplicationContext applicationContext: Context): Database {
22 | return Room.databaseBuilder(applicationContext, Database::class.java, DB_NAME).build()
23 | }
24 |
25 | @Provides
26 | fun provideUserDao(database: Database): UserDao {
27 | return database.getUserDao()
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/dependency/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.dependency
2 |
3 | import com.quickblox.qb_qmunicate.data.repository.AuthRepositoryImpl
4 | import com.quickblox.qb_qmunicate.data.repository.UserRepositoryImpl
5 | import com.quickblox.qb_qmunicate.data.repository.db.UserDao
6 | import com.quickblox.qb_qmunicate.data.repository.firebase.FirebaseSource
7 | import com.quickblox.qb_qmunicate.data.repository.quickblox.QuickBloxSource
8 | import com.quickblox.qb_qmunicate.domain.repository.AuthRepository
9 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
10 | import dagger.Module
11 | import dagger.Provides
12 | import dagger.hilt.InstallIn
13 | import dagger.hilt.components.SingletonComponent
14 | import javax.inject.Singleton
15 |
16 | @Module
17 | @InstallIn(SingletonComponent::class)
18 | object RepositoryModule {
19 |
20 | @Provides
21 | @Singleton
22 | fun provideUserRepository(userDao: UserDao, quickBloxSource: QuickBloxSource): UserRepository {
23 | return UserRepositoryImpl(userDao, quickBloxSource)
24 | }
25 |
26 | @Provides
27 | @Singleton
28 | fun provideAuthRepository(firebaseSource: FirebaseSource, quickBloxSource: QuickBloxSource): AuthRepository {
29 | return AuthRepositoryImpl(firebaseSource, quickBloxSource)
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/dependency/SourceModule.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.dependency
2 |
3 | import android.content.Context
4 | import com.quickblox.qb_qmunicate.data.repository.firebase.FirebaseSource
5 | import com.quickblox.qb_qmunicate.data.repository.quickblox.QuickBloxSource
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.android.qualifiers.ApplicationContext
10 | import dagger.hilt.components.SingletonComponent
11 | import javax.inject.Singleton
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | object SourceModule {
16 | @Provides
17 | @Singleton
18 | fun provideFirebaseSource(): FirebaseSource {
19 | return FirebaseSource()
20 | }
21 |
22 | @Provides
23 | @Singleton
24 | fun provideQuickBloxSource(@ApplicationContext applicationContext: Context): QuickBloxSource {
25 | return QuickBloxSource(applicationContext)
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/repository/AuthRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.repository
2 |
3 | import com.quickblox.qb_qmunicate.data.repository.firebase.FirebaseSource
4 | import com.quickblox.qb_qmunicate.data.repository.quickblox.QuickBloxSource
5 | import com.quickblox.qb_qmunicate.domain.exception.RepositoryException
6 | import com.quickblox.qb_qmunicate.domain.repository.AuthRepository
7 | import kotlinx.coroutines.flow.Flow
8 | import javax.inject.Inject
9 |
10 | class AuthRepositoryImpl @Inject constructor(
11 | private val firebaseSource: FirebaseSource, private val quickBloxSource: QuickBloxSource
12 | ) : AuthRepository {
13 | override fun subscribeSessionExpiredFlow(): Flow {
14 | return quickBloxSource.subscribeSessionExpiredFlow()
15 | }
16 |
17 | override fun getFirebaseToken(): String {
18 | try {
19 | return firebaseSource.getToken()
20 | } catch (e: Exception) {
21 | throw RepositoryException(e.message ?: RepositoryException.Types.UNEXPECTED.name)
22 | }
23 | }
24 |
25 | override fun getFirebaseProjectId(): String {
26 | try {
27 | return firebaseSource.getProjectId()
28 | } catch (e: Exception) {
29 | throw RepositoryException(e.message ?: RepositoryException.Types.UNEXPECTED.name)
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/repository/UserRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.repository
2 |
3 | import com.quickblox.qb_qmunicate.data.repository.db.DaoMapper
4 | import com.quickblox.qb_qmunicate.data.repository.db.UserDao
5 | import com.quickblox.qb_qmunicate.data.repository.firebase.FirebaseMapper
6 | import com.quickblox.qb_qmunicate.data.repository.quickblox.QuickBloxMapper
7 | import com.quickblox.qb_qmunicate.data.repository.quickblox.QuickBloxSource
8 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
9 | import com.quickblox.qb_qmunicate.domain.exception.RepositoryException
10 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
11 | import javax.inject.Inject
12 |
13 | class UserRepositoryImpl @Inject constructor(
14 | private val userDao: UserDao, private val quickBloxSource: QuickBloxSource
15 | ) : UserRepository {
16 | init {
17 | quickBloxSource.initSDK()
18 | }
19 |
20 | override fun signInRemoteUser(firebaseProjectId: String, firebaseToken: String): UserEntity? {
21 | try {
22 | val qbUser = quickBloxSource.signIn(firebaseProjectId, firebaseToken)
23 | val userEntity = FirebaseMapper.mapQBUserToUserEntity(qbUser)
24 | userEntity?.avatarFileUrl = buildAvatarUrlFrom(userEntity?.avatarFileId)
25 | return userEntity
26 | } catch (e: Exception) {
27 | throw RepositoryException(e.message ?: RepositoryException.Types.UNEXPECTED.name)
28 | }
29 | }
30 |
31 | override fun getLocalUser(): UserEntity? {
32 | val userDbModel = userDao.getUser()
33 | val userEntity = DaoMapper.mapUserDbModelToUserEntity(userDbModel)
34 | return userEntity
35 | }
36 |
37 | override fun saveLocalUser(userEntity: UserEntity) {
38 | val userDbModel = DaoMapper.mapUserEntityToUserDbModel(userEntity)
39 | userDbModel?.let {
40 | userDao.insert(it)
41 | }
42 | }
43 |
44 | override fun updateLocalUser(userEntity: UserEntity) {
45 | val userDbModel = DaoMapper.mapUserEntityToUserDbModel(userEntity)
46 | userDbModel?.let {
47 | userDao.update(it.id, it.login, it.fullName ?: "", it.avatarFileId ?: -1, it.avatarFileUrl ?: "")
48 | }
49 | }
50 |
51 | override fun deleteLocalUser() {
52 | userDao.clear()
53 | }
54 |
55 | override fun updateRemoteUser(userEntity: UserEntity): UserEntity? {
56 | val qbUser = QuickBloxMapper.mapUserEntityToQBUser(userEntity)
57 | if (qbUser == null) {
58 | throw RepositoryException("The mapping of the user entity to the QBUser is null")
59 | }
60 |
61 | try {
62 | val updatedQBUser = quickBloxSource.updateUser(qbUser)
63 | val updatedUserEntity = QuickBloxMapper.mapQBUserToUserEntity(updatedQBUser)
64 | updatedUserEntity?.avatarFileUrl = buildAvatarUrlFrom(updatedUserEntity?.avatarFileId)
65 | return updatedUserEntity
66 | } catch (e: Exception) {
67 | throw RepositoryException(e.message ?: RepositoryException.Types.UNEXPECTED.name)
68 | }
69 | }
70 |
71 | private fun buildAvatarUrlFrom(avatarFileId: Int?): String? {
72 | val fileIdExist = avatarFileId != null && avatarFileId > 0
73 | if (fileIdExist) {
74 | val avatarUrl = quickBloxSource.buildFileUrlFrom(avatarFileId)
75 | return avatarUrl
76 | }
77 | return ""
78 | }
79 |
80 | override fun logoutRemoteUser() {
81 | try {
82 | quickBloxSource.signOut()
83 | } catch (e: Exception) {
84 | throw RepositoryException(e.message ?: RepositoryException.Types.UNEXPECTED.name)
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/repository/db/DaoMapper.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.repository.db
2 |
3 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
4 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
5 |
6 | object DaoMapper {
7 | fun mapUserDbModelToUserEntity(userDbModel: UserDbModel?): UserEntity? {
8 | return userDbModel?.let {
9 | UserEntityImpl(
10 | id = userDbModel.id,
11 | login = it.login,
12 | fullName = it.fullName,
13 | avatarFileId = it.avatarFileId,
14 | avatarFileUrl = it.avatarFileUrl
15 | )
16 | }
17 | }
18 |
19 | fun mapUserEntityToUserDbModel(userEntity: UserEntity?): UserDbModel? {
20 | return userEntity?.let {
21 | UserDbModel(
22 | id = userEntity.id,
23 | login = it.login,
24 | fullName = it.fullName,
25 | avatarFileId = it.avatarFileId,
26 | avatarFileUrl = it.avatarFileUrl
27 | )
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/repository/db/Database.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.repository.db
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 |
6 | const val DB_NAME = "q-municate-db"
7 |
8 | @Database(entities = [UserDbModel::class], version = 1)
9 | abstract class Database : RoomDatabase() {
10 | abstract fun getUserDao(): UserDao
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/repository/db/UserDao.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.repository.db
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import dagger.hilt.android.AndroidEntryPoint
8 |
9 | @Dao
10 | interface UserDao {
11 | @Query("SELECT * FROM logged_user_table ORDER BY id DESC LIMIT 1")
12 | fun getUser(): UserDbModel
13 |
14 | @Query("UPDATE logged_user_table SET id=:id, login=:login, full_name=:fullName, avatar_file_id=:avatarFileId, avatar_file_url=:avatarFileUrl WHERE id = :id")
15 | fun update(id: Int, login: String, fullName: String, avatarFileId: Int, avatarFileUrl: String)
16 |
17 | @Insert
18 | fun insert(user: UserDbModel)
19 |
20 | @Delete
21 | fun delete(user: UserDbModel)
22 |
23 | @Query("DELETE FROM logged_user_table")
24 | fun clear()
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/repository/db/UserDbModel.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.repository.db
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(tableName = "logged_user_table")
8 | data class UserDbModel(
9 | @PrimaryKey
10 | @ColumnInfo(name = "id")
11 | val id: Int,
12 |
13 | @ColumnInfo(name = "login")
14 | val login: String,
15 |
16 | @ColumnInfo(name = "full_name")
17 | val fullName: String?,
18 |
19 | @ColumnInfo(name = "avatar_file_id")
20 | val avatarFileId: Int?,
21 |
22 | @ColumnInfo(name = "avatar_file_url")
23 | val avatarFileUrl: String?
24 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/repository/firebase/FirebaseMapper.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.repository.firebase
2 |
3 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
4 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
5 | import com.quickblox.users.model.QBUser
6 |
7 | object FirebaseMapper {
8 | fun mapQBUserToUserEntity(qbUser: QBUser?): UserEntity? {
9 | return qbUser?.let {
10 | UserEntityImpl(
11 | id = qbUser.id,
12 | login = it.login,
13 | fullName = it.fullName,
14 | avatarFileId = it.fileId,
15 | avatarFileUrl = null
16 | )
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/repository/firebase/FirebaseSource.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.repository.firebase
2 |
3 | import com.google.android.gms.tasks.Tasks
4 | import com.google.firebase.auth.FirebaseAuth
5 | import com.google.firebase.auth.FirebaseUser
6 | import com.quickblox.qb_qmunicate.BuildConfig.FIREBASE_APP_ID
7 | import java.util.concurrent.TimeUnit
8 |
9 | class FirebaseSource {
10 | fun getToken(): String {
11 | val firebaseUser = getUser()
12 | if (isUserNotCorrect(firebaseUser)) {
13 | throw IllegalArgumentException("Firebase User is not logged in")
14 | }
15 |
16 | val task = firebaseUser?.getIdToken(false)
17 | if (task == null) {
18 | throw IllegalArgumentException("Firebase Task is null")
19 | }
20 |
21 | val token = Tasks.await(task, 10, TimeUnit.SECONDS).token
22 |
23 | if (isTokenNotCorrect(token)) {
24 | throw IllegalArgumentException("Firebase Access token is not exist")
25 | }
26 |
27 | return token!!
28 | }
29 |
30 | private fun getUser(): FirebaseUser? {
31 | return FirebaseAuth.getInstance().currentUser
32 | }
33 |
34 | private fun isUserNotCorrect(firebaseUser: FirebaseUser?): Boolean {
35 | return firebaseUser == null || firebaseUser.isAnonymous
36 | }
37 |
38 | private fun isTokenNotCorrect(accessToken: String?): Boolean {
39 | return accessToken.isNullOrEmpty()
40 | }
41 |
42 | fun getProjectId(): String {
43 | val projectId = FIREBASE_APP_ID
44 | if (isProjectIdNotCorrect(projectId)) {
45 | throw IllegalArgumentException("Firebase Project Id is not exist")
46 | }
47 | return projectId!!
48 | }
49 |
50 | private fun isProjectIdNotCorrect(projectId: String?): Boolean {
51 | return projectId.isNullOrEmpty()
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/repository/quickblox/QuickBloxMapper.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.repository.quickblox
2 |
3 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
4 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
5 | import com.quickblox.users.model.QBUser
6 |
7 | object QuickBloxMapper {
8 | fun mapQBUserToUserEntity(qbUser: QBUser?): UserEntity? {
9 | return qbUser?.let {
10 | UserEntityImpl(
11 | id = qbUser.id,
12 | login = it.login,
13 | fullName = it.fullName,
14 | avatarFileId = it.fileId,
15 | avatarFileUrl = null
16 | )
17 | }
18 | }
19 |
20 | fun mapUserEntityToQBUser(userEntity: UserEntity?): QBUser? {
21 | return userEntity?.let {
22 | QBUser().apply {
23 | id = userEntity.id
24 | login = it.login
25 | fullName = it.fullName
26 | fileId = it.avatarFileId
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/data/repository/quickblox/QuickBloxSource.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.data.repository.quickblox
2 |
3 | import android.content.Context
4 | import androidx.annotation.VisibleForTesting
5 | import com.quickblox.auth.model.QBProvider
6 | import com.quickblox.auth.session.QBSessionListenerImpl
7 | import com.quickblox.auth.session.QBSessionManager
8 | import com.quickblox.auth.session.QBSettings
9 | import com.quickblox.chat.QBChatService
10 | import com.quickblox.content.QBContent
11 | import com.quickblox.content.model.QBFile
12 | import com.quickblox.core.ServiceZone
13 | import com.quickblox.qb_qmunicate.BuildConfig
14 | import com.quickblox.users.QBUsers
15 | import com.quickblox.users.model.QBUser
16 | import kotlinx.coroutines.CoroutineScope
17 | import kotlinx.coroutines.Dispatchers
18 | import kotlinx.coroutines.flow.MutableStateFlow
19 | import kotlinx.coroutines.launch
20 | import javax.inject.Inject
21 |
22 | private const val UNEXPECTED_ERROR_MESSAGE = "Unexpected error: "
23 |
24 | class QuickBloxSource @Inject constructor(private val context: Context) {
25 | private val sessionExpiredFlow: MutableStateFlow = MutableStateFlow(false)
26 |
27 | init {
28 | QBSessionManager.getInstance().addListener(object : QBSessionListenerImpl() {
29 | override fun onProviderSessionExpired(provider: String?) {
30 | if (provider == QBProvider.FIREBASE_PHONE) {
31 | CoroutineScope(Dispatchers.Main).launch {
32 | sessionExpiredFlow.emit(true)
33 | }
34 | }
35 | }
36 | })
37 | }
38 |
39 | fun initSDK() {
40 | QBSettings.getInstance().init(
41 | context, BuildConfig.QB_APPLICATION_ID, BuildConfig.QB_AUTH_KEY, BuildConfig.QB_AUTH_SECRET
42 | )
43 |
44 | QBSettings.getInstance().accountKey = BuildConfig.QB_ACCOUNT_KEY
45 |
46 | if (isApiAndChatPointAvailable()) {
47 | QBSettings.getInstance()
48 | .setEndpoints(BuildConfig.QB_API_DOMAIN, BuildConfig.QB_CHAT_DOMAIN, ServiceZone.PRODUCTION);
49 | }
50 |
51 | initChatSettings()
52 | }
53 |
54 | private fun isApiAndChatPointAvailable(): Boolean {
55 | return BuildConfig.QB_API_DOMAIN.isNotEmpty() && BuildConfig.QB_CHAT_DOMAIN.isNotEmpty()
56 | }
57 |
58 | private fun initChatSettings() {
59 | val configurationBuilder = QBChatService.ConfigurationBuilder().apply {
60 | socketTimeout = 360
61 | }
62 | QBChatService.setConfigurationBuilder(configurationBuilder)
63 |
64 | QBChatService.setDefaultPacketReplyTimeout(10000)
65 | QBChatService.getInstance().setUseStreamManagement(true)
66 | }
67 |
68 | fun subscribeSessionExpiredFlow() = sessionExpiredFlow
69 |
70 | fun signIn(firebaseProjectId: String, firebaseToken: String): QBUser {
71 | clearSession()
72 |
73 | try {
74 | val user = QBUsers.signInUsingFirebase(firebaseProjectId, firebaseToken).perform()
75 | QBSessionManager.getInstance().activeSession.userId = user.id
76 | return user
77 | } catch (e: Exception) {
78 | throw IllegalArgumentException(e.message ?: (UNEXPECTED_ERROR_MESSAGE + "sign in user in QuickBlox"))
79 | }
80 | }
81 |
82 | fun signOut() {
83 | try {
84 | QBUsers.signOut().perform()
85 | clearSession()
86 | } catch (e: Exception) {
87 | throw IllegalArgumentException(e.message ?: (UNEXPECTED_ERROR_MESSAGE + "sign out user in QuickBlox"))
88 | }
89 | }
90 |
91 | fun updateUser(user: QBUser): QBUser {
92 | try {
93 | return QBUsers.updateUser(user).perform()
94 | } catch (e: Exception) {
95 | throw IllegalArgumentException(e.message ?: (UNEXPECTED_ERROR_MESSAGE + "sign out user in QuickBlox"))
96 | }
97 | }
98 |
99 | @VisibleForTesting
100 | fun clearSession() {
101 | QBSessionManager.getInstance().deleteActiveSession()
102 | QBSessionManager.getInstance().deleteSessionParameters()
103 |
104 | QBSessionManager.getInstance().isManuallyCreated = false
105 | QBSettings.getInstance().isAutoCreateSession = true
106 | }
107 |
108 | fun buildFileUrlFrom(fileId: Int?): String? {
109 | if (fileId == null) {
110 | return null
111 | }
112 |
113 | try {
114 | val file = QBContent.getFile(fileId).perform()
115 | return QBFile.getPrivateUrlForUID(file.uid)
116 | } catch (e: Exception) {
117 | return null
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/dependency/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.dependency
2 |
3 | import com.quickblox.qb_qmunicate.domain.repository.AuthRepository
4 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
5 | import com.quickblox.qb_qmunicate.domain.use_case.auth.SessionUpdateUseCase
6 | import com.quickblox.qb_qmunicate.domain.use_case.user.CheckUserExistUseCase
7 | import com.quickblox.qb_qmunicate.domain.use_case.user.SignInUserUseCase
8 | import com.quickblox.qb_qmunicate.domain.use_case.user.SignOutUserUseCase
9 | import com.quickblox.qb_qmunicate.domain.use_case.user.GetUserUseCase
10 | import com.quickblox.qb_qmunicate.domain.use_case.user.UpdateUserUseCase
11 | import dagger.Module
12 | import dagger.Provides
13 | import dagger.hilt.InstallIn
14 | import dagger.hilt.components.SingletonComponent
15 | import javax.inject.Singleton
16 |
17 | @Module
18 | @InstallIn(SingletonComponent::class)
19 | object UseCaseModule {
20 | @Provides
21 | @Singleton
22 | fun provideCheckLoggedUserExistUseCase(userRepository: UserRepository): CheckUserExistUseCase {
23 | return CheckUserExistUseCase(userRepository)
24 | }
25 |
26 | @Provides
27 | @Singleton
28 | fun provideCreateLoggedUserUseCase(
29 | userRepository: UserRepository,
30 | authRepository: AuthRepository
31 | ): SignInUserUseCase {
32 | return SignInUserUseCase(userRepository, authRepository)
33 | }
34 |
35 | @Provides
36 | @Singleton
37 | fun provideDeleteLoggedUserUseCase(userRepository: UserRepository): SignOutUserUseCase {
38 | return SignOutUserUseCase(userRepository)
39 | }
40 |
41 | @Provides
42 | @Singleton
43 | fun provideGetLoggedUserUseCase(userRepository: UserRepository): GetUserUseCase {
44 | return GetUserUseCase(userRepository)
45 | }
46 |
47 | @Provides
48 | @Singleton
49 | fun provideUpdateLoggedUserUseCase(userRepository: UserRepository): UpdateUserUseCase {
50 | return UpdateUserUseCase(userRepository)
51 | }
52 |
53 | @Provides
54 | @Singleton
55 | fun provideSessionUpdateUseCase(
56 | userRepository: UserRepository, authRepository: AuthRepository
57 | ): SessionUpdateUseCase {
58 | return SessionUpdateUseCase(userRepository, authRepository)
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/entity/UserEntity.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.entity
2 |
3 | interface UserEntity {
4 | val id: Int
5 | val login: String
6 | var fullName: String?
7 | var avatarFileId: Int?
8 | var avatarFileUrl: String?
9 |
10 | fun applyEmptyAvatar()
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/entity/UserEntityImpl.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.entity
2 |
3 | private const val EMPTY_AVATAR_FILE_ID = -1
4 |
5 | class UserEntityImpl(
6 | override val id: Int,
7 | override val login: String,
8 | override var fullName: String?,
9 | override var avatarFileId: Int?,
10 | override var avatarFileUrl: String?
11 | ) : UserEntity {
12 | override fun applyEmptyAvatar() {
13 | avatarFileId = EMPTY_AVATAR_FILE_ID
14 | avatarFileUrl = null
15 | }
16 |
17 | override fun equals(other: Any?): Boolean {
18 | if (this === other) {
19 | return true
20 | }
21 |
22 | if (other !is UserEntityImpl) {
23 | return false
24 | }
25 |
26 | val idAreEqual = id == other.id
27 | val loginAreEqual = login == other.login
28 | val fullNameAreEqual = fullName == other.fullName
29 |
30 | return idAreEqual && loginAreEqual && fullNameAreEqual
31 | }
32 |
33 | override fun hashCode(): Int {
34 | var result = login.hashCode()
35 | result = 31 * result + (fullName?.hashCode() ?: 0)
36 | return result
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/exception/DomainException.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.exception
2 |
3 | const val DOMAIN_UNEXPECTED_EXCEPTION = "unexpected exception"
4 |
5 | class DomainException(description: String) : Exception(description)
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/exception/RepositoryException.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.exception
2 |
3 | class RepositoryException(description: String) : Exception(description) {
4 | //TODO: add fabric method to create RepositoryException with different types of exceptions
5 |
6 | enum class Types {
7 | USER_ALREADY_EXIST,
8 | UNEXPECTED
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/repository/AuthRepository.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.repository
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface AuthRepository {
6 | fun subscribeSessionExpiredFlow(): Flow
7 |
8 | fun getFirebaseToken(): String
9 |
10 | fun getFirebaseProjectId(): String
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/repository/UserRepository.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.repository
2 |
3 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
4 |
5 | interface UserRepository {
6 | fun getLocalUser(): UserEntity?
7 |
8 | fun saveLocalUser(userEntity: UserEntity)
9 |
10 | fun updateLocalUser(userEntity: UserEntity)
11 |
12 | fun deleteLocalUser()
13 |
14 | fun signInRemoteUser(firebaseProjectId: String, firebaseToken: String): UserEntity?
15 |
16 | fun updateRemoteUser(userEntity: UserEntity): UserEntity?
17 |
18 | fun logoutRemoteUser()
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/use_case/auth/SessionUpdateUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.use_case.auth
2 |
3 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
4 | import com.quickblox.qb_qmunicate.domain.exception.RepositoryException
5 | import com.quickblox.qb_qmunicate.domain.repository.AuthRepository
6 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
7 | import com.quickblox.qb_qmunicate.domain.use_case.base.FlowUseCase
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.channelFlow
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | class SessionUpdateUseCase @Inject constructor(
15 | private val userRepository: UserRepository, private val authRepository: AuthRepository
16 | ) : FlowUseCase() {
17 | override suspend fun execute(args: Unit): Flow {
18 | return channelFlow {
19 | launch(Dispatchers.Main) {
20 | authRepository.subscribeSessionExpiredFlow().collect { expired ->
21 | val notExpired = !expired
22 | if (notExpired) {
23 | return@collect
24 | }
25 |
26 | try {
27 | val user = signInRemote()
28 | if (user != null) {
29 | saveLocalLoggedUser(user)
30 | } else {
31 | send(Unit)
32 | }
33 | } catch (e: RepositoryException) {
34 | send(Unit)
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
41 | private fun signInRemote(): UserEntity? {
42 | val firebaseProjectId = authRepository.getFirebaseProjectId()
43 | val firebaseToken = authRepository.getFirebaseToken()
44 |
45 | return userRepository.signInRemoteUser(firebaseProjectId, firebaseToken)
46 | }
47 |
48 | private fun saveLocalLoggedUser(user: UserEntity) {
49 | userRepository.deleteLocalUser()
50 | userRepository.saveLocalUser(user)
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/use_case/base/BaseUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Injoit on 7.4.2023.
3 | * Copyright © 2023 Quickblox. All rights reserved.
4 | */
5 |
6 | package com.quickblox.qb_qmunicate.domain.use_case.base
7 |
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.isActive
10 |
11 | abstract class BaseUseCase : UseCase {
12 | protected fun isScopeNotActive(scope: CoroutineScope): Boolean {
13 | return !scope.isActive
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/use_case/base/FlowUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Injoit on 24.3.2023.
3 | * Copyright © 2023 Quickblox. All rights reserved.
4 | */
5 |
6 | package com.quickblox.qb_qmunicate.domain.use_case.base
7 |
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | abstract class FlowUseCase : BaseUseCase>()
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/use_case/base/UseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Injoit on 13.01.2023.
3 | * Copyright © 2023 Quickblox. All rights reserved.
4 | */
5 | package com.quickblox.qb_qmunicate.domain.use_case.base
6 |
7 | interface UseCase {
8 | suspend fun execute(args: TArgs): TResult
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/use_case/user/CheckUserExistUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.use_case.user
2 |
3 | import com.quickblox.qb_qmunicate.domain.exception.DOMAIN_UNEXPECTED_EXCEPTION
4 | import com.quickblox.qb_qmunicate.domain.exception.DomainException
5 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
6 | import com.quickblox.qb_qmunicate.domain.use_case.base.BaseUseCase
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.withContext
9 | import javax.inject.Inject
10 |
11 | class CheckUserExistUseCase @Inject constructor(private val userRepository: UserRepository) :
12 | BaseUseCase() {
13 |
14 | override suspend fun execute(args: Unit): Boolean {
15 | var isUserExist: Boolean? = null
16 |
17 | withContext(Dispatchers.IO) {
18 | runCatching {
19 | val user = userRepository.getLocalUser()
20 | isUserExist = user != null
21 | }.onFailure { error ->
22 | throw DomainException(error.message ?: DOMAIN_UNEXPECTED_EXCEPTION)
23 | }
24 | }
25 |
26 | return isUserExist!!
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/use_case/user/GetUserUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.use_case.user
2 |
3 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
4 | import com.quickblox.qb_qmunicate.domain.exception.DOMAIN_UNEXPECTED_EXCEPTION
5 | import com.quickblox.qb_qmunicate.domain.exception.DomainException
6 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
7 | import com.quickblox.qb_qmunicate.domain.use_case.base.BaseUseCase
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.withContext
10 | import javax.inject.Inject
11 |
12 | class GetUserUseCase @Inject constructor(private val userRepository: UserRepository) :
13 | BaseUseCase() {
14 |
15 | override suspend fun execute(args: Unit): UserEntity? {
16 | var localLoggedUser: UserEntity? = null
17 |
18 | withContext(Dispatchers.IO) {
19 | runCatching {
20 | localLoggedUser = userRepository.getLocalUser()
21 | }.onFailure { error ->
22 | throw DomainException(error.message ?: DOMAIN_UNEXPECTED_EXCEPTION)
23 | }
24 | }
25 |
26 | return localLoggedUser
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/use_case/user/SignInUserUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.use_case.user
2 |
3 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
4 | import com.quickblox.qb_qmunicate.domain.exception.DOMAIN_UNEXPECTED_EXCEPTION
5 | import com.quickblox.qb_qmunicate.domain.exception.DomainException
6 | import com.quickblox.qb_qmunicate.domain.repository.AuthRepository
7 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
8 | import com.quickblox.qb_qmunicate.domain.use_case.base.BaseUseCase
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.withContext
11 | import javax.inject.Inject
12 |
13 | class SignInUserUseCase @Inject constructor(
14 | private val userRepository: UserRepository, private val authRepository: AuthRepository
15 | ) : BaseUseCase() {
16 |
17 | override suspend fun execute(args: Unit): UserEntity? {
18 | var createdUser: UserEntity? = null
19 |
20 | withContext(Dispatchers.IO) {
21 | runCatching {
22 | val firebaseProjectId = authRepository.getFirebaseProjectId()
23 | val firebaseToken = authRepository.getFirebaseToken()
24 |
25 | createdUser = userRepository.signInRemoteUser(firebaseProjectId, firebaseToken)
26 | createdUser?.let { user ->
27 | userRepository.deleteLocalUser()
28 | userRepository.saveLocalUser(user)
29 | }
30 | }.onFailure { error ->
31 | throw DomainException(error.message ?: DOMAIN_UNEXPECTED_EXCEPTION)
32 | }
33 | }
34 |
35 | return createdUser
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/use_case/user/SignOutUserUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.use_case.user
2 |
3 | import com.quickblox.qb_qmunicate.domain.exception.DOMAIN_UNEXPECTED_EXCEPTION
4 | import com.quickblox.qb_qmunicate.domain.exception.DomainException
5 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
6 | import com.quickblox.qb_qmunicate.domain.use_case.base.BaseUseCase
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.withContext
9 | import javax.inject.Inject
10 |
11 | class SignOutUserUseCase @Inject constructor(private val userRepository: UserRepository) : BaseUseCase() {
12 | override suspend fun execute(args: Unit) {
13 | withContext(Dispatchers.IO) {
14 | runCatching {
15 | userRepository.logoutRemoteUser()
16 | userRepository.deleteLocalUser()
17 | }.onFailure { error ->
18 | throw DomainException(error.message ?: DOMAIN_UNEXPECTED_EXCEPTION)
19 | }
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/domain/use_case/user/UpdateUserUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.domain.use_case.user
2 |
3 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
4 | import com.quickblox.qb_qmunicate.domain.exception.DOMAIN_UNEXPECTED_EXCEPTION
5 | import com.quickblox.qb_qmunicate.domain.exception.DomainException
6 | import com.quickblox.qb_qmunicate.domain.repository.UserRepository
7 | import com.quickblox.qb_qmunicate.domain.use_case.base.BaseUseCase
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.withContext
10 | import javax.inject.Inject
11 |
12 | class UpdateUserUseCase @Inject constructor(private val userRepository: UserRepository) :
13 | BaseUseCase() {
14 |
15 | override suspend fun execute(userEntity: UserEntity): UserEntity? {
16 | var updatedUser: UserEntity? = null
17 |
18 | withContext(Dispatchers.IO) {
19 | runCatching {
20 | val updatedUserInRemote = userRepository.updateRemoteUser(userEntity)
21 |
22 | updatedUserInRemote?.let {
23 | userRepository.updateLocalUser(updatedUserInRemote)
24 | updatedUser = userRepository.getLocalUser()
25 | }
26 | }.onFailure { error ->
27 | throw DomainException(error.message ?: DOMAIN_UNEXPECTED_EXCEPTION)
28 | }
29 | }
30 |
31 | return updatedUser
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.base
2 |
3 | import android.widget.Toast
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.lifecycle.lifecycleScope
6 | import kotlinx.coroutines.launch
7 | import java.util.regex.Pattern
8 |
9 | abstract class BaseActivity : AppCompatActivity() {
10 | protected fun showToast(title: String) {
11 | lifecycleScope.launch {
12 | Toast.makeText(this@BaseActivity, title, Toast.LENGTH_SHORT).show()
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.base
2 |
3 | import android.widget.Toast
4 | import androidx.fragment.app.Fragment
5 | import java.util.regex.Pattern
6 |
7 | abstract class BaseFragment : Fragment() {
8 | protected fun showToast(title: String) {
9 | Toast.makeText(requireContext(), title, Toast.LENGTH_SHORT).show()
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.base
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 |
7 | abstract class BaseViewModel : ViewModel() {
8 | private val _errorMessage = MutableLiveData()
9 | val errorMessage: LiveData
10 | get() = _errorMessage
11 |
12 | private val _loading = MutableLiveData()
13 | val loading: LiveData
14 | get() = _loading
15 |
16 | protected fun showError(message: String?) {
17 | message?.let {
18 | _errorMessage.postValue(it)
19 | }
20 | }
21 |
22 | protected fun showLoading() {
23 | _loading.postValue(true)
24 | }
25 |
26 | protected fun hideLoading() {
27 | _loading.postValue(false)
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/dialog/AvatarDialog.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.dialog
2 |
3 | import android.app.Dialog
4 | import android.content.Context
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.view.Window
8 | import com.quickblox.android_ui_kit.databinding.BaseDialogBinding
9 | import com.quickblox.android_ui_kit.presentation.dialogs.MenuItem
10 | import com.quickblox.android_ui_kit.presentation.theme.UiKitTheme
11 | import com.quickblox.qb_qmunicate.R
12 |
13 | class AvatarDialog(context: Context, private val listener: UserAvatarListener, private val themeDialog: UiKitTheme?) :
14 | Dialog(context, R.style.RoundedCornersDialog) {
15 | init {
16 | prepare()
17 | }
18 |
19 | private fun prepare() {
20 | val binding = BaseDialogBinding.inflate(layoutInflater)
21 | requestWindowFeature(Window.FEATURE_NO_TITLE)
22 | setContentView(binding.root)
23 |
24 | applyParams()
25 |
26 | themeDialog?.getMainTextColor()?.let {
27 | binding.tvTitle.setTextColor(it)
28 | }
29 |
30 | themeDialog?.getMainBackgroundColor()?.let {
31 | binding.root.setBackgroundColor(it)
32 | }
33 | binding.tvTitle.text = context.getString(R.string.change_photo)
34 |
35 | val views = collectViewsTemplateMethod()
36 | for (view in views) {
37 | view?.let {
38 | binding.llContainer.addView(view)
39 | }
40 | }
41 | }
42 |
43 | private fun collectViewsTemplateMethod(): List {
44 | val views = mutableListOf()
45 |
46 | val cameraItem = buildCameraItem()
47 | val galleryItem = buildGalleryItem()
48 | val removeItem = buildRemoveItem()
49 |
50 | views.add(cameraItem)
51 | views.add(galleryItem)
52 | views.add(removeItem)
53 |
54 | return views
55 | }
56 |
57 | private fun buildCameraItem(): View {
58 | val cameraItem = MenuItem(context)
59 |
60 | themeDialog?.getMainTextColor()?.let {
61 | cameraItem.setColorText(it)
62 | }
63 | themeDialog?.getMainElementsColor()?.let {
64 | cameraItem.setRipple(it)
65 | }
66 | cameraItem.setText(context.getString(R.string.take_photo_from_camera))
67 | cameraItem.setItemClickListener {
68 | dismiss()
69 | listener.onClickCamera()
70 | }
71 |
72 | return cameraItem
73 | }
74 |
75 | private fun buildGalleryItem(): View {
76 | val galleryItem = MenuItem(context)
77 |
78 | themeDialog?.getMainTextColor()?.let {
79 | galleryItem.setColorText(it)
80 | }
81 | themeDialog?.getMainElementsColor()?.let {
82 | galleryItem.setRipple(it)
83 | }
84 | galleryItem.setText(context.getString(R.string.open_gallery))
85 | galleryItem.setItemClickListener {
86 | dismiss()
87 | listener.onClickGallery()
88 | }
89 |
90 | return galleryItem
91 | }
92 |
93 | private fun buildRemoveItem(): View {
94 | val removeItem = MenuItem(context)
95 |
96 | themeDialog?.getMainTextColor()?.let {
97 | removeItem.setColorText(it)
98 | }
99 | themeDialog?.getMainElementsColor()?.let {
100 | removeItem.setRipple(it)
101 | }
102 | removeItem.setText(context.getString(R.string.remove_photo))
103 | removeItem.setItemClickListener {
104 | dismiss()
105 | listener.onClickRemove()
106 | }
107 |
108 | return removeItem
109 | }
110 |
111 | interface UserAvatarListener {
112 | fun onClickCamera()
113 | fun onClickGallery()
114 | fun onClickRemove()
115 | }
116 |
117 | private fun applyParams() {
118 | setCancelable(true)
119 | window?.setLayout(
120 | (context.resources.displayMetrics.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT
121 | )
122 | }
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.main
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.activity.OnBackPressedCallback
7 | import androidx.activity.viewModels
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.FragmentTransaction
10 | import com.google.android.material.bottomnavigation.BottomNavigationView
11 | import com.quickblox.android_ui_kit.QuickBloxUiKit
12 | import com.quickblox.android_ui_kit.presentation.screens.dialogs.DialogsScreenSettings
13 | import com.quickblox.qb_qmunicate.R
14 | import com.quickblox.qb_qmunicate.databinding.MainLayoutBinding
15 | import com.quickblox.qb_qmunicate.presentation.base.BaseActivity
16 | import com.quickblox.qb_qmunicate.presentation.profile.settings.SettingsFragment
17 | import com.quickblox.qb_qmunicate.presentation.theme_manager.ThemeManager
18 | import dagger.hilt.android.AndroidEntryPoint
19 |
20 | @AndroidEntryPoint
21 | class MainActivity : BaseActivity() {
22 | private lateinit var binding: MainLayoutBinding
23 |
24 | private var activeFragment: Fragment? = null
25 |
26 | private val viewModel by viewModels()
27 |
28 | companion object {
29 | fun show(context: Context) {
30 | val intent = Intent(context, MainActivity::class.java)
31 | context.startActivity(intent)
32 | }
33 | }
34 |
35 |
36 | override fun onCreate(savedInstanceState: Bundle?) {
37 | super.onCreate(savedInstanceState)
38 | binding = MainLayoutBinding.inflate(layoutInflater)
39 | setContentView(binding.root)
40 |
41 | val uiKitTheme = ThemeManager.checkModeAndGetUIKitTheme(this)
42 | val dialogsScreenSettings = DialogsScreenSettings.Builder(this).setTheme(uiKitTheme).build()
43 | dialogsScreenSettings.getHeaderComponent()?.setVisibleLeftButton(false)
44 |
45 | val dialogsFragment = QuickBloxUiKit.getScreenFactory().createDialogs(dialogsScreenSettings)
46 |
47 | val settingsFragment = SettingsFragment.newInstance()
48 |
49 | supportFragmentManager.beginTransaction().add(R.id.container, dialogsFragment, getString(R.string.dialog_tag))
50 | .add(R.id.container, settingsFragment, getString(R.string.settings_tag)).hide(settingsFragment).commitNow()
51 |
52 | activeFragment = dialogsFragment
53 |
54 | binding.bottomNavigation.setOnItemSelectedListener { item ->
55 | when (item.itemId) {
56 | R.id.dialogs_page -> {
57 | switchFragment(dialogsFragment, R.id.dialogs_page)
58 | true
59 | }
60 |
61 | R.id.settings_page -> {
62 | settingsFragment.updateUser()
63 | switchFragment(settingsFragment, R.id.settings_page)
64 | true
65 | }
66 |
67 | else -> false
68 | }
69 | }
70 |
71 | onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
72 | override fun handleOnBackPressed() {
73 | // empty
74 | }
75 | })
76 | }
77 |
78 | private fun switchFragment(fragment: Fragment, id: Int) {
79 | if (fragment !== activeFragment) {
80 | var transaction = supportFragmentManager.beginTransaction()
81 | transaction = setAnimation(transaction, id)
82 |
83 | transaction.hide(activeFragment!!)
84 | transaction.show(fragment)
85 | transaction.commitNow()
86 |
87 | activeFragment = fragment
88 | }
89 | }
90 |
91 | private fun setAnimation(transaction: FragmentTransaction, id: Int): FragmentTransaction {
92 | if (id == R.id.settings_page) {
93 | transaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left)
94 | } else if (id == R.id.dialogs_page) {
95 | transaction.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right)
96 | }
97 | return transaction
98 | }
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.main
2 |
3 | import androidx.lifecycle.ViewModel
4 |
5 | class MainViewModel : ViewModel()
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/profile/create/CreateProfileActivity.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.profile.create
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.pm.PackageManager
7 | import android.net.Uri
8 | import android.os.Bundle
9 | import android.view.View
10 | import androidx.activity.result.ActivityResultLauncher
11 | import androidx.activity.result.contract.ActivityResultContracts
12 | import androidx.activity.viewModels
13 | import androidx.core.content.ContextCompat
14 | import androidx.core.widget.doAfterTextChanged
15 | import androidx.lifecycle.lifecycleScope
16 | import com.quickblox.android_ui_kit.R
17 | import com.quickblox.android_ui_kit.presentation.checkStringByRegex
18 | import com.quickblox.android_ui_kit.presentation.dialogs.PositiveNegativeDialog
19 | import com.quickblox.android_ui_kit.presentation.makeClickableBackground
20 | import com.quickblox.android_ui_kit.presentation.screens.loadCircleImageFromUri
21 | import com.quickblox.android_ui_kit.presentation.screens.setOnClick
22 | import com.quickblox.qb_qmunicate.databinding.CreateProfileLayoutBinding
23 | import com.quickblox.qb_qmunicate.presentation.base.BaseActivity
24 | import com.quickblox.qb_qmunicate.presentation.dialog.AvatarDialog
25 | import com.quickblox.qb_qmunicate.presentation.main.MainActivity
26 | import com.quickblox.qb_qmunicate.presentation.theme_manager.ThemeManager
27 | import dagger.hilt.android.AndroidEntryPoint
28 | import kotlinx.coroutines.launch
29 |
30 | private const val CAMERA_PERMISSION = "android.permission.CAMERA"
31 | private const val REGEX_USER_NAME = "^(?=[a-zA-Z])[-a-zA-Z_ ]{3,49}(?? = registerCameraLauncher()
39 | private var galleryLauncher: ActivityResultLauncher? = registerGalleryLauncher()
40 | private val viewModel by viewModels()
41 |
42 | companion object {
43 | fun show(context: Context) {
44 | val intent = Intent(context, CreateProfileActivity::class.java)
45 | context.startActivity(intent)
46 | }
47 | }
48 |
49 | override fun onCreate(savedInstanceState: Bundle?) {
50 | super.onCreate(savedInstanceState)
51 | binding = CreateProfileLayoutBinding.inflate(layoutInflater)
52 | setContentView(binding.root)
53 |
54 | binding.btnFinish.makeClickableBackground(ContextCompat.getColor(this, R.color.primary))
55 | binding.tilName.isHintEnabled = false
56 |
57 | setListeners()
58 |
59 | subscribeToError()
60 | subscribeToUpdateUser()
61 | subscribeToAvatarLoading()
62 | }
63 |
64 | private fun setListeners() {
65 | binding.btnFinish.setOnClick {
66 | val name = binding.etName.text.toString()
67 | if (isNotUploadingAvatar()) {
68 | binding.finishProgressBar.visibility = View.VISIBLE
69 | viewModel.updateUser(name)
70 | return@setOnClick
71 | }
72 | }
73 |
74 | binding.etName.doAfterTextChanged {
75 | val enteredUserName = it.toString()
76 | val isValidName = enteredUserName.checkStringByRegex(REGEX_USER_NAME)
77 |
78 | enableFinishButton(isValidName)
79 | }
80 |
81 | binding.ivAvatar.setOnClickListener {
82 | avatarPressed()
83 | }
84 | }
85 |
86 | private fun enableFinishButton(isEnable: Boolean) {
87 | binding.btnFinish.isEnabled = isEnable
88 | }
89 |
90 | private fun isNotUploadingAvatar(): Boolean {
91 | val isLoading = viewModel.loading.value
92 | return if (isLoading == true) {
93 | showToast("Please wait for the avatar to load.")
94 | false
95 | } else {
96 | true
97 | }
98 | }
99 |
100 | private fun subscribeToUpdateUser() {
101 | viewModel.updateUser.observe(this) {
102 | binding.finishProgressBar.visibility = View.GONE
103 | showMainScreen()
104 | }
105 | }
106 |
107 | private fun showMainScreen() {
108 | MainActivity.show(this)
109 | }
110 |
111 | private fun avatarPressed() {
112 | val uiKitTheme = ThemeManager.checkModeAndGetUIKitTheme(this)
113 | AvatarDialog(this, AvatarListenerImpl(), uiKitTheme).show()
114 | }
115 |
116 | private fun requestCameraPermission() {
117 | requestPermissionLauncher.launch(CAMERA_PERMISSION)
118 | }
119 |
120 | private fun registerPermissionLauncher(): ActivityResultLauncher {
121 | return registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
122 | if (isGranted) {
123 | checkPermissionAndLaunchCamera()
124 | } else {
125 | showToast(getString(R.string.permission_denied))
126 | }
127 | }
128 | }
129 |
130 | private fun registerCameraLauncher(): ActivityResultLauncher {
131 | return registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
132 | if (success) {
133 | val uri = viewModel.getUri()
134 | val view = binding.ivAvatar
135 | view.loadCircleImageFromUri(uri, R.drawable.user_avatar_holder)
136 |
137 | uploadFileBy(uri)
138 | }
139 | }
140 | }
141 |
142 | private fun registerGalleryLauncher(): ActivityResultLauncher {
143 | return registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
144 | val view = binding.ivAvatar
145 | view.loadCircleImageFromUri(uri, R.drawable.user_avatar_holder)
146 |
147 | uploadFileBy(uri)
148 | }
149 | }
150 |
151 | private fun uploadFileBy(uri: Uri?) {
152 | lifecycleScope.launch {
153 | val file = uri?.let {
154 | viewModel.getFileBy(it)
155 | }
156 | viewModel.uploadFile(file)
157 | }
158 | }
159 |
160 | private fun checkPermissionRequest(): Boolean {
161 | val checkedCameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
162 | return checkedCameraPermission == PackageManager.PERMISSION_GRANTED
163 | }
164 |
165 | private fun checkPermissionAndLaunchCamera() {
166 | val isHasPermission = checkPermissionRequest()
167 | if (isHasPermission) {
168 | lifecycleScope.launch {
169 | val uri = viewModel.createFileAndGetUri()
170 | cameraLauncher?.launch(uri)
171 | }
172 | } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
173 | val contentText = getString(R.string.permission_alert_text)
174 | val positiveText = getString(R.string.yes)
175 | val negativeText = getString(R.string.no)
176 |
177 | val uiKitTheme = ThemeManager.checkModeAndGetUIKitTheme(this)
178 | PositiveNegativeDialog.show(this, contentText, positiveText, negativeText, uiKitTheme, positiveListener = {
179 | requestCameraPermission()
180 | }, negativeListener = {
181 | showToast(getString(R.string.permission_denied))
182 | })
183 | } else {
184 | requestCameraPermission()
185 | }
186 | }
187 |
188 | private fun subscribeToAvatarLoading() {
189 | viewModel.loading.observe(this) { isLoading ->
190 | if (isLoading) {
191 | binding.progressBar.visibility = View.VISIBLE
192 | } else {
193 | binding.progressBar.visibility = View.GONE
194 | }
195 | }
196 | }
197 |
198 | private fun subscribeToError() {
199 | viewModel.errorMessage.observe(this) { message ->
200 | showToast(message)
201 | binding.progressBar.visibility = View.GONE
202 | binding.finishProgressBar.visibility = View.GONE
203 | }
204 | }
205 |
206 | private inner class AvatarListenerImpl : AvatarDialog.UserAvatarListener {
207 | override fun onClickCamera() {
208 | checkPermissionAndLaunchCamera()
209 | }
210 |
211 | override fun onClickGallery() {
212 | val IMAGE_MIME = "image/*"
213 | galleryLauncher?.launch(IMAGE_MIME)
214 | }
215 |
216 | override fun onClickRemove() {
217 | val view = binding.ivAvatar
218 | view.setImageResource(R.drawable.user_avatar_holder)
219 | viewModel.clearAvatar()
220 | }
221 | }
222 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/profile/create/CreateProfileViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.profile.create
2 |
3 | import android.net.Uri
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.viewModelScope
7 | import com.quickblox.android_ui_kit.domain.entity.FileEntity
8 | import com.quickblox.android_ui_kit.domain.exception.DomainException
9 | import com.quickblox.android_ui_kit.domain.usecases.CreateLocalFileUseCase
10 | import com.quickblox.android_ui_kit.domain.usecases.GetLocalFileByUriUseCase
11 | import com.quickblox.android_ui_kit.domain.usecases.UploadFileUseCase
12 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
13 | import com.quickblox.qb_qmunicate.domain.use_case.user.GetUserUseCase
14 | import com.quickblox.qb_qmunicate.domain.use_case.user.UpdateUserUseCase
15 | import com.quickblox.qb_qmunicate.presentation.base.BaseViewModel
16 | import dagger.hilt.android.lifecycle.HiltViewModel
17 | import kotlinx.coroutines.launch
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | class CreateProfileViewModel @Inject constructor(
22 | private val updateUserUseCase: UpdateUserUseCase,
23 | private val getUserUseCase: GetUserUseCase,
24 | ) : BaseViewModel() {
25 | private var uri: Uri? = null
26 | private var user: UserEntity? = null
27 |
28 | init {
29 | loadUser()
30 | }
31 |
32 | private val _updateUser = MutableLiveData()
33 | val updateUser: LiveData
34 | get() = _updateUser
35 |
36 | private fun loadUser() {
37 | viewModelScope.launch {
38 | runCatching {
39 | user = getUserUseCase.execute(Unit)
40 | }.onFailure {
41 | showError(it.message.toString())
42 | }
43 | }
44 | }
45 |
46 | fun getUri(): Uri? {
47 | return uri
48 | }
49 |
50 | suspend fun createFileAndGetUri(): Uri? {
51 | try {
52 | val fileEntity = CreateLocalFileUseCase("jpg").execute()
53 | uri = fileEntity?.getUri()
54 | return uri
55 | } catch (exception: DomainException) {
56 | showError(exception.message)
57 | return null
58 | }
59 | }
60 |
61 | suspend fun getFileBy(uri: Uri): FileEntity? {
62 | try {
63 | val file = GetLocalFileByUriUseCase(uri).execute()
64 | return file
65 | } catch (exception: DomainException) {
66 | showError(exception.message)
67 | return null
68 | }
69 | }
70 |
71 | suspend fun uploadFile(fileEntity: FileEntity?) {
72 | if (fileEntity == null) {
73 | showError("The file doesn't exist")
74 | return
75 | }
76 |
77 | showLoading()
78 | try {
79 | val remoteEntity = UploadFileUseCase(fileEntity).execute()
80 | user?.avatarFileId = remoteEntity?.getId()
81 | hideLoading()
82 | } catch (exception: DomainException) {
83 | hideLoading()
84 | showError(exception.message)
85 | }
86 | }
87 |
88 | fun updateUser(name: String) {
89 | user?.fullName = name
90 | viewModelScope.launch {
91 | runCatching {
92 | user?.let {
93 | updateUserUseCase.execute(it)
94 | _updateUser.postValue(Unit)
95 | }
96 | }.onFailure {
97 | showError(it.message.toString())
98 | }
99 | }
100 | }
101 |
102 | fun clearAvatar() {
103 | user?.applyEmptyAvatar()
104 | }
105 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/profile/settings/SettingsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.profile.settings
2 |
3 | import android.Manifest.permission.CAMERA
4 | import android.content.pm.PackageManager
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import androidx.activity.result.ActivityResultLauncher
11 | import androidx.activity.result.contract.ActivityResultContracts
12 | import androidx.core.content.ContextCompat
13 | import androidx.core.widget.doAfterTextChanged
14 | import androidx.fragment.app.viewModels
15 | import androidx.lifecycle.lifecycleScope
16 | import com.quickblox.android_ui_kit.R
17 | import com.quickblox.android_ui_kit.presentation.checkStringByRegex
18 | import com.quickblox.android_ui_kit.presentation.dialogs.PositiveNegativeDialog
19 | import com.quickblox.android_ui_kit.presentation.makeClickableBackground
20 | import com.quickblox.android_ui_kit.presentation.screens.loadCircleImageFromUri
21 | import com.quickblox.android_ui_kit.presentation.screens.loadCircleImageFromUrl
22 | import com.quickblox.android_ui_kit.presentation.screens.setOnClick
23 | import com.quickblox.qb_qmunicate.databinding.SettingsLayoutBinding
24 | import com.quickblox.qb_qmunicate.presentation.base.BaseFragment
25 | import com.quickblox.qb_qmunicate.presentation.dialog.AvatarDialog
26 | import com.quickblox.qb_qmunicate.presentation.start.StartActivity
27 | import com.quickblox.qb_qmunicate.presentation.theme_manager.ThemeManager
28 | import dagger.hilt.android.AndroidEntryPoint
29 | import kotlinx.coroutines.launch
30 |
31 | private const val CAMERA_PERMISSION = "android.permission.CAMERA"
32 | private const val REGEX_USER_NAME = "^(?=[a-zA-Z])[-a-zA-Z_ ]{3,49}(?? = registerCameraLauncher()
46 | private var galleryLauncher: ActivityResultLauncher? = registerGalleryLauncher()
47 |
48 | private val viewModel by viewModels()
49 |
50 | private var binding: SettingsLayoutBinding? = null
51 |
52 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
53 | binding = SettingsLayoutBinding.inflate(inflater, container, false)
54 | return binding?.root
55 | }
56 |
57 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
58 | super.onViewCreated(view, savedInstanceState)
59 | binding?.btnSave?.makeClickableBackground(ContextCompat.getColor(requireContext(), R.color.primary))
60 | binding?.tvLogout?.makeClickableBackground(ContextCompat.getColor(requireContext(), R.color.primary))
61 |
62 | binding?.tilName?.isHintEnabled = false
63 | binding?.tilName?.isEndIconVisible = false
64 | setListeners()
65 |
66 | subscribeToError()
67 | subscribeToUpdateUser()
68 | subscribeToLoadUser()
69 | subscribeToSignOut()
70 | subscribeToAvatarLoading()
71 | }
72 |
73 | private fun setListeners() {
74 | binding?.btnSave?.setOnClick {
75 | val name = binding?.etName?.text.toString()
76 |
77 | if (isNotUploadingAvatar()) {
78 | binding?.saveProgressBar?.visibility = View.VISIBLE
79 | viewModel.updateUser(name)
80 | binding?.tilName?.isEndIconVisible = false
81 | return@setOnClick
82 | }
83 | }
84 |
85 | binding?.tvLogout?.setOnClick {
86 | binding?.saveProgressBar?.visibility = View.VISIBLE
87 | viewModel.signOut()
88 | }
89 |
90 | binding?.etName?.doAfterTextChanged {
91 | val enteredUserName = it.toString()
92 | val isValidName = enteredUserName.checkStringByRegex(REGEX_USER_NAME)
93 |
94 | enableSaveButton(isValidName)
95 | }
96 |
97 | binding?.ivAvatar?.setOnClickListener {
98 | avatarPressed()
99 | }
100 | }
101 |
102 | private fun enableSaveButton(isEnable: Boolean) {
103 | binding?.btnSave?.isEnabled = isEnable
104 | }
105 |
106 | private fun isNotUploadingAvatar(): Boolean {
107 | val isLoading = viewModel.loading.value
108 | return if (isLoading == true) {
109 | showToast(getString(com.quickblox.qb_qmunicate.R.string.please_wait_for_the_avatar_to_load))
110 | false
111 | } else {
112 | true
113 | }
114 | }
115 |
116 | private fun subscribeToUpdateUser() {
117 | viewModel.updateUser.observe(viewLifecycleOwner) {
118 | binding?.saveProgressBar?.visibility = View.GONE
119 | showToast(getString(com.quickblox.qb_qmunicate.R.string.user_updated))
120 | }
121 | }
122 |
123 | private fun subscribeToLoadUser() {
124 | viewModel.loadUser.observe(viewLifecycleOwner) { user ->
125 | val isValidAvatarUrl = !user?.avatarFileUrl.isNullOrBlank()
126 | val view = binding?.ivAvatar
127 | if (isValidAvatarUrl) {
128 | view?.loadCircleImageFromUrl(user?.avatarFileUrl, R.drawable.user_avatar_holder)
129 | } else {
130 | view?.setImageResource(R.drawable.user_avatar_holder)
131 | }
132 |
133 | val userName = user?.fullName
134 | binding?.etName?.setText(userName ?: "")
135 |
136 | val isValidName = userName.toString().checkStringByRegex(REGEX_USER_NAME)
137 | enableSaveButton(isValidName)
138 | }
139 | }
140 |
141 | private fun subscribeToSignOut() {
142 | viewModel.signOut.observe(viewLifecycleOwner) { user ->
143 | binding?.saveProgressBar?.visibility = View.GONE
144 | StartActivity.show(requireContext())
145 | }
146 | }
147 |
148 |
149 | private fun avatarPressed() {
150 | val uiKitTheme = ThemeManager.checkModeAndGetUIKitTheme(requireContext())
151 | AvatarDialog(requireContext(), AvatarListenerImpl(), uiKitTheme).show()
152 | }
153 |
154 | private fun requestCameraPermission() {
155 | requestPermissionLauncher.launch(CAMERA_PERMISSION)
156 | }
157 |
158 | private fun registerPermissionLauncher(): ActivityResultLauncher {
159 | return registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
160 | if (isGranted) {
161 | checkPermissionAndLaunchCamera()
162 | } else {
163 | showToast(getString(R.string.permission_denied))
164 | }
165 | }
166 | }
167 |
168 | private fun registerCameraLauncher(): ActivityResultLauncher {
169 | return registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
170 | if (success) {
171 | val uri = viewModel.getUri()
172 | val view = binding?.ivAvatar
173 | view?.loadCircleImageFromUri(uri, R.drawable.user_avatar_holder)
174 |
175 | uploadFileBy(uri)
176 | }
177 | }
178 | }
179 |
180 | private fun registerGalleryLauncher(): ActivityResultLauncher {
181 | return registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
182 | val view = binding?.ivAvatar
183 | view?.loadCircleImageFromUri(uri, R.drawable.user_avatar_holder)
184 |
185 | uploadFileBy(uri)
186 | }
187 | }
188 |
189 | private fun uploadFileBy(uri: Uri?) {
190 | lifecycleScope.launch {
191 | val file = uri?.let {
192 | viewModel.getFileBy(it)
193 | }
194 | viewModel.uploadFile(file)
195 | }
196 | }
197 |
198 | private fun checkPermissionRequest(): Boolean {
199 | val checkedCameraPermission = ContextCompat.checkSelfPermission(requireContext(), CAMERA)
200 | return checkedCameraPermission == PackageManager.PERMISSION_GRANTED
201 | }
202 |
203 | private fun checkPermissionAndLaunchCamera() {
204 | val isHasPermission = checkPermissionRequest()
205 | if (isHasPermission) {
206 | lifecycleScope.launch {
207 | val uri = viewModel.createFileAndGetUri()
208 | cameraLauncher?.launch(uri)
209 | }
210 | } else if (shouldShowRequestPermissionRationale(CAMERA)) {
211 | val contentText = getString(R.string.permission_alert_text)
212 | val positiveText = getString(R.string.yes)
213 | val negativeText = getString(R.string.no)
214 |
215 | val uiKitTheme = ThemeManager.checkModeAndGetUIKitTheme(requireContext())
216 | PositiveNegativeDialog.show(requireContext(),
217 | contentText,
218 | positiveText,
219 | negativeText,
220 | uiKitTheme,
221 | positiveListener = {
222 | requestCameraPermission()
223 | },
224 | negativeListener = {
225 | showToast(getString(R.string.permission_denied))
226 | })
227 | } else {
228 | requestCameraPermission()
229 | }
230 | }
231 |
232 | private fun subscribeToAvatarLoading() {
233 | viewModel.loading.observe(viewLifecycleOwner) { isLoading ->
234 | if (isLoading) {
235 | binding?.progressBar?.visibility = View.VISIBLE
236 | } else {
237 | binding?.progressBar?.visibility = View.GONE
238 | }
239 | }
240 | }
241 |
242 | private fun subscribeToError() {
243 | viewModel.errorMessage.observe(viewLifecycleOwner) { message ->
244 | showToast(message)
245 | binding?.progressBar?.visibility = View.GONE
246 | binding?.saveProgressBar?.visibility = View.GONE
247 |
248 | }
249 | }
250 |
251 | private inner class AvatarListenerImpl : AvatarDialog.UserAvatarListener {
252 | override fun onClickCamera() {
253 | checkPermissionAndLaunchCamera()
254 | }
255 |
256 | override fun onClickGallery() {
257 | val IMAGE_MIME = "image/*"
258 | galleryLauncher?.launch(IMAGE_MIME)
259 | }
260 |
261 | override fun onClickRemove() {
262 | val view = binding?.ivAvatar
263 | view?.setImageResource(R.drawable.user_avatar_holder)
264 | viewModel.clearAvatar()
265 | }
266 | }
267 |
268 | override fun onDestroyView() {
269 | binding = null
270 | super.onDestroyView()
271 | }
272 |
273 | fun updateUser() {
274 | viewModel.loadUser()
275 | }
276 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/profile/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.profile.settings
2 |
3 | import android.net.Uri
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.viewModelScope
7 | import com.quickblox.android_ui_kit.domain.entity.FileEntity
8 | import com.quickblox.android_ui_kit.domain.exception.DomainException
9 | import com.quickblox.android_ui_kit.domain.usecases.CreateLocalFileUseCase
10 | import com.quickblox.android_ui_kit.domain.usecases.GetLocalFileByUriUseCase
11 | import com.quickblox.android_ui_kit.domain.usecases.UploadFileUseCase
12 | import com.quickblox.qb_qmunicate.domain.entity.UserEntity
13 | import com.quickblox.qb_qmunicate.domain.use_case.user.GetUserUseCase
14 | import com.quickblox.qb_qmunicate.domain.use_case.user.SignOutUserUseCase
15 | import com.quickblox.qb_qmunicate.domain.use_case.user.UpdateUserUseCase
16 | import com.quickblox.qb_qmunicate.presentation.base.BaseViewModel
17 | import dagger.hilt.android.lifecycle.HiltViewModel
18 | import kotlinx.coroutines.launch
19 | import javax.inject.Inject
20 |
21 | @HiltViewModel
22 | class SettingsViewModel @Inject constructor(
23 | private val updateUserUseCase: UpdateUserUseCase,
24 | private val getUserUseCase: GetUserUseCase,
25 | private val signOutUserUseCase: SignOutUserUseCase,
26 | ) : BaseViewModel() {
27 | private var avatarFileId: Int? = null
28 | private var uri: Uri? = null
29 | private var user: UserEntity? = null
30 |
31 | init {
32 | loadUser()
33 | }
34 |
35 | private val _updateUser = MutableLiveData()
36 | val updateUser: LiveData
37 | get() = _updateUser
38 |
39 | private val _loadUser = MutableLiveData()
40 | val loadUser: LiveData
41 | get() = _loadUser
42 |
43 | private val _signOut = MutableLiveData()
44 | val signOut: LiveData
45 | get() = _signOut
46 |
47 |
48 | internal fun loadUser() {
49 | user = null
50 | avatarFileId = null
51 | uri = null
52 |
53 | viewModelScope.launch {
54 | runCatching {
55 | user = getUserUseCase.execute(Unit)
56 | avatarFileId = user?.avatarFileId
57 |
58 | _loadUser.postValue(user)
59 | }.onFailure {
60 | showError(it.message.toString())
61 | }
62 | }
63 | }
64 |
65 | fun getUri(): Uri? {
66 | return uri
67 | }
68 |
69 | fun signOut() {
70 | viewModelScope.launch {
71 | runCatching {
72 | signOutUserUseCase.execute(Unit)
73 | _signOut.postValue(Unit)
74 | }.onFailure {
75 | showError(it.message.toString())
76 | }
77 | }
78 | }
79 |
80 | suspend fun createFileAndGetUri(): Uri? {
81 | try {
82 | val fileEntity = CreateLocalFileUseCase("jpg").execute()
83 | uri = fileEntity?.getUri()
84 | return uri
85 | } catch (exception: DomainException) {
86 | showError(exception.message)
87 | return null
88 | }
89 | }
90 |
91 | suspend fun getFileBy(uri: Uri): FileEntity? {
92 | try {
93 | val file = GetLocalFileByUriUseCase(uri).execute()
94 | return file
95 | } catch (exception: DomainException) {
96 | showError(exception.message)
97 | return null
98 | }
99 | }
100 |
101 | suspend fun uploadFile(fileEntity: FileEntity?) {
102 | if (fileEntity == null) {
103 | showError("The file doesn't exist")
104 | return
105 | }
106 |
107 | showLoading()
108 | try {
109 | val remoteEntity = UploadFileUseCase(fileEntity).execute()
110 | avatarFileId = remoteEntity?.getId()
111 | hideLoading()
112 | } catch (exception: DomainException) {
113 | hideLoading()
114 | showError(exception.message)
115 | }
116 | }
117 |
118 | fun updateUser(name: String) {
119 | val isNotExistChanges = !isExistChanges(user, name, avatarFileId)
120 | if (isNotExistChanges) {
121 | showError("User data has not been changed")
122 | return
123 | }
124 |
125 | user?.fullName = name
126 | avatarFileId?.let {
127 | user?.avatarFileId = avatarFileId
128 | }
129 |
130 | viewModelScope.launch {
131 | runCatching {
132 | user?.let {
133 | updateUserUseCase.execute(it)
134 | _updateUser.postValue(Unit)
135 | }
136 | }.onFailure {
137 | showError(it.message.toString())
138 | }
139 | }
140 | }
141 |
142 | private fun isExistChanges(userEntity: UserEntity?, name: String, avatarFileId: Int?): Boolean {
143 | return userEntity?.fullName != name || userEntity.avatarFileId != avatarFileId
144 | }
145 |
146 | fun clearAvatar() {
147 | avatarFileId = null
148 | uri = null
149 | user?.applyEmptyAvatar()
150 | }
151 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/start/StartActivity.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.start
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import androidx.activity.result.contract.ActivityResultContracts
8 | import androidx.activity.viewModels
9 | import androidx.lifecycle.lifecycleScope
10 | import com.firebase.ui.auth.AuthUI
11 | import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract
12 | import com.firebase.ui.auth.data.model.FirebaseAuthUIAuthenticationResult
13 | import com.google.android.play.core.appupdate.AppUpdateInfo
14 | import com.google.android.play.core.appupdate.AppUpdateManager
15 | import com.google.android.play.core.appupdate.AppUpdateManagerFactory
16 | import com.google.android.play.core.appupdate.AppUpdateOptions
17 | import com.google.android.play.core.install.model.AppUpdateType
18 | import com.google.android.play.core.ktx.isImmediateUpdateAllowed
19 | import com.quickblox.android_ui_kit.QuickBloxUiKit
20 | import com.quickblox.android_ui_kit.presentation.dialogs.PositiveNegativeDialog
21 | import com.quickblox.qb_qmunicate.BuildConfig
22 | import com.quickblox.qb_qmunicate.BuildConfig.PRIVACY_POLICY_URL
23 | import com.quickblox.qb_qmunicate.BuildConfig.QB_AI_PROXY_SERVER_URL
24 | import com.quickblox.qb_qmunicate.BuildConfig.QB_OPEN_AI_TOKEN
25 | import com.quickblox.qb_qmunicate.BuildConfig.TOS_URL
26 | import com.quickblox.qb_qmunicate.R
27 | import com.quickblox.qb_qmunicate.databinding.StartLayoutBinding
28 | import com.quickblox.qb_qmunicate.presentation.base.BaseActivity
29 | import com.quickblox.qb_qmunicate.presentation.main.MainActivity
30 | import com.quickblox.qb_qmunicate.presentation.profile.create.CreateProfileActivity
31 | import com.quickblox.qb_qmunicate.presentation.theme_manager.ThemeManager
32 | import dagger.hilt.android.AndroidEntryPoint
33 | import kotlinx.coroutines.Dispatchers
34 | import kotlinx.coroutines.launch
35 | import kotlinx.coroutines.tasks.await
36 |
37 | @AndroidEntryPoint
38 | class StartActivity : BaseActivity() {
39 | companion object {
40 | fun show(context: Context) {
41 | val intent = Intent(context, StartActivity::class.java)
42 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
43 | context.startActivity(intent)
44 | }
45 | }
46 |
47 | private lateinit var binding: StartLayoutBinding
48 | private val viewModel by viewModels()
49 |
50 | private val signInLauncher =
51 | registerForActivityResult(FirebaseAuthUIActivityResultContract()) { result: FirebaseAuthUIAuthenticationResult? ->
52 | if (result?.resultCode == Activity.RESULT_OK) {
53 | viewModel.signInAndShowProfileOrUIKit()
54 | } else {
55 | showRegistrationOrExitDialog()
56 | }
57 | }
58 |
59 | private fun showRegistrationOrExitDialog() {
60 | val contentText = getString(R.string.do_you_want_to_continue_authorization)
61 | val positiveText = getString(com.quickblox.android_ui_kit.R.string.yes)
62 | val negativeText = getString(com.quickblox.android_ui_kit.R.string.no)
63 |
64 | val uiKitTheme = ThemeManager.checkModeAndGetUIKitTheme(this)
65 | PositiveNegativeDialog.show(this, contentText, positiveText, negativeText, uiKitTheme, positiveListener = {
66 | signInLauncher.launch(getSignInIntent())
67 | }, negativeListener = {
68 | finish()
69 | }, false)
70 | }
71 |
72 | override fun onCreate(savedInstanceState: Bundle?) {
73 | super.onCreate(savedInstanceState)
74 | binding = StartLayoutBinding.inflate(layoutInflater)
75 | setContentView(binding.root)
76 |
77 | window.statusBarColor = getColor(R.color.main_color)
78 |
79 | subscribeToError()
80 | subscribeToShowUiKit()
81 | subscribeToFirebase()
82 | subscribeToProfile()
83 |
84 | updateAppVersionText()
85 | checkAndInstallUpdate()
86 | }
87 |
88 | private fun subscribeToShowUiKit() {
89 | viewModel.showUiKit.observe(this) {
90 | initUIKit()
91 | MainActivity.show(this@StartActivity)
92 | }
93 | }
94 |
95 | private fun subscribeToFirebase() {
96 | viewModel.showFirebase.observe(this) {
97 | signInLauncher.launch(getSignInIntent())
98 | }
99 | }
100 |
101 | private fun subscribeToProfile() {
102 | viewModel.showProfile.observe(this) {
103 | initUIKit()
104 | CreateProfileActivity.show(this@StartActivity)
105 | }
106 | }
107 |
108 | private fun getSignInIntent(): Intent {
109 | val selectedProviders: MutableList = ArrayList()
110 | selectedProviders.add(AuthUI.IdpConfig.PhoneBuilder().build())
111 | val builder: AuthUI.SignInIntentBuilder =
112 | AuthUI.getInstance().createSignInIntentBuilder().setAvailableProviders(selectedProviders)
113 | .setTheme(R.style.AuthFbTheme).setTosAndPrivacyPolicyUrls(TOS_URL, PRIVACY_POLICY_URL)
114 |
115 | return builder.build()
116 | }
117 |
118 | private fun subscribeToError() {
119 | viewModel.errorMessage.observe(this) { message ->
120 | showToast(message)
121 | }
122 | }
123 |
124 | private fun initUIKit() {
125 | initAiUiKit()
126 |
127 | QuickBloxUiKit.init(applicationContext)
128 |
129 | val REGEX_USER_NAME = "^(?=[a-zA-Z])[-a-zA-Z_ ]{3,49}(?
166 | showToast(getString(R.string.error_app_update_config))
167 | }
168 | return null
169 | }
170 |
171 | private fun updateApp(appUpdateManager: AppUpdateManager, appUpdateInfo: AppUpdateInfo) {
172 | val appUpdateOptions = AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
173 | appUpdateManager.startUpdateFlowForResult(appUpdateInfo, appUpdateResultLauncher, appUpdateOptions)
174 | }
175 |
176 | private val appUpdateResultLauncher =
177 | registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
178 | updateAppVersionText()
179 | viewModel.checkUserExistAndNotify()
180 | }
181 |
182 | private fun updateAppVersionText() {
183 | val versionCode = "(${BuildConfig.VERSION_NAME + "." + BuildConfig.VERSION_CODE})"
184 | binding.tvVersion.text = getString(R.string.powered_by_quickblox, versionCode)
185 | }
186 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/quickblox/qb_qmunicate/presentation/start/StartViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate.presentation.start
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.viewModelScope
6 | import com.quickblox.qb_qmunicate.domain.use_case.user.CheckUserExistUseCase
7 | import com.quickblox.qb_qmunicate.domain.use_case.user.SignInUserUseCase
8 | import com.quickblox.qb_qmunicate.presentation.base.BaseViewModel
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.launch
11 | import java.util.regex.Pattern
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | class StartViewModel @Inject constructor(
16 | private val signInUserUseCase: SignInUserUseCase,
17 | private val checkUserExistUseCase: CheckUserExistUseCase,
18 | ) : BaseViewModel() {
19 | private val _showUiKit = MutableLiveData()
20 | val showUiKit: LiveData
21 | get() = _showUiKit
22 |
23 | private val _showFirebase = MutableLiveData()
24 | val showFirebase: LiveData
25 | get() = _showFirebase
26 |
27 | private val _showProfile = MutableLiveData()
28 | val showProfile: LiveData
29 | get() = _showProfile
30 |
31 | fun checkUserExistAndNotify() {
32 | viewModelScope.launch {
33 | val isUserExist = isUserExist()
34 | if (isUserExist) {
35 | signInAndShowProfileOrUIKit()
36 | } else {
37 | _showFirebase.postValue(Unit)
38 | }
39 | }
40 | }
41 |
42 | fun signInAndShowProfileOrUIKit() {
43 | viewModelScope.launch {
44 | runCatching {
45 | val user = signInUserUseCase.execute(Unit)
46 |
47 | val userName = user?.fullName
48 |
49 | val regex = Pattern.compile("^(?=[a-zA-Z])[-a-zA-Z_ ]{3,49}(? true
21 | else -> false
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/color/box_stroke_states.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/color/navigation_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/color/text_color_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/clear_text_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/dialogs.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/qmunicate_logo.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
10 |
13 |
16 |
19 |
22 |
25 |
28 |
31 |
34 |
37 |
40 |
44 |
45 |
50 |
51 |
52 |
53 |
54 |
55 |
58 |
59 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/create_profile_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
23 |
24 |
39 |
40 |
46 |
47 |
60 |
61 |
74 |
75 |
87 |
88 |
95 |
96 |
97 |
106 |
107 |
119 |
120 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/main_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
21 |
22 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/settings_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
22 |
23 |
37 |
38 |
44 |
45 |
58 |
59 |
72 |
73 |
85 |
86 |
94 |
95 |
96 |
105 |
106 |
118 |
119 |
132 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/start_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_navigation_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuickBlox/q-municate-android/d1653fd3b8b10966be6a9147ae51bcc02a0e11bf/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuickBlox/q-municate-android/d1653fd3b8b10966be6a9147ae51bcc02a0e11bf/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuickBlox/q-municate-android/d1653fd3b8b10966be6a9147ae51bcc02a0e11bf/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuickBlox/q-municate-android/d1653fd3b8b10966be6a9147ae51bcc02a0e11bf/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuickBlox/q-municate-android/d1653fd3b8b10966be6a9147ae51bcc02a0e11bf/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
29 |
30 |
37 |
38 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3978FC
4 | #3978FC
5 | #FFFFFF
6 | #636D78
7 |
8 | #74A1FD
9 | #202F3E
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Q-municate
3 | Powered by QuickBlox %s
4 | Enter phone number
5 | Get code
6 | Finish
7 | Create profile
8 | Enter your name
9 | Min 3 symbols
10 | Settings
11 | Dialogs
12 | Log out
13 | Save
14 | Take photo from camera
15 | Open gallery
16 | Remove photo
17 | Please wait for the avatar to load.
18 | User updated
19 | dialog_tag
20 | settings_tag
21 | Change photo
22 | Do you want to continue authorization?
23 | The in app update has wrong configuration
24 | Start with a letter, use only a–z, A–Z, hyphens, underscores, and spaces. Length: 3–50 characters.
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
29 |
30 |
36 |
37 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/quickblox/qb_qmunicate/DaoMapperUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate
2 |
3 | import com.quickblox.qb_qmunicate.data.repository.db.DaoMapper
4 | import com.quickblox.qb_qmunicate.data.repository.db.UserDbModel
5 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
6 | import org.junit.Assert.assertEquals
7 | import org.junit.Assert.assertNull
8 | import org.junit.Test
9 | import kotlin.random.Random
10 |
11 | class DaoMapperUnitTest {
12 | @Test
13 | fun userDBModel_mapUserDbModelToUserEntity_loginAndFullNameAreSame() {
14 | val userDbModel = UserDbModel(
15 | id = Random.nextInt(),
16 | login = "login",
17 | fullName = "fullName",
18 | avatarFileId = Random.nextInt(),
19 | avatarFileUrl = "avatar_file_url"
20 | )
21 | val userEntity = DaoMapper.mapUserDbModelToUserEntity(userDbModel)
22 | assertEquals(userDbModel.login, userEntity?.login)
23 | assertEquals(userDbModel.fullName, userEntity?.fullName)
24 | assertEquals(userDbModel.id, userEntity?.id)
25 | }
26 |
27 | @Test
28 | fun userDBModelIsNull_mapUserDbModelToUserEntity_userEntityIsNull() {
29 | val userEntity = DaoMapper.mapUserDbModelToUserEntity(null)
30 | assertNull(userEntity)
31 | }
32 |
33 | @Test
34 | fun userDBModelFullNameIsNull_mapUserDbModelToUserEntity_loginAndFullNameAreSame() {
35 | val userDbModel = UserDbModel(
36 | id = Random.nextInt(),
37 | login = "login",
38 | fullName = null,
39 | avatarFileId = Random.nextInt(),
40 | avatarFileUrl = "avatar_file_url"
41 | )
42 | val userEntity = DaoMapper.mapUserDbModelToUserEntity(userDbModel)
43 | assertEquals(userDbModel.login, userEntity?.login)
44 | assertEquals(userDbModel.fullName, userEntity?.fullName)
45 | assertEquals(userDbModel.id, userEntity?.id)
46 | }
47 |
48 | @Test
49 | fun userEntity_mapUserEntityToUserDbModel_loginAndFullNameAreSame() {
50 | val userEntity = UserEntityImpl(
51 | id = Random.nextInt(),
52 | login = "login",
53 | fullName = "fullName",
54 | avatarFileId = Random.nextInt(),
55 | avatarFileUrl = "avatar_file_url"
56 | )
57 | val userDbModel = DaoMapper.mapUserEntityToUserDbModel(userEntity)
58 | assertEquals(userEntity.login, userDbModel?.login)
59 | assertEquals(userEntity.fullName, userDbModel?.fullName)
60 | assertEquals(userEntity.id, userDbModel?.id)
61 | }
62 |
63 | @Test
64 | fun userEntityIsNull_mapUserDbModelToUserEntity_userDbModelIsNull() {
65 | val userDbModel = DaoMapper.mapUserEntityToUserDbModel(null)
66 | assertNull(userDbModel)
67 | }
68 |
69 | @Test
70 | fun userEntityFullNameIsNull_mapUserDbModelToUserEntity_loginAndFullNameAreSame() {
71 | val userEntity = UserEntityImpl(
72 | id = Random.nextInt(),
73 | login = "login",
74 | fullName = null,
75 | avatarFileId = Random.nextInt(),
76 | avatarFileUrl = "avatar_file_url"
77 | )
78 | val userDbModel = DaoMapper.mapUserEntityToUserDbModel(userEntity)
79 | assertEquals(userEntity.login, userDbModel?.login)
80 | assertEquals(userEntity.fullName, userDbModel?.fullName)
81 | assertEquals(userEntity.id, userDbModel?.id)
82 | }
83 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/quickblox/qb_qmunicate/FirebaseMapperUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate
2 |
3 | import com.quickblox.qb_qmunicate.data.repository.firebase.FirebaseMapper
4 | import com.quickblox.users.model.QBUser
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Assert.assertNull
7 | import org.junit.Test
8 | import kotlin.random.Random
9 |
10 | class FirebaseMapperUnitTest {
11 | @Test
12 | fun qbUser_mapQBUserToUserEntity_loginAndFullNameAreSame() {
13 | val qbUser = QBUser().apply {
14 | id = Random.nextInt()
15 | login = "login"
16 | fullName = "fullName"
17 | }
18 | val userEntity = FirebaseMapper.mapQBUserToUserEntity(qbUser)
19 | assertEquals(qbUser.login, userEntity?.login)
20 | assertEquals(qbUser.fullName, userEntity?.fullName)
21 | assertEquals(qbUser.id, userEntity?.id)
22 | }
23 |
24 | @Test
25 | fun qbUserIsNull_mapQBUserToUserEntity_userEntityIsNull() {
26 | val userEntity = FirebaseMapper.mapQBUserToUserEntity(null)
27 | assertNull(userEntity)
28 | }
29 |
30 | @Test
31 | fun qbUserFullNameIsNull_mapUserDbModelToUserEntity_loginAndFullNameAreSame() {
32 | val qbUser = QBUser().apply {
33 | id = Random.nextInt()
34 | login = "login"
35 | fullName = null
36 | }
37 | val userEntity = FirebaseMapper.mapQBUserToUserEntity(qbUser)
38 | assertEquals(qbUser.login, userEntity?.login)
39 | assertEquals(qbUser.fullName, userEntity?.fullName)
40 | assertEquals(qbUser.id, userEntity?.id)
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/quickblox/qb_qmunicate/QuickBloxMapperUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.quickblox.qb_qmunicate
2 |
3 | import com.quickblox.qb_qmunicate.data.repository.quickblox.QuickBloxMapper
4 | import com.quickblox.qb_qmunicate.domain.entity.UserEntityImpl
5 | import com.quickblox.users.model.QBUser
6 | import org.junit.Assert.assertEquals
7 | import org.junit.Assert.assertNull
8 | import org.junit.Test
9 | import kotlin.random.Random
10 |
11 | class QuickBloxMapperUnitTest {
12 | @Test
13 | fun qbUser_mapQBUserToUserEntity_loginAndFullNameAreSame() {
14 | val qbUser = QBUser().apply {
15 | id = Random.nextInt()
16 | login = "login"
17 | fullName = "fullName"
18 | }
19 | val userEntity = QuickBloxMapper.mapQBUserToUserEntity(qbUser)
20 | assertEquals(qbUser.login, userEntity?.login)
21 | assertEquals(qbUser.fullName, userEntity?.fullName)
22 | assertEquals(qbUser.id, userEntity?.id)
23 | }
24 |
25 | @Test
26 | fun qbUserIsNull_mapQBUserToUserEntity_userEntityIsNull() {
27 | val userEntity = QuickBloxMapper.mapQBUserToUserEntity(null)
28 | assertNull(userEntity)
29 | }
30 |
31 | @Test
32 | fun qbUserFullNameIsNull_mapUserDbModelToUserEntity_loginAndFullNameAreSame() {
33 | val qbUser = QBUser().apply {
34 | id = Random.nextInt()
35 | login = "login"
36 | fullName = null
37 | }
38 | val userEntity = QuickBloxMapper.mapQBUserToUserEntity(qbUser)
39 | assertEquals(qbUser.login, userEntity?.login)
40 | assertEquals(qbUser.fullName, userEntity?.fullName)
41 | assertEquals(qbUser.id, userEntity?.id)
42 | }
43 |
44 | @Test
45 | fun userEntity_mapUserEntityToQBUser_loginAndFullNameAreSame() {
46 | val userEntity = UserEntityImpl(
47 | id = Random.nextInt(),
48 | login = "login",
49 | fullName = "fullName",
50 | avatarFileId = Random.nextInt(),
51 | avatarFileUrl = "avatar_file_url"
52 | )
53 | val qbUser = QuickBloxMapper.mapUserEntityToQBUser(userEntity)
54 | assertEquals(userEntity.login, qbUser?.login)
55 | assertEquals(userEntity.fullName, qbUser?.fullName)
56 | assertEquals(userEntity.id, qbUser?.id)
57 | }
58 |
59 | @Test
60 | fun userEntityIsNull_mapUserEntityToQBUser_qbUserIsNull() {
61 | val qbUser = QuickBloxMapper.mapUserEntityToQBUser(null)
62 | assertNull(qbUser)
63 | }
64 |
65 | @Test
66 | fun userEntityFullNameIsNull_mapUserEntityToQBUser_loginAndFullNameAreSame() {
67 | val userEntity = UserEntityImpl(
68 | id = Random.nextInt(),
69 | login = "login",
70 | fullName = null,
71 | avatarFileId = Random.nextInt(),
72 | avatarFileUrl = "avatar_file_url"
73 | )
74 | val qbUser = QuickBloxMapper.mapUserEntityToQBUser(userEntity)
75 | assertEquals(userEntity.login, qbUser?.login)
76 | assertEquals(userEntity.fullName, qbUser?.fullName)
77 | assertEquals(userEntity.id, qbUser?.id)
78 | }
79 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id("com.android.application") version "8.2.0" apply false
4 | id("org.jetbrains.kotlin.android") version "1.9.0" apply false
5 | id("com.google.gms.google-services") version "4.4.0" apply false
6 | id("com.google.dagger.hilt.android") version "2.49" apply false
7 | id("com.google.devtools.ksp") version "1.9.21-1.0.15" apply false
8 | id("org.jetbrains.kotlin.jvm") version "1.9.0" apply false
9 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | android.enableJetifier=true
19 | # Kotlin code style for this project: "official" or "obsolete":
20 | kotlin.code.style=official
21 | # Enables namespacing of each library's R class so that its R class includes only the
22 | # resources declared in the library itself and none from the library's dependencies,
23 | # thereby reducing the size of the R class for that library
24 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuickBlox/q-municate-android/d1653fd3b8b10966be6a9147ae51bcc02a0e11bf/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Dec 05 17:07:34 EET 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/screenshots/firebase-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuickBlox/q-municate-android/d1653fd3b8b10966be6a9147ae51bcc02a0e11bf/screenshots/firebase-settings.png
--------------------------------------------------------------------------------
/screenshots/google-json.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuickBlox/q-municate-android/d1653fd3b8b10966be6a9147ae51bcc02a0e11bf/screenshots/google-json.png
--------------------------------------------------------------------------------
/screenshots/run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuickBlox/q-municate-android/d1653fd3b8b10966be6a9147ae51bcc02a0e11bf/screenshots/run.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven("https://github.com/QuickBlox/quickblox-android-sdk-releases/raw/master/")
14 | maven("https://github.com/QuickBlox/android-ai-releases/raw/main")
15 | maven("https://github.com/QuickBlox/android-ui-kit-releases/raw/master/")
16 | }
17 | }
18 |
19 | rootProject.name = "q_municate"
20 | include(":app")
21 |
--------------------------------------------------------------------------------