├── app
├── .gitignore
├── src
│ ├── test
│ │ ├── resources
│ │ │ ├── images
│ │ │ │ └── empty.png
│ │ │ └── seeds
│ │ │ │ └── sample_userlist_response.json
│ │ └── java
│ │ │ └── com
│ │ │ └── codemate
│ │ │ └── koffeemate
│ │ │ ├── testutils
│ │ │ ├── RegexMatcher.kt
│ │ │ └── CommonTestUtils.kt
│ │ │ ├── views
│ │ │ ├── TimeAgoTextViewTest.kt
│ │ │ └── TimeAgoTextFormatterTest.kt
│ │ │ ├── usecases
│ │ │ ├── SendCoffeeAnnouncementUseCaseTest.kt
│ │ │ ├── PostAccidentUseCaseTest.kt
│ │ │ └── LoadUsersUseCaseTest.kt
│ │ │ ├── ui
│ │ │ └── userselector
│ │ │ │ └── UserSelectorPresenterTest.kt
│ │ │ └── common
│ │ │ └── BrewingProgressUpdaterTest.kt
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── codemate
│ │ │ │ └── koffeemate
│ │ │ │ ├── ui
│ │ │ │ ├── base
│ │ │ │ │ ├── MvpView.kt
│ │ │ │ │ ├── Presenter.kt
│ │ │ │ │ └── BasePresenter.kt
│ │ │ │ ├── userselector
│ │ │ │ │ ├── UserSelectorView.kt
│ │ │ │ │ ├── UserSelectorPresenter.kt
│ │ │ │ │ ├── UserSelectListener.kt
│ │ │ │ │ ├── UserItemAnimator.kt
│ │ │ │ │ ├── adapter
│ │ │ │ │ │ ├── UserQuickDialAdapter.kt
│ │ │ │ │ │ └── UserSelectorAdapter.kt
│ │ │ │ │ └── views
│ │ │ │ │ │ ├── UserQuickDialView.kt
│ │ │ │ │ │ └── UserSelectorOverlay.kt
│ │ │ │ ├── main
│ │ │ │ │ ├── MainView.kt
│ │ │ │ │ ├── MainPresenter.kt
│ │ │ │ │ └── MainActivity.kt
│ │ │ │ └── settings
│ │ │ │ │ └── SettingsActivity.kt
│ │ │ │ ├── extensions
│ │ │ │ ├── String.ext.kt
│ │ │ │ ├── Glide.ext.kt
│ │ │ │ └── Bitmap.ext.kt
│ │ │ │ ├── data
│ │ │ │ ├── models
│ │ │ │ │ ├── CoffeeBrewingEvent.kt
│ │ │ │ │ ├── Profile.kt
│ │ │ │ │ └── User.kt
│ │ │ │ ├── local
│ │ │ │ │ ├── UserRepository.kt
│ │ │ │ │ ├── CoffeePreferences.kt
│ │ │ │ │ ├── CoffeeEventRepository.kt
│ │ │ │ │ └── Migration.kt
│ │ │ │ └── network
│ │ │ │ │ └── SlackApi.kt
│ │ │ │ ├── di
│ │ │ │ ├── scopes
│ │ │ │ │ └── PerActivity.kt
│ │ │ │ ├── modules
│ │ │ │ │ ├── NetModule.kt
│ │ │ │ │ ├── ActivityModule.kt
│ │ │ │ │ ├── ThreadingModule.kt
│ │ │ │ │ ├── PersistenceModule.kt
│ │ │ │ │ └── AppModule.kt
│ │ │ │ └── components
│ │ │ │ │ ├── ActivityComponent.kt
│ │ │ │ │ └── AppComponent.kt
│ │ │ │ ├── KoffeemateApp.kt
│ │ │ │ ├── usecases
│ │ │ │ ├── SendCoffeeAnnouncementUseCase.kt
│ │ │ │ ├── PostAccidentUseCase.kt
│ │ │ │ └── LoadUsersUseCase.kt
│ │ │ │ ├── views
│ │ │ │ ├── CoffeeProgressView.kt
│ │ │ │ ├── UserSetterButton.kt
│ │ │ │ ├── TimeAgoTextFormatter.kt
│ │ │ │ └── TimeAgoTextView.kt
│ │ │ │ └── common
│ │ │ │ ├── BrewingProgressUpdater.kt
│ │ │ │ ├── AwardBadgeCreator.kt
│ │ │ │ └── ScreenSaver.kt
│ │ ├── res
│ │ │ ├── drawable-hdpi
│ │ │ │ ├── ic_more.png
│ │ │ │ ├── coffee_pan.png
│ │ │ │ ├── ic_add_user.png
│ │ │ │ ├── ic_settings.png
│ │ │ │ ├── ic_thumb_down.png
│ │ │ │ └── ic_user_unknown.png
│ │ │ ├── drawable-mdpi
│ │ │ │ ├── ic_more.png
│ │ │ │ ├── coffee_pan.png
│ │ │ │ ├── ic_add_user.png
│ │ │ │ ├── ic_settings.png
│ │ │ │ ├── ic_thumb_down.png
│ │ │ │ └── ic_user_unknown.png
│ │ │ ├── drawable-nodpi
│ │ │ │ ├── award.png
│ │ │ │ └── empty.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ ├── ic_more.png
│ │ │ │ ├── coffee_pan.png
│ │ │ │ ├── ic_add_user.png
│ │ │ │ ├── ic_settings.png
│ │ │ │ ├── ic_thumb_down.png
│ │ │ │ └── ic_user_unknown.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ ├── ic_more.png
│ │ │ │ ├── coffee_pan.png
│ │ │ │ ├── ic_add_user.png
│ │ │ │ ├── ic_settings.png
│ │ │ │ ├── ic_thumb_down.png
│ │ │ │ └── ic_user_unknown.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ ├── ic_more.png
│ │ │ │ ├── coffee_pan.png
│ │ │ │ ├── ic_add_user.png
│ │ │ │ ├── ic_settings.png
│ │ │ │ ├── ic_thumb_down.png
│ │ │ │ └── ic_user_unknown.png
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values
│ │ │ │ ├── attrs.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── strings.xml
│ │ │ ├── drawable
│ │ │ │ └── background_shadow.xml
│ │ │ ├── values-v21
│ │ │ │ └── styles.xml
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ ├── layout
│ │ │ │ ├── view_user_quick_dial.xml
│ │ │ │ ├── view_screen_saver_overlay.xml
│ │ │ │ ├── recycler_item_user.xml
│ │ │ │ ├── view_coffee_progress.xml
│ │ │ │ ├── view_user_selector.xml
│ │ │ │ └── activity_main.xml
│ │ │ └── xml
│ │ │ │ └── koffeemate_preferences.xml
│ │ └── AndroidManifest.xml
│ └── androidTest
│ │ ├── assets
│ │ └── sample-db-schema-v0.realm
│ │ └── java
│ │ └── com
│ │ └── codemate
│ │ └── koffeemate
│ │ └── data
│ │ └── local
│ │ ├── RealmTestRule.kt
│ │ ├── UserRepositoryTest.kt
│ │ ├── CoffeePreferencesTest.kt
│ │ ├── MigrationTest.kt
│ │ └── CoffeeEventRepositoryTest.kt
├── proguard-rules.pro
├── build.gradle
├── app-config.gradle
└── dependencies.gradle
├── settings.gradle
├── art
├── koffeemate_logo.png
├── coffee_incoming_animation.gif
└── screenshot_coffee_incoming.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── copyright
│ ├── profiles_settings.xml
│ └── Apache_2_0.xml
├── encodings.xml
├── vcs.xml
├── modules.xml
├── runConfigurations.xml
├── gradle.xml
├── compiler.xml
└── misc.xml
├── .travis.yml
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/art/koffeemate_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/art/koffeemate_logo.png
--------------------------------------------------------------------------------
/art/coffee_incoming_animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/art/coffee_incoming_animation.gif
--------------------------------------------------------------------------------
/art/screenshot_coffee_incoming.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/art/screenshot_coffee_incoming.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/test/resources/images/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/test/resources/images/empty.png
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/base/MvpView.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.ui.base
2 |
3 | interface MvpView {
4 |
5 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_more.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-hdpi/ic_more.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_more.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-mdpi/ic_more.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/award.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-nodpi/award.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-nodpi/empty.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/coffee_pan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-hdpi/coffee_pan.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/coffee_pan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-mdpi/coffee_pan.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_more.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xhdpi/ic_more.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_more.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxhdpi/ic_more.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_more.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxxhdpi/ic_more.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_add_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-hdpi/ic_add_user.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-hdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_add_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-mdpi/ic_add_user.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-mdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/coffee_pan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xhdpi/coffee_pan.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_add_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xhdpi/ic_add_user.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xhdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/coffee_pan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxhdpi/coffee_pan.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_thumb_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-hdpi/ic_thumb_down.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_user_unknown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-hdpi/ic_user_unknown.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_thumb_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-mdpi/ic_thumb_down.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_user_unknown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-mdpi/ic_user_unknown.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_thumb_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xhdpi/ic_thumb_down.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_add_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxhdpi/ic_add_user.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxhdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_thumb_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxhdpi/ic_thumb_down.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/coffee_pan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxxhdpi/coffee_pan.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_add_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxxhdpi/ic_add_user.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxxhdpi/ic_settings.png
--------------------------------------------------------------------------------
/app/src/androidTest/assets/sample-db-schema-v0.realm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/androidTest/assets/sample-db-schema-v0.realm
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_user_unknown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xhdpi/ic_user_unknown.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_user_unknown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxhdpi/ic_user_unknown.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_thumb_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxxhdpi/ic_thumb_down.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_user_unknown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodemateLtd/Koffeemate/HEAD/app/src/main/res/drawable-xxxhdpi/ic_user_unknown.png
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/base/Presenter.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.ui.base
2 |
3 | interface Presenter {
4 | fun attachView(mvpView: V)
5 | fun detachView()
6 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #03A9F4
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_shadow.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 28 10:00:20 PST 2015
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-2.14.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/extensions/String.ext.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.extensions
2 |
3 | import okhttp3.MediaType
4 | import okhttp3.RequestBody
5 |
6 | fun String.toRequestBody() : RequestBody {
7 | return RequestBody.create(MediaType.parse("text/plain"), this)
8 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorView.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.ui.userselector
2 |
3 | import com.codemate.koffeemate.data.models.User
4 | import com.codemate.koffeemate.ui.base.MvpView
5 |
6 | interface UserSelectorView : MvpView {
7 | fun showProgress()
8 | fun hideProgress()
9 | fun showError()
10 | fun showUsers(users: List)
11 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_user_quick_dial.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/data/models/CoffeeBrewingEvent.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.data.models
2 |
3 | import io.realm.RealmObject
4 | import io.realm.annotations.PrimaryKey
5 |
6 | open class CoffeeBrewingEvent(
7 | @PrimaryKey
8 | open var id: String = "",
9 | open var time: Long = 0,
10 | open var isSuccessful: Boolean = false,
11 | open var user: User? = null
12 | ) : RealmObject()
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 |
3 | android:
4 | components:
5 | - tools
6 | - platform-tools
7 | - build-tools-25.0.0
8 | - android-25
9 | - extra-android-m2repository
10 |
11 | jdk:
12 | oraclejdk8
13 |
14 | before_script:
15 | - echo no | android create avd --force -n test -t android-18 --abi armeabi-v7a
16 | - emulator -avd test -no-audio -no-window &
17 | - android-wait-for-emulator
18 | - adb shell input keyevent 82 &
19 |
20 | script:
21 | - ./gradlew test connectedAndroidTest
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_screen_saver_overlay.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/.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 | app/koffeemate.properties
20 |
21 | # Local configuration file (sdk path, etc)
22 | local.properties
23 |
24 | # Proguard folder generated by Eclipse
25 | proguard/
26 |
27 | # Log Files
28 | *.log
29 |
30 | # Android Studio Navigation editor temp files
31 | .navigation/
32 |
33 | # Android Studio captures folder
34 | captures/
35 |
36 | # Intellij
37 | *.iml
38 | .idea/workspace.xml
39 | .idea/libraries
40 |
41 | # Keystore files
42 | *.jks
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/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 /Users/ironman/Library/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/extensions/Glide.ext.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.extensions
2 |
3 | import android.graphics.Bitmap
4 | import com.bumptech.glide.RequestManager
5 | import com.bumptech.glide.request.animation.GlideAnimation
6 | import com.bumptech.glide.request.target.SimpleTarget
7 | import com.codemate.koffeemate.R
8 |
9 | fun RequestManager.loadBitmap(url: String, completeListener: (Bitmap) -> Unit) {
10 | load(url).asBitmap()
11 | .error(R.drawable.ic_user_unknown)
12 | .into(object : SimpleTarget(512, 512) {
13 | override fun onResourceReady(resource: Bitmap, glideAnimation: GlideAnimation?) {
14 | completeListener(resource)
15 | }
16 | })
17 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/.idea/copyright/Apache_2_0.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/base/BasePresenter.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.ui.base
2 |
3 | open class BasePresenter : Presenter {
4 | private var mvpView: T? = null
5 |
6 | override fun attachView(mvpView: T) {
7 | this.mvpView = mvpView
8 | }
9 |
10 | override fun detachView() {
11 | mvpView = null
12 | }
13 |
14 | fun getView(): T? {
15 | return mvpView
16 | }
17 |
18 | fun ensureViewIsAttached() {
19 | if (!isViewAttached()) {
20 | throw ViewNotAttachedException()
21 | }
22 | }
23 |
24 | fun isViewAttached(): Boolean {
25 | return mvpView != null
26 | }
27 |
28 | class ViewNotAttachedException : RuntimeException("View not attached! Please call attachView() first.")
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/di/scopes/PerActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.di.scopes
18 |
19 | import javax.inject.Scope
20 |
21 | @Scope
22 | @Retention
23 | annotation class PerActivity
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectorPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.ui.userselector
2 |
3 | import com.codemate.koffeemate.ui.base.BasePresenter
4 | import com.codemate.koffeemate.usecases.LoadUsersUseCase
5 | import javax.inject.Inject
6 |
7 | class UserSelectorPresenter @Inject constructor(
8 | val loadUsersUseCase: LoadUsersUseCase
9 | ) : BasePresenter() {
10 | fun loadUsers() {
11 | ensureViewIsAttached()
12 | getView()?.showProgress()
13 |
14 | loadUsersUseCase.execute()
15 | .subscribe(
16 | { users ->
17 | getView()?.showUsers(users)
18 | getView()?.hideProgress()
19 | },
20 | { getView()?.showError() }
21 | )
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
19 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/test/resources/seeds/sample_userlist_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "ok": true,
3 | "members": [{
4 | "id": "abc123",
5 | "name": "bobby",
6 | "is_bot": false,
7 | "profile": {
8 | "first_name": "Bobby",
9 | "last_name": "Tables",
10 | "real_name": "Bobby Tables"
11 | }
12 | }, {
13 | "id": "123abc",
14 | "name": "john",
15 | "is_bot": false,
16 | "profile": {
17 | "first_name": "John",
18 | "last_name": "Smith",
19 | "real_name": "John Smith"
20 | }
21 | }, {
22 | "id": "a1b2c3",
23 | "name": "slackbot",
24 | "is_bot": true,
25 | "profile": {
26 | "first_name": "Slack",
27 | "last_name": "Bot",
28 | "real_name": "Slackbot"
29 | }
30 | }, {
31 | "id": "c3b2a1",
32 | "name": "kevin",
33 | "is_bot": false,
34 | "deleted": true,
35 | "profile": {
36 | "first_name": "Kevin",
37 | "last_name": "Smith",
38 | "real_name": "Kevin Smith"
39 | }
40 | }],
41 | "cache_ts": 123456789
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/main/MainView.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.ui.main
2 |
3 | import com.codemate.koffeemate.data.models.CoffeeBrewingEvent
4 | import com.codemate.koffeemate.data.models.User
5 | import com.codemate.koffeemate.ui.base.MvpView
6 |
7 | interface MainView : MvpView {
8 | fun showNewCoffeeIsComing()
9 | fun showCancelCoffeeProgressPrompt()
10 |
11 | fun displayUserSelectorQuickDial(users: List)
12 | fun displayFullscreenUserSelector(requestCode: Int)
13 | fun clearCoffeeBrewingPerson()
14 |
15 | fun displayUserSetterButton()
16 | fun hideUserSetterButton()
17 |
18 | fun updateLastBrewingEvent(event: CoffeeBrewingEvent)
19 | fun updateCoffeeProgress(newProgress: Int)
20 | fun resetCoffeeViewStatus()
21 |
22 | fun showNoAnnouncementChannelSetError()
23 | fun showNoAccidentChannelSetError()
24 |
25 | fun showPostAccidentAnnouncementPrompt(user: User)
26 | fun showAccidentPostedSuccessfullyMessage()
27 | fun showErrorPostingAccidentMessage()
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserSelectListener.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.ui.userselector
18 |
19 | import com.codemate.koffeemate.data.models.User
20 |
21 | interface UserSelectListener {
22 | companion object {
23 | val REQUEST_WHOS_BREWING = 1
24 | val REQUEST_WHO_FAILED_BREWING = 2
25 | }
26 |
27 | fun onUserSelected(user: User, requestCode: Int)
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/di/modules/NetModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.di.modules
18 |
19 | import com.codemate.koffeemate.data.network.SlackApi
20 | import dagger.Module
21 | import dagger.Provides
22 | import okhttp3.HttpUrl
23 | import javax.inject.Singleton
24 |
25 | @Module
26 | class NetModule(val baseUrl: HttpUrl) {
27 | @Provides
28 | @Singleton
29 | fun provideApi() = SlackApi.create(baseUrl)
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/di/components/ActivityComponent.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.di.components
18 |
19 | import com.codemate.koffeemate.di.modules.ActivityModule
20 | import com.codemate.koffeemate.di.scopes.PerActivity
21 | import com.codemate.koffeemate.ui.main.MainActivity
22 | import dagger.Subcomponent
23 |
24 | @PerActivity
25 | @Subcomponent(modules = arrayOf(ActivityModule::class))
26 | interface ActivityComponent {
27 | fun inject(mainActivity: MainActivity)
28 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'realm-android'
5 |
6 | apply from: 'dependencies.gradle'
7 | apply from: 'app-config.gradle'
8 |
9 | android {
10 | compileSdkVersion 25
11 | buildToolsVersion "25.0.0"
12 | defaultConfig {
13 | applicationId "com.codemate.koffeemate"
14 | minSdkVersion 16
15 | targetSdkVersion 25
16 | versionCode 1
17 | versionName "0.3"
18 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
19 |
20 | buildConfigField "String", "SLACK_AUTH_TOKEN", SLACK_AUTH_TOKEN
21 | }
22 | buildTypes {
23 | debug {
24 | testCoverageEnabled true
25 | }
26 | release {
27 | minifyEnabled false
28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 | sourceSets {
32 | main.java.srcDirs += 'src/main/kotlin'
33 | }
34 | }
35 |
36 | kapt {
37 | generateStubs = true
38 | }
39 |
40 | repositories {
41 | mavenCentral()
42 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/recycler_item_user.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
17 |
18 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/di/modules/ActivityModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.di.modules
18 |
19 | import android.app.Activity
20 | import com.codemate.koffeemate.common.AndroidScreenSaver
21 | import com.codemate.koffeemate.common.ScreenSaver
22 | import com.codemate.koffeemate.di.scopes.PerActivity
23 | import dagger.Module
24 | import dagger.Provides
25 |
26 | @Module
27 | class ActivityModule(private val activity: Activity) {
28 | @Provides
29 | @PerActivity
30 | fun provideScreenSaver(): ScreenSaver = AndroidScreenSaver(activity)
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/di/modules/ThreadingModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.di.modules
18 |
19 | import dagger.Module
20 | import dagger.Provides
21 | import rx.Scheduler
22 | import rx.android.schedulers.AndroidSchedulers
23 | import rx.schedulers.Schedulers
24 | import javax.inject.Named
25 |
26 | @Module
27 | class ThreadingModule {
28 | @Provides
29 | @Named("subscriber")
30 | fun provideSubscriber(): Scheduler = Schedulers.newThread()
31 |
32 | @Provides
33 | @Named("observer")
34 | fun provideObserver(): Scheduler = AndroidSchedulers.mainThread()
35 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/codemate/koffeemate/testutils/RegexMatcher.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.testutils
18 |
19 | import org.hamcrest.BaseMatcher
20 | import org.hamcrest.Description
21 |
22 | class RegexMatcher(private val regex: String) : BaseMatcher() {
23 | override fun matches(o: Any) = regex.toRegex(RegexOption.DOT_MATCHES_ALL).matches(o as String)
24 |
25 | override fun describeTo(description: Description) {
26 | description.appendText("matches regex $regex")
27 | }
28 |
29 | companion object {
30 | fun matchesPattern(regex: String) = RegexMatcher(regex)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/KoffeemateApp.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate
2 |
3 | import android.app.Application
4 | import com.codemate.koffeemate.data.local.Migration
5 | import com.codemate.koffeemate.data.network.SlackApi
6 | import com.codemate.koffeemate.di.components.AppComponent
7 | import com.codemate.koffeemate.di.components.DaggerAppComponent
8 | import com.codemate.koffeemate.di.modules.AppModule
9 | import com.codemate.koffeemate.di.modules.NetModule
10 | import io.realm.Realm
11 | import io.realm.RealmConfiguration
12 |
13 | class KoffeemateApp : Application() {
14 | companion object {
15 | lateinit var appComponent: AppComponent
16 | }
17 |
18 | override fun onCreate() {
19 | super.onCreate()
20 |
21 | initializeRealm()
22 |
23 | appComponent = DaggerAppComponent.builder()
24 | .appModule(AppModule(this))
25 | .netModule(NetModule(SlackApi.BASE_URL))
26 | .build()
27 | }
28 |
29 | private fun initializeRealm() {
30 | Realm.init(this)
31 |
32 | val configuration = RealmConfiguration.Builder()
33 | .migration(Migration())
34 | .schemaVersion(1)
35 | .build()
36 | Realm.setDefaultConfiguration(configuration)
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/di/components/AppComponent.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.di.components
18 |
19 | import com.codemate.koffeemate.di.modules.*
20 | import com.codemate.koffeemate.ui.userselector.views.UserSelectorOverlay
21 | import dagger.Component
22 | import javax.inject.Singleton
23 |
24 | @Singleton
25 | @Component(modules = arrayOf(
26 | AppModule::class,
27 | PersistenceModule::class,
28 | NetModule::class,
29 | ThreadingModule::class)
30 | )
31 | interface AppComponent {
32 | fun inject(userSelectorOverlay: UserSelectorOverlay)
33 |
34 | fun plus(activityModule: ActivityModule): ActivityComponent
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/data/models/Profile.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.data.models
18 |
19 | import io.realm.RealmObject
20 |
21 | open class Profile(
22 | open var first_name: String = "",
23 | open var last_name: String = "",
24 | open var real_name: String = "",
25 | open var image_72: String? = null,
26 | open var image_192: String? = null,
27 | open var image_512: String? = null
28 | ) : RealmObject() {
29 | val largestAvailableImage: String
30 | get() = image_512 ?: image_192 ?: image_72 ?: ""
31 |
32 | val smallestAvailableImage: String
33 | get() = image_72 ?: image_192 ?: image_512 ?: ""
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/di/modules/PersistenceModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.di.modules
18 |
19 | import android.content.Context
20 | import com.codemate.koffeemate.data.local.*
21 | import dagger.Module
22 | import dagger.Provides
23 | import javax.inject.Singleton
24 |
25 | @Module
26 | class PersistenceModule {
27 | @Provides
28 | @Singleton
29 | fun provideCoffeePreferences(ctx: Context) = CoffeePreferences(ctx)
30 |
31 | @Provides
32 | @Singleton
33 | fun provideCoffeeEventRepository(): CoffeeEventRepository = RealmCoffeeEventRepository()
34 |
35 | @Provides
36 | @Singleton
37 | fun provideUserRepository(): UserRepository = RealmUserRepository()
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/extensions/Bitmap.ext.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.extensions
18 |
19 | import android.content.Context
20 | import android.graphics.Bitmap
21 | import java.io.File
22 | import java.io.FileOutputStream
23 |
24 | fun Bitmap.saveToFile(ctx: Context, filename: String): File {
25 | val extStorageDirectory = ctx.filesDir
26 | val file = File(extStorageDirectory, filename)
27 |
28 | try {
29 | val outStream = FileOutputStream(file)
30 | compress(Bitmap.CompressFormat.PNG, 100, outStream)
31 |
32 | outStream.flush()
33 | outStream.close()
34 | } catch (e: Exception) {
35 | e.printStackTrace()
36 | }
37 |
38 | return file
39 | }
--------------------------------------------------------------------------------
/app/app-config.gradle:
--------------------------------------------------------------------------------
1 | /**
2 | * Beautiful Gradle script taken from:
3 | * https://github.com/futurice/freesound-android/blob/master/app/app-config.gradle
4 | */
5 | ext {
6 | SLACK_AUTH_TOKEN = getSlackAuthToken()
7 | }
8 |
9 | def String getSlackAuthToken() {
10 | String slackAuthToken = isCiBuild() ? getApiKeyFromEnv() : getApiKeyFromFile();
11 | if (slackAuthToken == null || slackAuthToken.isEmpty()) {
12 | throw new IllegalStateException(
13 | "Could not find Slack API key value in environment or property file")
14 | }
15 | return "\"$slackAuthToken\""
16 | }
17 |
18 | private boolean isCiBuild() {
19 | System.getenv("CI") == "true"
20 | }
21 |
22 | private String getApiKeyFromFile() {
23 | Properties apiProperties = loadFileProperties("$projectDir/koffeemate.properties")
24 | return apiProperties.getProperty("SLACK_AUTH_TOKEN")
25 | }
26 |
27 | private String getApiKeyFromEnv() {
28 | System.getenv("SLACK_AUTH_TOKEN")
29 | }
30 |
31 | def Properties loadFileProperties(String fileLocation) {
32 | def Properties properties = new Properties()
33 | try {
34 | properties.load(new FileInputStream(fileLocation))
35 | } catch (FileNotFoundException fnf) {
36 | logger.log(LogLevel.ERROR,
37 | String.format("Missing Koffeemate properties file: %s", fileLocation),
38 | fnf)
39 | throw fnf
40 | }
41 | return properties
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/data/local/UserRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.data.local
18 |
19 | import com.codemate.koffeemate.data.models.User
20 | import io.realm.Realm
21 |
22 | interface UserRepository {
23 | fun addAll(users: List)
24 | fun getAll(): List
25 | }
26 |
27 | class RealmUserRepository : UserRepository {
28 | override fun addAll(users: List) {
29 | with(Realm.getDefaultInstance()) {
30 | executeTransaction { copyToRealmOrUpdate(users) }
31 | close()
32 | }
33 | }
34 |
35 | override fun getAll(): List = with(Realm.getDefaultInstance()) {
36 | val all = where(User::class.java).findAll()
37 | val copy = copyFromRealm(all)
38 |
39 | close()
40 | return@with copy
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/codemate/koffeemate/testutils/CommonTestUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.testutils
18 |
19 | import com.codemate.koffeemate.data.models.Profile
20 | import com.codemate.koffeemate.data.models.User
21 | import java.io.File
22 |
23 | fun Any.getResourceFile(path: String): File {
24 | return File(javaClass.classLoader.getResource(path).file)
25 | }
26 |
27 | fun fakeUser() = User().apply {
28 | id = "abc123"
29 | profile = Profile()
30 | profile.first_name = "Jorma"
31 | profile.real_name = "Jorma"
32 | }
33 |
34 | fun namedUser(name: String) = User().apply {
35 | id = name
36 | this.name = name
37 | profile.real_name = name
38 | }
39 |
40 | fun namedUserWithTimestamp(name: String, lastUpdated: Long) =
41 | namedUser(name).apply {
42 | last_updated = lastUpdated
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/usecases/SendCoffeeAnnouncementUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.usecases
18 |
19 | import com.codemate.koffeemate.data.network.SlackApi
20 | import okhttp3.ResponseBody
21 | import retrofit2.Response
22 | import rx.Observable
23 | import rx.Scheduler
24 | import javax.inject.Inject
25 | import javax.inject.Named
26 |
27 | open class SendCoffeeAnnouncementUseCase @Inject constructor(
28 | var slackApi: SlackApi,
29 | @Named("subscriber") var subscriber: Scheduler,
30 | @Named("observer") var observer: Scheduler
31 | ) {
32 | fun execute(channel: String, newCoffeeMessage: String): Observable> {
33 | return slackApi.postMessage(channel, newCoffeeMessage)
34 | .subscribeOn(subscriber)
35 | .observeOn(observer)
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codemate/koffeemate/data/local/RealmTestRule.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.data.local
2 |
3 | import io.realm.Realm
4 | import io.realm.RealmConfiguration
5 | import org.junit.rules.ExternalResource
6 | import org.junit.runner.Description
7 | import org.junit.runners.model.Statement
8 |
9 | /**
10 | * A JUnit Rule that sets up a test Realm database before each test,
11 | * clears all data from it, and deletes the whole database file alltogether
12 | * after the test completes.
13 | *
14 | * Usage:
15 | *
16 | * @Rule @JvmField
17 | * val realmRule: RealmTestRule = RealmTestRule()
18 | *
19 | * The above assumes that your application logic that uses Realm always uses
20 | * the default instance. Meaning that your code calls Realm.getDefaultInstance()
21 | * and uses that for all database logic.
22 | */
23 | class RealmTestRule : ExternalResource() {
24 | val testConfig: RealmConfiguration = RealmConfiguration.Builder()
25 | .name("test.realm")
26 | .build()
27 |
28 | override fun before() {
29 | Realm.setDefaultConfiguration(testConfig)
30 |
31 | with (Realm.getDefaultInstance()) {
32 | executeTransaction(Realm::deleteAll)
33 | close()
34 | }
35 | }
36 |
37 | override fun apply(base: Statement?, description: Description?): Statement {
38 | return super.apply(base, description)
39 | }
40 |
41 | override fun after() {
42 | Realm.deleteRealm(testConfig)
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_coffee_progress.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
20 |
21 |
26 |
27 |
28 |
29 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_user_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
19 |
20 |
27 |
28 |
32 |
33 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/di/modules/AppModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.di.modules
18 |
19 | import android.content.Context
20 | import com.codemate.koffeemate.KoffeemateApp
21 | import com.codemate.koffeemate.common.AndroidAwardBadgeCreator
22 | import com.codemate.koffeemate.common.AwardBadgeCreator
23 | import com.codemate.koffeemate.common.BrewingProgressUpdater
24 | import dagger.Module
25 | import dagger.Provides
26 | import java.util.concurrent.TimeUnit
27 | import javax.inject.Singleton
28 |
29 | @Module
30 | class AppModule(val app: KoffeemateApp) {
31 | @Provides
32 | @Singleton
33 | fun provideContext(): Context = app
34 |
35 | @Provides
36 | @Singleton
37 | fun provideApp() = app
38 |
39 | @Provides
40 | @Singleton
41 | fun provideBrewingProgressUpdater() = BrewingProgressUpdater(TimeUnit.MINUTES.toMillis(7), 30)
42 |
43 | @Provides
44 | @Singleton
45 | fun provideAwardBadgeCreator(ctx: Context): AwardBadgeCreator = AndroidAwardBadgeCreator(ctx)
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/settings/SettingsActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.ui.settings
18 |
19 | import android.os.Bundle
20 | import android.support.v7.app.AppCompatActivity
21 | import android.support.v7.preference.PreferenceFragmentCompat
22 | import android.view.MenuItem
23 | import com.codemate.koffeemate.R
24 |
25 | class SettingsActivity : AppCompatActivity() {
26 | override fun onCreate(savedInstanceState: Bundle?) {
27 | super.onCreate(savedInstanceState)
28 |
29 | if (savedInstanceState == null) {
30 | supportFragmentManager.beginTransaction()
31 | .add(android.R.id.content, SecretSettingsFragment())
32 | .commit()
33 | }
34 |
35 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
36 | }
37 |
38 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
39 | if (item.itemId == android.R.id.home) {
40 | finish()
41 | }
42 |
43 | return super.onOptionsItemSelected(item)
44 | }
45 |
46 | class SecretSettingsFragment : PreferenceFragmentCompat() {
47 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
48 | addPreferencesFromResource(R.xml.koffeemate_preferences)
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/test/java/com/codemate/koffeemate/views/TimeAgoTextViewTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.views
18 |
19 | import com.codemate.koffeemate.views.TimeAgoTextView.Companion.getUpdateInterval
20 | import org.hamcrest.core.IsEqual.equalTo
21 | import org.junit.Assert.assertThat
22 | import org.junit.Test
23 | import java.util.concurrent.TimeUnit
24 |
25 | class TimeAgoTextViewTest {
26 | private val MINUTE_IN_MILLIS = TimeUnit.MINUTES.toMillis(1)
27 | private val HOUR_IN_MILLIS = TimeUnit.HOURS.toMillis(1)
28 | private val DAY_IN_MILLIS = TimeUnit.DAYS.toMillis(1)
29 | private val WEEK_IN_MILLIS = DAY_IN_MILLIS * 7
30 |
31 | @Test
32 | fun updateIntervalCalculationTests() {
33 | assertThat(getUpdateInterval(MINUTE_IN_MILLIS), equalTo(MINUTE_IN_MILLIS))
34 | assertThat(getUpdateInterval(MINUTE_IN_MILLIS * 59), equalTo(MINUTE_IN_MILLIS))
35 |
36 | assertThat(getUpdateInterval(HOUR_IN_MILLIS), equalTo(HOUR_IN_MILLIS))
37 | assertThat(getUpdateInterval(HOUR_IN_MILLIS * 23), equalTo(HOUR_IN_MILLIS))
38 |
39 | assertThat(getUpdateInterval(DAY_IN_MILLIS), equalTo(DAY_IN_MILLIS))
40 | assertThat(getUpdateInterval(DAY_IN_MILLIS * 6), equalTo(DAY_IN_MILLIS))
41 |
42 | assertThat(getUpdateInterval(WEEK_IN_MILLIS), equalTo(WEEK_IN_MILLIS))
43 | assertThat(getUpdateInterval(WEEK_IN_MILLIS * 3), equalTo(WEEK_IN_MILLIS))
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/data/local/CoffeePreferences.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.data.local
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import com.codemate.koffeemate.R
6 | import org.jetbrains.anko.defaultSharedPreferences
7 |
8 | open class CoffeePreferences(ctx: Context) {
9 | val DEFAULT_COFFEE_BREWING_TIME_MINUTES = "7"
10 |
11 | var preferences: SharedPreferences = ctx.defaultSharedPreferences
12 |
13 | val coffeeBrewingTimeKey: String = ctx.getString(R.string.preference_coffee_brewing_time_key)
14 | val announcementChannelKey: String = ctx.getString(R.string.preference_coffee_announcement_slack_channel_key)
15 | val differentChannelForAccidentsKey: String = ctx.getString(R.string.preference_use_different_channel_for_accidents_key)
16 | val accidentChannelKey: String = ctx.getString(R.string.preference_coffee_accident_slack_channel_key)
17 |
18 | open fun getCoffeeBrewingTime(): Long {
19 | return preferences.getString(
20 | coffeeBrewingTimeKey,
21 | DEFAULT_COFFEE_BREWING_TIME_MINUTES
22 | ).toLong()
23 | }
24 |
25 | open fun isCoffeeAnnouncementChannelSet() = !getCoffeeAnnouncementChannel().isBlank()
26 |
27 | open fun getCoffeeAnnouncementChannel(): String {
28 | return preferences.getString(announcementChannelKey, null) ?: ""
29 | }
30 |
31 | open fun useDifferentChannelForAccidents() =
32 | preferences.getBoolean(differentChannelForAccidentsKey, false)
33 |
34 | open fun isAccidentChannelSet(): Boolean {
35 | if (useDifferentChannelForAccidents()) {
36 | return !getAccidentChannel().isBlank()
37 | }
38 |
39 | return isCoffeeAnnouncementChannelSet()
40 | }
41 |
42 | open fun getAccidentChannel(): String {
43 | if (useDifferentChannelForAccidents()) {
44 | return preferences.getString(accidentChannelKey, null) ?: ""
45 | }
46 |
47 | return preferences.getString(announcementChannelKey, null) ?: ""
48 | }
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/views/CoffeeProgressView.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.views
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.MotionEvent
6 | import android.view.View
7 | import android.widget.FrameLayout
8 | import com.codemate.koffeemate.R
9 | import kotlinx.android.synthetic.main.view_coffee_progress.view.*
10 | import org.jetbrains.anko.dip
11 | import org.jetbrains.anko.onClick
12 | import org.jetbrains.anko.onTouch
13 | import org.jetbrains.anko.padding
14 |
15 | class CoffeeProgressView(ctx: Context, attrs: AttributeSet) : FrameLayout(ctx, attrs) {
16 | init {
17 | padding = ctx.dip(16)
18 | clipToPadding = false
19 |
20 | inflate(ctx, R.layout.view_coffee_progress, this)
21 | initializeTouchListener()
22 | }
23 |
24 | fun setOnCoffeePotClickListener(listener: (View?) -> Unit) {
25 | coffeePotButton.onClick(listener)
26 | }
27 |
28 | fun setOnUserSetterClickListener(listener: (View?) -> Unit) {
29 | userSetterButton.onClick(listener)
30 | }
31 |
32 | fun setCoffeeIncoming() {
33 | animate().alpha(1f).start()
34 | }
35 |
36 | fun reset() {
37 | animate().alpha(0.2f).start()
38 | userSetterButton.hide()
39 | }
40 |
41 | fun setProgress(newProgress: Int) {
42 | // For some reason, the CircularFillableLoaders library uses inverted
43 | // progress values: 0 means full and 100 means empty.
44 | coffeeFillableLoader.setProgress(100 - newProgress)
45 | }
46 |
47 | private fun initializeTouchListener() {
48 | onTouch { view, motionEvent ->
49 | when(motionEvent.action) {
50 | MotionEvent.ACTION_DOWN -> animateTouch(0.9f)
51 | MotionEvent.ACTION_UP -> animateTouch(1.0f)
52 | }
53 |
54 | return@onTouch false
55 | }
56 | }
57 |
58 | private fun animateTouch(scale: Float) {
59 | animate().scaleX(scale)
60 | .scaleY(scale)
61 | .setDuration(100)
62 | .start()
63 | }
64 | }
--------------------------------------------------------------------------------
/app/dependencies.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | androidSupportVersion = "25.0.1"
3 | ankoVersion = "0.9"
4 | retrofitVersion = "2.1.0"
5 | daggerVersion = "2.4"
6 |
7 | espressoVersion = "2.2.2"
8 | mockitoVersion = "2.4.1"
9 |
10 | dependencies {
11 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
12 | compile "org.jetbrains.anko:anko-sdk15:$ankoVersion"
13 | compile "org.jetbrains.anko:anko-sqlite:$ankoVersion"
14 |
15 | compile "com.android.support:appcompat-v7:$androidSupportVersion"
16 | compile "com.android.support:recyclerview-v7:$androidSupportVersion"
17 | compile "com.android.support:design:$androidSupportVersion"
18 | compile "com.android.support:preference-v7:$androidSupportVersion"
19 | compile "com.android.support:preference-v14:$androidSupportVersion"
20 |
21 | compile "com.squareup.retrofit2:retrofit:$retrofitVersion"
22 | compile "com.squareup.retrofit2:converter-gson:$retrofitVersion"
23 | compile "com.squareup.retrofit2:adapter-rxjava:$retrofitVersion"
24 |
25 | compile "com.google.dagger:dagger:$daggerVersion"
26 | kapt "com.google.dagger:dagger-compiler:$daggerVersion"
27 | provided "javax.annotation:jsr250-api:1.0"
28 |
29 | compile "com.github.bumptech.glide:glide:3.7.0"
30 | compile "com.mikhaellopez:circularfillableloaders:1.2.0"
31 | compile "de.hdodenhof:circleimageview:2.1.0"
32 |
33 | compile "io.reactivex:rxjava:1.1.6"
34 | compile "io.reactivex:rxandroid:1.2.1"
35 |
36 | androidTestCompile "com.android.support.test.espresso:espresso-core:$espressoVersion"
37 | androidTestCompile "com.android.support.test.espresso:espresso-intents:$espressoVersion"
38 | androidTestCompile "org.mockito:mockito-core:$mockitoVersion"
39 |
40 | testCompile "junit:junit-dep:4.10"
41 | testCompile "org.hamcrest:hamcrest-core:1.3"
42 | testCompile "org.mockito:mockito-core:$mockitoVersion"
43 | testCompile "com.nhaarman:mockito-kotlin:1.0.1"
44 | testCompile "com.squareup.okhttp3:mockwebserver:3.3.0"
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/data/network/SlackApi.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.data.network
2 |
3 | import com.codemate.koffeemate.BuildConfig
4 | import com.codemate.koffeemate.data.models.UserListResponse
5 | import com.codemate.koffeemate.extensions.toRequestBody
6 | import okhttp3.HttpUrl
7 | import okhttp3.MultipartBody
8 | import okhttp3.RequestBody
9 | import okhttp3.ResponseBody
10 | import retrofit2.Response
11 | import retrofit2.Retrofit
12 | import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
13 | import retrofit2.converter.gson.GsonConverterFactory
14 | import retrofit2.http.*
15 | import rx.Observable
16 |
17 | interface SlackApi {
18 |
19 | @GET("users.list")
20 | fun getUsers(@Query("token") token: String): Observable
21 |
22 | @FormUrlEncoded
23 | @POST("chat.postMessage")
24 | fun postMessage(
25 | @Field("channel") channel: String,
26 | @Field("text") text: String,
27 | @Field("token") token: String = BuildConfig.SLACK_AUTH_TOKEN,
28 | @Field("as_user") asUser: Boolean = false,
29 | @Field("username") username: String = "Koffeemate",
30 | @Field("icon_emoji") icon: String = ":coffee:"
31 | ): Observable>
32 |
33 | @Multipart
34 | @POST("files.upload")
35 | fun postImage(
36 | @Part file: MultipartBody.Part,
37 | @Part("filename") filename: RequestBody,
38 | @Part("channels") channels: RequestBody,
39 | @Part("initial_comment") comment: RequestBody,
40 | @Part("token") token: RequestBody = BuildConfig.SLACK_AUTH_TOKEN.toRequestBody()
41 | ): Observable>
42 |
43 | companion object {
44 | val BASE_URL = HttpUrl.parse("https://slack.com/api/")!!
45 |
46 | fun create(baseUrl: HttpUrl): SlackApi {
47 | val retrofit = Retrofit.Builder()
48 | .baseUrl(baseUrl)
49 | .addConverterFactory(GsonConverterFactory.create())
50 | .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
51 | .build()
52 |
53 | return retrofit.create(SlackApi::class.java)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/data/models/User.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.data.models
18 |
19 | import io.realm.RealmObject
20 | import io.realm.annotations.PrimaryKey
21 |
22 | class UserListResponse {
23 | var members = listOf()
24 | }
25 |
26 | fun List.isFreshEnough(maxStaleness: Long): Boolean {
27 | val oldestAcceptedTimestamp = System.currentTimeMillis() - maxStaleness
28 | val sorted = sortedByDescending(User::last_updated)
29 | val freshestUser = sorted.first()
30 |
31 | return freshestUser.last_updated > oldestAcceptedTimestamp
32 | }
33 |
34 | open class User(
35 | @PrimaryKey
36 | open var id: String = "",
37 | open var name: String = "",
38 | open var profile: Profile = Profile(),
39 | open var real_name: String? = null,
40 | open var is_bot: Boolean = false,
41 | open var deleted: Boolean = false,
42 | open var last_updated: Long = 0
43 | ) : RealmObject() {
44 | override fun equals(other: Any?): Boolean {
45 | if (other is User) {
46 | return id == other.id
47 | }
48 |
49 | return this == other
50 | }
51 |
52 | override fun hashCode(): Int {
53 | var result = id.hashCode()
54 | result = 31 * result + name.hashCode()
55 | result = 31 * result + profile.hashCode()
56 | result = 31 * result + (real_name?.hashCode() ?: 0)
57 | result = 31 * result + is_bot.hashCode()
58 | result = 31 * result + deleted.hashCode()
59 | result = 31 * result + last_updated.hashCode()
60 | return result
61 | }
62 | }
--------------------------------------------------------------------------------
/.idea/misc.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 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codemate/koffeemate/data/local/UserRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.data.local
18 |
19 | import com.codemate.koffeemate.data.models.User
20 | import org.hamcrest.core.IsEqual.equalTo
21 | import org.junit.Assert.assertThat
22 | import org.junit.Before
23 | import org.junit.Rule
24 | import org.junit.Test
25 |
26 | class UserRepositoryTest {
27 | val TEST_USERS_UNIQUE = listOf(User(id = "abc123"), User(id = "123abc"), User(id = "a1b2c3"))
28 | val TEST_USERS_DUPLICATE = listOf(User(id = "abc123"), User(id = "abc123"), User(id = "abc123"))
29 |
30 | lateinit var userRepository: UserRepository
31 |
32 | @Rule @JvmField
33 | val realmRule = RealmTestRule()
34 |
35 | @Before
36 | fun setUp() {
37 | userRepository = RealmUserRepository()
38 | }
39 |
40 | @Test
41 | fun addAll_WhenUsersAreUnique_PersistsAllInDatabase() {
42 | userRepository.addAll(TEST_USERS_UNIQUE)
43 |
44 | val all = userRepository.getAll()
45 | assertThat(all[0].id, equalTo(TEST_USERS_UNIQUE[0].id))
46 | assertThat(all[1].id, equalTo(TEST_USERS_UNIQUE[1].id))
47 | assertThat(all[2].id, equalTo(TEST_USERS_UNIQUE[2].id))
48 | }
49 |
50 | @Test
51 | fun addAll_WhenUsersAreDuplicate_PersistsOnlyOne() {
52 | userRepository.addAll(TEST_USERS_DUPLICATE)
53 |
54 | val all = userRepository.getAll()
55 | assertThat(all.size, equalTo(1))
56 | }
57 |
58 | @Test
59 | fun addAll_WhenTryingToAddExistingUser_UpdatesIt() {
60 | userRepository.addAll(listOf(User(id = "abc123", name = "John Smith")))
61 | assertThat(userRepository.getAll().first().name, equalTo("John Smith"))
62 |
63 | userRepository.addAll(listOf(User(id = "abc123", name = "Kevin Doe")))
64 |
65 | val all = userRepository.getAll()
66 | assertThat(all.size, equalTo(1))
67 | assertThat(all.first().name, equalTo("Kevin Doe"))
68 | }
69 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/common/BrewingProgressUpdater.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.common
18 |
19 | import android.os.Handler
20 |
21 | /**
22 | * Class responsible for updating the CircularFillableLoader on
23 | * the main screen from empty to full state, since the library
24 | * doesn't support it out of the box.
25 | *
26 | * This is generalized enough for other purposes as well.
27 | *
28 | * See MainPresenter & BrewingProgressUpdateTest for usage.
29 | */
30 | open class BrewingProgressUpdater(
31 | brewingTimeMillis: Long,
32 | private val totalSteps: Int) : Runnable {
33 | var updateHandler: Handler = Handler()
34 | val updateInterval: Long = brewingTimeMillis / totalSteps
35 |
36 | var isUpdating = false
37 | var currentStep = 0
38 |
39 | var updateListener: ((Int) -> Unit)? = null
40 | var completeListener: (() -> Unit)? = null
41 |
42 | fun startUpdating(updateListener: (Int) -> Unit, completeListener: () -> Unit) {
43 | if (!isUpdating) {
44 | this.updateListener = updateListener
45 | this.completeListener = completeListener
46 |
47 | isUpdating = true
48 | updateListener(currentStep)
49 | updateHandler.postDelayed(this, updateInterval)
50 | }
51 | }
52 |
53 | fun reset() {
54 | updateListener = null
55 | completeListener = null
56 | isUpdating = false
57 | currentStep = 0
58 | updateHandler.removeCallbacks(this)
59 | }
60 |
61 | override fun run() {
62 | if (!isUpdating) {
63 | return
64 | }
65 |
66 | currentStep++
67 |
68 | if (currentStep >= totalSteps) {
69 | completeListener?.invoke()
70 | reset()
71 | } else {
72 | updateListener?.invoke(calculateCurrentProgress())
73 | updateHandler.postDelayed(this, updateInterval)
74 | }
75 | }
76 |
77 | fun calculateCurrentProgress() = Math.round((currentStep / (totalSteps * 1.0)) * 100.0).toInt()
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/userselector/UserItemAnimator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.ui.userselector
18 |
19 | import android.animation.Animator
20 | import android.animation.AnimatorListenerAdapter
21 | import android.support.v7.widget.DefaultItemAnimator
22 | import android.support.v7.widget.RecyclerView
23 | import android.view.animation.DecelerateInterpolator
24 | import com.codemate.koffeemate.ui.userselector.adapter.UserSelectorAdapter
25 | import kotlinx.android.synthetic.main.recycler_item_user.view.*
26 |
27 | class UserItemAnimator : DefaultItemAnimator() {
28 | private val interpolator = DecelerateInterpolator(3f)
29 |
30 | override fun animateAdd(viewHolder: RecyclerView.ViewHolder): Boolean {
31 | if (viewHolder is UserSelectorAdapter.ViewHolder) {
32 | viewHolder.itemView.userName.alpha = 0f
33 |
34 | viewHolder.itemView.profileImage.alpha = 0.5f
35 | viewHolder.itemView.profileImage.scaleX = 0f
36 | viewHolder.itemView.profileImage.scaleY = 0f
37 | viewHolder.itemView.profileImage.rotation = 180f
38 | viewHolder.itemView.profileImage.animate()
39 | .setInterpolator(interpolator)
40 | .setDuration(500)
41 | .setStartDelay((250 + viewHolder.layoutPosition * 75).toLong())
42 | .setListener(object : AnimatorListenerAdapter() {
43 | override fun onAnimationEnd(animation: Animator) {
44 | viewHolder.itemView.userName.animate()
45 | .alpha(1f)
46 | .withEndAction { this@UserItemAnimator.dispatchAddFinished(viewHolder) }
47 | .start()
48 | }
49 | })
50 | .alpha(1f)
51 | .scaleX(1f)
52 | .scaleY(1f)
53 | .rotation(0f)
54 | .start()
55 | }
56 |
57 | return true
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/koffeemate_preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
20 |
21 |
28 |
29 |
34 |
35 |
43 |
44 |
45 |
46 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/common/AwardBadgeCreator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.common
18 |
19 | import android.content.Context
20 | import android.graphics.*
21 | import com.codemate.koffeemate.R
22 | import com.codemate.koffeemate.extensions.saveToFile
23 | import java.io.File
24 |
25 | interface AwardBadgeCreator {
26 | fun createBitmapFileWithAward(bitmap: Bitmap, awardCount: Long): File
27 | }
28 |
29 | /**
30 | * Creates a nice little badge with a number on it, stores it to a file
31 | * and returns the File object that points to it.
32 | */
33 | class AndroidAwardBadgeCreator(private val ctx: Context) : AwardBadgeCreator {
34 | private val MARGIN = 5
35 |
36 | override fun createBitmapFileWithAward(bitmap: Bitmap, awardCount: Long): File {
37 | val scaledBitmap = Bitmap.createScaledBitmap(bitmap, 480, 480, false)
38 | val scaledBitmapCanvas = Canvas(scaledBitmap)
39 | val awardBitmap = BitmapFactory.decodeResource(ctx.resources, R.drawable.award)
40 |
41 | val awardX = (scaledBitmap.width - awardBitmap.width - MARGIN).toFloat()
42 | val awardY = (scaledBitmap.height - awardBitmap.height - MARGIN).toFloat()
43 | scaledBitmapCanvas.drawBitmap(awardBitmap, awardX, awardY, null)
44 |
45 | val shadowPaint = Paint()
46 | with(shadowPaint) {
47 | color = Color.parseColor("#55000000")
48 | textSize = 50f
49 | isAntiAlias = true
50 | typeface = Typeface.DEFAULT_BOLD
51 | style = Paint.Style.FILL
52 | textAlign = Paint.Align.CENTER
53 | }
54 |
55 | val textX = awardX + (awardBitmap.width / 2.05f)
56 | val textY = awardY + (awardBitmap.height / 2.15f)
57 | scaledBitmapCanvas.drawText(awardCount.toString(), textX, textY, shadowPaint)
58 |
59 | val foregroundPaint = shadowPaint
60 | foregroundPaint.color = Color.parseColor("#FAE9D3")
61 | scaledBitmapCanvas.drawText(awardCount.toString(), textX + 1, textY + 1, foregroundPaint)
62 |
63 | return scaledBitmap.saveToFile(ctx, "koffeemate-temp.png")
64 | }
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/views/UserSetterButton.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.views
18 |
19 | import android.animation.Animator
20 | import android.animation.AnimatorListenerAdapter
21 | import android.content.Context
22 | import android.util.AttributeSet
23 | import android.view.View
24 | import android.view.animation.AccelerateDecelerateInterpolator
25 | import com.codemate.koffeemate.R
26 | import de.hdodenhof.circleimageview.CircleImageView
27 | import org.jetbrains.anko.imageResource
28 |
29 | class UserSetterButton(ctx: Context, attrs: AttributeSet) : CircleImageView(ctx, attrs) {
30 | private val EMPTY_IMAGE_RESOURCE = R.drawable.ic_add_user
31 |
32 | init {
33 | alpha = 0f
34 | scaleX = 0f
35 | scaleY = 0f
36 | reset()
37 | }
38 |
39 | fun show() {
40 | visibility = View.VISIBLE
41 | isClickable = true
42 | animateVisibility(1f)
43 | }
44 |
45 | fun hide() {
46 | animateVisibility(0f, object : AnimatorListenerAdapter(){
47 | override fun onAnimationEnd(animation: Animator?) {
48 | reset()
49 | }
50 | })
51 | }
52 |
53 | fun animateVisibility(value: Float, listener: Animator.AnimatorListener? = null) {
54 | animate().alpha(value)
55 | .scaleX(value)
56 | .scaleY(value)
57 | .setInterpolator(AccelerateDecelerateInterpolator())
58 | .setStartDelay(100)
59 | .setDuration(200)
60 | .setListener(listener)
61 | .start()
62 | }
63 |
64 | fun clearUser() {
65 | animate().alpha(0.1f)
66 | .scaleX(0.1f)
67 | .scaleY(0.1f)
68 | .rotation(180f)
69 | .withEndAction {
70 | imageResource = EMPTY_IMAGE_RESOURCE
71 | rotation = -180f
72 |
73 | animate().alpha(1f)
74 | .scaleX(1f)
75 | .scaleY(1f)
76 | .rotation(0f)
77 | .start()
78 | }
79 | }
80 |
81 | fun reset() {
82 | imageResource = EMPTY_IMAGE_RESOURCE
83 | visibility = View.GONE
84 | isClickable = false
85 | }
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/usecases/PostAccidentUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.usecases
18 |
19 | import android.graphics.Bitmap
20 | import com.codemate.koffeemate.common.AwardBadgeCreator
21 | import com.codemate.koffeemate.data.local.CoffeeEventRepository
22 | import com.codemate.koffeemate.data.local.CoffeePreferences
23 | import com.codemate.koffeemate.data.models.User
24 | import com.codemate.koffeemate.data.network.SlackApi
25 | import com.codemate.koffeemate.extensions.toRequestBody
26 | import okhttp3.MediaType
27 | import okhttp3.MultipartBody
28 | import okhttp3.RequestBody
29 | import okhttp3.ResponseBody
30 | import retrofit2.Response
31 | import rx.Observable
32 | import rx.Scheduler
33 | import javax.inject.Inject
34 | import javax.inject.Named
35 |
36 | open class PostAccidentUseCase @Inject constructor(
37 | var slackApi: SlackApi,
38 | val coffeeEventRepository: CoffeeEventRepository,
39 | val coffeePreferences: CoffeePreferences,
40 | val awardBadgeCreator: AwardBadgeCreator,
41 | @Named("subscriber") var subscriber: Scheduler,
42 | @Named("observer") var observer: Scheduler
43 | ) {
44 | fun execute(
45 | comment: String,
46 | user: User,
47 | profilePic: Bitmap
48 | ): Observable> {
49 | coffeeEventRepository.recordBrewingAccident(user)
50 |
51 | val awardCount = coffeeEventRepository.getAccidentCountForUser(user)
52 | val profilePicWithAward = awardBadgeCreator.createBitmapFileWithAward(profilePic, awardCount)
53 |
54 | // Evaluates to "johns-certificate.png" etc
55 | val fileName = "${user.profile.first_name.toLowerCase()}s-certificate.png"
56 | val channel = coffeePreferences.getAccidentChannel()
57 |
58 | return slackApi.postImage(
59 | MultipartBody.Part.createFormData(
60 | "file",
61 | fileName,
62 | RequestBody.create(
63 | MediaType.parse("image/png"),
64 | profilePicWithAward
65 | )
66 | ),
67 | fileName.toRequestBody(),
68 | channel.toRequestBody(),
69 | comment.toRequestBody()
70 | ).subscribeOn(subscriber).observeOn(observer)
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/userselector/adapter/UserQuickDialAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.ui.userselector.adapter
18 |
19 | import android.animation.ObjectAnimator
20 | import android.view.animation.AccelerateDecelerateInterpolator
21 | import com.codemate.koffeemate.R
22 | import com.codemate.koffeemate.data.models.User
23 | import kotlinx.android.synthetic.main.recycler_item_user.view.*
24 | import org.jetbrains.anko.imageResource
25 | import org.jetbrains.anko.onClick
26 |
27 | class UserQuickDialAdapter(
28 | onUserSelectedListener: (User) -> Unit,
29 | private val onMoreClickedListener: () -> Unit) : UserSelectorAdapter(onUserSelectedListener) {
30 | val accelerateDecelerateInterpolator = AccelerateDecelerateInterpolator()
31 |
32 | private val TYPE_USER = 1
33 | private val TYPE_MORE = 2
34 |
35 | override fun getItemCount() =
36 | if (users.isEmpty()) 0
37 | else users.size + 1
38 |
39 | override fun getItemViewType(position: Int) =
40 | if (position < users.size)
41 | TYPE_USER
42 | else
43 | TYPE_MORE
44 |
45 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
46 | when (getItemViewType(position)) {
47 | TYPE_USER -> {
48 | super.onBindViewHolder(holder, position)
49 | holder.itemView.userName.text = users[position].profile.first_name
50 |
51 | }
52 | TYPE_MORE -> {
53 | holder.itemView.profileImage.imageResource = R.drawable.ic_more
54 | holder.itemView.userName.text = holder.itemView.context.getString(R.string.more)
55 | holder.itemView.onClick { onMoreClickedListener.invoke() }
56 | }
57 | }
58 |
59 | with(ObjectAnimator.ofFloat(1f, 0f)) {
60 | interpolator = accelerateDecelerateInterpolator
61 | duration = 750
62 | repeatMode = ObjectAnimator.REVERSE
63 | repeatCount = ObjectAnimator.INFINITE
64 | startDelay = (position * 250).toLong()
65 |
66 | addUpdateListener {
67 | val value = animatedValue as Float
68 |
69 | holder.itemView.apply {
70 | translationY = -value * 10
71 | }
72 | }
73 |
74 | start()
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/views/TimeAgoTextFormatter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.views
18 |
19 | import android.content.Context
20 | import com.codemate.koffeemate.R
21 | import java.util.concurrent.TimeUnit
22 |
23 | class TimeAgoTextFormatter(private val ctx: Context) {
24 | fun getHowLongAgoText(millisecondsAgo: Long): String {
25 | if (isUnder(TimeUnit.MINUTES, millisecondsAgo)) {
26 | return ctx.getString(R.string.time_just_now)
27 | } else if (isUnder(TimeUnit.HOURS, millisecondsAgo)) {
28 | val timeDifferenceMinutes = TimeUnit.MILLISECONDS.toMinutes(millisecondsAgo)
29 |
30 | return getProperFormattedText(
31 | timeDifferenceMinutes,
32 | R.string.time_one_minute_ago,
33 | R.string.time_n_minutes_ago
34 | )
35 | } else if (isUnder(TimeUnit.DAYS, millisecondsAgo)) {
36 | val timeDifferenceHours = TimeUnit.MILLISECONDS.toHours(millisecondsAgo)
37 |
38 | return getProperFormattedText(
39 | timeDifferenceHours,
40 | R.string.time_one_hour_ago,
41 | R.string.time_n_hours_ago
42 | )
43 | } else if (isUnder(TimeUnit.DAYS, howMany = 7, what = millisecondsAgo)) {
44 | val timeDifferenceDays = TimeUnit.MILLISECONDS.toDays(millisecondsAgo)
45 |
46 | return getProperFormattedText(
47 | timeDifferenceDays,
48 | R.string.time_one_day_ago,
49 | R.string.time_n_days_ago
50 | )
51 | } else {
52 | val timeDifferenceWeeks = TimeUnit.MILLISECONDS.toDays(millisecondsAgo) / 7
53 |
54 | return getProperFormattedText(
55 | timeDifferenceWeeks,
56 | R.string.time_one_week_ago,
57 | R.string.time_n_weeks_ago
58 | )
59 | }
60 | }
61 |
62 | private fun isUnder(timeUnit: TimeUnit, what: Long, howMany: Long = 1): Boolean {
63 | return what < timeUnit.toMillis(howMany)
64 | }
65 |
66 | private fun getProperFormattedText(timeDifference: Long, oneTimeUnitAgoResource: Int, manyTimeUnitsAgoResource: Int): String {
67 | if (timeDifference <= 1) {
68 | return ctx.getString(oneTimeUnitAgoResource)
69 | }
70 |
71 | return ctx.getString(manyTimeUnitsAgoResource, timeDifference)
72 | }
73 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/codemate/koffeemate/usecases/SendCoffeeAnnouncementUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.usecases
18 |
19 | import com.codemate.koffeemate.BuildConfig
20 | import com.codemate.koffeemate.data.network.SlackApi
21 | import okhttp3.ResponseBody
22 | import okhttp3.mockwebserver.MockResponse
23 | import okhttp3.mockwebserver.MockWebServer
24 | import org.hamcrest.core.IsEqual.equalTo
25 | import org.hamcrest.core.StringContains.containsString
26 | import org.junit.After
27 | import org.junit.Assert.assertThat
28 | import org.junit.Before
29 | import org.junit.Test
30 | import org.mockito.MockitoAnnotations
31 | import retrofit2.Response
32 | import rx.observers.TestSubscriber
33 | import rx.schedulers.Schedulers
34 |
35 | class SendCoffeeAnnouncementUseCaseTest {
36 | val CHANNEL_NAME = "fake-channel"
37 |
38 | lateinit var mockServer: MockWebServer
39 | lateinit var slackApi: SlackApi
40 | lateinit var useCase: SendCoffeeAnnouncementUseCase
41 | lateinit var testSubscriber: TestSubscriber>
42 |
43 | @Before
44 | fun setUp() {
45 | MockitoAnnotations.initMocks(this)
46 |
47 | mockServer = MockWebServer()
48 | mockServer.start()
49 |
50 | slackApi = SlackApi.create(mockServer.url("/"))
51 | useCase = SendCoffeeAnnouncementUseCase(
52 | slackApi,
53 | Schedulers.immediate(),
54 | Schedulers.immediate()
55 | )
56 |
57 | testSubscriber = TestSubscriber()
58 | }
59 |
60 | @After
61 | fun tearDown() {
62 | mockServer.shutdown()
63 | }
64 |
65 | @Test
66 | fun execute_MakesCorrectRequest() {
67 | mockServer.enqueue(MockResponse().setBody(""))
68 | useCase.execute(CHANNEL_NAME, "A happy message about coffee").subscribe(testSubscriber)
69 |
70 | val apiRequest = mockServer.takeRequest()
71 | assertThat(apiRequest.path, equalTo("/chat.postMessage"))
72 |
73 | val requestBody = apiRequest.body.readUtf8()
74 | assertThat(requestBody, containsString("token=${BuildConfig.SLACK_AUTH_TOKEN}"))
75 | assertThat(requestBody, containsString("channel=$CHANNEL_NAME"))
76 | assertThat(requestBody, containsString("text=A%20happy%20message%20about%20coffee"))
77 | assertThat(requestBody, containsString("as_user=false"))
78 |
79 | testSubscriber.assertCompleted()
80 | testSubscriber.assertNoErrors()
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/codemate/koffeemate/ui/userselector/UserSelectorPresenterTest.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.ui.userselector
2 |
3 | import android.graphics.Bitmap
4 | import com.codemate.koffeemate.common.AwardBadgeCreator
5 | import com.codemate.koffeemate.data.local.CoffeeEventRepository
6 | import com.codemate.koffeemate.data.local.CoffeePreferences
7 | import com.codemate.koffeemate.data.local.UserRepository
8 | import com.codemate.koffeemate.data.models.UserListResponse
9 | import com.codemate.koffeemate.data.network.SlackApi
10 | import com.codemate.koffeemate.testutils.fakeUser
11 | import com.codemate.koffeemate.testutils.getResourceFile
12 | import com.codemate.koffeemate.usecases.LoadUsersUseCase
13 | import com.nhaarman.mockito_kotlin.*
14 | import org.junit.Before
15 | import org.junit.Test
16 | import org.mockito.Mock
17 | import org.mockito.MockitoAnnotations
18 | import rx.Observable
19 | import rx.schedulers.Schedulers
20 |
21 | class UserSelectorPresenterTest {
22 | val FAKE_USERS = listOf(
23 | fakeUser(),
24 | fakeUser(),
25 | fakeUser()
26 | )
27 |
28 | @Mock
29 | lateinit var mockSlackApi: SlackApi
30 |
31 | @Mock
32 | lateinit var view: UserSelectorView
33 |
34 | @Mock
35 | lateinit var mockCoffeePreferences: CoffeePreferences
36 |
37 | @Mock
38 | lateinit var mockAwardBadgeCreator: AwardBadgeCreator
39 |
40 | @Mock
41 | lateinit var mockBitmap: Bitmap
42 |
43 | lateinit var presenter: UserSelectorPresenter
44 |
45 | @Before
46 | fun setUp() {
47 | MockitoAnnotations.initMocks(this)
48 |
49 | val loadUsersUseCase = LoadUsersUseCase(
50 | mock(),
51 | mock(),
52 | mockSlackApi,
53 | Schedulers.immediate(),
54 | Schedulers.immediate()
55 | )
56 |
57 | whenever(mockCoffeePreferences.getAccidentChannel())
58 | .thenReturn("")
59 | whenever(mockAwardBadgeCreator.createBitmapFileWithAward(any(), any()))
60 | .thenReturn(getResourceFile("images/empty.png"))
61 |
62 | presenter = UserSelectorPresenter(loadUsersUseCase)
63 | presenter.attachView(view)
64 | }
65 |
66 | @Test
67 | fun loadUsers_OnSuccess_ShowsUsersOnUI() {
68 | whenever(mockSlackApi.getUsers(any())).thenReturn(
69 | Observable.just(UserListResponse().apply { members = FAKE_USERS })
70 | )
71 |
72 | presenter.loadUsers()
73 |
74 | inOrder(view) {
75 | verify(view).showProgress()
76 | verify(view).showUsers(FAKE_USERS)
77 | verify(view).hideProgress()
78 | }
79 |
80 | verifyNoMoreInteractions(view)
81 | }
82 |
83 | @Test
84 | fun loadUsers_OnError_ShowsErrorOnUI() {
85 | whenever(mockSlackApi.getUsers(any()))
86 | .thenReturn(Observable.error(Throwable()))
87 |
88 | presenter.loadUsers()
89 |
90 | inOrder(view) {
91 | verify(view).showProgress()
92 | verify(view).showError()
93 | }
94 |
95 | verifyNoMoreInteractions(view)
96 | }
97 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/userselector/adapter/UserSelectorAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.ui.userselector.adapter
18 |
19 | import android.support.v7.util.DiffUtil
20 | import android.support.v7.widget.RecyclerView
21 | import android.view.LayoutInflater
22 | import android.view.View
23 | import android.view.ViewGroup
24 | import com.bumptech.glide.Glide
25 | import com.codemate.koffeemate.R
26 | import com.codemate.koffeemate.data.models.User
27 | import kotlinx.android.synthetic.main.recycler_item_user.view.*
28 | import org.jetbrains.anko.onClick
29 |
30 | open class UserSelectorAdapter(val onUserSelectedListener: (user: User) -> Unit) :
31 | RecyclerView.Adapter() {
32 | internal var users = emptyList()
33 |
34 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
35 | val inflater = LayoutInflater.from(parent.context)
36 | val itemView = inflater.inflate(R.layout.recycler_item_user, parent, false)
37 |
38 | return ViewHolder(itemView)
39 | }
40 |
41 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
42 | val user = users[position]
43 | holder.bind(user)
44 | }
45 |
46 | override fun getItemCount() = users.size
47 |
48 | open fun setItems(users: List) {
49 | val diffResult = DiffUtil.calculateDiff(UserDiffCallback(this.users, users))
50 | this.users = users
51 | diffResult.dispatchUpdatesTo(this)
52 | }
53 |
54 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
55 | fun bind(user: User) = with(itemView) {
56 | Glide.with(context)
57 | .load(user.profile.smallestAvailableImage)
58 | .error(R.drawable.ic_user_unknown)
59 | .into(profileImage)
60 | userName.text = user.profile.real_name
61 |
62 | onClick { onUserSelectedListener(user) }
63 | }
64 | }
65 |
66 | fun clear() {
67 | this.users = emptyList()
68 | notifyDataSetChanged()
69 | }
70 | }
71 |
72 | class UserDiffCallback(val oldUsers: List, val newUsers: List) : DiffUtil.Callback() {
73 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldUsers[oldItemPosition].id == newUsers[newItemPosition].id
74 | override fun getOldListSize() = oldUsers.size
75 | override fun getNewListSize() = newUsers.size
76 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldUsers[oldItemPosition] == newUsers[newItemPosition]
77 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
24 |
25 |
33 |
34 |
42 |
43 |
49 |
50 |
57 |
58 |
59 |
60 |
70 |
71 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/views/TimeAgoTextView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.views
18 |
19 | import android.content.Context
20 | import android.text.format.DateUtils
21 | import android.util.AttributeSet
22 | import android.widget.TextView
23 | import com.codemate.koffeemate.R
24 | import java.util.*
25 | import java.util.concurrent.TimeUnit
26 |
27 | class TimeAgoTextView : TextView, Runnable {
28 | private var customText: String? = null
29 | private var time: Long = -1
30 |
31 | lateinit var formatter: TimeAgoTextFormatter
32 |
33 | constructor(context: Context) : super(context)
34 |
35 | constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
36 | val ta = ctx.obtainStyledAttributes(attrs, R.styleable.TimeAgoTextView, 0, 0)
37 |
38 | try {
39 | customText = ta.getString(R.styleable.TimeAgoTextView_tatv_customText)
40 |
41 | if (customText == null) {
42 | customText = "%s"
43 | }
44 | } finally {
45 | ta.recycle()
46 | }
47 |
48 | formatter = TimeAgoTextFormatter(ctx)
49 | }
50 |
51 | fun setTime(date: Date) {
52 | setTime(date.time)
53 | }
54 |
55 | fun setTime(time: Long) {
56 | this.time = time
57 | startUpdatingIfNecessary()
58 | }
59 |
60 | override fun onAttachedToWindow() {
61 | super.onAttachedToWindow()
62 | startUpdatingIfNecessary()
63 | }
64 |
65 | override fun onDetachedFromWindow() {
66 | super.onDetachedFromWindow()
67 | removeCallbacks(this)
68 | }
69 |
70 | private fun startUpdatingIfNecessary() {
71 | if (time != -1L) {
72 | removeCallbacks(this)
73 | post(this)
74 | }
75 | }
76 |
77 | override fun run() {
78 | val elapsedTime = System.currentTimeMillis() - time
79 |
80 | val timeAgo = formatter.getHowLongAgoText(elapsedTime)
81 | text = String.format(customText!!, timeAgo)
82 |
83 | postDelayed(this, getUpdateInterval(elapsedTime))
84 | }
85 |
86 | companion object {
87 | internal fun getUpdateInterval(elapsedTime: Long): Long {
88 | if (elapsedTime < DateUtils.HOUR_IN_MILLIS) {
89 | return DateUtils.MINUTE_IN_MILLIS
90 | } else if (elapsedTime < DateUtils.DAY_IN_MILLIS) {
91 | return DateUtils.HOUR_IN_MILLIS
92 | } else if (elapsedTime < DateUtils.WEEK_IN_MILLIS) {
93 | return DateUtils.DAY_IN_MILLIS
94 | } else {
95 | return DateUtils.WEEK_IN_MILLIS
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/userselector/views/UserQuickDialView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.ui.userselector.views
18 |
19 | import android.content.Context
20 | import android.support.v7.widget.LinearLayoutManager
21 | import android.support.v7.widget.RecyclerView
22 | import android.util.AttributeSet
23 | import android.widget.FrameLayout
24 | import com.codemate.koffeemate.R
25 | import com.codemate.koffeemate.data.models.User
26 | import com.codemate.koffeemate.ui.userselector.UserItemAnimator
27 | import com.codemate.koffeemate.ui.userselector.UserSelectListener
28 | import com.codemate.koffeemate.ui.userselector.adapter.UserQuickDialAdapter
29 | import kotlinx.android.synthetic.main.view_user_quick_dial.view.*
30 | import org.jetbrains.anko.backgroundResource
31 | import org.jetbrains.anko.dip
32 |
33 | class UserQuickDialView : FrameLayout {
34 | private val HIDE_DELAY_MS = 10000L
35 |
36 | constructor(ctx: Context) : super(ctx)
37 | constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
38 |
39 | var userSelectorAdapter: UserQuickDialAdapter
40 | var hideOffset = 0f
41 | var resetRunnable: Runnable? = null
42 | var userSelectListener: UserSelectListener? = null
43 | var onMoreClickedListener: (() -> Unit)? = null
44 |
45 | init {
46 | layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
47 | backgroundResource = R.drawable.background_shadow
48 |
49 | inflate(context, R.layout.view_user_quick_dial, this)
50 |
51 | userSelectorAdapter = UserQuickDialAdapter(
52 | onUserSelectedListener = {
53 | reset()
54 | userSelectListener?.onUserSelected(it, UserSelectListener.Companion.REQUEST_WHOS_BREWING)
55 | },
56 | onMoreClickedListener = {
57 | reset()
58 | onMoreClickedListener?.invoke()
59 | }
60 | )
61 |
62 | quickDialRecycler.adapter = userSelectorAdapter
63 | quickDialRecycler.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
64 | quickDialRecycler.itemAnimator = UserItemAnimator()
65 |
66 | hideOffset = dip(200).toFloat()
67 | translationY = hideOffset
68 | alpha = 0f
69 | }
70 |
71 | fun setUsers(users: List, hideListener: () -> Unit) {
72 | userSelectorAdapter.setItems(users)
73 |
74 | animate().alpha(1f)
75 | .translationY(0f)
76 | .start()
77 |
78 | resetRunnable = Runnable {
79 | reset()
80 | hideListener()
81 | }
82 |
83 | handler.postDelayed(resetRunnable, HIDE_DELAY_MS)
84 | }
85 |
86 | fun reset() {
87 | handler.removeCallbacks(resetRunnable)
88 | animate().alpha(0f)
89 | .translationY(hideOffset)
90 | .withEndAction {
91 | userSelectorAdapter.clear()
92 | }
93 | .start()
94 | }
95 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/usecases/LoadUsersUseCase.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.usecases
18 |
19 | import com.codemate.koffeemate.BuildConfig
20 | import com.codemate.koffeemate.data.local.CoffeeEventRepository
21 | import com.codemate.koffeemate.data.local.UserRepository
22 | import com.codemate.koffeemate.data.models.User
23 | import com.codemate.koffeemate.data.models.isFreshEnough
24 | import com.codemate.koffeemate.data.network.SlackApi
25 | import rx.Observable
26 | import rx.Scheduler
27 | import java.util.concurrent.TimeUnit
28 | import javax.inject.Inject
29 | import javax.inject.Named
30 |
31 | open class LoadUsersUseCase @Inject constructor(
32 | var userRepository: UserRepository,
33 | var coffeeEventRepository: CoffeeEventRepository,
34 | var slackApi: SlackApi,
35 | @Named("subscriber") var subscriber: Scheduler,
36 | @Named("observer") var observer: Scheduler
37 | ) {
38 | val MAX_CACHE_STALENESS = TimeUnit.HOURS.toMillis(12)
39 |
40 | fun execute(): Observable> {
41 | val currentTime = System.currentTimeMillis()
42 | val cachedUsers = Observable.just(userRepository.getAll())
43 | val networkUsers = slackApi.getUsers(BuildConfig.SLACK_AUTH_TOKEN)
44 | .flatMap { userResponse ->
45 | val usersWithTimestamp = userResponse.members.toMutableList()
46 | usersWithTimestamp.forEach {
47 | it.last_updated = currentTime
48 | }
49 |
50 | Observable.just(usersWithTimestamp)
51 | }
52 | .subscribeOn(subscriber)
53 | .map { filterNonCompanyUsers(it) }
54 | .doOnNext { userRepository.addAll(it) }
55 |
56 | return Observable
57 | .concat(cachedUsers, networkUsers)
58 | .first {
59 | it.isNotEmpty() && it.isFreshEnough(MAX_CACHE_STALENESS)
60 | }
61 | .map { brewersFirst(it) }
62 | .observeOn(observer)
63 | }
64 |
65 | private fun filterNonCompanyUsers(response: List): List {
66 | return response.filter {
67 | !it.is_bot
68 | // At Codemate, profiles starting with "Ext-" aren't employees,
69 | // but customers instead: they don't hang out in the office.
70 | && !it.profile.first_name.toLowerCase().startsWith("ext-")
71 | && it.real_name != "slackbot"
72 | && !it.deleted
73 | }
74 | }
75 |
76 | private fun brewersFirst(users: List): List {
77 | val brewers = coffeeEventRepository
78 | .getAllBrewers()
79 | .sortedBy { it.profile.real_name }
80 |
81 | val all = users.toMutableList()
82 | all.removeAll { brewers.contains(it) }
83 | all.sortBy { it.profile.real_name }
84 |
85 | return brewers + all
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/userselector/views/UserSelectorOverlay.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.ui.userselector.views
18 |
19 | import android.content.Context
20 | import android.support.v7.widget.GridLayoutManager
21 | import android.util.AttributeSet
22 | import android.view.View
23 | import android.view.ViewGroup
24 | import android.widget.FrameLayout
25 | import com.codemate.koffeemate.KoffeemateApp
26 | import com.codemate.koffeemate.R
27 | import com.codemate.koffeemate.data.models.User
28 | import com.codemate.koffeemate.ui.userselector.UserItemAnimator
29 | import com.codemate.koffeemate.ui.userselector.UserSelectListener
30 | import com.codemate.koffeemate.ui.userselector.UserSelectorPresenter
31 | import com.codemate.koffeemate.ui.userselector.UserSelectorView
32 | import com.codemate.koffeemate.ui.userselector.adapter.UserSelectorAdapter
33 | import kotlinx.android.synthetic.main.view_user_selector.view.*
34 | import org.jetbrains.anko.onClick
35 | import javax.inject.Inject
36 |
37 | class UserSelectorOverlay : FrameLayout, UserSelectorView {
38 | private lateinit var userSelectorAdapter: UserSelectorAdapter
39 |
40 | var requestCode: Int = 0
41 | var userSelectListener: UserSelectListener? = null
42 |
43 | @Inject
44 | lateinit var presenter: UserSelectorPresenter
45 |
46 | constructor(ctx: Context) : super(ctx)
47 | constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
48 |
49 | init {
50 | inflate(context, R.layout.view_user_selector, this)
51 | alpha = 0f
52 | }
53 |
54 | override fun onAttachedToWindow() {
55 | super.onAttachedToWindow()
56 | KoffeemateApp.appComponent.inject(this)
57 |
58 | setUpUserRecycler()
59 |
60 | presenter.attachView(this)
61 | presenter.loadUsers()
62 |
63 | errorLayout.tryAgain.onClick {
64 | presenter.loadUsers()
65 | }
66 |
67 | animate().alpha(1f).start()
68 | }
69 |
70 | override fun onDetachedFromWindow() {
71 | super.onDetachedFromWindow()
72 | presenter.detachView()
73 | }
74 |
75 | private fun setUpUserRecycler() {
76 | userSelectorAdapter = UserSelectorAdapter { user ->
77 | userSelectListener?.onUserSelected(user, requestCode)
78 | (parent as ViewGroup).removeView(this)
79 | }
80 |
81 | userRecycler.adapter = userSelectorAdapter
82 | userRecycler.layoutManager = GridLayoutManager(context, 4)
83 | userRecycler.itemAnimator = UserItemAnimator()
84 | }
85 |
86 | override fun showProgress() {
87 | progress.visibility = View.VISIBLE
88 | errorLayout.visibility = View.GONE
89 | }
90 |
91 | override fun hideProgress() {
92 | progress.visibility = View.GONE
93 | errorLayout.visibility = View.GONE
94 | }
95 |
96 | override fun showError() {
97 | progress.visibility = View.GONE
98 | errorLayout.visibility = View.VISIBLE
99 | }
100 |
101 | override fun showUsers(users: List) {
102 | userSelectorAdapter.setItems(users)
103 | }
104 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codemate/koffeemate/data/local/CoffeePreferencesTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.data.local
18 |
19 | import android.support.test.InstrumentationRegistry
20 | import android.support.test.runner.AndroidJUnit4
21 | import junit.framework.Assert.assertFalse
22 | import junit.framework.Assert.assertTrue
23 | import org.hamcrest.core.IsEqual.equalTo
24 | import org.junit.Assert.assertThat
25 | import org.junit.Before
26 | import org.junit.Test
27 | import org.junit.runner.RunWith
28 |
29 | @RunWith(AndroidJUnit4::class)
30 | class CoffeePreferencesTest {
31 | lateinit var coffeePreferences: CoffeePreferences
32 |
33 | @Before
34 | fun setUp() {
35 | coffeePreferences = CoffeePreferences(InstrumentationRegistry.getTargetContext())
36 | coffeePreferences.preferences.edit().clear().apply()
37 | }
38 |
39 | @Test
40 | fun getCoffeeBrewingTime_DefaultsToSevenMinutes() {
41 | assertThat(coffeePreferences.getCoffeeBrewingTime(), equalTo(7L))
42 | }
43 |
44 | @Test
45 | fun getCoffeeBrewingTime_ReturnsProperLongValue() {
46 | coffeePreferences.preferences.edit()
47 | .putString(coffeePreferences.coffeeBrewingTimeKey, "4")
48 | .apply()
49 |
50 | assertThat(coffeePreferences.getCoffeeBrewingTime(), equalTo(4L))
51 | }
52 |
53 | @Test
54 | fun isCoffeeAnnouncementChannelSet_WhenNotSet_ReturnsFalse() {
55 | assertFalse(coffeePreferences.isCoffeeAnnouncementChannelSet())
56 | }
57 |
58 | @Test
59 | fun isCoffeeAnnouncementChannelSet_WhenIsSet_ReturnsTrue() {
60 | putString(coffeePreferences.announcementChannelKey, "test")
61 | assertTrue(coffeePreferences.isCoffeeAnnouncementChannelSet())
62 | }
63 |
64 | @Test
65 | fun isAccidentChannelSet_WhenNotSet_ReturnsFalse() {
66 | assertFalse(coffeePreferences.isAccidentChannelSet())
67 | }
68 |
69 | @Test
70 | fun isAccidentChannelSet_WhenIsSet_ButNotUsingSeparateChannels_ReturnsFalse() {
71 | putBoolean(coffeePreferences.differentChannelForAccidentsKey, false)
72 | putString(coffeePreferences.accidentChannelKey, "test")
73 |
74 | assertFalse(coffeePreferences.isAccidentChannelSet())
75 | }
76 |
77 | @Test
78 | fun isAccidentChannelSet_WhenIsSet_AndUsingSeparateChannels_ReturnsTrue() {
79 | putBoolean(coffeePreferences.differentChannelForAccidentsKey, true)
80 | putString(coffeePreferences.accidentChannelKey, "test")
81 |
82 | assertTrue(coffeePreferences.isAccidentChannelSet())
83 | }
84 |
85 | @Test
86 | fun isAccidentChannelSet_WhenAnnouncementChannelSet_AndNotUsingSeparateChannels_ReturnsTrue() {
87 | putBoolean(coffeePreferences.differentChannelForAccidentsKey, false)
88 | putString(coffeePreferences.announcementChannelKey, "test")
89 |
90 | assertTrue(coffeePreferences.isAccidentChannelSet())
91 | }
92 |
93 | private fun putString(key: String, value: String) {
94 | coffeePreferences.preferences.edit()
95 | .putString(key, value)
96 | .apply()
97 | }
98 |
99 | private fun putBoolean(key: String, value: Boolean) {
100 | coffeePreferences.preferences.edit()
101 | .putBoolean(key, value)
102 | .apply()
103 | }
104 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 
2 |
3 | [](https://travis-ci.org/CodemateLtd/Koffeemate)
4 |
5 | 
6 |
7 | # What?
8 | We at [Codemate](http://www.codemate.com/) **love** coffee. Numerous cups of that sweet black nectar are brewed every day at our office. Coffee is what keeps us productive, creative and especially on Mondays, awake. Simply put, we just couldn't function without it.
9 |
10 | # Okay, but still, what?
11 | Koffeemate was made for three purposes:
12 |
13 | 1. Informing others on Slack when freshly brewed coffee is available
14 | 2. Gathering interesting data of our coffee consumption
15 | 3. Publicly shaming those who leave a giant mess behind while they try to brew coffee.
16 |
17 | This project is also a great opportunity to practice some Android testing and architecture skills.
18 |
19 | # How does it work?
20 | The system is very elegant: we have a cheap Android phone glued to the wall next to our coffee machine. Running this app is the only thing that phone can do. We made this extra secure by taping some cardboard over the physical buttons.
21 |
22 | Every time someone starts the coffee machine, they also press the coffee pot button on the center of the screen. After exactly 7 minutes, which is the most appropriate delay we've found, everyone in the special Slack channel gets notified.
23 |
24 | However, if someone fails the coffee brewing process, they can be publicly shamed by using the "Log an accident" button.
25 |
26 | # That's neat! We want it too!
27 | Of course you do. Here's the steps to get it working:
28 |
29 | ## Create a bot user on Slack
30 | 1. Go to [the custom integrations page on Slack](https://api.slack.com/custom-integrations), and click the ```Create a bot user``` button.
31 | 2. Click the green ```Add Configuration``` button on the left.
32 | 3. Choose a username for your bot and click ```Add bot integration```.
33 | 4. Configure your bot the way you like. **Take note of the API token, you'll need it next.**
34 | 5. **IMPORTANT:** Invite the newly-made bot to any channels you would like the coffee announcements to be made on.
35 |
36 | ## Make it work with Koffeemate
37 | 1. Change to a folder of your liking and do a ```git clone https://github.com/CodemateLtd/Koffeemate.git```
38 | 2. **Don't open the project yet.**
39 | 3. Create an empty ```koffeemate.properties``` file in your **app module** with the following contents:
40 |
41 | **Koffeemate/app/koffeemate.properties:**
42 | ```groovy
43 | SLACK_AUTH_TOKEN = your_api_token // Replace with the actual token, without quotation ("") marks
44 | ```
45 |
46 | Now you can open the project in Android Studio. Make sure you have the Kotlin plugin installed.
47 |
48 | Install the app to an old phone, glue it to a wall near a coffee machine and enjoy!
49 |
50 | # Contributing
51 |
52 | We'd love to have you contribute, and we do not have any strict rules.
53 |
54 | However, here's some tips for a great start:
55 |
56 | * We love PR's related to test coverage / code cleanliness improvements.
57 | * Out of ideas? Look for the [issue tracker](https://github.com/CodemateLtd/Koffeemate/issues) for something to do. Tell if you want to do something, and we'll assign it to you.
58 | * If you have something big in mind, create an issue first. Major functionality changes might not necessarily get merged.
59 |
60 | # License
61 |
62 | ```
63 | Copyright 2016 Codemate Ltd
64 |
65 | Licensed under the Apache License, Version 2.0 (the "License");
66 | you may not use this file except in compliance with the License.
67 | You may obtain a copy of the License at
68 |
69 | http://www.apache.org/licenses/LICENSE-2.0
70 |
71 | Unless required by applicable law or agreed to in writing, software
72 | distributed under the License is distributed on an "AS IS" BASIS,
73 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
74 | See the License for the specific language governing permissions and
75 | limitations under the License.
76 | ```
77 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codemate/koffeemate/data/local/MigrationTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.data.local
18 |
19 | import android.content.Context
20 | import android.support.test.InstrumentationRegistry
21 | import com.codemate.koffeemate.data.models.CoffeeBrewingEvent
22 | import io.realm.Realm
23 | import io.realm.RealmConfiguration
24 | import org.hamcrest.core.IsEqual.equalTo
25 | import org.hamcrest.core.IsNull.nullValue
26 | import org.junit.Assert.assertThat
27 | import org.junit.Before
28 | import org.junit.Test
29 | import java.io.File
30 | import java.io.IOException
31 |
32 | class MigrationTest {
33 | lateinit var context: Context
34 |
35 | @Before
36 | fun setUp() {
37 | context = InstrumentationRegistry.getContext()
38 | }
39 |
40 | /*************************************************************
41 | * Migration tests from schema version 0 to 1
42 | *************************************************************/
43 | @Test
44 | fun testMigrationFromVersionZeroToOne() {
45 | val config = RealmConfiguration.Builder()
46 | .name("test.realm")
47 | .schemaVersion(1)
48 | .migration(Migration())
49 | .build()
50 |
51 | // The "sample-db-schema-v0.realm" contains three sample records,
52 | // in the old database schema, which used userIds instead of User
53 | // objects.
54 | copyRealmFromAssets(context, "sample-db-schema-v0.realm", config)
55 | val realm = Realm.getInstance(config)
56 |
57 | val all = realm.where(CoffeeBrewingEvent::class.java).findAll()
58 | assertThat(all.size, equalTo(3))
59 |
60 | val brewingEventWithoutUserId = all[0]
61 | assertThat(brewingEventWithoutUserId.id, equalTo("adf9c9b9-e521-462f-9d67-ff2a11d7b62c"))
62 | assertThat(brewingEventWithoutUserId.time, equalTo(1485872637115L))
63 | assertThat(brewingEventWithoutUserId.isSuccessful, equalTo(true))
64 | assertThat(brewingEventWithoutUserId.user, nullValue())
65 |
66 | val brewingEventWithUserId = all[1]
67 | assertThat(brewingEventWithUserId.id, equalTo("0e742762-7181-4bc0-b7b5-d1ff68991dd6"))
68 | assertThat(brewingEventWithUserId.time, equalTo(1485872637117L))
69 | assertThat(brewingEventWithUserId.isSuccessful, equalTo(true))
70 | assertThat(brewingEventWithUserId.user!!.id, equalTo("abc-123"))
71 | assertThat(brewingEventWithUserId.user!!.last_updated, equalTo(0L))
72 |
73 | val brewingAccident = all[2]
74 | assertThat(brewingAccident.id, equalTo("480bb3b9-a01f-45cb-87cd-113465d4038a"))
75 | assertThat(brewingAccident.time, equalTo(1485872637118L))
76 | assertThat(brewingAccident.isSuccessful, equalTo(false))
77 | assertThat(brewingAccident.user!!.id, equalTo("abc-123"))
78 | assertThat(brewingEventWithUserId.user!!.last_updated, equalTo(0L))
79 |
80 | // Make sure we don't generate different timestamps for the users in
81 | // this new schema.
82 | assertThat(brewingEventWithUserId.user!!.last_updated, equalTo(brewingAccident.user!!.last_updated))
83 |
84 | realm.close()
85 | Realm.deleteRealm(config)
86 | }
87 |
88 | @Throws(IOException::class)
89 | fun copyRealmFromAssets(context: Context, realmPath: String, config: RealmConfiguration) {
90 | Realm.deleteRealm(config)
91 |
92 | context.assets.open(realmPath).use { inputStream ->
93 | val outFile = File(config.realmDirectory, config.realmFileName)
94 |
95 | outFile.outputStream().use { outputStream ->
96 | inputStream.copyTo(outputStream)
97 | }
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/test/java/com/codemate/koffeemate/views/TimeAgoTextFormatterTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.views
18 |
19 | import android.content.Context
20 | import com.codemate.koffeemate.R
21 | import com.nhaarman.mockito_kotlin.mock
22 | import com.nhaarman.mockito_kotlin.whenever
23 | import org.hamcrest.core.IsEqual
24 | import org.junit.Assert
25 | import org.junit.Before
26 | import org.junit.Test
27 | import java.util.concurrent.TimeUnit
28 |
29 | class TimeAgoTextFormatterTest {
30 | lateinit var mockContext: Context
31 | lateinit var formatter: TimeAgoTextFormatter
32 |
33 | @Before
34 | fun setUp() {
35 | mockContext = mock()
36 | formatter = TimeAgoTextFormatter(mockContext)
37 |
38 | // Yup, avoiding those Android instrumentation tests at any cost ¯\_(ツ)_/¯
39 | returnStringForStringResource(R.string.time_just_now, "just now")
40 | returnStringForStringResource(R.string.time_one_minute_ago, "a minute ago")
41 | returnStringForStringResource(R.string.time_n_minutes_ago, 59, "%d minutes ago")
42 | returnStringForStringResource(R.string.time_one_hour_ago, "an hour ago")
43 | returnStringForStringResource(R.string.time_n_hours_ago, 23, "%d hours ago")
44 | returnStringForStringResource(R.string.time_one_day_ago, "a day ago")
45 | returnStringForStringResource(R.string.time_n_days_ago, 6, "%d days ago")
46 | returnStringForStringResource(R.string.time_one_week_ago, "a week ago")
47 | returnStringForStringResource(R.string.time_n_weeks_ago, 3, "%d weeks ago")
48 | }
49 |
50 | @Test
51 | fun justNowTests() {
52 | val justNowOne = formatter.getHowLongAgoText(0)
53 | Assert.assertThat(justNowOne, IsEqual.equalTo("just now"))
54 |
55 | val justNowTwo = formatter.getHowLongAgoText(TimeUnit.SECONDS.toMillis(59))
56 | Assert.assertThat(justNowTwo, IsEqual.equalTo("just now"))
57 | }
58 |
59 | @Test
60 | fun minuteTests() {
61 | val oneMinuteAgo = formatter.getHowLongAgoText(TimeUnit.MINUTES.toMillis(1))
62 | Assert.assertThat(oneMinuteAgo, IsEqual.equalTo("a minute ago"))
63 |
64 | val minutesAgo = formatter.getHowLongAgoText(TimeUnit.MINUTES.toMillis(59))
65 | Assert.assertThat(minutesAgo, IsEqual.equalTo("59 minutes ago"))
66 | }
67 |
68 | @Test
69 | fun hourTests() {
70 | val oneHourAgo = formatter.getHowLongAgoText(TimeUnit.HOURS.toMillis(1))
71 | Assert.assertThat(oneHourAgo, IsEqual.equalTo("an hour ago"))
72 |
73 | val hoursAgo = formatter.getHowLongAgoText(TimeUnit.HOURS.toMillis(23))
74 | Assert.assertThat(hoursAgo, IsEqual.equalTo("23 hours ago"))
75 | }
76 |
77 | @Test
78 | fun dayTests() {
79 | val oneDayAgo = formatter.getHowLongAgoText(TimeUnit.DAYS.toMillis(1))
80 | Assert.assertThat(oneDayAgo, IsEqual.equalTo("a day ago"))
81 |
82 | val daysAgo = formatter.getHowLongAgoText(TimeUnit.DAYS.toMillis(6))
83 | Assert.assertThat(daysAgo, IsEqual.equalTo("6 days ago"))
84 | }
85 |
86 | @Test
87 | fun weekTests() {
88 | val oneWeekAgo = formatter.getHowLongAgoText(TimeUnit.DAYS.toMillis(7))
89 | Assert.assertThat(oneWeekAgo, IsEqual.equalTo("a week ago"))
90 |
91 | val weeksAgo = formatter.getHowLongAgoText(TimeUnit.DAYS.toMillis(27))
92 | Assert.assertThat(weeksAgo, IsEqual.equalTo("3 weeks ago"))
93 | }
94 |
95 | private fun returnStringForStringResource(stringRes: Int, returnedValue: String) {
96 | whenever(mockContext.getString(stringRes)).thenReturn(returnedValue)
97 | }
98 |
99 | private fun returnStringForStringResource(stringRes: Int, howMany: Long, returnedValue: String) {
100 | whenever(mockContext.getString(stringRes, howMany))
101 | .thenReturn(String.format(returnedValue, howMany))
102 | }
103 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/codemate/koffeemate/common/BrewingProgressUpdaterTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.common
18 |
19 | import android.os.Handler
20 | import com.codemate.koffeemate.common.BrewingProgressUpdater
21 | import com.nhaarman.mockito_kotlin.mock
22 | import com.nhaarman.mockito_kotlin.times
23 | import com.nhaarman.mockito_kotlin.verify
24 | import com.nhaarman.mockito_kotlin.verifyNoMoreInteractions
25 | import org.hamcrest.core.IsEqual.equalTo
26 | import org.junit.Assert.*
27 | import org.junit.Before
28 | import org.junit.Test
29 | import java.util.concurrent.TimeUnit
30 |
31 | class BrewingProgressUpdaterTest {
32 | lateinit var mockHandler: Handler
33 |
34 | @Before
35 | fun setUp() {
36 | mockHandler = mock()
37 | }
38 |
39 | @Test
40 | fun calculatesCorrectUpdateIntervals() {
41 | val sutOne = createUpdater(TimeUnit.SECONDS.toMillis(5), 10)
42 | assertThat(sutOne.updateInterval, equalTo(500L))
43 |
44 | val sutTwo = createUpdater(TimeUnit.SECONDS.toMillis(9), 4)
45 | assertThat(sutTwo.updateInterval, equalTo(2250L))
46 | }
47 |
48 | @Test
49 | fun reset_ShouldCleanStateAndRemoveCallbacks() {
50 | val sut = createUpdater(1, 1)
51 |
52 | sut.startUpdating({}, {})
53 | verify(mockHandler, times(1)).postDelayed(sut, 1)
54 |
55 | sut.reset()
56 |
57 | assertNull(sut.updateListener)
58 | assertNull(sut.completeListener)
59 | assertFalse(sut.isUpdating)
60 | assertThat(sut.currentStep, equalTo(0))
61 |
62 | verify(mockHandler, times(1)).removeCallbacks(sut)
63 | verifyNoMoreInteractions(mockHandler)
64 | }
65 |
66 | @Test
67 | fun startUpdating_CallsPostDelayed() {
68 | val sut = createUpdater(1, 1)
69 | sut.startUpdating({}, {})
70 |
71 | verify(mockHandler).postDelayed(sut, 1)
72 | verifyNoMoreInteractions(mockHandler)
73 | }
74 |
75 | @Test
76 | fun startUpdating_WhenCalledMultipleTimes_CallsPostDelayedOnlyOnce() {
77 | val sut = createUpdater(1, 1)
78 |
79 | sut.startUpdating({}, {})
80 | sut.startUpdating({}, {})
81 | sut.startUpdating({}, {})
82 |
83 | verify(mockHandler, times(1)).postDelayed(sut, 1)
84 | verifyNoMoreInteractions(mockHandler)
85 | }
86 |
87 | /**
88 | * If you come up with a better name, just send a PR ¯\_(ツ)_/¯
89 | */
90 | @Test
91 | fun shouldRunProperly() {
92 | val sut = createUpdater(TimeUnit.SECONDS.toMillis(2), 4)
93 | sut.startUpdating({}, {})
94 |
95 | sut.run()
96 | assertThat(sut.currentStep, equalTo(1))
97 | assertThat(sut.calculateCurrentProgress(), equalTo(25))
98 |
99 | sut.run()
100 | sut.run()
101 | assertThat(sut.currentStep, equalTo(3))
102 | assertThat(sut.calculateCurrentProgress(), equalTo(75))
103 |
104 | sut.run()
105 | assertThat(sut.currentStep, equalTo(0))
106 | assertThat(sut.calculateCurrentProgress(), equalTo(0))
107 |
108 | verify(mockHandler, times(4)).postDelayed(sut, 500)
109 | verify(mockHandler, times(1)).removeCallbacks(sut)
110 | verifyNoMoreInteractions(mockHandler)
111 | }
112 |
113 | @Test
114 | fun run_WhenIsUpdatingEqualsFalse_DoesNothing() {
115 | val sut = createUpdater(TimeUnit.SECONDS.toMillis(5), 15)
116 |
117 | sut.startUpdating({}, {})
118 | sut.isUpdating = false
119 |
120 | sut.run()
121 | sut.run()
122 | sut.run()
123 |
124 | assertThat(sut.currentStep, equalTo(0))
125 | }
126 |
127 | fun createUpdater(totalTimeMillis: Long, totalSteps: Int) : BrewingProgressUpdater {
128 | val updater = BrewingProgressUpdater(totalTimeMillis, totalSteps)
129 | updater.updateHandler = mockHandler
130 |
131 | return updater
132 | }
133 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/codemate/koffeemate/usecases/PostAccidentUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.usecases
18 |
19 | import android.content.SharedPreferences
20 | import android.graphics.Bitmap
21 | import com.codemate.koffeemate.BuildConfig
22 | import com.codemate.koffeemate.common.AwardBadgeCreator
23 | import com.codemate.koffeemate.data.local.CoffeeEventRepository
24 | import com.codemate.koffeemate.data.local.CoffeePreferences
25 | import com.codemate.koffeemate.data.network.SlackApi
26 | import com.codemate.koffeemate.testutils.RegexMatcher.Companion.matchesPattern
27 | import com.codemate.koffeemate.testutils.fakeUser
28 | import com.codemate.koffeemate.testutils.getResourceFile
29 | import com.nhaarman.mockito_kotlin.*
30 | import okhttp3.ResponseBody
31 | import okhttp3.mockwebserver.MockResponse
32 | import okhttp3.mockwebserver.MockWebServer
33 | import org.hamcrest.core.StringContains
34 | import org.junit.After
35 | import org.junit.Assert.assertThat
36 | import org.junit.Before
37 | import org.junit.Test
38 | import org.mockito.Mock
39 | import org.mockito.MockitoAnnotations
40 | import retrofit2.Response
41 | import rx.observers.TestSubscriber
42 | import rx.schedulers.Schedulers
43 |
44 | class PostAccidentUseCaseTest {
45 | @Mock
46 | lateinit var mockCoffeePreferences: CoffeePreferences
47 |
48 | @Mock
49 | lateinit var mockCoffeeEventRepository: CoffeeEventRepository
50 |
51 | @Mock
52 | lateinit var mockAwardBadgeCreator: AwardBadgeCreator
53 |
54 | @Mock
55 | lateinit var mockBitmap: Bitmap
56 |
57 | lateinit var mockServer: MockWebServer
58 | lateinit var slackApi: SlackApi
59 | lateinit var useCase: PostAccidentUseCase
60 | lateinit var testSubscriber: TestSubscriber>
61 |
62 | @Before
63 | fun setUp() {
64 | MockitoAnnotations.initMocks(this)
65 |
66 | mockServer = MockWebServer()
67 | mockServer.start()
68 | mockServer.enqueue(MockResponse().setBody(""))
69 |
70 | mockCoffeePreferences.preferences = mock()
71 | whenever(mockCoffeePreferences.getAccidentChannel()).thenReturn("test-channel")
72 | whenever(mockCoffeeEventRepository.getAccidentCountForUser(any())).thenReturn(1)
73 |
74 | whenever(mockAwardBadgeCreator.createBitmapFileWithAward(mockBitmap, 1))
75 | .thenReturn(getResourceFile("images/empty.png"))
76 |
77 | slackApi = SlackApi.create(mockServer.url("/"))
78 | useCase = PostAccidentUseCase(
79 | slackApi,
80 | mockCoffeeEventRepository,
81 | mockCoffeePreferences,
82 | mockAwardBadgeCreator,
83 | Schedulers.immediate(),
84 | Schedulers.immediate()
85 | )
86 |
87 | testSubscriber = TestSubscriber>()
88 | }
89 |
90 | @After
91 | fun tearDown() {
92 | mockServer.shutdown()
93 | }
94 |
95 | @Test
96 | fun announceCoffeeBrewingAccident_ShouldMakeCorrectRequest() {
97 | val user = fakeUser()
98 | useCase.execute("Test comment", user, mockBitmap).subscribe(testSubscriber)
99 |
100 | // TODO: There has to be a better way to verify these multipart post params, right? :S
101 | val requestBody = mockServer.takeRequest().body.readUtf8()
102 | assertThat(requestBody, StringContains.containsString("filename=\"jormas-certificate.png\""))
103 | assertThat(requestBody, matchesPattern(".*channels.*test-channel.*"))
104 | assertThat(requestBody, matchesPattern(".*initial_comment.*Test comment.*"))
105 | assertThat(requestBody, matchesPattern(".*token.*${BuildConfig.SLACK_AUTH_TOKEN}.*"))
106 | }
107 |
108 | @Test
109 | fun announceCoffeeBrewingAccident_WhenSuccessful_NotifiesUIAndStoresEvent() {
110 | val user = fakeUser()
111 | useCase.execute("", user, mockBitmap).subscribe(testSubscriber)
112 |
113 | testSubscriber.assertValueCount(1)
114 | testSubscriber.assertCompleted()
115 |
116 | verify(mockCoffeeEventRepository).recordBrewingAccident(user)
117 | verify(mockCoffeeEventRepository).getAccidentCountForUser(user)
118 | verifyNoMoreInteractions(mockCoffeeEventRepository)
119 | }
120 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/common/ScreenSaver.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.common
18 |
19 | import android.app.Activity
20 | import android.app.AlarmManager
21 | import android.app.PendingIntent
22 | import android.content.BroadcastReceiver
23 | import android.content.Context
24 | import android.content.Intent
25 | import android.content.IntentFilter
26 | import android.view.View
27 | import android.view.ViewGroup
28 | import com.codemate.koffeemate.R
29 | import org.jetbrains.anko.alarmManager
30 | import org.jetbrains.anko.onClick
31 | import java.util.*
32 | import java.util.concurrent.TimeUnit
33 |
34 | interface ScreenSaver {
35 | fun defer()
36 | fun start()
37 | fun stop()
38 | }
39 |
40 | open class AndroidScreenSaver constructor(private val activity: Activity) : ScreenSaver {
41 | private val ACTION_ENABLE_SCREEN_SAVER = "com.codemate.brewstat.ACTION_ENABLE_SCREEN_SAVER"
42 | private val ACTION_DISABLE_SCREEN_SAVER = "com.codemate.brewstat.ACTION_DISABLE_SCREEN_SAVER"
43 |
44 | private val receiver = object : BroadcastReceiver() {
45 | override fun onReceive(ctx: Context, intent: Intent) {
46 | if (isScreenSavingTime()) {
47 | screenOverlay.visibility = View.VISIBLE
48 | } else {
49 | screenOverlay.visibility = View.GONE
50 | }
51 | }
52 | }
53 |
54 | private val showScreenSaverRunnable = {
55 | if (isScreenSavingTime()) {
56 | screenOverlay.visibility = View.VISIBLE
57 | }
58 | }
59 |
60 | private lateinit var screenOverlay: View
61 |
62 | init {
63 | initializeOverlay()
64 | }
65 |
66 | override fun start() {
67 | registerScreenSaverReceiver()
68 | scheduleDailyAlarms()
69 | }
70 |
71 | override fun stop() {
72 | activity.unregisterReceiver(receiver)
73 | }
74 |
75 | override fun defer() {
76 | screenOverlay.removeCallbacks(showScreenSaverRunnable)
77 | showScreenSaverDelayedIfNecessary()
78 | }
79 |
80 | private fun showScreenSaverDelayedIfNecessary() {
81 | screenOverlay.removeCallbacks(showScreenSaverRunnable)
82 | screenOverlay.postDelayed(showScreenSaverRunnable, TimeUnit.MINUTES.toMillis(5))
83 | }
84 |
85 | private fun isScreenSavingTime(): Boolean {
86 | val calendar = Calendar.getInstance()
87 | calendar.timeInMillis = System.currentTimeMillis()
88 |
89 | val currentHour = calendar.get(Calendar.HOUR_OF_DAY)
90 | return !(currentHour > 8 && currentHour < 16)
91 | }
92 |
93 | private fun registerScreenSaverReceiver() {
94 | val intentFilter = IntentFilter(ACTION_ENABLE_SCREEN_SAVER)
95 | activity.registerReceiver(receiver, intentFilter)
96 | }
97 |
98 | private fun initializeOverlay() {
99 | val rootView = activity.window.decorView as ViewGroup
100 | screenOverlay = activity.layoutInflater.inflate(R.layout.view_screen_saver_overlay, rootView, false)
101 | screenOverlay.visibility = View.GONE
102 | rootView.addView(screenOverlay)
103 |
104 | screenOverlay.onClick {
105 | screenOverlay.visibility = View.GONE
106 | showScreenSaverDelayedIfNecessary()
107 | }
108 | }
109 |
110 | private fun scheduleDailyAlarms() {
111 | scheduleAlarm(ACTION_ENABLE_SCREEN_SAVER, 16)
112 | scheduleAlarm(ACTION_DISABLE_SCREEN_SAVER, 8)
113 | }
114 |
115 | private fun scheduleAlarm(action: String, hourOfDay: Int) {
116 | val alarmIntent = getIntent(action)
117 | val calendar = Calendar.getInstance()
118 | calendar.timeInMillis = System.currentTimeMillis()
119 | calendar.set(Calendar.HOUR_OF_DAY, hourOfDay)
120 | calendar.set(Calendar.MINUTE, 0)
121 |
122 | activity.alarmManager.setRepeating(
123 | AlarmManager.RTC_WAKEUP,
124 | calendar.timeInMillis,
125 | AlarmManager.INTERVAL_DAY,
126 | alarmIntent
127 | )
128 | }
129 |
130 | private fun getIntent(action: String): PendingIntent? {
131 | return PendingIntent.getBroadcast(
132 | activity,
133 | 0,
134 | Intent(action),
135 | PendingIntent.FLAG_UPDATE_CURRENT
136 | )
137 | }
138 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/data/local/CoffeeEventRepository.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.data.local
2 |
3 | import com.codemate.koffeemate.data.models.CoffeeBrewingEvent
4 | import com.codemate.koffeemate.data.models.User
5 | import io.realm.Realm
6 | import io.realm.Sort
7 | import java.util.*
8 |
9 | interface CoffeeEventRepository {
10 | fun recordBrewingEvent(user: User? = null): CoffeeBrewingEvent
11 | fun recordBrewingAccident(user: User): CoffeeBrewingEvent
12 | fun getAccidentCountForUser(user: User): Long
13 |
14 | fun getLastBrewingEvent(): CoffeeBrewingEvent?
15 | fun getLastBrewingAccident(): CoffeeBrewingEvent?
16 |
17 | fun getTopBrewers(): List
18 | fun getLatestBrewers(): List
19 | fun getAllBrewers(): List
20 | }
21 |
22 | class RealmCoffeeEventRepository : CoffeeEventRepository {
23 | override fun recordBrewingEvent(user: User?) = with(Realm.getDefaultInstance()) {
24 | var event: CoffeeBrewingEvent? = null
25 | executeTransaction {
26 | event = newEvent(it).apply {
27 | time = System.currentTimeMillis()
28 | isSuccessful = true
29 | this.user = if (user != null) copyToRealmOrUpdate(user) else null
30 | }
31 | }
32 |
33 | val copy = copyFromRealm(event)
34 | close()
35 | return@with copy!!
36 | }
37 |
38 | override fun recordBrewingAccident(user: User) = with(Realm.getDefaultInstance()) {
39 | var accident: CoffeeBrewingEvent? = null
40 | executeTransaction {
41 | accident = newEvent(it).apply {
42 | time = System.currentTimeMillis()
43 | isSuccessful = false
44 | this.user = copyToRealmOrUpdate(user)
45 | }
46 | }
47 |
48 | val copy = copyFromRealm(accident)
49 | close()
50 | return@with copy!!
51 | }
52 |
53 | override fun getAccidentCountForUser(user: User) = with(Realm.getDefaultInstance()) {
54 | val count = where(CoffeeBrewingEvent::class.java)
55 | .equalTo("isSuccessful", false)
56 | .equalTo("user.id", user.id)
57 | .count()
58 |
59 | close()
60 | return@with count
61 | }
62 |
63 |
64 | override fun getLastBrewingEvent() = with(Realm.getDefaultInstance()) {
65 | val lastEvent = where(CoffeeBrewingEvent::class.java)
66 | .equalTo("isSuccessful", true)
67 | .findAllSorted("time", Sort.ASCENDING)
68 | .lastOrNull()
69 | val copy = if (lastEvent != null) copyFromRealm(lastEvent) else null
70 |
71 | close()
72 | return@with copy
73 | }
74 |
75 | override fun getLastBrewingAccident() = with(Realm.getDefaultInstance()) {
76 | val accident = where(CoffeeBrewingEvent::class.java)
77 | .equalTo("isSuccessful", false)
78 | .findAllSorted("time", Sort.ASCENDING)
79 | .lastOrNull()
80 | val copy = if (accident != null) copyFromRealm(accident) else null
81 |
82 | close()
83 | return@with copy
84 | }
85 |
86 | override fun getTopBrewers() = with(Realm.getDefaultInstance()) {
87 | val users = where(CoffeeBrewingEvent::class.java)
88 | .equalTo("isSuccessful", true)
89 | .isNotNull("user")
90 | .findAll()
91 |
92 | val copy = copyFromRealm(users)
93 | .groupBy(CoffeeBrewingEvent::user)
94 | .entries
95 | .sortedByDescending { it.value.size }
96 | .map { it.key }
97 | .filterNotNull()
98 |
99 | close()
100 | return@with copy
101 | }
102 |
103 | override fun getLatestBrewers() = with(Realm.getDefaultInstance()) {
104 | val users = where(CoffeeBrewingEvent::class.java)
105 | .equalTo("isSuccessful", true)
106 | .isNotNull("user")
107 | .findAllSorted("time", Sort.DESCENDING)
108 |
109 | val copy = copyFromRealm(users)
110 | .groupBy(CoffeeBrewingEvent::user)
111 | .entries
112 | .map { it.key }
113 | .filterNotNull()
114 |
115 | close()
116 | return@with copy
117 | }
118 |
119 | override fun getAllBrewers() = with(Realm.getDefaultInstance()) {
120 | val users = where(CoffeeBrewingEvent::class.java)
121 | .equalTo("isSuccessful", true)
122 | .isNotNull("user")
123 | .findAll()
124 |
125 | val copy = copyFromRealm(users)
126 | .groupBy(CoffeeBrewingEvent::user)
127 | .entries
128 | .map { it.key }
129 | .filterNotNull()
130 |
131 | close()
132 | return@with copy
133 | }
134 |
135 | private fun newEvent(realm: Realm) =
136 | realm.createObject(
137 | CoffeeBrewingEvent::class.java,
138 | UUID.randomUUID().toString()
139 | )
140 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/data/local/Migration.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.data.local
18 |
19 | import io.realm.DynamicRealm
20 | import io.realm.DynamicRealmObject
21 | import io.realm.FieldAttribute
22 | import io.realm.RealmMigration
23 | import io.realm.exceptions.RealmPrimaryKeyConstraintException
24 |
25 | class Migration : RealmMigration {
26 | override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
27 | val schema = realm.schema
28 |
29 | /*************************************************************
30 | // Version 0
31 | class CoffeeBrewingEvent
32 | @PrimaryKey
33 | id: String
34 | time: Long
35 | isSuccessful: Boolean
36 | userId: String
37 |
38 | // Version 1
39 | Changed Tables:
40 | class CoffeeBrewingEvent
41 | @PrimaryKey
42 | id: String
43 | time: Long
44 | isSuccessful: Boolean
45 | user: User
46 |
47 | New Tables:
48 | class User
49 | @PrimaryKey
50 | id: String
51 | name: String
52 | profile: Profile
53 | real_name: String
54 | is_bot: Boolean
55 | deleted: Boolean
56 | last_updated: Long
57 |
58 | class Profile
59 | first_name: String
60 | last_name: String
61 | real_name: String
62 | image_72: String
63 | image_192: String
64 | image_512: String
65 | *************************************************************/
66 | if (oldVersion == 0L) {
67 | val profileSchema = schema.create("Profile")
68 | .addField("first_name", String::class.java)
69 | .addField("last_name", String::class.java)
70 | .addField("real_name", String::class.java)
71 | .addField("image_72", String::class.java)
72 | .addField("image_192", String::class.java)
73 | .addField("image_512", String::class.java)
74 |
75 | val userSchema = schema.create("User")
76 | .addField("id", String::class.java, FieldAttribute.PRIMARY_KEY)
77 | .addField("name", String::class.java)
78 | .addRealmObjectField("profile", profileSchema)
79 | .addField("real_name", String::class.java)
80 | .addField("is_bot", Boolean::class.java)
81 | .addField("deleted", Boolean::class.java)
82 | .addField("last_updated", Long::class.java)
83 |
84 | schema.get("CoffeeBrewingEvent")
85 | .addRealmObjectField("user", userSchema)
86 | .transform { brewingEvent ->
87 | brewingEvent.getString("userId")?.let { previousUserId ->
88 | if (previousUserId.isNotBlank()) {
89 | var user: DynamicRealmObject?
90 |
91 | // DynamicRealm doesn't allow us create duplicate User objects,
92 | // since the id field is a primary key. insertOrUpdate() and similar
93 | // methods are unavailable when using DynamicRealms.
94 | //
95 | // Try-catch is the only way to handle the migration of userIds to
96 | // users in this case.
97 | try {
98 | user = realm.createObject("User", previousUserId)
99 | } catch (e: RealmPrimaryKeyConstraintException) {
100 | user = realm.where("User")
101 | .equalTo("id", previousUserId)
102 | .findFirst()
103 | }
104 |
105 | brewingEvent.setObject("user", user)
106 | }
107 | }
108 | }
109 | .removeField("userId")
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Koffeemate
3 |
4 |
5 | Log an accident
6 | Cancel
7 | Try again
8 | Inform everyone!
9 | Yes
10 | No
11 |
12 |
13 | Brewing fresh coffee?
14 | Touch the coffee pot above!
15 |
16 | Coffee is coming!
17 | Informing others on Slack when ready.
18 |
19 |
20 | General
21 |
22 | coffee_brewing_time
23 | Coffee brewing time in minutes
24 | How long does your coffee maker take to brew coffee?
25 |
26 | Slack channels
27 |
28 | coffee_announcement_slack_channel
29 | Coffee announcement channel
30 | Slack channel name for announcing when new coffee is ready.
31 |
32 | use_different_channel_for_accidents
33 | Different channel for accidents
34 | Use different channel for posting brewing accidents.
35 |
36 | coffee_accident_slack_channel
37 | Accident announcement channel
38 | Slack channel name for announcing when new coffee brewing accidents happen.
39 |
40 | Screensaver settings
41 |
42 | use_screensaver
43 | Use screensaver
44 | Extend your precious screen\'s lifetime and prevent it from getting screen burn-ins.
45 |
46 |
47 | just now
48 |
49 | a minute ago
50 | %d minutes ago
51 |
52 | an hour ago
53 | %d hours ago
54 |
55 | a day ago
56 | %d days ago
57 |
58 | a week ago
59 | %d weeks ago
60 |
61 |
62 | Shaming person on Slack…
63 |
64 |
65 | Reset the counter?
66 | Who failed coffee brewing?
67 | Who is brewing?
68 | Select a guilty person
69 |
70 | Cancel Coffee Progress
71 | Are you sure you want to stop the coffee progress counter?
72 |
73 | No Slack channel set for coffee announcements. Please set it in the preferences.
74 | No Slack channel set for brewing failures. Please set it in the preferences.
75 |
76 |
77 | Posting to Slack that %s didn\'t know how to make coffee.\n\nIs this correct?
78 | New MoccaMaster certified person announced successfully on Slack!
79 | Freshly brewed coffee available!]]>
80 | Congratulations to %s for being a certified MoccaMaster user!
81 |
82 |
83 | Last brewed:\n%s
84 | Screensaver is on. Touch here to stop.
85 |
86 |
87 | Couldn\'t post to Slack about new MoccaMaster graduate for some reason. :(
88 | Couldn\'t load the profile list. :(
89 | More
90 |
91 |
92 |
--------------------------------------------------------------------------------
/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 [ -day "$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 -day`
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 -day $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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/main/MainPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.ui.main
2 |
3 | import android.graphics.Bitmap
4 | import com.codemate.koffeemate.common.BrewingProgressUpdater
5 | import com.codemate.koffeemate.common.ScreenSaver
6 | import com.codemate.koffeemate.data.local.CoffeeEventRepository
7 | import com.codemate.koffeemate.data.local.CoffeePreferences
8 | import com.codemate.koffeemate.data.models.User
9 | import com.codemate.koffeemate.ui.base.BasePresenter
10 | import com.codemate.koffeemate.ui.userselector.UserSelectListener
11 | import com.codemate.koffeemate.usecases.PostAccidentUseCase
12 | import com.codemate.koffeemate.usecases.SendCoffeeAnnouncementUseCase
13 | import javax.inject.Inject
14 |
15 | class MainPresenter @Inject constructor(
16 | val coffeePreferences: CoffeePreferences,
17 | val coffeeEventRepository: CoffeeEventRepository,
18 | val brewingProgressUpdater: BrewingProgressUpdater,
19 | val sendCoffeeAnnouncementUseCase: SendCoffeeAnnouncementUseCase,
20 | val postAccidentUseCase: PostAccidentUseCase
21 | ) : BasePresenter() {
22 | private var screensaver: ScreenSaver? = null
23 | var personBrewingCoffee: User? = null
24 |
25 | fun setScreenSaver(screensaver: ScreenSaver) {
26 | this.screensaver = screensaver
27 | }
28 |
29 | fun startDelayedCoffeeAnnouncement(newCoffeeMessage: String) {
30 | ensureViewIsAttached()
31 | screensaver?.defer()
32 |
33 | if (shouldAskForAnnouncementChannel()) {
34 | getView()?.showNoAnnouncementChannelSetError()
35 | return
36 | }
37 |
38 | if (!brewingProgressUpdater.isUpdating) {
39 | getView()?.showNewCoffeeIsComing()
40 | personBrewingCoffee = null
41 |
42 | if (!displayUserQuickDial()) {
43 | getView()?.displayUserSetterButton()
44 | }
45 |
46 | brewingProgressUpdater.startUpdating(
47 | updateListener = { progress ->
48 | // For UX: this way the user gets instant feedback, as the waves
49 | // can't be below 10%
50 | val adjustedProgress = Math.max(10, progress)
51 | getView()?.updateCoffeeProgress(adjustedProgress)
52 | },
53 | completeListener = {
54 | sendCoffeeAnnouncementUseCase
55 | .execute(coffeePreferences.getCoffeeAnnouncementChannel(), newCoffeeMessage)
56 | .subscribe(
57 | {
58 | getView()?.updateCoffeeProgress(0)
59 | getView()?.resetCoffeeViewStatus()
60 |
61 | coffeeEventRepository.recordBrewingEvent(personBrewingCoffee)
62 | updateLastBrewingEventTime()
63 | },
64 | { e -> e.printStackTrace() }
65 | )
66 | }
67 | )
68 | } else {
69 | getView()?.showCancelCoffeeProgressPrompt()
70 | }
71 | }
72 |
73 | private fun shouldAskForAnnouncementChannel(): Boolean {
74 | return !brewingProgressUpdater.isUpdating
75 | && !coffeePreferences.isCoffeeAnnouncementChannelSet()
76 | }
77 |
78 | fun handlePersonChange() {
79 | if (personBrewingCoffee == null) {
80 | getView()?.hideUserSetterButton()
81 |
82 | if (!displayUserQuickDial()) {
83 | getView()?.displayFullscreenUserSelector(UserSelectListener.REQUEST_WHOS_BREWING)
84 | }
85 | } else {
86 | personBrewingCoffee = null
87 | getView()?.clearCoffeeBrewingPerson()
88 | }
89 | }
90 |
91 | fun updateLastBrewingEventTime() {
92 | coffeeEventRepository.getLastBrewingEvent()?.let {
93 | getView()?.updateLastBrewingEvent(it)
94 | }
95 | }
96 |
97 | fun cancelCoffeeCountDown() {
98 | ensureViewIsAttached()
99 |
100 | getView()?.updateCoffeeProgress(0)
101 | getView()?.resetCoffeeViewStatus()
102 |
103 | brewingProgressUpdater.reset()
104 | personBrewingCoffee = null
105 | }
106 |
107 | fun launchAccidentReportingScreen() {
108 | screensaver?.defer()
109 |
110 | if (coffeePreferences.isAccidentChannelSet()) {
111 | personBrewingCoffee?.let {
112 | getView()?.showPostAccidentAnnouncementPrompt(it)
113 | return
114 | }
115 |
116 | getView()?.displayFullscreenUserSelector(UserSelectListener.REQUEST_WHO_FAILED_BREWING)
117 | } else {
118 | getView()?.showNoAccidentChannelSetError()
119 | }
120 | }
121 |
122 | fun announceCoffeeBrewingAccident(comment: String, user: User, profilePic: Bitmap) {
123 | ensureViewIsAttached()
124 |
125 | postAccidentUseCase
126 | .execute(comment, user, profilePic)
127 | .subscribe(
128 | {
129 | getView()?.showAccidentPostedSuccessfullyMessage()
130 | personBrewingCoffee = null
131 | },
132 | { e ->
133 | e.printStackTrace()
134 | getView()?.showErrorPostingAccidentMessage()
135 | }
136 | )
137 | }
138 |
139 | private fun displayUserQuickDial(): Boolean {
140 | val latestBrewers = coffeeEventRepository.getLatestBrewers().take(4)
141 |
142 | if (latestBrewers.isNotEmpty()) {
143 | getView()?.displayUserSelectorQuickDial(latestBrewers)
144 | return true
145 | }
146 |
147 | return false
148 | }
149 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/codemate/koffeemate/usecases/LoadUsersUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.usecases
18 |
19 | import com.codemate.koffeemate.BuildConfig
20 | import com.codemate.koffeemate.data.local.CoffeeEventRepository
21 | import com.codemate.koffeemate.data.local.UserRepository
22 | import com.codemate.koffeemate.data.models.User
23 | import com.codemate.koffeemate.data.network.SlackApi
24 | import com.codemate.koffeemate.testutils.getResourceFile
25 | import com.codemate.koffeemate.testutils.namedUser
26 | import com.codemate.koffeemate.testutils.namedUserWithTimestamp
27 | import com.nhaarman.mockito_kotlin.verify
28 | import com.nhaarman.mockito_kotlin.whenever
29 | import okhttp3.mockwebserver.MockResponse
30 | import okhttp3.mockwebserver.MockWebServer
31 | import org.hamcrest.core.IsEqual.equalTo
32 | import org.junit.After
33 | import org.junit.Assert.assertThat
34 | import org.junit.Before
35 | import org.junit.Test
36 | import org.mockito.Mock
37 | import org.mockito.MockitoAnnotations
38 | import rx.observers.TestSubscriber
39 | import rx.schedulers.Schedulers
40 |
41 | class LoadUsersUseCaseTest {
42 | @Mock
43 | lateinit var mockUserRepository: UserRepository
44 |
45 | @Mock
46 | lateinit var mockCoffeeEventRepository: CoffeeEventRepository
47 |
48 | lateinit var mockServer: MockWebServer
49 | lateinit var slackApi: SlackApi
50 | lateinit var useCase: LoadUsersUseCase
51 | lateinit var testSubscriber: TestSubscriber>
52 |
53 | @Before
54 | fun setUp() {
55 | MockitoAnnotations.initMocks(this)
56 |
57 | mockServer = MockWebServer()
58 | mockServer.start()
59 |
60 | slackApi = SlackApi.create(mockServer.url("/"))
61 | useCase = LoadUsersUseCase(
62 | mockUserRepository,
63 | mockCoffeeEventRepository,
64 | slackApi,
65 | Schedulers.immediate(),
66 | Schedulers.immediate()
67 | )
68 |
69 | testSubscriber = TestSubscriber>()
70 | }
71 |
72 | @After
73 | fun tearDown() {
74 | mockServer.shutdown()
75 | }
76 |
77 | @Test
78 | fun execute_MakesRequestToRightPathWithToken() {
79 | mockServer.enqueue(MockResponse().setBody(""))
80 | useCase.execute().subscribe(testSubscriber)
81 |
82 | val request = mockServer.takeRequest()
83 | assertThat(request.path, equalTo("/users.list?token=${BuildConfig.SLACK_AUTH_TOKEN}"))
84 | }
85 |
86 | @Test
87 | fun execute_WhenLoadingUsersFromApi_CachesThem() {
88 | enqueUserJsonResponseFromServer()
89 |
90 | useCase.execute().subscribe(testSubscriber)
91 | testSubscriber.assertValueCount(1)
92 |
93 | val userList = testSubscriber.onNextEvents[0]
94 | verify(mockUserRepository).addAll(userList)
95 | }
96 |
97 | @Test
98 | fun execute_WhenHasNoCachedUsers_LoadsThemFromApi() {
99 | enqueUserJsonResponseFromServer()
100 |
101 | useCase.execute().subscribe(testSubscriber)
102 | testSubscriber.assertValueCount(1)
103 |
104 | val userList = testSubscriber.onNextEvents[0]
105 | verifyUsersFromApi(userList)
106 | }
107 |
108 | @Test
109 | fun execute_WhenHasFreshEnoughCachedUsers_LoadsThemFromCache() {
110 | val currentTime = System.currentTimeMillis()
111 | val cachedUsers = listOf(
112 | User(last_updated = currentTime),
113 | User(last_updated = currentTime),
114 | User(last_updated = currentTime)
115 | )
116 |
117 | whenever(mockUserRepository.getAll()).thenReturn(cachedUsers)
118 | enqueUserJsonResponseFromServer()
119 |
120 | useCase.execute().subscribe(testSubscriber)
121 | testSubscriber.assertValueCount(1)
122 |
123 | val userList = testSubscriber.onNextEvents[0]
124 | assertThat(userList.size, equalTo(3))
125 | assertThat(userList, equalTo(cachedUsers))
126 | }
127 |
128 | @Test
129 | fun execute_WhenHasTooOldCachedUsers_LoadsThemFromApi() {
130 | val currentTime = System.currentTimeMillis() - useCase.MAX_CACHE_STALENESS
131 | val cachedUsers = listOf(
132 | User(last_updated = currentTime),
133 | User(last_updated = currentTime),
134 | User(last_updated = currentTime)
135 | )
136 |
137 | whenever(mockUserRepository.getAll()).thenReturn(cachedUsers)
138 | enqueUserJsonResponseFromServer()
139 |
140 | useCase.execute().subscribe(testSubscriber)
141 | testSubscriber.assertValueCount(1)
142 |
143 | val userList = testSubscriber.onNextEvents[0]
144 | verifyUsersFromApi(userList)
145 | }
146 |
147 | @Test
148 | fun execute_WhenHasCoffeeBrewers_ReturnsCoffeeBrewersFirst_InSortedOrder() {
149 | val brewers = listOf(namedUser("ddd"), namedUser("bbb"), namedUser("eee"))
150 | whenever(mockCoffeeEventRepository.getAllBrewers()).thenReturn(brewers)
151 |
152 | val currentTime = System.currentTimeMillis()
153 | val cachedUsers = listOf(
154 | namedUserWithTimestamp(name = "eee", lastUpdated = currentTime),
155 | namedUserWithTimestamp(name = "ccc", lastUpdated = currentTime),
156 | namedUserWithTimestamp(name = "bbb", lastUpdated = currentTime),
157 | namedUserWithTimestamp(name = "ddd", lastUpdated = currentTime),
158 | namedUserWithTimestamp(name = "aaa", lastUpdated = currentTime)
159 | )
160 |
161 | whenever(mockUserRepository.getAll()).thenReturn(cachedUsers)
162 |
163 | useCase.execute().subscribe(testSubscriber)
164 | testSubscriber.assertValueCount(1)
165 |
166 | val userList = testSubscriber.onNextEvents[0]
167 | assertThat(userList.size, equalTo(5))
168 | assertThat(userList[0].id, equalTo("bbb"))
169 | assertThat(userList[1].id, equalTo("ddd"))
170 | assertThat(userList[2].id, equalTo("eee"))
171 | assertThat(userList[3].id, equalTo("aaa"))
172 | assertThat(userList[4].id, equalTo("ccc"))
173 | }
174 |
175 | // Utility functions -->
176 | private fun enqueUserJsonResponseFromServer() {
177 | val userListJson = getResourceFile("seeds/sample_userlist_response.json").readText()
178 | mockServer.enqueue(MockResponse().setBody(userListJson))
179 | }
180 |
181 | private fun verifyUsersFromApi(userList: List) {
182 | assertThat(userList.size, equalTo(2))
183 |
184 | with(userList[0]) {
185 | assertThat(id, equalTo("abc123"))
186 | assertThat(name, equalTo("bobby"))
187 | assertThat(is_bot, equalTo(false))
188 | assertThat(profile.first_name, equalTo("Bobby"))
189 | assertThat(profile.last_name, equalTo("Tables"))
190 | assertThat(profile.real_name, equalTo("Bobby Tables"))
191 | }
192 |
193 | with(userList[1]) {
194 | assertThat(id, equalTo("123abc"))
195 | assertThat(name, equalTo("john"))
196 | assertThat(is_bot, equalTo(false))
197 | assertThat(profile.first_name, equalTo("John"))
198 | assertThat(profile.last_name, equalTo("Smith"))
199 | assertThat(profile.real_name, equalTo("John Smith"))
200 | }
201 |
202 | testSubscriber.assertCompleted()
203 | testSubscriber.assertNoErrors()
204 | }
205 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codemate/koffeemate/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.codemate.koffeemate.ui.main
2 |
3 | import android.app.ProgressDialog
4 | import android.os.Bundle
5 | import android.support.v4.view.ViewCompat
6 | import android.support.v7.app.AppCompatActivity
7 | import android.view.View
8 | import android.view.WindowManager
9 | import com.bumptech.glide.Glide
10 | import com.codemate.koffeemate.KoffeemateApp
11 | import com.codemate.koffeemate.R
12 | import com.codemate.koffeemate.common.ScreenSaver
13 | import com.codemate.koffeemate.data.models.CoffeeBrewingEvent
14 | import com.codemate.koffeemate.data.models.User
15 | import com.codemate.koffeemate.di.modules.ActivityModule
16 | import com.codemate.koffeemate.extensions.loadBitmap
17 | import com.codemate.koffeemate.ui.settings.SettingsActivity
18 | import com.codemate.koffeemate.ui.userselector.UserSelectListener
19 | import com.codemate.koffeemate.ui.userselector.views.UserSelectorOverlay
20 | import kotlinx.android.synthetic.main.activity_main.*
21 | import kotlinx.android.synthetic.main.view_coffee_progress.view.*
22 | import org.jetbrains.anko.*
23 | import javax.inject.Inject
24 |
25 | class MainActivity : AppCompatActivity(), MainView, UserSelectListener {
26 | @Inject
27 | lateinit var presenter: MainPresenter
28 |
29 | @Inject
30 | lateinit var screensaver: ScreenSaver
31 |
32 | var accidentProgress: ProgressDialog? = null
33 |
34 | override fun onCreate(savedInstanceState: Bundle?) {
35 | super.onCreate(savedInstanceState)
36 | setContentView(R.layout.activity_main)
37 |
38 | KoffeemateApp.appComponent
39 | .plus(ActivityModule(this))
40 | .inject(this)
41 |
42 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
43 | window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
44 |
45 | presenter.attachView(this)
46 | presenter.setScreenSaver(screensaver)
47 |
48 | setUpListeners()
49 | }
50 |
51 | fun setUpListeners() {
52 | coffeeProgressView.setOnCoffeePotClickListener {
53 | val newCoffeeMessage = getString(R.string.message_new_coffee_available)
54 | presenter.startDelayedCoffeeAnnouncement(newCoffeeMessage)
55 | }
56 |
57 | coffeeProgressView.setOnUserSetterClickListener {
58 | presenter.handlePersonChange()
59 | }
60 |
61 | settingsButton.onClick {
62 | screensaver.defer()
63 | startActivity(intentFor())
64 | }
65 |
66 | logAccidentButton.onClick { presenter.launchAccidentReportingScreen() }
67 |
68 | userQuickDial.userSelectListener = this
69 | userQuickDial.onMoreClickedListener = {
70 | showUserSelector(UserSelectListener.REQUEST_WHOS_BREWING)
71 | }
72 | }
73 |
74 | override fun onStart() {
75 | super.onStart()
76 | screensaver.start()
77 | }
78 |
79 | override fun onResume() {
80 | super.onResume()
81 | presenter.updateLastBrewingEventTime()
82 | }
83 |
84 | override fun onStop() {
85 | super.onStop()
86 | screensaver.stop()
87 | }
88 |
89 | override fun onDestroy() {
90 | super.onDestroy()
91 | presenter.detachView()
92 | accidentProgress?.dismiss()
93 | }
94 |
95 | private fun showUserSelector(requestCode: Int) {
96 | val selector = UserSelectorOverlay(this)
97 | selector.userSelectListener = this
98 | selector.requestCode = requestCode
99 |
100 | ViewCompat.setElevation(selector, dip(12).toFloat())
101 | container.addView(selector)
102 | }
103 |
104 | override fun displayUserSelectorQuickDial(users: List) {
105 | userQuickDial.setUsers(users) {
106 | coffeeProgressView.userSetterButton.show()
107 | }
108 | }
109 |
110 | override fun displayFullscreenUserSelector(requestCode: Int) {
111 | showUserSelector(requestCode)
112 | }
113 |
114 | override fun clearCoffeeBrewingPerson() {
115 | coffeeProgressView.userSetterButton.clearUser()
116 | }
117 |
118 | override fun displayUserSetterButton() {
119 | coffeeProgressView.userSetterButton.show()
120 | }
121 |
122 | override fun hideUserSetterButton() {
123 | coffeeProgressView.userSetterButton.hide()
124 | }
125 |
126 | override fun onUserSelected(user: User, requestCode: Int) {
127 | when (requestCode) {
128 | UserSelectListener.REQUEST_WHOS_BREWING -> {
129 | coffeeProgressView.userSetterButton.show()
130 |
131 | Glide.with(this)
132 | .load(user.profile.smallestAvailableImage)
133 | .error(R.drawable.ic_user_unknown)
134 | .into(coffeeProgressView.userSetterButton)
135 | presenter.personBrewingCoffee = user
136 | }
137 | UserSelectListener.REQUEST_WHO_FAILED_BREWING -> {
138 | showPostAccidentAnnouncementPrompt(user)
139 | }
140 | }
141 | }
142 |
143 | // MainView methods -->
144 | override fun showNewCoffeeIsComing() {
145 | coffeeStatusTitle.text = getString(R.string.title_coffeeview_brewing)
146 | coffeeStatusMessage.text = getString(R.string.message_coffeeview_brewing)
147 | coffeeProgressView.setCoffeeIncoming()
148 |
149 | logAccidentButton.hide()
150 | }
151 |
152 | override fun showCancelCoffeeProgressPrompt() {
153 | alert {
154 | title(R.string.prompt_cancel_coffee_progress_title)
155 | message(R.string.prompt_cancel_coffee_progress_message)
156 |
157 | negativeButton(R.string.action_no)
158 | positiveButton(R.string.action_yes) {
159 | presenter.cancelCoffeeCountDown()
160 | }
161 | }.show()
162 | }
163 |
164 | override fun updateLastBrewingEvent(event: CoffeeBrewingEvent) {
165 | lastBrewingEventTime.setTime(event.time)
166 | }
167 |
168 | override fun updateCoffeeProgress(newProgress: Int) {
169 | coffeeProgressView.setProgress(newProgress)
170 | }
171 |
172 | override fun resetCoffeeViewStatus() {
173 | coffeeStatusTitle.text = getString(R.string.title_coffeeview_idle)
174 | coffeeStatusMessage.text = getString(R.string.message_coffeeview_idle)
175 |
176 | coffeeProgressView.reset()
177 | userQuickDial.reset()
178 | logAccidentButton.show()
179 | }
180 |
181 | override fun showNoAnnouncementChannelSetError() {
182 | longToast(R.string.prompt_no_announcement_channel_set)
183 | startActivity(intentFor())
184 | }
185 |
186 | override fun showNoAccidentChannelSetError() {
187 | longToast(R.string.prompt_no_accident_channel_set)
188 | startActivity(intentFor())
189 | }
190 |
191 | override fun showPostAccidentAnnouncementPrompt(user: User) {
192 | alert {
193 | title(R.string.prompt_reset_the_counter)
194 | message(getString(R.string.message_posting_to_slack_fmt, user.profile.real_name))
195 |
196 | negativeButton(R.string.action_cancel)
197 | positiveButton(R.string.action_announce_coffee_accident) {
198 | accidentProgress = indeterminateProgressDialog(R.string.progress_message_shaming_person_on_slack)
199 | val comment = getString(R.string.message_congratulations_to_user_fmt, user.profile.first_name)
200 |
201 | Glide.with(this@MainActivity)
202 | .loadBitmap(user.profile.largestAvailableImage) { profilePic ->
203 | presenter.announceCoffeeBrewingAccident(comment, user, profilePic)
204 | }
205 | }
206 | }.show()
207 | }
208 |
209 | override fun showAccidentPostedSuccessfullyMessage() {
210 | accidentProgress?.dismiss()
211 | toast(R.string.message_posted_successfully)
212 | }
213 |
214 | override fun showErrorPostingAccidentMessage() {
215 | accidentProgress?.dismiss()
216 | toast(R.string.error_could_not_post_message)
217 | }
218 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codemate/koffeemate/data/local/CoffeeEventRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 Codemate Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.codemate.koffeemate.data.local
18 |
19 | import com.codemate.koffeemate.data.models.CoffeeBrewingEvent
20 | import com.codemate.koffeemate.data.models.User
21 | import io.realm.Realm
22 | import org.hamcrest.core.IsEqual.equalTo
23 | import org.junit.Assert.assertThat
24 | import org.junit.Assert.assertTrue
25 | import org.junit.Before
26 | import org.junit.Rule
27 | import org.junit.Test
28 |
29 | class CoffeeEventRepositoryTest {
30 | lateinit var coffeeEventRepository: RealmCoffeeEventRepository
31 |
32 | @Rule @JvmField
33 | val realmRule: RealmTestRule = RealmTestRule()
34 |
35 | @Before
36 | fun setUp() {
37 | coffeeEventRepository = RealmCoffeeEventRepository()
38 | }
39 |
40 | @Test
41 | fun recordBrewingEvent_PersistsEventsInDatabase() {
42 | coffeeEventRepository.recordBrewingEvent()
43 | coffeeEventRepository.recordBrewingEvent()
44 | coffeeEventRepository.recordBrewingEvent()
45 |
46 | assertThat(coffeeEventCount(), equalTo(3L))
47 | }
48 |
49 | @Test
50 | fun recordBrewingEvent_WithUserId_SavesUserId() {
51 | coffeeEventRepository.recordBrewingEvent(User(id = "abc123"))
52 |
53 | assertThat(coffeeEventRepository.getLastBrewingEvent()!!.user!!.id, equalTo("abc123"))
54 | }
55 |
56 | @Test
57 | fun getLastBrewingEvent_ReturnsLastBrewingEvent() {
58 | coffeeEventRepository.recordBrewingEvent()
59 | coffeeEventRepository.recordBrewingEvent()
60 |
61 | val lastEvent = coffeeEventRepository.recordBrewingEvent()
62 | assertThat(coffeeEventRepository.getLastBrewingEvent()!!.id, equalTo(lastEvent.id))
63 | }
64 |
65 | @Test
66 | fun getLastBrewingEvent_WhenHavingAccidentsAndSuccessfulEvents_ReturnsOnlyLastBrewingEvent() {
67 | val lastSuccessfulEvent = coffeeEventRepository.recordBrewingEvent()
68 | coffeeEventRepository.recordBrewingAccident(User())
69 |
70 | assertThat(coffeeEventRepository.getLastBrewingEvent()!!.user, equalTo(lastSuccessfulEvent.user))
71 | }
72 |
73 | @Test
74 | fun getLastBrewingAccident_ReturnsLastBrewingAccident() {
75 | val user = User(id = "abc123")
76 | coffeeEventRepository.recordBrewingAccident(user)
77 | coffeeEventRepository.recordBrewingAccident(user)
78 |
79 | val lastAccident = coffeeEventRepository.recordBrewingAccident(user)
80 | assertThat(coffeeEventRepository.getLastBrewingAccident()!!.id, equalTo(lastAccident.id))
81 | assertThat(coffeeEventRepository.getLastBrewingAccident()!!.user!!.id, equalTo(lastAccident.user!!.id))
82 | }
83 |
84 | @Test
85 | fun getAccidentCountForUser_ReturnsAccidentCountForThatSpecificUser() {
86 | val user = User(id = "abc123")
87 | assertThat(coffeeEventRepository.getAccidentCountForUser(user), equalTo(0L))
88 |
89 | coffeeEventRepository.recordBrewingAccident(user)
90 | coffeeEventRepository.recordBrewingAccident(user)
91 | coffeeEventRepository.recordBrewingAccident(user)
92 |
93 | val otherUser = User(id = "someotherid")
94 | coffeeEventRepository.recordBrewingAccident(otherUser)
95 | coffeeEventRepository.recordBrewingAccident(otherUser)
96 |
97 | assertThat(coffeeEventRepository.getAccidentCountForUser(user), equalTo(3L))
98 | }
99 |
100 | @Test
101 | fun getTopBrewers_WhenHasBrewers_ReturnsTopBrewersSorted() {
102 | coffeeEventRepository.recordBrewingEvent()
103 | coffeeEventRepository.recordBrewingEvent()
104 |
105 | val brewingMaster = User(id = "abc123")
106 | coffeeEventRepository.recordBrewingEvent(brewingMaster)
107 | coffeeEventRepository.recordBrewingEvent(brewingMaster)
108 | coffeeEventRepository.recordBrewingEvent(brewingMaster)
109 |
110 | val brewingApprentice = User(id = "a1b2c3")
111 | coffeeEventRepository.recordBrewingEvent(brewingApprentice)
112 | coffeeEventRepository.recordBrewingEvent(brewingApprentice)
113 |
114 | val brewingNoob = User(id = "cba321")
115 | coffeeEventRepository.recordBrewingEvent(brewingNoob)
116 |
117 | val sortedBrewers = coffeeEventRepository.getTopBrewers()
118 | assertThat(sortedBrewers.size, equalTo(3))
119 | assertThat(sortedBrewers[0].id, equalTo(brewingMaster.id))
120 | assertThat(sortedBrewers[1].id, equalTo(brewingApprentice.id))
121 | assertThat(sortedBrewers[2].id, equalTo(brewingNoob.id))
122 | }
123 |
124 | @Test
125 | fun getTopBrewers_WhenHasNoBrewers_ReturnsEmptyList() {
126 | coffeeEventRepository.recordBrewingEvent()
127 | coffeeEventRepository.recordBrewingEvent()
128 |
129 | val topBrewers = coffeeEventRepository.getTopBrewers()
130 | assertTrue(topBrewers.isEmpty())
131 | }
132 |
133 | @Test
134 | fun getTopBrewers_ReturnsNoAccidents() {
135 | val user = User(id = "abc123")
136 | coffeeEventRepository.recordBrewingAccident(user)
137 | coffeeEventRepository.recordBrewingAccident(user)
138 |
139 | val topBrewers = coffeeEventRepository.getTopBrewers()
140 | assertTrue(topBrewers.isEmpty())
141 | }
142 |
143 | @Test
144 | fun getLatestBrewers_WhenHasBrewers_ReturnsAllSortedByLatest() {
145 | val oldest = User(id = "abc123")
146 | coffeeEventRepository.recordBrewingEvent(oldest)
147 | coffeeEventRepository.recordBrewingEvent(oldest)
148 |
149 | val secondOldest = User(id = "a1b2c3")
150 | coffeeEventRepository.recordBrewingEvent(secondOldest)
151 | coffeeEventRepository.recordBrewingEvent(secondOldest)
152 |
153 | val newest = User(id = "cba321")
154 | coffeeEventRepository.recordBrewingEvent(newest)
155 | coffeeEventRepository.recordBrewingEvent(newest)
156 |
157 | val latestBrewers = coffeeEventRepository.getLatestBrewers()
158 | assertThat(latestBrewers.size, equalTo(3))
159 | assertThat(latestBrewers[0].id, equalTo(newest.id))
160 | assertThat(latestBrewers[1].id, equalTo(secondOldest.id))
161 | assertThat(latestBrewers[2].id, equalTo(oldest.id))
162 | }
163 |
164 | @Test
165 | fun getLatestBrewers_WhenHasNoBrewers_ReturnsEmptyList() {
166 | coffeeEventRepository.recordBrewingEvent()
167 | coffeeEventRepository.recordBrewingEvent()
168 |
169 | val latestBrewers = coffeeEventRepository.getLatestBrewers()
170 | assertTrue(latestBrewers.isEmpty())
171 | }
172 |
173 | @Test
174 | fun getLatestBrewers_ReturnsNoAccidents() {
175 | val user = User(id = "abc123")
176 | coffeeEventRepository.recordBrewingAccident(user)
177 | coffeeEventRepository.recordBrewingAccident(user)
178 |
179 | val latestBrewers = coffeeEventRepository.getLatestBrewers()
180 | assertTrue(latestBrewers.isEmpty())
181 | }
182 |
183 | @Test
184 | fun getAllBrewers_WhenHasBrewers_ReturnsAll() {
185 | val first = User(id = "abc123")
186 | coffeeEventRepository.recordBrewingEvent(first)
187 | coffeeEventRepository.recordBrewingEvent(first)
188 |
189 | val second = User(id = "a1b2c3")
190 | coffeeEventRepository.recordBrewingEvent(second)
191 | coffeeEventRepository.recordBrewingEvent(second)
192 |
193 | val third = User(id = "cba321")
194 | coffeeEventRepository.recordBrewingEvent(third)
195 | coffeeEventRepository.recordBrewingEvent(third)
196 |
197 | val brewers = coffeeEventRepository.getAllBrewers()
198 | assertThat(brewers.size, equalTo(3))
199 | assertThat(brewers[0].id, equalTo(first.id))
200 | assertThat(brewers[1].id, equalTo(second.id))
201 | assertThat(brewers[2].id, equalTo(third.id))
202 | }
203 |
204 | @Test
205 | fun getAllBrewers_WhenHasNoBrewers_ReturnsEmptyList() {
206 | coffeeEventRepository.recordBrewingEvent()
207 | coffeeEventRepository.recordBrewingEvent()
208 |
209 | val brewers = coffeeEventRepository.getAllBrewers()
210 | assertTrue(brewers.isEmpty())
211 | }
212 |
213 | @Test
214 | fun getAllUsers_ReturnsNoAccidents() {
215 | val user = User(id = "abc123")
216 | coffeeEventRepository.recordBrewingAccident(user)
217 | coffeeEventRepository.recordBrewingAccident(user)
218 |
219 | val brewers = coffeeEventRepository.getAllBrewers()
220 |
221 | assertTrue(brewers.isEmpty())
222 | }
223 |
224 | private fun coffeeEventCount() = with(Realm.getDefaultInstance()) {
225 | val count = where(CoffeeBrewingEvent::class.java)
226 | .equalTo("isSuccessful", true)
227 | .count()
228 | close()
229 |
230 | return@with count
231 | }
232 | }
--------------------------------------------------------------------------------