├── .DS_Store
├── .gitignore
├── .idea
├── caches
│ └── build_file_checksums.ser
├── checkstyle-idea.xml
├── codeStyles
│ └── Project.xml
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── dbnavigator.xml
├── misc.xml
├── modules.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── frangsierra
│ │ └── kotlinfirechat
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── frangsierra
│ │ │ └── kotlinfirechat
│ │ │ ├── chat
│ │ │ ├── controller
│ │ │ │ └── MessagesController.kt
│ │ │ ├── model
│ │ │ │ └── Models.kt
│ │ │ └── store
│ │ │ │ ├── ChatActions.kt
│ │ │ │ ├── ChatState.kt
│ │ │ │ └── ChatStore.kt
│ │ │ ├── core
│ │ │ ├── SplashActivity.kt
│ │ │ ├── dagger
│ │ │ │ ├── AppComponent.kt
│ │ │ │ ├── DaggerUtils.kt
│ │ │ │ └── Scopes.kt
│ │ │ ├── errors
│ │ │ │ ├── CrashlyticsHandler.kt
│ │ │ │ └── ErrorHandlingModule.kt
│ │ │ ├── firebase
│ │ │ │ ├── FirebaseConstants.kt
│ │ │ │ ├── FirebaseModels.kt
│ │ │ │ ├── FirebaseModule.kt
│ │ │ │ └── FirebaseRefs.kt
│ │ │ ├── flux
│ │ │ │ ├── App.kt
│ │ │ │ ├── Async.kt
│ │ │ │ ├── CustomLoggerInterceptor.kt
│ │ │ │ ├── FluxActivity.kt
│ │ │ │ └── FluxFragment.kt
│ │ │ └── service
│ │ │ │ └── DataUploadService.kt
│ │ │ ├── home
│ │ │ ├── HomeActivity.kt
│ │ │ └── MessageAdapter.kt
│ │ │ ├── profile
│ │ │ ├── controller
│ │ │ │ └── ProfileController.kt
│ │ │ ├── model
│ │ │ │ └── Models.kt
│ │ │ └── store
│ │ │ │ ├── ProfileActions.kt
│ │ │ │ ├── ProfileState.kt
│ │ │ │ └── ProfileStore.kt
│ │ │ ├── session
│ │ │ ├── CreateAccountActivity.kt
│ │ │ ├── LoginActivity.kt
│ │ │ ├── VerifyEmailActivity.kt
│ │ │ ├── controller
│ │ │ │ └── SessionController.kt
│ │ │ ├── model
│ │ │ │ ├── LoginProvider.kt
│ │ │ │ ├── SessionExceptions.kt
│ │ │ │ └── User.kt
│ │ │ └── store
│ │ │ │ ├── SessionActions.kt
│ │ │ │ ├── SessionState.kt
│ │ │ │ └── SessionStore.kt
│ │ │ └── util
│ │ │ ├── AlertDialogExtensions.kt
│ │ │ ├── AndroidUtils.kt
│ │ │ ├── GlideExtensions.kt
│ │ │ ├── GoogleLoginCallback.kt
│ │ │ ├── GoogleSignInApiUtils.kt
│ │ │ ├── ImageUtils.kt
│ │ │ ├── Prefs.kt
│ │ │ ├── ProgressDialogFragment.kt
│ │ │ ├── RequestState.kt
│ │ │ ├── RxUtils.kt
│ │ │ └── UtilExtensions.kt
│ └── res
│ │ ├── drawable
│ │ ├── firebase.png
│ │ └── firebase_icon.png
│ │ ├── layout
│ │ ├── create_account_activity.xml
│ │ ├── home_activity.xml
│ │ ├── item_message.xml
│ │ ├── login_activity.xml
│ │ ├── progress_dialog_layout.xml
│ │ └── verification_email_activity.xml
│ │ ├── menu
│ │ └── chat_menu.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── provider_paths.xml
│ └── test
│ └── java
│ └── frangsierra
│ └── kotlinfirechat
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # Intellij
36 | *.iml
37 | .idea/workspace.xml
38 | .idea/tasks.xml
39 | .idea/gradle.xml
40 | .idea/dictionaries
41 | .idea/libraries
42 |
43 | # Keystore files
44 | *.jks
45 |
46 | # External native build folder generated in Android Studio 2.2 and later
47 | .externalNativeBuild
48 |
49 | # Google Services (e.g. APIs or Firebase)
50 | google-services.json
51 |
52 | # Freeline
53 | freeline.py
54 | freeline/
55 | freeline_project_description.json
56 |
--------------------------------------------------------------------------------
/.idea/caches/build_file_checksums.ser:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/.idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/.idea/checkstyle-idea.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/dbnavigator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
16 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 1.7
38 |
39 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KotlinFirechat
2 | This project is a basic chat application using Flux Architecture together with [Rx Java 2.0](https://github.com/ReactiveX/RxJava/tree/2.x) and [Dagger2](https://google.github.io/dagger/).
3 |
4 | ## Firebase Android Series
5 | The repository works as a sample for Firebase Android Series. A series of articles published on medium that shows how to build an Android application with Kotlin and Firebase from scratch. Each chapter of the series comes together with a branch of this repository:
6 | * [Introduction](https://proandroiddev.com/firebase-android-series-learning-firebase-from-zero-to-hero-3bacbdf8e048)
7 | * [Firebase Authentication](https://proandroiddev.com/firebase-android-series-authentication-74f209c59738) -> [Sample](https://github.com/FrangSierra/KotlinFirechat/tree/01-Auth)
8 | * [Firebase Firestore](https://proandroiddev.com/firebase-android-series-firestore-17e8951c574e) -> [Sample](https://github.com/FrangSierra/KotlinFirechat/tree/02-Database)
9 | * [Crashlytics](https://proandroiddev.com/firebase-android-series-crashlytics-29de3f507d6) -> [Sample](https://github.com/FrangSierra/KotlinFirechat/tree/03-Crashlytics)
10 | * **Firebase Test Lab** -> Coming soon
11 | * **Firebase Storage** -> Coming soon
12 | * **Firebase Cloud Functions** -> Coming soon
13 | * **Firebase Cloud Messaging** -> Coming soon
14 | * **Firebase Dynamic Links** -> Coming soon
15 | * **Firebase Performance** -> Coming soon
16 | * **Firebase Analytics** -> Coming soon
17 |
18 |
19 | ## Flux Architecure
20 | Flux is an Architecture which works pretty well with Firebase(It does aswell with Redux), it allows you to keep all the data in cache in a really easy mode, together with data persistence of Firebase it becomes a really strongh way of develop applications.
21 |
22 | 
23 | Graph by [Luis G. Valle](http://lgvalle.xyz/)
24 |
25 | ## Further Reading:
26 | * [Facebook Flux Overview](https://facebook.github.io/flux/docs/overview.html)
27 | * [Flux & Android](http://armueller.github.io/android/2015/03/29/flux-and-android.html)
28 | * [Testing Flux Applications](https://facebook.github.io/flux/docs/testing-flux-applications.html#content)
29 | * [Flux Step by Step](http://blogs.atlassian.com/2014/08/flux-architecture-step-by-step/)
30 | * [What's different in Rx 2.0](https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0)
31 | * [Dependency Injection with Dagger 2](https://guides.codepath.com/android/Dependency-Injection-with-Dagger-2)
32 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea/
4 | /local.properties
5 | /.idea/workspace.xml
6 | /.idea/libraries
7 | .DS_Store
8 | /build
9 | /captures
10 | .externalNativeBuild
11 |
12 |
13 | # Created by https://www.gitignore.io/api/java,gradle,intellij,osx,windows,linux
14 |
15 | ### Intellij ###
16 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
17 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
18 |
19 | # User-specific stuff:
20 | .idea/**/workspace.xml
21 | .idea/**/tasks.xml
22 |
23 | # Sensitive or high-churn files:
24 | .idea/**/dataSources/
25 | .idea/**/dataSources.ids
26 | .idea/**/dataSources.xml
27 | .idea/**/dataSources.local.xml
28 | .idea/**/sqlDataSources.xml
29 | .idea/**/dynamic.xml
30 | .idea/**/uiDesigner.xml
31 |
32 | # Gradle:
33 | .idea/**/gradle.xml
34 | .idea/**/libraries
35 |
36 | # Mongo Explorer plugin:
37 | .idea/**/mongoSettings.xml
38 |
39 | ## File-based project format:
40 | *.iws
41 |
42 | ## Plugin-specific files:
43 |
44 | # IntelliJ
45 | /out/
46 |
47 | # mpeltonen/sbt-idea plugin
48 | .idea_modules/
49 |
50 | # JIRA plugin
51 | atlassian-ide-plugin.xml
52 |
53 | # Crashlytics plugin (for Android Studio and IntelliJ)
54 | com_crashlytics_export_strings.xml
55 | crashlytics.properties
56 | crashlytics-build.properties
57 | fabric.properties
58 |
59 | ### Intellij Patch ###
60 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
61 |
62 | # *.iml
63 | # modules.xml
64 | # .idea/misc.xml
65 | # *.ipr
66 |
67 | ### Java ###
68 | # Compiled class file
69 | *.class
70 |
71 | # Log file
72 | *.message
73 |
74 | # BlueJ files
75 | *.ctxt
76 |
77 | # Mobile Tools for Java (J2ME)
78 | .mtj.tmp/
79 |
80 | # Package Files #
81 | *.jar
82 | *.war
83 | *.ear
84 | *.zip
85 | *.tar.gz
86 | *.rar
87 |
88 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
89 | hs_err_pid*
90 |
91 | ### Linux ###
92 | *~
93 |
94 | # temporary files which can be created if a process still has a handle open of a deleted file
95 | .fuse_hidden*
96 |
97 | # KDE directory preferences
98 | .directory
99 |
100 | # Linux trash folder which might appear on any partition or disk
101 | .Trash-*
102 |
103 | # .nfs files are created when an open file is removed but is still being accessed
104 | .nfs*
105 |
106 | ### OSX ###
107 | *.DS_Store
108 | .AppleDouble
109 | .LSOverride
110 |
111 | # Icon must end with two \r
112 | Icon
113 |
114 |
115 | # Thumbnails
116 | ._*
117 |
118 | # Files that might appear in the root of a volume
119 | .DocumentRevisions-V100
120 | .fseventsd
121 | .Spotlight-V100
122 | .TemporaryItems
123 | .Trashes
124 | .VolumeIcon.icns
125 | .com.apple.timemachine.donotpresent
126 |
127 | # Directories potentially created on remote AFP share
128 | .AppleDB
129 | .AppleDesktop
130 | Network Trash Folder
131 | Temporary Items
132 | .apdisk
133 |
134 | ### Windows ###
135 | # Windows thumbnail cache files
136 | Thumbs.db
137 | ehthumbs.db
138 | ehthumbs_vista.db
139 |
140 | # Folder config file
141 | Desktop.ini
142 |
143 | # Recycle Bin used on file shares
144 | $RECYCLE.BIN/
145 |
146 | # Windows Installer files
147 | *.cab
148 | *.msi
149 | *.msm
150 | *.msp
151 |
152 | # Windows shortcuts
153 | *.lnk
154 |
155 | ### Gradle ###
156 | .gradle
157 | /build/
158 |
159 | # Ignore Gradle GUI config
160 | gradle-app.setting
161 |
162 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
163 | !gradle-wrapper.jar
164 |
165 | # Cache of project
166 | .gradletasknamecache
167 |
168 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
169 | # gradle/wrapper/gradle-wrapper.properties
170 |
171 | # End of https://www.gitignore.io/api/java,gradle,intellij,osx,windows,linux
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | gson_version = "2.8.2"
4 | dagger_version = "2.15"
5 | rx_version = "2.1.8"
6 | glide_version = "4.7.1"
7 | rx_firebase_version = "1.1.3"
8 | support_version = "27.1.1"
9 | rx_android_version = "2.0.2"
10 | glide_version = "4.7.1"
11 | target_sdk_version = 27
12 | }
13 | }
14 |
15 | apply plugin: 'com.android.application'
16 | apply plugin: 'io.fabric'
17 | apply plugin: 'kotlin-android'
18 | apply plugin: 'kotlin-kapt'
19 | apply plugin: 'kotlin-android-extensions'
20 |
21 | android {
22 | compileSdkVersion target_sdk_version
23 | buildToolsVersion "27.0.3"
24 | defaultConfig {
25 | applicationId "frangsierra.kotlinfirechat"
26 | minSdkVersion 21
27 | targetSdkVersion target_sdk_version
28 | versionCode 1
29 | versionName "1.0"
30 | multiDexEnabled true
31 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
32 | }
33 |
34 | packagingOptions {
35 | exclude 'META-INF/main.kotlin_module'
36 | }
37 |
38 | buildTypes {
39 | release {
40 | minifyEnabled false
41 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
42 | }
43 | }
44 | sourceSets {
45 | main.java.srcDirs += 'src/main/kotlin'
46 | }
47 |
48 | compileOptions {
49 | sourceCompatibility "1.7"
50 | targetCompatibility "1.7"
51 | }
52 |
53 | lintOptions {
54 | abortOnError false
55 | }
56 |
57 | configurations.all {
58 | //This is library is included with two different versions
59 | resolutionStrategy.force 'com.google.code.findbugs:jsr305:3.0.1'
60 | }
61 | }
62 |
63 | dependencies {
64 | implementation fileTree(dir: 'libs', include: ['*.jar'])
65 | //Kotlin
66 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
67 | implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
68 |
69 | //Support
70 | implementation "com.android.support:appcompat-v7:$support_version"
71 | implementation "com.android.support:design:$support_version"
72 | implementation 'com.android.support.constraint:constraint-layout:1.1.2'
73 |
74 | //Firebase
75 | implementation "com.google.firebase:firebase-firestore:17.0.4"
76 | implementation 'com.google.firebase:firebase-storage:16.0.1'
77 | implementation "com.google.firebase:firebase-auth:16.0.2"
78 | implementation "com.google.firebase:firebase-core:16.0.1"
79 | implementation "com.google.android.gms:play-services-auth:15.0.1"
80 | implementation 'com.crashlytics.sdk.android:crashlytics:2.9.4'
81 | implementation "com.firebase:firebase-jobdispatcher:0.8.5"
82 |
83 | //Mini
84 | implementation 'com.github.pabloogc:Mini:1.0.5'
85 | annotationProcessor 'com.github.pabloogc.Mini:mini-processor:1.0.5'
86 |
87 | //Glide
88 | implementation "com.github.bumptech.glide:glide:$glide_version"
89 | kapt "com.github.bumptech.glide:compiler:$glide_version"
90 |
91 | //Dagger
92 | implementation "com.google.dagger:dagger-android:$dagger_version"
93 | implementation "com.google.dagger:dagger-android-support:$dagger_version"
94 | kapt "com.google.dagger:dagger-compiler:$dagger_version"
95 | kapt "com.google.dagger:dagger-android-processor:$dagger_version"
96 |
97 | //Rx
98 | implementation "io.reactivex.rxjava2:rxjava:$rx_version"
99 | implementation "io.reactivex.rxjava2:rxandroid:$rx_android_version"
100 | implementation 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.5@aar'
101 |
102 | //Test
103 | testImplementation 'junit:junit:4.12'
104 | testImplementation 'com.natpryce:hamkrest:1.4.2.0'
105 | testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
106 | testImplementation 'org.junit.platform:junit-platform-runner:1.0.1'
107 | }
108 |
109 | repositories {
110 | mavenCentral()
111 | maven { url "http://dl.bintray.com/jetbrains/spek" }
112 | }
113 |
114 | apply plugin: 'com.google.gms.google-services'
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Users\Durdin\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/frangsierra/kotlinfirechat/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("frangsierra.kotlinfirechat", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
33 |
36 |
39 |
40 |
45 |
48 |
49 |
50 |
51 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/chat/controller/MessagesController.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.chat.controller
2 |
3 | import android.net.Uri
4 | import com.firebase.jobdispatcher.FirebaseJobDispatcher
5 | import com.google.android.gms.tasks.Tasks
6 | import com.google.firebase.firestore.FirebaseFirestore
7 | import com.google.firebase.firestore.QuerySnapshot
8 | import frangsierra.kotlinfirechat.chat.store.ListeningChatMessagesCompleteAction
9 | import frangsierra.kotlinfirechat.chat.store.MessagesLoadedAction
10 | import frangsierra.kotlinfirechat.chat.store.SendMessageCompleteAction
11 | import frangsierra.kotlinfirechat.core.dagger.AppScope
12 | import frangsierra.kotlinfirechat.core.firebase.*
13 | import frangsierra.kotlinfirechat.core.firebase.FirebaseConstants.TOTAL_MESSAGES
14 | import frangsierra.kotlinfirechat.core.flux.doAsync
15 | import frangsierra.kotlinfirechat.core.service.buildUploadJob
16 | import frangsierra.kotlinfirechat.profile.model.PublicProfile
17 | import frangsierra.kotlinfirechat.util.taskFailure
18 | import frangsierra.kotlinfirechat.util.taskSuccess
19 | import io.reactivex.BackpressureStrategy
20 | import io.reactivex.Flowable
21 | import io.reactivex.schedulers.Schedulers
22 | import mini.Dispatcher
23 | import javax.inject.Inject
24 |
25 | interface ChatController {
26 | fun startListeningMessages()
27 | fun sendMessage(message: String, imageUri: Uri?, publicProfile: PublicProfile)
28 | }
29 |
30 | @AppScope
31 | class ChatControllerImpl @Inject constructor(private val firestore: FirebaseFirestore,
32 | private val dispatcher: Dispatcher,
33 | private val firebaseJobDispatcher: FirebaseJobDispatcher) : ChatController {
34 | override fun startListeningMessages() {
35 | val disposable = listenMessagesFlowable()
36 | .map { it.documents.map { it.toMessage() } }
37 | .subscribeOn(Schedulers.io())
38 | .subscribe { dispatcher.dispatchOnUi(MessagesLoadedAction(it)) }
39 | dispatcher.dispatchOnUi(ListeningChatMessagesCompleteAction(disposable))
40 | }
41 |
42 | override fun sendMessage(message: String, imageUri: Uri?, publicProfile: PublicProfile) {
43 | doAsync {
44 | val newId = firestore.messages().document().id
45 | val firebaseMessage = FirebaseMessage(publicProfile.userData.toFirebaseUserData(), message)
46 |
47 | if (imageUri != null) {
48 | val uploadJob = buildUploadJob(imageUri.toString(), publicProfile.userData.uid, newId, firebaseJobDispatcher.newJobBuilder())
49 | firebaseJobDispatcher.mustSchedule(uploadJob)
50 | }
51 |
52 | try {
53 | val batch = firestore.batch()
54 | batch.set(firestore.messageDoc(newId), firebaseMessage)
55 | batch.update(firestore.publicProfileDoc(publicProfile.userData.uid),
56 | mapOf(TOTAL_MESSAGES to publicProfile.totalMessages.plus(1)))
57 | Tasks.await(batch.commit())
58 |
59 | dispatcher.dispatchOnUi(SendMessageCompleteAction(firebaseMessage.toMessage(newId), taskSuccess()))
60 | } catch (e: Throwable) {
61 | dispatcher.dispatchOnUi(SendMessageCompleteAction(null, taskFailure(e)))
62 | }
63 | }
64 | }
65 |
66 | private fun listenMessagesFlowable(): Flowable {
67 | return Flowable.create({ emitter ->
68 | val registration = firestore.messages().addSnapshotListener { documentSnapshot, e ->
69 | if (e != null && !emitter.isCancelled) {
70 | emitter.onError(e)
71 | } else if (documentSnapshot != null) {
72 | emitter.onNext(documentSnapshot)
73 | }
74 | }
75 | emitter.setCancellable { registration.remove() }
76 | }, BackpressureStrategy.BUFFER)
77 | }
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/chat/model/Models.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.chat.model
2 |
3 | import com.google.firebase.firestore.ServerTimestamp
4 | import frangsierra.kotlinfirechat.profile.model.UserData
5 | import java.util.*
6 |
7 | data class Message(val uid: String = "",
8 | val author: UserData,
9 | val message: String,
10 | val attachedImageUrl: String?,
11 | @ServerTimestamp val timestamp: Date)
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/chat/store/ChatActions.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.chat.store
2 |
3 | import android.net.Uri
4 | import frangsierra.kotlinfirechat.chat.model.Message
5 | import frangsierra.kotlinfirechat.util.Task
6 | import io.reactivex.disposables.Disposable
7 | import mini.Action
8 |
9 | class StartListeningChatMessagesAction : Action
10 |
11 | data class ListeningChatMessagesCompleteAction(val disposable: Disposable) : Action
12 |
13 | class MessagesLoadedAction(val messages: List) : Action
14 |
15 | class StopListeningChatMessagesAction : Action
16 |
17 | data class SendMessageAction(val message: String, val attachedImageUri : Uri? = null) : Action
18 |
19 | data class SendMessageCompleteAction(val message : Message?, val task: Task) : Action
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/chat/store/ChatState.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.chat.store
2 |
3 | import frangsierra.kotlinfirechat.chat.model.Message
4 | import frangsierra.kotlinfirechat.util.Task
5 | import io.reactivex.disposables.CompositeDisposable
6 |
7 | data class ChatState(val messages: Map = emptyMap(),
8 | val sendMessageTask : Task = Task(),
9 | val disposables: CompositeDisposable = CompositeDisposable())
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/chat/store/ChatStore.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.chat.store
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.multibindings.ClassKey
6 | import dagger.multibindings.IntoMap
7 | import frangsierra.kotlinfirechat.chat.controller.ChatController
8 | import frangsierra.kotlinfirechat.chat.controller.ChatControllerImpl
9 | import frangsierra.kotlinfirechat.core.dagger.AppScope
10 | import frangsierra.kotlinfirechat.profile.store.ProfileStore
11 | import frangsierra.kotlinfirechat.session.store.SignOutAction
12 | import frangsierra.kotlinfirechat.util.taskRunning
13 | import io.reactivex.disposables.CompositeDisposable
14 | import mini.Reducer
15 | import mini.Store
16 | import javax.inject.Inject
17 |
18 | @AppScope
19 | class ChatStore @Inject constructor(val controller: ChatController, val profileStore: ProfileStore) : Store() {
20 |
21 | @Reducer
22 | fun loadMessages(action: StartListeningChatMessagesAction): ChatState {
23 | controller.startListeningMessages()
24 | return state
25 | }
26 |
27 | @Reducer
28 | fun messagesReceived(action: MessagesLoadedAction): ChatState {
29 | return state.copy(messages = state.messages.plus(action.messages.map { it.uid to it }.toMap()))
30 | }
31 |
32 | @Reducer
33 | fun sendMessage(action: SendMessageAction): ChatState {
34 | controller.sendMessage(action.message, action.attachedImageUri, profileStore.state.publicProfile!!)
35 | return state.copy(sendMessageTask = taskRunning())
36 | }
37 |
38 | @Reducer
39 | fun messageSent(action: SendMessageCompleteAction): ChatState {
40 | return state.copy(sendMessageTask = action.task,
41 | messages = if (action.task.isSuccessful()) state.messages
42 | .plus(action.message!!.uid to action.message) else state.messages)
43 | }
44 |
45 | @Reducer
46 | fun stopListeningMessages(action: StopListeningChatMessagesAction): ChatState {
47 | state.disposables.dispose()
48 | return state.copy(disposables = CompositeDisposable())
49 | }
50 |
51 | @Reducer
52 | fun signOut(action: SignOutAction): ChatState {
53 | return initialState()
54 | }
55 | }
56 |
57 | @Module
58 | abstract class ChatModule {
59 | @Binds
60 | @AppScope
61 | @IntoMap
62 | @ClassKey(ChatStore::class)
63 | abstract fun provideChatStore(store: ChatStore): Store<*>
64 |
65 | @Binds
66 | @AppScope
67 | abstract fun bindChatController(impl: ChatControllerImpl): ChatController
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import com.google.android.gms.common.ConnectionResult
6 | import com.google.android.gms.common.GoogleApiAvailability
7 | import frangsierra.kotlinfirechat.core.flux.FluxActivity
8 | import frangsierra.kotlinfirechat.home.HomeActivity
9 | import frangsierra.kotlinfirechat.session.LoginActivity
10 | import frangsierra.kotlinfirechat.session.store.SessionStore
11 | import frangsierra.kotlinfirechat.session.store.TryToLoginInFirstInstanceAction
12 | import frangsierra.kotlinfirechat.util.filterOne
13 | import frangsierra.kotlinfirechat.util.toast
14 | import javax.inject.Inject
15 |
16 | class SplashActivity : FluxActivity() {
17 |
18 | @Inject
19 | lateinit var sessionStore: SessionStore
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 |
24 | if (checkPlayServices()) {
25 | dispatcher.dispatch(TryToLoginInFirstInstanceAction())
26 | sessionStore.flowable()
27 | .filterOne { it.loginTask.isTerminal() }
28 | .subscribe { status ->
29 | if (status.loginTask.isSuccessful()) goToHome()
30 | else goToOnLogin()
31 | }.track()
32 | }
33 | }
34 |
35 | private fun goToHome() {
36 | val intent = HomeActivity.newIntent(this)
37 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
38 | startActivity(intent)
39 | }
40 |
41 | private fun goToOnLogin() {
42 | val intent = LoginActivity.newIntent(this)
43 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
44 | startActivity(intent)
45 | }
46 |
47 | /**
48 | * Check the device to make sure it has the proper Google Play Services version.
49 | * If it doesn't, display a dialog that allows users to download it from
50 | * Google Play or enable it in the device's system settings.
51 | */
52 | private fun checkPlayServices(): Boolean {
53 | val apiAvailability = GoogleApiAvailability.getInstance()
54 | val resultCode = apiAvailability.isGooglePlayServicesAvailable(this)
55 | if (resultCode != ConnectionResult.SUCCESS) {
56 | if (apiAvailability.isUserResolvableError(resultCode)) {
57 | apiAvailability.getErrorDialog(this, resultCode, 666).show()
58 | } else {
59 | toast("Google play is not supported in this device")
60 | finish()
61 | }
62 | return false
63 | }
64 | return true
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/dagger/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.dagger
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import dagger.Component
6 | import dagger.Module
7 | import dagger.Provides
8 | import frangsierra.kotlinfirechat.chat.store.ChatModule
9 | import frangsierra.kotlinfirechat.core.SplashActivity
10 | import frangsierra.kotlinfirechat.core.errors.ErrorHandlingModule
11 | import frangsierra.kotlinfirechat.core.firebase.FirebaseModule
12 | import frangsierra.kotlinfirechat.core.flux.App
13 | import frangsierra.kotlinfirechat.core.flux.FluxActivity
14 | import frangsierra.kotlinfirechat.home.HomeActivity
15 | import frangsierra.kotlinfirechat.profile.store.ProfileModule
16 | import frangsierra.kotlinfirechat.session.CreateAccountActivity
17 | import frangsierra.kotlinfirechat.session.EmailVerificationActivity
18 | import frangsierra.kotlinfirechat.session.LoginActivity
19 | import frangsierra.kotlinfirechat.session.store.SessionModule
20 | import mini.Dispatcher
21 | import mini.StoreMap
22 |
23 | interface AppComponent {
24 | fun dispatcher(): Dispatcher
25 | fun stores(): StoreMap
26 | }
27 |
28 | @AppScope
29 | @Component(modules = [(AppModule::class),
30 | (FirebaseModule::class),
31 | (ChatModule::class),
32 | (ErrorHandlingModule::class),
33 | (ProfileModule::class),
34 | (SessionModule::class)])
35 |
36 | interface DefaultAppComponent : AppComponent {
37 | fun inject(target: SplashActivity)
38 | fun inject(target: FluxActivity)
39 | fun inject(target: LoginActivity)
40 | fun inject(target: CreateAccountActivity)
41 | fun inject(target: EmailVerificationActivity)
42 | fun inject(target: HomeActivity)
43 | }
44 |
45 | @Module
46 | class AppModule(val app: App) {
47 | @Provides
48 | @AppScope
49 | fun provideDispatcher() = Dispatcher()
50 |
51 | @Provides
52 | fun provideApplication(): Application = app
53 |
54 | @Provides
55 | fun provideAppContext(): Context = app
56 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/dagger/DaggerUtils.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.dagger
2 |
3 | /**
4 | * Look for an injection method in the component and invoke it.
5 | */
6 | fun inject(component: Any, target: Any) {
7 | val javaClass = target.javaClass
8 | try {
9 | val injectMethod = component.javaClass.getMethod("inject", javaClass)
10 | injectMethod.invoke(component, target)
11 | } catch (e: NoSuchMethodException) {
12 | throw UnsupportedOperationException(
13 | """No injection point for $javaClass in: ${component.javaClass}.
14 | Expected a method in the component with signature:
15 | fun inject($javaClass)""".trimMargin())
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/dagger/Scopes.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.dagger
2 |
3 | import javax.inject.Scope
4 |
5 | @Scope
6 | @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
7 | annotation class AppScope
8 |
9 | @Scope
10 | @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
11 | annotation class ActivityScope
12 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/errors/CrashlyticsHandler.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.errors
2 |
3 | import com.crashlytics.android.Crashlytics
4 |
5 | class CrashlyticsHandler(val defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler) : Thread.UncaughtExceptionHandler {
6 |
7 | val runtime by lazy { Runtime.getRuntime() }
8 |
9 | override fun uncaughtException(thread: Thread?, ex: Throwable?) {
10 | // Our custom logic goes here. For example calculate the memory heap
11 | val maxMemory = runtime.maxMemory()
12 | val freeMemory = runtime.freeMemory()
13 | val usedMemory = runtime.totalMemory() - freeMemory
14 | val availableMemory = maxMemory - usedMemory
15 |
16 | //Set values to Crashlytics
17 | Crashlytics.setLong("used_memory", usedMemory)
18 | Crashlytics.setLong("available_memory", availableMemory)
19 |
20 | // This will make Crashlytics do its job
21 | defaultUncaughtExceptionHandler.uncaughtException(thread, ex)
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/errors/ErrorHandlingModule.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.errors
2 |
3 | import android.content.Context
4 | import com.crashlytics.android.Crashlytics
5 | import com.google.android.gms.auth.api.signin.GoogleSignInStatusCodes
6 | import com.google.android.gms.common.api.ApiException
7 | import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
8 | import com.google.firebase.auth.FirebaseAuthInvalidUserException
9 | import com.google.firebase.auth.FirebaseAuthUserCollisionException
10 | import com.google.firebase.auth.FirebaseAuthWeakPasswordException
11 | import com.google.firebase.firestore.FirebaseFirestoreException
12 | import dagger.Binds
13 | import dagger.Module
14 | import frangsierra.kotlinfirechat.R
15 | import frangsierra.kotlinfirechat.core.dagger.AppScope
16 | import frangsierra.kotlinfirechat.session.model.FirebaseUserNotFound
17 | import mini.Dispatcher
18 | import mini.Grove
19 | import java.net.UnknownHostException
20 | import java.util.concurrent.TimeoutException
21 | import javax.inject.Inject
22 | import javax.net.ssl.SSLPeerUnverifiedException
23 |
24 | @Module
25 | @Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction")
26 | interface ErrorHandlingModule {
27 | @Binds
28 | @AppScope
29 | fun provideErrorHandler(errorHandler: DefaultErrorHandler): ErrorHandler
30 | }
31 |
32 | /**
33 | * A basic application generated error with a message.
34 | */
35 | class GenericError(message: String?, cause: Throwable?) : Exception(message, cause)
36 |
37 | /**
38 | * Interface that exposes methods to handle app errors.
39 | */
40 | interface ErrorHandler {
41 | /** Generate an user friendly message for known errors. */
42 | fun getMessageForError(e: Throwable?): String
43 |
44 | /** Handle the error, may result in a new activity being launched. */
45 | fun handle(e: Throwable?)
46 |
47 | /**
48 | * Unwrap an error into the underlying http code for manual handling,
49 | * or null if the error is not http related
50 | * */
51 | fun unwrapCode(e: Throwable?): Int?
52 | }
53 |
54 | /**
55 | * Class that implements the main handler of the application. It maps errors and exceptions to strings
56 | * or starts a new activity.
57 | */
58 | class DefaultErrorHandler @Inject constructor(private val context: Context,
59 | private val dispatcher: Dispatcher) : ErrorHandler {
60 |
61 | @Suppress("UndocumentedPublicFunction")
62 | override fun getMessageForError(e: Throwable?): String {
63 | return when (e) {
64 | is GenericError -> e.message ?: context.getString(R.string.error_unknown)
65 | is UnknownHostException -> context.getString(R.string.error_no_connection)
66 | is SSLPeerUnverifiedException -> context.getString(R.string.error_invalid_certificate)
67 | is TimeoutException -> context.getString(R.string.error_weak_password)
68 | is FirebaseAuthWeakPasswordException -> context.getString(R.string.error_weak_password)
69 | is FirebaseAuthInvalidCredentialsException -> context.getString(R.string.error_invalid_password)
70 | is FirebaseAuthUserCollisionException -> context.getString(R.string.error_email_already_exist)
71 | is FirebaseAuthInvalidUserException -> context.getString(R.string.error_invalid_account)
72 | is FirebaseUserNotFound -> context.getString(R.string.error_invalid_account)
73 | is ApiException -> {
74 | "${context.getString(R.string.error_google)} ${GoogleSignInStatusCodes.getStatusCodeString(e.statusCode)}"
75 | }
76 | is FirebaseFirestoreException -> retrieveFirebaseErrorMessage(e)
77 | else -> {
78 | Grove.e { "Unexpected error: $e" }
79 | context.getString(R.string.error_unexpected)
80 | }
81 | }
82 | }
83 |
84 | @Suppress("UndocumentedPublicFunction")
85 | override fun handle(e: Throwable?) {
86 | val exception = e as? Exception ?: Exception(e)
87 | Crashlytics.logException(exception)
88 | val errorCode = unwrapCode(e)
89 | if (errorCode == 401) {
90 | // Unauthorized
91 | }
92 | }
93 |
94 | @Suppress("UndocumentedPublicFunction")
95 | override fun unwrapCode(e: Throwable?): Int? {
96 | return null
97 | }
98 |
99 | private fun retrieveFirebaseErrorMessage(error: FirebaseFirestoreException): String {
100 | when (error.code) {
101 | //OK -> TODO()
102 | //CANCELLED -> TODO()
103 | //UNKNOWN -> TODO()
104 | //INVALID_ARGUMENT -> TODO()
105 | //DEADLINE_EXCEEDED -> TODO()
106 | //NOT_FOUND -> TODO()
107 | //ALREADY_EXISTS -> TODO()
108 | //PERMISSION_DENIED -> TODO()
109 | //RESOURCE_EXHAUSTED -> TODO()
110 | //FAILED_PRECONDITION -> TODO()
111 | //ABORTED -> TODO()
112 | //OUT_OF_RANGE -> TODO()
113 | //UNIMPLEMENTED -> TODO()
114 | //INTERNAL -> TODO()
115 | //UNAVAILABLE -> TODO()
116 | //DATA_LOSS -> TODO()
117 | //UNAUTHENTICATED -> TODO()
118 | }
119 | return context.getString(R.string.error_unknown)
120 | }
121 | }
122 |
123 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/firebase/FirebaseConstants.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.firebase
2 |
3 |
4 | object FirebaseConstants {
5 |
6 | const val LAST_LOGIN = "lastLogin"
7 | const val TOTAL_MESSAGES = "totalMessages"
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/firebase/FirebaseModels.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.firebase
2 |
3 | import com.google.firebase.Timestamp
4 | import com.google.firebase.firestore.DocumentSnapshot
5 | import com.google.firebase.firestore.ServerTimestamp
6 | import frangsierra.kotlinfirechat.chat.model.Message
7 | import frangsierra.kotlinfirechat.profile.model.PrivateData
8 | import frangsierra.kotlinfirechat.profile.model.PublicProfile
9 | import frangsierra.kotlinfirechat.profile.model.UserData
10 | import java.util.*
11 |
12 | fun UserData.toFirebaseUserData() = FirebaseUserData(username, photoUrl, uid)
13 |
14 | data class FirebaseUserData(val username: String = "",
15 | val photoUrl: String? = null,
16 | var uid: String = "")
17 |
18 | fun FirebaseUserData.toUserData() = UserData(username = username, photoUrl = photoUrl, uid = uid)
19 |
20 | data class FirebaseMessage(val author: FirebaseUserData = FirebaseUserData(),
21 | val message: String = "",
22 | val attachedImageUrl : String? = null,
23 | @ServerTimestamp val timestamp: Timestamp? = null)
24 |
25 | fun DocumentSnapshot.toMessage() = toObject(FirebaseMessage::class.java)!!.toMessage(id)
26 |
27 | fun FirebaseMessage.toMessage(id: String) = Message(uid = id,
28 | author = author.toUserData(),
29 | message = message,
30 | attachedImageUrl = attachedImageUrl,
31 | timestamp = timestamp?.toDate() ?: Timestamp.now().toDate())
32 |
33 | data class FirebasePrivateData(
34 | val email: String = "",
35 | val messagingTokens: List = emptyList()
36 | )
37 |
38 | fun DocumentSnapshot.toPrivateData() = toObject(FirebasePrivateData::class.java)!!.toPrivateData(id)
39 |
40 | fun FirebasePrivateData.toPrivateData(id: String) = PrivateData(uid = id,
41 | email = email,
42 | messagingTokens = messagingTokens)
43 |
44 | data class FirebasePublicProfile(
45 | val userData: FirebaseUserData = FirebaseUserData(),
46 | val lowerCaseUsername: String = "",
47 | val totalMessages: Int = 0,
48 | @ServerTimestamp val lastLogin: Timestamp? = null
49 | )
50 |
51 | fun DocumentSnapshot.toPublicProfile() = toObject(FirebasePublicProfile::class.java)!!.toPublicProfile()
52 |
53 | fun FirebasePublicProfile.toPublicProfile() = PublicProfile(userData = userData.toUserData(),
54 | lowerCaseUsername = lowerCaseUsername,
55 | totalMessages = totalMessages,
56 | lastLogin = lastLogin?.toDate() ?: Timestamp.now().toDate())
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/firebase/FirebaseModule.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.firebase
2 |
3 | import android.content.Context
4 | import com.firebase.jobdispatcher.FirebaseJobDispatcher
5 | import com.firebase.jobdispatcher.GooglePlayDriver
6 | import com.google.firebase.analytics.FirebaseAnalytics
7 | import com.google.firebase.auth.FirebaseAuth
8 | import com.google.firebase.firestore.FirebaseFirestore
9 | import com.google.firebase.firestore.FirebaseFirestoreSettings
10 | import com.google.firebase.iid.FirebaseInstanceId
11 | import com.google.firebase.storage.FirebaseStorage
12 | import dagger.Module
13 | import dagger.Provides
14 | import frangsierra.kotlinfirechat.core.dagger.AppScope
15 | import frangsierra.kotlinfirechat.core.flux.app
16 |
17 |
18 | @Module
19 | class FirebaseModule {
20 | @Provides
21 | @AppScope
22 | fun provideAuthentication(): FirebaseAuth = FirebaseAuth.getInstance()
23 |
24 | @Provides
25 | @AppScope
26 | fun provideInstanceId(): FirebaseInstanceId = FirebaseInstanceId.getInstance()
27 |
28 | @Provides
29 | @AppScope
30 | fun provideAnalytics(): FirebaseAnalytics = FirebaseAnalytics.getInstance(app)
31 |
32 | @Provides
33 | @AppScope
34 | fun provideStorage(): FirebaseStorage = FirebaseStorage.getInstance()
35 |
36 | @Provides
37 | @AppScope
38 | fun provideFirestore(): FirebaseFirestore = FirebaseFirestore.getInstance().apply {
39 | val settings = FirebaseFirestoreSettings.Builder()
40 | .setTimestampsInSnapshotsEnabled(true)
41 | .setPersistenceEnabled(true)
42 | .build()
43 | firestoreSettings = settings
44 | }
45 |
46 | @Provides @AppScope
47 | fun provideFirebaseDispatcher(context: Context): FirebaseJobDispatcher = FirebaseJobDispatcher(GooglePlayDriver(context))
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/firebase/FirebaseRefs.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.firebase
2 |
3 | import com.google.firebase.firestore.FirebaseFirestore
4 | import com.google.firebase.storage.FirebaseStorage
5 |
6 | const val DEFAULT_QUERY_LIMIT = 10L
7 | const val PUBLIC_PROFILE = "PublicProfile"
8 | const val PRIVATE_DATA = "PrivateData"
9 | const val MESSAGES = "Messages"
10 | const val STORAGE_USER = "User"
11 | const val STORAGE_MESSAGES = "Messages"
12 |
13 | fun FirebaseFirestore.publicProfile() = collection(PUBLIC_PROFILE)
14 |
15 | fun FirebaseFirestore.publicProfileDoc(uid: String) = publicProfile().document(uid)
16 |
17 | fun FirebaseFirestore.privateData() = collection(PRIVATE_DATA)
18 |
19 | fun FirebaseFirestore.privateDataDoc(uid: String) = privateData().document(uid)
20 |
21 | fun FirebaseFirestore.messages() = collection(MESSAGES)
22 |
23 | fun FirebaseFirestore.messageDoc(uid: String) = messages().document(uid)
24 |
25 | fun FirebaseStorage.chatStorageMessageRef(userId: String, uid: String) = reference
26 | .child(STORAGE_USER)
27 | .child(userId)
28 | .child(STORAGE_MESSAGES)
29 | .child(uid)
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/flux/App.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.flux
2 |
3 | import android.app.Application
4 | import com.crashlytics.android.Crashlytics
5 | import frangsierra.kotlinfirechat.BuildConfig
6 | import frangsierra.kotlinfirechat.core.dagger.AppComponent
7 | import frangsierra.kotlinfirechat.core.dagger.AppModule
8 | import frangsierra.kotlinfirechat.core.dagger.DaggerDefaultAppComponent
9 | import frangsierra.kotlinfirechat.core.errors.CrashlyticsHandler
10 | import frangsierra.kotlinfirechat.util.Prefs
11 | import io.fabric.sdk.android.Fabric
12 | import mini.MiniActionReducer
13 | import org.jetbrains.annotations.TestOnly
14 | import kotlin.properties.Delegates
15 |
16 |
17 | private var _app: App? = null
18 |
19 | private var _prefs: Prefs by Delegates.notNull()
20 | private var _appComponent: AppComponent? = null
21 | val app: App get() = _app!!
22 | val prefs: Prefs get() = _prefs
23 | val appComponent: AppComponent get() = _appComponent!!
24 |
25 | @TestOnly
26 | fun setAppComponent(component: AppComponent) {
27 | _appComponent = component
28 | }
29 |
30 | class App : Application() {
31 | override fun onCreate() {
32 | super.onCreate()
33 | _app = this
34 | _prefs = Prefs(this)
35 |
36 | //Initialize Fabric before add the custom UncaughtExceptionHandler!
37 | val fabric = Fabric.Builder(this)
38 | .kits(Crashlytics())
39 | .debuggable(BuildConfig.DEBUG) // Enables Crashlytics debugger
40 | .build()
41 | Fabric.with(fabric)
42 |
43 | val defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
44 | Thread.setDefaultUncaughtExceptionHandler(CrashlyticsHandler(defaultUncaughtExceptionHandler))
45 |
46 | if (_appComponent == null) {
47 | _appComponent = DaggerDefaultAppComponent
48 | .builder()
49 | .appModule(AppModule(this))
50 | .build()
51 | _appComponent!!.dispatcher().actionReducers.add(MiniActionReducer(stores = _appComponent!!.stores()))
52 | _appComponent!!.dispatcher().addInterceptor(CustomLoggerInterceptor
53 | (_appComponent!!.stores().values))
54 | }
55 |
56 | }
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/flux/Async.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.flux
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import java.lang.ref.WeakReference
6 | import java.util.concurrent.ExecutorService
7 | import java.util.concurrent.Executors
8 | import java.util.concurrent.Future
9 |
10 | class AsyncContext(val weakRef: WeakReference)
11 |
12 | private val crashLogger = { throwable : Throwable -> throwable.printStackTrace() }
13 | /**
14 | * Execute [task] asynchronously.
15 | *
16 | * @param exceptionHandler optional exception handler.
17 | * If defined, any exceptions thrown inside [task] will be passed to it. If not, exceptions will be ignored.
18 | * @param task the code to execute asynchronously.
19 | */
20 | fun T.doAsync(
21 | exceptionHandler: ((Throwable) -> Unit)? = crashLogger,
22 | task: AsyncContext.() -> Unit
23 | ): Future {
24 | val context = AsyncContext(WeakReference(this))
25 | return BackgroundExecutor.submit {
26 | return@submit try {
27 | context.task()
28 | } catch (thr: Throwable) {
29 | val result = exceptionHandler?.invoke(thr)
30 | if (result != null) {
31 | result
32 | } else {
33 | Unit
34 | }
35 | }
36 | }
37 | }
38 |
39 | fun T.doAsync(
40 | exceptionHandler: ((Throwable) -> Unit)? = crashLogger,
41 | executorService: ExecutorService,
42 | task: AsyncContext.() -> Unit
43 | ): Future {
44 | val context = AsyncContext(WeakReference(this))
45 | return executorService.submit {
46 | try {
47 | context.task()
48 | } catch (thr: Throwable) {
49 | exceptionHandler?.invoke(thr)
50 | }
51 | }
52 | }
53 |
54 | fun T.doAsyncResult(
55 | exceptionHandler: ((Throwable) -> Unit)? = crashLogger,
56 | task: AsyncContext.() -> R
57 | ): Future {
58 | val context = AsyncContext(WeakReference(this))
59 | return BackgroundExecutor.submit {
60 | try {
61 | context.task()
62 | } catch (thr: Throwable) {
63 | exceptionHandler?.invoke(thr)
64 | throw thr
65 | }
66 | }
67 | }
68 |
69 | fun T.doAsyncResult(
70 | exceptionHandler: ((Throwable) -> Unit)? = crashLogger,
71 | executorService: ExecutorService,
72 | task: AsyncContext.() -> R
73 | ): Future {
74 | val context = AsyncContext(WeakReference(this))
75 | return executorService.submit {
76 | try {
77 | context.task()
78 | } catch (thr: Throwable) {
79 | exceptionHandler?.invoke(thr)
80 | throw thr
81 | }
82 | }
83 | }
84 |
85 | internal object BackgroundExecutor {
86 | var executor: ExecutorService =
87 | Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors())
88 |
89 | fun submit(task: () -> T): Future = executor.submit(task)
90 |
91 | }
92 |
93 | private object ContextHelper {
94 | val handler = Handler(Looper.getMainLooper())
95 | val mainThread: Thread = Looper.getMainLooper().thread
96 | }
97 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/flux/CustomLoggerInterceptor.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.flux
2 |
3 | import io.reactivex.Completable
4 | import io.reactivex.schedulers.Schedulers
5 | import mini.*
6 |
7 | /** Actions implementing this interface won't log anything */
8 | interface SilentActionTag
9 |
10 | class CustomLoggerInterceptor constructor(stores: Collection>,
11 | private val logInBackground: Boolean = false) : Interceptor {
12 |
13 | private val stores = stores.toList()
14 | private var lastActionTime = System.currentTimeMillis()
15 | private var actionCounter: Long = 0
16 |
17 | override fun invoke(action: Action, chain: Chain): Action {
18 | if (action is SilentActionTag) return chain.proceed(action) //Do nothing
19 |
20 | val beforeStates: Array = Array(stores.size, { _ -> Unit })
21 | val afterStates: Array = Array(stores.size, { _ -> Unit })
22 |
23 | stores.forEachIndexed({ idx, store -> beforeStates[idx] = store.state })
24 | val start = System.currentTimeMillis()
25 | val timeSinceLastAction = Math.min(start - lastActionTime, 9999)
26 | lastActionTime = start
27 | actionCounter++
28 | val out = chain.proceed(action)
29 | val processTime = System.currentTimeMillis() - start
30 | stores.forEachIndexed({ idx, store -> afterStates[idx] = store.state })
31 |
32 | if (action is SilentActionTag) return out
33 |
34 | Completable.fromAction {
35 | val sb = StringBuilder()
36 | sb.append("┌────────────────────────────────────────────\n")
37 | sb.append(String.format("├─> %s %dms [+%dms][%d] - %s",
38 | action.javaClass.simpleName, processTime, timeSinceLastAction, actionCounter % 10, action))
39 | .append("\n")
40 |
41 | // Log whether an interceptor changed the action and display the resulting action
42 | if (out != action) {
43 | sb.append(String.format("│ %s", "=== Action has been intercepted, result: ===")).append("\n")
44 | sb.append(String.format("├─> %s %dms [+%dms][%d] - %s",
45 | out.javaClass.simpleName, processTime, timeSinceLastAction, actionCounter % 10, out))
46 | .append("\n")
47 | }
48 |
49 | for (i in beforeStates.indices) {
50 | val oldState = beforeStates[i]
51 | val newState = afterStates[i]
52 | if (oldState !== newState) {
53 | //This operation is costly, don't do it in prod
54 | val line = "${stores[i].javaClass.simpleName}: $newState"
55 | sb.append(String.format("│ %s", line)).append("\n")
56 | }
57 | }
58 |
59 | sb.append("└────────────────────────────────────────────\n")
60 | if (action !is SilentActionTag) {
61 | Grove.i { "LoggerStore $sb" }
62 | }
63 | }.let {
64 | if (logInBackground) it.subscribeOn(Schedulers.single())
65 | else it
66 | }.subscribe()
67 |
68 | return out
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/flux/FluxActivity.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.flux
2 |
3 | import android.os.Bundle
4 | import android.support.v7.app.AppCompatActivity
5 | import frangsierra.kotlinfirechat.core.dagger.inject
6 | import frangsierra.kotlinfirechat.core.flux.appComponent
7 | import mini.DefaultSubscriptionTracker
8 | import mini.Dispatcher
9 | import mini.SubscriptionTracker
10 | import javax.inject.Inject
11 |
12 | abstract class FluxActivity : AppCompatActivity(),
13 | SubscriptionTracker by DefaultSubscriptionTracker() {
14 |
15 | @Inject
16 | lateinit protected var dispatcher: Dispatcher
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | inject(appComponent, this)
21 | }
22 |
23 | override fun onDestroy() {
24 | super.onDestroy()
25 | cancelSubscriptions()
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/flux/FluxFragment.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.common.flux
2 |
3 | import android.support.annotation.CallSuper
4 | import android.support.v4.app.Fragment
5 | import mini.DefaultSubscriptionTracker
6 | import mini.Dispatcher
7 | import mini.SubscriptionTracker
8 | import javax.inject.Inject
9 |
10 | /**
11 | * Custom [Fragment] capable of tracking subscriptions and cancel them when the fragment
12 | * is destroyed in order to avoid possible memory leaks.
13 | */
14 | open class FluxFragment :
15 | Fragment(),
16 | SubscriptionTracker by DefaultSubscriptionTracker() {
17 |
18 | @Inject
19 | lateinit var dispatcher: Dispatcher
20 |
21 | @Suppress("KDocMissingDocumentation")
22 | @CallSuper
23 | override fun onDestroyView() {
24 | super.onDestroyView()
25 | cancelSubscriptions()
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/core/service/DataUploadService.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.core.service
2 |
3 | import android.net.Uri
4 | import android.os.Bundle
5 | import com.firebase.jobdispatcher.*
6 | import com.google.android.gms.tasks.Tasks
7 | import com.google.firebase.firestore.FirebaseFirestore
8 | import com.google.firebase.storage.FirebaseStorage
9 | import com.google.firebase.storage.StorageReference
10 | import com.google.firebase.storage.UploadTask
11 | import frangsierra.kotlinfirechat.core.firebase.chatStorageMessageRef
12 | import frangsierra.kotlinfirechat.core.firebase.messageDoc
13 | import frangsierra.kotlinfirechat.util.ImageUtils.resizeUriToPostImage
14 | import io.reactivex.Single
15 | import io.reactivex.schedulers.Schedulers
16 | import mini.Grove
17 |
18 | private const val MESSAGE_ID_SERVICE_KEY = "message_service_id"
19 | private const val USER_ID_SERVICE_KEY = "user_service_id"
20 | private const val UPLOAD_FILE_URL_KEY = "file_url_id"
21 |
22 | class DataUploadService : JobService() {
23 | private val firebaseStorage: FirebaseStorage = FirebaseStorage.getInstance()
24 | private val firebaseFirestore: FirebaseFirestore = FirebaseFirestore.getInstance()
25 |
26 | override fun onStartJob(job: JobParameters): Boolean {
27 | Grove.d { "Crash reporting job called ${job.tag}" }
28 |
29 | val extras = job.extras ?: throw NullPointerException("Bundle can't be null")
30 |
31 | val messageID = extras.getString(MESSAGE_ID_SERVICE_KEY)
32 | val userId = extras.getString(USER_ID_SERVICE_KEY)
33 | val fileUri = Uri.parse(extras.getString(UPLOAD_FILE_URL_KEY))
34 |
35 | uploadFile(job, messageID, userId, fileUri)
36 | return true
37 | }
38 |
39 | private fun uploadFile(job: JobParameters, messageId: String, userId: String, fileUrl: Uri) {
40 | resizeUriToPostImage(fileUrl).flatMap { bytes -> putBytes(firebaseStorage.chatStorageMessageRef(userId, messageId), bytes) }
41 | .subscribeOn(Schedulers.io())
42 | .observeOn(Schedulers.io())
43 | .subscribe(
44 | { task ->
45 | try {
46 | val uri = Tasks.await(task.storage.downloadUrl)
47 | Tasks.await(firebaseFirestore.messageDoc(messageId).update("attachedImageUrl", uri.toString()))
48 | jobFinished(job, false)
49 | } catch (e: Exception) {
50 | jobFinished(job, true)
51 | }
52 | }, {
53 | jobFinished(job, true)
54 | })
55 | }
56 |
57 | /**
58 | * onStopJob is only called when we manually cancel the current jobs,
59 | * this happens when the user disconnects his wifi network.
60 | */
61 | override fun onStopJob(job: JobParameters): Boolean {
62 | Grove.d { "Crash reporting job stopped ${job.tag}" }
63 | //onStop should return true to re-schedule the canceled job.
64 | return true
65 | }
66 | }
67 |
68 | fun buildUploadJob(uri: String, userId: String, messageId: String, builder: Job.Builder): Job {
69 | val myExtrasBundle = Bundle()
70 |
71 | myExtrasBundle.putString(MESSAGE_ID_SERVICE_KEY, messageId)
72 | myExtrasBundle.putString(USER_ID_SERVICE_KEY, userId)
73 | myExtrasBundle.putString(UPLOAD_FILE_URL_KEY, uri)
74 |
75 | return builder
76 | .setService(DataUploadService::class.java) // the JobService that will be called
77 | .setTag("$messageId-$userId") // uniquely identifies the job
78 | .setRecurring(false) // one-off job
79 | .setLifetime(Lifetime.FOREVER) // don't persist past a device reboot
80 | .setTrigger(Trigger.NOW) // start immediately
81 | .setReplaceCurrent(true) // don't overwrite an existing job with the same tag
82 | .setRetryStrategy(RetryStrategy.DEFAULT_LINEAR) // retry with linear backoff
83 | .setExtras(myExtrasBundle)
84 | .build()
85 | }
86 |
87 | internal fun putBytes(storageRef: StorageReference, bytes: ByteArray): Single {
88 | return Single.create { emitter ->
89 | val taskSnapshotStorageTask = storageRef.putBytes(bytes)
90 | .addOnSuccessListener { taskSnapshot -> emitter.onSuccess(taskSnapshot) }
91 | .addOnFailureListener { e ->
92 | if (!emitter.isDisposed) {
93 | emitter.onError(e)
94 | }
95 | }
96 | emitter.setCancellable { taskSnapshotStorageTask.cancel() }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/home/HomeActivity.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.home
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.net.Uri
8 | import android.os.Bundle
9 | import android.support.v4.content.ContextCompat
10 | import com.crashlytics.android.Crashlytics
11 | import com.tbruyelle.rxpermissions2.RxPermissions
12 | import frangsierra.kotlinfirechat.R
13 | import frangsierra.kotlinfirechat.chat.store.ChatStore
14 | import frangsierra.kotlinfirechat.chat.store.SendMessageAction
15 | import frangsierra.kotlinfirechat.chat.store.StartListeningChatMessagesAction
16 | import frangsierra.kotlinfirechat.chat.store.StopListeningChatMessagesAction
17 | import frangsierra.kotlinfirechat.core.errors.ErrorHandler
18 | import frangsierra.kotlinfirechat.core.flux.FluxActivity
19 | import frangsierra.kotlinfirechat.profile.store.ProfileStore
20 | import frangsierra.kotlinfirechat.session.LoginActivity
21 | import frangsierra.kotlinfirechat.util.*
22 | import kotlinx.android.synthetic.main.home_activity.*
23 | import javax.inject.Inject
24 |
25 | class HomeActivity : FluxActivity() {
26 |
27 | @Inject
28 | lateinit var profileStore: ProfileStore
29 | @Inject
30 | lateinit var chatStore: ChatStore
31 | @Inject
32 | lateinit var errorHandler: ErrorHandler
33 |
34 | companion object {
35 | fun newIntent(context: Context): Intent =
36 | Intent(context, HomeActivity::class.java)
37 | }
38 |
39 | private val messageAdapter = MessageAdapter()
40 | private var outputFileUri: Uri? = null
41 | //FIXME inject me if it's used somewhere else
42 | private val rxPermissionInstance: RxPermissions by lazy { RxPermissions(this) }
43 |
44 | override fun onCreate(savedInstanceState: Bundle?) {
45 | super.onCreate(savedInstanceState)
46 | setContentView(R.layout.home_activity)
47 | initializeInterface()
48 | startListeningStoreChanges()
49 | }
50 |
51 | private fun initializeInterface() {
52 | messageRecycler.setLinearLayoutManager(this, reverseLayout = false, stackFromEnd = false)
53 | messageRecycler.adapter = messageAdapter
54 | sendButton.setOnClickListener { sendMessage() }
55 | photoPickerButton.setOnClickListener { requestPermissionsAndPickImage() }
56 | }
57 |
58 | private fun startListeningStoreChanges() {
59 | profileStore.flowable()
60 | .view { it.loadProfileTask }
61 | .subscribe {
62 | when (it.status) {
63 | TypedTask.Status.RUNNING -> showProgressDialog("Loading user profile")
64 | TypedTask.Status.SUCCESS -> dismissProgressDialog()
65 | TypedTask.Status.FAILURE -> goToLogin()
66 | }
67 | }.track()
68 |
69 | chatStore.flowable()
70 | .view { it.messages }
71 | .filter { it.isNotEmpty() }
72 | .subscribe { messageAdapter.updateMessages(it.values.toList()) }
73 | .track()
74 | }
75 |
76 | private fun sendMessage() {
77 | if (messageEditText.text.isEmpty()) {
78 | toast("You should add a text")
79 | return
80 | }
81 | sendButton.isEnabled = false
82 | dispatcher.dispatchOnUi(SendMessageAction(messageEditText.text.toString(), outputFileUri))
83 | messageEditText.text.clear()
84 | chatStore.flowable()
85 | .filterOne { it.sendMessageTask.isTerminal() } //Wait for request to finish
86 | .subscribe {
87 | if (it.sendMessageTask.isFailure()) {
88 | errorHandler.handle(it.sendMessageTask.error)
89 | toast("There was an error sending your message")
90 | }
91 | sendButton.isEnabled = true
92 | }.track()
93 | }
94 |
95 | private fun goToLogin() {
96 | dismissProgressDialog()
97 | val intent = LoginActivity.newIntent(this).apply {
98 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
99 | }
100 | startActivity(intent)
101 | }
102 |
103 | private fun requestPermissionsAndPickImage() {
104 | rxPermissionInstance.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
105 | .subscribe {
106 | if (it) {
107 | outputFileUri = AndroidUtils.generateUniqueFireUri(this)
108 | AndroidUtils.showImageIntentDialog(this, outputFileUri!!)
109 | }
110 | }
111 | }
112 |
113 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
114 | if (resultCode != Activity.RESULT_CANCELED) {
115 | when (requestCode) {
116 | TC_REQUEST_GALLERY, TC_REQUEST_CAMERA ->
117 | if (resultCode == Activity.RESULT_OK) {
118 | val uri = if (requestCode == TC_REQUEST_GALLERY) data?.data else outputFileUri
119 | if (uri == null) {
120 | Crashlytics.log("Uri was null when updating profile picture and using code $resultCode")
121 | toast(getString(R.string.error_picture_null))
122 | return
123 | }
124 | onImageReady()
125 | }
126 | }
127 | }
128 | }
129 |
130 | private fun onImageReady() {
131 | toast("your image have been attached")
132 | //TODO move to selector
133 | photoPickerButton.setColorFilter(ContextCompat.getColor(this, R.color.image_picked_color),
134 | android.graphics.PorterDuff.Mode.MULTIPLY)
135 | }
136 |
137 | override fun onStart() {
138 | super.onStart()
139 | dispatcher.dispatch(StartListeningChatMessagesAction())
140 | }
141 |
142 | override fun onStop() {
143 | super.onStop()
144 | dispatcher.dispatch(StopListeningChatMessagesAction())
145 | }
146 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/home/MessageAdapter.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.home
2 |
3 |
4 | import android.support.v7.util.DiffUtil
5 | import android.support.v7.util.DiffUtil.calculateDiff
6 | import android.support.v7.widget.RecyclerView
7 | import android.view.View
8 | import android.widget.ImageView
9 | import android.widget.TextView
10 | import frangsierra.kotlinfirechat.R
11 | import frangsierra.kotlinfirechat.chat.model.Message
12 | import frangsierra.kotlinfirechat.util.setCircularImage
13 | import frangsierra.kotlinfirechat.util.setImage
14 | import kotlinx.android.synthetic.main.item_message.view.*
15 |
16 | class MessageAdapter : RecyclerView.Adapter() {
17 | private val messageList: MutableList = java.util.ArrayList()
18 |
19 | override fun onBindViewHolder(holder: MessageAdapter.MessageViewHolder, position: Int) {
20 | with(messageList[position]) {
21 | holder.messageTextView.text = message
22 | holder.authorTextView.text = author.username
23 | //holder.timeAgoTextView.text = timestamp.time
24 | if (author.photoUrl == null) {
25 | holder.photoImageView.visibility = View.GONE
26 | } else {
27 | holder.photoImageView.setCircularImage(author.photoUrl)
28 | holder.photoImageView.visibility = View.VISIBLE
29 | }
30 | if (attachedImageUrl == null) {
31 | holder.messagePhoto.visibility = View.GONE
32 | } else {
33 | holder.messagePhoto.setImage(attachedImageUrl) { holder.messagePhoto.visibility = View.GONE}
34 | holder.messagePhoto.visibility = View.VISIBLE
35 | }
36 |
37 | }
38 | }
39 |
40 | override fun onCreateViewHolder(parent: android.view.ViewGroup, viewType: Int): MessageAdapter.MessageViewHolder {
41 | val v = android.view.LayoutInflater.from(parent.context).inflate(R.layout.item_message, parent, false)
42 | return MessageViewHolder(v)
43 | }
44 |
45 | override fun getItemCount(): Int {
46 | return messageList.size
47 | }
48 |
49 | fun updateMessages(messages: List) {
50 | val diffResult = calculateDiff(MessageDiff(this.messageList, messages))
51 | messageList.clear()
52 | messageList.addAll(messages)
53 | diffResult.dispatchUpdatesTo(this)
54 | }
55 |
56 | inner class MessageViewHolder(v: android.view.View) : RecyclerView.ViewHolder(v) {
57 | val photoImageView: ImageView = v.comment_author_picture
58 | val messagePhoto: ImageView = v.comment_picture
59 | val messageTextView: TextView = v.comment_description
60 | val authorTextView: TextView = v.comment_author_username
61 | val timeAgoTextView: TextView = v.comment_time_ago
62 | }
63 |
64 | inner class MessageDiff(val oldList: List, val newList: List) : DiffUtil.Callback() {
65 |
66 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
67 | return oldList[oldItemPosition].uid == newList[newItemPosition].uid
68 | }
69 |
70 | override fun getOldListSize(): Int {
71 | return oldList.size
72 | }
73 |
74 | override fun getNewListSize(): Int {
75 | return newList.size
76 | }
77 |
78 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
79 | return oldList[oldItemPosition] == newList[newItemPosition]
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/profile/controller/ProfileController.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.profile.controller
2 |
3 | import com.google.android.gms.tasks.Tasks
4 | import com.google.firebase.Timestamp
5 | import com.google.firebase.firestore.FirebaseFirestore
6 | import frangsierra.kotlinfirechat.core.dagger.AppScope
7 | import frangsierra.kotlinfirechat.core.firebase.*
8 | import frangsierra.kotlinfirechat.core.firebase.FirebaseConstants.LAST_LOGIN
9 | import frangsierra.kotlinfirechat.core.flux.doAsync
10 | import frangsierra.kotlinfirechat.profile.model.PrivateData
11 | import frangsierra.kotlinfirechat.profile.model.PublicProfile
12 | import frangsierra.kotlinfirechat.profile.model.UserData
13 | import frangsierra.kotlinfirechat.profile.store.LoadUserDataCompleteAction
14 | import frangsierra.kotlinfirechat.session.model.User
15 | import frangsierra.kotlinfirechat.util.taskFailure
16 | import frangsierra.kotlinfirechat.util.taskSuccess
17 | import mini.Dispatcher
18 | import javax.inject.Inject
19 |
20 | interface ProfileController {
21 | fun loadUserProfile(user: User)
22 | }
23 |
24 | @AppScope
25 | class ProfileControllerImpl @Inject constructor(private val firestore: FirebaseFirestore, val dispatcher: Dispatcher) : ProfileController {
26 | override fun loadUserProfile(user: User) {
27 | doAsync {
28 | try {
29 | val privateData = getAndCreateIfNoyExistsPrivateData(user)
30 | val publicProfile = getAndCreateIfNotExistsPublicData(user)
31 | updateUserLastLogin(userId = user.uid)
32 | dispatcher.dispatchOnUi(LoadUserDataCompleteAction(privateData, publicProfile, taskSuccess()))
33 | } catch (e: Throwable) {
34 | dispatcher.dispatchOnUi(LoadUserDataCompleteAction(null, null, taskFailure(e)))
35 | }
36 | }
37 | }
38 |
39 | private fun updateUserLastLogin(userId: String) {
40 | Tasks.await(firestore.publicProfileDoc(userId).update(LAST_LOGIN, Timestamp.now()))
41 | }
42 |
43 | private fun getAndCreateIfNoyExistsPrivateData(user: User): PrivateData {
44 | val privateDataDocument = Tasks.await(firestore.privateDataDoc(user.uid).get())
45 | return if (privateDataDocument.exists()) privateDataDocument.toPrivateData()
46 | else {
47 | val firebasePrivateData = FirebasePrivateData(user.email)
48 | Tasks.await(firestore.privateDataDoc(user.uid).set(firebasePrivateData))
49 | firebasePrivateData.toPrivateData(user.uid)
50 | }
51 | }
52 |
53 | private fun getAndCreateIfNotExistsPublicData(user: User): PublicProfile {
54 | val userData = UserData(user.username, user.photoUrl, user.uid)
55 | val publicProfile = Tasks.await(firestore.publicProfileDoc(userData.uid).get())
56 | return if (publicProfile.exists()) publicProfile.toPublicProfile()
57 | else {
58 | val firebasePublicProfile = FirebasePublicProfile(userData.toFirebaseUserData(), userData.username.toLowerCase())
59 | Tasks.await(firestore.publicProfileDoc(userData.uid).set(firebasePublicProfile))
60 | firebasePublicProfile.toPublicProfile()
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/profile/model/Models.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.profile.model
2 |
3 | import com.google.firebase.firestore.ServerTimestamp
4 | import java.util.*
5 |
6 |
7 | data class PrivateData(
8 | val uid: String = "",
9 | val email: String = "",
10 | val messagingTokens: List = emptyList()
11 | )
12 |
13 | data class PublicProfile(
14 | val userData: UserData = UserData(),
15 | val lowerCaseUsername: String = "",
16 | val totalMessages: Int = 0,
17 | val lastLogin: Date
18 | )
19 |
20 | data class UserData(val username: String = "",
21 | val photoUrl: String? = null,
22 | var uid: String = "") {
23 | val loaded = !uid.isEmpty()
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/profile/store/ProfileActions.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.profile.store
2 |
3 | import frangsierra.kotlinfirechat.profile.model.PrivateData
4 | import frangsierra.kotlinfirechat.profile.model.PublicProfile
5 | import frangsierra.kotlinfirechat.util.Task
6 | import mini.Action
7 |
8 | data class LoadUserDataCompleteAction(val privateData: PrivateData?,
9 | val publicProfile: PublicProfile?,
10 | val task: Task) : Action
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/profile/store/ProfileState.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.profile.store
2 |
3 | import frangsierra.kotlinfirechat.profile.model.PrivateData
4 | import frangsierra.kotlinfirechat.profile.model.PublicProfile
5 | import frangsierra.kotlinfirechat.util.Task
6 |
7 | data class ProfileState(val privateData: PrivateData? = null,
8 | val publicProfile: PublicProfile? = null,
9 | val loadProfileTask: Task = Task())
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/profile/store/ProfileStore.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.profile.store
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.multibindings.ClassKey
6 | import dagger.multibindings.IntoMap
7 | import frangsierra.kotlinfirechat.chat.store.SendMessageCompleteAction
8 | import frangsierra.kotlinfirechat.core.dagger.AppScope
9 | import frangsierra.kotlinfirechat.profile.controller.ProfileController
10 | import frangsierra.kotlinfirechat.profile.controller.ProfileControllerImpl
11 | import frangsierra.kotlinfirechat.session.store.CreateAccountCompleteAction
12 | import frangsierra.kotlinfirechat.session.store.LoginCompleteAction
13 | import frangsierra.kotlinfirechat.util.taskRunning
14 | import mini.Reducer
15 | import mini.Store
16 | import javax.inject.Inject
17 |
18 | @AppScope
19 | class ProfileStore @Inject constructor(val controller: ProfileController) : Store() {
20 |
21 | @Reducer
22 | fun loadAndCreateUserData(action: CreateAccountCompleteAction): ProfileState {
23 | if (!action.task.isSuccessful()) return state
24 | controller.loadUserProfile(action.user!!) //User can't be null if the request is successful
25 | return state.copy(loadProfileTask = taskRunning())
26 | }
27 |
28 | @Reducer
29 | fun loadUserOnLogin(action: LoginCompleteAction): ProfileState {
30 | if (!action.task.isSuccessful()) return state
31 | controller.loadUserProfile(action.user!!) //User can't be null if the request is successful
32 | return state.copy(loadProfileTask = taskRunning())
33 | }
34 |
35 | fun updateMessageCount(action: SendMessageCompleteAction): ProfileState {
36 | if (!action.task.isSuccessful()) return state
37 | return state.copy(publicProfile = state.publicProfile?.copy(totalMessages = state.publicProfile!!.totalMessages.plus(1)))
38 | }
39 |
40 | @Reducer
41 | fun userDataLoaded(action: LoadUserDataCompleteAction): ProfileState {
42 | if (!state.loadProfileTask.isRunning()) return state
43 | return state.copy(loadProfileTask = action.task, publicProfile = action.publicProfile, privateData = action.privateData)
44 | }
45 | }
46 |
47 | @Module
48 | abstract class ProfileModule {
49 | @Binds
50 | @AppScope
51 | @IntoMap
52 | @ClassKey(ProfileStore::class)
53 | abstract fun provideProfileStore(store: ProfileStore): Store<*>
54 |
55 | @Binds
56 | @AppScope
57 | abstract fun bindProfileController(impl: ProfileControllerImpl): ProfileController
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/session/CreateAccountActivity.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.session
2 |
3 | import android.content.Intent
4 | import com.google.android.gms.auth.api.signin.GoogleSignIn
5 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount
6 | import com.google.android.gms.auth.api.signin.GoogleSignInClient
7 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions
8 | import com.google.android.gms.common.api.ApiException
9 | import com.google.firebase.auth.AuthCredential
10 | import frangsierra.kotlinfirechat.home.HomeActivity
11 | import frangsierra.kotlinfirechat.R
12 | import frangsierra.kotlinfirechat.core.errors.ErrorHandler
13 | import frangsierra.kotlinfirechat.core.flux.FluxActivity
14 | import frangsierra.kotlinfirechat.session.store.CreateAccountWithCredentialsAction
15 | import frangsierra.kotlinfirechat.session.store.CreateAccountWithProviderCredentialsAction
16 | import frangsierra.kotlinfirechat.session.store.SessionStore
17 | import frangsierra.kotlinfirechat.util.*
18 | import kotlinx.android.synthetic.main.create_account_activity.*
19 | import javax.inject.Inject
20 |
21 | class CreateAccountActivity : FluxActivity(), GoogleLoginCallback {
22 |
23 | @Inject
24 | lateinit var sessionStore: SessionStore
25 | @Inject
26 | lateinit var errorHandler: ErrorHandler
27 |
28 | override val googleApiClient: GoogleSignInOptions by lazy {
29 | GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
30 | .requestIdToken(getString(R.string.default_web_client_id))
31 | .requestEmail()
32 | .build()
33 | }
34 |
35 | override val googleSingInClient: GoogleSignInClient by lazy { GoogleSignIn.getClient(this, googleApiClient) }
36 |
37 | override fun onCreate(savedInstanceState: android.os.Bundle?) {
38 | super.onCreate(savedInstanceState)
39 | setContentView(frangsierra.kotlinfirechat.R.layout.create_account_activity)
40 |
41 | initializeInterface()
42 | }
43 |
44 | private fun initializeInterface() {
45 | createAccountButton.setOnClickListener { signInWithEmailAndPassword() }
46 | createGoogleButton.setOnClickListener { logInWithGoogle(this) }
47 | }
48 |
49 | private fun signInWithEmailAndPassword() {
50 | if (!fieldsAreFilled()) return
51 | showProgressDialog("Creating account")
52 | dispatcher.dispatch(CreateAccountWithCredentialsAction(editTextEmail.text.toString(), editTextPassword.text.toString(), editTextUsername.text.toString()))
53 | sessionStore.flowable()
54 | .filterOne { it.createAccountTask.isTerminal() } //Wait for request to finish
55 | .subscribe {
56 | if (it.createAccountTask.isSuccessful() && it.loggedUser != null) {
57 | if (it.verified) goToHome() else goToVerificationEmailScreen()
58 | } else if (it.createAccountTask.isFailure()) {
59 | it.createAccountTask.error?.let {
60 | toast(errorHandler.getMessageForError(it))
61 | }
62 | }
63 | dismissProgressDialog()
64 | }.track()
65 | }
66 |
67 | //How to retrieve SHA1 for Firebase Google Sign In https://stackoverflow.com/questions/15727912/sha-1-fingerprint-of-keystore-certificate
68 | override fun onGoogleCredentialReceived(credential: AuthCredential, account: GoogleSignInAccount) {
69 | showProgressDialog("Creating account")
70 | dispatcher.dispatch(CreateAccountWithProviderCredentialsAction(credential, GoogleSignInApiUtils.getUserData(account)))
71 | sessionStore.flowable()
72 | .filterOne { it.createAccountTask.isTerminal() } //Wait for request to finish
73 | .subscribe {
74 | if (it.createAccountTask.isSuccessful() && it.loggedUser != null) {
75 | if (it.verified) goToHome() else goToVerificationEmailScreen()
76 | } else if (it.createAccountTask.isFailure()) {
77 | it.createAccountTask.error?.let {
78 | toast(errorHandler.getMessageForError(it))
79 | }
80 | }
81 | dismissProgressDialog()
82 | }.track()
83 | }
84 |
85 | override fun onGoogleSignInFailed(e: ApiException) {
86 | dismissProgressDialog()
87 | toast(e.toString())
88 | }
89 |
90 | private fun fieldsAreFilled(): Boolean {
91 | editTextUsername.text.toString().takeIf { it.isEmpty() }?.let {
92 | inputUsername.onError(getString(frangsierra.kotlinfirechat.R.string.error_cannot_be_empty))
93 | return false
94 | }
95 | inputUsername.onError(null, false)
96 |
97 | editTextEmail.text.toString().takeIf { it.isEmpty() }?.let {
98 | inputEmail.onError(getString(frangsierra.kotlinfirechat.R.string.error_cannot_be_empty))
99 | return false
100 | }
101 | inputEmail.onError(null, false)
102 |
103 | editTextPassword.text.toString().takeIf { it.isEmpty() }?.let {
104 | inputPassword.onError(getString(frangsierra.kotlinfirechat.R.string.error_cannot_be_empty))
105 | return false
106 | }
107 | editTextPassword.text.toString().takeIf { it.length < 6 }?.let {
108 | inputPassword.onError(getString(frangsierra.kotlinfirechat.R.string.error_invalid_password_not_valid))
109 | return false
110 | }
111 | inputPassword.onError(null, false)
112 | return true
113 | }
114 |
115 | private fun goToHome() {
116 | dismissProgressDialog()
117 | val intent = HomeActivity.newIntent(this).apply {
118 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
119 | }
120 | startActivity(intent)
121 | }
122 |
123 | private fun goToVerificationEmailScreen() {
124 | dismissProgressDialog()
125 | EmailVerificationActivity.startActivity(this, sessionStore.state.loggedUser!!.email)
126 | overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
127 | }
128 |
129 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
130 | super.onActivityResult(requestCode, resultCode, data)
131 | manageGoogleResult(requestCode, data)
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/session/LoginActivity.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.session
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import com.google.android.gms.auth.api.signin.GoogleSignIn
7 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount
8 | import com.google.android.gms.auth.api.signin.GoogleSignInClient
9 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions
10 | import com.google.android.gms.common.api.ApiException
11 | import com.google.firebase.auth.AuthCredential
12 | import frangsierra.kotlinfirechat.home.HomeActivity
13 | import frangsierra.kotlinfirechat.R
14 | import frangsierra.kotlinfirechat.core.errors.ErrorHandler
15 | import frangsierra.kotlinfirechat.core.flux.FluxActivity
16 | import frangsierra.kotlinfirechat.session.store.LoginWithCredentials
17 | import frangsierra.kotlinfirechat.session.store.LoginWithProviderCredentials
18 | import frangsierra.kotlinfirechat.session.store.SessionStore
19 | import frangsierra.kotlinfirechat.util.*
20 | import kotlinx.android.synthetic.main.login_activity.*
21 | import javax.inject.Inject
22 |
23 | class LoginActivity : FluxActivity(), GoogleLoginCallback {
24 |
25 | @Inject
26 | lateinit var sessionStore: SessionStore
27 | @Inject
28 | lateinit var errorHandler: ErrorHandler
29 |
30 | companion object {
31 | fun newIntent(context: Context): Intent =
32 | Intent(context, LoginActivity::class.java)
33 | }
34 |
35 | override val googleApiClient: GoogleSignInOptions by lazy {
36 | GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
37 | .requestIdToken(getString(R.string.default_web_client_id))
38 | .requestEmail()
39 | .build()
40 | }
41 |
42 | override val googleSingInClient: GoogleSignInClient by lazy { GoogleSignIn.getClient(this, googleApiClient) }
43 |
44 | override fun onCreate(savedInstanceState: Bundle?) {
45 | super.onCreate(savedInstanceState)
46 | setContentView(R.layout.login_activity)
47 |
48 | initializeInterface()
49 | }
50 |
51 | private fun initializeInterface() {
52 | loginPasswordButton.setOnClickListener { loginWithEmailAndPassword() }
53 | loginGoogleButton.setOnClickListener { logInWithGoogle(this) }
54 | createAccountText.setOnClickListener {
55 | startActivity(Intent(this, CreateAccountActivity::class.java))
56 | }
57 | }
58 |
59 | private fun loginWithEmailAndPassword() {
60 | if (!fieldsAreFilled()) return
61 | showProgressDialog("Logging")
62 | dispatcher.dispatch(LoginWithCredentials(editTextEmail.text.toString(), editTextPassword.text.toString()))
63 | sessionStore.flowable()
64 | .filterOne { it.loginTask.isTerminal() } //Wait for request to finish
65 | .subscribe {
66 | if (it.loginTask.isSuccessful() && it.loggedUser != null) {
67 | if (it.verified) goToHome() else goToVerificationEmailScreen()
68 | } else if (it.loginTask.isFailure()) {
69 | it.loginTask.error?.let {
70 | toast(errorHandler.getMessageForError(it))
71 | }
72 | }
73 | dismissProgressDialog()
74 | }.track()
75 | }
76 |
77 | //How to retrieve SHA1 for Firebase Google Sign In https://stackoverflow.com/questions/15727912/sha-1-fingerprint-of-keystore-certificate
78 | override fun onGoogleCredentialReceived(credential: AuthCredential, account: GoogleSignInAccount) {
79 | showProgressDialog("Logging")
80 | dispatcher.dispatch(LoginWithProviderCredentials(credential, account.email!!))
81 | sessionStore.flowable()
82 | .filterOne { it.loginTask.isTerminal() } //Wait for request to finish
83 | .subscribe {
84 | if (it.loginTask.isSuccessful() && it.loggedUser != null) {
85 | if (it.verified) goToHome() else goToVerificationEmailScreen()
86 | } else if (it.loginTask.isFailure()) {
87 | it.loginTask.error?.let {
88 | toast(errorHandler.getMessageForError(it))
89 | }
90 | }
91 | dismissProgressDialog()
92 | }.track()
93 | }
94 |
95 | override fun onGoogleSignInFailed(e: ApiException) {
96 | dismissProgressDialog()
97 | toast(e.toString())
98 | }
99 |
100 | private fun fieldsAreFilled(): Boolean {
101 | editTextEmail.text.toString().takeIf { it.isEmpty() }?.let {
102 | inputEmail.onError(getString(R.string.error_cannot_be_empty))
103 | return false
104 | }
105 | inputEmail.onError(null, false)
106 | editTextPassword.text.toString().takeIf { it.isEmpty() }?.let {
107 | inputPassword.onError(getString(R.string.error_cannot_be_empty))
108 | return false
109 | }
110 | inputPassword.onError(null, false)
111 | return true
112 | }
113 |
114 | private fun goToHome() {
115 | dismissProgressDialog()
116 | val intent = HomeActivity.newIntent(this).apply {
117 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
118 | }
119 | startActivity(intent)
120 | }
121 |
122 | private fun goToVerificationEmailScreen() {
123 | dismissProgressDialog()
124 | EmailVerificationActivity.startActivity(this, sessionStore.state.loggedUser!!.email)
125 | overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
126 | }
127 |
128 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
129 | super.onActivityResult(requestCode, resultCode, data)
130 | manageGoogleResult(requestCode, data)
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/session/VerifyEmailActivity.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.session
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import frangsierra.kotlinfirechat.home.HomeActivity
7 | import frangsierra.kotlinfirechat.R
8 | import frangsierra.kotlinfirechat.core.errors.ErrorHandler
9 | import frangsierra.kotlinfirechat.core.flux.FluxActivity
10 | import frangsierra.kotlinfirechat.session.store.SendVerificationEmailAction
11 | import frangsierra.kotlinfirechat.session.store.SessionStore
12 | import frangsierra.kotlinfirechat.session.store.SignOutAction
13 | import frangsierra.kotlinfirechat.session.store.VerifyUserEmailAction
14 | import frangsierra.kotlinfirechat.util.dismissProgressDialog
15 | import frangsierra.kotlinfirechat.util.filterOne
16 | import frangsierra.kotlinfirechat.util.showProgressDialog
17 | import frangsierra.kotlinfirechat.util.toast
18 | import kotlinx.android.synthetic.main.verification_email_activity.*
19 | import javax.inject.Inject
20 |
21 | class EmailVerificationActivity : FluxActivity() {
22 |
23 | @Inject
24 | lateinit var sessionStore: SessionStore
25 |
26 | @Inject
27 | lateinit var errorHandler: ErrorHandler
28 |
29 | private lateinit var verificationEmail: String
30 |
31 | companion object {
32 | val VERIFICATION_EMAIL = "verification_email"
33 | fun startActivity(context: Context, email: String) {
34 | val intent = Intent(context, EmailVerificationActivity::class.java)
35 | .apply {
36 | putExtra(VERIFICATION_EMAIL, email)
37 | }
38 | context.startActivity(intent)
39 | }
40 | }
41 |
42 | override fun onCreate(savedInstanceState: Bundle?) {
43 | super.onCreate(savedInstanceState)
44 | setContentView(R.layout.verification_email_activity)
45 | if (!intent.hasExtra(VERIFICATION_EMAIL)) {
46 | throw IllegalAccessException("verification email needed")
47 | }
48 | verificationEmail = intent.getStringExtra(VERIFICATION_EMAIL)
49 | initializeInterface()
50 | }
51 |
52 | private fun initializeInterface() {
53 | confirm_email_next_button.setOnClickListener { verifyUserEmail() }
54 | confirm_email_re_send_button.setOnClickListener { sendVerificationEmailAction() }
55 | }
56 |
57 | private fun verifyUserEmail() {
58 | confirm_email_next_button.isEnabled = false
59 | showProgressDialog("Verifying")
60 | dispatcher.dispatchOnUi(VerifyUserEmailAction())
61 | sessionStore.flowable()
62 | .filterOne { it.verifyUserTask.isTerminal() } //Wait for request to finish
63 | .subscribe {
64 | if (it.verifyUserTask.isSuccessful() && it.loggedUser != null && it.verified) {
65 | goHome()
66 | } else {
67 | toast("Your account hasn´t been confirmed yet")
68 | }
69 | confirm_email_next_button.isEnabled = true
70 | dismissProgressDialog()
71 | }.track()
72 | }
73 |
74 | private fun sendVerificationEmailAction() {
75 | showProgressDialog("Sending notification email")
76 | dispatcher.dispatch(SendVerificationEmailAction())
77 | sessionStore.flowable()
78 | .filterOne { it.verificationEmailTask.isTerminal() } //Wait for request to finish
79 | .subscribe {
80 | if (it.verificationEmailTask.isSuccessful()) {
81 | toast("We have sent a verification email to your address")
82 | } else {
83 | it.verificationEmailTask.error?.let {
84 | toast(errorHandler.getMessageForError(it))
85 | }
86 | }
87 | dismissProgressDialog()
88 | }.track()
89 | }
90 |
91 | private fun goHome() {
92 | dismissProgressDialog()
93 | val intent = HomeActivity.newIntent(this).apply {
94 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
95 | }
96 | startActivity(intent)
97 | }
98 |
99 | override fun onBackPressed() {
100 | dispatcher.dispatchOnUi(SignOutAction())
101 | }
102 |
103 | override fun finish() {
104 | super.finish()
105 | overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
106 | }
107 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/session/controller/SessionController.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.session.controller
2 |
3 | import com.google.android.gms.tasks.Tasks
4 | import com.google.firebase.auth.*
5 | import frangsierra.kotlinfirechat.core.dagger.AppScope
6 | import frangsierra.kotlinfirechat.core.flux.doAsync
7 | import frangsierra.kotlinfirechat.session.model.*
8 | import frangsierra.kotlinfirechat.session.store.CreateAccountCompleteAction
9 | import frangsierra.kotlinfirechat.session.store.LoginCompleteAction
10 | import frangsierra.kotlinfirechat.session.store.VerificationEmailSentAction
11 | import frangsierra.kotlinfirechat.session.store.VerifyUserEmailCompleteAction
12 | import frangsierra.kotlinfirechat.util.taskFailure
13 | import frangsierra.kotlinfirechat.util.taskSuccess
14 | import mini.Dispatcher
15 | import javax.inject.Inject
16 |
17 | interface SessionController {
18 |
19 | /**
20 | * To control the splash, when the app is launched try to make a first login.
21 | */
22 | fun tryToLoginInFirstInstance()
23 |
24 | /**
25 | * Try to signIn in the Firebase Auth API with a email and a password, returning an [AuthResult]
26 | * if it's successful or a [FirebaseAuthException] if fails.
27 | *
28 | * @param email email of the user
29 | * @param password password introduced
30 | */
31 | fun loginWithCredentials(email: String, password: String)
32 |
33 | /**
34 | * Call to the server to create a new user account using a email and a password.
35 | *
36 | * @param email email of the user
37 | * @param password password introduced
38 | * @param username username for the user
39 | */
40 | fun createAccountWithCredentials(email: String, password: String, username: String)
41 |
42 | /**
43 | * Try to signIn in the Firebase Auth API with a credential, returning an [AuthResult]
44 | * if it's successful, a [ProviderNotLinkedException] if the user is trying to login without register first
45 | * the credential, or an [FirebaseAuthException] if fails.
46 | *
47 | * @param credential the [AuthCredential] with all the info of the provider
48 | * @param email email of the user
49 | */
50 | fun loginWithProviderCredentials(credential: AuthCredential, email: String)
51 |
52 | /**
53 | * Call to the server to create a new user account using a [AuthCredential] from a provider.
54 | *
55 | * @param credential the [AuthCredential] with all the info of the provider
56 | * @param userData data of the user
57 | */
58 | fun createAccountWithProviderCredentials(credential: AuthCredential, userData: User)
59 |
60 | /**
61 | * Send a verification email to the user logged in the auth instance.
62 | */
63 | fun sendVerificationEmail()
64 |
65 | /**
66 | * Sign out the firebase auth instance.
67 | */
68 | fun signOut()
69 |
70 | /**
71 | * Refresh the instance of the [FirebaseUser] with all the updated data from the server to verify the email.
72 | */
73 | fun verifyUser()
74 | }
75 |
76 | @AppScope
77 | class SessionControllerImpl @Inject constructor(private val authInstance: FirebaseAuth,
78 | private val dispatcher: Dispatcher) : SessionController {
79 | override fun tryToLoginInFirstInstance() {
80 | authInstance.addAuthStateListener { firebaseAuth ->
81 | doAsync {
82 | if (firebaseAuth.currentUser == null || !firebaseAuth.currentUser!!.isEmailVerified)
83 | dispatcher.dispatchOnUi(LoginCompleteAction(task = taskFailure()))
84 | else {
85 | val currentUser = firebaseAuth.currentUser!!
86 | dispatcher.dispatchOnUi(LoginCompleteAction(task = taskSuccess(),
87 | user = currentUser.toUser(),
88 | associatedProviders = currentUser.associatedProviders()))
89 |
90 | }
91 | }
92 | }
93 | }
94 |
95 | override fun loginWithCredentials(email: String, password: String) {
96 | doAsync {
97 | try {
98 | val result = Tasks.await(authInstance.signInWithEmailAndPassword(email, password))
99 | val emailVerified = result.user.isEmailVerified
100 | val user = result.user.toUser()
101 | val providers = result.user.associatedProviders()
102 | dispatcher.dispatchOnUi(LoginCompleteAction(
103 | user = user,
104 | emailVerified = emailVerified,
105 | task = taskSuccess(),
106 | associatedProviders = providers))
107 | } catch (e: Throwable) {
108 | dispatcher.dispatchOnUi(LoginCompleteAction(user = null, task = taskFailure(e)))
109 | }
110 | }
111 | }
112 |
113 | override fun createAccountWithCredentials(email: String, password: String, username: String) {
114 | doAsync {
115 | try {
116 | val result = Tasks.await(authInstance.createUserWithEmailAndPassword(email, password))
117 | val firebaseUser = result.user
118 | val emailVerified = firebaseUser.isEmailVerified
119 | val user = firebaseUser.toUser().copy(username = username)
120 | val providers = firebaseUser.associatedProviders()
121 | dispatcher.dispatchOnUi(CreateAccountCompleteAction(
122 | user = user,
123 | emailVerified = emailVerified,
124 | task = taskSuccess(),
125 | associatedProviders = providers))
126 | if (!emailVerified) sendVerificationEmailToUser(firebaseUser)
127 | } catch (e: Throwable) {
128 | dispatcher.dispatchOnUi(LoginCompleteAction(user = null, task = taskFailure(e)))
129 | }
130 | }
131 | }
132 |
133 | override fun loginWithProviderCredentials(credential: AuthCredential, email: String) {
134 | doAsync {
135 | try {
136 | val providerResults = Tasks.await(authInstance.fetchSignInMethodsForEmail(email))
137 | val isNewAccount = providerResults.signInMethods!!.contains((credential.provider))
138 | if (isNewAccount) {
139 | val authResult = Tasks.await(authInstance.signInWithCredential(credential))
140 | val emailVerified = authResult.user.isEmailVerified
141 | val user = authResult.user.toUser()
142 | val providers = authResult.user.associatedProviders()
143 | dispatcher.dispatchOnUi(LoginCompleteAction(
144 | user = user,
145 | emailVerified = emailVerified,
146 | task = taskSuccess(),
147 | associatedProviders = providers))
148 | if (!emailVerified) sendVerificationEmailToUser(authResult.user)
149 | } else {
150 | dispatcher.dispatchOnUi(LoginCompleteAction(user = null, task = taskFailure(ProviderNotLinkedException(credential.provider))))
151 | }
152 | } catch (e: Throwable) {
153 | dispatcher.dispatchOnUi(LoginCompleteAction(user = null, task = taskFailure(e)))
154 | }
155 | }
156 | }
157 |
158 | override fun createAccountWithProviderCredentials(credential: AuthCredential, userData: User) {
159 | doAsync {
160 | try {
161 | val providerResults = Tasks.await(authInstance.fetchSignInMethodsForEmail(userData.email))
162 | val isNewAccount = providerResults.signInMethods!!.contains((credential.provider))
163 | if (isNewAccount) {
164 | val authResult = Tasks.await(authInstance.signInWithCredential(credential))
165 | val emailVerified = authResult.user.isEmailVerified
166 | val user = authResult.user.toUser().copy(photoUrl = userData.photoUrl)
167 | val providers = authResult.user.associatedProviders()
168 | dispatcher.dispatchOnUi(CreateAccountCompleteAction(
169 | user = user,
170 | alreadyExisted = !isNewAccount,
171 | emailVerified = emailVerified,
172 | task = taskSuccess(),
173 | associatedProviders = providers))
174 | if (!emailVerified) sendVerificationEmailToUser(authResult.user)
175 | } else {
176 | dispatcher.dispatchOnUi(CreateAccountCompleteAction(user = null, task = taskFailure(ProviderNotLinkedException(credential.provider))))
177 | }
178 | } catch (e: Throwable) {
179 | dispatcher.dispatchOnUi(CreateAccountCompleteAction(user = null, task = taskFailure(e)))
180 | }
181 | }
182 | }
183 |
184 | override fun sendVerificationEmail() {
185 | if (authInstance.currentUser == null) {
186 | dispatcher.dispatchOnUi(VerificationEmailSentAction(taskFailure(FirebaseUserNotFound())))
187 | return
188 | }
189 | sendVerificationEmailToUser(authInstance.currentUser!!)
190 | }
191 |
192 | override fun signOut() {
193 | authInstance.signOut()
194 | }
195 |
196 | override fun verifyUser() {
197 | if (authInstance.currentUser == null) {
198 | dispatcher.dispatchOnUi(VerifyUserEmailCompleteAction(taskFailure(FirebaseUserNotFound()),
199 | verified = false))
200 | return
201 | }
202 |
203 | authInstance.currentUser!!.reload() //send a verification email needs to have a recent user instance. We need to reload it to avoid errors
204 | .addOnCompleteListener { reloadTask ->
205 | //After the reload the currentUser can be null because it takes some time in be updated
206 | if (reloadTask.isSuccessful && authInstance.currentUser != null) {
207 | val user = authInstance.currentUser!!
208 | dispatcher.dispatchOnUi(VerifyUserEmailCompleteAction(task = taskSuccess(), verified = user.isEmailVerified))
209 | } else dispatcher.dispatchOnUi(VerifyUserEmailCompleteAction(task = taskFailure(reloadTask.exception)))
210 | }
211 | }
212 |
213 | private fun sendVerificationEmailToUser(user: FirebaseUser) {
214 | doAsync {
215 | try {
216 | Tasks.await(user.reload())
217 | Tasks.await(user.sendEmailVerification())
218 | dispatcher.dispatchOnUi(VerificationEmailSentAction(taskSuccess()))
219 | } catch (e: Throwable) {
220 | dispatcher.dispatchOnUi(VerificationEmailSentAction(task = taskFailure(e)))
221 | }
222 | }
223 | }
224 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/session/model/LoginProvider.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.session.model
2 |
3 | /**
4 | * Represent the possible providers used in the application.
5 | */
6 | enum class LoginProvider(private val providerName: String) {
7 | FIREBASE("firebase"),
8 | PASSWORD("password"),
9 | GOOGLE("google.com");
10 |
11 | companion object {
12 | fun withValue(providerName: String) = LoginProvider.values().first { it.providerName == providerName }
13 | }
14 |
15 | fun value(): String = this.providerName
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/session/model/SessionExceptions.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.session.model
2 |
3 | class FirebaseUserNotFound : Exception() {
4 | override val message: String = "There is no authenticated user connected to this auth instance"
5 | }
6 |
7 | class ProviderNotLinkedException(provider: String) : Exception() {
8 | override val message: String = "The provider $provider is not linked to the given account"
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/session/model/User.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.session.model
2 |
3 | import com.google.firebase.auth.FirebaseUser
4 | import com.google.firebase.firestore.ServerTimestamp
5 | import java.util.*
6 |
7 | data class User(val uid: String,
8 | val username: String,
9 | val photoUrl: String?,
10 | val email: String)
11 |
12 | fun FirebaseUser.toUser(): User = User(
13 | uid = uid,
14 | username = displayName ?: "Anonymous",
15 | photoUrl = photoUrl?.toString(),
16 | email = email!!)
17 |
18 | fun FirebaseUser.associatedProviders(): List =
19 | providerData.mapNotNull { userInfo ->
20 | LoginProvider.values().firstOrNull { it.value() == userInfo.providerId }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/session/store/SessionActions.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.session.store
2 |
3 | import com.google.firebase.auth.AuthCredential
4 | import frangsierra.kotlinfirechat.session.model.LoginProvider
5 | import frangsierra.kotlinfirechat.session.model.User
6 | import frangsierra.kotlinfirechat.util.Task
7 | import mini.Action
8 |
9 | /**
10 | * Action dispatched to sign out the current account and reset all the data.
11 | */
12 | class SignOutAction : Action
13 |
14 | /**
15 | * Action dispatched to refresh the user data.
16 | */
17 | class RefreshUserAction : Action
18 |
19 | /**
20 | * Action dispatched when the user token is refresheed.
21 | */
22 | class UserRefreshedCompleteAction(val task: Task,
23 | val user: User?,
24 | val associatedProviders: List = listOf()) : Action
25 |
26 | /**
27 | * Action dispatched to start the process for verify the email of the current user.
28 | */
29 | class VerifyUserEmailAction : Action
30 |
31 | /**
32 | * Action dispatched when the verification refresh is complete.
33 | */
34 | class VerifyUserEmailCompleteAction(val task: Task,
35 | val verified: Boolean = false) : Action
36 |
37 | /**
38 | * Action dispatched to reset the password of the current user.
39 | */
40 | data class ResetPasswordAction(val email: String) : Action
41 |
42 | /**
43 | * Action dispatched when the reset password email as been sent.
44 | */
45 | data class ResetPasswordEmailSentAction(val task: Task) : Action
46 |
47 | /**
48 | * Action dispatched to send a verification email when a new account is created or a
49 | * email provider is attached to an user.
50 | */
51 | class SendVerificationEmailAction : Action
52 |
53 | /**
54 | * Action dispatched when the verification email has been sent.
55 | */
56 | data class VerificationEmailSentAction(val task: Task) : Action
57 |
58 | /**
59 | * Action dispatched on Appstart to try to log the user if the credentials still on the cache.
60 | */
61 | class TryToLoginInFirstInstanceAction : Action
62 |
63 | /**
64 | * Action dispatched to login in the app with an email and a password.
65 | */
66 | data class LoginWithCredentials(val email: String, val password: String) : Action
67 |
68 | /**
69 | * Action dispatched to login with an external provider credential.
70 | */
71 | data class LoginWithProviderCredentials(val credential: AuthCredential,
72 | val email: String) : Action
73 |
74 | /**
75 | * Action dispatched when login process as finished.
76 | */
77 | data class LoginCompleteAction(val user: User? = null,
78 | val emailVerified: Boolean = false,
79 | val task: Task,
80 | val associatedProviders: List = listOf()) : Action
81 |
82 | /**
83 | * Action dispatched to create an account with an user, password and a new username.
84 | */
85 | data class CreateAccountWithCredentialsAction(val email: String,
86 | val password: String,
87 | val username: String) : Action
88 |
89 | /**
90 | * Action dispatched to create an account with an external provider credential.
91 | */
92 | data class CreateAccountWithProviderCredentialsAction(val credential: AuthCredential,
93 | val user: User) : Action
94 |
95 | /**
96 | * Action dispatched when the create account process as finished.
97 | */
98 | data class CreateAccountCompleteAction(val user: User?,
99 | val alreadyExisted: Boolean = false,
100 | val emailVerified: Boolean = false,
101 | val task: Task,
102 | val associatedProviders: List = listOf()) : Action
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/session/store/SessionState.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.session.store
2 |
3 | import frangsierra.kotlinfirechat.session.model.LoginProvider
4 | import frangsierra.kotlinfirechat.session.model.User
5 | import frangsierra.kotlinfirechat.util.Task
6 |
7 | data class SessionState(val verified: Boolean = false,
8 | val createAccountTask: Task = Task(),
9 | val loginTask: Task = Task(),
10 | val verifyUserTask: Task = Task(),
11 | val verificationEmailTask: Task = Task(),
12 | val refreshUserTask: Task = Task(),
13 | val providers: List = listOf(),
14 | val loggedUser: User? = null)
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/session/store/SessionStore.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.session.store
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.multibindings.ClassKey
6 | import dagger.multibindings.IntoMap
7 | import frangsierra.kotlinfirechat.core.dagger.AppScope
8 | import frangsierra.kotlinfirechat.core.flux.prefs
9 | import frangsierra.kotlinfirechat.session.controller.SessionController
10 | import frangsierra.kotlinfirechat.session.controller.SessionControllerImpl
11 | import frangsierra.kotlinfirechat.util.taskRunning
12 | import mini.Reducer
13 | import mini.Store
14 | import javax.inject.Inject
15 |
16 | @AppScope
17 | class SessionStore @Inject constructor(val controller: SessionController) : Store() {
18 |
19 | @Reducer
20 | fun loginWithCredentials(action: LoginWithCredentials): SessionState {
21 | if (state.loginTask.isRunning()) return state
22 | controller.loginWithCredentials(action.email, action.password)
23 | return state.copy(loginTask = taskRunning())
24 | }
25 |
26 | @Reducer
27 | fun loginWithProvider(action: LoginWithProviderCredentials): SessionState {
28 | if (state.loginTask.isRunning()) return state
29 | controller.loginWithProviderCredentials(action.credential, action.email)
30 | return state.copy(loginTask = taskRunning())
31 | }
32 |
33 | @Reducer(priority = 10)
34 | fun loginComplete(action: LoginCompleteAction): SessionState {
35 | if (!state.loginTask.isRunning()) return state
36 | prefs.loggedUserId = action.user?.uid //Update the shared prefs of the current user
37 | prefs.loggedUsername = action.user?.username //Update the shared prefs of the current user
38 | return state.copy(
39 | loggedUser = action.user,
40 | loginTask = action.task,
41 | verified = action.emailVerified,
42 | providers = action.associatedProviders)
43 | }
44 |
45 | @Reducer
46 | fun createAccountWithCredentials(action: CreateAccountWithCredentialsAction): SessionState {
47 | if (state.createAccountTask.isRunning()) return state
48 | controller.createAccountWithCredentials(action.email, action.password, action.username)
49 | return state.copy(createAccountTask = taskRunning())
50 | }
51 |
52 | @Reducer
53 | fun createAccountWithProvider(action: CreateAccountWithProviderCredentialsAction): SessionState {
54 | if (state.createAccountTask.isRunning()) return state
55 | controller.createAccountWithProviderCredentials(action.credential, action.user)
56 | return state.copy(createAccountTask = taskRunning())
57 | }
58 |
59 | @Reducer
60 | fun createAccountComplete(action: CreateAccountCompleteAction): SessionState {
61 | if (!state.createAccountTask.isRunning()) return state
62 | if (action.task.isSuccessful()) {
63 | prefs.loggedUserId = action.user!!.uid //Update the shared prefs of the current user
64 | prefs.loggedUsername = action.user.username //Update the shared prefs of the current user
65 | }
66 | return state.copy(loggedUser = action.user, createAccountTask = action.task,
67 | verified = action.emailVerified, providers = action.associatedProviders)
68 | }
69 |
70 | @Reducer
71 | fun verifyUserEmail(action: VerifyUserEmailAction): SessionState {
72 | if (state.verifyUserTask.isRunning()) return state
73 | controller.verifyUser()
74 | return state.copy(verifyUserTask = taskRunning())
75 | }
76 |
77 | @Reducer
78 | fun userEmailVerified(action: VerifyUserEmailCompleteAction): SessionState {
79 | if (!state.verifyUserTask.isRunning()) return state
80 | return state.copy(verifyUserTask = action.task, verified = action.verified)
81 | }
82 |
83 | @Reducer
84 | fun sendVerificationEmail(action: SendVerificationEmailAction): SessionState {
85 | if (state.verificationEmailTask.isRunning()) return state
86 | controller.sendVerificationEmail()
87 | return state.copy(verificationEmailTask = taskRunning())
88 | }
89 |
90 | @Reducer
91 | fun verificationEmailSent(action: VerificationEmailSentAction): SessionState {
92 | if (!state.verificationEmailTask.isRunning()) return state
93 | return state.copy(verificationEmailTask = action.task)
94 | }
95 |
96 | @Reducer
97 | fun tryToLoginOnFirstInstance(action: TryToLoginInFirstInstanceAction): SessionState {
98 | if (state.loginTask.isRunning()) return state
99 | controller.tryToLoginInFirstInstance()
100 | return state.copy(loginTask = taskRunning())
101 | }
102 |
103 | @Reducer
104 | fun signOut(action: SignOutAction): SessionState {
105 | controller.signOut()
106 | prefs.loggedUserId = null //Update the shared prefs of the current user
107 | prefs.loggedUsername = null //Update the shared prefs of the current user
108 | return initialState()
109 | }
110 | }
111 |
112 | @Module
113 | abstract class SessionModule {
114 | @Binds
115 | @AppScope
116 | @IntoMap
117 | @ClassKey(SessionStore::class)
118 | abstract fun provideSessionStore(store: SessionStore): Store<*>
119 |
120 | @Binds
121 | @AppScope
122 | abstract fun bindSessionController(impl: SessionControllerImpl): SessionController
123 | }
124 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/AlertDialogExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2017 Mundo Reader S.L.
3 | * All Rights Reserved.
4 | * Confidential and Proprietary - Mundo Reader S.L.
5 | */
6 |
7 | package frangsierra.kotlinfirechat.util
8 |
9 | import android.content.DialogInterface
10 | import android.support.annotation.StringRes
11 | import android.support.v7.app.AlertDialog
12 | import android.view.View
13 |
14 |
15 | /**
16 | * Generate a custom dialog with the given values. It works with resources ids or strings.
17 | * @param title value for the dialog title.
18 | * @param titleId resource id for the dialog title.
19 | * @param content value for the dialog content.
20 | * @param contentId resource id for the dialog content.
21 | * @param view custom view to be added to the dialog.
22 | * @param positiveButton value for the dialog positive button text.
23 | * @param positiveButtonId resource id for the dialog positive button text.
24 | * @param positiveFunction function invoked when the user click in the positive button.
25 | * @param negativeButton value for the dialog negative button text.
26 | * @param negativeButtonId resource id for the dialog negative button text.
27 | * @param negativeFunction function invoked when the user click in the negative button.
28 | *
29 | * @throws IllegalArgumentException if and resource id and a String for the same value are used.
30 | */
31 | @Suppress("LongParameterList")
32 | fun AlertDialog.Builder.createCustomAlertDialog(@StringRes titleId: Int? = null, title: String? = null,
33 | @StringRes contentId: Int? = null, content: String? = null,
34 | @StringRes positiveButtonId: Int? = null, positiveButton: String? = null,
35 | @StringRes negativeButtonId: Int? = null, negativeButton: String? = null,
36 | @StringRes neutralButtonId: Int? = null, neutralButton: String? = null,
37 | view: View? = null,
38 | positiveFunction: (dialog: DialogInterface?) -> Unit = {},
39 | negativeFunction: (dialog: DialogInterface?) -> Unit = { it!!.dismiss() },
40 | neutralFunction: (dialog: DialogInterface?) -> Unit = {},
41 | cancelable: Boolean = true): AlertDialog {
42 |
43 | // Check if there is not duplicated values
44 | if (titleId != null && title != null)
45 | throw IllegalArgumentException("Can't use two different sources for title")
46 | if (contentId != null && content != null)
47 | throw IllegalArgumentException("Can't use two different sources for content")
48 | if (positiveButtonId != null && positiveButton != null)
49 | throw IllegalArgumentException("Can't use two different sources for positive button")
50 | if (negativeButtonId != null && negativeButton != null)
51 | throw IllegalArgumentException("Can't use two different sources for negative button")
52 | if (neutralButtonId != null && neutralButton != null)
53 | throw IllegalArgumentException("Can't use two different sources for neutral button")
54 | // Set up the input title
55 | titleId?.let { setTitle(it) }
56 | title?.let { setTitle(it) }
57 | // Set up content
58 | content?.let { setMessage(it) }
59 | contentId?.let { setMessage(it) }
60 | // Set up view
61 | view?.let { setView(it) }
62 | // Set up the buttons
63 | positiveButton?.let { setPositiveButton(it, { dialog, _ -> positiveFunction.invoke(dialog) }) }
64 | positiveButtonId?.let { setPositiveButton(it, { dialog, _ -> positiveFunction.invoke(dialog) }) }
65 | negativeButton?.let { setNegativeButton(it) { dialog, _ -> negativeFunction.invoke(dialog) } }
66 | negativeButtonId?.let { setNegativeButton(it) { dialog, _ -> negativeFunction.invoke(dialog) } }
67 | neutralButton?.let { setNeutralButton(it) { dialog, _ -> neutralFunction.invoke(dialog) } }
68 | neutralButtonId?.let { setNeutralButton(it) { dialog, _ -> neutralFunction.invoke(dialog) } }
69 | setCancelable(cancelable)
70 | return create()
71 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/AndroidUtils.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.util
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.DialogInterface
6 | import android.content.Intent
7 | import android.net.Uri
8 | import android.os.Environment
9 | import android.provider.MediaStore
10 | import android.support.v4.app.Fragment
11 | import android.support.v4.content.FileProvider
12 | import android.support.v7.app.AlertDialog
13 | import frangsierra.kotlinfirechat.R
14 | import java.io.File
15 |
16 | object AndroidUtils {
17 | private val INTENT_TARGET_TYPE: String = "image/*"
18 | private val APP_IMAGE_FOLDER_PATH: String = "kotlin_chat/"
19 | val TC_REQUEST_GALLERY: Int = 101
20 | val TC_REQUEST_CAMERA: Int = 102
21 | val FORMAT_JPG = ".jpg"
22 |
23 | /**
24 | * Create an alertdialog which allows the user to choose between pick an image from the gallery or
25 | * take a new one with his camera.
26 | */
27 | fun showImageIntentDialog(activity: Activity, outputFileUri: Uri) {
28 | val builder: AlertDialog.Builder = AlertDialog.Builder(activity)
29 | builder.setTitle(activity.getString(R.string.choose_image_picker_text))
30 | builder.setItems(arrayOf(activity.getString(R.string.gallery_text), activity.getString(R.string.camera_text))) { _, which ->
31 | when (which) {
32 | 0 -> {
33 | // GET IMAGE FROM THE GALLERY
34 | val chooser = Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
35 | type = INTENT_TARGET_TYPE
36 | }, activity.getString(R.string.choose_image_picture_text))
37 | activity.startActivityForResult(chooser, TC_REQUEST_GALLERY)
38 | }
39 | 1 -> {
40 | val cameraFolder: File = if (Environment.getExternalStorageState() == android.os.Environment.MEDIA_MOUNTED)
41 | File(Environment.getExternalStorageDirectory(), APP_IMAGE_FOLDER_PATH)
42 | else activity.cacheDir
43 |
44 | if (!cameraFolder.exists())
45 | cameraFolder.mkdirs()
46 |
47 | val getCameraImage = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
48 | getCameraImage.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri)
49 |
50 | activity.startActivityForResult(getCameraImage, TC_REQUEST_CAMERA)
51 | }
52 | }
53 | }
54 | builder.show()
55 | }
56 |
57 | /**
58 | * Create an alertdialog which allows the user to choose between pick an image from the gallery or
59 | * take a new one with his camera.
60 | */
61 | fun showImageIntentDialogFromFragment(fragment: Fragment, outputFileUri: Uri, cancelListener: (DialogInterface) -> Unit = {}) {
62 | val builder: AlertDialog.Builder = AlertDialog.Builder(fragment.activity!!)
63 | builder.setTitle(fragment.getString(R.string.choose_image_picker_text))
64 | builder.setOnCancelListener { cancelListener(it) }
65 | builder.setCancelable(true)
66 | builder.setItems(arrayOf(fragment.getString(R.string.gallery_text), fragment.getString(R.string.camera_text))) { _, which ->
67 | when (which) {
68 | 0 -> {
69 | // GET IMAGE FROM THE GALLERY
70 | val chooser = Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
71 | type = INTENT_TARGET_TYPE
72 | }, fragment.getString(R.string.choose_image_picture_text))
73 | fragment.startActivityForResult(chooser, TC_REQUEST_GALLERY)
74 | }
75 | 1 -> {
76 | val cameraFolder: File = if (Environment.getExternalStorageState() == android.os.Environment.MEDIA_MOUNTED)
77 | File(Environment.getExternalStorageDirectory(), APP_IMAGE_FOLDER_PATH)
78 | else fragment.activity!!.cacheDir
79 |
80 | if (!cameraFolder.exists())
81 | cameraFolder.mkdirs()
82 |
83 | val getCameraImage = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
84 | getCameraImage.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri)
85 |
86 | fragment.startActivityForResult(getCameraImage, TC_REQUEST_CAMERA)
87 | }
88 | }
89 | }
90 | builder.show()
91 | }
92 |
93 | fun generateUniqueFireUri(context: Context): Uri {
94 | // Determine Uri of camera image to save.
95 | val root = File(context.filesDir, "firechat")
96 | root.mkdirs()
97 | val fileName = "${System.currentTimeMillis()}$FORMAT_JPG"
98 | val newFile = File(root, fileName)
99 | return FileProvider.getUriForFile(context, "frangsierra.kotlinfirechat.fileprovider", newFile)
100 | }
101 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/GlideExtensions.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.util
2 |
3 | import android.graphics.drawable.Drawable
4 | import android.support.annotation.DrawableRes
5 | import android.widget.ImageView
6 | import com.bumptech.glide.Glide
7 | import com.bumptech.glide.load.DataSource
8 | import com.bumptech.glide.load.engine.GlideException
9 | import com.bumptech.glide.request.RequestListener
10 | import com.bumptech.glide.request.RequestOptions
11 | import com.bumptech.glide.request.target.Target
12 |
13 |
14 | fun ImageView.setCircularImage(url: String?) {
15 | val requestOptions = RequestOptions
16 | .circleCropTransform()
17 | .override(width, height)
18 |
19 | Glide.with(context)
20 | .asDrawable()
21 | .apply(requestOptions)
22 | .into(this)
23 | .waitForLayout()
24 | .clearOnDetach()
25 | }
26 |
27 | fun ImageView.setCircularImage(@DrawableRes drawableRes: Int) {
28 | val requestOptions = RequestOptions
29 | .circleCropTransform()
30 | .override(width, height)
31 |
32 | Glide.with(context)
33 | .asDrawable()
34 | .load(drawableRes)
35 | .apply(requestOptions)
36 | .into(this)
37 | .waitForLayout()
38 | .clearOnDetach()
39 | }
40 |
41 | fun ImageView.setThumnailImage(url: String) {
42 | val requestOptions = RequestOptions
43 | .centerCropTransform()
44 | .override(width, height)
45 |
46 | Glide.with(context)
47 | .asDrawable()
48 | .load(url)
49 | .apply(requestOptions)
50 | .into(this)
51 | .waitForLayout()
52 | .clearOnDetach()
53 | }
54 |
55 | fun ImageView.setImage(url: String?, onLoadError: () -> Unit = {}) {
56 | val requestOptions = RequestOptions
57 | .centerCropTransform()
58 | .override(width, height)
59 |
60 | Glide.with(context)
61 | .load(url)
62 | .apply(requestOptions)
63 | .listener(object : RequestListener {
64 | override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean {
65 | onLoadError()
66 | return true
67 | }
68 |
69 | override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean = false
70 | })
71 | .thumbnail(0.1f)
72 | .into(this)
73 | .waitForLayout()
74 | .clearOnDetach()
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/GoogleLoginCallback.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.util
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import com.google.android.gms.auth.api.signin.GoogleSignIn
6 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount
7 | import com.google.android.gms.auth.api.signin.GoogleSignInClient
8 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions
9 | import com.google.android.gms.common.api.ApiException
10 | import com.google.firebase.auth.AuthCredential
11 | import com.google.firebase.auth.GoogleAuthProvider
12 | import mini.Grove
13 |
14 | const val RC_SIGN_IN = 1001
15 |
16 | interface GoogleLoginCallback {
17 | val googleApiClient: GoogleSignInOptions
18 | val googleSingInClient: GoogleSignInClient
19 |
20 | fun logInWithGoogle(activity: Activity) {
21 | activity.startActivityForResult(googleSingInClient.signInIntent, RC_SIGN_IN)
22 | }
23 |
24 | fun manageGoogleResult(requestCode: Int, data: Intent?) {
25 | // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
26 | if (requestCode == RC_SIGN_IN) {
27 | val task = GoogleSignIn.getSignedInAccountFromIntent(data)
28 | try {
29 | val account = task.getResult(ApiException::class.java)
30 | val credential = GoogleAuthProvider.getCredential(account.idToken, null)
31 | onGoogleCredentialReceived(credential, account)
32 | } catch (e: ApiException) {
33 | Grove.w { "Google sign in failed with error ${e.message}" }
34 | onGoogleSignInFailed(e)
35 | }
36 |
37 | }
38 | }
39 |
40 | fun onGoogleCredentialReceived(credential: AuthCredential, account: GoogleSignInAccount)
41 |
42 | fun onGoogleSignInFailed(e: ApiException)
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/GoogleSignInApiUtils.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.util
2 |
3 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount
4 | import frangsierra.kotlinfirechat.session.model.User
5 |
6 | object GoogleSignInApiUtils {
7 |
8 | private const val GOOGLE_ACCOUNT_DEFAULT_IMAGE_SIZE = "s96-c"
9 | private const val GOOGLE_ACCOUNT_DESIRED_IMAGE_SIZE = "s600-c"
10 |
11 | fun getUserData(account: GoogleSignInAccount): User {
12 | return User(email = account.email!!,
13 | photoUrl = retrieveResizedGoogleAccountPicture(account),
14 | username = "${account.displayName}",
15 | uid = "")
16 | }
17 |
18 | private fun retrieveResizedGoogleAccountPicture(account: GoogleSignInAccount): String? =
19 | account.photoUrl?.toString()?.replace(GOOGLE_ACCOUNT_DEFAULT_IMAGE_SIZE, GOOGLE_ACCOUNT_DESIRED_IMAGE_SIZE, true)
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/ImageUtils.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.util
2 |
3 | import android.graphics.Bitmap.CompressFormat
4 | import android.graphics.BitmapFactory
5 | import android.net.Uri
6 | import frangsierra.kotlinfirechat.core.flux.app
7 | import io.reactivex.Single
8 | import java.io.ByteArrayOutputStream
9 | import java.io.FileNotFoundException
10 |
11 | object ImageUtils {
12 | private val MAX_THUMB_SIZE = 150
13 | private val POST_MAX_IMAGE_SIZE = 1080
14 |
15 | fun resizeUriToThumbnail(uri: Uri, isUrl: Boolean = false): Single {
16 | return Single.create { emitter ->
17 | try {
18 | val o = BitmapFactory.Options()
19 | o.inJustDecodeBounds = true
20 |
21 | val stream = if (isUrl) java.net.URL(uri.toString()).openStream() else app.contentResolver.openInputStream(uri)
22 | BitmapFactory.decodeStream(stream, null, o)
23 |
24 | var width_tmp = o.outWidth
25 | var height_tmp = o.outHeight
26 | var scale = 1
27 |
28 | while (true) {
29 | if (width_tmp / 2 < MAX_THUMB_SIZE || height_tmp / 2 < MAX_THUMB_SIZE)
30 | break
31 | width_tmp /= 2
32 | height_tmp /= 2
33 | scale *= 2
34 | }
35 |
36 | val o2 = BitmapFactory.Options()
37 | o2.inSampleSize = scale
38 | val resizedStream = if (isUrl) java.net.URL(uri.toString()).openStream() else app.contentResolver.openInputStream(uri)
39 | val decodedResizedBitmap = BitmapFactory.decodeStream(resizedStream, null, o2)
40 | val bos = ByteArrayOutputStream()
41 | decodedResizedBitmap.compress(CompressFormat.PNG, 0 /*ignored for PNG*/, bos)
42 | emitter.onSuccess(bos.toByteArray())
43 | } catch (e: FileNotFoundException) {
44 | emitter.onError(e)
45 | }
46 | }
47 | }
48 |
49 | fun resizeUriToPostImage(uri: Uri, isUrl: Boolean = false): Single {
50 | return Single.create { emitter ->
51 | try {
52 | val o = BitmapFactory.Options()
53 | o.inJustDecodeBounds = true
54 |
55 | val stream = if (isUrl) java.net.URL(uri.toString()).openStream() else app.contentResolver.openInputStream(uri)
56 | BitmapFactory.decodeStream(stream, null, o)
57 |
58 | var width_tmp = o.outWidth
59 | var height_tmp = o.outHeight
60 | var scale = 1
61 |
62 | while (true) {
63 | if (width_tmp < POST_MAX_IMAGE_SIZE || height_tmp < POST_MAX_IMAGE_SIZE)
64 | break
65 | width_tmp /= 2
66 | height_tmp /= 2
67 | scale *= 2
68 | }
69 |
70 | val o2 = BitmapFactory.Options()
71 | o2.inSampleSize = scale
72 | val resizedStream = if (isUrl) java.net.URL(uri.toString()).openStream() else app.contentResolver.openInputStream(uri)
73 | val decodedResizedBitmap = BitmapFactory.decodeStream(resizedStream, null, o2)
74 | val bos = ByteArrayOutputStream()
75 | decodedResizedBitmap.compress(CompressFormat.PNG, 0 /*ignored for PNG*/, bos)
76 | emitter.onSuccess(bos.toByteArray())
77 | } catch (e: FileNotFoundException) {
78 | emitter.onError(e)
79 | }
80 | }
81 | }
82 |
83 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/Prefs.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.util
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 |
7 | class Prefs(context: Context) {
8 | val PREFS_FILENAME = "kotlinfirechat.prefs"
9 | private val LOGGED_USER = "logged_user"
10 | private val LOGGED_USERNAME = "logged_username"
11 | val prefs: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, 0)
12 |
13 | var loggedUserId: String?
14 | get() {
15 | return prefs.getString(LOGGED_USER, null)
16 | }
17 | @SuppressLint("ApplySharedPref")
18 | set(value) {
19 | prefs.edit().putString(LOGGED_USER, value).commit()
20 | }
21 |
22 | var loggedUsername: String?
23 | get() {
24 | return prefs.getString(LOGGED_USERNAME, null)
25 | }
26 | @SuppressLint("ApplySharedPref")
27 | set(value) {
28 | prefs.edit().putString(LOGGED_USERNAME, value).commit()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/ProgressDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.util
2 |
3 | import android.support.v4.app.DialogFragment
4 | import android.support.v4.app.Fragment
5 | import android.support.v7.app.AlertDialog
6 | import android.support.v7.app.AppCompatActivity
7 | import android.view.LayoutInflater
8 | import frangsierra.kotlinfirechat.R
9 | import kotlinx.android.synthetic.main.progress_dialog_layout.view.*
10 |
11 |
12 | /** [DialogFragment] displayed in the app for default purposes. */
13 | private var customProgressDialog: AlertDialog? = null
14 |
15 | fun AppCompatActivity.showProgressDialog(message: String) {
16 | val progressDialog = LayoutInflater.from(this)
17 | .inflate(R.layout.progress_dialog_layout, null)
18 | progressDialog.dialog_title.text = message
19 | customProgressDialog = AlertDialog.Builder(this)
20 | .createCustomAlertDialog(view = progressDialog, cancelable = false)
21 |
22 | customProgressDialog?.show()
23 | }
24 |
25 | fun AppCompatActivity.dismissProgressDialog() {
26 | customProgressDialog?.dismiss()
27 | customProgressDialog = null
28 | }
29 |
30 | fun Fragment.showProgressDialog(message: String) {
31 | (activity as? AppCompatActivity)?.showProgressDialog(message)
32 | }
33 |
34 | fun Fragment.dismissProgressDialog() {
35 | (activity as? AppCompatActivity)?.dismissProgressDialog()
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/RequestState.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.util
2 |
3 | import mini.Grove
4 |
5 | data class TypedTask(
6 | val status: Status = TypedTask.Status.IDLE,
7 | val lastUpdate: Long = System.currentTimeMillis(),
8 | val data: T? = null,
9 | val error: Throwable? = null,
10 | val progress: Float? = null) {
11 |
12 | enum class Status {
13 | IDLE, RUNNING, SUCCESS, FAILURE
14 | }
15 |
16 | fun isRunning() = this.status == Status.RUNNING
17 | fun isTerminal() = this.status == Status.SUCCESS || this.status == Status.FAILURE
18 | fun isSuccessful() = this.status == Status.SUCCESS
19 | fun isFailure() = this.status == Status.FAILURE
20 | }
21 |
22 | fun taskRunning(): TypedTask {
23 | return TypedTask(status = TypedTask.Status.RUNNING)
24 | }
25 |
26 | fun taskSuccess(data: T? = null): TypedTask {
27 | return TypedTask(status = TypedTask.Status.SUCCESS, data = data)
28 | }
29 |
30 | fun taskFailure(error: Throwable? = null): TypedTask {
31 | Grove.e(error) { "Task error" }
32 | return TypedTask(status = TypedTask.Status.FAILURE, error = error)
33 | }
34 |
35 | fun taskFailure(error: Throwable? = null, data: T): TypedTask {
36 | Grove.e(error) { "Task error" }
37 | return TypedTask(status = TypedTask.Status.FAILURE, error = error, data = data)
38 | }
39 |
40 | typealias Task = TypedTask
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/RxUtils.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.util
2 |
3 | import io.reactivex.*
4 | import io.reactivex.disposables.CompositeDisposable
5 | import io.reactivex.disposables.Disposable
6 | import java.util.concurrent.TimeUnit
7 |
8 | /** Apply the mapping function if object is not null. */
9 | inline fun Flowable.view(crossinline fn: (T) -> U?): Flowable {
10 | return flatMap {
11 | val mapped = fn(it)
12 | if (mapped == null) Flowable.empty()
13 | else Flowable.just(mapped)
14 | }.distinctUntilChanged()
15 | }
16 |
17 | /** Apply the mapping function if object is not null. */
18 | inline fun Observable.view(crossinline fn: (T) -> U?): Observable {
19 | return flatMap {
20 | val mapped = fn(it)
21 | if (mapped == null) Observable.empty()
22 | else Observable.just(mapped)
23 | }.distinctUntilChanged()
24 | }
25 |
26 | /** Take the first element that matches the filter function. */
27 | inline fun Observable.filterOne(crossinline fn: (T) -> Boolean): Maybe {
28 | return filter { fn(it) }.take(1).singleElement()
29 | }
30 |
31 | /** Take the first element that matches the filter function. */
32 | inline fun Flowable.filterOne(crossinline fn: (T) -> Boolean): Maybe {
33 | return filter { fn(it) }.take(1).singleElement()
34 | }
35 |
36 | inline fun > Flowable.onNextTerminalState(
37 | crossinline mapFn: (S) -> T,
38 | crossinline success: (S) -> Unit = {},
39 | crossinline failure: (Throwable) -> Unit) {
40 |
41 | filterOne { mapFn(it).isTerminal() }
42 | .subscribe {
43 | val task = mapFn(it)
44 | if (task.isSuccessful()) {
45 | success(it)
46 | } else {
47 | failure(task.error!!)
48 | }
49 | }
50 | }
51 |
52 | fun Completable.defaultTimeout() = timeout(20, TimeUnit.SECONDS)
53 | fun Maybe.defaultTimeout() = timeout(20, TimeUnit.SECONDS)
54 | fun Single.defaultTimeout() = timeout(20, TimeUnit.SECONDS)
55 | fun Observable.defaultTimeout() = timeout(20, TimeUnit.SECONDS)
56 |
57 | interface SubscriptionTracker {
58 | /** Clear Subscriptions. */
59 | fun cancelSubscriptions()
60 |
61 | /** Start tracking a disposable. */
62 | fun T.track(): T
63 | }
64 |
65 | class DefaultSubscriptionTracker : SubscriptionTracker {
66 | private val disposables = CompositeDisposable()
67 | override fun cancelSubscriptions() = disposables.clear()
68 | override fun T.track(): T {
69 | disposables.add(this)
70 | return this
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/frangsierra/kotlinfirechat/util/UtilExtensions.kt:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat.util
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.os.Environment
8 | import android.provider.MediaStore
9 | import android.support.design.widget.TextInputLayout
10 | import android.support.v4.content.FileProvider
11 | import android.support.v7.app.AlertDialog
12 | import android.support.v7.widget.LinearLayoutManager
13 | import android.support.v7.widget.RecyclerView
14 | import android.widget.Toast
15 | import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
16 | import com.google.firebase.auth.FirebaseAuthInvalidUserException
17 | import com.google.firebase.auth.FirebaseAuthUserCollisionException
18 | import com.google.firebase.auth.FirebaseAuthWeakPasswordException
19 | import frangsierra.kotlinfirechat.R
20 | import java.io.File
21 | import java.util.*
22 | import java.util.concurrent.TimeUnit
23 |
24 | const val TC_REQUEST_GALLERY: Int = 101
25 | const val TC_REQUEST_CAMERA: Int = 102
26 | const val INTENT_TARGET_TYPE: String = "image/*"
27 | const val APP_IMAGE_FOLDER_PATH: String = "firechat/"
28 | val JPG = ".jpg"
29 |
30 | fun Context.toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
31 | Toast.makeText(this, message, duration).show()
32 | }
33 |
34 | fun TextInputLayout.onError(errorText: String? = null, enable: Boolean = true) {
35 | isErrorEnabled = enable
36 | error = errorText
37 | }
38 |
39 | fun RecyclerView.setLinearLayoutManager(context: Context, reverseLayout: Boolean = true, stackFromEnd: Boolean = true) {
40 | val linearLayoutManager = LinearLayoutManager(context)
41 | linearLayoutManager.reverseLayout = reverseLayout
42 | linearLayoutManager.stackFromEnd = stackFromEnd
43 | layoutManager = linearLayoutManager
44 | }
45 |
46 | fun Activity.showImageIntentDialog(outputFileUri: Uri) {
47 | val builder: AlertDialog.Builder = AlertDialog.Builder(this)
48 | builder.setTitle(getString(R.string.choose_image_picker_text))
49 | builder.setItems(arrayOf(getString(R.string.gallery_text), getString(R.string.camera_text))) { _, which ->
50 | when (which) {
51 | 0 -> {
52 | // GET IMAGE FROM THE GALLERY
53 | val chooser = Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
54 | type = INTENT_TARGET_TYPE
55 | }, getString(R.string.choose_image_picture_text))
56 | startActivityForResult(chooser, TC_REQUEST_GALLERY)
57 | }
58 | 1 -> {
59 | val cameraFolder: File = if (Environment.getExternalStorageState() == android.os.Environment.MEDIA_MOUNTED)
60 | File(Environment.getExternalStorageDirectory(), APP_IMAGE_FOLDER_PATH)
61 | else cacheDir
62 |
63 | if (!cameraFolder.exists())
64 | cameraFolder.mkdirs()
65 |
66 | val getCameraImage = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
67 | getCameraImage.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri)
68 |
69 | startActivityForResult(getCameraImage, TC_REQUEST_CAMERA)
70 | }
71 | }
72 | }
73 | builder.show()
74 | }
75 |
76 | fun Long.getTimeAgoText(): String {
77 | val differenceTimeStamp = System.currentTimeMillis() - this
78 | val days = TimeUnit.MILLISECONDS.toDays(differenceTimeStamp)
79 | val hours = TimeUnit.MILLISECONDS.toHours(differenceTimeStamp)
80 | val minutes = TimeUnit.MILLISECONDS.toMinutes(differenceTimeStamp)
81 | val formatString = "%d %s"
82 | return when {
83 | days.div(375) > 0 -> formatString.format(Locale.getDefault(), days.div(375), "y")
84 | days.div(30) > 0 -> formatString.format(Locale.getDefault(), days.div(30), "mon")
85 | days.div(7) > 0 -> formatString.format(Locale.getDefault(), days.div(7), "wk")
86 | days > 0 -> formatString.format(days, "d")
87 | hours > 0 -> formatString.format(hours, "h")
88 | minutes > 0 -> formatString.format(minutes, "min")
89 | else -> formatString.format(Locale.getDefault(), TimeUnit.MILLISECONDS.toSeconds(differenceTimeStamp).plus(1), "s")
90 | }
91 | }
92 |
93 | fun Context.generateUniqueFireUri(): Uri {
94 | // Determine Uri of camera image to save.
95 | //todo change name to new app
96 | val root = File(Environment.getExternalStorageDirectory().toString() + File.separator + "KotlinFirechat" + File.separator)
97 | root.mkdirs()
98 | val fileName = "${System.currentTimeMillis()}$JPG"
99 | val sdImageMainDirectory = File(root, fileName)
100 | return FileProvider.getUriForFile(this, "$packageName.provider", sdImageMainDirectory)
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/drawable/firebase.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/firebase_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/drawable/firebase_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/layout/create_account_activity.xml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
18 |
19 |
30 |
31 |
32 |
38 |
39 |
50 |
51 |
52 |
58 |
59 |
69 |
70 |
71 |
79 |
80 |
84 |
85 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/home_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
20 |
21 |
29 |
30 |
35 |
36 |
42 |
43 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_message.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
33 |
34 |
45 |
46 |
56 |
57 |
69 |
70 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/login_activity.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
17 |
18 |
23 |
24 |
29 |
30 |
36 |
37 |
49 |
50 |
51 |
57 |
58 |
70 |
71 |
72 |
80 |
81 |
85 |
86 |
92 |
93 |
99 |
100 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/progress_dialog_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/verification_email_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
20 |
21 |
31 |
32 |
45 |
46 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/chat_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 | #AEAEAE
8 | #616161
9 | #4187dc
10 | #22bf1d
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 | 36dp
7 | 16dp
8 | 12dp
9 |
10 | 100dp
11 | 10sp
12 | 48dp
13 |
14 | 14dp
15 | 16sp
16 |
17 | 16dp
18 | 8dp
19 |
20 | 64dp
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | KotlinFirechat
3 |
4 |
5 | Choose Image Source
6 | Gallery
7 | Camera
8 | Choose a Picture
9 |
10 |
11 |
12 | There is no user record corresponding to this identifier.
13 | The password is invalid or the user does not have a password
14 | Use another password, last one was so weak
15 | A problem has occurred, try it again later
16 | Server found an unexpected error, try again later.
17 | Error retrieving data from Google
18 | Looks like you don\'t have internet connection, try again later.
19 | Your connection is not secure, try again later.
20 | Invalid email
21 | An user with this email already exist
22 | Unknown error, please, try again
23 | Don\'t have an account?
24 | Already have an account?
25 | Sign Up
26 | Sign In
27 | SIGN IN WITH GOOGLE
28 | SIGN IN
29 | Enter your email
30 | Enter your name
31 | Enter password
32 | Logo Image
33 | This cannot be empty.
34 | Create Account
35 | Please enter a password that is at least 6 characters long.
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
14 |
15 |
20 |
21 |
28 |
29 |
30 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/test/java/frangsierra/kotlinfirechat/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package frangsierra.kotlinfirechat;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.2.50'
5 |
6 | repositories {
7 | jcenter()
8 | maven { url "https://jitpack.io" }
9 | maven { url "https://maven.fabric.io/public" }
10 | google()
11 | }
12 | dependencies {
13 | classpath 'com.android.tools.build:gradle:3.1.3'
14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
15 | classpath 'com.google.gms:google-services:4.0.1'
16 | classpath 'io.fabric.tools:gradle:1.25.4'
17 |
18 | // NOTE: Do not place your application dependencies here; they belong
19 | // in the individual module build.gradle files
20 | }
21 | }
22 |
23 | allprojects {
24 | repositories {
25 | mavenLocal()
26 | jcenter()
27 | google()
28 | maven { url "https://jitpack.io" }
29 | }
30 | }
31 |
32 | task clean(type: Delete) {
33 | delete rootProject.buildDir
34 | }
35 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrangSierra/KotlinFirechat/52ea49bb5c07c6689ad5d163e0eba7caad468674/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon May 21 13:59:47 CEST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------