diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3eeae8af --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +.idea +/local.properties +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..2041e660 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,70 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 27 + defaultConfig { + applicationId "com.example.joao.photoscodechallenge" + minSdkVersion 21 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + androidExtensions { + experimental = true + } +} + +dependencies { + implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + implementation "com.android.support:design:27.1.1" + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:recyclerview-v7:27.1.1' + implementation 'com.android.support.constraint:constraint-layout:1.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + + implementation "org.mockito:mockito-core:2.12.0" + implementation "com.squareup.okhttp3:mockwebserver:3.8.0" + testImplementation "com.nhaarman:mockito-kotlin-kt1.1:1.5.0" + androidTestImplementation "org.mockito:mockito-android:2.9.0" + testImplementation "android.arch.core:core-testing:1.1.1" + + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2' + androidTestImplementation('com.android.support.test.espresso:espresso-contrib:2.0') { + exclude group: 'com.android.support', module: 'appcompat' + exclude group: 'com.android.support', module: 'support-v4' + exclude module: 'recyclerview-v7' + } + + implementation "com.squareup.retrofit2:retrofit:2.4.0" + implementation "com.squareup.okhttp3:logging-interceptor:3.10.0" + implementation "com.squareup.retrofit2:converter-gson:2.4.0" + + implementation "io.reactivex.rxjava2:rxandroid:2.0.1" + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' + implementation "com.jakewharton.rxbinding2:rxbinding-kotlin:2.1.1" + implementation "io.reactivex.rxjava2:rxkotlin:2.2.0" + + + implementation "android.arch.lifecycle:viewmodel:1.1.1" + implementation "android.arch.lifecycle:extensions:1.1.1" + + implementation "com.github.salomonbrys.kodein:kodein:4.1.0" + implementation "com.github.salomonbrys.kodein:kodein-conf:4.1.0" + implementation "com.github.salomonbrys.kodein:kodein-android:4.1.0" + implementation "com.github.salomonbrys.kodein:kodein-core:4.1.0" + + implementation 'com.squareup.picasso:picasso:2.71828' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/assets/success.json b/app/src/androidTest/assets/success.json new file mode 100644 index 00000000..e615a434 --- /dev/null +++ b/app/src/androidTest/assets/success.json @@ -0,0 +1,582 @@ +[ + { + "id": "gE1phX0Lbos", + "created_at": "2018-05-06T23:17:47-04:00", + "updated_at": "2018-05-07T09:52:16-04:00", + "width": 3744, + "height": 5616, + "color": "#C7A4CD", + "description": null, + "urls": { + "raw": "https://images.unsplash.com/photo-1525663018617-37753d540108?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=b6d1772ec0f0113676f7b21b426917bb", + "full": "https://images.unsplash.com/photo-1525663018617-37753d540108?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=5c66b5d86f0c30d4d04e038da6f1b9aa", + "regular": "https://images.unsplash.com/photo-1525663018617-37753d540108?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=defc0b102ec094665a358464fe694232", + "small": "https://images.unsplash.com/photo-1525663018617-37753d540108?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=1ab22b071233de00f89432f314d91328", + "thumb": "https://images.unsplash.com/photo-1525663018617-37753d540108?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=ed4e5c0d72cf6739c57c628b8bafb1bf" + }, + "links": { + "self": "https://api.unsplash.com/photos/gE1phX0Lbos", + "html": "https://unsplash.com/photos/gE1phX0Lbos", + "download": "https://unsplash.com/photos/gE1phX0Lbos/download", + "download_location": "https://api.unsplash.com/photos/gE1phX0Lbos/download" + }, + "categories": [], + "sponsored": false, + "likes": 1, + "liked_by_user": false, + "current_user_collections": [], + "slug": null, + "user": { + "id": "tIXKLyaN9PY", + "updated_at": "2018-05-07T10:04:06-04:00", + "username": "_hybrid_", + "name": "Hybrid", + "first_name": "Hybrid", + "last_name": null, + "twitter_username": "AmbitiousCloud", + "portfolio_url": null, + "bio": "Indianapolis based photographer, videographer, freelance designer. Instagram: @artbyhybrid //\r\nLIVE. CREATE. INSPIRE. Available for freelance work - madisoncoren@gmail.com", + "location": "indianapolis", + "links": { + "self": "https://api.unsplash.com/users/_hybrid_", + "html": "https://unsplash.com/@_hybrid_", + "photos": "https://api.unsplash.com/users/_hybrid_/photos", + "likes": "https://api.unsplash.com/users/_hybrid_/likes", + "portfolio": "https://api.unsplash.com/users/_hybrid_/portfolio", + "following": "https://api.unsplash.com/users/_hybrid_/following", + "followers": "https://api.unsplash.com/users/_hybrid_/followers" + }, + "profile_image": { + "small": "https://images.unsplash.com/profile-1509210755922-56355b2314e1?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=2c0588c5f36216664474aef1c026959a", + "medium": "https://images.unsplash.com/profile-1509210755922-56355b2314e1?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=41c643b34b8438d697e3e22566e0c46c", + "large": "https://images.unsplash.com/profile-1509210755922-56355b2314e1?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=72f29f20fc0e598a5b384da986ea868c" + }, + "total_collections": 9, + "instagram_username": "artbyhybrid", + "total_likes": 205, + "total_photos": 92 + } + }, + { + "id": "MDvwDSYhLeY", + "created_at": "2018-05-06T13:54:24-04:00", + "updated_at": "2018-05-07T09:52:13-04:00", + "width": 3001, + "height": 4501, + "color": "#E2D8E3", + "description": null, + "urls": { + "raw": "https://images.unsplash.com/photo-1525629179084-f7a7c04f50f1?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=49fcc05538712685073135bd7a9ecb31", + "full": "https://images.unsplash.com/photo-1525629179084-f7a7c04f50f1?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=f62e9c4b85411b3e3822b6a376cd21af", + "regular": "https://images.unsplash.com/photo-1525629179084-f7a7c04f50f1?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=18fe7963acce29815da44025d5a2ae52", + "small": "https://images.unsplash.com/photo-1525629179084-f7a7c04f50f1?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=1e64ee35c84e0cf318cc3d272527c9fc", + "thumb": "https://images.unsplash.com/photo-1525629179084-f7a7c04f50f1?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=0447678b7cdce7b77523b84303dd1f77" + }, + "links": { + "self": "https://api.unsplash.com/photos/MDvwDSYhLeY", + "html": "https://unsplash.com/photos/MDvwDSYhLeY", + "download": "https://unsplash.com/photos/MDvwDSYhLeY/download", + "download_location": "https://api.unsplash.com/photos/MDvwDSYhLeY/download" + }, + "categories": [], + "sponsored": false, + "likes": 1, + "liked_by_user": false, + "current_user_collections": [], + "slug": null, + "user": { + "id": "aJmvNKmrzxI", + "updated_at": "2018-05-06T20:22:16-04:00", + "username": "oliver_photographer", + "name": "Oliver Cole", + "first_name": "Oliver", + "last_name": "Cole", + "twitter_username": "rescw", + "portfolio_url": "http://www.oliverphotographer.co.uk", + "bio": "Photographer/retoucher from England.", + "location": "Winchester, Hampshire", + "links": { + "self": "https://api.unsplash.com/users/oliver_photographer", + "html": "https://unsplash.com/@oliver_photographer", + "photos": "https://api.unsplash.com/users/oliver_photographer/photos", + "likes": "https://api.unsplash.com/users/oliver_photographer/likes", + "portfolio": "https://api.unsplash.com/users/oliver_photographer/portfolio", + "following": "https://api.unsplash.com/users/oliver_photographer/following", + "followers": "https://api.unsplash.com/users/oliver_photographer/followers" + }, + "profile_image": { + "small": "https://images.unsplash.com/profile-1491602702638-1b85eb30dd99?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=8e10309827fbd82d1fcf824c7f07e81d", + "medium": "https://images.unsplash.com/profile-1491602702638-1b85eb30dd99?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=843581b3ec266fc96546440cb7b28d46", + "large": "https://images.unsplash.com/profile-1491602702638-1b85eb30dd99?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=49910c78233cd8f213cdc544ab4e1d7d" + }, + "total_collections": 1, + "instagram_username": "oliver_photographer", + "total_likes": 73, + "total_photos": 52 + } + }, + { + "id": "WfQgHNWhAck", + "created_at": "2018-05-07T08:24:18-04:00", + "updated_at": "2018-05-07T09:52:06-04:00", + "width": 3429, + "height": 4800, + "color": "#1C2024", + "description": null, + "urls": { + "raw": "https://images.unsplash.com/photo-1525695825379-c5aa8c9b467e?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=a189780140f789f5385dc5316fea00f7", + "full": "https://images.unsplash.com/photo-1525695825379-c5aa8c9b467e?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=506a62a642641a8e6a446f0464fb012a", + "regular": "https://images.unsplash.com/photo-1525695825379-c5aa8c9b467e?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=03894f338badb83af19c2a7dbbda521d", + "small": "https://images.unsplash.com/photo-1525695825379-c5aa8c9b467e?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=0a9364b80d1618f59b3422e5187e91ac", + "thumb": "https://images.unsplash.com/photo-1525695825379-c5aa8c9b467e?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=a7ae7e1027f4d4fbe3d0d34dc2c75119" + }, + "links": { + "self": "https://api.unsplash.com/photos/WfQgHNWhAck", + "html": "https://unsplash.com/photos/WfQgHNWhAck", + "download": "https://unsplash.com/photos/WfQgHNWhAck/download", + "download_location": "https://api.unsplash.com/photos/WfQgHNWhAck/download" + }, + "categories": [], + "sponsored": false, + "likes": 3, + "liked_by_user": false, + "current_user_collections": [], + "slug": null, + "user": { + "id": "etGQ2Xm8ySQ", + "updated_at": "2018-05-07T08:27:47-04:00", + "username": "kasperrasmussen", + "name": "Kasper Rasmussen", + "first_name": "Kasper", + "last_name": "Rasmussen", + "twitter_username": null, + "portfolio_url": null, + "bio": null, + "location": null, + "links": { + "self": "https://api.unsplash.com/users/kasperrasmussen", + "html": "https://unsplash.com/@kasperrasmussen", + "photos": "https://api.unsplash.com/users/kasperrasmussen/photos", + "likes": "https://api.unsplash.com/users/kasperrasmussen/likes", + "portfolio": "https://api.unsplash.com/users/kasperrasmussen/portfolio", + "following": "https://api.unsplash.com/users/kasperrasmussen/following", + "followers": "https://api.unsplash.com/users/kasperrasmussen/followers" + }, + "profile_image": { + "small": "https://images.unsplash.com/placeholder-avatars/extra-large.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=0ad68f44c4725d5a3fda019bab9d3edc", + "medium": "https://images.unsplash.com/placeholder-avatars/extra-large.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=356bd4b76a3d4eb97d63f45b818dd358", + "large": "https://images.unsplash.com/placeholder-avatars/extra-large.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=ee8bbf5fb8d6e43aaaa238feae2fe90d" + }, + "total_collections": 0, + "instagram_username": null, + "total_likes": 19, + "total_photos": 5 + } + }, + { + "id": "2MIvtFP68OQ", + "created_at": "2018-05-07T08:27:47-04:00", + "updated_at": "2018-05-07T09:51:57-04:00", + "width": 3840, + "height": 4800, + "color": "#F8C9B1", + "description": null, + "urls": { + "raw": "https://images.unsplash.com/photo-1525696015091-0c325054e92f?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=29767df133edf38bc98e27a646e2eb43", + "full": "https://images.unsplash.com/photo-1525696015091-0c325054e92f?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=a68b0ab4aa35fd897f7bed8db9f366d7", + "regular": "https://images.unsplash.com/photo-1525696015091-0c325054e92f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=dab331711aeb3fdb2b9b0ce8386c58aa", + "small": "https://images.unsplash.com/photo-1525696015091-0c325054e92f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=5282e3cb4d20d72548aa54ea959ed33a", + "thumb": "https://images.unsplash.com/photo-1525696015091-0c325054e92f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=7439691cd3dfdcf18d67c2bdcf577d6b" + }, + "links": { + "self": "https://api.unsplash.com/photos/2MIvtFP68OQ", + "html": "https://unsplash.com/photos/2MIvtFP68OQ", + "download": "https://unsplash.com/photos/2MIvtFP68OQ/download", + "download_location": "https://api.unsplash.com/photos/2MIvtFP68OQ/download" + }, + "categories": [], + "sponsored": false, + "likes": 1, + "liked_by_user": false, + "current_user_collections": [], + "slug": null, + "user": { + "id": "etGQ2Xm8ySQ", + "updated_at": "2018-05-07T08:27:47-04:00", + "username": "kasperrasmussen", + "name": "Kasper Rasmussen", + "first_name": "Kasper", + "last_name": "Rasmussen", + "twitter_username": null, + "portfolio_url": null, + "bio": null, + "location": null, + "links": { + "self": "https://api.unsplash.com/users/kasperrasmussen", + "html": "https://unsplash.com/@kasperrasmussen", + "photos": "https://api.unsplash.com/users/kasperrasmussen/photos", + "likes": "https://api.unsplash.com/users/kasperrasmussen/likes", + "portfolio": "https://api.unsplash.com/users/kasperrasmussen/portfolio", + "following": "https://api.unsplash.com/users/kasperrasmussen/following", + "followers": "https://api.unsplash.com/users/kasperrasmussen/followers" + }, + "profile_image": { + "small": "https://images.unsplash.com/placeholder-avatars/extra-large.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=0ad68f44c4725d5a3fda019bab9d3edc", + "medium": "https://images.unsplash.com/placeholder-avatars/extra-large.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=356bd4b76a3d4eb97d63f45b818dd358", + "large": "https://images.unsplash.com/placeholder-avatars/extra-large.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=ee8bbf5fb8d6e43aaaa238feae2fe90d" + }, + "total_collections": 0, + "instagram_username": null, + "total_likes": 19, + "total_photos": 5 + } + }, + { + "id": "KBSOJaWNTlU", + "created_at": "2018-05-06T13:14:42-04:00", + "updated_at": "2018-05-07T09:50:43-04:00", + "width": 2242, + "height": 3408, + "color": "#1C2331", + "description": null, + "urls": { + "raw": "https://images.unsplash.com/photo-1525626712546-694f3d60f30a?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=be221e4e545a0a9ac23297e8c4286cdf", + "full": "https://images.unsplash.com/photo-1525626712546-694f3d60f30a?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=31225573b77eedfa2cb803d9e43de2ea", + "regular": "https://images.unsplash.com/photo-1525626712546-694f3d60f30a?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=b6ee474fd55735336f1917547e4d53d5", + "small": "https://images.unsplash.com/photo-1525626712546-694f3d60f30a?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=f48c8f9a23cb0132ddfbcf6bc2179d11", + "thumb": "https://images.unsplash.com/photo-1525626712546-694f3d60f30a?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=1a42d4f37e2722e152d4f17f47b52622" + }, + "links": { + "self": "https://api.unsplash.com/photos/KBSOJaWNTlU", + "html": "https://unsplash.com/photos/KBSOJaWNTlU", + "download": "https://unsplash.com/photos/KBSOJaWNTlU/download", + "download_location": "https://api.unsplash.com/photos/KBSOJaWNTlU/download" + }, + "categories": [], + "sponsored": false, + "likes": 3, + "liked_by_user": false, + "current_user_collections": [], + "slug": null, + "user": { + "id": "l3XWZHiX-nQ", + "updated_at": "2018-05-07T09:39:40-04:00", + "username": "piensaenpixel", + "name": "Emilio Garcia", + "first_name": "Emilio", + "last_name": "Garcia", + "twitter_username": "piensaenpixel", + "portfolio_url": null, + "bio": "@piensaenpixel Everywhere. Ping me for collaborations!", + "location": null, + "links": { + "self": "https://api.unsplash.com/users/piensaenpixel", + "html": "https://unsplash.com/@piensaenpixel", + "photos": "https://api.unsplash.com/users/piensaenpixel/photos", + "likes": "https://api.unsplash.com/users/piensaenpixel/likes", + "portfolio": "https://api.unsplash.com/users/piensaenpixel/portfolio", + "following": "https://api.unsplash.com/users/piensaenpixel/following", + "followers": "https://api.unsplash.com/users/piensaenpixel/followers" + }, + "profile_image": { + "small": "https://images.unsplash.com/profile-1523306443217-4c56454b65ca?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=4f1a0104d4e8425fd3e66fd2e8eed138", + "medium": "https://images.unsplash.com/profile-1523306443217-4c56454b65ca?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=6dbed2a2416a5db92a70a92adeabb7bd", + "large": "https://images.unsplash.com/profile-1523306443217-4c56454b65ca?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=244ef60769f71fa4fc97b718345d73e2" + }, + "total_collections": 0, + "instagram_username": "piensaenpixel", + "total_likes": 5, + "total_photos": 30 + } + }, + { + "id": "q_B6nmzhZw0", + "created_at": "2018-05-06T07:07:35-04:00", + "updated_at": "2018-05-07T09:50:39-04:00", + "width": 3648, + "height": 5472, + "color": "#36393F", + "description": null, + "urls": { + "raw": "https://images.unsplash.com/photo-1525604803468-3064e402d70c?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=f6262232013bf9967ff5e1ad58bcf4de", + "full": "https://images.unsplash.com/photo-1525604803468-3064e402d70c?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=3508af1e96c1fca9abe194a649a980c8", + "regular": "https://images.unsplash.com/photo-1525604803468-3064e402d70c?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=b706a29a6f2c31906371569310c100a7", + "small": "https://images.unsplash.com/photo-1525604803468-3064e402d70c?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=31e6853740a4b7cf9a2d8f7e268ac88a", + "thumb": "https://images.unsplash.com/photo-1525604803468-3064e402d70c?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=fb2c50a2010c82837d03c591b7a39fab" + }, + "links": { + "self": "https://api.unsplash.com/photos/q_B6nmzhZw0", + "html": "https://unsplash.com/photos/q_B6nmzhZw0", + "download": "https://unsplash.com/photos/q_B6nmzhZw0/download", + "download_location": "https://api.unsplash.com/photos/q_B6nmzhZw0/download" + }, + "categories": [], + "sponsored": false, + "likes": 1, + "liked_by_user": false, + "current_user_collections": [], + "slug": null, + "user": { + "id": "6NMhhbXu_2o", + "updated_at": "2018-05-06T22:26:54-04:00", + "username": "shotsbywolf", + "name": "Geert Pieters", + "first_name": "Geert", + "last_name": "Pieters", + "twitter_username": "Wolf_Images", + "portfolio_url": "https://wolf-fotografie.rocks", + "bio": "“I capture your memories” • Personal Photographer • 100% YOU • Antwerp, BE geert@wolf-fotografie.be 📸 | wolf 🐺", + "location": "Belgium", + "links": { + "self": "https://api.unsplash.com/users/shotsbywolf", + "html": "https://unsplash.com/@shotsbywolf", + "photos": "https://api.unsplash.com/users/shotsbywolf/photos", + "likes": "https://api.unsplash.com/users/shotsbywolf/likes", + "portfolio": "https://api.unsplash.com/users/shotsbywolf/portfolio", + "following": "https://api.unsplash.com/users/shotsbywolf/following", + "followers": "https://api.unsplash.com/users/shotsbywolf/followers" + }, + "profile_image": { + "small": "https://images.unsplash.com/profile-1512283343268-4e67d76d6d08?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=79256122dcb41aa62259ad4f7e100c34", + "medium": "https://images.unsplash.com/profile-1512283343268-4e67d76d6d08?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=f96cebb05b58e2797b81683578742508", + "large": "https://images.unsplash.com/profile-1512283343268-4e67d76d6d08?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=09120eff8c19b123a3dc3ee3a978b049" + }, + "total_collections": 6, + "instagram_username": "shotsbywolf", + "total_likes": 65, + "total_photos": 10 + } + }, + { + "id": "77ojznuSomk", + "created_at": "2018-05-06T07:23:17-04:00", + "updated_at": "2018-05-07T09:49:59-04:00", + "width": 3992, + "height": 2992, + "color": "#3EA8EE", + "description": null, + "urls": { + "raw": "https://images.unsplash.com/photo-1525605777981-0b8a8d8bc5c2?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=d075d84855aa5cdcebc98de35495e5b1", + "full": "https://images.unsplash.com/photo-1525605777981-0b8a8d8bc5c2?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=956317e599179bbfd4df7013596bcdee", + "regular": "https://images.unsplash.com/photo-1525605777981-0b8a8d8bc5c2?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=ae8615bdb45257bce6f39db9112c1339", + "small": "https://images.unsplash.com/photo-1525605777981-0b8a8d8bc5c2?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=bd74285b8e6f539a7a50475095cfbe97", + "thumb": "https://images.unsplash.com/photo-1525605777981-0b8a8d8bc5c2?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=cd12b83337cec0b4d2cb590da1f61004" + }, + "links": { + "self": "https://api.unsplash.com/photos/77ojznuSomk", + "html": "https://unsplash.com/photos/77ojznuSomk", + "download": "https://unsplash.com/photos/77ojznuSomk/download", + "download_location": "https://api.unsplash.com/photos/77ojznuSomk/download" + }, + "categories": [], + "sponsored": false, + "likes": 0, + "liked_by_user": false, + "current_user_collections": [], + "slug": null, + "user": { + "id": "nFtx7xLaipM", + "updated_at": "2018-05-06T21:17:58-04:00", + "username": "osmanrana", + "name": "Osman Rana", + "first_name": "Osman", + "last_name": "Rana", + "twitter_username": null, + "portfolio_url": "Https://www.oranaphotography.com", + "bio": null, + "location": "Boston, USA", + "links": { + "self": "https://api.unsplash.com/users/osmanrana", + "html": "https://unsplash.com/@osmanrana", + "photos": "https://api.unsplash.com/users/osmanrana/photos", + "likes": "https://api.unsplash.com/users/osmanrana/likes", + "portfolio": "https://api.unsplash.com/users/osmanrana/portfolio", + "following": "https://api.unsplash.com/users/osmanrana/following", + "followers": "https://api.unsplash.com/users/osmanrana/followers" + }, + "profile_image": { + "small": "https://images.unsplash.com/profile-1470101925356-0e31654f5fad?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=786b661410ef01b8fd296ba260c0ddc3", + "medium": "https://images.unsplash.com/profile-1470101925356-0e31654f5fad?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=fbb7a619aee2308c576b0c169c177e2d", + "large": "https://images.unsplash.com/profile-1470101925356-0e31654f5fad?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=eb2485779f78aeaeaaae611351c194dc" + }, + "total_collections": 0, + "instagram_username": "ranagraphs", + "total_likes": 615, + "total_photos": 156 + } + }, + { + "id": "s99cWsdckks", + "created_at": "2018-05-07T09:31:06-04:00", + "updated_at": "2018-05-07T09:47:13-04:00", + "width": 1536, + "height": 2302, + "color": "#F0A988", + "description": null, + "urls": { + "raw": "https://images.unsplash.com/photo-1525699857086-c5c1822cd53f?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=d4a7b2f1f584ad4478c76358d3bb3ef8", + "full": "https://images.unsplash.com/photo-1525699857086-c5c1822cd53f?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=20f2c085fced900698eb5f87f492fd01", + "regular": "https://images.unsplash.com/photo-1525699857086-c5c1822cd53f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=6460a5f6cce0f6f643de848cc1a7bde3", + "small": "https://images.unsplash.com/photo-1525699857086-c5c1822cd53f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=22138bea50b354ea6fe920e2ebf41fcb", + "thumb": "https://images.unsplash.com/photo-1525699857086-c5c1822cd53f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=0d1b1ecb011c98d99edc17c9c33e9616" + }, + "links": { + "self": "https://api.unsplash.com/photos/s99cWsdckks", + "html": "https://unsplash.com/photos/s99cWsdckks", + "download": "https://unsplash.com/photos/s99cWsdckks/download", + "download_location": "https://api.unsplash.com/photos/s99cWsdckks/download" + }, + "categories": [], + "sponsored": false, + "likes": 2, + "liked_by_user": false, + "current_user_collections": [], + "slug": null, + "user": { + "id": "AnECqpDoDeA", + "updated_at": "2018-05-07T09:31:22-04:00", + "username": "perchekindustrie", + "name": "Clément Percheron", + "first_name": "Clément", + "last_name": "Percheron", + "twitter_username": null, + "portfolio_url": null, + "bio": "Mes photos sont mises à votre disposition. Vous en faites ce que vous voulez.\r\nMon compte Instagram : @perchek.industrie\r\n", + "location": null, + "links": { + "self": "https://api.unsplash.com/users/perchekindustrie", + "html": "https://unsplash.com/@perchekindustrie", + "photos": "https://api.unsplash.com/users/perchekindustrie/photos", + "likes": "https://api.unsplash.com/users/perchekindustrie/likes", + "portfolio": "https://api.unsplash.com/users/perchekindustrie/portfolio", + "following": "https://api.unsplash.com/users/perchekindustrie/following", + "followers": "https://api.unsplash.com/users/perchekindustrie/followers" + }, + "profile_image": { + "small": "https://images.unsplash.com/profile-1525248649777-15310cc7ef81?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=135a6d10647e42e4e1a1b1186ad08e64", + "medium": "https://images.unsplash.com/profile-1525248649777-15310cc7ef81?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=a5b2e2f592a2ba674af72ac53cf9b55e", + "large": "https://images.unsplash.com/profile-1525248649777-15310cc7ef81?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=3bcb841f00a8bdf5f3e3c188ba5f02e7" + }, + "total_collections": 0, + "instagram_username": "perchek.industrie", + "total_likes": 1, + "total_photos": 33 + } + }, + { + "id": "mqTeZeOwP3o", + "created_at": "2018-05-07T08:51:31-04:00", + "updated_at": "2018-05-07T09:47:04-04:00", + "width": 6720, + "height": 4480, + "color": "#030914", + "description": null, + "urls": { + "raw": "https://images.unsplash.com/photo-1525697320842-41b3fa1a4b99?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=b195efc93f29e8ac3b3b674827c07e83", + "full": "https://images.unsplash.com/photo-1525697320842-41b3fa1a4b99?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=c266d4033ee99008159e6b24d7fab13f", + "regular": "https://images.unsplash.com/photo-1525697320842-41b3fa1a4b99?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=d573e1fd3935e9ec357746ab04d86d1d", + "small": "https://images.unsplash.com/photo-1525697320842-41b3fa1a4b99?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=3012488d07b15dce95c89fc8595fa173", + "thumb": "https://images.unsplash.com/photo-1525697320842-41b3fa1a4b99?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=2bf91b310ee6f6be7a06c46fb8d7c98c" + }, + "links": { + "self": "https://api.unsplash.com/photos/mqTeZeOwP3o", + "html": "https://unsplash.com/photos/mqTeZeOwP3o", + "download": "https://unsplash.com/photos/mqTeZeOwP3o/download", + "download_location": "https://api.unsplash.com/photos/mqTeZeOwP3o/download" + }, + "categories": [], + "sponsored": false, + "likes": 1, + "liked_by_user": false, + "current_user_collections": [], + "slug": null, + "user": { + "id": "EDrR_qL61bc", + "updated_at": "2018-05-07T08:51:31-04:00", + "username": "sickhews", + "name": "Wes Hicks", + "first_name": "Wes", + "last_name": "Hicks", + "twitter_username": "sickhews", + "portfolio_url": "http://www.weshicks.photography/", + "bio": "Programmer by day, photographer by night (also by day from time to time). \r\n\r\nIf you like my work, please follow me on Instagram @sickhews, and @stateofportrait for my portrait work. ", + "location": " Charlotte, NC", + "links": { + "self": "https://api.unsplash.com/users/sickhews", + "html": "https://unsplash.com/@sickhews", + "photos": "https://api.unsplash.com/users/sickhews/photos", + "likes": "https://api.unsplash.com/users/sickhews/likes", + "portfolio": "https://api.unsplash.com/users/sickhews/portfolio", + "following": "https://api.unsplash.com/users/sickhews/following", + "followers": "https://api.unsplash.com/users/sickhews/followers" + }, + "profile_image": { + "small": "https://images.unsplash.com/profile-1506428741023-5a008554979b?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=ffd0609064b44b5b2eff85fcadb6b07b", + "medium": "https://images.unsplash.com/profile-1506428741023-5a008554979b?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=e4f0cb5622e07dd4b8db1cc71fcd1c2c", + "large": "https://images.unsplash.com/profile-1506428741023-5a008554979b?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=a16d9b4257872d8cbb062aa5fcc7e3b5" + }, + "total_collections": 0, + "instagram_username": "sickhews", + "total_likes": 46, + "total_photos": 88 + } + }, + { + "id": "d1ardVRHGH4", + "created_at": "2018-05-06T13:56:12-04:00", + "updated_at": "2018-05-07T09:45:50-04:00", + "width": 5184, + "height": 3456, + "color": "#FD8A31", + "description": null, + "urls": { + "raw": "https://images.unsplash.com/photo-1525629353533-7cad035c00dc?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=c8d07a6dc1cb326b24f8fd5a8ddf5837", + "full": "https://images.unsplash.com/photo-1525629353533-7cad035c00dc?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=813d2749806136598d5c08d24ca80515", + "regular": "https://images.unsplash.com/photo-1525629353533-7cad035c00dc?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=719b85288a9de9ad48687fd9e7d905ff", + "small": "https://images.unsplash.com/photo-1525629353533-7cad035c00dc?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=bf5f186e93bf87b6a72287c91a20a864", + "thumb": "https://images.unsplash.com/photo-1525629353533-7cad035c00dc?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjI2MDUwfQ&s=6a3cffce08ddf87f80ab6eb712188a31" + }, + "links": { + "self": "https://api.unsplash.com/photos/d1ardVRHGH4", + "html": "https://unsplash.com/photos/d1ardVRHGH4", + "download": "https://unsplash.com/photos/d1ardVRHGH4/download", + "download_location": "https://api.unsplash.com/photos/d1ardVRHGH4/download" + }, + "categories": [], + "sponsored": false, + "likes": 1, + "liked_by_user": false, + "current_user_collections": [], + "slug": null, + "user": { + "id": "UrmR23-PAf8", + "updated_at": "2018-05-06T20:59:43-04:00", + "username": "jhjowen", + "name": "James Owen", + "first_name": "James", + "last_name": "Owen", + "twitter_username": "JHJOwen", + "portfolio_url": "http://divine-film.com", + "bio": "Follower of Jesus | Creative Media Student | Filmmaker & Photographer", + "location": "Llanelli, Wales", + "links": { + "self": "https://api.unsplash.com/users/jhjowen", + "html": "https://unsplash.com/@jhjowen", + "photos": "https://api.unsplash.com/users/jhjowen/photos", + "likes": "https://api.unsplash.com/users/jhjowen/likes", + "portfolio": "https://api.unsplash.com/users/jhjowen/portfolio", + "following": "https://api.unsplash.com/users/jhjowen/following", + "followers": "https://api.unsplash.com/users/jhjowen/followers" + }, + "profile_image": { + "small": "https://images.unsplash.com/profile-1513487514106-38ee2aa39ccf?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=bc506300b719af917d08e50be35823dc", + "medium": "https://images.unsplash.com/profile-1513487514106-38ee2aa39ccf?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=9e057df56fb0f85104c08437aeb58abb", + "large": "https://images.unsplash.com/profile-1513487514106-38ee2aa39ccf?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=82ff3dabbf7c987f9641bf8624d8b6c8" + }, + "total_collections": 0, + "instagram_username": "JHJOwen", + "total_likes": 49, + "total_photos": 21 + } + } +] \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/AcceptanceTest.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/AcceptanceTest.kt new file mode 100644 index 00000000..f0ac5d4e --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/AcceptanceTest.kt @@ -0,0 +1,57 @@ +package com.example.joao.photoscodechallenge + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.support.test.InstrumentationRegistry +import android.support.test.espresso.intent.Intents +import android.support.test.filters.LargeTest +import android.support.test.rule.ActivityTestRule +import android.support.test.runner.AndroidJUnit4 +import com.github.salomonbrys.kodein.Kodein +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.runner.RunWith + + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +@LargeTest +@RunWith(AndroidJUnit4::class) +abstract class AcceptanceTest(clazz: Class){ + + val server = MockWebServer() + + @Rule + @JvmField + val testRule: ActivityTestRule = ActivityTestRule(clazz, true, false) + + @Before + fun setup() { + val app = InstrumentationRegistry.getInstrumentation().targetContext.asApplication() + app.resetInjection() + app.addModule(testDependencies) + Intents.init() + server.start() + } + + protected fun startActivity(args: Bundle = Bundle()): T { + val intent = Intent() + intent.putExtras(args) + return testRule.launchActivity(intent) + } + + fun getJson(fileName : String) = FileHandler().readResource(InstrumentationRegistry.getInstrumentation().context,fileName) + + abstract val testDependencies: Kodein.Module + + @After + fun tearDown(){ + Intents.release() + server.shutdown() + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..1df46fbf --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.example.joao.photoscodechallenge + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("com.example.joao.photoscodechallenge", appContext.packageName) + } +} diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/FileHandler.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/FileHandler.kt new file mode 100644 index 00000000..203154e8 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/FileHandler.kt @@ -0,0 +1,30 @@ +package com.example.joao.photoscodechallenge + +import android.content.Context +import java.util.* + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class FileHandler { + + fun readResource(context: Context, fileName: String): String{ + + val stream = context.resources.assets.open(fileName) + val sb = StringBuilder() + + try { + val scanner = Scanner(stream) + while (scanner.hasNextLine()) { + val line = scanner.nextLine() + sb.append(line).append("\n") + } + }catch (e: Exception){ + e.printStackTrace() + }finally { + stream.close() + } + return sb.toString() + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/RequestInterceptorMock.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/RequestInterceptorMock.kt new file mode 100644 index 00000000..c04b9bff --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/RequestInterceptorMock.kt @@ -0,0 +1,10 @@ +package com.example.joao.photoscodechallenge + +import okhttp3.Interceptor + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class RequestInterceptorMock constructor(private val exception: Exception): Interceptor { + override fun intercept(chain: Interceptor.Chain) = throw exception +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityBadRequestTest.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityBadRequestTest.kt new file mode 100644 index 00000000..50a26f9a --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityBadRequestTest.kt @@ -0,0 +1,37 @@ +package com.example.joao.photoscodechallenge.home + +import com.example.joao.photoscodechallenge.AcceptanceTest +import com.example.joao.photoscodechallenge.RequestInterceptorMock +import com.example.joao.photoscodechallenge.di.Injector +import com.example.joao.photoscodechallenge.robots.robot +import com.example.joao.photoscodechallenge.ui.MainActivity +import com.example.joao.photoscodechallenge.webservice.exceptions.BadRequestException +import com.github.salomonbrys.kodein.Kodein +import com.github.salomonbrys.kodein.bind +import com.github.salomonbrys.kodein.provider +import okhttp3.Interceptor +import org.junit.Test + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class HomeActivityBadRequestTest : AcceptanceTest(MainActivity::class.java) { + + @Test + fun testWithBadRequestState() { + + startActivity() + + robot { + + } withBadRequest { + errorHasBeenShown() + } + } + + override val testDependencies = Kodein.Module(allowSilentOverride = true) { + bind(tag = Injector.REQUEST_INTERCEPTOR,overrides = true) with provider { + RequestInterceptorMock(BadRequestException()) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityClientErrorTest.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityClientErrorTest.kt new file mode 100644 index 00000000..6c8afd30 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityClientErrorTest.kt @@ -0,0 +1,38 @@ +package com.example.joao.photoscodechallenge.home + + +import com.example.joao.photoscodechallenge.AcceptanceTest +import com.example.joao.photoscodechallenge.RequestInterceptorMock +import com.example.joao.photoscodechallenge.di.Injector +import com.example.joao.photoscodechallenge.robots.robot +import com.example.joao.photoscodechallenge.ui.MainActivity +import com.example.joao.photoscodechallenge.webservice.exceptions.Error4XXException +import com.github.salomonbrys.kodein.Kodein +import com.github.salomonbrys.kodein.bind +import com.github.salomonbrys.kodein.provider +import okhttp3.Interceptor +import org.junit.Test + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class HomeActivityClientErrorTest : AcceptanceTest(MainActivity::class.java) { + + @Test + fun testWithClientErrorState() { + + startActivity() + + robot { + + } withClientError { + errorHasBeenShown() + } + } + + override val testDependencies = Kodein.Module(allowSilentOverride = true) { + bind(tag = Injector.REQUEST_INTERCEPTOR, overrides = true) with provider { + RequestInterceptorMock(Error4XXException()) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityContentTest.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityContentTest.kt new file mode 100644 index 00000000..ee0ada64 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityContentTest.kt @@ -0,0 +1,32 @@ +package com.example.joao.photoscodechallenge.home + + +import com.example.joao.photoscodechallenge.AcceptanceTest +import com.example.joao.photoscodechallenge.robots.robot +import com.example.joao.photoscodechallenge.ui.MainActivity +import com.github.salomonbrys.kodein.Kodein +import okhttp3.mockwebserver.MockResponse +import org.junit.Test + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class HomeActivityContentTest : AcceptanceTest(MainActivity::class.java) { + + @Test + fun testWithContentState() { + + server.enqueue(MockResponse() + .setResponseCode(200) + .setBody(getJson("success.json"))) + + startActivity() + + robot{ + + } withContent { + isSuccess() + } + } + override val testDependencies = Kodein.Module(allowSilentOverride = true) {} +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityEmptyStateTest.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityEmptyStateTest.kt new file mode 100644 index 00000000..95c18bd8 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityEmptyStateTest.kt @@ -0,0 +1,38 @@ +package com.example.joao.photoscodechallenge.home + + +import com.example.joao.photoscodechallenge.AcceptanceTest +import com.example.joao.photoscodechallenge.RequestInterceptorMock +import com.example.joao.photoscodechallenge.di.Injector +import com.example.joao.photoscodechallenge.robots.robot +import com.example.joao.photoscodechallenge.ui.MainActivity +import com.example.joao.photoscodechallenge.webservice.exceptions.NoDataException +import com.github.salomonbrys.kodein.Kodein +import com.github.salomonbrys.kodein.bind +import com.github.salomonbrys.kodein.provider +import okhttp3.Interceptor +import org.junit.Test + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class HomeActivityEmptyStateTest : AcceptanceTest(MainActivity::class.java) { + + @Test + fun testWithEmptyState() { + + startActivity() + + robot { + + } withEmptyState { + errorHasBeenShown() + } + } + + override val testDependencies = Kodein.Module(allowSilentOverride = true) { + bind(tag = Injector.REQUEST_INTERCEPTOR,overrides = true) with provider { + RequestInterceptorMock(NoDataException()) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityGenericErrorTest.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityGenericErrorTest.kt new file mode 100644 index 00000000..872c2021 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityGenericErrorTest.kt @@ -0,0 +1,38 @@ +package com.example.joao.photoscodechallenge.home + + + +import com.example.joao.photoscodechallenge.AcceptanceTest +import com.example.joao.photoscodechallenge.RequestInterceptorMock +import com.example.joao.photoscodechallenge.di.Injector +import com.example.joao.photoscodechallenge.robots.robot +import com.example.joao.photoscodechallenge.ui.MainActivity +import com.github.salomonbrys.kodein.Kodein +import com.github.salomonbrys.kodein.bind +import com.github.salomonbrys.kodein.provider +import okhttp3.Interceptor +import org.junit.Test + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class HomeActivityGenericErrorTest : AcceptanceTest(MainActivity::class.java) { + + @Test + fun testWithGenericErrorState() { + + startActivity() + + robot { + + } withGenericError { + errorHasBeenShown() + } + } + + override val testDependencies = Kodein.Module(allowSilentOverride = true) { + bind(tag = Injector.REQUEST_INTERCEPTOR,overrides = true) with provider { + RequestInterceptorMock(Exception()) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityNoNetworkTest.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityNoNetworkTest.kt new file mode 100644 index 00000000..57b63ced --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityNoNetworkTest.kt @@ -0,0 +1,38 @@ +package com.example.joao.photoscodechallenge.home + + +import com.example.joao.photoscodechallenge.AcceptanceTest +import com.example.joao.photoscodechallenge.RequestInterceptorMock +import com.example.joao.photoscodechallenge.di.Injector +import com.example.joao.photoscodechallenge.robots.robot +import com.example.joao.photoscodechallenge.ui.MainActivity +import com.example.joao.photoscodechallenge.webservice.exceptions.NoNetworkException +import com.github.salomonbrys.kodein.Kodein +import com.github.salomonbrys.kodein.bind +import com.github.salomonbrys.kodein.provider +import okhttp3.Interceptor +import org.junit.Test + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class HomeActivityNoNetworkTest : AcceptanceTest(MainActivity::class.java) { + + @Test + fun testWithoutConnectionState() { + + startActivity() + + robot { + + } withoutConnection { + errorHasBeenShown() + } + } + + override val testDependencies = Kodein.Module(allowSilentOverride = true) { + bind(tag = Injector.REQUEST_INTERCEPTOR,overrides = true) with provider { + RequestInterceptorMock(NoNetworkException()) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityServerErrorTest.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityServerErrorTest.kt new file mode 100644 index 00000000..71013fd0 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityServerErrorTest.kt @@ -0,0 +1,39 @@ +package com.example.joao.photoscodechallenge.home + + +import com.example.joao.photoscodechallenge.AcceptanceTest +import com.example.joao.photoscodechallenge.RequestInterceptorMock +import com.example.joao.photoscodechallenge.di.Injector +import com.example.joao.photoscodechallenge.robots.robot +import com.example.joao.photoscodechallenge.ui.MainActivity +import com.example.joao.photoscodechallenge.webservice.exceptions.Error5XXException +import com.github.salomonbrys.kodein.Kodein +import com.github.salomonbrys.kodein.bind +import com.github.salomonbrys.kodein.provider +import okhttp3.Interceptor +import org.junit.Test + + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class HomeActivityServerErrorTest : AcceptanceTest(MainActivity::class.java) { + + @Test + fun testWithServerErrorState() { + + startActivity() + + robot { + + } withServerError { + errorHasBeenShown() + } + } + + override val testDependencies = Kodein.Module(allowSilentOverride = true) { + bind(tag = Injector.REQUEST_INTERCEPTOR,overrides = true) with provider { + RequestInterceptorMock(Error5XXException()) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityTimeoutTest.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityTimeoutTest.kt new file mode 100644 index 00000000..5db3cedc --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/home/HomeActivityTimeoutTest.kt @@ -0,0 +1,38 @@ +package com.example.joao.photoscodechallenge.home + + +import com.example.joao.photoscodechallenge.AcceptanceTest +import com.example.joao.photoscodechallenge.RequestInterceptorMock +import com.example.joao.photoscodechallenge.di.Injector +import com.example.joao.photoscodechallenge.robots.robot +import com.example.joao.photoscodechallenge.ui.MainActivity +import com.example.joao.photoscodechallenge.webservice.exceptions.TimeoutException +import com.github.salomonbrys.kodein.Kodein +import com.github.salomonbrys.kodein.bind +import com.github.salomonbrys.kodein.provider +import okhttp3.Interceptor +import org.junit.Test + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class HomeActivityTimeoutTest : AcceptanceTest(MainActivity::class.java) { + + @Test + fun testWithTimeoutState() { + + startActivity() + + robot { + + } withTimeout { + errorHasBeenShown() + } + } + + override val testDependencies = Kodein.Module(allowSilentOverride = true) { + bind(tag = Injector.REQUEST_INTERCEPTOR,overrides = true) with provider { + RequestInterceptorMock(TimeoutException()) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/HomeRobots.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/HomeRobots.kt new file mode 100644 index 00000000..b0fd9b5d --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/HomeRobots.kt @@ -0,0 +1,35 @@ +package com.example.joao.photoscodechallenge.robots + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +fun robot(func: HomeRobot.() -> Unit) = HomeRobot().apply{func()} + +class HomeRobot { + + + infix fun withContent(func: RobotContentResult.() -> Unit) = + RobotContentResult().apply{ func() } + + infix fun withBadRequest(func: RobotBadRequestResult.() -> Unit) = + RobotBadRequestResult().apply{ func() } + + infix fun withEmptyState(func: RobotEmptyResult.() -> Unit) = + RobotEmptyResult().apply{ func() } + + infix fun withGenericError(func: RobotGenericErrorResult.() -> Unit) = + RobotGenericErrorResult().apply{ func() } + + infix fun withClientError(func: RobotClientErrorResult.() -> Unit) = + RobotClientErrorResult().apply{ func() } + + infix fun withServerError(func: RobotServerErrorResult.() -> Unit) = + RobotServerErrorResult().apply{ func() } + + infix fun withTimeout(func: RobotTimeoutResult.() -> Unit) = + RobotTimeoutResult().apply{ func() } + + infix fun withoutConnection(func: RobotNoNetworkResult.() -> Unit) = + RobotNoNetworkResult().apply{ func() } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotBadRequestResult.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotBadRequestResult.kt new file mode 100644 index 00000000..285339b9 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotBadRequestResult.kt @@ -0,0 +1,21 @@ +package com.example.joao.photoscodechallenge.robots + +import android.support.test.espresso.Espresso.onView +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.matcher.ViewMatchers.* +import com.example.joao.photoscodechallenge.R + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class RobotBadRequestResult { + + fun errorHasBeenShown(): Boolean{ + onView(withText(R.string.bad_request_error_message)).check(matches(isDisplayed())) + + onView(withId(R.id.errorButton)).check(matches((isDisplayed()))) + onView(withText(R.string.try_again)).check(matches((isDisplayed()))) + + return true + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotClientErrorResult.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotClientErrorResult.kt new file mode 100644 index 00000000..4b41373e --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotClientErrorResult.kt @@ -0,0 +1,23 @@ +package com.example.joao.photoscodechallenge.robots + +import android.support.test.espresso.Espresso.onView + +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.matcher.ViewMatchers.* +import com.example.joao.photoscodechallenge.R + + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class RobotClientErrorResult { + + fun errorHasBeenShown(): Boolean{ + onView(withText(R.string.client_error_message)).check(matches(isDisplayed())) + + onView(withId(R.id.errorButton)).check(matches((isDisplayed()))) + onView(withText(R.string.try_again)).check(matches((isDisplayed()))) + + return true + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotContentResult.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotContentResult.kt new file mode 100644 index 00000000..beddfa4c --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotContentResult.kt @@ -0,0 +1,19 @@ +package com.example.joao.photoscodechallenge.robots + +import android.support.test.espresso.Espresso.onView + +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.matcher.ViewMatchers.isDisplayed +import android.support.test.espresso.matcher.ViewMatchers.withId +import com.example.joao.photoscodechallenge.R + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class RobotContentResult { + + fun isSuccess(): Boolean{ + onView(withId(R.id.recyclerView)).check(matches(isDisplayed())) + return true + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotEmptyResult.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotEmptyResult.kt new file mode 100644 index 00000000..0800a320 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotEmptyResult.kt @@ -0,0 +1,22 @@ +package com.example.joao.photoscodechallenge.robots + +import android.support.test.espresso.Espresso.onView + +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.matcher.ViewMatchers.* +import com.example.joao.photoscodechallenge.R + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class RobotEmptyResult { + + fun errorHasBeenShown(): Boolean{ + onView(withText(R.string.empty_error_message)).check(matches(isDisplayed())) + + onView(withId(R.id.errorButton)).check(matches((isDisplayed()))) + onView(withText(R.string.try_again)).check(matches((isDisplayed()))) + + return true + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotGenericErrorResult.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotGenericErrorResult.kt new file mode 100644 index 00000000..019aa925 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotGenericErrorResult.kt @@ -0,0 +1,23 @@ +package com.example.joao.photoscodechallenge.robots + +import android.support.test.espresso.Espresso.onView + +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.matcher.ViewMatchers.* +import com.example.joao.photoscodechallenge.R + + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class RobotGenericErrorResult { + + fun errorHasBeenShown(): Boolean{ + onView(withText(R.string.generic_error_message)).check(matches(isDisplayed())) + + onView(withId(R.id.errorButton)).check(matches((isDisplayed()))) + onView(withText(R.string.try_again)).check(matches((isDisplayed()))) + + return true + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotLoadingResult.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotLoadingResult.kt new file mode 100644 index 00000000..73b69063 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotLoadingResult.kt @@ -0,0 +1,19 @@ +package com.example.joao.photoscodechallenge.robots + +import android.support.test.espresso.Espresso.onView + +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.matcher.ViewMatchers.isDisplayed +import android.support.test.espresso.matcher.ViewMatchers.withId +import com.example.joao.photoscodechallenge.R + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class RobotLoadingResult { + + fun isSuccess(): Boolean{ + onView(withId(R.id.progress)).check(matches(isDisplayed())) + return true + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotNoNetworkResult.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotNoNetworkResult.kt new file mode 100644 index 00000000..beec678e --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotNoNetworkResult.kt @@ -0,0 +1,22 @@ +package com.example.joao.photoscodechallenge.robots + +import android.support.test.espresso.Espresso.onView + +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.matcher.ViewMatchers.* +import com.example.joao.photoscodechallenge.R + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class RobotNoNetworkResult { + + fun errorHasBeenShown(): Boolean{ + onView(withText(R.string.no_connection_error_message)).check(matches(isDisplayed())) + + onView(withId(R.id.errorButton)).check(matches((isDisplayed()))) + onView(withText(R.string.try_again)).check(matches((isDisplayed()))) + + return true + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotServerErrorResult.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotServerErrorResult.kt new file mode 100644 index 00000000..350a7a7c --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotServerErrorResult.kt @@ -0,0 +1,19 @@ +package com.example.joao.photoscodechallenge.robots + +import android.support.test.espresso.Espresso.onView +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.matcher.ViewMatchers.isDisplayed +import android.support.test.espresso.matcher.ViewMatchers.withText +import com.example.joao.photoscodechallenge.R + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class RobotServerErrorResult { + + fun errorHasBeenShown(): Boolean{ + onView(withText(R.string.server_error_message)).check(matches(isDisplayed())) + + return true + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotTimeoutResult.kt b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotTimeoutResult.kt new file mode 100644 index 00000000..16640f25 --- /dev/null +++ b/app/src/androidTest/java/com/example/joao/photoscodechallenge/robots/RobotTimeoutResult.kt @@ -0,0 +1,22 @@ +package com.example.joao.photoscodechallenge.robots + +import android.support.test.espresso.Espresso.onView + +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.matcher.ViewMatchers.* +import com.example.joao.photoscodechallenge.R + +/** + * Created by Joao Alvares Neto on 07/05/2018. + */ +class RobotTimeoutResult { + + fun errorHasBeenShown(): Boolean{ + onView(withText(R.string.timeout_error_message)).check(matches(isDisplayed())) + + onView(withId(R.id.errorButton)).check(matches((isDisplayed()))) + onView(withText(R.string.try_again)).check(matches((isDisplayed()))) + + return true + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..dc3c663b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/MyApplication.kt b/app/src/main/java/com/example/joao/photoscodechallenge/MyApplication.kt new file mode 100644 index 00000000..82fb6001 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/MyApplication.kt @@ -0,0 +1,33 @@ +package com.example.joao.photoscodechallenge + +import android.app.Application +import android.content.Context +import com.example.joao.photoscodechallenge.di.Injector +import com.github.salomonbrys.kodein.Kodein +import com.github.salomonbrys.kodein.KodeinAware +import com.github.salomonbrys.kodein.conf.ConfigurableKodein + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +class MyApplication: Application(), KodeinAware { + + override val kodein = ConfigurableKodein(mutable = true) + + override fun onCreate() { + super.onCreate() + resetInjection() + } + + fun addModule(activityModules: Kodein.Module) { + kodein.addImport(activityModules, true) + } + + fun resetInjection() { + kodein.clear() + kodein.addImport(appDependencies(), true) + } + + private fun appDependencies() = Injector(this).dependencies +} +fun Context.asApplication() = this.applicationContext as MyApplication \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/MyCompose.kt b/app/src/main/java/com/example/joao/photoscodechallenge/MyCompose.kt new file mode 100644 index 00000000..0d97bcad --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/MyCompose.kt @@ -0,0 +1,24 @@ +package com.example.joao.photoscodechallenge + +import com.example.joao.photoscodechallenge.entry.Photo +import io.reactivex.Observable +import io.reactivex.ObservableSource +import io.reactivex.ObservableTransformer + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +class MyCompose: ObservableTransformer, State> { + + override fun apply(upstream: Observable>): ObservableSource { + + return upstream. + map { + photos: List -> + State.Success(photos) as State + } + .onErrorResumeNext { error: Throwable -> + Observable.just(State.Error(error as Exception)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/State.kt b/app/src/main/java/com/example/joao/photoscodechallenge/State.kt new file mode 100644 index 00000000..d217ba98 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/State.kt @@ -0,0 +1,16 @@ +package com.example.joao.photoscodechallenge + +import com.example.joao.photoscodechallenge.entry.Photo + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +sealed class State { + + class Loading: State() + + data class Error(val exception: Exception): State() + + data class Success(val photos: List): State() + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/adapter/MyImageAdapter.kt b/app/src/main/java/com/example/joao/photoscodechallenge/adapter/MyImageAdapter.kt new file mode 100644 index 00000000..f58d0bf6 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/adapter/MyImageAdapter.kt @@ -0,0 +1,68 @@ +package com.example.joao.photoscodechallenge.adapter + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.example.joao.photoscodechallenge.R +import com.example.joao.photoscodechallenge.entry.Photo +import com.jakewharton.rxbinding2.view.RxView +import com.squareup.picasso.Picasso +import kotlinx.android.synthetic.main.image_item.view.* + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +class MyImageAdapter(private val photos: MutableList, val listener: Listener) + : RecyclerView.Adapter() { + + companion object { + const val TYPE_IMAGE = 0 + const val TYPE_FOOTER = 1 + } + + class ViewHolder(v: View) : RecyclerView.ViewHolder(v) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.image_item, parent, false)) + } + + override fun getItemCount() = photos.size + + override fun getItemViewType(position: Int): Int { + return if (isPositionFooter(position)) + TYPE_FOOTER + else + TYPE_IMAGE + } + + private fun isPositionFooter(position: Int): Boolean { + return position == photos.size + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + + val photo = photos[position] + + Picasso + .get() + .load(photo.smallUrl) + .into(holder.itemView.image) + + RxView. + clicks(holder.itemView) + .subscribe({ + listener.onItemClickAtPosition(position) + }) + } + + fun appendImages(newPhotos: List) { + photos.addAll(newPhotos) + notifyItemRangeInserted(itemCount + 1, newPhotos.size) + } +} + +interface Listener { + fun onItemClickAtPosition(position: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/adapter/MyImageDetailsAdapter.kt b/app/src/main/java/com/example/joao/photoscodechallenge/adapter/MyImageDetailsAdapter.kt new file mode 100644 index 00000000..84fad36a --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/adapter/MyImageDetailsAdapter.kt @@ -0,0 +1,40 @@ +package com.example.joao.photoscodechallenge.adapter + +import android.support.v7.widget.RecyclerView +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.example.joao.photoscodechallenge.R +import com.squareup.picasso.Picasso +import kotlinx.android.synthetic.main.image_details_item.view.* + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +class MyImageDetailsAdapter(private val urls: MutableList) + : RecyclerView.Adapter() { + + class ViewHolder(v: View) : RecyclerView.ViewHolder(v) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.image_details_item, parent, false)) + } + + override fun getItemCount(): Int { + Log.i("AAAA", "adaptersize" + urls.size.toString()) + + return urls.size + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val url = urls[position] + Log.i("AAAA DetailsADapter", position.toString()) + Picasso + .get() + .load(url) + .into(holder.itemView.image) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/decorator/GridDividerItemDecoration.java b/app/src/main/java/com/example/joao/photoscodechallenge/decorator/GridDividerItemDecoration.java new file mode 100644 index 00000000..7cbc3ad2 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/decorator/GridDividerItemDecoration.java @@ -0,0 +1,77 @@ +package com.example.joao.photoscodechallenge.decorator; + +import android.graphics.Rect; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import com.example.joao.photoscodechallenge.adapter.MyImageAdapter; + +//got from https://github.com/gonzalomelov/KonaTools/blob/master/library/src/main/java/io/teamkona/konatools/ui/GridItemDecorator.java + +public class GridDividerItemDecoration extends RecyclerView.ItemDecoration { + + private int mSizeGridSpacingPx; + private int mGridSize; + + private boolean mNeedLeftSpacing = false; + + public GridDividerItemDecoration(int gridSpacingPx, int gridSize) { + mSizeGridSpacingPx = gridSpacingPx; + mGridSize = gridSize; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + + int frameWidth = (int) ((parent.getWidth() - (float) mSizeGridSpacingPx * (mGridSize - 1)) / mGridSize); + + int padding = parent.getWidth() / mGridSize - frameWidth; + + + int itemPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewAdapterPosition(); + + + int itemViewType = parent.getAdapter().getItemViewType(itemPosition); + + + if(itemViewType == MyImageAdapter.TYPE_FOOTER) { + return; + } + + if (itemPosition < mGridSize) { + outRect.top = 0; + } else { + outRect.top = mSizeGridSpacingPx; + } + + if (itemPosition % mGridSize == 0) { + + outRect.left = 0; + outRect.right = padding; + mNeedLeftSpacing = true; + + + } else if ((itemPosition + 1) % mGridSize == 0) { + mNeedLeftSpacing = false; + outRect.right = 0; + outRect.left = padding; + } else if (mNeedLeftSpacing) { + mNeedLeftSpacing = false; + outRect.left = mSizeGridSpacingPx - padding; + if ((itemPosition + 2) % mGridSize == 0) { + outRect.right = mSizeGridSpacingPx - padding; + } else { + outRect.right = mSizeGridSpacingPx / 2; + } + } else if ((itemPosition + 2) % mGridSize == 0) { + mNeedLeftSpacing = false; + outRect.left = mSizeGridSpacingPx / 2; + outRect.right = mSizeGridSpacingPx - padding; + } else { + mNeedLeftSpacing = false; + outRect.left = mSizeGridSpacingPx / 2; + outRect.right = mSizeGridSpacingPx / 2; + } + outRect.bottom = 0; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/di/Injector.kt b/app/src/main/java/com/example/joao/photoscodechallenge/di/Injector.kt new file mode 100644 index 00000000..5009fd81 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/di/Injector.kt @@ -0,0 +1,95 @@ +package com.example.joao.photoscodechallenge.di + +import android.content.Context +import android.net.ConnectivityManager +import com.example.joao.photoscodechallenge.service.MyService +import com.example.joao.photoscodechallenge.viewModel.MyViewModel +import com.example.joao.photoscodechallenge.webservice.MyWebServiceAPI +import com.example.joao.photoscodechallenge.webservice.NetworkService +import com.example.joao.photoscodechallenge.webservice.interceptor.RequestInterceptor +import com.github.salomonbrys.kodein.Kodein +import com.github.salomonbrys.kodein.bind +import com.github.salomonbrys.kodein.instance +import com.github.salomonbrys.kodein.provider +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.CallAdapter +import retrofit2.Converter +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ + +class Injector (val context: Context) { + + val dependencies = Kodein.Module(allowSilentOverride = true) { + + bind() with provider{ + MyViewModel(myService = instance()) + } + + bind() with provider { + MyService(webServiceAPI = instance()) + } + + bind() with provider { + NetworkService(connectivityManager = instance()) + } + + bind() with provider { + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + } + + bind() with provider { + val builder = OkHttpClient.Builder() + builder.writeTimeout(30, TimeUnit.SECONDS) + builder.readTimeout(30, TimeUnit.SECONDS) + builder.addInterceptor(instance(REQUEST_INTERCEPTOR)) + builder.addNetworkInterceptor(instance(LOG_INTERCEPTOR)) + builder.build() + } + + bind(tag = LOG_INTERCEPTOR) with provider { + val logging = HttpLoggingInterceptor() + logging.level = HttpLoggingInterceptor.Level.BODY + logging + } + + bind(tag = REQUEST_INTERCEPTOR) with provider { + RequestInterceptor(networkService = instance()) + } + + bind() with provider { + RxJava2CallAdapterFactory.create() + } + + bind() with provider { + GsonConverterFactory.create() + } + + bind() with provider { + val retrofit = Retrofit.Builder() + .addCallAdapterFactory(instance()) + .addConverterFactory(instance()) + .client(instance()) + .baseUrl("https://api.unsplash.com/") + + retrofit.build() + } + + bind() with provider { + instance().create(MyWebServiceAPI::class.java) + } + } + + companion object { + val LOG_INTERCEPTOR = "LogInterceptor" + val REQUEST_INTERCEPTOR = "RequestInterceptor" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/entry/Photo.kt b/app/src/main/java/com/example/joao/photoscodechallenge/entry/Photo.kt new file mode 100644 index 00000000..d074fa0b --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/entry/Photo.kt @@ -0,0 +1,10 @@ +package com.example.joao.photoscodechallenge.entry + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +@Parcelize +data class Photo(val id: String, val smallUrl: String, val regularUrl: String): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/extensions/ViewEx.kt b/app/src/main/java/com/example/joao/photoscodechallenge/extensions/ViewEx.kt new file mode 100644 index 00000000..d185d7ab --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/extensions/ViewEx.kt @@ -0,0 +1,13 @@ +package com.example.joao.photoscodechallenge.extensions + +import android.view.View + +/** + * Created by Joao Alvares Neto on 06/05/2018. + */ +fun View.visible(){ + this.visibility = View.VISIBLE +} +fun View.gone(){ + this.visibility = View.GONE +} diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/service/MyService.kt b/app/src/main/java/com/example/joao/photoscodechallenge/service/MyService.kt new file mode 100644 index 00000000..752c3947 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/service/MyService.kt @@ -0,0 +1,18 @@ +package com.example.joao.photoscodechallenge.service + +import com.example.joao.photoscodechallenge.transformer.SchedulerTransformer +import com.example.joao.photoscodechallenge.webservice.MyWebServiceAPI +import com.example.joao.photoscodechallenge.webservice.payload.MyResponseObject +import io.reactivex.Observable + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +class MyService (val webServiceAPI: MyWebServiceAPI) { + + fun loadPhotos(pageCount: Int): Observable> { + return webServiceAPI. + loadPhotos("a8861af9539a7fce15f9ca4cb443d62423000e32fb5db3377c7209e08f16ca3a", pageCount) + .compose(SchedulerTransformer()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/transformer/SchedulerTransformer.kt b/app/src/main/java/com/example/joao/photoscodechallenge/transformer/SchedulerTransformer.kt new file mode 100644 index 00000000..ce620cff --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/transformer/SchedulerTransformer.kt @@ -0,0 +1,18 @@ +package com.example.joao.photoscodechallenge.transformer + +import io.reactivex.Observable +import io.reactivex.ObservableSource +import io.reactivex.ObservableTransformer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +/** + * Created by Joao Alvares Neto on 06/05/2018. + */ +class SchedulerTransformer : ObservableTransformer { + override fun apply(upstream: Observable): ObservableSource { + return upstream + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/ui/DetailsActivity.kt b/app/src/main/java/com/example/joao/photoscodechallenge/ui/DetailsActivity.kt new file mode 100644 index 00000000..7cef97b0 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/ui/DetailsActivity.kt @@ -0,0 +1,92 @@ +package com.example.joao.photoscodechallenge.ui + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.LinearSnapHelper +import android.support.v7.widget.RecyclerView +import com.example.joao.photoscodechallenge.R +import com.example.joao.photoscodechallenge.adapter.MyImageDetailsAdapter +import com.example.joao.photoscodechallenge.extensions.visible +import com.jakewharton.rxbinding2.view.RxView +import kotlinx.android.synthetic.main.activity_details.* + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ + +class DetailsActivity: AppCompatActivity() { + + private val snapHelper by lazy { LinearSnapHelper() } + + private var currentPosition: Int = 0 + + private val linearLayoutManager by lazy { + LinearLayoutManager(this@DetailsActivity, + LinearLayoutManager.HORIZONTAL, + false) + } + + companion object { + + private const val DETAILS = "details" + private const val POSITION = "position" + const val CURRENT_POSITION = "currentPosition" + const val REQUEST_CODE = 100 + const val VIEW_NAME_HEADER_IMAGE = "IMAGE_TRANSITION" + + fun startActivityForResult(activity: Activity, position: Int, photoDetails: ArrayList) { + + val intent = Intent(activity,DetailsActivity::class.java) + + val bundle = Bundle() + bundle.putStringArrayList(DETAILS, photoDetails) + bundle.putInt(POSITION,position) + activity.startActivityForResult(intent.putExtras(bundle),REQUEST_CODE) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_details) + + RxView + .clicks(close) + .subscribe({ + setResult(Activity.RESULT_OK, mountIntent()) + finish() + }) + + val listPhotoDetails = intent.extras.getStringArrayList(DETAILS) + val position = intent.extras.getInt(POSITION) + + snapHelper.attachToRecyclerView(detailsRecyclerView) + + with(detailsRecyclerView){ + adapter = MyImageDetailsAdapter(listPhotoDetails) + setHasFixedSize(true) + layoutManager = linearLayoutManager + layoutManager.scrollToPosition(position) + visible() + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) { + currentPosition = linearLayoutManager.findFirstCompletelyVisibleItemPosition() + } + }) + } + } + + private fun mountIntent(): Intent { + val resultIntent = Intent() + resultIntent.putExtra(CURRENT_POSITION, currentPosition) + return resultIntent + } + + override fun onBackPressed() { + super.onBackPressed() + setResult(Activity.RESULT_OK, mountIntent()) + finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/ui/MainActivity.kt b/app/src/main/java/com/example/joao/photoscodechallenge/ui/MainActivity.kt new file mode 100644 index 00000000..a1b9ff87 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/ui/MainActivity.kt @@ -0,0 +1,257 @@ +package com.example.joao.photoscodechallenge.ui + +import android.app.Activity +import android.arch.lifecycle.ViewModel +import android.arch.lifecycle.ViewModelProvider +import android.arch.lifecycle.ViewModelProviders +import android.content.Intent +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.View +import com.example.joao.photoscodechallenge.R +import com.example.joao.photoscodechallenge.State +import com.example.joao.photoscodechallenge.adapter.Listener +import com.example.joao.photoscodechallenge.adapter.MyImageAdapter +import com.example.joao.photoscodechallenge.decorator.GridDividerItemDecoration +import com.example.joao.photoscodechallenge.entry.Photo +import com.example.joao.photoscodechallenge.extensions.gone +import com.example.joao.photoscodechallenge.extensions.visible +import com.example.joao.photoscodechallenge.viewModel.MyViewModel +import com.example.joao.photoscodechallenge.webservice.exceptions.* +import com.github.salomonbrys.kodein.LazyKodein +import com.github.salomonbrys.kodein.android.appKodein +import com.github.salomonbrys.kodein.instance +import com.jakewharton.rxbinding2.view.RxView +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.rxkotlin.plusAssign +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.error_state.* +import kotlinx.android.synthetic.main.loading_state.* + + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +class MainActivity : AppCompatActivity() { + + private lateinit var myAdapter: MyImageAdapter + + private var photoList = arrayListOf() + + private lateinit var customGridLayoutManager: GridLayoutManager + + private val CACHED_PHOTOS = "CACHED_PHOTOS" + + private val disposables by lazy { CompositeDisposable() } + + val kodein by lazy { + LazyKodein(appKodein) + } + + private val viewModel by lazy(LazyThreadSafetyMode.NONE) { + + @Suppress("UNCHECKED_CAST") + val factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class) = kodein.value.instance() as T + } + ViewModelProviders.of(this, factory).get(MyViewModel::class.java) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + if (savedInstanceState != null && savedInstanceState.containsKey(CACHED_PHOTOS)) { + photoList = savedInstanceState.getParcelableArrayList(CACHED_PHOTOS) + } + } + + override fun onStart() { + super.onStart() + loadContent() + } + + private fun loadContent() { + if (photoList.isNotEmpty()) { + hideLoading() + showImages(photoList) + return + } + disposables += viewModel.listPhotos() + .subscribe({ + status: State -> handleStatus(status) + }, { + t -> processError(t as Exception) + }) + } + override fun onSaveInstanceState(outState: Bundle?) { + photoList.apply { + outState?.clear() + outState?.putParcelableArrayList(CACHED_PHOTOS, this) + return + } + super.onSaveInstanceState(outState) + } + + private fun handleStatus(status: State) { + clearView() + when(status){ + is State.Loading -> showLoading() + is State.Error -> processError(status.exception) + is State.Success -> { + hideLoading() + photoList.addAll(status.photos) + showImages(status.photos) + } + } + } + + private fun processError(t: Throwable) { + clearView() + when (t) { + is TimeoutException -> showErrorView(R.string.timeout_error_message) + is Error4XXException -> showErrorView(R.string.client_error_message) + is NoNetworkException -> showErrorView(R.string.no_connection_error_message) + is BadRequestException -> showErrorView(R.string.bad_request_error_message) + is NoDataException -> showErrorView(R.string.empty_error_message) + is Error5XXException -> showErrorView(R.string.server_error_message) + else -> showErrorView(R.string.generic_error_message) + } + } + + + private fun loadMore(): Disposable { + return viewModel.loadMore() + .subscribe({ status: State -> handleStatusWhenLoadingMore(status) + },{ + t -> processErrorWhenLoadingMore(t as Exception) + }) + } + + private fun handleStatusWhenLoadingMore(status: State) { + clearView() + when(status){ + is State.Loading -> showLoading() + is State.Error -> processError(status.exception) + is State.Success -> { + hideLoading() + photoList.addAll(status.photos) + appendImages(status.photos) + } + } + } + + private fun processErrorWhenLoadingMore(t: Throwable) { + clearView() + when (t) { + is TimeoutException -> showSnackBar(R.string.timeout_error_message) + is Error4XXException -> showSnackBar(R.string.client_error_message) + is NoNetworkException -> showSnackBar(R.string.no_connection_error_message) + is BadRequestException -> showSnackBar(R.string.bad_request_error_message) + is NoDataException -> showSnackBar(R.string.empty_error_message) + is Error5XXException -> showSnackBar(R.string.server_error_message) + else -> showSnackBar(R.string.generic_error_message) + } + } + + private fun appendImages(photos: List) { + photoList.addAll(photos) + myAdapter.appendImages(photos) + } + + private fun showImages(photos: List) { + + myAdapter = MyImageAdapter(photos.toMutableList(), object : Listener { + override fun onItemClickAtPosition(position: Int) { + val photosDetailsList = arrayListOf() + photoList.mapTo(photosDetailsList) { it.regularUrl } + DetailsActivity.startActivityForResult(this@MainActivity, position, photosDetailsList) + } + }) + + customGridLayoutManager = GridLayoutManager(this, 3) + customGridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + + override fun getSpanSize(position: Int): Int { + + return when (myAdapter.getItemViewType(position)) { + MyImageAdapter.TYPE_FOOTER -> 3 + MyImageAdapter.TYPE_IMAGE -> 1 + else -> 1 + } + } + + } + + val dimensionPixelSize = resources.getDimensionPixelSize(R.dimen.recycler_margin) + + with(recyclerView) { + addItemDecoration(GridDividerItemDecoration(dimensionPixelSize, 3)) + layoutManager = customGridLayoutManager + adapter = myAdapter + visible() + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + if (cannotScrollVertically(recyclerView)) disposables += loadMore() + } + }) + } + + position?.let { + customGridLayoutManager.scrollToPosition(it) + } + } + + private fun clearView() { + hideErrorView() + hideLoading() + } + + private fun hideErrorView() { + errorRoot.visibility = View.GONE + } + + private fun hideLoading() = progress.gone() + + private fun showLoading() = progress.visible() + + private var position: Int? = 0 + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == DetailsActivity.REQUEST_CODE + && resultCode == Activity.RESULT_OK) { + + position = data?.getIntExtra(DetailsActivity.CURRENT_POSITION, 0) + position?.let { + customGridLayoutManager.scrollToPosition(it) + } + } + } + + private fun showErrorView(msgId: Int) { + errorState.setText(msgId) + RxView + .clicks(errorButton) + .subscribe({ + clearView() + disposables += loadMore() + }) + errorRoot.visibility = View.VISIBLE + } + + private fun showSnackBar(msgId: Int) = Snackbar.make(root, "$msgId", Snackbar.LENGTH_LONG).show() + + override fun onDestroy() { + disposables.clear() + super.onDestroy() + } + + private fun cannotScrollVertically(recyclerView: RecyclerView) = + !recyclerView.canScrollVertically(1) +} diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/viewModel/MyViewModel.kt b/app/src/main/java/com/example/joao/photoscodechallenge/viewModel/MyViewModel.kt new file mode 100644 index 00000000..aa7fb627 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/viewModel/MyViewModel.kt @@ -0,0 +1,46 @@ +package com.example.joao.photoscodechallenge.viewModel + +import android.arch.lifecycle.ViewModel +import com.example.joao.photoscodechallenge.MyCompose +import com.example.joao.photoscodechallenge.State +import com.example.joao.photoscodechallenge.entry.Photo +import com.example.joao.photoscodechallenge.service.MyService +import com.example.joao.photoscodechallenge.webservice.payload.MyResponseObject +import io.reactivex.Observable + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +class MyViewModel (val myService: MyService): ViewModel() { + + private var pageCount = 1 + + fun listPhotos(): Observable { + + return myService + .loadPhotos(pageCount) + .map { + pageCount++ + it + } + .map { it.toPhotos() } + .compose(MyCompose()) + .startWith(State.Loading()) + } + + + fun loadMore(): Observable { + + return myService + .loadPhotos(pageCount) + .map { + pageCount++ + it + } + .map { it.toPhotos() } + .compose(MyCompose()) + } + + private fun List.toPhotos() = map { it.toPhoto() } + private fun MyResponseObject.toPhoto() = Photo(id,urlsObject.smallImage,urlsObject.regularImage) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/MyWebServiceAPI.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/MyWebServiceAPI.kt new file mode 100644 index 00000000..46570678 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/MyWebServiceAPI.kt @@ -0,0 +1,16 @@ +package com.example.joao.photoscodechallenge.webservice + +import com.example.joao.photoscodechallenge.webservice.payload.MyResponseObject +import io.reactivex.Observable +import retrofit2.http.GET +import retrofit2.http.Query + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +interface MyWebServiceAPI { + + @GET("photos") + fun loadPhotos(@Query("client_id") id: String,@Query("page") pageCount: Int): Observable> + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/NetworkService.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/NetworkService.kt new file mode 100644 index 00000000..4ae30d00 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/NetworkService.kt @@ -0,0 +1,15 @@ +package com.example.joao.photoscodechallenge.webservice + +import android.net.ConnectivityManager + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +class NetworkService(var connectivityManager: ConnectivityManager) { + + val isConnected: Boolean + get() { + val activeNetwork = connectivityManager.activeNetworkInfo + return activeNetwork != null && activeNetwork.isConnectedOrConnecting + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/BadRequestException.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/BadRequestException.kt new file mode 100644 index 00000000..6fcb44c8 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/BadRequestException.kt @@ -0,0 +1,4 @@ +package com.example.joao.photoscodechallenge.webservice.exceptions + +/** * Created by Joao Alvares Neto on 18/02/2018. */ +class BadRequestException : Exception() \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/Error4XXException.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/Error4XXException.kt new file mode 100644 index 00000000..a2cffcfe --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/Error4XXException.kt @@ -0,0 +1,4 @@ +package com.example.joao.photoscodechallenge.webservice.exceptions + +/** * Created by Joao Alvares Neto on 18/02/2018. */ +class Error4XXException : Exception() \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/Error5XXException.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/Error5XXException.kt new file mode 100644 index 00000000..d7ebf006 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/Error5XXException.kt @@ -0,0 +1,4 @@ +package com.example.joao.photoscodechallenge.webservice.exceptions + +/** * Created by Joao Alvares Neto on 18/02/2018. */ +class Error5XXException : Exception() \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/NoDataException.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/NoDataException.kt new file mode 100644 index 00000000..cb9f74af --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/NoDataException.kt @@ -0,0 +1,4 @@ +package com.example.joao.photoscodechallenge.webservice.exceptions + +/** * Created by Joao Alvares Neto on 18/02/2018. */ +class NoDataException : Exception() \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/NoNetworkException.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/NoNetworkException.kt new file mode 100644 index 00000000..cee33249 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/NoNetworkException.kt @@ -0,0 +1,4 @@ +package com.example.joao.photoscodechallenge.webservice.exceptions + +/** * Created by Joao Alvares Neto on 18/02/2018. */ +class NoNetworkException : Exception() \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/TimeoutException.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/TimeoutException.kt new file mode 100644 index 00000000..17a67d2d --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/exceptions/TimeoutException.kt @@ -0,0 +1,4 @@ +package com.example.joao.photoscodechallenge.webservice.exceptions + +/** * Created by Joao Alvares Neto on 18/02/2018. */ +class TimeoutException : Exception() \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/interceptor/RequestInterceptor.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/interceptor/RequestInterceptor.kt new file mode 100644 index 00000000..294b87fc --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/interceptor/RequestInterceptor.kt @@ -0,0 +1,43 @@ +package com.example.joao.photoscodechallenge.webservice.interceptor + +import com.example.joao.photoscodechallenge.webservice.NetworkService +import com.example.joao.photoscodechallenge.webservice.exceptions.* +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException +import java.net.SocketException +import java.util.concurrent.TimeoutException + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +class RequestInterceptor (val networkService: NetworkService): Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + + if(isNotConnected()) throw NoNetworkException() + + return try{ + + val request = chain.request() + val response = chain.proceed(request) + + when(response.code()){ + 400 -> throw BadRequestException() + 404 -> throw NoDataException() + 401,402,403, in 405..499 -> throw Error4XXException() + in 500..599 -> throw Error5XXException() + else -> response + } + + }catch (e: Exception){ + when(e) { + is IOException, is SocketException -> throw TimeoutException() + else -> throw e + } + } + + } + + private fun isNotConnected() = !networkService.isConnected +} \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/payload/MyResponseObject.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/payload/MyResponseObject.kt new file mode 100644 index 00000000..9fee2b8c --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/payload/MyResponseObject.kt @@ -0,0 +1,10 @@ +package com.example.joao.photoscodechallenge.webservice.payload + +import com.google.gson.annotations.SerializedName + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +data class MyResponseObject (val id: String, + @SerializedName("urls") + val urlsObject: MyUrlsObject) \ No newline at end of file diff --git a/app/src/main/java/com/example/joao/photoscodechallenge/webservice/payload/MyUrlsObject.kt b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/payload/MyUrlsObject.kt new file mode 100644 index 00000000..89ccd2e0 --- /dev/null +++ b/app/src/main/java/com/example/joao/photoscodechallenge/webservice/payload/MyUrlsObject.kt @@ -0,0 +1,9 @@ +package com.example.joao.photoscodechallenge.webservice.payload + +import com.google.gson.annotations.SerializedName + +/** + * Created by Joao Alvares Neto on 05/05/2018. + */ +data class MyUrlsObject (@SerializedName("small")val smallImage: String, + @SerializedName("regular") val regularImage: String) \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..c7bd21db --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..1e65d9a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..d5fccc53 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml new file mode 100644 index 00000000..4284b6e7 --- /dev/null +++ b/app/src/main/res/layout/activity_details.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..69dfd082 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/error_state.xml b/app/src/main/res/layout/error_state.xml new file mode 100644 index 00000000..72c58621 --- /dev/null +++ b/app/src/main/res/layout/error_state.xml @@ -0,0 +1,31 @@ + + + + + +