├── .circleci └── config.yml ├── .github └── FUNDING.yml ├── .gitignore ├── app ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── proguard-rules.pro └── src │ ├── debug │ └── java │ │ └── com │ │ └── jcminarro │ │ └── authexample │ │ ├── Environment.java │ │ └── Utils.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── jcminarro │ │ │ └── authexample │ │ │ ├── AuthExampleApplication.java │ │ │ ├── internal │ │ │ ├── di │ │ │ │ ├── component │ │ │ │ │ ├── ActivityComponent.java │ │ │ │ │ ├── AppComponent.java │ │ │ │ │ ├── FragmentComponent.java │ │ │ │ │ ├── LoginComponent.java │ │ │ │ │ ├── QuoteComponent.java │ │ │ │ │ └── StartUpComponent.java │ │ │ │ ├── injectablebase │ │ │ │ │ ├── BaseInjectionActivity.java │ │ │ │ │ └── BaseInjectionFragment.java │ │ │ │ ├── module │ │ │ │ │ ├── ActivityModule.java │ │ │ │ │ ├── AppModule.java │ │ │ │ │ ├── QuoteModule.java │ │ │ │ │ └── SessionModule.java │ │ │ │ └── scope │ │ │ │ │ ├── PerActivity.java │ │ │ │ │ └── PerFragment.java │ │ │ ├── interactor │ │ │ │ ├── AsyncInteractor.java │ │ │ │ ├── ExecutorCallbackDecorator.java │ │ │ │ ├── ExecutorCallbackToInteractorCallbackAdapter.java │ │ │ │ ├── InUiThreadDispatcherDecorator.java │ │ │ │ ├── InteractorExecutor.java │ │ │ │ ├── JobExecutor.java │ │ │ │ ├── ReferenceRetainerDecorator.java │ │ │ │ └── ThreadExecutor.java │ │ │ ├── localdatasource │ │ │ │ ├── SessionDatasource.java │ │ │ │ └── SharedPreferenceSesssionDatasource.java │ │ │ ├── model │ │ │ │ └── Models.kt │ │ │ ├── navigator │ │ │ │ └── Navigator.kt │ │ │ ├── network │ │ │ │ ├── APIIOException.kt │ │ │ │ ├── AccessTokenProvider.java │ │ │ │ ├── ApiClient.java │ │ │ │ ├── EndpointFactory.java │ │ │ │ ├── OAuth.kt │ │ │ │ ├── SessionReauthorizer.java │ │ │ │ ├── authorizator │ │ │ │ │ ├── AuthorizatedApi.java │ │ │ │ │ ├── AuthorizatedApiInterceptor.java │ │ │ │ │ ├── UnauthorizatedApi.java │ │ │ │ │ └── UnauthorizatedApiInterceptor.java │ │ │ │ ├── login │ │ │ │ │ ├── LoginApiClient.kt │ │ │ │ │ ├── LoginBody.java │ │ │ │ │ ├── LoginEndpoint.java │ │ │ │ │ ├── LoginResponse.java │ │ │ │ │ └── Mapper.kt │ │ │ │ ├── quote │ │ │ │ │ ├── Mapper.kt │ │ │ │ │ ├── QuoteApiClient.kt │ │ │ │ │ ├── QuoteEndpoint.java │ │ │ │ │ └── QuoteResponse.java │ │ │ │ ├── reauthorizate │ │ │ │ │ ├── ReauthorizatedApi.java │ │ │ │ │ ├── ReauthorizatedApiInterceptor.java │ │ │ │ │ └── Reauthorizer.java │ │ │ │ └── refresh │ │ │ │ │ ├── Mapper.kt │ │ │ │ │ ├── RefreshApiClient.kt │ │ │ │ │ ├── RefreshBody.java │ │ │ │ │ ├── RefreshEndpoint.java │ │ │ │ │ └── RefreshResponse.java │ │ │ ├── presenter │ │ │ │ ├── BasePresenter.java │ │ │ │ ├── Presenter.java │ │ │ │ └── lifecycle │ │ │ │ │ ├── PresenterAnnotationException.java │ │ │ │ │ ├── PresenterLifecycleLinker.java │ │ │ │ │ └── PresenterNotAccessibleException.java │ │ │ └── repository │ │ │ │ ├── QuoteRepository.kt │ │ │ │ └── SessionRepository.java │ │ │ ├── login │ │ │ ├── LoginActivity.java │ │ │ ├── LoginInteractor.java │ │ │ └── LoginPresenter.java │ │ │ ├── quote │ │ │ ├── GetQuoteInteractor.java │ │ │ ├── QuoteActivity.java │ │ │ └── QuotePresenter.java │ │ │ └── startup │ │ │ ├── RefreshSessionInteractor.java │ │ │ ├── StartUpActivity.java │ │ │ └── StartUpPresenter.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_login.xml │ │ └── activity_quote.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── release │ └── java │ │ └── com │ │ └── jcminarro │ │ └── authexample │ │ ├── Environment.java │ │ └── Utils.kt │ └── test │ ├── java │ ├── android │ │ └── util │ │ │ └── Log.java │ └── com │ │ └── jcminarro │ │ └── authexample │ │ ├── EndpointMother.kt │ │ ├── EndpointPath.kt │ │ ├── ResponseJsonMother.kt │ │ ├── ResponseMother.kt │ │ ├── StringExtensions.kt │ │ ├── internal │ │ ├── network │ │ │ ├── authorizator │ │ │ │ └── AuthorizatedApiInterceptorTest.kt │ │ │ ├── login │ │ │ │ ├── LoginApiClientTest.kt │ │ │ │ └── LoginApiClientWithMockWebServerTest.kt │ │ │ ├── quote │ │ │ │ ├── QuoteApiClientTest.kt │ │ │ │ └── QuoteApiClientWithMockWebServerTest.kt │ │ │ ├── reauthorizate │ │ │ │ └── ReauthorizatedApiInterceptorTest.kt │ │ │ └── refresh │ │ │ │ └── RefreshApiClientTest.kt │ │ └── repository │ │ │ ├── QuoteRepositoryTest.kt │ │ │ └── SessionRepositoryTest.kt │ │ ├── login │ │ └── LoginInteractorTest.kt │ │ ├── quote │ │ └── GetQuoteInteractorTest.kt │ │ └── startup │ │ └── RefreshSessionInteractorTest.kt │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── build.gradle ├── buildSrc └── src │ └── main │ └── groovy │ └── com │ └── jcminarro │ └── Dependencies.groovy ├── config └── checkstyle │ ├── checkstyle.xml │ └── suppressions.xml ├── extras └── settings.jar ├── gradle.properties ├── gradle ├── checkstyle.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/AuthExample 5 | docker: 6 | - image: circleci/android:api-26-alpha 7 | environment: 8 | TZ: Europe/Madrid 9 | JVM_OPTS: -Xmx3200m 10 | GRADLE_OPTS: '-Dorg.gradle.daemon=false' 11 | _JAVA_OPTIONS: "-Xms256m -Xmx1280m -XX:MaxPermSize=350m" 12 | steps: 13 | - checkout 14 | - restore_cache: 15 | key: jars--{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 16 | - run: 17 | name: Download dependencies 18 | command: ./gradlew dependencies 19 | - save_cache: 20 | paths: 21 | - ~/.gradle 22 | key: jars--{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 23 | - run: 24 | name: Configure Gradle Properties 25 | command: echo "org.gradle.daemon=false" > ~/.gradle/gradle.properties && echo "org.gradle.jvmargs=-Xmx1536m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError" >> ~/.gradle/gradle.properties 26 | - run: 27 | name: Compile AuthExample 28 | command: ./gradlew clean build --stacktrace 29 | - store_artifacts: 30 | path: app/build/outputs 31 | destination: outputs 32 | - store_artifacts: 33 | path: app/build/reports 34 | destination: reports 35 | - store_test_results: 36 | path: app/build/test-results -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jcminarro 2 | ko_fi: jcminarro 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Android Studio 2 | .idea 3 | *.iml 4 | *.ipr 5 | *.iws 6 | classes 7 | gen-external-apklibs 8 | 9 | #Gradle 10 | .gradle 11 | local.properties 12 | build 13 | 14 | #Other 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | import com.jcminarro.Dependencies 2 | 3 | apply plugin: 'com.android.application' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: 'kotlin-android' 6 | apply plugin: 'kotlin-android-extensions' 7 | 8 | android { 9 | compileSdkVersion 26 10 | 11 | defaultConfig { 12 | applicationId "com.jcminarro.authexample" 13 | minSdkVersion 16 14 | targetSdkVersion 26 15 | versionCode 1 16 | versionName "1.0" 17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | 26 | debug { 27 | minifyEnabled false 28 | debuggable true 29 | applicationIdSuffix '.debug' 30 | } 31 | } 32 | 33 | testOptions.unitTests.all { 34 | testLogging { 35 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' 36 | } 37 | } 38 | } 39 | 40 | dependencies { 41 | kapt Dependencies.daggerCompiler 42 | kapt Dependencies.butterKnifeCompiler 43 | implementation Dependencies.supportDesign 44 | implementation Dependencies.butterKnife 45 | implementation Dependencies.dagger 46 | implementation Dependencies.kotlinSTDLib 47 | implementation Dependencies.appCompat 48 | implementation Dependencies.retrofit 49 | implementation Dependencies.retrofitGsonConverter 50 | implementation Dependencies.okHttpLogger 51 | implementation Dependencies.ok2curl 52 | testImplementation Dependencies.jUnit 53 | testImplementation Dependencies.mockito 54 | testImplementation Dependencies.mockitoKotlin 55 | testImplementation Dependencies.kotlinTestJunit 56 | testImplementation Dependencies.kluent 57 | testImplementation Dependencies.wiremock 58 | testImplementation Dependencies.mockwebserver 59 | androidTestImplementation Dependencies.AndroidTestRunner 60 | androidTestImplementation Dependencies.espresso 61 | } 62 | -------------------------------------------------------------------------------- /app/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JcMinarro/AuthExample-Android/4240164a450e91aeaf12134e7afac55b1860d856/app/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jun 14 13:26:54 CEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /app/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/debug/java/com/jcminarro/authexample/Environment.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample; 2 | 3 | public class Environment { 4 | public static final String ENDPOINT_BASE_URL = "https://jcminarro-auth-example.herokuapp.com"; 5 | } 6 | -------------------------------------------------------------------------------- /app/src/debug/java/com/jcminarro/authexample/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample 2 | 3 | import android.util.Log 4 | 5 | val DEBUG_TAG = "debugTag" 6 | 7 | @JvmOverloads 8 | fun log(tag: String = DEBUG_TAG, text: String) { 9 | Log.d(DEBUG_TAG, text) 10 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/AuthExampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample; 2 | 3 | import android.app.Application; 4 | 5 | import com.jcminarro.authexample.internal.di.component.AppComponent; 6 | import com.jcminarro.authexample.internal.di.component.DaggerAppComponent; 7 | import com.jcminarro.authexample.internal.di.module.AppModule; 8 | 9 | public class AuthExampleApplication extends Application{ 10 | 11 | private AppComponent component; 12 | 13 | @Override 14 | public void onCreate() { 15 | super.onCreate(); 16 | component = DaggerAppComponent.builder().appModule(new AppModule(this)).build(); 17 | } 18 | 19 | public AppComponent getAppComponent() { 20 | return component; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/component/ActivityComponent.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.component; 2 | 3 | import android.content.res.Resources; 4 | import android.support.v4.app.FragmentManager; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | public interface ActivityComponent { 8 | 9 | AppCompatActivity provideAppCompatActivity(); 10 | 11 | Resources provideResources(); 12 | 13 | FragmentManager provideFragmentManager(); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/component/AppComponent.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.component; 2 | 3 | import android.content.Context; 4 | 5 | import com.jcminarro.authexample.internal.di.module.AppModule; 6 | import com.jcminarro.authexample.internal.interactor.InteractorExecutor; 7 | import com.jcminarro.authexample.internal.localdatasource.SessionDatasource; 8 | import com.jcminarro.authexample.internal.network.AccessTokenProvider; 9 | import com.jcminarro.authexample.internal.network.EndpointFactory; 10 | 11 | import javax.inject.Singleton; 12 | 13 | import dagger.Component; 14 | 15 | @Singleton 16 | @Component(modules = {AppModule.class}) 17 | public interface AppComponent { 18 | 19 | Context provideContext(); 20 | 21 | InteractorExecutor provideInteractorExecutor(); 22 | 23 | EndpointFactory.Builder provideEndpointFactoryBuilder(); 24 | 25 | SessionDatasource provideSessionDatasource(); 26 | 27 | AccessTokenProvider provideAccessTokenProvider(); 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/component/FragmentComponent.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.component; 2 | 3 | public interface FragmentComponent { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/component/LoginComponent.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.component; 2 | 3 | import com.jcminarro.authexample.internal.di.module.ActivityModule; 4 | import com.jcminarro.authexample.internal.di.module.SessionModule; 5 | import com.jcminarro.authexample.internal.di.scope.PerActivity; 6 | import com.jcminarro.authexample.login.LoginActivity; 7 | 8 | import dagger.Component; 9 | 10 | @PerActivity 11 | @Component(modules = {ActivityModule.class, SessionModule.class}, 12 | dependencies = AppComponent.class) 13 | public interface LoginComponent extends ActivityComponent { 14 | 15 | void inject(LoginActivity loginActivity); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/component/QuoteComponent.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.component; 2 | 3 | import com.jcminarro.authexample.internal.di.module.ActivityModule; 4 | import com.jcminarro.authexample.internal.di.module.QuoteModule; 5 | import com.jcminarro.authexample.internal.di.module.SessionModule; 6 | import com.jcminarro.authexample.internal.di.scope.PerActivity; 7 | import com.jcminarro.authexample.quote.QuoteActivity; 8 | 9 | import dagger.Component; 10 | 11 | @PerActivity 12 | @Component(modules = {ActivityModule.class, 13 | SessionModule.class, 14 | QuoteModule.class}, 15 | dependencies = AppComponent.class) 16 | public interface QuoteComponent extends ActivityComponent { 17 | 18 | void inject(QuoteActivity quoteActivity); 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/component/StartUpComponent.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.component; 2 | 3 | import com.jcminarro.authexample.internal.di.module.ActivityModule; 4 | import com.jcminarro.authexample.internal.di.module.SessionModule; 5 | import com.jcminarro.authexample.internal.di.scope.PerActivity; 6 | import com.jcminarro.authexample.startup.StartUpActivity; 7 | 8 | import dagger.Component; 9 | 10 | @PerActivity 11 | @Component(modules = {ActivityModule.class, SessionModule.class}, 12 | dependencies = AppComponent.class) 13 | public interface StartUpComponent extends ActivityComponent { 14 | 15 | void inject(StartUpActivity startUpActivity); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/injectablebase/BaseInjectionActivity.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.injectablebase; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.jcminarro.authexample.AuthExampleApplication; 7 | import com.jcminarro.authexample.internal.di.component.ActivityComponent; 8 | import com.jcminarro.authexample.internal.di.component.AppComponent; 9 | import com.jcminarro.authexample.internal.presenter.BasePresenter; 10 | import com.jcminarro.authexample.internal.presenter.lifecycle.PresenterLifecycleLinker; 11 | 12 | import javax.inject.Inject; 13 | 14 | import butterknife.ButterKnife; 15 | 16 | public abstract class BaseInjectionActivity extends AppCompatActivity implements 17 | BasePresenter.View { 18 | 19 | protected T activityComponent; 20 | @Inject PresenterLifecycleLinker presenterLifecycleLinker; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | initDI(); 26 | if (getLayout() > 0) { 27 | setContentView(getLayout()); 28 | ButterKnife.bind(this); 29 | onConfigureViews(); 30 | } 31 | onPreparePresenter(); 32 | presenterLifecycleLinker.initialize(this); 33 | } 34 | 35 | protected void onConfigureViews() { 36 | } 37 | 38 | @Override 39 | protected void onResume() { 40 | super.onResume(); 41 | presenterLifecycleLinker.updatePresenters(this); 42 | } 43 | 44 | @Override 45 | public void onPause() { 46 | super.onPause(); 47 | presenterLifecycleLinker.pausePresenters(); 48 | } 49 | 50 | @Override 51 | protected void onDestroy() { 52 | super.onDestroy(); 53 | activityComponent = null; 54 | presenterLifecycleLinker.destroyPresenters(); 55 | } 56 | 57 | protected abstract void initDI(); 58 | 59 | protected abstract int getLayout(); 60 | 61 | public AppComponent getAppComponent() { 62 | return ((AuthExampleApplication) getApplication()).getAppComponent(); 63 | } 64 | 65 | T getActivityComponent() { 66 | return activityComponent; 67 | } 68 | 69 | /** 70 | * Called before to initialize all the presenter instances linked to the component lifecycle. 71 | * Override this method to configure your presenter with extra data if needed. 72 | */ 73 | protected void onPreparePresenter() { 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/injectablebase/BaseInjectionFragment.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.injectablebase; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.Fragment; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import com.jcminarro.authexample.internal.di.component.ActivityComponent; 12 | import com.jcminarro.authexample.internal.di.component.AppComponent; 13 | import com.jcminarro.authexample.internal.di.component.FragmentComponent; 14 | import com.jcminarro.authexample.internal.presenter.BasePresenter; 15 | import com.jcminarro.authexample.internal.presenter.lifecycle.PresenterLifecycleLinker; 16 | 17 | import javax.inject.Inject; 18 | 19 | import butterknife.ButterKnife; 20 | 21 | public abstract class BaseInjectionFragment 22 | extends Fragment implements BasePresenter.View { 23 | 24 | protected T fragmentComponent; 25 | private ActivityComponent activityComponent; 26 | private AppComponent appComponent; 27 | @Inject PresenterLifecycleLinker presenterLifecycleLinker; 28 | 29 | @Override 30 | public void onPause() { 31 | super.onPause(); 32 | presenterLifecycleLinker.pausePresenters(); 33 | } 34 | 35 | @Override 36 | public void onAttach(Context context) { 37 | super.onAttach(context); 38 | if (context instanceof BaseInjectionActivity) { 39 | activityComponent = ((BaseInjectionActivity) context).activityComponent; 40 | appComponent = ((BaseInjectionActivity) context).getAppComponent(); 41 | } else { 42 | throw new IllegalArgumentException( 43 | "This fragment need to be attached to a class that extend : " + 44 | BaseInjectionActivity.class.getCanonicalName()); 45 | } 46 | } 47 | 48 | @Override 49 | public void onResume() { 50 | super.onResume(); 51 | presenterLifecycleLinker.updatePresenters(this); 52 | } 53 | 54 | @Override 55 | public void onCreate(@Nullable Bundle savedInstanceState) { 56 | super.onCreate(savedInstanceState); 57 | initDI(); 58 | } 59 | 60 | @Override 61 | public View onCreateView( 62 | LayoutInflater inflater, 63 | @Nullable ViewGroup container, 64 | @Nullable Bundle savedInstanceState) { 65 | final View view; 66 | if (getFragmentLayout() > 0) { 67 | view = inflater.inflate(getFragmentLayout(), container, false); 68 | ButterKnife.bind(this, view); 69 | onConfigureViews(); 70 | } else { 71 | view = super.onCreateView(inflater, container, savedInstanceState); 72 | } 73 | onPreparePresenter(); 74 | initializePresenterLifecycle(); 75 | return view; 76 | } 77 | 78 | protected void onConfigureViews() { 79 | } 80 | 81 | /** 82 | * Called before to initialize all the presenter instances linked to the component lifecycle. 83 | * Override this method to configure your presenter with extra data if needed. 84 | */ 85 | protected void onPreparePresenter() { 86 | 87 | } 88 | 89 | protected void initializePresenterLifecycle() { 90 | presenterLifecycleLinker.initialize(this); 91 | } 92 | 93 | @Override 94 | public void onDestroy() { 95 | super.onDestroy(); 96 | appComponent = null; 97 | activityComponent = null; 98 | fragmentComponent = null; 99 | presenterLifecycleLinker.destroyPresenters(); 100 | } 101 | 102 | protected abstract int getFragmentLayout(); 103 | 104 | protected abstract void initDI(); 105 | 106 | protected ActivityComponent getActivityComponent() { 107 | return activityComponent; 108 | } 109 | 110 | protected AppComponent getAppComponent() { 111 | return appComponent; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/module/ActivityModule.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.module; 2 | 3 | import android.content.res.Resources; 4 | import android.support.v4.app.FragmentManager; 5 | import android.support.v7.app.AppCompatActivity; 6 | import com.jcminarro.authexample.internal.di.injectablebase.BaseInjectionActivity; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | 11 | @Module 12 | public class ActivityModule { 13 | 14 | private BaseInjectionActivity baseInjectionActivity; 15 | 16 | public ActivityModule(BaseInjectionActivity baseInjectionActivity) { 17 | this.baseInjectionActivity = baseInjectionActivity; 18 | } 19 | 20 | @Provides 21 | public AppCompatActivity provideAppCompatActivity() { 22 | return baseInjectionActivity; 23 | } 24 | 25 | @Provides 26 | FragmentManager provideFragmentManager() { 27 | return baseInjectionActivity.getSupportFragmentManager(); 28 | } 29 | 30 | @Provides 31 | Resources provideResources() { 32 | return baseInjectionActivity.getResources(); 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/module/AppModule.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.module; 2 | 3 | import android.content.Context; 4 | 5 | import com.jcminarro.authexample.AuthExampleApplication; 6 | import com.jcminarro.authexample.Environment; 7 | import com.jcminarro.authexample.internal.interactor.InUiThreadDispatcherDecorator; 8 | import com.jcminarro.authexample.internal.interactor.InteractorExecutor; 9 | import com.jcminarro.authexample.internal.interactor.JobExecutor; 10 | import com.jcminarro.authexample.internal.interactor.ReferenceRetainerDecorator; 11 | import com.jcminarro.authexample.internal.interactor.ThreadExecutor; 12 | import com.jcminarro.authexample.internal.localdatasource.SessionDatasource; 13 | import com.jcminarro.authexample.internal.localdatasource.SharedPreferenceSesssionDatasource; 14 | import com.jcminarro.authexample.internal.network.AccessTokenProvider; 15 | import com.jcminarro.authexample.internal.network.EndpointFactory; 16 | 17 | import javax.inject.Singleton; 18 | 19 | import dagger.Module; 20 | import dagger.Provides; 21 | 22 | @Module 23 | public class AppModule { 24 | 25 | private static final String SHARED_PREFERENCES_SESSION = ".session"; 26 | private AuthExampleApplication application; 27 | 28 | public AppModule(AuthExampleApplication application) { 29 | this.application = application; 30 | } 31 | 32 | @Provides 33 | Context provideContext() { 34 | return application; 35 | } 36 | 37 | @Provides 38 | @Singleton 39 | ThreadExecutor provideThreadExecutor(JobExecutor jobExecutor) { 40 | return jobExecutor; 41 | } 42 | 43 | @Provides 44 | InteractorExecutor provideInteractorExecutor( 45 | ThreadExecutor threadExecutor, 46 | ReferenceRetainerDecorator referenceRetainerDecorator, 47 | InUiThreadDispatcherDecorator inUiThreadDispatcherDecorator) { 48 | return new InteractorExecutor( 49 | threadExecutor, 50 | referenceRetainerDecorator, 51 | inUiThreadDispatcherDecorator 52 | ); 53 | } 54 | 55 | @Provides 56 | EndpointFactory.Builder provideEndpointFactoryBuilder() { 57 | return new EndpointFactory.Builder(Environment.ENDPOINT_BASE_URL); 58 | } 59 | 60 | @Provides 61 | @Singleton 62 | SharedPreferenceSesssionDatasource provideSharedPreferenceSesssionDatasource(Context context) { 63 | return new SharedPreferenceSesssionDatasource( 64 | context.getSharedPreferences(SHARED_PREFERENCES_SESSION, Context.MODE_PRIVATE)); 65 | } 66 | 67 | @Provides 68 | AccessTokenProvider provideAccessTokenProvider( 69 | SharedPreferenceSesssionDatasource sharedPreferenceSesssionDatasource) { 70 | return sharedPreferenceSesssionDatasource; 71 | } 72 | 73 | @Provides 74 | SessionDatasource provideSessionDatasource( 75 | SharedPreferenceSesssionDatasource sharedPreferenceSesssionDatasource) { 76 | return sharedPreferenceSesssionDatasource; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/module/QuoteModule.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.module; 2 | 3 | import com.jcminarro.authexample.internal.network.EndpointFactory; 4 | import com.jcminarro.authexample.internal.network.SessionReauthorizer; 5 | import com.jcminarro.authexample.internal.network.authorizator.AuthorizatedApiInterceptor; 6 | import com.jcminarro.authexample.internal.network.quote.QuoteEndpoint; 7 | import com.jcminarro.authexample.internal.network.reauthorizate.ReauthorizatedApiInterceptor; 8 | import com.jcminarro.authexample.internal.network.reauthorizate.Reauthorizer; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | 13 | @Module 14 | public class QuoteModule { 15 | 16 | @Provides 17 | QuoteEndpoint provideQuoteEndpoint( 18 | EndpointFactory.Builder builder, 19 | AuthorizatedApiInterceptor authorizatedApiInterceptor, 20 | ReauthorizatedApiInterceptor reauthorizatedApiInterceptor) { 21 | return builder.withAuthorizatedApiInterceptor(authorizatedApiInterceptor) 22 | .withReauthorizatedApiInterceptor(reauthorizatedApiInterceptor) 23 | .build() 24 | .create(QuoteEndpoint.class); 25 | } 26 | 27 | @Provides 28 | Reauthorizer provideReauthorizer(SessionReauthorizer sessionReauthorizer) { 29 | return sessionReauthorizer; 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/module/SessionModule.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.module; 2 | 3 | import com.jcminarro.authexample.internal.network.EndpointFactory; 4 | import com.jcminarro.authexample.internal.network.authorizator.UnauthorizatedApiInterceptor; 5 | import com.jcminarro.authexample.internal.network.login.LoginEndpoint; 6 | import com.jcminarro.authexample.internal.network.refresh.RefreshEndpoint; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | 11 | @Module 12 | public class SessionModule { 13 | 14 | @Provides 15 | LoginEndpoint provideLoginEndpoint( 16 | EndpointFactory.Builder builder, 17 | UnauthorizatedApiInterceptor unauthorizatedApiInterceptor) { 18 | return builder.withUnaouthorizatedApiInterceptor(unauthorizatedApiInterceptor) 19 | .build() 20 | .create(LoginEndpoint.class); 21 | } 22 | 23 | @Provides 24 | RefreshEndpoint provideRefreshEndpoint( 25 | EndpointFactory.Builder builder, 26 | UnauthorizatedApiInterceptor unauthorizatedApiInterceptor) { 27 | return builder.withUnaouthorizatedApiInterceptor(unauthorizatedApiInterceptor) 28 | .build() 29 | .create(RefreshEndpoint.class); 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/scope/PerActivity.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.scope; 2 | 3 | import java.lang.annotation.Retention; 4 | 5 | import javax.inject.Scope; 6 | 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Scope 10 | @Retention(RUNTIME) 11 | public @interface PerActivity {} -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/di/scope/PerFragment.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.di.scope; 2 | 3 | import java.lang.annotation.Retention; 4 | 5 | import javax.inject.Scope; 6 | 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Scope 10 | @Retention(RUNTIME) 11 | public @interface PerFragment {} -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/interactor/AsyncInteractor.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.interactor; 2 | 3 | public interface AsyncInteractor { 4 | 5 | interface Callback { 6 | 7 | void onSuccess(O output); 8 | 9 | void onError(E error); 10 | } 11 | 12 | void execute(I input, final Callback callback); 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/interactor/ExecutorCallbackDecorator.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.interactor; 2 | 3 | public interface ExecutorCallbackDecorator { 4 | 5 | InteractorExecutor.Callback decorate(InteractorExecutor.Callback callback); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/interactor/ExecutorCallbackToInteractorCallbackAdapter.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.interactor; 2 | 3 | public class ExecutorCallbackToInteractorCallbackAdapter implements AsyncInteractor.Callback { 4 | 5 | private final InteractorExecutor.Callback callback; 6 | 7 | ExecutorCallbackToInteractorCallbackAdapter(InteractorExecutor.Callback callback) { 8 | this.callback = callback; 9 | } 10 | 11 | @Override 12 | public final void onSuccess(O output) { 13 | callback.onSuccess(output); 14 | } 15 | 16 | @Override 17 | public void onError(E error) { 18 | callback.onError(error); 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/interactor/InUiThreadDispatcherDecorator.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.interactor; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | import java.lang.ref.WeakReference; 7 | 8 | import javax.inject.Inject; 9 | 10 | public class InUiThreadDispatcherDecorator implements ExecutorCallbackDecorator { 11 | 12 | private final Handler handler; 13 | 14 | @Inject 15 | public InUiThreadDispatcherDecorator() { 16 | this.handler = new Handler(Looper.getMainLooper()); 17 | } 18 | 19 | public InteractorExecutor.Callback decorate(InteractorExecutor.Callback callback) { 20 | return new CallbackDecorator<>(handler, callback); 21 | } 22 | 23 | private static class CallbackDecorator implements InteractorExecutor.Callback { 24 | 25 | private final Handler handler; 26 | private final WeakReference> callback; 27 | 28 | private CallbackDecorator(Handler handler, InteractorExecutor.Callback callback) { 29 | this.handler = handler; 30 | this.callback = new WeakReference<>(callback); 31 | } 32 | 33 | @Override 34 | public void onSuccess(final O output) { 35 | final InteractorExecutor.Callback unwrappedCallback = callback.get(); 36 | if (unwrappedCallback != null) { 37 | handler.post(new Runnable() { 38 | @Override 39 | public void run() { 40 | unwrappedCallback.onSuccess(output); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | @Override 47 | public void onError(final E error) { 48 | final InteractorExecutor.Callback unwrappedCallback = callback.get(); 49 | if (unwrappedCallback != null) { 50 | handler.post(new Runnable() { 51 | @Override 52 | public void run() { 53 | unwrappedCallback.onError(error); 54 | } 55 | }); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/interactor/InteractorExecutor.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.interactor; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | public class InteractorExecutor { 7 | 8 | public interface Callback { 9 | void onSuccess(O output); 10 | 11 | void onError(E error); 12 | } 13 | 14 | public static abstract class SuccessCallback implements Callback { 15 | 16 | @Override 17 | public void onError(E error) { 18 | } 19 | } 20 | 21 | public static class NullCallback implements Callback { 22 | 23 | @Override 24 | public void onSuccess(O output) { 25 | } 26 | 27 | @Override 28 | public void onError(E error) { 29 | } 30 | } 31 | 32 | private final ThreadExecutor threadExecutor; 33 | private final List callbackDecorators; 34 | 35 | public InteractorExecutor( 36 | ThreadExecutor threadExecutor, 37 | ExecutorCallbackDecorator... decorators) { 38 | this.threadExecutor = threadExecutor; 39 | this.callbackDecorators = Arrays.asList(decorators); 40 | } 41 | 42 | public void execute( 43 | final AsyncInteractor interactor, 44 | final I input, 45 | Callback callback) { 46 | final AsyncInteractor.Callback wrappedCallback = adaptCallback(callback); 47 | threadExecutor.execute(new Runnable() { 48 | @Override 49 | public void run() { 50 | interactor.execute(input, wrappedCallback); 51 | } 52 | }); 53 | } 54 | 55 | private AsyncInteractor.Callback adaptCallback(Callback callback) { 56 | Callback adaptedCallback = decorateCallback(callback); 57 | return new ExecutorCallbackToInteractorCallbackAdapter<>(adaptedCallback); 58 | } 59 | 60 | private Callback decorateCallback(Callback callback) { 61 | Callback decoratedCallback = callback; 62 | 63 | for (ExecutorCallbackDecorator adapter : callbackDecorators) { 64 | decoratedCallback = adapter.decorate(decoratedCallback); 65 | } 66 | 67 | return decoratedCallback; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/interactor/JobExecutor.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.interactor; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.util.concurrent.BlockingQueue; 6 | import java.util.concurrent.LinkedBlockingQueue; 7 | import java.util.concurrent.ThreadFactory; 8 | import java.util.concurrent.ThreadPoolExecutor; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import javax.inject.Inject; 12 | 13 | /** 14 | * Decorated {@link ThreadPoolExecutor} 15 | */ 16 | public class JobExecutor implements ThreadExecutor { 17 | 18 | // Sets the amount of time an idle thread waits before terminating 19 | private static final int KEEP_ALIVE_TIME = 10; 20 | 21 | // Sets the Time Unit to seconds 22 | private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; 23 | private static final int NUMBER_OF_CORES = 2 * Runtime.getRuntime().availableProcessors(); 24 | 25 | private final BlockingQueue workQueue; 26 | private final ThreadPoolExecutor threadPoolExecutor; 27 | private final ThreadFactory threadFactory; 28 | 29 | @Inject 30 | public JobExecutor() { 31 | this.workQueue = new LinkedBlockingQueue<>(); 32 | this.threadFactory = new JobThreadFactory(); 33 | this.threadPoolExecutor = new ThreadPoolExecutor(NUMBER_OF_CORES, NUMBER_OF_CORES, 34 | KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, this.workQueue, this.threadFactory); 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | * 40 | * @param runnable The class that implements {@link Runnable} interface. 41 | */ 42 | @Override 43 | public void execute(Runnable runnable) { 44 | if (runnable == null) { 45 | throw new IllegalArgumentException("Runnable to execute cannot be null"); 46 | } 47 | this.threadPoolExecutor.execute(runnable); 48 | } 49 | 50 | private static class JobThreadFactory implements ThreadFactory { 51 | 52 | private static final String THREAD_NAME = "android_"; 53 | private int counter = 0; 54 | 55 | @Override 56 | public Thread newThread(@NonNull Runnable runnable) { 57 | return new Thread(runnable, THREAD_NAME + counter); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/interactor/ReferenceRetainerDecorator.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.interactor; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import javax.inject.Inject; 7 | 8 | /** 9 | * This class is in charge of holding references to all the interactor callbacks during the lifetime of its holder, 10 | * primarily, the presenter. 11 | *

12 | * This is done in order to keep anonymous classes referenced either during the interactor execution lifetime or 13 | * the presenter lifetime, whichever lives less. 14 | */ 15 | public class ReferenceRetainerDecorator implements ExecutorCallbackDecorator { 16 | 17 | private final Set> callbacks = new HashSet<>(); 18 | 19 | @Inject 20 | public ReferenceRetainerDecorator() { 21 | } 22 | 23 | public InteractorExecutor.Callback decorate(InteractorExecutor.Callback callback) { 24 | CallbackDecorator decorator = new CallbackDecorator<>( 25 | callbacks, 26 | callback 27 | ); 28 | callbacks.add(decorator); 29 | return decorator; 30 | } 31 | 32 | private static class CallbackDecorator implements InteractorExecutor.Callback { 33 | 34 | private final Set> callbacks; 35 | private final InteractorExecutor.Callback retainedCallback; 36 | 37 | private CallbackDecorator( 38 | Set> callbacks, 39 | InteractorExecutor.Callback retainedCallback) { 40 | this.callbacks = callbacks; 41 | this.retainedCallback = retainedCallback; 42 | } 43 | 44 | @Override 45 | public void onSuccess(O output) { 46 | retainedCallback.onSuccess(output); 47 | callbacks.remove(retainedCallback); 48 | } 49 | 50 | @Override 51 | public void onError(E exception) { 52 | retainedCallback.onError(exception); 53 | callbacks.remove(retainedCallback); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/interactor/ThreadExecutor.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.interactor; 2 | 3 | public interface ThreadExecutor { 4 | 5 | void execute(final Runnable runnable); 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/localdatasource/SessionDatasource.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.localdatasource; 2 | 3 | import com.jcminarro.authexample.internal.network.OAuth; 4 | 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | public interface SessionDatasource { 9 | 10 | void storeOAuthSession(@NotNull OAuth auth); 11 | 12 | @Nullable 13 | OAuth getOAuthSession(); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/localdatasource/SharedPreferenceSesssionDatasource.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.localdatasource; 2 | 3 | import android.content.SharedPreferences; 4 | 5 | import com.jcminarro.authexample.internal.network.AccessTokenProvider; 6 | import com.jcminarro.authexample.internal.network.OAuth; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | public class SharedPreferenceSesssionDatasource implements SessionDatasource, AccessTokenProvider { 12 | 13 | private static final String ACCESS_TOKEN_KEY = "accessToken"; 14 | private static final String REFRESH_TOKEN_KEY = "refreshToken"; 15 | private final SharedPreferences sharedPreferences; 16 | 17 | public SharedPreferenceSesssionDatasource(SharedPreferences sharedPreferences) { 18 | this.sharedPreferences = sharedPreferences; 19 | } 20 | 21 | @Override 22 | public void storeOAuthSession(@NotNull OAuth auth) { 23 | sharedPreferences.edit() 24 | .putString(ACCESS_TOKEN_KEY, auth.getAccessToken()) 25 | .putString(REFRESH_TOKEN_KEY, auth.getRefreshToken()) 26 | .apply(); 27 | } 28 | 29 | @Nullable 30 | @Override 31 | public OAuth getOAuthSession() { 32 | String accessToken = sharedPreferences.getString(ACCESS_TOKEN_KEY, null); 33 | String refreshToken = sharedPreferences.getString(REFRESH_TOKEN_KEY, null); 34 | if (accessToken != null && refreshToken != null) { 35 | return new OAuth(accessToken, refreshToken); 36 | } else { 37 | return null; 38 | } 39 | } 40 | 41 | @Override 42 | public String getAccessToken() { 43 | return sharedPreferences.getString(ACCESS_TOKEN_KEY, ""); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/model/Models.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.model 2 | 3 | data class Quote(val author: String, val message: String) -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/navigator/Navigator.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.navigator 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.support.v7.app.AppCompatActivity 6 | import com.jcminarro.authexample.login.LoginActivity 7 | import com.jcminarro.authexample.quote.QuoteActivity 8 | import java.lang.ref.WeakReference 9 | import javax.inject.Inject 10 | 11 | class Navigator @Inject constructor(var appCompactActivity: AppCompatActivity) { 12 | val appCompactActivityWeakReference = WeakReference(appCompactActivity) 13 | 14 | fun navigateToLogin() { 15 | startActivity { LoginActivity.getLaunchIntent(it) } 16 | } 17 | 18 | fun navigateToQuote() { 19 | startActivity { QuoteActivity.getLaunchIntent(it) } 20 | } 21 | 22 | private fun startActivity(getIntentFunction: (context: Context) -> Intent) = 23 | appCompactActivityWeakReference.get()?.let { 24 | it.startActivity(getIntentFunction(it)) 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/APIIOException.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network 2 | 3 | import java.io.IOException 4 | 5 | class APIIOException(response: okhttp3.Response) : IOException() 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/AccessTokenProvider.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network; 2 | 3 | public interface AccessTokenProvider { 4 | 5 | String getAccessToken(); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/ApiClient.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network; 2 | 3 | import java.io.IOException; 4 | 5 | import retrofit2.Call; 6 | import retrofit2.Response; 7 | 8 | public class ApiClient { 9 | 10 | protected final T endpoint; 11 | 12 | public ApiClient(T endpoint) { 13 | this.endpoint = endpoint; 14 | } 15 | 16 | protected U evaluateCall(Call call) throws IOException { 17 | Response response = call.execute(); 18 | if (!response.isSuccessful()) { 19 | throw new APIIOException(response.raw()); 20 | } 21 | return response.body(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/EndpointFactory.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.jcminarro.authexample.UtilsKt; 6 | import com.jcminarro.authexample.internal.network.authorizator.AuthorizatedApi; 7 | import com.jcminarro.authexample.internal.network.authorizator.AuthorizatedApiInterceptor; 8 | import com.jcminarro.authexample.internal.network.authorizator.UnauthorizatedApiInterceptor; 9 | import com.jcminarro.authexample.internal.network.authorizator.UnauthorizatedApi; 10 | import com.jcminarro.authexample.internal.network.reauthorizate.ReauthorizatedApi; 11 | import com.jcminarro.authexample.internal.network.reauthorizate.ReauthorizatedApiInterceptor; 12 | import com.moczul.ok2curl.CurlInterceptor; 13 | import com.moczul.ok2curl.logger.Loggable; 14 | 15 | import java.util.concurrent.TimeUnit; 16 | 17 | import okhttp3.ConnectionPool; 18 | import okhttp3.OkHttpClient; 19 | import okhttp3.logging.HttpLoggingInterceptor; 20 | import retrofit2.Converter; 21 | import retrofit2.Retrofit; 22 | import retrofit2.converter.gson.GsonConverterFactory; 23 | 24 | public class EndpointFactory { 25 | 26 | private static final int CONNECT_TIMEOUT_SECONDS = 60; 27 | private static final int READ_TIMEOUT_SECONDS = 60; 28 | private static final ConnectionPool CONNECTION_POOL = new ConnectionPool(); 29 | 30 | private final String apiHost; 31 | private final AuthorizatedApiInterceptor authorizatedApiInterceptor; 32 | private final UnauthorizatedApiInterceptor unauthorizatedApiInterceptor; 33 | private final ReauthorizatedApiInterceptor reauthorizatedApiInterceptor; 34 | 35 | private EndpointFactory(String apiHost, 36 | AuthorizatedApiInterceptor authorizatedApiInterceptor, 37 | UnauthorizatedApiInterceptor unauthorizatedApiInterceptor, 38 | ReauthorizatedApiInterceptor reauthorizatedApiInterceptor) { 39 | this.apiHost = apiHost; 40 | this.authorizatedApiInterceptor = authorizatedApiInterceptor; 41 | this.unauthorizatedApiInterceptor = unauthorizatedApiInterceptor; 42 | this.reauthorizatedApiInterceptor = reauthorizatedApiInterceptor; 43 | } 44 | 45 | public T create(Class api) { 46 | return new Retrofit.Builder() 47 | .baseUrl(apiHost) 48 | .client(getDefaultHttpClient(api)) 49 | .addConverterFactory(getConverterFactory()) 50 | .build() 51 | .create(api); 52 | } 53 | 54 | private Converter.Factory getConverterFactory() { 55 | Gson gson = new GsonBuilder().create(); 56 | return GsonConverterFactory.create(gson); 57 | } 58 | 59 | private OkHttpClient getDefaultHttpClient(Class api) { 60 | OkHttpClient.Builder builder = new OkHttpClient.Builder() 61 | .connectionPool(CONNECTION_POOL) 62 | .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) 63 | .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS); 64 | addLoggingInterceptor(builder); 65 | addReauthorizatorInterceptorIfNeeded(api, builder); 66 | addAuthorizatorInterceptorIfNeeded(api, builder); 67 | addUnauthorizatorInterceptorIfNeeded(api, builder); 68 | return builder.build(); 69 | } 70 | 71 | private void addLoggingInterceptor(OkHttpClient.Builder builder) { 72 | builder.addInterceptor( 73 | new HttpLoggingInterceptor( 74 | new HttpLoggingInterceptor.Logger() { 75 | @Override 76 | public void log(String message) { 77 | UtilsKt.log(message); 78 | } 79 | }).setLevel(HttpLoggingInterceptor.Level.BODY)); 80 | builder.addNetworkInterceptor(new CurlInterceptor(new Loggable() { 81 | @Override 82 | public void log(String message) { 83 | UtilsKt.log("Curl", message); 84 | } 85 | })); 86 | } 87 | 88 | private void addAuthorizatorInterceptorIfNeeded(Class api, OkHttpClient.Builder builder) { 89 | if (api.isAnnotationPresent(AuthorizatedApi.class)) { 90 | if (authorizatedApiInterceptor == null) { 91 | throw new IllegalStateException("To build a " + api.getName() + " that is annotated with " + 92 | AuthorizatedApi.class.getName() + " you need to add a " + 93 | AuthorizatedApiInterceptor.class.getName() + " to the " + EndpointFactory.class.getName()); 94 | } 95 | builder.addInterceptor(authorizatedApiInterceptor); 96 | } 97 | } 98 | 99 | private void addUnauthorizatorInterceptorIfNeeded(Class api, OkHttpClient.Builder builder) { 100 | if (api.isAnnotationPresent(UnauthorizatedApi.class)) { 101 | if (unauthorizatedApiInterceptor == null) { 102 | throw new IllegalStateException("To build a " + api.getName() + " that is annotated with " + 103 | UnauthorizatedApi.class.getName() + " you need to add a " + 104 | UnauthorizatedApiInterceptor.class.getName() + " to the " + EndpointFactory.class.getName()); 105 | } 106 | builder.addInterceptor(unauthorizatedApiInterceptor); 107 | } 108 | } 109 | 110 | private void addReauthorizatorInterceptorIfNeeded(Class api, OkHttpClient.Builder builder) { 111 | if (api.isAnnotationPresent(ReauthorizatedApi.class)) { 112 | if (reauthorizatedApiInterceptor == null) { 113 | throw new IllegalStateException("To build a " + api.getName() + " that is annotated with " + 114 | ReauthorizatedApi.class.getName() + " you need to add a " + 115 | ReauthorizatedApiInterceptor.class.getName() + " to the " + EndpointFactory.class.getName()); 116 | } 117 | builder.addInterceptor(reauthorizatedApiInterceptor); 118 | } 119 | } 120 | 121 | public static class Builder { 122 | private String apiHost; 123 | private AuthorizatedApiInterceptor authorizatedApiInterceptor; 124 | private UnauthorizatedApiInterceptor unauthorizatedApiInterceptor; 125 | private ReauthorizatedApiInterceptor reauthorizatedApiInterceptor; 126 | 127 | public Builder(String apiHost) { 128 | this.apiHost = apiHost; 129 | } 130 | 131 | public Builder withAuthorizatedApiInterceptor(AuthorizatedApiInterceptor authorizatedApiInterceptor) { 132 | this.authorizatedApiInterceptor = authorizatedApiInterceptor; 133 | return this; 134 | } 135 | 136 | public Builder withUnaouthorizatedApiInterceptor(UnauthorizatedApiInterceptor unauthorizatedApiInterceptor) { 137 | this.unauthorizatedApiInterceptor = unauthorizatedApiInterceptor; 138 | return this; 139 | } 140 | 141 | public Builder withReauthorizatedApiInterceptor(ReauthorizatedApiInterceptor reauthorizatedApiInterceptor) { 142 | this.reauthorizatedApiInterceptor = reauthorizatedApiInterceptor; 143 | return this; 144 | } 145 | 146 | public EndpointFactory build() { 147 | return new EndpointFactory( 148 | apiHost, 149 | authorizatedApiInterceptor, 150 | unauthorizatedApiInterceptor, 151 | reauthorizatedApiInterceptor); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/OAuth.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network 2 | 3 | data class OAuth(val accessToken: String, val refreshToken: String) 4 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/SessionReauthorizer.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network; 2 | 3 | import com.jcminarro.authexample.internal.network.reauthorizate.Reauthorizer; 4 | import com.jcminarro.authexample.internal.repository.SessionRepository; 5 | 6 | import javax.inject.Inject; 7 | 8 | public class SessionReauthorizer implements Reauthorizer { 9 | 10 | private final SessionRepository sessionRepository; 11 | 12 | @Inject 13 | public SessionReauthorizer(SessionRepository sessionRepository) { 14 | this.sessionRepository = sessionRepository; 15 | } 16 | 17 | @Override 18 | public void reauthorize() { 19 | sessionRepository.refreshSession(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/authorizator/AuthorizatedApi.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.authorizator; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.TYPE) 10 | public @interface AuthorizatedApi { 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/authorizator/AuthorizatedApiInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.authorizator; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import com.jcminarro.authexample.internal.network.AccessTokenProvider; 6 | 7 | import java.io.IOException; 8 | 9 | import javax.inject.Inject; 10 | 11 | import okhttp3.Interceptor; 12 | import okhttp3.Request; 13 | import okhttp3.Response; 14 | 15 | public class AuthorizatedApiInterceptor implements Interceptor { 16 | 17 | private static final String HEADER_AUTH_KEY = "X-access-token"; 18 | 19 | private final AccessTokenProvider accessTokenProvider; 20 | 21 | @Inject 22 | public AuthorizatedApiInterceptor(AccessTokenProvider accessTokenProvider) { 23 | this.accessTokenProvider = accessTokenProvider; 24 | } 25 | 26 | @Override 27 | public Response intercept(@NonNull Chain chain) throws IOException { 28 | return chain.proceed(addAuthHeaderToken(chain.request())); 29 | } 30 | 31 | private Request addAuthHeaderToken(Request request) { 32 | return request 33 | .newBuilder() 34 | .addHeader(HEADER_AUTH_KEY, accessTokenProvider.getAccessToken()) 35 | .build(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/authorizator/UnauthorizatedApi.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.authorizator; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.TYPE) 10 | public @interface UnauthorizatedApi { 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/authorizator/UnauthorizatedApiInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.authorizator; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.io.IOException; 6 | 7 | import javax.inject.Inject; 8 | 9 | import okhttp3.Interceptor; 10 | import okhttp3.Response; 11 | 12 | public class UnauthorizatedApiInterceptor implements Interceptor { 13 | 14 | @Inject 15 | public UnauthorizatedApiInterceptor() { } 16 | 17 | @Override 18 | public Response intercept(@NonNull Chain chain) throws IOException { 19 | return chain.proceed(chain.request()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/login/LoginApiClient.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.login 2 | 3 | import com.jcminarro.authexample.internal.network.APIIOException 4 | import com.jcminarro.authexample.internal.network.ApiClient 5 | import com.jcminarro.authexample.internal.network.OAuth 6 | import javax.inject.Inject 7 | 8 | class LoginApiClient @Inject 9 | constructor(endpoint: LoginEndpoint) : ApiClient(endpoint) { 10 | 11 | @Throws(APIIOException::class) 12 | fun login(username: String, password: String): OAuth = 13 | map(evaluateCall(endpoint.login(LoginBody(username, password)))) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/login/LoginBody.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.login; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class LoginBody { 6 | private final @SerializedName("username") String username; 7 | private final @SerializedName("password") String password; 8 | 9 | public LoginBody(String username, String password) { 10 | this.username = username; 11 | this.password = password; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/login/LoginEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.login; 2 | 3 | import com.jcminarro.authexample.internal.network.authorizator.UnauthorizatedApi; 4 | 5 | import retrofit2.Call; 6 | import retrofit2.http.Body; 7 | import retrofit2.http.POST; 8 | 9 | @UnauthorizatedApi 10 | public interface LoginEndpoint { 11 | 12 | @POST("/login") 13 | Call login(@Body LoginBody loginBody); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/login/LoginResponse.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.login; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class LoginResponse { 6 | private final @SerializedName("accessToken") String accessToken; 7 | private final @SerializedName("refreshToken") String refreshToken; 8 | 9 | public LoginResponse(String accessToken, String refreshToken) { 10 | this.accessToken = accessToken; 11 | this.refreshToken = refreshToken; 12 | } 13 | 14 | public String getAccessToken() { 15 | return accessToken; 16 | } 17 | 18 | public String getRefreshToken() { 19 | return refreshToken; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/login/Mapper.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.login 2 | 3 | import com.jcminarro.authexample.internal.network.OAuth 4 | 5 | fun map(loginResponse: LoginResponse): OAuth = OAuth(loginResponse.accessToken, loginResponse.refreshToken) -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/quote/Mapper.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.quote 2 | 3 | import com.jcminarro.authexample.internal.model.Quote 4 | 5 | fun map(quoteResponse: QuoteResponse) = Quote(quoteResponse.author, quoteResponse.message) -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/quote/QuoteApiClient.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.quote 2 | 3 | import com.jcminarro.authexample.internal.model.Quote 4 | import com.jcminarro.authexample.internal.network.APIIOException 5 | import com.jcminarro.authexample.internal.network.ApiClient 6 | import javax.inject.Inject 7 | 8 | class QuoteApiClient @Inject 9 | constructor(endpoint: QuoteEndpoint) : ApiClient(endpoint) { 10 | 11 | @Throws(APIIOException::class) 12 | fun getRandomQuote(): Quote = 13 | map(evaluateCall(endpoint.randomQuote)) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/quote/QuoteEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.quote; 2 | 3 | import com.jcminarro.authexample.internal.network.authorizator.AuthorizatedApi; 4 | import com.jcminarro.authexample.internal.network.reauthorizate.ReauthorizatedApi; 5 | 6 | import retrofit2.Call; 7 | import retrofit2.http.GET; 8 | import retrofit2.http.Headers; 9 | 10 | @AuthorizatedApi 11 | @ReauthorizatedApi 12 | public interface QuoteEndpoint { 13 | 14 | @Headers("Content-Type: application/json") 15 | @GET("/quote") 16 | Call getRandomQuote(); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/quote/QuoteResponse.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.quote; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class QuoteResponse { 6 | private final @SerializedName("author") String author; 7 | private final @SerializedName("message") String message; 8 | 9 | public QuoteResponse(String author, String message) { 10 | this.author = author; 11 | this.message = message; 12 | } 13 | 14 | public String getAuthor() { 15 | return author; 16 | } 17 | 18 | public String getMessage() { 19 | return message; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/reauthorizate/ReauthorizatedApi.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.reauthorizate; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.TYPE) 10 | public @interface ReauthorizatedApi { 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/reauthorizate/ReauthorizatedApiInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.reauthorizate; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.io.IOException; 6 | 7 | import javax.inject.Inject; 8 | 9 | import okhttp3.Interceptor; 10 | import okhttp3.Response; 11 | 12 | public class ReauthorizatedApiInterceptor implements Interceptor { 13 | 14 | private static final int UNAUTHORIZED_HTTP_CODE = 401; 15 | private final Reauthorizer reauthorizer; 16 | 17 | @Inject 18 | public ReauthorizatedApiInterceptor(Reauthorizer reauthorizer) { 19 | this.reauthorizer = reauthorizer; 20 | } 21 | 22 | @Override 23 | public Response intercept(@NonNull Chain chain) throws IOException { 24 | Response response = chain.proceed(chain.request()); 25 | if (response.code() == UNAUTHORIZED_HTTP_CODE) { 26 | reauthorizer.reauthorize(); 27 | response = chain.proceed(chain.request()); 28 | } 29 | return response; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/reauthorizate/Reauthorizer.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.reauthorizate; 2 | 3 | public interface Reauthorizer { 4 | 5 | void reauthorize(); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/refresh/Mapper.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.refresh 2 | 3 | import com.jcminarro.authexample.internal.network.OAuth 4 | 5 | fun map(refreshResponse: RefreshResponse): OAuth = OAuth(refreshResponse.accessToken, refreshResponse.refreshToken) -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/refresh/RefreshApiClient.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.refresh 2 | 3 | import com.jcminarro.authexample.internal.network.APIIOException 4 | import com.jcminarro.authexample.internal.network.ApiClient 5 | import com.jcminarro.authexample.internal.network.OAuth 6 | import javax.inject.Inject 7 | 8 | class RefreshApiClient @Inject 9 | constructor(endpoint: RefreshEndpoint) : ApiClient(endpoint) { 10 | 11 | @Throws(APIIOException::class) 12 | fun refresh(refreshToken: String): OAuth = 13 | map(evaluateCall(endpoint.refreshTokens(RefreshBody(refreshToken)))) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/refresh/RefreshBody.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.refresh; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class RefreshBody { 6 | private final @SerializedName("refreshToken") String refreshToken; 7 | 8 | public RefreshBody(String refreshToken) { 9 | this.refreshToken = refreshToken; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/refresh/RefreshEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.refresh; 2 | 3 | import com.jcminarro.authexample.internal.network.authorizator.UnauthorizatedApi; 4 | 5 | import retrofit2.Call; 6 | import retrofit2.http.Body; 7 | import retrofit2.http.POST; 8 | 9 | @UnauthorizatedApi 10 | public interface RefreshEndpoint { 11 | 12 | @POST("/refresh") 13 | Call refreshTokens(@Body RefreshBody refreshBody); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/network/refresh/RefreshResponse.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.network.refresh; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class RefreshResponse { 6 | private final @SerializedName("accessToken") String accessToken; 7 | private final @SerializedName("refreshToken") String refreshToken; 8 | 9 | public RefreshResponse(String accessToken, String refreshToken) { 10 | this.accessToken = accessToken; 11 | this.refreshToken = refreshToken; 12 | } 13 | 14 | public String getAccessToken() { 15 | return accessToken; 16 | } 17 | 18 | public String getRefreshToken() { 19 | return refreshToken; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/presenter/BasePresenter.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.presenter; 2 | 3 | import com.jcminarro.authexample.internal.interactor.AsyncInteractor; 4 | import com.jcminarro.authexample.internal.interactor.InteractorExecutor; 5 | 6 | import java.lang.reflect.InvocationHandler; 7 | import java.lang.reflect.Method; 8 | import java.lang.reflect.Proxy; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.LinkedHashSet; 12 | import java.util.List; 13 | 14 | public class BasePresenter { 15 | 16 | /** 17 | * Represents the View component inside the Model View Presenter pattern. This interface must be 18 | * used as base interface for every View interface declared. 19 | */ 20 | public interface View { 21 | 22 | } 23 | 24 | private T view; 25 | private LinkedHashSet> viewImplementedInterfaces = new LinkedHashSet<>(); 26 | private InteractorExecutor executor; 27 | 28 | /** 29 | * Method called in the presenter lifecycle. Invoked when the component containing the presenter 30 | * is initialized. 31 | */ 32 | public void initialize() { 33 | 34 | } 35 | 36 | /** 37 | * Method called in the presenter lifecycle. Invoked when the component containing the presenter 38 | * is resumed. 39 | */ 40 | public void update() { 41 | 42 | } 43 | 44 | /** 45 | * Method called in the presenter lifecycle. Invoked when the component containing the presenter 46 | * is paused. 47 | */ 48 | public void pause() { 49 | 50 | } 51 | 52 | /** 53 | * Method called in the presenter lifecycle. Invoked when the component containing the presenter 54 | * is destroyed. 55 | */ 56 | public void destroy() { 57 | 58 | } 59 | 60 | /** 61 | * Returns the view configured in the presenter which real implementation is an Activity or 62 | * Fragment using this presenter. 63 | */ 64 | public final T getView() { 65 | return view; 66 | } 67 | 68 | /** 69 | * Configures the View instance used in this presenter as view. 70 | */ 71 | public void setView(T view) { 72 | this.view = view; 73 | } 74 | 75 | public void addViewInterfaces(Class[] interfaces) { 76 | viewImplementedInterfaces.addAll(Arrays.asList(interfaces)); 77 | } 78 | 79 | public void setExecutor(InteractorExecutor executor) { 80 | this.executor = executor; 81 | } 82 | 83 | /** 84 | * Changes the current view instance with a dynamic proxy to avoid real UI updates. 85 | */ 86 | public void resetView() { 87 | final List> viewClasses = getViewInterfaceClass(); 88 | InvocationHandler emptyHandler = new InvocationHandler() { 89 | @Override 90 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 91 | return null; 92 | } 93 | }; 94 | ClassLoader classLoader = viewClasses.get(0).getClassLoader(); 95 | this.view = (T) Proxy.newProxyInstance(classLoader, viewClasses.toArray(new Class[0]), emptyHandler); 96 | } 97 | 98 | protected void execute( 99 | AsyncInteractor interactor, 100 | I input, 101 | InteractorExecutor.Callback callback) { 102 | executor.execute(interactor, input, callback); 103 | } 104 | 105 | protected void execute( 106 | AsyncInteractor interactor, 107 | I input) { 108 | execute(interactor, input, new InteractorExecutor.NullCallback()); 109 | } 110 | 111 | private List> getViewInterfaceClass() { 112 | Class matchingClass = BasePresenter.View.class; 113 | List> interfaceClasses = new ArrayList<>(); 114 | 115 | for (Class interfaceCandidate : viewImplementedInterfaces) { 116 | if (matchingClass.isAssignableFrom(interfaceCandidate)) { 117 | interfaceClasses.add(interfaceCandidate); 118 | } 119 | } 120 | 121 | return interfaceClasses; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/presenter/Presenter.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.presenter; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Annotation to define the designed presenter 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.FIELD) 13 | public @interface Presenter { 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/presenter/lifecycle/PresenterAnnotationException.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.presenter.lifecycle; 2 | 3 | class PresenterAnnotationException extends RuntimeException { 4 | 5 | PresenterAnnotationException(String detailMessage) { 6 | super(detailMessage); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/presenter/lifecycle/PresenterLifecycleLinker.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.presenter.lifecycle; 2 | 3 | import android.app.Activity; 4 | import android.support.v4.app.Fragment; 5 | import android.view.View; 6 | 7 | import com.jcminarro.authexample.internal.interactor.InteractorExecutor; 8 | import com.jcminarro.authexample.internal.presenter.BasePresenter; 9 | import com.jcminarro.authexample.internal.presenter.Presenter; 10 | 11 | import java.lang.reflect.Field; 12 | import java.lang.reflect.Modifier; 13 | import java.util.Arrays; 14 | import java.util.Collection; 15 | import java.util.Collections; 16 | import java.util.HashSet; 17 | import java.util.Set; 18 | 19 | import javax.inject.Inject; 20 | 21 | public class PresenterLifecycleLinker { 22 | 23 | private final Set presenters = new HashSet<>(); 24 | private static final Collection> NON_PRESENTED_CLASSES = 25 | Collections.unmodifiableCollection( 26 | Arrays.asList(Activity.class, Fragment.class, View.class, Object.class)); 27 | private final InteractorExecutor executor; 28 | 29 | @Inject 30 | public PresenterLifecycleLinker(InteractorExecutor executor) { 31 | this.executor = executor; 32 | } 33 | 34 | public void initialize(BasePresenter.View view) { 35 | addAnnotatedPresenters(view); 36 | setView(view); 37 | setExecutor(); 38 | initializePresenters(); 39 | } 40 | 41 | /** 42 | * Initializes all the already registered presenters lifecycle. 43 | */ 44 | private void initializePresenters() { 45 | for (BasePresenter presenter : presenters) { 46 | presenter.initialize(); 47 | } 48 | } 49 | 50 | /** 51 | * Updates all the already registered presenters lifecycle and updates the view instance 52 | * associated to these presenters. 53 | * 54 | * @param view to be updated for every registered presenter. 55 | */ 56 | public void updatePresenters(BasePresenter.View view) { 57 | if (view == null) { 58 | throw new IllegalArgumentException( 59 | "The view instance used to update the presenters can't be null"); 60 | } 61 | for (BasePresenter presenter : presenters) { 62 | presenter.setView(view); 63 | presenter.update(); 64 | } 65 | } 66 | 67 | /** 68 | * Pauses all the already registered presenters lifecycle. 69 | */ 70 | public void pausePresenters() { 71 | for (BasePresenter presenter : presenters) { 72 | presenter.pause(); 73 | presenter.resetView(); 74 | } 75 | } 76 | 77 | /** 78 | * Destroys all the already registered presenters lifecycle. 79 | */ 80 | public void destroyPresenters() { 81 | for (BasePresenter presenter : presenters) { 82 | presenter.destroy(); 83 | } 84 | } 85 | 86 | private void addAnnotatedPresenters(Object source) { 87 | addAnnotatedPresenters(source.getClass(), source); 88 | } 89 | 90 | private void addAnnotatedPresenters(Class clazz, Object source) { 91 | if (isOneOfOurNonPresentedClass(clazz)) { 92 | return; 93 | } 94 | addAnnotatedPresenters(clazz.getSuperclass(), source); 95 | 96 | for (Field field : clazz.getDeclaredFields()) { 97 | if (field.isAnnotationPresent(Presenter.class)) { 98 | if (Modifier.isPrivate(field.getModifiers())) { 99 | throw new PresenterNotAccessibleException( 100 | "Presenter must be accessible for this class. The visibility modifier used can't be" 101 | + " private"); 102 | } else { 103 | try { 104 | field.setAccessible(true); 105 | BasePresenter presenter = (BasePresenter) field.get(source); 106 | registerPresenter(presenter); 107 | presenter.addViewInterfaces(clazz.getInterfaces()); 108 | field.setAccessible(false); 109 | } catch (IllegalAccessException e) { 110 | PresenterNotAccessibleException exception = new PresenterNotAccessibleException( 111 | "The presenter " + field.getName() + " into class " + clazz.getCanonicalName() 112 | + " can not be accessed"); 113 | exception.initCause(e); 114 | throw exception; 115 | } catch (ClassCastException e) { 116 | throw new PresenterAnnotationException( 117 | "The annotation " + Presenter.class.getCanonicalName() + " is being used on an object" + 118 | " that is not a " + BasePresenter.class.getCanonicalName() + " on the class " + 119 | clazz.getCanonicalName() 120 | ); 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | private boolean isOneOfOurNonPresentedClass(Class clazz) { 128 | return NON_PRESENTED_CLASSES.contains(clazz); 129 | } 130 | 131 | private void registerPresenter(BasePresenter presenter) { 132 | if (presenter == null) { 133 | throw new IllegalArgumentException("The presenter instance to be registered can't be null"); 134 | } 135 | presenters.add(presenter); 136 | } 137 | 138 | private void setView(BasePresenter.View view) { 139 | for (BasePresenter presenter : presenters) { 140 | presenter.setView(view); 141 | } 142 | } 143 | 144 | private void setExecutor() { 145 | for (BasePresenter presenter : presenters) { 146 | presenter.setExecutor(executor); 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/presenter/lifecycle/PresenterNotAccessibleException.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.presenter.lifecycle; 2 | 3 | class PresenterNotAccessibleException extends RuntimeException { 4 | 5 | PresenterNotAccessibleException(String detailMessage) { 6 | super(detailMessage); 7 | } 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/repository/QuoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.repository 2 | 3 | import com.jcminarro.authexample.internal.network.APIIOException 4 | import com.jcminarro.authexample.internal.network.quote.QuoteApiClient 5 | import javax.inject.Inject 6 | 7 | class QuoteRepository @Inject constructor(private val quoteApiClient: QuoteApiClient) { 8 | 9 | @Throws(APIIOException::class) 10 | fun getRandomQuote() = quoteApiClient.getRandomQuote() 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/internal/repository/SessionRepository.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.internal.repository; 2 | 3 | import com.jcminarro.authexample.internal.localdatasource.SessionDatasource; 4 | import com.jcminarro.authexample.internal.network.APIIOException; 5 | import com.jcminarro.authexample.internal.network.OAuth; 6 | import com.jcminarro.authexample.internal.network.login.LoginApiClient; 7 | import com.jcminarro.authexample.internal.network.refresh.RefreshApiClient; 8 | 9 | import javax.inject.Inject; 10 | 11 | public class SessionRepository { 12 | 13 | private final LoginApiClient loginApiClient; 14 | private final RefreshApiClient refreshApiClient; 15 | private final SessionDatasource sessionDatasource; 16 | 17 | @Inject 18 | public SessionRepository( 19 | LoginApiClient loginApiClient, 20 | RefreshApiClient refreshApiClient, 21 | SessionDatasource sessionDatasource) { 22 | this.loginApiClient = loginApiClient; 23 | this.refreshApiClient = refreshApiClient; 24 | this.sessionDatasource = sessionDatasource; 25 | } 26 | 27 | public boolean login(String username, String password) throws APIIOException { 28 | sessionDatasource.storeOAuthSession(loginApiClient.login(username, password)); 29 | return true; 30 | } 31 | 32 | public boolean refreshSession() { 33 | OAuth oAuth = sessionDatasource.getOAuthSession(); 34 | if (oAuth != null) { 35 | try { 36 | sessionDatasource.storeOAuthSession(refreshApiClient.refresh(oAuth.getRefreshToken())); 37 | return true; 38 | } catch (Exception e) { 39 | e.printStackTrace(); 40 | } 41 | } 42 | return false; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/login/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.login; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.view.KeyEvent; 6 | import android.view.View; 7 | import android.view.inputmethod.EditorInfo; 8 | import android.widget.AutoCompleteTextView; 9 | import android.widget.EditText; 10 | import android.widget.TextView; 11 | import android.widget.Toast; 12 | 13 | import com.jcminarro.authexample.R; 14 | import com.jcminarro.authexample.internal.di.component.DaggerLoginComponent; 15 | import com.jcminarro.authexample.internal.di.component.LoginComponent; 16 | import com.jcminarro.authexample.internal.di.injectablebase.BaseInjectionActivity; 17 | import com.jcminarro.authexample.internal.di.module.ActivityModule; 18 | import com.jcminarro.authexample.internal.presenter.Presenter; 19 | 20 | import javax.inject.Inject; 21 | 22 | import butterknife.BindView; 23 | import butterknife.OnClick; 24 | 25 | public class LoginActivity extends BaseInjectionActivity implements LoginPresenter.View { 26 | 27 | @BindView(R.id.username) AutoCompleteTextView username; 28 | @BindView(R.id.password) EditText password; 29 | @BindView(R.id.login_progress) View loading; 30 | @BindView(R.id.login_form) View loginForm; 31 | 32 | @Inject 33 | @Presenter LoginPresenter presenter; 34 | 35 | public static Intent getLaunchIntent(Context context) { 36 | return new Intent(context, LoginActivity.class); 37 | } 38 | 39 | @Override 40 | protected void onConfigureViews() { 41 | super.onConfigureViews(); 42 | password.setOnEditorActionListener(new TextView.OnEditorActionListener() { 43 | @Override 44 | public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { 45 | if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { 46 | onLogin(); 47 | return true; 48 | } 49 | return false; 50 | } 51 | }); 52 | } 53 | 54 | @Override 55 | protected void initDI() { 56 | activityComponent = DaggerLoginComponent 57 | .builder() 58 | .appComponent(getAppComponent()) 59 | .activityModule(new ActivityModule(this)) 60 | .build(); 61 | activityComponent.inject(this); 62 | } 63 | 64 | private void onLogin() { 65 | presenter.login(username.getText().toString(), 66 | password.getText().toString()); 67 | } 68 | 69 | @Override 70 | protected int getLayout() { 71 | return R.layout.activity_login; 72 | } 73 | 74 | @OnClick(R.id.signIn) 75 | public void onLoginClick() { 76 | onLogin(); 77 | } 78 | 79 | @Override 80 | public void close() { 81 | onBackPressed(); 82 | } 83 | 84 | @Override 85 | public void showError() { 86 | Toast.makeText(this, R.string.error_invalid_credential, Toast.LENGTH_LONG).show(); 87 | } 88 | 89 | @Override 90 | public void showLogin() { 91 | loginForm.setVisibility(View.VISIBLE); 92 | } 93 | 94 | @Override 95 | public void hideLogin() { 96 | loginForm.setVisibility(View.GONE); 97 | } 98 | 99 | @Override 100 | public void showLoading() { 101 | loading.setVisibility(View.VISIBLE); 102 | } 103 | 104 | @Override 105 | public void hideLoading() { 106 | loading.setVisibility(View.GONE); 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/login/LoginInteractor.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.login; 2 | 3 | import com.jcminarro.authexample.internal.interactor.AsyncInteractor; 4 | import com.jcminarro.authexample.internal.network.APIIOException; 5 | import com.jcminarro.authexample.internal.repository.SessionRepository; 6 | 7 | import javax.inject.Inject; 8 | 9 | public class LoginInteractor implements AsyncInteractor { 10 | 11 | private final SessionRepository sessionRepository; 12 | 13 | @Inject 14 | public LoginInteractor(SessionRepository sessionRepository) { 15 | this.sessionRepository = sessionRepository; 16 | } 17 | 18 | @Override 19 | public void execute(Input input, Callback callback) { 20 | try { 21 | callback.onSuccess(sessionRepository.login(input.username, input.password)); 22 | } catch (APIIOException e) { 23 | callback.onError(e); 24 | } 25 | } 26 | 27 | public static class Input { 28 | private final String username; 29 | private final String password; 30 | 31 | public Input(String username, String password) { 32 | this.username = username; 33 | this.password = password; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/login/LoginPresenter.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.login; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.jcminarro.authexample.internal.interactor.InteractorExecutor; 6 | import com.jcminarro.authexample.internal.navigator.Navigator; 7 | import com.jcminarro.authexample.internal.presenter.BasePresenter; 8 | 9 | import javax.inject.Inject; 10 | 11 | public class LoginPresenter extends BasePresenter { 12 | 13 | private final LoginInteractor loginInteractor; 14 | private final Navigator navigator; 15 | 16 | @Inject 17 | public LoginPresenter(LoginInteractor loginInteractor, Navigator navigator) { 18 | this.loginInteractor = loginInteractor; 19 | this.navigator = navigator; 20 | } 21 | 22 | @Override 23 | public void update() { 24 | super.update(); 25 | showLoginStatus(); 26 | } 27 | 28 | private void showLoginStatus() { 29 | getView().showLogin(); 30 | getView().hideLoading(); 31 | } 32 | 33 | public void login(String username, String password) { 34 | if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) { 35 | onLoginError(); 36 | } else { 37 | performLogin(username, password); 38 | } 39 | } 40 | 41 | private void performLogin(String username, String password) { 42 | showLoadingStatus(); 43 | execute(loginInteractor, 44 | new LoginInteractor.Input(username, password), 45 | new InteractorExecutor.Callback() { 46 | @Override 47 | public void onSuccess(Boolean isLoggedIn) { 48 | if (isLoggedIn) { 49 | onLoggedIn(); 50 | } else { 51 | onLoginError(); 52 | } 53 | } 54 | 55 | @Override 56 | public void onError(Exception error) { 57 | onLoginError(); 58 | } 59 | }); 60 | } 61 | 62 | private void showLoadingStatus() { 63 | getView().showLoading(); 64 | getView().hideLogin(); 65 | } 66 | 67 | private void onLoginError() { 68 | showLoginStatus(); 69 | getView().showError(); 70 | } 71 | 72 | private void onLoggedIn() { 73 | getView().close(); 74 | navigator.navigateToQuote(); 75 | } 76 | 77 | interface View extends BasePresenter.View { 78 | 79 | void close(); 80 | 81 | void showError(); 82 | 83 | void showLogin(); 84 | 85 | void hideLogin(); 86 | 87 | void showLoading(); 88 | 89 | void hideLoading(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/quote/GetQuoteInteractor.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.quote; 2 | 3 | import com.jcminarro.authexample.internal.interactor.AsyncInteractor; 4 | import com.jcminarro.authexample.internal.model.Quote; 5 | import com.jcminarro.authexample.internal.network.APIIOException; 6 | import com.jcminarro.authexample.internal.repository.QuoteRepository; 7 | 8 | import javax.inject.Inject; 9 | 10 | public class GetQuoteInteractor implements AsyncInteractor { 11 | 12 | private final QuoteRepository quoteRepository; 13 | 14 | @Inject 15 | public GetQuoteInteractor(QuoteRepository quoteRepository) { 16 | this.quoteRepository = quoteRepository; 17 | } 18 | 19 | @Override 20 | public void execute(Void input, Callback callback) { 21 | try { 22 | callback.onSuccess(quoteRepository.getRandomQuote()); 23 | } catch (APIIOException e) { 24 | callback.onError(e); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/quote/QuoteActivity.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.quote; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.view.View; 6 | import android.widget.TextView; 7 | import android.widget.Toast; 8 | 9 | import com.jcminarro.authexample.R; 10 | import com.jcminarro.authexample.internal.di.component.DaggerQuoteComponent; 11 | import com.jcminarro.authexample.internal.di.component.QuoteComponent; 12 | import com.jcminarro.authexample.internal.di.injectablebase.BaseInjectionActivity; 13 | import com.jcminarro.authexample.internal.di.module.ActivityModule; 14 | import com.jcminarro.authexample.internal.presenter.Presenter; 15 | 16 | import javax.inject.Inject; 17 | 18 | import butterknife.BindView; 19 | import butterknife.OnClick; 20 | 21 | public class QuoteActivity extends BaseInjectionActivity implements QuotePresenter.View { 22 | 23 | @BindView(R.id.loading) View loading; 24 | @BindView(R.id.author) TextView authorView; 25 | @BindView(R.id.message) TextView messageView; 26 | 27 | @Inject 28 | @Presenter QuotePresenter presenter; 29 | 30 | public static Intent getLaunchIntent(Context context) { 31 | return new Intent(context, QuoteActivity.class); 32 | } 33 | 34 | @Override 35 | protected void initDI() { 36 | activityComponent = DaggerQuoteComponent 37 | .builder() 38 | .appComponent(getAppComponent()) 39 | .activityModule(new ActivityModule(this)) 40 | .build(); 41 | activityComponent.inject(this); 42 | } 43 | 44 | @Override 45 | protected int getLayout() { 46 | return R.layout.activity_quote; 47 | } 48 | 49 | @Override 50 | public void showQuote(String author, String message) { 51 | authorView.setText(author); 52 | messageView.setText(message); 53 | } 54 | 55 | @Override 56 | public void showError() { 57 | Toast.makeText(this, R.string.generic_error, Toast.LENGTH_LONG).show(); 58 | } 59 | 60 | @Override 61 | public void showLoading() { 62 | loading.setVisibility(View.VISIBLE); 63 | } 64 | 65 | @Override 66 | public void hideLoading() { 67 | loading.setVisibility(View.GONE); 68 | } 69 | 70 | @OnClick(R.id.getQuote) 71 | public void onGetQuoteClick() { 72 | presenter.getRandomQuote(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/quote/QuotePresenter.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.quote; 2 | 3 | import com.jcminarro.authexample.internal.interactor.InteractorExecutor; 4 | import com.jcminarro.authexample.internal.model.Quote; 5 | import com.jcminarro.authexample.internal.presenter.BasePresenter; 6 | 7 | import javax.inject.Inject; 8 | 9 | public class QuotePresenter extends BasePresenter { 10 | 11 | private final GetQuoteInteractor getQuoteInteractor; 12 | 13 | @Inject 14 | public QuotePresenter(GetQuoteInteractor getQuoteInteractor) { 15 | this.getQuoteInteractor = getQuoteInteractor; 16 | } 17 | 18 | @Override 19 | public void initialize() { 20 | super.initialize(); 21 | getRandomQuote(); 22 | } 23 | 24 | public void getRandomQuote() { 25 | getView().showLoading(); 26 | execute(getQuoteInteractor, null, new InteractorExecutor.Callback() { 27 | @Override 28 | public void onSuccess(Quote quote) { 29 | renderQuote(quote); 30 | } 31 | 32 | @Override 33 | public void onError(Exception error) { 34 | renderError(); 35 | } 36 | }); 37 | } 38 | 39 | private void renderError() { 40 | getView().hideLoading(); 41 | getView().showError(); 42 | } 43 | 44 | private void renderQuote(Quote quote) { 45 | getView().hideLoading(); 46 | getView().showQuote(quote.getAuthor(), quote.getMessage()); 47 | } 48 | 49 | interface View extends BasePresenter.View { 50 | 51 | void showQuote(String author, String message); 52 | 53 | void showError(); 54 | 55 | void showLoading(); 56 | 57 | void hideLoading(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/startup/RefreshSessionInteractor.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.startup; 2 | 3 | import com.jcminarro.authexample.internal.interactor.AsyncInteractor; 4 | import com.jcminarro.authexample.internal.repository.SessionRepository; 5 | 6 | import javax.inject.Inject; 7 | 8 | public class RefreshSessionInteractor implements AsyncInteractor { 9 | 10 | private final SessionRepository sessionRepository; 11 | 12 | @Inject 13 | public RefreshSessionInteractor(SessionRepository sessionRepository) { 14 | this.sessionRepository = sessionRepository; 15 | } 16 | 17 | @Override 18 | public void execute(Void input, Callback callback) { 19 | callback.onSuccess(sessionRepository.refreshSession()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/startup/StartUpActivity.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.startup; 2 | 3 | import com.jcminarro.authexample.internal.di.component.DaggerStartUpComponent; 4 | import com.jcminarro.authexample.internal.di.component.StartUpComponent; 5 | import com.jcminarro.authexample.internal.di.injectablebase.BaseInjectionActivity; 6 | import com.jcminarro.authexample.internal.di.module.ActivityModule; 7 | import com.jcminarro.authexample.internal.presenter.Presenter; 8 | 9 | import javax.inject.Inject; 10 | 11 | public class StartUpActivity extends BaseInjectionActivity 12 | implements StartUpPresenter.View { 13 | 14 | @Inject 15 | @Presenter StartUpPresenter presenter; 16 | 17 | @Override 18 | protected void initDI() { 19 | activityComponent = DaggerStartUpComponent 20 | .builder() 21 | .appComponent(getAppComponent()) 22 | .activityModule(new ActivityModule(this)) 23 | .build(); 24 | activityComponent.inject(this); 25 | } 26 | 27 | @Override 28 | protected int getLayout() { 29 | return 0; 30 | } 31 | 32 | @Override 33 | public void close() { 34 | onBackPressed(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/jcminarro/authexample/startup/StartUpPresenter.java: -------------------------------------------------------------------------------- 1 | package com.jcminarro.authexample.startup; 2 | 3 | import com.jcminarro.authexample.internal.interactor.InteractorExecutor; 4 | import com.jcminarro.authexample.internal.navigator.Navigator; 5 | import com.jcminarro.authexample.internal.presenter.BasePresenter; 6 | 7 | import javax.inject.Inject; 8 | 9 | public class StartUpPresenter extends BasePresenter { 10 | 11 | private final RefreshSessionInteractor refreshSessionInteractor; 12 | private final Navigator navigator; 13 | 14 | @Inject 15 | public StartUpPresenter(RefreshSessionInteractor refreshSessionInteractor, Navigator navigator) { 16 | this.refreshSessionInteractor = refreshSessionInteractor; 17 | this.navigator = navigator; 18 | } 19 | 20 | @Override 21 | public void update() { 22 | super.update(); 23 | execute(refreshSessionInteractor, 24 | null, 25 | new InteractorExecutor.SuccessCallback() { 26 | 27 | @Override 28 | public void onSuccess(Boolean isRefreshedSession) { 29 | if (isRefreshedSession) { 30 | navigateToMain(); 31 | } else { 32 | navigateToLogin(); 33 | } 34 | getView().close(); 35 | } 36 | }); 37 | } 38 | 39 | private void navigateToLogin() { 40 | navigator.navigateToLogin(); 41 | } 42 | 43 | private void navigateToMain() { 44 | navigator.navigateToQuote(); 45 | } 46 | 47 | interface View extends BasePresenter.View { 48 | 49 | void close(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | 23 | 28 | 29 | 35 | 36 | 40 | 41 | 49 | 50 | 51 | 55 | 56 | 68 | 69 | 70 |