├── .github
└── workflows
│ └── android-pipeline.yml
├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ ├── AndroidManifest.xml
│ ├── assets
│ │ └── long_message.txt
│ └── java
│ │ └── com
│ │ └── atiurin
│ │ └── espressoguide
│ │ ├── IntegrationTest.kt
│ │ ├── framework
│ │ ├── CustomActivityTestRule.kt
│ │ ├── idlingresource
│ │ │ ├── BaseIdlingResource.kt
│ │ │ └── IdlingUtils.kt
│ │ └── reporting
│ │ │ ├── ScreenshotLifecycleListener.kt
│ │ │ ├── Step.kt
│ │ │ └── WindowHierarchyDumpListener.kt
│ │ ├── pages
│ │ ├── BlacklistPage.kt
│ │ ├── ChatPage.kt
│ │ └── FriendsListPage.kt
│ │ └── tests
│ │ ├── AdvancedEspressoTest.kt
│ │ ├── BadDataPreparationTest.kt
│ │ ├── BaseTest.kt
│ │ ├── BlacklistTests.kt
│ │ ├── ChatPageTest.kt
│ │ ├── DemoEspressoTest.kt
│ │ ├── SetUpTearDownDemoTest.kt
│ │ └── SimpleEspressoTest.kt
│ ├── debug
│ └── AndroidManifest.xml
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── atiurin
│ │ │ └── espressoguide
│ │ │ ├── Logger.kt
│ │ │ ├── MyApplication.kt
│ │ │ ├── activity
│ │ │ ├── ChatActivity.kt
│ │ │ ├── LoginActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── ProfileActivity.kt
│ │ │ ├── SettingsActivity.kt
│ │ │ └── SplashActivity.kt
│ │ │ ├── adapters
│ │ │ ├── ContactAdapter.kt
│ │ │ ├── MessageAdapter.kt
│ │ │ └── SettingsAdapter.kt
│ │ │ ├── async
│ │ │ ├── ContactsPresenter.kt
│ │ │ ├── Either.kt
│ │ │ ├── GetBlacklist.kt
│ │ │ ├── GetContacts.kt
│ │ │ └── UseCase.kt
│ │ │ ├── core
│ │ │ └── settings
│ │ │ │ └── SettingsItem.kt
│ │ │ ├── data
│ │ │ ├── Tags.kt
│ │ │ ├── entities
│ │ │ │ ├── Contact.kt
│ │ │ │ ├── Message.kt
│ │ │ │ └── User.kt
│ │ │ ├── loaders
│ │ │ │ └── MessageLoader.kt
│ │ │ └── repositories
│ │ │ │ ├── ContactsRepositoty.kt
│ │ │ │ ├── MessageRepository.kt
│ │ │ │ └── Storage.kt
│ │ │ ├── fragment
│ │ │ └── settings
│ │ │ │ ├── BlacklistFragment.kt
│ │ │ │ ├── FragmentInfoContainer.kt
│ │ │ │ ├── SettingsFragment.kt
│ │ │ │ └── SettingsFragmentNavigator.kt
│ │ │ ├── idlingresources
│ │ │ ├── AbstractIdlingResource.kt
│ │ │ ├── AppIdlingResource.kt
│ │ │ ├── IdlingResourses.kt
│ │ │ └── IdlingScope.kt
│ │ │ ├── managers
│ │ │ ├── AccountManager.kt
│ │ │ └── PrefsManager.kt
│ │ │ └── view
│ │ │ └── CircleImageView.java
│ └── res
│ │ ├── drawable-anydpi
│ │ ├── ic_account.xml
│ │ ├── ic_attach_file.xml
│ │ ├── ic_exit.xml
│ │ ├── ic_messages.xml
│ │ └── ic_send.xml
│ │ ├── drawable-hdpi
│ │ ├── ic_account.png
│ │ ├── ic_attach_file.png
│ │ ├── ic_exit.png
│ │ ├── ic_messages.png
│ │ └── ic_send.png
│ │ ├── drawable-mdpi
│ │ ├── ic_account.png
│ │ ├── ic_attach_file.png
│ │ ├── ic_exit.png
│ │ ├── ic_messages.png
│ │ └── ic_send.png
│ │ ├── drawable-v24
│ │ ├── chandler.png
│ │ ├── ic_launcher_foreground.xml
│ │ ├── joey.png
│ │ └── ross.png
│ │ ├── drawable-xhdpi
│ │ ├── ic_account.png
│ │ ├── ic_attach_file.png
│ │ ├── ic_exit.png
│ │ ├── ic_messages.png
│ │ └── ic_send.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_account.png
│ │ ├── ic_attach_file.png
│ │ ├── ic_exit.png
│ │ ├── ic_messages.png
│ │ └── ic_send.png
│ │ ├── drawable
│ │ ├── background_splash.xml
│ │ ├── circle.xml
│ │ ├── default_avatar.png
│ │ ├── gunther.png
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_menu_camera.xml
│ │ ├── ic_menu_gallery.xml
│ │ ├── ic_menu_manage.xml
│ │ ├── ic_menu_send.xml
│ │ ├── ic_menu_share.xml
│ │ ├── ic_menu_slideshow.xml
│ │ ├── img.xml
│ │ ├── janice.png
│ │ ├── monica.png
│ │ ├── phoebe.png
│ │ ├── rachel.png
│ │ └── side_nav_bar.xml
│ │ ├── layout
│ │ ├── activity_chat.xml
│ │ ├── activity_login.xml
│ │ ├── activity_main.xml
│ │ ├── activity_profile.xml
│ │ ├── activity_settings.xml
│ │ ├── app_bar_main.xml
│ │ ├── content_main.xml
│ │ ├── fragment_blacklist.xml
│ │ ├── fragment_settings_list.xml
│ │ ├── friends_list_item.xml
│ │ ├── message_item.xml
│ │ ├── my_text_view.xml
│ │ ├── nav_header_main.xml
│ │ └── settings_list_item.xml
│ │ ├── menu
│ │ ├── activity_main_drawer.xml
│ │ ├── blacklist_menu.xml
│ │ └── main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values-ru
│ │ └── strings.xml
│ │ ├── values-v21
│ │ └── styles.xml
│ │ └── values
│ │ ├── attrs.xml
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── atiurin
│ └── espressoguide
│ ├── MockitoTest.kt
│ ├── RobolectricTest.kt
│ ├── SimpleUnitTest.kt
│ └── UnitTestSuite.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── prepare-emulator.sh
├── settings.gradle
└── sonar-project.properties
/.github/workflows/android-pipeline.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | run_androidTests:
11 | runs-on: macos-latest
12 | strategy:
13 | matrix:
14 | api-level: [28]
15 | steps:
16 | - name: checkout
17 | uses: actions/checkout@v2
18 |
19 | - name: run tests
20 | uses: reactivecircus/android-emulator-runner@v2
21 | with:
22 | api-level: ${{ matrix.api-level }}
23 | script: ./gradlew connectedCheck --stacktrace
24 | profile: pixel
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | /.idea
11 | .idea
12 | allure*
13 | .DS_Store
14 | /build
15 | /captures
16 | .externalNativeBuild
17 | /.idea/
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # espresso-guide
2 |
3 | This app is created to demonstrate different approaches of android automation tests.
4 |
5 | There are:
6 | 1. Unit tests with Robolectric and Mockito.
7 | 2. Simple integration test.
8 | 3. Simple and advanced espresso tests.
9 |
10 | ## Ultron framework
11 |
12 | All advanced actions and assertions in this sample project are executed using [Ultron](https://github.com/alex-tiurin/ultron) framework.
13 |
14 | ## Deep dive into espresso.
15 |
16 | **It will be really useful if you read this [README](https://github.com/alex-tiurin/ultron) firstly**
17 |
18 | In **EspressoGuide** you can find:
19 |
20 | 1. A page object approach for Espresso tests (look at the description of page object classes [here](https://github.com/alex-tiurin/espresso-guide/tree/master/app/src/androidTest/java/com/atiurin/espressoguide/pages)).
21 | 2. A new style of Espresso test using [Ultron](https://github.com/alex-tiurin/ultron) .
22 | The best example of how these tests looks like you can find in class [DemoEspressoTest](https://github.com/alex-tiurin/espresso-guide/blob/master/app/src/androidTest/java/com/atiurin/espressoguide/tests/DemoEspressoTest.kt)
23 | 3. A new great approach of how to work with RecyclerView lists (look inside page object classes again).
24 | 4. A new safe and simple way of using IdlingResources (look at [BaseTest](https://github.com/alex-tiurin/espresso-guide/blob/master/app/src/androidTest/java/com/atiurin/espressoguide/tests/BaseTest.kt),
25 | [IdlingUtils](https://github.com/alex-tiurin/espresso-guide/blob/master/app/src/androidTest/java/com/atiurin/espressoguide/framework/IdlingUtils.kt), and all things inside [idlingresources package](https://github.com/alex-tiurin/espresso-guide/tree/master/app/src/main/java/com/atiurin/espressoguide/idlingresources)).
26 | 5. How to configure allure report for a project.
27 | 6. How to find UI elements with **view.tag** value. This way of searching an element on the screen is really useful when resource id, content description and text are not applicable.
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'jacoco'
5 | apply plugin: 'org.sonarqube'
6 |
7 | android {
8 | compileSdkVersion 31
9 | defaultConfig {
10 | applicationId "com.atiurin.espressoguide"
11 | minSdkVersion 23
12 | targetSdkVersion 31
13 | versionCode 1
14 | versionName "1.0"
15 | testInstrumentationRunner "io.qameta.allure.android.runners.AllureAndroidJUnitRunner"
16 | }
17 | packagingOptions {
18 | exclude 'META-INF/DEPENDENCIES'
19 | exclude 'META-INF/LICENSE'
20 | exclude 'META-INF/LICENSE.txt'
21 | exclude 'META-INF/license.txt'
22 | exclude 'META-INF/NOTICE'
23 | exclude 'META-INF/NOTICE.txt'
24 | exclude 'META-INF/notice.txt'
25 | exclude 'META-INF/ASL2.0'
26 | exclude 'META-INF/AL2.0'
27 | exclude 'META-INF/LGPL2.1'
28 | exclude("META-INF/*.kotlin_module")
29 | }
30 |
31 | buildTypes {
32 | release {
33 | minifyEnabled false
34 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
35 | }
36 | debug {
37 | testCoverageEnabled = true
38 | }
39 | }
40 | testOptions {
41 | animationsDisabled true
42 |
43 | unitTests {
44 | includeAndroidResources = true
45 | }
46 | execution 'ANDROIDX_TEST_ORCHESTRATOR'
47 | }
48 | compileOptions {
49 | sourceCompatibility JavaVersion.VERSION_1_8
50 | targetCompatibility JavaVersion.VERSION_1_8
51 | }
52 | // For Kotlin projects
53 | kotlinOptions {
54 | jvmTarget = "1.8"
55 | }
56 | }
57 |
58 | sonarqube {
59 | properties {
60 | property "sonar.projectName", "espressoguide"
61 | property "sonar.projectBaseDir", "."
62 | property "sonar.source", "app/src/main/java"
63 | property "sonar.tests", "app/src/test/java, app/src/androidTest/java"
64 | property "sonar.java.test.binaries", "app/build/intermediates/class/debug"
65 | property "sonar.jacoco.reportPath", "app/build/jacoco/testDebugUnitTest.exec"
66 | property "sonar.java.coveragePlugin", "jacoco"
67 | }
68 | }
69 | jacoco {
70 | toolVersion = '0.8.0'
71 | }
72 | dependencies {
73 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.30"
74 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4"
75 | implementation 'androidx.appcompat:appcompat:1.2.0'
76 | implementation 'androidx.core:core-ktx:1.3.2'
77 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
78 | implementation 'com.google.android.material:material:1.2.1'
79 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
80 | implementation 'androidx.recyclerview:recyclerview:1.1.0'
81 | testImplementation 'junit:junit:4.13'
82 | //noinspection GradleCompatible
83 | implementation 'com.android.support:cardview-v7:28.0.0'
84 | implementation "androidx.fragment:fragment-ktx:1.2.5"
85 | // AndroidJUnitRunner and JUnit Rules
86 |
87 | testImplementation "org.robolectric:robolectric:4.4"
88 | testImplementation "androidx.test:core:1.3.0"
89 | testImplementation 'org.mockito:mockito-core:3.4.0'
90 | androidTestImplementation 'com.google.truth:truth:1.0'
91 |
92 | androidTestImplementation 'androidx.test:core:1.3.0'
93 | androidTestImplementation 'androidx.test:runner:1.3.0'
94 | androidTestImplementation 'androidx.test:rules:1.3.0'
95 |
96 | // Assertions
97 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
98 | androidTestImplementation 'androidx.test.ext:truth:1.3.0'
99 |
100 | // Espresso dependencies
101 | androidTestImplementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) // allure artifacts
102 | androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
103 | androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
104 | androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.4.0'
105 | androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
106 | androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
107 | androidTestImplementation 'com.google.code.gson:gson:2.8.6'
108 | androidTestImplementation 'com.microsoft.appcenter:espresso-test-extension:1.4'
109 | androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
110 |
111 | //ultron
112 | androidTestImplementation "com.atiurin:ultron:2.0.0-alpha02"
113 | //Allure
114 | androidTestImplementation "io.qameta.allure:allure-kotlin-model:2.2.6"
115 | androidTestImplementation "io.qameta.allure:allure-kotlin-commons:2.2.6"
116 | androidTestImplementation "io.qameta.allure:allure-kotlin-junit4:2.2.6"
117 | androidTestImplementation "io.qameta.allure:allure-kotlin-android:2.2.6"
118 | //Android Test Orchestrator
119 | androidTestUtil 'androidx.test:orchestrator:1.3.0'
120 | }
121 |
122 | task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) {
123 | reports {
124 | xml.enabled = true
125 | html.enabled = true
126 | }
127 |
128 | def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*']
129 | getSourceDirectories().setFrom("$project.projectDir/src/main/java")
130 | getClassDirectories().setFrom(fileTree(dir: "$project.buildDir/tmp/kotlin-classes/debug", excludes: fileFilter))
131 | getExecutionData().setFrom(fileTree(dir: project.buildDir, includes: [
132 | 'jacoco/testDebugUnitTest.exec', 'outputs/code-coverage/connected/*coverage.ec'
133 | ]))
134 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/androidTest/assets/long_message.txt:
--------------------------------------------------------------------------------
1 | Heisenbug 2020 Piter is a large technical conference that takes place this summer 2020.
2 |
3 | Heisenbug conference brings together more than 1000 professionals in the field of quality assurance. These professionals are testers, programmers developing tests for their code, engineers in automated and load testing, and team leads willing to improve testing efficiency in their projects. All speakers talk about the most important, practical and hardcore things on software testing:
4 |
5 | Automation testing;
6 | Manual testing;
7 | Load testing, performance testing, benchmarking;
8 | Testing of distributed systems;
9 | Testing of mobile applications;
10 | UX, Security, A/B testing;
11 | Code analysis and its tools;
12 | Tools and testing environment;
13 | Test frameworks best practices;
14 | Testing of compilers, nuclear power plants, and so on.
15 | Our focus is on technological aspect of testing, and if you are not that familiar with it yet, it's a valid reason to attend the conference. We guarantee that there's no Agile, Scrum and team management stuff.
16 |
17 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/IntegrationTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import com.atiurin.espressoguide.managers.AccountManager
5 | import org.junit.Assert
6 | import org.junit.Ignore
7 | import org.junit.Test
8 |
9 | class IntegrationTest{
10 | @Test
11 | @Ignore
12 | fun testLoginWithValidData() {
13 | val validPassword = "1234"
14 | val validUserName = "joey"
15 | val context = InstrumentationRegistry.getInstrumentation().targetContext
16 | val manager = AccountManager(context)
17 | manager.logout()
18 | manager.login(validUserName, validPassword)
19 | Assert.assertTrue(manager.isLoggedIn())
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/framework/CustomActivityTestRule.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.framework
2 |
3 | import android.app.Activity
4 | import androidx.test.rule.ActivityTestRule
5 | import com.atiurin.espressoguide.Logger
6 |
7 | open class CustomActivityTestRule : ActivityTestRule {
8 | constructor(activityClass: Class) : super(activityClass)
9 | constructor(activityClass: Class, initialTouchMode: Boolean, launchActivity: Boolean) : super(activityClass, initialTouchMode, launchActivity)
10 |
11 | override fun beforeActivityLaunched() {
12 | super.beforeActivityLaunched()
13 | Logger.life("beforeActivityLaunched")
14 | }
15 |
16 | override fun afterActivityLaunched() {
17 | super.afterActivityLaunched()
18 | Logger.life("afterActivityLaunched")
19 | }
20 |
21 | override fun finishActivity() {
22 | super.finishActivity()
23 | Logger.life("finishActivity")
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/framework/idlingresource/BaseIdlingResource.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.framework.idlingresource
2 |
3 | import androidx.test.espresso.IdlingResource
4 | import com.atiurin.espressoguide.Logger
5 | import com.atiurin.espressoguide.idlingresources.AppIdlingResource
6 |
7 | class BaseIdlingResource(private val appIdlingResource: AppIdlingResource) : IdlingResource, AppIdlingResource by appIdlingResource {
8 | override fun isIdleNow(): Boolean {
9 | return appIdlingResource.isIdle()
10 | }
11 |
12 | override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
13 | Logger.debug("Idling. registerIdleTransitionCallback $name")
14 | appIdlingResource.registerIdleTransitionCallback(BaseResourceCallback(callback))
15 | }
16 |
17 | inner class BaseResourceCallback(private val callback: IdlingResource.ResourceCallback) : AppIdlingResource.AppResourceCallback, IdlingResource.ResourceCallback by callback {
18 | override fun onTransitionToIdleState() {
19 | Logger.debug("Idling. onTransitionToIdleState resource ${this@BaseIdlingResource.name}")
20 | callback.onTransitionToIdle()
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/framework/idlingresource/IdlingUtils.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.framework
2 |
3 | import com.atiurin.espressoguide.idlingresources.ContactsIdling
4 | import com.atiurin.espressoguide.idlingresources.IdlingScope
5 | import com.atiurin.espressoguide.idlingresources.MessagesIdling
6 |
7 | fun getDefaultIdlingScope(): IdlingScope {
8 | return object : IdlingScope {
9 | override val messagesIdling: MessagesIdling = MessagesIdling()
10 | override var contactsIdling: ContactsIdling = ContactsIdling()
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/framework/reporting/ScreenshotLifecycleListener.kt:
--------------------------------------------------------------------------------
1 |
2 | package com.atiurin.espressoguide.framework.reporting
3 |
4 | import com.atiurin.ultron.core.common.Operation
5 | import com.atiurin.ultron.core.common.OperationResult
6 | import com.atiurin.ultron.core.config.UltronConfig.UiAutomator.Companion.uiDevice
7 | import com.atiurin.ultron.listeners.UltronLifecycleListener
8 | import io.qameta.allure.android.allureScreenshot
9 |
10 | class ScreenshotLifecycleListener : UltronLifecycleListener() {
11 | override fun afterFailure(operationResult: OperationResult) {
12 | takeScreenshot(operationResult.operation.name)
13 | }
14 |
15 | private fun takeScreenshot(name: String){
16 | allureScreenshot(name, 10, 1f )
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/framework/reporting/Step.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.framework.reporting
2 |
3 | import android.util.Log
4 | import io.qameta.allure.kotlin.Allure.step
5 |
6 | inline fun step (description: String, crossinline action: () -> T): T {
7 | Log.d("Espresso step", "------------ $description ------------ ")
8 | return step(description) {
9 | action()
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/framework/reporting/WindowHierarchyDumpListener.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.framework.reporting
2 |
3 | import androidx.test.uiautomator.UiDevice
4 | import com.atiurin.ultron.core.common.Operation
5 | import com.atiurin.ultron.core.common.OperationResult
6 | import com.atiurin.ultron.listeners.UltronLifecycleListener
7 | import java.io.ByteArrayOutputStream
8 | import java.util.concurrent.TimeUnit
9 |
10 | class WindowHierarchyDumpListener(private val fileName: String = "window-hierarchy") :
11 | UltronLifecycleListener() {
12 | override fun afterFailure(operationResult: OperationResult) {
13 | dumpHierarchy()
14 | }
15 |
16 | private fun dumpHierarchy() {
17 | // val attachmentFile = createAttachmentFile().apply {
18 | // this.writeBytes(uiDevice.dumpWindowHierarchy())
19 | // }
20 | // AllureAndroidLifecycle.addAttachment(
21 | // fileName, type = "text/xml", fileExtension = "xml", attachmentFile
22 | // )
23 | }
24 |
25 | private fun UiDevice.dumpWindowHierarchy(): ByteArray = ByteArrayOutputStream().apply {
26 | waitForIdle(TimeUnit.SECONDS.toMillis(10))
27 | dumpWindowHierarchy(this)
28 | }.toByteArray()
29 | }
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/pages/BlacklistPage.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.pages
2 |
3 | import androidx.test.espresso.matcher.ViewMatchers.*
4 | import com.atiurin.espressoguide.R
5 | import com.atiurin.espressoguide.framework.reporting.step
6 | import com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewItem
7 | import com.atiurin.ultron.core.espresso.recyclerview.withRecyclerView
8 | import com.atiurin.ultron.page.Page
9 | import org.hamcrest.CoreMatchers.allOf
10 |
11 | object BlacklistPage : Page() {
12 | private val itemsList = withRecyclerView(R.id.recycler_blacklist)
13 |
14 | fun assertPageDisplayed() = apply {
15 | step("Assert blacklist page displayed") {
16 | itemsList.isDisplayed()
17 | }
18 | }
19 |
20 | fun assertContactDisplayed(name: String) = apply {
21 | step("Assert contact with name $name is displayed in blacklist") {
22 | getListItem(name).isDisplayed()
23 | }
24 | }
25 |
26 | private fun getListItem(name: String): BlacklistItem {
27 | return itemsList.getItem(hasDescendant(allOf(withId(R.id.tv_name), withText(name))))
28 | }
29 |
30 | private fun getItemAtPosition(position: Int): BlacklistItem {
31 | return itemsList.getItem(position)
32 | }
33 |
34 | class BlacklistItem : UltronRecyclerViewItem() {
35 | val name by lazy { getChild(withId(R.id.tv_name)) }
36 | val status by lazy { getChild(withId(R.id.tv_status)) }
37 | val avatar by lazy { getChild(withId(R.id.avatar)) }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/pages/ChatPage.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.pages
2 |
3 | import android.view.View
4 | import androidx.test.espresso.matcher.ViewMatchers
5 | import androidx.test.espresso.matcher.ViewMatchers.*
6 | import com.atiurin.espressoguide.Logger
7 | import com.atiurin.espressoguide.R
8 | import com.atiurin.espressoguide.data.entities.Contact
9 | import com.atiurin.espressoguide.framework.*
10 | import com.atiurin.espressoguide.framework.reporting.step
11 | import com.atiurin.ultron.core.espresso.UltronEspresso
12 | import com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewItem
13 | import com.atiurin.ultron.core.espresso.recyclerview.withRecyclerView
14 | import com.atiurin.ultron.extensions.click
15 | import com.atiurin.ultron.extensions.hasText
16 | import com.atiurin.ultron.extensions.isDisplayed
17 | import com.atiurin.ultron.extensions.typeText
18 | import com.atiurin.ultron.page.Page
19 | import org.hamcrest.Matcher
20 | import org.hamcrest.Matchers.allOf
21 |
22 | object ChatPage : Page() {
23 | lateinit var contact: Contact
24 | private val list = withRecyclerView(R.id.messages_list)
25 | private val clearHistoryBtn = withText("Clear history")
26 | private val inputMessageText = withId(R.id.message_input_text)
27 | private val sendMessageBtn = withId(R.id.send_button)
28 |
29 | private fun getMessageListItem(text: String): ChatRecyclerItem {
30 | return list.getItem(hasDescendant(
31 | allOf(
32 | withId(R.id.message_text),
33 | withText(text)
34 | )
35 | )
36 | )
37 | }
38 |
39 | private fun getMessageListItemAtPosition(position: Int): ChatRecyclerItem {
40 | return list.getItem(position)
41 | }
42 |
43 | private fun getTitle(title: String): Matcher {
44 | return allOf(withId(R.id.toolbar_title), withText(title))
45 | }
46 |
47 | class ChatRecyclerItem : UltronRecyclerViewItem() {
48 | val text by lazy { getChild(withId(R.id.message_text)) }
49 | }
50 |
51 | fun assertPageDisplayed() = apply {
52 | step("Assert chat page is displayed") {
53 | list.isDisplayed()
54 | inputMessageText.isDisplayed()
55 | }
56 | }
57 |
58 | fun assertChatTitle() = apply {
59 | step("Assert chat with contact '${contact.name}' has correct title") {
60 | getTitle(contact.name).isDisplayed()
61 | }
62 | }
63 |
64 | fun sendMessage(text: String) = apply {
65 | step("Send message with text '$text'") {
66 | inputMessageText.typeText(text)
67 | sendMessageBtn.click()
68 | getMessageListItem(text).text
69 | .isDisplayed()
70 | .hasText(text)
71 | }
72 | }
73 |
74 | fun clearHistory() = apply {
75 | step("Clear chat history") {
76 | UltronEspresso.openContextualActionModeOverflowMenu()
77 | clearHistoryBtn.click()
78 | }
79 | }
80 |
81 | fun assertMessageDisplayed(text: String) = apply {
82 | step("Assert message with text is displayed") {
83 | getMessageListItem(text).text
84 | .isDisplayed()
85 | .hasText(text)
86 | }
87 | }
88 |
89 | fun assertMessageTextAtPosition(position: Int, text: String) = apply {
90 | step("Assert message at position $position has text '$text' and displayed") {
91 | getMessageListItemAtPosition(position).text
92 | .hasText(text)
93 | .isDisplayed()
94 | }
95 | }
96 | }
97 |
98 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/pages/FriendsListPage.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.pages
2 |
3 | import androidx.test.espresso.matcher.ViewMatchers.*
4 | import com.atiurin.espressoguide.Logger
5 | import com.atiurin.espressoguide.R
6 | import com.atiurin.espressoguide.data.Tags
7 | import com.atiurin.espressoguide.data.entities.Contact
8 | import com.atiurin.espressoguide.framework.*
9 | import com.atiurin.espressoguide.framework.reporting.step
10 | import com.atiurin.ultron.core.espresso.recyclerview.UltronRecyclerViewItem
11 | import com.atiurin.ultron.core.espresso.recyclerview.withRecyclerView
12 | import com.atiurin.ultron.extensions.hasText
13 | import com.atiurin.ultron.page.Page
14 | import org.hamcrest.Matchers.`is`
15 | import org.hamcrest.Matchers.allOf
16 |
17 | object FriendsListPage : Page() {
18 | private val list = withRecyclerView(withTagValue(`is`(Tags.CONTACTS_LIST)))
19 |
20 | private fun getFriendListItem(title: String): FriendRecyclerItem {
21 | return list.getItem(hasDescendant(allOf(withId(R.id.tv_name), withText(title))))
22 | }
23 |
24 | class FriendRecyclerItem : UltronRecyclerViewItem() {
25 | val name by lazy { getChild(withId(R.id.tv_name)) }
26 | val status by lazy { getChild(withId(R.id.tv_status)) }
27 | }
28 |
29 | fun assertPageDisplayed() = apply {
30 | step("Assert friends list page displayed") {
31 | list.isDisplayed()
32 | }
33 | }
34 |
35 | fun openChat(contact: Contact): ChatPage {
36 | return step("Open chat with friend '${contact.name}'") {
37 | getFriendListItem(contact.name).click()
38 | ChatPage {
39 | this.contact = contact
40 | assertPageDisplayed()
41 | assertChatTitle()
42 | }
43 | }
44 | }
45 |
46 | fun assertStatus(name: String, status: String) = apply {
47 | step("Assert friend with name '$name' has status '$status'") {
48 | getFriendListItem(name).status.hasText(status)
49 | }
50 | }
51 |
52 | fun assertName(nameText: String) = apply {
53 | step("Assert friend name '$nameText' in the right place") {
54 | getFriendListItem(nameText).name.hasText(nameText)
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/tests/AdvancedEspressoTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.tests
2 |
3 | import android.content.Intent
4 | import androidx.recyclerview.widget.RecyclerView
5 | import androidx.test.espresso.Espresso.onView
6 | import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
7 | import androidx.test.espresso.action.ViewActions.click
8 | import androidx.test.espresso.action.ViewActions.typeText
9 | import androidx.test.espresso.assertion.ViewAssertions.matches
10 | import androidx.test.espresso.contrib.RecyclerViewActions
11 | import androidx.test.espresso.matcher.ViewMatchers.*
12 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
13 | import androidx.test.rule.ActivityTestRule
14 | import com.atiurin.espressoguide.R
15 | import com.atiurin.espressoguide.activity.MainActivity
16 | import com.atiurin.espressoguide.data.repositories.CURRENT_USER
17 | import com.atiurin.espressoguide.managers.AccountManager
18 | import org.hamcrest.Matchers.allOf
19 | import org.junit.Before
20 | import org.junit.Ignore
21 | import org.junit.Rule
22 | import org.junit.Test
23 |
24 | class AdvancedEspressoTest : BaseTest() {
25 | @Rule
26 | @JvmField
27 | val activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
28 |
29 | @Before
30 | fun backgroundLogin() {
31 | AccountManager(getInstrumentation().targetContext).login(
32 | CURRENT_USER.login,
33 | CURRENT_USER.password
34 | )
35 | activityTestRule.launchActivity(Intent())
36 | }
37 |
38 | /**
39 | * clear espresso test. it is hard to be maintained
40 | * Look at better approach [DemoEspressoTest]
41 | */
42 | // @Ignore
43 | @Test
44 | fun clearEspressoTestWithRecyclerViewActions() {
45 | val messageText = "message4"
46 | val itemMatcher = hasDescendant(allOf(withId(R.id.tv_name), withText("Janice")))
47 | onView(withId(R.id.recycler_friends))
48 | .perform(
49 | RecyclerViewActions
50 | .actionOnItem(itemMatcher, click())
51 | )
52 |
53 | openActionBarOverflowOrOptionsMenu(getInstrumentation().context)
54 | onView(withText("Clear history")).perform(click())
55 | onView(withId(R.id.message_input_text)).perform(typeText(messageText))
56 | onView(withId(R.id.send_button)).perform(click())
57 | onView(withId(R.id.messages_list))
58 | .perform(
59 | RecyclerViewActions
60 | .scrollTo(hasDescendant(withText(messageText)))
61 | )
62 | onView(
63 | allOf(
64 | withText(messageText),
65 | withId(R.id.message_text)
66 | )
67 | ).check(matches(isDisplayed()))
68 | }
69 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/tests/BadDataPreparationTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.tests
2 |
3 | import android.content.Intent
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import androidx.test.rule.ActivityTestRule
6 | import com.atiurin.espressoguide.activity.ChatActivity
7 | import com.atiurin.espressoguide.activity.INTENT_CONTACT_ID_EXTRA_NAME
8 | import com.atiurin.espressoguide.data.entities.Message
9 | import com.atiurin.espressoguide.data.repositories.CURRENT_USER
10 | import com.atiurin.espressoguide.data.repositories.ContactsRepositoty
11 | import com.atiurin.espressoguide.data.repositories.MessageRepository
12 | import com.atiurin.espressoguide.managers.AccountManager
13 | import com.atiurin.espressoguide.pages.ChatPage
14 | import org.junit.Before
15 | import org.junit.BeforeClass
16 | import org.junit.Test
17 |
18 |
19 | /**
20 | * This test class was written to demonstrate a BAD approach. Don't use it in you project.
21 | * To understand a good way of configuring test data look at [ChatPageTest]
22 | */
23 | class BadDataPreparationTest : BaseTest() {
24 | companion object {
25 | private val contact = ContactsRepositoty.getContact(2)
26 | private val simpleMessage = Message(CURRENT_USER.id, contact.id, "SimpleText")
27 | private val specialCharsMessage = Message(CURRENT_USER.id, contact.id, "!@#$%^&*(){}:\",./<>?_+±§`~][")
28 | private val longMessage = Message(CURRENT_USER.id, contact.id, InstrumentationRegistry.getInstrumentation().context.assets.open("long_message.txt").reader().readText())
29 | @BeforeClass
30 | @JvmStatic
31 | fun prepareData() {
32 | MessageRepository.clearChatMessages(contact.id)
33 | MessageRepository.addChatMessage(contact.id, longMessage)
34 | MessageRepository.addChatMessage(contact.id, simpleMessage)
35 | MessageRepository.addChatMessage(contact.id, specialCharsMessage)
36 | }
37 | }
38 | private val activityTestRule = ActivityTestRule(ChatActivity::class.java, false, false)
39 |
40 | @Before
41 | fun prepareConditions(){
42 | AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login(
43 | CURRENT_USER.login,
44 | CURRENT_USER.password
45 | )
46 | val intent = Intent().putExtra(INTENT_CONTACT_ID_EXTRA_NAME, contact.id)
47 | activityTestRule.launchActivity(intent)
48 | }
49 |
50 | @Test
51 | fun assertSimpleMessage() {
52 | ChatPage.assertMessageDisplayed(simpleMessage.text)
53 | }
54 |
55 | @Test
56 | fun assertSpecialMessage() {
57 | ChatPage.assertMessageDisplayed(specialCharsMessage.text)
58 | }
59 | @Test
60 | fun assertLongMessage() {
61 | ChatPage.assertMessageDisplayed(longMessage.text)
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/tests/BaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.tests
2 |
3 | import androidx.test.espresso.IdlingRegistry
4 | import com.atiurin.espressoguide.framework.getDefaultIdlingScope
5 | import com.atiurin.espressoguide.framework.idlingresource.BaseIdlingResource
6 | import com.atiurin.espressoguide.framework.reporting.ScreenshotLifecycleListener
7 | import com.atiurin.espressoguide.framework.reporting.WindowHierarchyDumpListener
8 | import com.atiurin.espressoguide.idlingresources.idling
9 | import com.atiurin.espressoguide.idlingresources.idlingContainer
10 | import com.atiurin.ultron.core.config.UltronConfig
11 | import com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence
12 | import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule
13 | import com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule
14 | import org.junit.*
15 |
16 | abstract class BaseTest {
17 | //attach screenshot to allure report in case of failure
18 | //attach logcat to allure report in case of failure
19 | @get:Rule
20 | val ruleSequence = RuleSequence(
21 | SetUpRule().add {
22 | UltronConfig.Espresso.ESPRESSO_OPERATION_POLLING_TIMEOUT = 0L
23 | idlingContainer.set(getDefaultIdlingScope())
24 | idling { IdlingRegistry.getInstance().register(BaseIdlingResource(contactsIdling)) }
25 | },
26 | TearDownRule().add {
27 | idling { IdlingRegistry.getInstance().unregister(BaseIdlingResource(contactsIdling)) }
28 | }
29 | )
30 |
31 | companion object {
32 | @BeforeClass
33 | @JvmStatic
34 | fun beforeClassBase() {
35 | UltronConfig.addGlobalListener(ScreenshotLifecycleListener())
36 | UltronConfig.addGlobalListener(WindowHierarchyDumpListener())
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/tests/BlacklistTests.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.tests
2 |
3 | import androidx.test.rule.ActivityTestRule
4 | import com.atiurin.espressoguide.activity.SettingsActivity
5 | import com.atiurin.espressoguide.data.repositories.ContactsRepositoty
6 | import com.atiurin.espressoguide.fragment.settings.BlacklistFragment
7 | import com.atiurin.espressoguide.fragment.settings.SettingsFragmentNavigator
8 | import com.atiurin.espressoguide.pages.BlacklistPage
9 | import com.atiurin.ultron.testlifecycle.setupteardown.SetUp
10 | import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule
11 | import io.qameta.allure.kotlin.junit4.DisplayName
12 | import org.junit.Test
13 |
14 | class BlacklistTests : BaseTest() {
15 | companion object {
16 | const val ADD_CONTACT_TO_BLACKLIST = "ADD_CONTACT_TO_BLACKLIST"
17 | const val CLEAR_BLACKLIST = "CLEAR_BLACKLIST"
18 | }
19 |
20 | val contact = ContactsRepositoty.getContact(2)
21 |
22 | private val activityRule = ActivityTestRule(SettingsActivity::class.java)
23 | private val setUpTearDownRule = SetUpRule()
24 | .add(CLEAR_BLACKLIST) {
25 | ContactsRepositoty.clearBlacklist()
26 | }
27 | .add(ADD_CONTACT_TO_BLACKLIST) {
28 | ContactsRepositoty.addToBlacklist(contact.id)
29 | }
30 |
31 | init {
32 | ruleSequence
33 | .add(setUpTearDownRule, activityRule)
34 | .addLast(SetUpRule().add {
35 | activityRule.runOnUiThread {
36 | SettingsFragmentNavigator.go(BlacklistFragment::class.java)
37 | }
38 | })
39 | }
40 |
41 | @DisplayName("when contact in blacklist then it displayed in blacklist")
42 | @SetUp(CLEAR_BLACKLIST, ADD_CONTACT_TO_BLACKLIST)
43 | @Test
44 | fun testItemDisplayedInBlacklist() {
45 | BlacklistPage.assertContactDisplayed(contact.name)
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/tests/ChatPageTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.tests
2 |
3 | import android.content.Intent
4 | import androidx.test.core.app.ActivityScenario
5 | import androidx.test.platform.app.InstrumentationRegistry
6 | import com.atiurin.espressoguide.activity.ChatActivity
7 | import com.atiurin.espressoguide.activity.INTENT_CONTACT_ID_EXTRA_NAME
8 | import com.atiurin.espressoguide.data.entities.Message
9 | import com.atiurin.espressoguide.data.repositories.CURRENT_USER
10 | import com.atiurin.espressoguide.data.repositories.ContactsRepositoty
11 | import com.atiurin.espressoguide.data.repositories.MessageRepository
12 | import com.atiurin.espressoguide.managers.AccountManager
13 | import com.atiurin.espressoguide.pages.ChatPage
14 | import com.atiurin.ultron.testlifecycle.setupteardown.SetUp
15 | import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule
16 | import org.junit.Test
17 |
18 | class ChatPageTest : BaseTest() {
19 | companion object {
20 | const val ADD_SIMPLE_MESSAGE = "simple_message"
21 | const val ADD_SPECIAL_CHARS_MESSAGE = "special_chars_message"
22 | const val ADD_LONG_MESSAGE = "long_message"
23 | }
24 |
25 | private val contact = ContactsRepositoty.getContact(2)
26 | private val simpleMessage = Message(CURRENT_USER.id, contact.id, "SimpleText")
27 | private val specialCharsMessage =
28 | Message(CURRENT_USER.id, contact.id, "!@#$%^&*(){}:\",./<>?_+±§`~][")
29 | private val longMessage = Message(
30 | CURRENT_USER.id,
31 | contact.id,
32 | InstrumentationRegistry.getInstrumentation().context.assets.open("long_message.txt")
33 | .reader().readText()
34 | )
35 | private val setUpTearDownRule = SetUpRule()
36 | .add { MessageRepository.clearChatMessages(contact.id) }
37 | .add(ADD_SIMPLE_MESSAGE) {
38 | MessageRepository.addChatMessage(contact.id, simpleMessage)
39 | }
40 | .add(ADD_SPECIAL_CHARS_MESSAGE) {
41 | MessageRepository.addChatMessage(contact.id, specialCharsMessage)
42 | }
43 | .add(ADD_LONG_MESSAGE) {
44 | MessageRepository.addChatMessage(contact.id, longMessage)
45 | }
46 | .add {
47 | AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login(
48 | CURRENT_USER.login,
49 | CURRENT_USER.password
50 | )
51 | val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ChatActivity::class.java).putExtra(INTENT_CONTACT_ID_EXTRA_NAME, contact.id)
52 | ActivityScenario.launch(intent)
53 | }
54 |
55 | init {
56 | ruleSequence.add(setUpTearDownRule)
57 | ChatPage.contact = contact
58 | }
59 |
60 | @Test
61 | @SetUp(ADD_SIMPLE_MESSAGE)
62 | fun assertSimpleMessage() {
63 | ChatPage.assertMessageDisplayed(simpleMessage.text)
64 | }
65 |
66 | @Test
67 | @SetUp(ADD_SPECIAL_CHARS_MESSAGE)
68 | fun assertSpecialCharsMessage() {
69 | ChatPage.assertMessageDisplayed(specialCharsMessage.text)
70 | }
71 |
72 | @Test
73 | @SetUp(ADD_LONG_MESSAGE)
74 | fun assertLongMessage() {
75 | ChatPage.assertMessageDisplayed(longMessage.text)
76 | }
77 |
78 | @Test
79 | fun assertChatTitle() {
80 | ChatPage.assertChatTitle()
81 | }
82 |
83 | @Test
84 | fun addNewMessage() {
85 | val messageText = "new message"
86 | ChatPage {
87 | sendMessage(messageText)
88 | assertMessageDisplayed(messageText)
89 | }
90 | }
91 |
92 | @Test
93 | @SetUp(ADD_SIMPLE_MESSAGE, ADD_SPECIAL_CHARS_MESSAGE)
94 | fun assertMessagePosition() {
95 | val messageText = "position message"
96 | val initialMaxPosition = MessageRepository.getChatMessagesCount(contact.id) - 1
97 | ChatPage {
98 | sendMessage(messageText)
99 | assertMessageTextAtPosition(initialMaxPosition + 1, messageText)
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/tests/DemoEspressoTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.tests
2 |
3 | import androidx.test.ext.junit.rules.ActivityScenarioRule
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import com.atiurin.espressoguide.activity.MainActivity
6 | import com.atiurin.espressoguide.data.repositories.CURRENT_USER
7 | import com.atiurin.espressoguide.data.repositories.ContactsRepositoty
8 | import com.atiurin.espressoguide.managers.AccountManager
9 | import com.atiurin.espressoguide.pages.ChatPage
10 | import com.atiurin.espressoguide.pages.FriendsListPage
11 | import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule
12 | import io.qameta.allure.kotlin.junit4.DisplayName
13 | import org.junit.*
14 |
15 |
16 | /**
17 | * There are several ways of how to use page object class.
18 | * Use the one you prefer
19 | */
20 | class DemoEspressoTest : BaseTest() {
21 | private val contact = ContactsRepositoty.getContact("Chandler Bing")
22 |
23 | init {
24 | ruleSequence
25 | .add(
26 | SetUpRule()
27 | .add {
28 | //make login into app before test starts and activity is launched
29 | //to make sure that user is logged in when test starts
30 | AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login(
31 | CURRENT_USER.login,
32 | CURRENT_USER.password
33 | )
34 | ChatPage.contact = contact
35 | })
36 | .addLast(ActivityScenarioRule(MainActivity::class.java))
37 | }
38 |
39 | @Test
40 | fun friendsItemCheck() {
41 | FriendsListPage {
42 | assertName("Janice")
43 | assertStatus("Janice", "Oh. My. God")
44 | }
45 | }
46 |
47 | @Test
48 | fun sendMessage() {
49 | val chatPage = FriendsListPage.openChat(contact)
50 | chatPage.clearHistory()
51 | chatPage.sendMessage("test message")
52 | }
53 |
54 | @Test
55 | fun checkMessagesPositionsInChat() {
56 | val firstMessage = "first message"
57 | val secondMessage = "second message"
58 | FriendsListPage { openChat(contact) }
59 | ChatPage {
60 | clearHistory()
61 | sendMessage(firstMessage)
62 | sendMessage(secondMessage)
63 | assertMessageTextAtPosition(0, firstMessage)
64 | assertMessageTextAtPosition(1, secondMessage)
65 | }
66 | }
67 |
68 | /**
69 | * Test should fail
70 | */
71 | @DisplayName("Special failed test for allure report demo")
72 | @Test
73 | fun specialFailedTestForAllureReport() {
74 | val firstMessage = "first message"
75 | val secondMessage = "second message"
76 | val janiceContact = ContactsRepositoty.getContact("Janice")
77 | val chatPage = FriendsListPage.openChat(janiceContact)
78 | chatPage {
79 | clearHistory()
80 | sendMessage(firstMessage)
81 | sendMessage(secondMessage)
82 | assertMessageTextAtPosition(0, secondMessage)
83 | assertMessageTextAtPosition(1, firstMessage)
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/tests/SetUpTearDownDemoTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.tests
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import com.atiurin.espressoguide.activity.MainActivity
5 | import com.atiurin.espressoguide.data.repositories.CURRENT_USER
6 | import com.atiurin.espressoguide.data.repositories.ContactsRepositoty
7 | import com.atiurin.espressoguide.framework.CustomActivityTestRule
8 | import com.atiurin.espressoguide.Logger
9 | import com.atiurin.espressoguide.managers.AccountManager
10 | import com.atiurin.espressoguide.pages.ChatPage
11 | import com.atiurin.espressoguide.pages.FriendsListPage
12 | import com.atiurin.ultron.testlifecycle.setupteardown.SetUp
13 | import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule
14 | import com.atiurin.ultron.testlifecycle.setupteardown.TearDown
15 | import com.atiurin.ultron.testlifecycle.setupteardown.TearDownRule
16 | import org.junit.*
17 |
18 | class SetUpTearDownDemoTest : BaseTest() {
19 | lateinit var chatPage: ChatPage
20 | private val contact = ContactsRepositoty.getContact("Chandler Bing")
21 |
22 | companion object {
23 | const val FIRST_SETUP = "FirstSetup"
24 | const val SECOND_SETUP = "SecondSetup"
25 | const val FIRST_TEARDOWN = "FirstTearDown"
26 | const val SECOND_TEARDOWN = "SecondTearDown"
27 | }
28 |
29 | init {
30 | ruleSequence
31 | .addFirst(
32 | SetUpRule()
33 | .add {
34 | Logger.life("common setup")
35 | //make login into app before test starts and activity is launched
36 | //to make sure that user is logged in when test starts
37 | AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login(
38 | CURRENT_USER.login,
39 | CURRENT_USER.password
40 | )
41 | }
42 | .add(FIRST_SETUP) { Logger.life("first setup") }
43 | .add(SECOND_SETUP) { Logger.life("second setup") } ,
44 | TearDownRule()
45 | .add { Logger.life("common tear down") }
46 | .add(FIRST_TEARDOWN) { Logger.life("first tear down") }
47 | .add(SECOND_TEARDOWN) { Logger.life("second tear down") })
48 | .addLast(CustomActivityTestRule(MainActivity::class.java))
49 | }
50 |
51 | @Test
52 | fun friendsItemCheck() {
53 | Logger.life("test friendsItemCheck")
54 | FriendsListPage {
55 | assertName("Janice")
56 | assertStatus("Janice", "Oh. My. God")
57 | }
58 | }
59 |
60 | @SetUp(FIRST_SETUP)
61 | @TearDown(SECOND_TEARDOWN)
62 | @Test
63 | fun sendMessage() {
64 | Logger.life("test sendMessage")
65 | chatPage = FriendsListPage.openChat(contact)
66 | chatPage {
67 | clearHistory()
68 | sendMessage("test message")
69 | }
70 |
71 | }
72 |
73 | @SetUp(SECOND_SETUP, FIRST_SETUP)
74 | @TearDown(SECOND_TEARDOWN)
75 | @Test
76 | fun checkMessagesPositionsInChat() {
77 | Logger.life("test checkMessagesPositionsInChat")
78 | val firstMessage = "first message"
79 | val secondMessage = "second message"
80 | val chatPage = FriendsListPage.openChat(contact)
81 | chatPage {
82 | clearHistory()
83 | sendMessage(firstMessage)
84 | sendMessage(secondMessage)
85 | assertMessageTextAtPosition(0, firstMessage)
86 | assertMessageTextAtPosition(1, secondMessage)
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/atiurin/espressoguide/tests/SimpleEspressoTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.tests
2 |
3 | import android.content.Intent
4 | import androidx.test.espresso.Espresso.onView
5 | import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
6 | import androidx.test.espresso.action.ViewActions.click
7 | import androidx.test.espresso.action.ViewActions.typeText
8 | import androidx.test.espresso.assertion.ViewAssertions.matches
9 | import androidx.test.espresso.matcher.ViewMatchers.*
10 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
11 | import androidx.test.rule.ActivityTestRule
12 | import com.atiurin.espressoguide.R
13 | import com.atiurin.espressoguide.activity.MainActivity
14 | import com.atiurin.espressoguide.data.repositories.CURRENT_USER
15 | import com.atiurin.espressoguide.managers.AccountManager
16 | import org.junit.Before
17 | import org.junit.Rule
18 | import org.junit.Test
19 |
20 | class SimpleEspressoTest : BaseTest() {
21 | @Rule
22 | @JvmField
23 | val activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
24 |
25 | @Before
26 | fun backgroundLogin() {
27 | //make login into app before test start and activity launched
28 | //to be sure that user is logged in when test start
29 | AccountManager(getInstrumentation().targetContext).login(
30 | CURRENT_USER.login,
31 | CURRENT_USER.password
32 | )
33 | activityTestRule.launchActivity(Intent())
34 | }
35 |
36 | /**
37 | * bad approach: chandler could be invisible (out of screen)
38 | in this case test will fail, look at better way in [AdvancedEspressoTest]
39 | */
40 | @Test
41 | fun simpleEspressoTest_SendStringMessage() {
42 | val messageText = "message1"
43 | onView(withText("Chandler Bing")).perform(click())
44 | openActionBarOverflowOrOptionsMenu(getInstrumentation().context)
45 | onView(withText("Clear history")).perform(click())
46 | onView(withId(R.id.message_input_text)).perform(typeText(messageText))
47 | onView(withId(R.id.send_button)).perform(click())
48 | onView(withText(messageText)).check(matches(isDisplayed()))
49 | }
50 |
51 | @Test
52 | fun simpleEspressoTest_SendNumbers() {
53 | val messageText = "1234567890"
54 | onView(withText("Rachel Green")).perform(click())
55 | openActionBarOverflowOrOptionsMenu(getInstrumentation().context)
56 | onView(withText("Clear history")).perform(click())
57 | onView(withId(R.id.message_input_text)).perform(typeText(messageText))
58 | onView(withId(R.id.send_button)).perform(click())
59 | onView(withText(messageText)).check(matches(isDisplayed()))
60 | }
61 | }
--------------------------------------------------------------------------------
/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
31 |
34 |
37 |
40 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide
2 |
3 | import android.util.Log
4 |
5 | object Logger {
6 | private val LOG_TAG = "EspressoGuide"
7 | fun info(message: String) = Log.i(LOG_TAG, message)
8 | fun debug(message: String) = Log.d(LOG_TAG, message)
9 | fun error(message: String) = Log.e(LOG_TAG, message)
10 | fun warning(message: String) = Log.w(LOG_TAG, message)
11 | fun life(message: String) = Log.d("Life>>", message)
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/MyApplication.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide
2 |
3 | import android.app.Application
4 | import android.content.Context
5 |
6 | object MyApplication : Application() {
7 | override fun onCreate() {
8 | super.onCreate()
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/activity/ChatActivity.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.activity
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.util.Log
6 | import android.view.Menu
7 | import android.view.MenuItem
8 | import android.view.View
9 | import android.widget.*
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.appcompat.widget.Toolbar
12 | import androidx.recyclerview.widget.LinearLayoutManager
13 | import androidx.recyclerview.widget.RecyclerView
14 | import com.atiurin.espressoguide.view.CircleImageView
15 | import com.atiurin.espressoguide.R
16 | import com.atiurin.espressoguide.adapters.MessageAdapter
17 | import com.atiurin.espressoguide.data.entities.Contact
18 | import com.atiurin.espressoguide.data.entities.Message
19 | import com.atiurin.espressoguide.data.repositories.CURRENT_USER
20 | import com.atiurin.espressoguide.data.repositories.ContactsRepositoty
21 | import com.atiurin.espressoguide.data.repositories.MessageRepository
22 | import com.atiurin.espressoguide.managers.AccountManager
23 | import com.google.android.material.snackbar.Snackbar
24 |
25 | const val INTENT_CONTACT_ID_EXTRA_NAME = "contactId"
26 |
27 | class ChatActivity : AppCompatActivity() {
28 | private lateinit var recyclerView: RecyclerView
29 | private lateinit var viewAdapter: MessageAdapter
30 | private lateinit var viewManager: RecyclerView.LayoutManager
31 | private lateinit var contact: Contact
32 | private val onItemClickListener: View.OnClickListener? = null
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 | val accountManager = AccountManager(applicationContext)
36 | if (!accountManager.isLoggedIn()) {
37 | val intent = Intent(applicationContext, LoginActivity::class.java)
38 | startActivity(intent)
39 | }
40 | setContentView(R.layout.activity_chat)
41 | val context = this
42 | //TOOLBAR
43 | val toolbar: Toolbar = findViewById(R.id.toolbar)
44 | toolbar.title = ""
45 | setSupportActionBar(toolbar)
46 | supportActionBar!!.setDisplayHomeAsUpEnabled(true)
47 | window.statusBarColor = getColor(R.color.colorPrimaryDark)
48 | val intent = intent
49 | val contactId = intent.getIntExtra(INTENT_CONTACT_ID_EXTRA_NAME, -1)
50 | val title = findViewById(R.id.toolbar_title)
51 | if (contactId < 0) {
52 | Log.d("EspressoGuide", "Something goes wrong!")
53 | }
54 | contact = ContactsRepositoty.getContact(contactId)
55 | title.text = contact.name
56 | val avatar = findViewById(R.id.toolbar_avatar)
57 | avatar.setImageDrawable(getDrawable(contact.avatar))
58 | //message input area
59 | val messageInput = findViewById(R.id.message_input_text)
60 | val sendBtn = findViewById(R.id.send_button)
61 | val attachBtn = findViewById(R.id.attach_button)
62 |
63 | //recycler view and adapter
64 | viewManager = LinearLayoutManager(this)
65 | viewAdapter = MessageAdapter(
66 | ArrayList(),
67 | object : MessageAdapter.OnItemClickListener {
68 | override fun onItemClick(message: Message) {
69 | Log.w("EspressoGuid", "Clicked message ${message.text}")
70 | }
71 |
72 | override fun onItemLongClick(message: Message) {
73 | Log.w("EspressoGuid", "Long Clicked message ${message.text}")
74 | }
75 | })
76 | recyclerView = findViewById(R.id.messages_list).apply {
77 | setHasFixedSize(true)
78 | layoutManager = viewManager
79 | adapter = viewAdapter
80 | }
81 | sendBtn.setOnClickListener {
82 | if (messageInput.text.isEmpty()) {
83 | Toast.makeText(context, "Type message text", Toast.LENGTH_LONG).show()
84 | } else {
85 | val mes = Message(CURRENT_USER.id, contactId, messageInput.text.toString())
86 | val chatMessages = MessageRepository.getChatMessages(contactId)
87 | chatMessages.add(mes)
88 | updateAdapter(chatMessages)
89 | messageInput.setText("")
90 | recyclerView.smoothScrollToPosition(viewAdapter.itemCount - 1)
91 | }
92 | }
93 | attachBtn.setOnClickListener{
94 | Snackbar.make(recyclerView, "Attach not implemented", Snackbar.LENGTH_LONG)
95 | .setAction("Action", null).show()
96 | }
97 | }
98 |
99 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
100 | menuInflater.inflate(R.menu.main, menu)
101 | return true
102 | }
103 |
104 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
105 | return when (item.itemId) {
106 | R.id.action_clear -> {
107 | MessageRepository.clearChatMessages(contact.id)
108 | updateAdapter(ArrayList())
109 | true
110 | }
111 | R.id.action_add_blacklist -> {
112 | ContactsRepositoty.addToBlacklist(contact.id)
113 | true
114 | }
115 | else -> super.onOptionsItemSelected(item)
116 | }
117 | }
118 |
119 | override fun onSupportNavigateUp(): Boolean {
120 | onBackPressed()
121 | return true
122 | }
123 |
124 | override fun onResume() {
125 | super.onResume()
126 | viewAdapter.updateData(MessageRepository.getChatMessages(contact.id))
127 | viewAdapter.notifyDataSetChanged()
128 | }
129 |
130 | private fun updateAdapter(list: MutableList) {
131 | viewAdapter.updateData(list)
132 | viewAdapter.notifyDataSetChanged()
133 | }
134 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/activity/LoginActivity.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.activity
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.Gravity
6 | import android.widget.Button
7 | import android.widget.EditText
8 | import android.widget.Toast
9 | import androidx.appcompat.app.ActionBar
10 | import androidx.appcompat.app.AppCompatActivity
11 | import com.atiurin.espressoguide.R
12 | import com.atiurin.espressoguide.managers.AccountManager
13 |
14 | class LoginActivity : AppCompatActivity(){
15 | lateinit var etUserName : EditText
16 | lateinit var etPassword : EditText
17 | lateinit var loginBtn : Button
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | setContentView(R.layout.activity_login)
22 |
23 | val bar = supportActionBar
24 | bar!!.title = "Login or Sign Up"
25 |
26 | etUserName = findViewById(R.id.et_username)
27 | etPassword = findViewById(R.id.et_password)
28 | loginBtn = findViewById(R.id.login_button)
29 |
30 | loginBtn.setOnClickListener{
31 | val accountManager = AccountManager(applicationContext)
32 | val userName = etUserName.text.toString()
33 | val password = etPassword.text.toString()
34 |
35 | if (userName.isEmpty()){
36 | with(etUserName){
37 | setHint("Enter user name")
38 | setHintTextColor(resources.getColor(android.R.color.holo_red_dark))
39 | }
40 | }
41 | if (password.isEmpty()){
42 | with(etPassword){
43 | setHint("Enter password")
44 | setHintTextColor(resources.getColor(android.R.color.holo_red_dark))
45 | }
46 | }
47 | val result = accountManager.login(userName, password)
48 | if (result){
49 | var intent = Intent(this, MainActivity::class.java)
50 | startActivity(intent)
51 | }else{
52 | var toast = Toast.makeText(applicationContext, "Wrong login or password", Toast.LENGTH_LONG)
53 | toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0)
54 | toast.show()
55 | }
56 | }
57 | }
58 |
59 |
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.activity
2 |
3 | import android.os.Bundle
4 | import com.google.android.material.snackbar.Snackbar
5 | import androidx.core.view.GravityCompat
6 | import androidx.appcompat.app.ActionBarDrawerToggle
7 | import android.view.MenuItem
8 | import androidx.drawerlayout.widget.DrawerLayout
9 | import com.google.android.material.navigation.NavigationView
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.appcompat.widget.Toolbar
12 | import androidx.recyclerview.widget.LinearLayoutManager
13 | import androidx.recyclerview.widget.RecyclerView
14 | import android.content.Intent
15 | import com.atiurin.espressoguide.adapters.ContactAdapter
16 | import com.atiurin.espressoguide.data.entities.Contact
17 | import com.atiurin.espressoguide.R
18 | import kotlin.collections.ArrayList
19 | import android.view.View
20 | import android.widget.Toast
21 | import com.atiurin.espressoguide.Logger
22 | import com.atiurin.espressoguide.async.ContactsPresenter
23 | import com.atiurin.espressoguide.async.ContactsProvider
24 | import com.atiurin.espressoguide.data.Tags
25 | import com.atiurin.espressoguide.idlingresources.idling
26 | import com.atiurin.espressoguide.managers.AccountManager
27 | import com.atiurin.espressoguide.view.CircleImageView
28 |
29 |
30 | class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, ContactsProvider {
31 |
32 |
33 | private lateinit var recyclerView: RecyclerView
34 | private lateinit var viewAdapter: ContactAdapter
35 | private lateinit var viewManager: RecyclerView.LayoutManager
36 | private lateinit var accountManager: AccountManager
37 | private val onItemClickListener: View.OnClickListener? = null
38 | private val contactsPresenter = ContactsPresenter(this)
39 |
40 | override fun onCreate(savedInstanceState: Bundle?) {
41 | super.onCreate(savedInstanceState)
42 | accountManager = AccountManager(applicationContext)
43 | if (!accountManager.isLoggedIn()) {
44 | val intent = Intent(applicationContext, LoginActivity::class.java)
45 | startActivity(intent)
46 | }
47 | setContentView(R.layout.activity_main)
48 | val toolbar: Toolbar = findViewById(R.id.toolbar)
49 | toolbar.setTitle(R.string.title_friends_list)
50 | setSupportActionBar(toolbar)
51 | val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
52 | val navView: NavigationView = findViewById(R.id.nav_view)
53 | val navigationAvatar = navView.getHeaderView(0).findViewById(R.id.navigation_user_avatar)
54 | navigationAvatar.setOnClickListener {
55 | startActivity(Intent(applicationContext, ProfileActivity::class.java))
56 | }
57 |
58 | val toggle = ActionBarDrawerToggle(
59 | this, drawerLayout, toolbar,
60 | R.string.navigation_drawer_open,
61 | R.string.navigation_drawer_close
62 | )
63 | drawerLayout.addDrawerListener(toggle)
64 | toggle.syncState()
65 |
66 | navView.setNavigationItemSelectedListener(this)
67 |
68 | viewManager = LinearLayoutManager(this)
69 | viewAdapter = ContactAdapter(ArrayList(),
70 | object : ContactAdapter.OnItemClickListener {
71 | override fun onItemClick(contact: Contact) {
72 | val intent = Intent(applicationContext, ChatActivity::class.java)
73 | intent.putExtra(INTENT_CONTACT_ID_EXTRA_NAME, contact.id)
74 | startActivity(intent)
75 | }
76 | override fun onItemLongClick(contact: Contact) {
77 | }
78 | })
79 | recyclerView = findViewById(R.id.recycler_friends).apply {
80 | setHasFixedSize(true)
81 | layoutManager = viewManager
82 | adapter = viewAdapter
83 | }
84 |
85 | recyclerView.tag = Tags.CONTACTS_LIST
86 | contactsPresenter.getAllContacts()
87 | }
88 |
89 | override fun onBackPressed() {
90 | val drawerLayout: DrawerLayout = findViewById(com.atiurin.espressoguide.R.id.drawer_layout)
91 | if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
92 | drawerLayout.closeDrawer(GravityCompat.START)
93 | } else {
94 | super.onBackPressed()
95 | }
96 | }
97 |
98 | override fun onResume() {
99 | super.onResume()
100 | contactsPresenter.getAllContacts()
101 | }
102 |
103 | override fun onContactsLoaded(contacts: List) {
104 | viewAdapter.updateData(contacts)
105 | viewAdapter.notifyDataSetChanged()
106 | idling { contactsIdling.onDataLoaded() }
107 | }
108 |
109 | override fun onFailedToLoadContacts(message: String?) {
110 | Toast.makeText(this, message, Toast.LENGTH_LONG).show()
111 | }
112 |
113 | override fun onNavigationItemSelected(item: MenuItem): Boolean {
114 | // Handle navigation view item clicks here.
115 | when (item.itemId) {
116 | R.id.nav_settings -> {
117 | Logger.error("Starting activity SettingsActivity")
118 | startActivity(Intent(applicationContext, SettingsActivity::class.java))
119 | }
120 | R.id.nav_saved_messages -> {
121 | Snackbar.make(recyclerView, "Saved messages not implemented", Snackbar.LENGTH_LONG)
122 | .setAction("Action", null).show()
123 | }
124 | R.id.nav_profile -> {
125 | startActivity(Intent(applicationContext, ProfileActivity::class.java))
126 | }
127 | R.id.nav_logout -> {
128 | accountManager.logout()
129 | val intent = Intent(applicationContext, LoginActivity::class.java)
130 | startActivity(intent)
131 | }
132 | }
133 | val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
134 | drawerLayout.closeDrawer(GravityCompat.START)
135 | return true
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/activity/ProfileActivity.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.activity
2 |
3 | import android.os.Bundle
4 | import android.os.PersistableBundle
5 | import android.text.Editable
6 | import android.widget.EditText
7 | import android.widget.TextView
8 | import androidx.appcompat.app.AppCompatActivity
9 | import com.atiurin.espressoguide.R
10 | import com.atiurin.espressoguide.data.repositories.CURRENT_USER
11 | import com.atiurin.espressoguide.view.CircleImageView
12 | import kotlinx.android.synthetic.main.activity_profile.*
13 |
14 | class ProfileActivity : AppCompatActivity(){
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | setContentView(R.layout.activity_profile)
18 | val avatar = findViewById(R.id.avatar)
19 | avatar.setImageDrawable(getDrawable(CURRENT_USER.avatar))
20 | val name = findViewById(R.id.et_username)
21 | name.hint = CURRENT_USER.name
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/activity/SettingsActivity.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.activity
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.appcompat.widget.Toolbar
6 | import com.atiurin.espressoguide.Logger
7 | import com.atiurin.espressoguide.R
8 | import com.atiurin.espressoguide.fragment.settings.BlacklistFragment
9 | import com.atiurin.espressoguide.fragment.settings.SettingsFragmentNavigator
10 | import com.atiurin.espressoguide.fragment.settings.SettingsFragment
11 |
12 | class SettingsActivity : AppCompatActivity() {
13 | companion object {
14 | const val INTENT_FRAGMENT_CLASS = "INTENT_FRAGMENT_CLASS"
15 | }
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | Logger.error("SettingsActivity")
19 | super.onCreate(savedInstanceState)
20 | setContentView(R.layout.activity_settings)
21 | val intent = intent
22 | val fragmentClass = intent.getStringExtra(INTENT_FRAGMENT_CLASS)
23 | ?: SettingsFragmentNavigator.FragmentType.SETTINGS_LIST.name
24 | //TOOLBAR
25 | val toolbar: Toolbar = findViewById(R.id.toolbar)
26 | setSupportActionBar(toolbar)
27 | supportActionBar!!.setDisplayHomeAsUpEnabled(true)
28 | window.statusBarColor = getColor(R.color.colorPrimaryDark)
29 | SettingsFragmentNavigator.init(this)
30 | val fragmentManager = supportFragmentManager
31 | val fragment = SettingsFragmentNavigator.getFragment(fragmentClass)
32 | fragmentManager.beginTransaction()
33 | .add(R.id.fragment_container, fragment)
34 | .commit()
35 | }
36 |
37 | override fun onSupportNavigateUp(): Boolean {
38 | onBackPressed()
39 | return true
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/activity/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.activity
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import androidx.appcompat.app.AppCompatActivity
6 | import com.atiurin.espressoguide.managers.AccountManager
7 |
8 | class SplashActivity : AppCompatActivity() {
9 |
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 |
13 | val accountManager = AccountManager(applicationContext)
14 | if (accountManager.isLoggedIn()){
15 | val intent = Intent(applicationContext, MainActivity::class.java)
16 | startActivity(intent)
17 | }else{
18 | val intent = Intent(applicationContext, LoginActivity::class.java)
19 | startActivity(intent)
20 | }
21 | finish()
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/adapters/ContactAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.adapters
2 |
3 | import android.view.ContextMenu
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.LinearLayout
8 | import android.widget.TextView
9 | import androidx.recyclerview.widget.RecyclerView
10 | import com.atiurin.espressoguide.R
11 | import com.atiurin.espressoguide.data.entities.Contact
12 | import com.atiurin.espressoguide.view.CircleImageView
13 |
14 | class ContactAdapter(private var contacts: List, val listener: OnItemClickListener) :
15 | RecyclerView.Adapter() {
16 | private val contactPosition = mutableMapOf()
17 | interface OnItemClickListener {
18 | fun onItemClick(contact: Contact)
19 | fun onItemLongClick(contact: Contact)
20 | }
21 |
22 | class MyViewHolder(val view: LinearLayout) : RecyclerView.ViewHolder(view)
23 |
24 | open fun updateData(data: List) {
25 | contacts = data
26 | }
27 |
28 | override fun onCreateViewHolder(
29 | parent: ViewGroup,
30 | viewType: Int
31 | ): MyViewHolder {
32 | val view = LayoutInflater.from(parent.context)
33 | .inflate(R.layout.friends_list_item, parent, false) as LinearLayout
34 | val viewHolder = MyViewHolder(view)
35 | return viewHolder
36 | }
37 |
38 | override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
39 | holder.view.setOnClickListener { listener.onItemClick(contacts[position]) }
40 | holder.view.setOnLongClickListener {
41 | listener.onItemLongClick(contacts[position])
42 | true
43 | }
44 | val tvTitle = holder.view.findViewById(R.id.tv_name) as TextView
45 | val avatar = holder.view.findViewById(R.id.avatar) as CircleImageView
46 | val status = holder.view.findViewById(R.id.tv_status) as TextView
47 | tvTitle.text = contacts[position].name
48 | status.text = contacts[position].status
49 | avatar.setImageDrawable(holder.view.context.resources.getDrawable(contacts[position].avatar))
50 | contactPosition[contacts[position]] = holder.view
51 | }
52 |
53 | fun getContactView(contact: Contact): View {
54 | return contactPosition[contact]!!
55 | }
56 | override fun getItemCount() = contacts.size
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/adapters/MessageAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.adapters
2 |
3 | import android.view.Gravity
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import android.widget.LinearLayout
7 | import android.widget.TextView
8 | import androidx.cardview.widget.CardView
9 | import androidx.core.view.get
10 | import androidx.recyclerview.widget.RecyclerView
11 | import com.atiurin.espressoguide.R
12 | import com.atiurin.espressoguide.data.entities.Message
13 | import com.atiurin.espressoguide.data.repositories.CURRENT_USER
14 |
15 |
16 | class MessageAdapter(private var messages: MutableList, val listener: OnItemClickListener) :
17 | RecyclerView.Adapter() {
18 |
19 | interface OnItemClickListener {
20 | fun onItemClick(item: Message)
21 | fun onItemLongClick(item: Message)
22 | }
23 | class MessageViewHolder(val view: LinearLayout) : RecyclerView.ViewHolder(view)
24 |
25 | open fun updateData(data: MutableList) {
26 | messages = data
27 | }
28 |
29 | override fun onCreateViewHolder(
30 | parent: ViewGroup,
31 | viewType: Int
32 | ): MessageViewHolder {
33 | val view = LayoutInflater.from(parent.context)
34 | .inflate(R.layout.message_item, parent, false) as LinearLayout
35 | return MessageViewHolder(view)
36 | }
37 |
38 | override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
39 | holder.view.setOnClickListener {
40 | listener.onItemClick(messages[position])
41 | }
42 | holder.view.setOnLongClickListener {
43 | listener.onItemLongClick(messages[position])
44 | true
45 | }
46 | val messageText = holder.view.findViewById(R.id.message_text) as TextView
47 | val authorName = holder.view.findViewById(R.id.author) as TextView
48 | val message = messages[position]
49 | messageText.text = message.text
50 | if (message.authorId == CURRENT_USER.id){
51 | val view = holder.view[0]
52 | val cardView = view.findViewById(R.id.card_view)
53 | cardView.setCardBackgroundColor(view.context.resources.getColor(R.color.colorLight))
54 | val layoutParams = view.layoutParams
55 | if (layoutParams is LinearLayout.LayoutParams){
56 | layoutParams.gravity = Gravity.RIGHT
57 | }
58 | view.layoutParams = layoutParams
59 | }
60 |
61 | }
62 |
63 | override fun getItemCount() = messages.size
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/adapters/SettingsAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.adapters
2 |
3 |
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import android.widget.LinearLayout
7 | import android.widget.TextView
8 | import androidx.recyclerview.widget.RecyclerView
9 | import com.atiurin.espressoguide.R
10 | import com.atiurin.espressoguide.core.settings.SettingsItem
11 |
12 | class SettingsAdapter(private var dataset: List, val listener: OnItemClickListener) :
13 | RecyclerView.Adapter() {
14 |
15 | interface OnItemClickListener {
16 | fun onItemClick(item: SettingsItem)
17 | }
18 | class MyViewHolder(val view: LinearLayout) : RecyclerView.ViewHolder(view)
19 |
20 | override fun onCreateViewHolder(
21 | parent: ViewGroup,
22 | viewType: Int
23 | ): MyViewHolder {
24 | val view = LayoutInflater.from(parent.context)
25 | .inflate(R.layout.settings_list_item, parent, false) as LinearLayout
26 | return MyViewHolder(view)
27 | }
28 |
29 | override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
30 | holder.view.setOnClickListener { listener.onItemClick(dataset.get(position)) }
31 | val tvTitle = holder.view.findViewById(R.id.tv_name) as TextView
32 | tvTitle.text = dataset[position].name
33 | }
34 |
35 | override fun getItemCount() = dataset.size
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/async/ContactsPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.async
2 |
3 | import com.atiurin.espressoguide.data.entities.Contact
4 | import com.atiurin.espressoguide.idlingresources.idling
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.Job
8 | import kotlinx.coroutines.launch
9 | import kotlin.coroutines.CoroutineContext
10 |
11 | class ContactsPresenter (
12 | private val executor: T,
13 | private val coroutineContext: CoroutineContext = Dispatchers.Default) {
14 |
15 | protected lateinit var scope: PresenterCoroutineScope
16 |
17 | fun getAllContacts() {
18 | idling { contactsIdling.onLoadStarted() }
19 | scope = PresenterCoroutineScope(coroutineContext)
20 | scope.launch {
21 | GetContacts()(
22 | UseCase.None,
23 | onSuccess = { executor.onContactsLoaded(it) },
24 | onFailure = { executor.onFailedToLoadContacts(it.message) }
25 | )
26 | }
27 | }
28 |
29 | fun getBlacklist() {
30 | idling { contactsIdling.onLoadStarted() }
31 | scope = PresenterCoroutineScope(coroutineContext)
32 | scope.launch {
33 | GetBlacklist()(
34 | UseCase.None,
35 | onSuccess = { executor.onContactsLoaded(it) },
36 | onFailure = { executor.onFailedToLoadContacts(it.message) }
37 | )
38 | }
39 | }
40 | }
41 |
42 | class PresenterCoroutineScope(context: CoroutineContext) : CoroutineScope {
43 | override val coroutineContext: CoroutineContext = context + Job()
44 | }
45 |
46 | interface ContactsProvider {
47 | fun onContactsLoaded(contacts: List)
48 | fun onFailedToLoadContacts(message: String?)
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/async/Either.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.async
2 |
3 | /**
4 | * An algebraic data type to provide either a [Failure][F] or a [Success][S] result.
5 | */
6 | sealed class Either {
7 |
8 | /**
9 | * Calls [failed] with the [failure][Failure.failure] value if result is a [Failure]
10 | * or [succeeded] with the [success][Success.success] value if result is a [Success]
11 | */
12 | inline fun fold(failed: (F) -> T, succeeded: (S) -> T): T =
13 | when (this) {
14 | is Failure -> failed(failure)
15 | is Success -> succeeded(success)
16 | }
17 | }
18 |
19 | data class Failure(val failure: F) : Either()
20 |
21 | data class Success(val success: S) : Either()
22 |
23 | /**
24 | * Allows chaining of multiple calls taking as argument the [success][Success.success] value of the previous call and
25 | * returning an [Either].
26 | *
27 | * 1. Unwrap the result of the first call from the [Either] wrapper.
28 | * 2. Check if it is a [Success].
29 | * 3. If yes, call the next function (passed as [ifSucceeded]) with the value of the [success][Success.success]
30 | * property as an input parameter (chain the calls).
31 | * 4. If no, just pass the [Failure] through as the end result of the whole call chain.
32 | *
33 | * In case any of the calls in the chain returns a [Failure], none of the subsequent flatmapped functions is called
34 | * and the whole chain returns this failure.
35 | *
36 | * @param ifSucceeded next function which should be called if this is a [Success]. The [success][Success.success]
37 | * value will be then passed as the input parameter.
38 | */
39 | inline fun Either.flatMap(succeeded: (S1) -> Either): Either =
40 | fold({ this as Failure }, succeeded)
41 |
42 | /**
43 | * Map the [Success] value of the [Either] to another value.
44 | *
45 | * You can for example map an `Success` to an `Success` by
46 | * using the following code:
47 | * ```
48 | * val fiveString: Either = Success("5")
49 | * val fiveInt : Either = fiveString.map { it.toInt() }
50 | * ```
51 | */
52 | inline fun Either.map(f: (S1) -> S2): Either =
53 | flatMap { Success(f(it)) }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/async/GetBlacklist.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.async
2 |
3 | import com.atiurin.espressoguide.data.entities.Contact
4 | import com.atiurin.espressoguide.data.repositories.ContactsRepositoty
5 | import kotlinx.coroutines.delay
6 |
7 | class GetBlacklist: UseCase, UseCase.None>() {
8 | override suspend fun run(params: None): Either> {
9 | return try {
10 | delay(200)
11 | val contacts = ContactsRepositoty.getBlacklist()
12 | Success(contacts)
13 | } catch (e: Exception) {
14 | Failure(e)
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/async/GetContacts.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.async
2 |
3 | import com.atiurin.espressoguide.data.entities.Contact
4 | import com.atiurin.espressoguide.data.repositories.ContactsRepositoty
5 | import kotlinx.coroutines.delay
6 |
7 | class GetContacts : UseCase, UseCase.None>() {
8 | override suspend fun run(params: None): Either> {
9 | return try {
10 | delay(200)
11 | val contacts = ContactsRepositoty.getWhiteList()
12 | Success(contacts)
13 | } catch (e: Exception) {
14 | Failure(e)
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/async/UseCase.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.async
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.coroutineScope
5 | import kotlinx.coroutines.launch
6 |
7 |
8 | /**
9 | * Base class for a `coroutine` use case.
10 | */
11 | abstract class UseCase where Type : Any {
12 |
13 | /**
14 | * Runs the actual logic of the use case.
15 | */
16 | abstract suspend fun run(params: Params): Either
17 |
18 | suspend operator fun invoke(params: Params, onSuccess: (Type) -> Unit, onFailure: (Exception) -> Unit) {
19 | val result = run(params)
20 | coroutineScope {
21 | launch(Dispatchers.Main) {
22 | result.fold(
23 | failed = { onFailure(it) },
24 | succeeded = { onSuccess(it) }
25 | )
26 | }
27 | }
28 | }
29 |
30 | /**
31 | * Placeholder for a use case that doesn't need any input parameters.
32 | */
33 | object None
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/core/settings/SettingsItem.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.core.settings
2 |
3 | data class SettingsItem(val type: SettingsType, val name: String)
4 |
5 | enum class SettingsType{
6 | ACCOUNT,
7 | PRIVACY,
8 | NOTIFICATIONS,
9 | BLACKLIST
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/data/Tags.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.data
2 |
3 | enum class Tags{
4 | CONTACTS_LIST,
5 | MESSAGES_LIST
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/data/entities/Contact.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.data.entities
2 |
3 | data class Contact( val id: Int,val name: String, val status: String, val avatar: Int)
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/data/entities/Message.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.data.entities
2 |
3 | data class Message(val authorId: Int,
4 | val receiverId: Int,
5 | val text: String)
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/data/entities/User.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.data.entities
2 |
3 | data class User( val id: Int,val name: String, val avatar: Int, val login: String, val password: String)
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/data/loaders/MessageLoader.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.data.loaders
2 |
3 | import com.atiurin.espressoguide.data.entities.Message
4 | import com.atiurin.espressoguide.data.repositories.MESSAGES
5 |
6 | open class MessageLoader{
7 | open fun load() : HashMap>{
8 | return MESSAGES
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/data/repositories/ContactsRepositoty.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.data.repositories
2 |
3 | import android.os.Handler
4 | import com.atiurin.espressoguide.adapters.ContactAdapter
5 | import com.atiurin.espressoguide.data.entities.Contact
6 |
7 | object ContactsRepositoty {
8 | private val contacts = CONTACTS
9 | private val blacklist = mutableListOf()
10 | fun getContact(id: Int): Contact {
11 | return contacts.find { it.id == id }!!
12 | }
13 |
14 | fun getContact(name: String): Contact {
15 | return contacts.find { it.name == name }!!
16 | }
17 |
18 | fun getAllContacts(): ArrayList {
19 | return contacts
20 | }
21 |
22 | fun getWhiteList(): List{
23 | return contacts - blacklist
24 | }
25 |
26 | fun addToBlacklist(contactId: Int) {
27 | blacklist.add(getContact(contactId))
28 | }
29 |
30 | fun deleteFromBlacklist(contactId: Int) {
31 | blacklist.remove(getContact(contactId))
32 | }
33 |
34 | fun getBlacklist(): MutableList {
35 | return blacklist
36 | }
37 |
38 | fun clearBlacklist(){
39 | blacklist.clear()
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/data/repositories/MessageRepository.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.data.repositories
2 |
3 | import com.atiurin.espressoguide.data.entities.Message
4 | import com.atiurin.espressoguide.data.loaders.MessageLoader
5 |
6 |
7 | object MessageRepository {
8 | var messages : HashMap>
9 |
10 | init {
11 | messages = loadMessages(MessageLoader())
12 | }
13 |
14 | fun loadMessages(loader: MessageLoader)
15 | : HashMap>{
16 | messages = loader.load()
17 | return messages
18 | }
19 |
20 | fun searchMessage(contactId: Int, author: Int, recipient: Int, text: String) : Message?{
21 | return messages[contactId]?.find { it.authorId == author && it.receiverId == recipient && it.text == text }
22 | }
23 |
24 | fun getChatMessages(contactId: Int): MutableList{
25 | val chatMessages = messages[contactId]
26 | if (chatMessages== null){
27 | messages[contactId] = mutableListOf()
28 | }
29 | return messages[contactId] as MutableList
30 | }
31 |
32 | fun clearChatMessages(contactId: Int){
33 | getChatMessages(contactId).clear()
34 | }
35 |
36 | fun addChatMessage(contactId: Int, message: Message){
37 | getChatMessages(contactId).add(message)
38 | }
39 |
40 | fun getChatMessagesCount(contactId: Int) : Int{
41 | return getChatMessages(contactId).size
42 | }
43 |
44 |
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/data/repositories/Storage.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.data.repositories
2 |
3 | import com.atiurin.espressoguide.R
4 | import com.atiurin.espressoguide.data.entities.Contact
5 | import com.atiurin.espressoguide.data.entities.Message
6 | import com.atiurin.espressoguide.data.entities.User
7 |
8 | val CURRENT_USER = User(1, "Joey Tribbiani", Avatars.JOEY.drawable, "joey", "1234")
9 |
10 | val CONTACTS = arrayListOf(
11 | Contact(2, "Chandler Bing", "Joey doesn't share food!", Avatars.CHANDLER.drawable),
12 | Contact(3, "Ross Geller", "UNAGI", Avatars.ROSS.drawable),
13 | Contact(4, "Rachel Green", "I got off the plane!", Avatars.RACHEL.drawable),
14 | Contact(5, "Phoebe Buffay", "Smelly cat, smelly cat..", Avatars.PHOEBE.drawable),
15 | Contact(6, "Monica Geller", "I need to clean up", Avatars.MONICA.drawable),
16 | Contact(7, "Gunther", "They were on break :(", Avatars.GUNTHER.drawable),
17 | Contact(8, "Janice", "Oh. My. God", Avatars.JANICE.drawable),
18 | Contact(9, "Bob", "I wanna drink", Avatars.DEFAULT.drawable),
19 | Contact(10, "Marty McFly", "Back to the ...", Avatars.DEFAULT.drawable),
20 | Contact(12, "Emmet Brown", "Time fluid capacitor", Avatars.DEFAULT.drawable),
21 | Contact(13, "Friend_1", "I'm a friend", Avatars.DEFAULT.drawable),
22 | Contact(14, "Friend_2", "I'm a friend", Avatars.DEFAULT.drawable),
23 | Contact(15, "Friend_3", "I'm a friend", Avatars.DEFAULT.drawable),
24 |
25 | Contact(16, "Friend_4", "I'm a friend", Avatars.DEFAULT.drawable),
26 | Contact(17, "Friend_5", "I'm a friend", Avatars.DEFAULT.drawable)
27 | )
28 |
29 | enum class Avatars(val drawable: Int) {
30 | CHANDLER(R.drawable.chandler),
31 | ROSS(R.drawable.ross),
32 | MONICA(R.drawable.monica),
33 | RACHEL(R.drawable.rachel),
34 | PHOEBE(R.drawable.phoebe),
35 | GUNTHER(R.drawable.gunther),
36 | JOEY(R.drawable.joey),
37 | JANICE(R.drawable.janice),
38 | DEFAULT(R.drawable.default_avatar)
39 | }
40 |
41 |
42 | val MESSAGES = hashMapOf>(
43 | 2 to arrayListOf(
44 | Message(1, 2, "What's up Chandler"),
45 | Message(2, 1, "Hi Joey"),
46 | Message(1, 2, "Let's drink coffee"),
47 | Message(2, 1, "Ok")
48 | ),
49 | 4 to arrayListOf(
50 | Message(1, 3, "How u doing?"),
51 | Message(3, 1, "Better"),
52 | Message(1, 3, "Come with me"),
53 | ),
54 | 3 to arrayListOf(
55 | Message(1, 3, "Do u wanna coffee?"),
56 | Message(3, 1, "yep, let's go")
57 | ),
58 | 8 to arrayListOf(
59 | Message(8, 1, "We are neighbours!"),
60 | Message(1, 8, "oh my God")
61 | )
62 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/fragment/settings/BlacklistFragment.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.fragment.settings
2 |
3 | import android.app.Activity
4 | import android.os.Bundle
5 | import android.view.*
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.appcompat.widget.PopupMenu
8 | import androidx.appcompat.widget.Toolbar
9 | import androidx.fragment.app.Fragment
10 | import androidx.recyclerview.widget.LinearLayoutManager
11 | import androidx.recyclerview.widget.RecyclerView
12 | import com.atiurin.espressoguide.Logger
13 | import com.atiurin.espressoguide.R
14 | import com.atiurin.espressoguide.adapters.ContactAdapter
15 | import com.atiurin.espressoguide.async.ContactsPresenter
16 | import com.atiurin.espressoguide.async.ContactsProvider
17 | import com.atiurin.espressoguide.data.entities.Contact
18 | import com.atiurin.espressoguide.data.repositories.ContactsRepositoty
19 | import com.atiurin.espressoguide.idlingresources.idling
20 |
21 | class BlacklistFragment(
22 | val activity: Activity
23 | ) : Fragment(), ContactsProvider {
24 |
25 | private lateinit var recyclerView: RecyclerView
26 | private lateinit var viewAdapter: ContactAdapter
27 | private lateinit var viewManager: RecyclerView.LayoutManager
28 | private val contactsPresenter = ContactsPresenter(this)
29 |
30 | override fun onCreateView(
31 | inflater: LayoutInflater,
32 | container: ViewGroup?,
33 | savedInstanceState: Bundle?
34 | ): View? {
35 | val toolbar: Toolbar = activity.findViewById(R.id.toolbar)
36 | (activity as AppCompatActivity).setSupportActionBar(toolbar)
37 | toolbar.title = "Blacklist"
38 |
39 | FragmentInfoContainer.currentFragment = this.javaClass.name
40 | val rootView = inflater.inflate(R.layout.fragment_blacklist, container, false)
41 | viewManager = LinearLayoutManager(activity)
42 | viewAdapter = ContactAdapter(
43 | emptyList(),
44 | object : ContactAdapter.OnItemClickListener {
45 | override fun onItemClick(contact: Contact) {
46 | Logger.debug("Click on item ${contact.name}")
47 |
48 | }
49 |
50 | override fun onItemLongClick(contact: Contact) {
51 | Logger.debug("Long Click on item ${contact.name}")
52 |
53 | val pop = PopupMenu(activity, viewAdapter.getContactView(contact))
54 | pop.menu.add("Delete ${contact.name} from blacklist")
55 | pop.setOnMenuItemClickListener { item ->
56 | Logger.debug("MenuClicked on item ${contact.name}")
57 | ContactsRepositoty.deleteFromBlacklist(contact.id)
58 | contactsPresenter.getBlacklist()
59 | true
60 | }
61 | pop.show()
62 | true
63 | }
64 | })
65 | recyclerView = rootView.findViewById(R.id.recycler_blacklist).apply {
66 | layoutManager = viewManager
67 | adapter = viewAdapter
68 | }
69 | registerForContextMenu(recyclerView)
70 | contactsPresenter.getBlacklist()
71 | return rootView
72 | }
73 |
74 | override fun onContactsLoaded(contacts: List) {
75 | viewAdapter.updateData(contacts)
76 | viewAdapter.notifyDataSetChanged()
77 | idling { contactsIdling.onDataLoaded()}
78 | }
79 |
80 | override fun onFailedToLoadContacts(message: String?) {
81 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
82 | }
83 |
84 | override fun onContextItemSelected(item: MenuItem): Boolean {
85 | return super.onContextItemSelected(item)
86 |
87 | }
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/fragment/settings/FragmentInfoContainer.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.fragment.settings
2 |
3 | object FragmentInfoContainer {
4 | var currentFragment: String? = null
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/fragment/settings/SettingsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.fragment.settings
2 |
3 | import android.app.Activity
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.appcompat.widget.Toolbar
10 | import androidx.fragment.app.Fragment
11 | import androidx.recyclerview.widget.LinearLayoutManager
12 | import androidx.recyclerview.widget.RecyclerView
13 | import com.atiurin.espressoguide.Logger
14 | import com.atiurin.espressoguide.R
15 | import com.atiurin.espressoguide.adapters.SettingsAdapter
16 | import com.atiurin.espressoguide.core.settings.SettingsItem
17 | import com.atiurin.espressoguide.core.settings.SettingsType
18 | import com.google.android.material.snackbar.Snackbar
19 |
20 | class SettingsFragment(
21 | val activity: Activity
22 | ) : Fragment() {
23 | private lateinit var recyclerView: RecyclerView
24 | private lateinit var viewAdapter: SettingsAdapter
25 | private lateinit var viewManager: RecyclerView.LayoutManager
26 | override fun onCreateView(
27 | inflater: LayoutInflater,
28 | container: ViewGroup?,
29 | savedInstanceState: Bundle?
30 | ): View? {
31 | FragmentInfoContainer.currentFragment = this.javaClass.name
32 | val rootView = inflater.inflate(R.layout.fragment_settings_list, container, false)
33 | val toolbar: Toolbar = activity.findViewById(R.id.toolbar)
34 | (activity as AppCompatActivity).setSupportActionBar(toolbar)
35 | toolbar.title = "Settings"
36 | viewManager = LinearLayoutManager(activity)
37 | viewAdapter = SettingsAdapter(
38 | listOf(
39 | SettingsItem(SettingsType.ACCOUNT, "Account"),
40 | SettingsItem(SettingsType.PRIVACY, "Privacy"),
41 | SettingsItem(SettingsType.NOTIFICATIONS, "Notifications"),
42 | SettingsItem(SettingsType.BLACKLIST, "Blacklist")
43 | ),
44 | object : SettingsAdapter.OnItemClickListener {
45 | override fun onItemClick(item: SettingsItem) {
46 | Logger.debug("Click on item ${item.name}")
47 | when (item.type) {
48 | SettingsType.BLACKLIST -> SettingsFragmentNavigator.go(BlacklistFragment::class.java)
49 | else -> {
50 | Snackbar.make(recyclerView, "This option is not implemented", Snackbar.LENGTH_LONG)
51 | .setAction("Action", null).show()
52 | }
53 | }
54 | }
55 | })
56 | recyclerView = rootView.findViewById(R.id.settings_list).apply {
57 | layoutManager = viewManager
58 | adapter = viewAdapter
59 | }
60 | return rootView
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/fragment/settings/SettingsFragmentNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.fragment.settings
2 |
3 |
4 | import android.annotation.SuppressLint
5 | import android.app.Activity
6 | import android.content.Intent
7 | import androidx.fragment.app.Fragment
8 | import com.atiurin.espressoguide.activity.SettingsActivity
9 |
10 | @SuppressLint("StaticFieldLeak")
11 | object SettingsFragmentNavigator {
12 | lateinit var activity: Activity
13 |
14 | fun init(activity: Activity) {
15 | this.activity = activity
16 | }
17 |
18 | fun go(fragmentClass: Class<*>) {
19 | val intent = Intent(activity, SettingsActivity::class.java)
20 | intent.putExtra(SettingsActivity.INTENT_FRAGMENT_CLASS, getKey(fragmentClass))
21 | activity.startActivity(intent)
22 | }
23 |
24 | fun getKey(fragmentClass: Class<*>): String {
25 | return when (fragmentClass) {
26 | BlacklistFragment::class.java -> FragmentType.BLACKLIST.name
27 | else -> FragmentType.SETTINGS_LIST.name
28 | }
29 | }
30 |
31 | fun getFragment(key: String): Fragment {
32 | return when(FragmentType.valueOf(key)){
33 | FragmentType.BLACKLIST -> BlacklistFragment(activity)
34 | else -> SettingsFragment(activity)
35 | }
36 | }
37 |
38 | enum class FragmentType{
39 | BLACKLIST, SETTINGS_LIST
40 | }
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/idlingresources/AbstractIdlingResource.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.idlingresources
2 |
3 | import androidx.annotation.Nullable
4 | import java.util.concurrent.atomic.AtomicBoolean
5 |
6 | abstract class AbstractIdlingResource : AppIdlingResource {
7 | @Nullable
8 | @Volatile
9 | private var resourceCallback: AppIdlingResource.AppResourceCallback? = null
10 | private val idleNow = AtomicBoolean(true)
11 | override fun getName(): String {
12 | return this.javaClass.name
13 | }
14 |
15 | override fun isIdle(): Boolean {
16 | return idleNow.get()
17 | }
18 |
19 | override fun registerIdleTransitionCallback(resourceCallback: AppIdlingResource.AppResourceCallback) {
20 | this.resourceCallback = resourceCallback
21 | }
22 |
23 | private fun setIdleState(isIdleNow: Boolean) {
24 | idleNow.set(isIdleNow)
25 | if (isIdleNow && resourceCallback != null) {
26 | resourceCallback?.onTransitionToIdleState()
27 | }
28 | }
29 |
30 | fun onLoadStarted() = setIdleState(false)
31 | fun onDataLoaded() = setIdleState(true)
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/idlingresources/AppIdlingResource.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.idlingresources
2 |
3 |
4 | /*
5 | * This interface is a representation of Espresso.IdlingResource interface.
6 | * It is created to avoid espresso_idling dependency in app
7 | */
8 | interface AppIdlingResource {
9 | fun getName(): String
10 | fun isIdle(): Boolean
11 | fun registerIdleTransitionCallback(resourceCallback: AppResourceCallback)
12 |
13 | interface AppResourceCallback{
14 | fun onTransitionToIdleState()
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/idlingresources/IdlingResourses.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.idlingresources
2 |
3 | class ContactsIdling : AbstractIdlingResource()
4 | class MessagesIdling : AbstractIdlingResource()
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/idlingresources/IdlingScope.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.idlingresources
2 |
3 | import java.util.concurrent.atomic.AtomicReference
4 |
5 | fun isReleaseBuild(): Boolean {
6 | return false
7 | }
8 |
9 | interface IdlingScope {
10 | val contactsIdling: ContactsIdling
11 | val messagesIdling: MessagesIdling
12 | }
13 |
14 | val idlingContainer = AtomicReference()
15 |
16 | fun idling(action: IdlingScope.() -> Unit){
17 | if (!isReleaseBuild()){
18 | idlingContainer.get()?.action()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/managers/AccountManager.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.managers
2 |
3 | import android.content.Context
4 |
5 | class AccountManager(val context: Context){
6 | companion object {
7 | private const val expectedUserName = "joey"
8 | private const val expectedPassword = "1234"
9 | private const val USER_KEY = "username"
10 | private const val PASSWORD_KEY = "password"
11 | }
12 |
13 | fun login(user: String, password: String) : Boolean{
14 | var success = false
15 | // there should be some network request to app server
16 | if ((user == expectedUserName) &&(password == expectedPassword)){
17 | success = true
18 | with(PrefsManager(context)){
19 | savePref(USER_KEY, user)
20 | savePref(PASSWORD_KEY, password)
21 | }
22 | }
23 | return success
24 | }
25 |
26 | fun isLoggedIn() : Boolean{
27 | var userName = ""
28 | var password = ""
29 | with(PrefsManager(context)){
30 | userName = getPref(USER_KEY)
31 | password = getPref(PASSWORD_KEY)
32 | }
33 | if (userName.isEmpty() || password.isEmpty()) return false
34 | return true
35 | }
36 |
37 | fun logout(){
38 | with(PrefsManager(context)){
39 | remove(USER_KEY)
40 | remove(PASSWORD_KEY)
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/managers/PrefsManager.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.managers
2 |
3 | import android.content.Context.MODE_PRIVATE
4 | import android.R.id.edit
5 | import android.app.Application
6 | import android.content.Context
7 | import android.content.SharedPreferences
8 | import android.content.Context.MODE_PRIVATE
9 |
10 |
11 |
12 | class PrefsManager(val context: Context){
13 | val PREFS_NAME = "MyPrefsFileName"
14 |
15 | fun savePref(key: String, value: String){
16 | val editor = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
17 | editor.putString(key, value)
18 | editor.apply()
19 | }
20 |
21 | fun getPref(key: String) : String{
22 | val prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
23 | var value = prefs.getString(key, null)
24 | if (value == null) value = ""
25 | return value
26 | }
27 |
28 | fun remove(key: String){
29 | val editor = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
30 | editor.remove(key)
31 | editor.commit()
32 | }
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/atiurin/espressoguide/view/CircleImageView.java:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide.view;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.content.res.TypedArray;
6 | import android.graphics.Bitmap;
7 | import android.graphics.BitmapShader;
8 | import android.graphics.Canvas;
9 | import android.graphics.Color;
10 | import android.graphics.ColorFilter;
11 | import android.graphics.Matrix;
12 | import android.graphics.Outline;
13 | import android.graphics.Paint;
14 | import android.graphics.Rect;
15 | import android.graphics.RectF;
16 | import android.graphics.Shader;
17 | import android.graphics.drawable.BitmapDrawable;
18 | import android.graphics.drawable.ColorDrawable;
19 | import android.graphics.drawable.Drawable;
20 | import android.net.Uri;
21 | import android.os.Build;
22 | import android.util.AttributeSet;
23 | import android.view.MotionEvent;
24 | import android.view.View;
25 | import android.view.ViewOutlineProvider;
26 | import android.widget.ImageView;
27 | import androidx.annotation.ColorInt;
28 | import androidx.annotation.ColorRes;
29 | import androidx.annotation.DrawableRes;
30 | import androidx.annotation.RequiresApi;
31 | import androidx.appcompat.widget.AppCompatImageView;
32 | import com.atiurin.espressoguide.R;
33 |
34 | @SuppressWarnings("UnusedDeclaration")
35 | public class CircleImageView extends AppCompatImageView {
36 |
37 | private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;
38 |
39 | private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
40 | private static final int COLORDRAWABLE_DIMENSION = 2;
41 |
42 | private static final int DEFAULT_BORDER_WIDTH = 0;
43 | private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
44 | private static final int DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT;
45 | private static final boolean DEFAULT_BORDER_OVERLAY = false;
46 |
47 | private final RectF mDrawableRect = new RectF();
48 | private final RectF mBorderRect = new RectF();
49 |
50 | private final Matrix mShaderMatrix = new Matrix();
51 | private final Paint mBitmapPaint = new Paint();
52 | private final Paint mBorderPaint = new Paint();
53 | private final Paint mCircleBackgroundPaint = new Paint();
54 |
55 | private int mBorderColor = DEFAULT_BORDER_COLOR;
56 | private int mBorderWidth = DEFAULT_BORDER_WIDTH;
57 | private int mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR;
58 |
59 | private Bitmap mBitmap;
60 | private BitmapShader mBitmapShader;
61 | private int mBitmapWidth;
62 | private int mBitmapHeight;
63 |
64 | private float mDrawableRadius;
65 | private float mBorderRadius;
66 |
67 | private ColorFilter mColorFilter;
68 |
69 | private boolean mReady;
70 | private boolean mSetupPending;
71 | private boolean mBorderOverlay;
72 | private boolean mDisableCircularTransformation;
73 |
74 | public CircleImageView(Context context) {
75 | super(context);
76 |
77 | init();
78 | }
79 |
80 | public CircleImageView(Context context, AttributeSet attrs) {
81 | this(context, attrs, 0);
82 | }
83 |
84 | public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
85 | super(context, attrs, defStyle);
86 |
87 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);
88 |
89 | mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH);
90 | mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR);
91 | mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY);
92 | mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_circle_background_color, DEFAULT_CIRCLE_BACKGROUND_COLOR);
93 |
94 | a.recycle();
95 |
96 | init();
97 | }
98 |
99 | private void init() {
100 | super.setScaleType(SCALE_TYPE);
101 | mReady = true;
102 |
103 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
104 | setOutlineProvider(new OutlineProvider());
105 | }
106 |
107 | if (mSetupPending) {
108 | setup();
109 | mSetupPending = false;
110 | }
111 | }
112 |
113 | @Override
114 | public ScaleType getScaleType() {
115 | return SCALE_TYPE;
116 | }
117 |
118 | @Override
119 | public void setScaleType(ScaleType scaleType) {
120 | if (scaleType != SCALE_TYPE) {
121 | throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
122 | }
123 | }
124 |
125 | @Override
126 | public void setAdjustViewBounds(boolean adjustViewBounds) {
127 | if (adjustViewBounds) {
128 | throw new IllegalArgumentException("adjustViewBounds not supported.");
129 | }
130 | }
131 |
132 | @Override
133 | protected void onDraw(Canvas canvas) {
134 | if (mDisableCircularTransformation) {
135 | super.onDraw(canvas);
136 | return;
137 | }
138 |
139 | if (mBitmap == null) {
140 | return;
141 | }
142 |
143 | if (mCircleBackgroundColor != Color.TRANSPARENT) {
144 | canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint);
145 | }
146 | canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
147 | if (mBorderWidth > 0) {
148 | canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
149 | }
150 | }
151 |
152 | @Override
153 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
154 | super.onSizeChanged(w, h, oldw, oldh);
155 | setup();
156 | }
157 |
158 | @Override
159 | public void setPadding(int left, int top, int right, int bottom) {
160 | super.setPadding(left, top, right, bottom);
161 | setup();
162 | }
163 |
164 | @Override
165 | public void setPaddingRelative(int start, int top, int end, int bottom) {
166 | super.setPaddingRelative(start, top, end, bottom);
167 | setup();
168 | }
169 |
170 | public int getBorderColor() {
171 | return mBorderColor;
172 | }
173 |
174 | public void setBorderColor(@ColorInt int borderColor) {
175 | if (borderColor == mBorderColor) {
176 | return;
177 | }
178 |
179 | mBorderColor = borderColor;
180 | mBorderPaint.setColor(mBorderColor);
181 | invalidate();
182 | }
183 |
184 | public int getCircleBackgroundColor() {
185 | return mCircleBackgroundColor;
186 | }
187 |
188 | public void setCircleBackgroundColor(@ColorInt int circleBackgroundColor) {
189 | if (circleBackgroundColor == mCircleBackgroundColor) {
190 | return;
191 | }
192 |
193 | mCircleBackgroundColor = circleBackgroundColor;
194 | mCircleBackgroundPaint.setColor(circleBackgroundColor);
195 | invalidate();
196 | }
197 |
198 | public void setCircleBackgroundColorResource(@ColorRes int circleBackgroundRes) {
199 | setCircleBackgroundColor(getContext().getResources().getColor(circleBackgroundRes));
200 | }
201 |
202 | public int getBorderWidth() {
203 | return mBorderWidth;
204 | }
205 |
206 | public void setBorderWidth(int borderWidth) {
207 | if (borderWidth == mBorderWidth) {
208 | return;
209 | }
210 |
211 | mBorderWidth = borderWidth;
212 | setup();
213 | }
214 |
215 | public boolean isBorderOverlay() {
216 | return mBorderOverlay;
217 | }
218 |
219 | public void setBorderOverlay(boolean borderOverlay) {
220 | if (borderOverlay == mBorderOverlay) {
221 | return;
222 | }
223 |
224 | mBorderOverlay = borderOverlay;
225 | setup();
226 | }
227 |
228 | public boolean isDisableCircularTransformation() {
229 | return mDisableCircularTransformation;
230 | }
231 |
232 | public void setDisableCircularTransformation(boolean disableCircularTransformation) {
233 | if (mDisableCircularTransformation == disableCircularTransformation) {
234 | return;
235 | }
236 |
237 | mDisableCircularTransformation = disableCircularTransformation;
238 | initializeBitmap();
239 | }
240 |
241 | @Override
242 | public void setImageBitmap(Bitmap bm) {
243 | super.setImageBitmap(bm);
244 | initializeBitmap();
245 | }
246 |
247 | @Override
248 | public void setImageDrawable(Drawable drawable) {
249 | super.setImageDrawable(drawable);
250 | initializeBitmap();
251 | }
252 |
253 | @Override
254 | public void setImageResource(@DrawableRes int resId) {
255 | super.setImageResource(resId);
256 | initializeBitmap();
257 | }
258 |
259 | @Override
260 | public void setImageURI(Uri uri) {
261 | super.setImageURI(uri);
262 | initializeBitmap();
263 | }
264 |
265 | @Override
266 | public void setColorFilter(ColorFilter cf) {
267 | if (cf == mColorFilter) {
268 | return;
269 | }
270 |
271 | mColorFilter = cf;
272 | applyColorFilter();
273 | invalidate();
274 | }
275 |
276 | @Override
277 | public ColorFilter getColorFilter() {
278 | return mColorFilter;
279 | }
280 |
281 | private void applyColorFilter() {
282 | mBitmapPaint.setColorFilter(mColorFilter);
283 | }
284 |
285 | private Bitmap getBitmapFromDrawable(Drawable drawable) {
286 | if (drawable == null) {
287 | return null;
288 | }
289 |
290 | if (drawable instanceof BitmapDrawable) {
291 | return ((BitmapDrawable) drawable).getBitmap();
292 | }
293 |
294 | try {
295 | Bitmap bitmap;
296 |
297 | if (drawable instanceof ColorDrawable) {
298 | bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
299 | } else {
300 | bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
301 | }
302 |
303 | Canvas canvas = new Canvas(bitmap);
304 | drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
305 | drawable.draw(canvas);
306 | return bitmap;
307 | } catch (Exception e) {
308 | e.printStackTrace();
309 | return null;
310 | }
311 | }
312 |
313 | private void initializeBitmap() {
314 | if (mDisableCircularTransformation) {
315 | mBitmap = null;
316 | } else {
317 | mBitmap = getBitmapFromDrawable(getDrawable());
318 | }
319 | setup();
320 | }
321 |
322 | private void setup() {
323 | if (!mReady) {
324 | mSetupPending = true;
325 | return;
326 | }
327 |
328 | if (getWidth() == 0 && getHeight() == 0) {
329 | return;
330 | }
331 |
332 | if (mBitmap == null) {
333 | invalidate();
334 | return;
335 | }
336 |
337 | mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
338 |
339 | mBitmapPaint.setAntiAlias(true);
340 | mBitmapPaint.setShader(mBitmapShader);
341 |
342 | mBorderPaint.setStyle(Paint.Style.STROKE);
343 | mBorderPaint.setAntiAlias(true);
344 | mBorderPaint.setColor(mBorderColor);
345 | mBorderPaint.setStrokeWidth(mBorderWidth);
346 |
347 | mCircleBackgroundPaint.setStyle(Paint.Style.FILL);
348 | mCircleBackgroundPaint.setAntiAlias(true);
349 | mCircleBackgroundPaint.setColor(mCircleBackgroundColor);
350 |
351 | mBitmapHeight = mBitmap.getHeight();
352 | mBitmapWidth = mBitmap.getWidth();
353 |
354 | mBorderRect.set(calculateBounds());
355 | mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);
356 |
357 | mDrawableRect.set(mBorderRect);
358 | if (!mBorderOverlay && mBorderWidth > 0) {
359 | mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f);
360 | }
361 | mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
362 |
363 | applyColorFilter();
364 | updateShaderMatrix();
365 | invalidate();
366 | }
367 |
368 | private RectF calculateBounds() {
369 | int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
370 | int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();
371 |
372 | int sideLength = Math.min(availableWidth, availableHeight);
373 |
374 | float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
375 | float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
376 |
377 | return new RectF(left, top, left + sideLength, top + sideLength);
378 | }
379 |
380 | private void updateShaderMatrix() {
381 | float scale;
382 | float dx = 0;
383 | float dy = 0;
384 |
385 | mShaderMatrix.set(null);
386 |
387 | if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
388 | scale = mDrawableRect.height() / (float) mBitmapHeight;
389 | dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
390 | } else {
391 | scale = mDrawableRect.width() / (float) mBitmapWidth;
392 | dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
393 | }
394 |
395 | mShaderMatrix.setScale(scale, scale);
396 | mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
397 |
398 | mBitmapShader.setLocalMatrix(mShaderMatrix);
399 | }
400 |
401 | @SuppressLint("ClickableViewAccessibility")
402 | @Override
403 | public boolean onTouchEvent(MotionEvent event) {
404 | return inTouchableArea(event.getX(), event.getY()) && super.onTouchEvent(event);
405 | }
406 |
407 | private boolean inTouchableArea(float x, float y) {
408 | return Math.pow(x - mBorderRect.centerX(), 2) + Math.pow(y - mBorderRect.centerY(), 2) <= Math.pow(mBorderRadius, 2);
409 | }
410 |
411 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
412 | private class OutlineProvider extends ViewOutlineProvider {
413 |
414 | @Override
415 | public void getOutline(View view, Outline outline) {
416 | Rect bounds = new Rect();
417 | mBorderRect.roundOut(bounds);
418 | outline.setRoundRect(bounds, bounds.width() / 2.0f);
419 | }
420 |
421 | }
422 |
423 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_account.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_attach_file.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_exit.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_messages.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_send.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_account.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-hdpi/ic_account.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_attach_file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-hdpi/ic_attach_file.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-hdpi/ic_exit.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_messages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-hdpi/ic_messages.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_send.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-hdpi/ic_send.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_account.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-mdpi/ic_account.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_attach_file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-mdpi/ic_attach_file.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-mdpi/ic_exit.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_messages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-mdpi/ic_messages.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_send.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-mdpi/ic_send.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/chandler.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-v24/chandler.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/joey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-v24/joey.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ross.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-v24/ross.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_account.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-xhdpi/ic_account.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_attach_file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-xhdpi/ic_attach_file.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-xhdpi/ic_exit.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_messages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-xhdpi/ic_messages.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_send.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-xhdpi/ic_send.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_account.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-xxhdpi/ic_account.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_attach_file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-xxhdpi/ic_attach_file.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-xxhdpi/ic_exit.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_messages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-xxhdpi/ic_messages.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_send.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable-xxhdpi/ic_send.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/circle.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/default_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable/default_avatar.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/gunther.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable/gunther.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_menu_camera.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_menu_gallery.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_menu_manage.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_menu_send.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_menu_share.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_menu_slideshow.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/img.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/janice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable/janice.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/monica.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable/monica.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/phoebe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable/phoebe.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rachel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/drawable/rachel.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/side_nav_bar.xml:
--------------------------------------------------------------------------------
1 |
3 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_chat.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
22 |
30 |
39 |
40 |
41 |
42 |
50 |
51 |
52 |
60 |
61 |
63 |
64 |
69 |
70 |
80 |
96 |
97 |
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_login.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
26 |
27 |
35 |
43 |
44 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
17 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_profile.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
19 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
11 |
12 |
18 |
27 |
28 |
29 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/app_bar_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_blacklist.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_settings_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/friends_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
15 |
19 |
27 |
28 |
35 |
43 |
44 |
52 |
53 |
54 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/message_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
19 |
20 |
24 |
25 |
30 |
31 |
40 |
41 |
47 |
48 |
49 |
56 |
57 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/my_text_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/nav_header_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
24 |
25 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/settings_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/activity_main_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/blacklist_menu.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | EspressoGuide
4 | Открыть меню
5 | Закрыть меню
6 | Настройки
7 | Выход
8 | Очистить историю
9 | Добавить в черный список
10 | Настройки
11 | Сохраненные сообщения
12 | Мой профиль
13 | Выход
14 | Д.Р.У.З.Ь.Я
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #2A4B9F
6 | #58E1CA
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 8dp
6 | 176dp
7 | 16dp
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | EspressoGuide
3 | Open navigation drawer
4 | Close navigation drawer
5 | Android Studio
6 | android.studio@android.com
7 | Navigation header
8 | Settings
9 | Logout
10 | Clear history
11 | Add to blacklist
12 |
13 | Settings
14 | Saved messages
15 | My Profile
16 | Logout
17 |
18 | F.R.I.E.N.D.S
19 | User: \'joey\', Password: \'1234\'
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
14 |
15 |
16 |
19 |
20 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/test/java/com/atiurin/espressoguide/MockitoTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide
2 |
3 | import com.atiurin.espressoguide.data.entities.Message
4 | import com.atiurin.espressoguide.data.loaders.MessageLoader
5 | import com.atiurin.espressoguide.data.repositories.MessageRepository
6 | import org.junit.Assert
7 | import org.junit.Test
8 | import org.mockito.Mockito
9 |
10 | class MockitoTest{
11 | @Test
12 | fun testWithMockLoader(){
13 | val messages = HashMap>()
14 | messages[2] = listOf(Message(1, 2, "new message"))
15 | val mockLoader = Mockito.mock(MessageLoader::class.java)
16 | Mockito.`when`(mockLoader.load()).thenReturn(messages)
17 | MessageRepository.loadMessages(mockLoader)
18 | val actualCount = MessageRepository.getChatMessagesCount(2)
19 | Assert.assertEquals("Expected messages count is 1 " +
20 | "but actual count is $actualCount",
21 | 1, actualCount)
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/atiurin/espressoguide/RobolectricTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide
2 |
3 | import android.content.Context
4 | import androidx.test.core.app.ApplicationProvider
5 | import androidx.test.platform.app.InstrumentationRegistry
6 | import com.atiurin.espressoguide.managers.AccountManager
7 | import org.junit.Test
8 | import org.junit.runner.RunWith
9 | import org.robolectric.RobolectricTestRunner
10 | import org.junit.Assert
11 | import org.robolectric.Robolectric
12 |
13 | @RunWith(RobolectricTestRunner::class)
14 | class RobolectricTest {
15 | @Test
16 | fun testLoginWithValidData() {
17 | val validPassword = "1234"
18 | val validUserName = "joey"
19 |
20 | val context = ApplicationProvider
21 | .getApplicationContext()
22 | val result = AccountManager(context)
23 | .login(validUserName, validPassword)
24 | Assert.assertTrue(result)
25 | }
26 |
27 | @Test
28 | fun testLoginWithInvalidPassword(){
29 | val invalidPassword = "777777"
30 | val validUserName = "joey"
31 | val result = AccountManager(context = ApplicationProvider.getApplicationContext())
32 | .login(user = validUserName,password = invalidPassword)
33 | Assert.assertFalse("Expected login result is false but get $result", result)
34 | }
35 | @Test
36 | fun testLoginWithInvalidUserName(){
37 | val invalidPassword = "1234"
38 | val validUserName = "chandler"
39 | val result = AccountManager(context = ApplicationProvider.getApplicationContext())
40 | .login(user = validUserName,password = invalidPassword)
41 | Assert.assertFalse("Expected login result is false but get $result", result)
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/atiurin/espressoguide/SimpleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide
2 |
3 | import com.atiurin.espressoguide.data.entities.Message
4 | import com.atiurin.espressoguide.data.repositories.MessageRepository
5 | import org.junit.Assert
6 | import org.junit.Before
7 | import org.junit.Ignore
8 | import org.junit.Test
9 |
10 | class SimpleUnitTest {
11 |
12 | @Before
13 | fun clearRepository(){
14 | MessageRepository.clearChatMessages(2)
15 | }
16 |
17 | @Ignore
18 | @Test
19 | fun testAddNewMessage() {
20 | val message = Message(1, 2, "new message")
21 | val initialCount = MessageRepository.getChatMessagesCount(2)
22 | MessageRepository.addChatMessage(2,message)
23 | val actualCount = MessageRepository.getChatMessagesCount(2)
24 | Assert.assertEquals(
25 | "Expected messages count is ${initialCount + 1} " +
26 | "but actual count is $actualCount",
27 | initialCount + 1, actualCount
28 | )
29 | }
30 |
31 | @Ignore
32 | @Test
33 | fun testSearchAddedMessage() {
34 | val message = Message(1, 3, "add message")
35 | MessageRepository.addChatMessage(2, message)
36 | val searchedMessage = MessageRepository.searchMessage(message.authorId, message.receiverId,2, message.text)
37 | Assert.assertEquals("Expected message aren't the same as found one", message, searchedMessage)
38 | }
39 |
40 | @Ignore
41 | @Test
42 | fun testSearchRightMessage() {
43 | val expectedMessage = Message(1, 3, "add message")
44 | val message2 = Message(2, 3, "another message")
45 | MessageRepository.addChatMessage(2, expectedMessage)
46 | MessageRepository.addChatMessage(2, message2)
47 | val searchedMessage = MessageRepository.searchMessage(
48 | expectedMessage.authorId,
49 | expectedMessage.receiverId,2,
50 | expectedMessage.text)
51 | Assert.assertEquals("Expected message aren't the same as found one", expectedMessage, searchedMessage)
52 | }
53 |
54 | @Ignore
55 | @Test
56 | fun testDemoAssertionFailed() {
57 | val expectedMessage = Message(1, 3, "add message")
58 | val message2 = Message(2, 3, "another message")
59 | MessageRepository.addChatMessage(2, expectedMessage)
60 | MessageRepository.addChatMessage(2, message2)
61 | val searchedMessage = MessageRepository.searchMessage(
62 | expectedMessage.authorId,
63 | expectedMessage.receiverId,2,
64 | expectedMessage.text)
65 | Assert.assertEquals("Expected message aren't the same as found one", expectedMessage, message2)
66 | }
67 |
68 | @Ignore
69 | @Test
70 | fun testClearAllMessages() {
71 | val message = Message(1, 2, "new message")
72 | MessageRepository.addChatMessage(2, message)
73 | MessageRepository.clearChatMessages(2)
74 | val actualCount = MessageRepository.getChatMessagesCount(2)
75 | Assert.assertEquals(
76 | "Expected messages count is 0 but actual count is $actualCount",
77 | 0, actualCount
78 | )
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/atiurin/espressoguide/UnitTestSuite.kt:
--------------------------------------------------------------------------------
1 | package com.atiurin.espressoguide
2 |
3 | import org.junit.runner.RunWith
4 | import org.junit.runners.Suite
5 |
6 | @Suite.SuiteClasses(
7 | SimpleUnitTest::class,
8 | MockitoTest::class,
9 | RobolectricTest::class
10 | )
11 | @RunWith(Suite::class)
12 | class UnitTestSuite
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.5.20'
5 | ext.coroutine_version = "1.2.1"
6 | repositories {
7 | google()
8 | mavenCentral()
9 | maven {
10 | url "https://plugins.gradle.org/m2/"
11 | }
12 |
13 | }
14 | dependencies {
15 | classpath 'com.android.tools.build:gradle:4.1.3'
16 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
17 | classpath "gradle.plugin.org.codehaus.groovy:groovy-android-gradle-plugin:3.0.0"
18 | classpath 'me.tatarka:gradle-retrolambda:3.6.0'
19 | classpath 'org.jacoco:org.jacoco.core:0.8.0'
20 | classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2'
21 | // NOTE: Do not place your application dependencies here; they belong
22 | // in the individual module build.gradle files
23 | }
24 | }
25 |
26 |
27 |
28 | allprojects {
29 | repositories {
30 | google()
31 | mavenCentral()
32 | }
33 | }
34 |
35 | task clean(type: Delete) {
36 | delete rootProject.buildDir
37 | }
38 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Feb 03 23:25:43 MSK 2021
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-6.5-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/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 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
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 Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/prepare-emulator.sh:
--------------------------------------------------------------------------------
1 | adb shell settings put global development_settings_enabled 1
2 | adb shell settings put global window_animation_scale 0.0
3 | adb shell settings put global transition_animation_scale 0.0
4 | adb shell settings put global animator_duration_scale 0.0
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alex-tiurin/espresso-guide/f340ef61af65cd78746ce5034ad8de8f8af42bdc/sonar-project.properties
--------------------------------------------------------------------------------