Repository: rheinfabrik/Heimdall.droid Branch: master Commit: 855d253c682c Files: 61 Total size: 91.4 KB Directory structure: gitextract_8ir_jlm8/ ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── library/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── de/ │ │ └── rheinfabrik/ │ │ └── heimdall2/ │ │ ├── OAuth2AccessToken.kt │ │ ├── OAuth2AccessTokenManager.kt │ │ ├── OAuth2AccessTokenStorage.kt │ │ └── grants/ │ │ ├── OAuth2AuthorizationCodeGrant.kt │ │ ├── OAuth2ClientCredentialsGrant.kt │ │ ├── OAuth2Grant.kt │ │ ├── OAuth2ImplicitGrant.kt │ │ ├── OAuth2RefreshAccessTokenGrant.kt │ │ └── OAuth2ResourceOwnerPasswordCredentialsGrant.kt │ └── test/ │ ├── java/ │ │ └── de/ │ │ └── rheinfabrik/ │ │ └── heimdall2/ │ │ ├── OAuth2AccessTokenIsExpiredTest.kt │ │ ├── OAuth2AccessTokenManagerGetValidAccessTokenTest.kt │ │ ├── OAuth2AccessTokenManagerGrantNewAccessTokenTest.kt │ │ ├── OAuth2AccessTokenManagerTest.kt │ │ ├── OAuth2AccessTokenSerializationTest.kt │ │ └── grants/ │ │ ├── OAuth2AuthorizationCodeGrantTest.kt │ │ ├── OAuth2ClientCredentialsGrantTest.kt │ │ ├── OAuth2ImplicitGrantTest.kt │ │ ├── OAuth2RefreshAccessTokenGrantTest.kt │ │ └── OAuth2ResourceOwnerPasswordCredentialsGrantTest.kt │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── sample/ │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── de/ │ │ └── rheinfabrik/ │ │ └── heimdalldroid/ │ │ ├── TraktTvAPIConfiguration.java │ │ ├── actvities/ │ │ │ ├── LoginActivity.java │ │ │ └── MainActivity.java │ │ ├── adapter/ │ │ │ ├── TraktTvListsRecyclerViewAdapter.java │ │ │ └── viewholder/ │ │ │ └── TraktTvListViewHolder.java │ │ ├── network/ │ │ │ ├── TraktTvApiFactory.java │ │ │ ├── TraktTvApiService.java │ │ │ ├── models/ │ │ │ │ ├── AccessTokenRequestBody.java │ │ │ │ ├── RefreshTokenRequestBody.java │ │ │ │ ├── RevokeAccessTokenBody.java │ │ │ │ └── TraktTvList.java │ │ │ └── oauth2/ │ │ │ ├── TraktTvAuthorizationCodeGrant.java │ │ │ ├── TraktTvOauth2AccessTokenManager.java │ │ │ └── TraktTvRefreshAccessTokenGrant.java │ │ └── utils/ │ │ ├── AlertDialogFactory.java │ │ ├── IntentFactory.java │ │ └── SharedPreferencesOAuth2AccessTokenStorage.java │ └── res/ │ ├── layout/ │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ └── item_view_trakt_tv_list.xml │ ├── menu/ │ │ └── menu_main.xml │ └── values/ │ ├── colors.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.ap_ bin/ gen/ classes/ gen-external-apklibs/ # Eclipse project files .classpath .project .metadata .settings # IntelliJ files .idea *.iml # OSX files .DS_Store # Windows files Thumbs.db # vi swap files *.swp # backup files *.bak # Files for the Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ # Gradle files .gradle/ build/ .gradle #maven files target/ /null # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ #Log Files *.log sample/src/main/java/de/rheinfabrik/heimdalldroid/network/TraktTvAPIConfiguration.java ================================================ FILE: .travis.yml ================================================ language: android sudo: required jdk: oraclejdk8 env: global: - ANDROID_API_LEVEL=30 - ANDROID_BUILD_TOOLS_VERSION=30.0.2 - ANDROID_ABI=armeabi-v7a android: components: - tools - platform-tools - extra-android-m2repository licenses: - 'android-sdk-preview-license-52d11cd2' - 'android-sdk-license-.+' - 'google-gdk-license-.+' before_install: - touch $HOME/.android/repositories.cfg - yes | sdkmanager "platforms;android-30" - yes | sdkmanager "build-tools;30.0.2" before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ cache: directories: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ - $HOME/.android/build-cache before_script: - chmod +x gradlew script: - ./gradlew library:test ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2015 B264 GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Deprecated Project ⚠️ This project is no longer maintained This repository is deprecated and will no longer receive updates, bug fixes, or support. # Heimdall Heimdall is an [OAuth 2.0](https://tools.ietf.org/html/rfc6749) client specifically designed for easy usage and high flexibility. It supports all grants as described in [Section 4](https://tools.ietf.org/html/rfc6749#section-4) as well as refreshing an access token as described in [Section 6](https://tools.ietf.org/html/rfc6749#section-6) of the [The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749) specification. This library makes use of [RxJava](https://github.com/ReactiveX/RxJava). Therefore you should be familar with [Observables](https://github.com/ReactiveX/RxJava/wiki/Observable). If you are an iOS Developer then please take a look at the [Swift version of Heimdall](https://github.com/trivago/Heimdallr.swift). [![JitPack.io](https://jitpack.io/v/trivago/Heimdall.droid.svg)](https://jitpack.io/#trivago/Heimdall.droid) [![Travis Ci](https://travis-ci.org/trivago/Heimdall.droid.svg?branch=master)](https://travis-ci.org/trivago/Heimdall.droid) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Heimdall.droid-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/2016) [![Api](https://img.shields.io/badge/API-9%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=9) ## Installation Heimdall is ready to be used via [jitpack.io](https://jitpack.io/#rheinfabrik/Heimdall.droid). Simply add the following code to your root `build.gradle`: ```groovy allprojects { repositories { jcenter() maven { url "https://jitpack.io" } } } ``` Now add the gradle dependency in your application's `build.gradle`: ```groovy dependencies { compile 'com.github.rheinfabrik:Heimdall.droid:{latest_version}' } ``` ## Examples Heimdall's main class is the `OAuth2AccessTokenManager`. It is responsible for retrieving a new access token and keeping it valid by refreshing it. In order to initialize an `OAuth2AccessTokenManager` instance, you need to pass an object implementing the `OAuth2AccessTokenStorage` interface. You can use the predefined `SharedPreferencesOAuth2AccessTokenStorage` if it suits your needs. Make sure that your `OAuth2AccessTokenStorage` is as secure as possible! ```java SharedPreferencesOAuth2AccessTokenStorage storage = new SharedPreferencesOAuth2AccessTokenStorage<>(mySharedPreferences, OAuth2AccessToken.class); OAuth2AccessTokenManager<> manager = new OAuth2AccessTokenManager(storage); ``` On your manager instance you can now call `grantNewAccessToken(grant)` to receive a new access token. The grant instance you pass must implement the `OAuth2Grant` interface and your actual server call. Here is an example of an `OAuth2ResourceOwnerPasswordCredentialsGrant`. ```java public class MyOAuth2Grant extends OAuth2ResourceOwnerPasswordCredentialsGrant { // Constructor @Override public Observable grantNewAccessToken() { // Create the network request based on the username, the password and the grant type. // You can use Retrofit to make things easier. } } ``` Your manager instance also has a method called `getValidAccessToken(refreshGrant)`. This is probably the main reason we build this library. It firstly checks if the stored access token is expired and then either emits the unexpired one or refreshs it if it is expired using the passed refresh grant. Here is an example of an `OAuth2RefreshAccessTokenGrant`. ```java public class MyOAuth2Grant extends OAuth2RefreshAccessTokenGrant { // Constructor @Override public Observable grantNewAccessToken() { // Create the network request based on the grant type and the refresh token. // You can use Retrofit to make things easier. } } ``` Mostly you will use the `OAuth2AuthorizationCodeGrant` to authorize the user via a third party service such as Trakt.tv. The implemention of a grant authorizing with Trakt.tv might look as following: ```java public final class TraktTVAuthorizationCodeGrant extends OAuth2AuthorizationCodeGrant { public String clientSecret; @Override public Uri buildAuthorizationUri() { return Uri.parse("https://trakt.tv/oauth/authorize") .buildUpon() .appendQueryParameter("client_id", clientId) .appendQueryParameter("redirect_uri", redirectUri) .appendQueryParameter("response_type", RESPONSE_TYPE).build(); } @Override public Observable exchangeTokenForCode(String code) { // Create the network request based on the grant type, clientSecret and the retrieved code. // You can use Retrofit to make things easier. } } ``` Using that grant with an Android WebView might look like this (please note that we use [Retrolambda](https://github.com/evant/gradle-retrolambda) here): ```java // Create the grant TraktTVAuthorizationCodeGrant grant = new TraktTVAuthorizationCodeGrant(); grant.clientSecret = "secret" grant.clientId = "id" grant.redirectUri = "uri" // Set up web view loading webView.setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); // Tell the grant we loaded an url grant.onUrlLoadedCommand.onNext(Uri.parse(url)); } }); // Load the authorization url once build grant.authorizationUri() .map(Uri::parse) .observeOn(AndroidSchedulers.mainThread()) .subscribe(myWebView::load) // Start the authorization process grant.grantNewAccessToken() .subscribe(token -> Log.d("Heimdall", "New token: " + token)) ``` ## Sample Application Please also check out our sample application which performs an authorization against [trakt.tv](https://trakt.tv/) and displays a simple list of the user's watchlists. **Note:** In order to build the sample by yourself you have to [create a new application on trakt.tv](https://trakt.tv/oauth/applications/new) and add the credentials wherever `TraktTvAPIConfiguration.java` is used. ## About Heimdall was built by [trivago](http://www.trivago.com) :factory: ## License Heimdall is licensed under Apache Version 2.0. ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.0.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Tue Oct 24 09:19:37 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true android.enableJetifier=true android.useAndroidX=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: library/.gitignore ================================================ /build ================================================ FILE: library/build.gradle ================================================ apply plugin: 'kotlin' apply plugin: 'groovy' apply plugin: 'maven' buildscript { ext.kotlin_version = '1.4.10' repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } jar.archiveName = "Heimdall.Droid.jar" group = 'com.github.rheinfabrik' repositories { mavenCentral() jcenter() } dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // Rx implementation 'io.reactivex.rxjava2:rxjava:2.2.19' // GSON implementation 'com.google.code.gson:gson:2.8.6' // Testing testImplementation 'junit:junit:4.13' testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "org.mockito:mockito-inline:3.5.11" } compileKotlin { kotlinOptions { jvmTarget = "1.8" } } compileTestKotlin { kotlinOptions { jvmTarget = "1.8" } } ================================================ FILE: library/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/gilo/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: library/src/main/java/de/rheinfabrik/heimdall2/OAuth2AccessToken.kt ================================================ package de.rheinfabrik.heimdall2 import com.google.gson.annotations.SerializedName import java.io.Serializable import java.util.Calendar data class OAuth2AccessToken( /** * REQUIRED * The type of the token issued as described in https://tools.ietf.org/html/rfc6749#section-7.1. * Value is case insensitive. */ @SerializedName("token_type") val tokenType: String = "", /** * REQUIRED * The access token issued by the authorization server. */ @SerializedName("access_token") val accessToken: String = "", /** * OPTIONAL * The refresh token, which can be used to obtain new * access tokens using the same authorization grant as described * in https://tools.ietf.org/html/rfc6749#section-6. */ @SerializedName("refresh_token") val refreshToken: String? = null, /** * RECOMMENDED * The lifetime in seconds of the access token. For * example, the value "3600" denotes that the access token will * expire in one hour from the time the response was generated. * If omitted, the authorization server SHOULD provide the * expiration time via other means or document the default value. */ @SerializedName("expires_in") val expiresIn: Int? = null, /** * The expiration date used by Heimdall. */ @SerializedName("heimdall_expiration_date") val expirationDate: Calendar? = null ) : Serializable { // Public API /** * Returns whether the access token expired or not. * * @return True if expired. Otherwise false. */ fun isExpired(): Boolean = expirationDate != null && Calendar.getInstance().after(expirationDate) } ================================================ FILE: library/src/main/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenManager.kt ================================================ package de.rheinfabrik.heimdall2 import de.rheinfabrik.heimdall2.grants.OAuth2Grant import de.rheinfabrik.heimdall2.grants.OAuth2RefreshAccessTokenGrant import io.reactivex.Single import java.util.Calendar open class OAuth2AccessTokenManager( private val mStorage: OAuth2AccessTokenStorage ) { // Public API /** * Returns the underlying storage. * * @return - An OAuth2AccessTokenStorage. */ fun getStorage(): OAuth2AccessTokenStorage = mStorage /** * Grants a new access token using the given OAuth2 grant. * * @param grant A class implementing the OAuth2Grant interface. * @return - An Single emitting the granted access token. */ fun grantNewAccessToken( grant: OAuth2Grant, calendar: Calendar = Calendar.getInstance() ): Single = grant.grantNewAccessToken() .map { if (it.expiresIn != null) { val newExpirationDate = (calendar.clone() as Calendar).apply { add(Calendar.SECOND, it.expiresIn) } it.copy(expirationDate = newExpirationDate) } else { it } } .doOnSuccess { token -> mStorage.storeAccessToken( token = token ) } .cache() /** * Returns an Observable emitting an unexpired access token. * NOTE: In order to work, Heimdall needs an access token which has a refresh_token and an * expires_in field. * * @param refreshAccessTokenGrant The refresh grant that will be used if the access token is expired. * @return - An Single emitting an unexpired access token. */ fun getValidAccessToken(refreshAccessTokenGrant: OAuth2RefreshAccessTokenGrant): Single = mStorage.getStoredAccessToken() .flatMap { accessToken -> if (accessToken.isExpired()) { refreshAccessTokenGrant.refreshToken = accessToken.refreshToken grantNewAccessToken(refreshAccessTokenGrant) } else { Single.just(accessToken) } } } ================================================ FILE: library/src/main/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenStorage.kt ================================================ package de.rheinfabrik.heimdall2 import io.reactivex.Single /** * Interface used to define how to store and retrieve a stored access token. * * @param The access token type. */ interface OAuth2AccessTokenStorage { // Public API /** * Queries the stored access token. * * @return - An Observable emitting the stored access token. */ fun getStoredAccessToken(): Single /** * Stores the given access token. * * @param token The access token which will be stored. */ fun storeAccessToken(token: OAuth2AccessToken) /** * Checks whether there is or is not an access token * * @return - An Observable emitting true or false based on whether there is or is not an * access token. */ fun hasAccessToken(): Single /** * Removes the stored access token. */ fun removeAccessToken() } ================================================ FILE: library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2AuthorizationCodeGrant.kt ================================================ package de.rheinfabrik.heimdall2.grants import de.rheinfabrik.heimdall2.OAuth2AccessToken import io.reactivex.Observable import io.reactivex.Single import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.PublishSubject import java.net.URL import java.net.URLDecoder /** * Class representing the Authorization Code Grant as described in https://tools.ietf.org/html/rfc6749#section-4.1. * * @param The access token type. */ abstract class OAuth2AuthorizationCodeGrant( /** * REQUIRED * The client identifier as described in https://tools.ietf.org/html/rfc6749#section-2.2. */ var clientId: String = "", /** * OPTIONAL * As described in https://tools.ietf.org/html/rfc6749#section-3.1.2. */ var redirectUri: String? = null, /** * OPTIONAL * The scope of the access request as described in https://tools.ietf.org/html/rfc6749#section-3.3. */ var scope: String? = null, /** * RECOMMENDED * An opaque value used by the client to maintain * state between the request and callback. The authorization * server includes this value when redirecting the user-agent back * to the client. The parameter SHOULD be used for preventing * cross-site request forgery as described in https://tools.ietf.org/html/rfc6749#section-10.12. */ var state: String? = null ) : OAuth2Grant { // Constants companion object { @JvmField val RESPONSE_TYPE = "code" @JvmField val GRANT_TYPE = "authorization_code" private const val UTF_8 = "UTF-8" } // Public Members /** * Command you should send a value to whenever an url in e.g. your web view has been loaded. */ val onUrlLoadedCommand = PublishSubject.create() // Private Members private val mAuthorizationUrlSubject = BehaviorSubject.create() // Abstract API /** * Called when the grant needs the authorization url. */ abstract fun buildAuthorizationUrl(): URL /** * Called when the grant was able to grab the code and it wants to exchange for an access token. */ abstract fun exchangeTokenUsingCode(code: String): Observable // Public API /** * Observable emitting the authorization Uri. */ fun authorizationUri() = mAuthorizationUrlSubject override fun grantNewAccessToken(): Single { mAuthorizationUrlSubject.onNext(buildAuthorizationUrl()) return onUrlLoadedCommand.map { getQueryParameters(it)[RESPONSE_TYPE]?.get(0) }.filter { it.isNotBlank() }.take(1) .retry() .concatMap { exchangeTokenUsingCode(it) } .singleOrError() } // Private API private fun getQueryParameters(url: URL): LinkedHashMap> { val queryParams = linkedMapOf>() url.query.split("&").forEach { val idx = it.indexOf("=") try { val key = if (idx > 0) { URLDecoder.decode( it.substring(0, idx), UTF_8 ) } else { it } if (!queryParams.containsKey(key)) { queryParams[key] = mutableListOf() } val value = if (idx > 0 && it.length > idx + 1) { URLDecoder.decode( it.substring(idx + 1), UTF_8 ) } else "" queryParams[key]?.add(value) } catch (e: Exception) { // Do nothing } } return queryParams } } ================================================ FILE: library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2ClientCredentialsGrant.kt ================================================ package de.rheinfabrik.heimdall2.grants abstract class OAuth2ClientCredentialsGrant( /** * OPTIONAL * The scope of the access request as described in https://tools.ietf.org/html/rfc6749#section-3.3. */ val scope: String? = null ) : OAuth2Grant { // Constants companion object { /** * REQUIRED * The OAuth2 "grant_type". */ @JvmField val GRANT_TYPE = "client_credentials" } } ================================================ FILE: library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2Grant.kt ================================================ package de.rheinfabrik.heimdall2.grants import de.rheinfabrik.heimdall2.OAuth2AccessToken import io.reactivex.Single /** * Interface describing an OAuth2 Grant as described in https://tools.ietf.org/html/rfc6749#page-23. * * @param The access token type. */ interface OAuth2Grant { // Abstract Api /** * Performs the actual request to grant a new access token. * * @return - An Observable emitting the granted access token. */ fun grantNewAccessToken(): Single } ================================================ FILE: library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2ImplicitGrant.kt ================================================ package de.rheinfabrik.heimdall2.grants /** * Class representing the Implicit Grant as described in https://tools.ietf.org/html/rfc6749#section-4.2. * * @param The access token type. */ abstract class OAuth2ImplicitGrant( /** * REQUIRED * The client identifier as described in https://tools.ietf.org/html/rfc6749#section-2.2. */ var clientId: String = "", /** * OPTIONAL * As described in https://tools.ietf.org/html/rfc6749#section-3.1.2. */ var redirectUri: String? = null, /** * OPTIONAL * The scope of the access request as described in https://tools.ietf.org/html/rfc6749#section-3.3. */ var scope: String? = null, /** * RECOMMENDED * An opaque value used by the client to maintain * state between the request and callback. The authorization * server includes this value when redirecting the user-agent back * to the client. The parameter SHOULD be used for preventing * cross-site request forgery as described in https://tools.ietf.org/html/rfc6749#section-10.12. */ var state: String? = null ) : OAuth2Grant { // Constants /** * REQUIRED * The "response_type" which MUST be "token". */ companion object { @JvmField val RESPONSE_TYPE = "token" } } ================================================ FILE: library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2RefreshAccessTokenGrant.kt ================================================ package de.rheinfabrik.heimdall2.grants abstract class OAuth2RefreshAccessTokenGrant( /** * REQUIRED * The "refresh_token" issued to the client. */ var refreshToken: String? = null, /** * OPTIONAL * The "scope" of the access request as described by here (https://tools.ietf.org/html/rfc6749#section-3.3). */ var scope: String? = null ) : OAuth2Grant { // Constants /** * REQUIRED * The OAuth2 "grant_type". */ companion object { @JvmField val GRANT_TYPE = "refresh_token" } } ================================================ FILE: library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2ResourceOwnerPasswordCredentialsGrant.kt ================================================ package de.rheinfabrik.heimdall2.grants abstract class OAuth2ResourceOwnerPasswordCredentialsGrant( /** * REQUIRED * The resource owner "username". */ var username: String? = null, /** * REQUIRED * The resource owner "password". */ var password: String? = null, /** * OPTIONAL * The "scope" of the access request as described by here (https://tools.ietf.org/html/rfc6749#section-3.3). */ var scope: String? = null ) : OAuth2Grant { companion object { @JvmField val GRANT_TYPE = "password" } } ================================================ FILE: library/src/test/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenIsExpiredTest.kt ================================================ package de.rheinfabrik.heimdall2 import org.junit.Assert.assertEquals import org.junit.Test import java.util.* class OAuth2AccessTokenIsExpiredTest { @Test fun `when the expirationDate is null, isExpired method should return false`() { // given an accessToken val accessToken = OAuth2AccessToken(expirationDate = null) // when the isExpired method is called val value = accessToken.isExpired() // then false is returned assertEquals(value, false) } @Test fun `when the expirationDate is in the past, isExpired method should return false`() { // given a date in the past val pastCalendar = Calendar.getInstance() pastCalendar.add(Calendar.YEAR, 1) // and a token with a past date val accessToken = OAuth2AccessToken(expirationDate = pastCalendar) // when the isExpired method is called val value = accessToken.isExpired() // then true is returned assertEquals(value, false) } @Test fun `when the expirationDate is in the future, isExpired method should return false`() { // given a date in the past val futureDate = Calendar.getInstance() futureDate.add(Calendar.YEAR, 1) // and a token with a past date val accessToken = OAuth2AccessToken(expirationDate = futureDate) // when the isExpired method is called val value = accessToken.isExpired() // then true is returned assertEquals(value, false) } } ================================================ FILE: library/src/test/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenManagerGetValidAccessTokenTest.kt ================================================ package de.rheinfabrik.heimdall2 import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import de.rheinfabrik.heimdall2.grants.OAuth2RefreshAccessTokenGrant import io.reactivex.Single import org.junit.Test import java.util.* class OAuth2AccessTokenManagerGetValidAccessTokenTest { @Test fun `when subscribed to getValidAccessToken(), the non-expired token should be emitted`() { // given a non expired token val accessToken = mock().apply { whenever(isExpired()).thenReturn(false) } // and a token manager with a valid storage and token val storage = mock().apply { whenever(getStoredAccessToken()).thenReturn(Single.just(accessToken)) } val grant = mock().apply { whenever(grantNewAccessToken()).thenReturn(Single.just(accessToken)) } val tokenManager = OAuth2AccessTokenManager( mStorage = storage ) // when a valid access token is requested val validTokenTest = tokenManager.getValidAccessToken(grant).test() // then the non expired token gets received validTokenTest.assertValue(accessToken) } @Test fun `when the token expires, the refresh grant should be called to refresh it`(){ // given an expired accesstoken val accessToken = mock().apply { whenever(isExpired()).thenReturn(true) } // and a token manager val tokenManager = OAuth2AccessTokenManager( mStorage = mock().apply { whenever(getStoredAccessToken()).thenReturn(Single.just(accessToken)) } ) // and a OAuth2RefreshAccessTokenGrant val grant = mock() // when a valid token is needed tokenManager.getValidAccessToken(grant).test() // then a refresh grant asks for a new token verify(grant).grantNewAccessToken() } @Test fun `when the token expires, the grant should be updated with a new token`() { // given an expired OAuthAccessToken val pastDate = Calendar.getInstance() pastDate.add(Calendar.YEAR, -1) val accessToken = OAuth2AccessToken( refreshToken = "rt", expirationDate = pastDate ) // and a token manager val tokenManager = OAuth2AccessTokenManager( mStorage = mock().apply { whenever(getStoredAccessToken()).thenReturn(Single.just(accessToken)) } ) // and a grant val grant = mock().apply { whenever(grantNewAccessToken()).thenReturn(Single.just(accessToken)) } // when a valid access token is needed val grantToken = tokenManager.getValidAccessToken(grant).test() // then the grants new refreshToken gets updated grantToken.assertValue{ it.refreshToken == accessToken.refreshToken } } } ================================================ FILE: library/src/test/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenManagerGrantNewAccessTokenTest.kt ================================================ package de.rheinfabrik.heimdall2 import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import de.rheinfabrik.heimdall2.grants.OAuth2Grant import io.reactivex.Single import org.junit.Test import java.util.* class OAuth2AccessTokenManagerGrantNewAccessTokenTest { @Test fun `when a new access token has an expiration date, the token manager should generate and set the correct expiration date`() { // given an OAuth2AccessToken val accessToken = OAuth2AccessToken( expirationDate = null ) val changedAccessToken = accessToken.copy( expiresIn = 3 ) // and a grant that emits that token val grant = mock().apply { whenever(grantNewAccessToken()).thenReturn(Single.just(changedAccessToken)) } // and a tokenManager val tokenManager = OAuth2AccessTokenManager( mStorage = mock() ) // when a new access token is needed val calendar = Calendar.getInstance() val newToken = tokenManager.grantNewAccessToken( grant = grant, calendar = calendar ).test() // then the access token should have the correct expiration date newToken.assertValue { it.expirationDate?.timeInMillis == calendar.timeInMillis + 3000 } } @Test fun `when a new access token doesn't have an expiration date, the token manager should not generate and a new expiration date`() { // given an OAuth2AccessToken val accessToken = OAuth2AccessToken( expirationDate = null ) val changedAccessToken = accessToken.copy( expiresIn = null ) // and a grant that emits that token val grant = mock().apply { whenever(grantNewAccessToken()).thenReturn(Single.just(changedAccessToken)) } // and a tokenManager val tokenManager = OAuth2AccessTokenManager( mStorage = mock() ) // when a new access token is needed val calendar = Calendar.getInstance() val newToken = tokenManager.grantNewAccessToken( grant = grant, calendar = calendar ).test() // then the access token should have the correct expiration date newToken.assertValue { it.expirationDate == null } } @Test fun `when a new access token is needed, the storage should be called to store the token`() { // given a grant that emits a token val accessToken = OAuth2AccessToken() val grant = mock().apply { whenever(grantNewAccessToken()).thenReturn(Single.just(accessToken)) } // and a token manager val storage = mock() val tokenManager = OAuth2AccessTokenManager( mStorage = storage ) // when a new access token is needed tokenManager.grantNewAccessToken(grant).test() // then the storage is called verify(storage).storeAccessToken(accessToken) } } ================================================ FILE: library/src/test/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenManagerTest.kt ================================================ package de.rheinfabrik.heimdall2 import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever import de.rheinfabrik.heimdall2.grants.OAuth2RefreshAccessTokenGrant import io.reactivex.Single import org.junit.Test class OAuth2AccessTokenManagerTest { @Test fun `when the access token is accessed, the token manager should emit a non expired token`(){ // given a stored access token val validToken = mock() val accessTokenStorage = mock() whenever(accessTokenStorage.getStoredAccessToken()).thenReturn(Single.just(validToken)) // and a refresh access token grant val refreshAccessTokenGrant = mock() whenever(refreshAccessTokenGrant.grantNewAccessToken()).thenReturn(Single.just(validToken)) // and a token manager val tokenManager = OAuth2AccessTokenManager(accessTokenStorage) // when the token is accessed val getValidAccessTokenTest = tokenManager.getValidAccessToken(refreshAccessTokenGrant).test() // then the token is received getValidAccessTokenTest.assertValue(validToken) } } ================================================ FILE: library/src/test/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenSerializationTest.kt ================================================ package de.rheinfabrik.heimdall2 import com.google.gson.Gson import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import java.util.* class OAuth2AccessTokenSerializationTest { @Before fun setup() { // Set default locale and time zone Locale.setDefault(Locale.GERMANY) TimeZone.setDefault(TimeZone.getTimeZone("CEST")) } @Test fun `when an OAuth2AccessToken is transformed to a JSON string, it should be created`() { // given an OAuth2AccessToken val accessToken = OAuth2AccessToken( refreshToken = "rt", expiresIn = 3600, accessToken = "at", tokenType = "bearer" ) val expirationDate = Calendar.getInstance() expirationDate.timeInMillis = 0 // and an updated expiration date val changedAccessToken = accessToken.copy( expirationDate = expirationDate ) // when it gets serialized with Gson val json = Gson().toJson(changedAccessToken) // then the json should be written correctly assertEquals( "{\"token_type\":\"bearer\",\"access_token\":\"at\",\"refresh_token\":\"rt\",\"expires_in\":3600,\"heimdall_expiration_date\":{\"year\":1970,\"month\":0,\"dayOfMonth\":1,\"hourOfDay\":0,\"minute\":0,\"second\":0}}", json ) } @Test fun `when a token is created from a JSON string, it should be created`() { // given a json string representing a OAuth2AccessToken val jsonString = "{\"access_token\":\"at\",\"heimdall_expiration_date\":{\"year\":1970,\"month\":0,\"dayOfMonth\":1,\"hourOfDay\":0,\"minute\":0,\"second\":0},\"expires_in\":3600,\"refresh_token\":\"rt\",\"token_type\":\"bearer\"}" // and an expiration date val calendar = Calendar.getInstance() calendar.timeInMillis = 0 // when it gets deserialized with Gson val accessToken = Gson().fromJson(jsonString, OAuth2AccessToken::class.java) // then the token should be the same assertEquals("rt", accessToken.refreshToken) assertEquals(3600, accessToken.expiresIn) assertEquals("at", accessToken.accessToken) assertEquals("bearer", accessToken.tokenType) assertEquals(calendar.timeInMillis, accessToken.expirationDate?.timeInMillis) } } ================================================ FILE: library/src/test/java/de/rheinfabrik/heimdall2/grants/OAuth2AuthorizationCodeGrantTest.kt ================================================ package de.rheinfabrik.heimdall2.grants import org.junit.Assert.assertEquals import org.junit.Test class OAuth2AuthorizationCodeGrantTest { // Specifications for https://tools.ietf.org/html/rfc6749#section-4.1 @Test fun `when an authorization code grant is created, the grant type is code`() { // the grant type is the correct assertEquals(OAuth2AuthorizationCodeGrant.GRANT_TYPE, "authorization_code") } @Test fun `when an authorization code grant is created, the response type is code`() { // the grant type is the correct assertEquals(OAuth2AuthorizationCodeGrant.RESPONSE_TYPE, "code") } } ================================================ FILE: library/src/test/java/de/rheinfabrik/heimdall2/grants/OAuth2ClientCredentialsGrantTest.kt ================================================ package de.rheinfabrik.heimdall2.grants import org.junit.Assert.assertEquals import org.junit.Test class OAuth2ClientCredentialsGrantTest { // Specification for https://tools.ietf.org/html/rfc6749#section-4.4 @Test fun `when a client credentials grant is created, the grant type is code`() { // when an client credentials grant is created val grant = OAuth2ClientCredentialsGrant // then the grant type is the correct assertEquals(grant.GRANT_TYPE, "client_credentials") } } ================================================ FILE: library/src/test/java/de/rheinfabrik/heimdall2/grants/OAuth2ImplicitGrantTest.kt ================================================ package de.rheinfabrik.heimdall2.grants import org.junit.Assert.assertEquals import org.junit.Test class OAuth2ImplicitGrantTest { // Specifications for https://tools.ietf.org/html/rfc6749#section-4.2 @Test fun `when an implicit grant is created, the response type is token`() { // when an authorization code grant is created val grant = OAuth2ImplicitGrant // then the grant type is the correct assertEquals(grant.RESPONSE_TYPE, "token") } } ================================================ FILE: library/src/test/java/de/rheinfabrik/heimdall2/grants/OAuth2RefreshAccessTokenGrantTest.kt ================================================ package de.rheinfabrik.heimdall2.grants import org.junit.Assert.assertEquals import org.junit.Test class OAuth2RefreshAccessTokenGrantTest { // Specifications for https://tools.ietf.org/html/rfc6749#section-6 @Test fun `when an refresh access token grant is created, the grant type is refresh_token`() { // the grant type is the correct assertEquals(OAuth2RefreshAccessTokenGrant.GRANT_TYPE, "refresh_token") } } ================================================ FILE: library/src/test/java/de/rheinfabrik/heimdall2/grants/OAuth2ResourceOwnerPasswordCredentialsGrantTest.kt ================================================ package de.rheinfabrik.heimdall2.grants import org.junit.Assert.assertEquals import org.junit.Test class OAuth2ResourceOwnerPasswordCredentialsGrantTest { // Specifications for https://tools.ietf.org/html/rfc6749#section-4.3 @Test fun `when a access token grant is created, the grant type is refresh_token`() { // the grant type is the correct assertEquals(OAuth2ResourceOwnerPasswordCredentialsGrant.GRANT_TYPE, "password") } } ================================================ FILE: library/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker ================================================ mock-maker-inline ================================================ FILE: sample/build.gradle ================================================ apply plugin: 'com.android.application' buildscript { repositories { mavenCentral() } } android { compileSdkVersion 30 buildToolsVersion "30.0.2" defaultConfig { applicationId "de.rheinfabrik.heimdall" minSdkVersion 21 targetSdkVersion 30 versionCode 1 versionName "1.0" } // https://github.com/evant/gradle-retrolambda#android-studio-setup compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { // Android implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.cardview:cardview:1.0.0' // Heimdall implementation project(':library') // Rx implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxjava:2.2.19' implementation 'com.trello.rxlifecycle3:rxlifecycle-components:3.0.0' // Serialization implementation 'com.google.code.gson:gson:2.8.6' implementation 'org.parceler:parceler-api:0.2.16' // Network implementation 'com.squareup.retrofit:retrofit:1.9.0' // Butterknife annotationProcessor 'com.jakewharton:butterknife-compiler:8.2.1' implementation 'com.jakewharton:butterknife:8.2.1' } ================================================ FILE: sample/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/gilo/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: sample/src/main/AndroidManifest.xml ================================================ ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/TraktTvAPIConfiguration.java ================================================ package de.rheinfabrik.heimdalldroid; public class TraktTvAPIConfiguration { public static final String CLIENT_ID = "enter_your_client_id_here"; public static final String CLIENT_SECRET = "enter_your_client_secret_here"; public static final String REDIRECT_URI = "enter_your_redirect_uri_here"; } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/actvities/LoginActivity.java ================================================ package de.rheinfabrik.heimdalldroid.actvities; import android.graphics.Bitmap; import android.os.Bundle; import android.view.View; import android.webkit.WebView; import android.webkit.WebViewClient; import butterknife.BindView; import com.trello.rxlifecycle3.components.support.RxAppCompatActivity; import io.reactivex.android.schedulers.AndroidSchedulers; import java.net.MalformedURLException; import java.net.URL; import butterknife.ButterKnife; import de.rheinfabrik.heimdalldroid.R; import de.rheinfabrik.heimdalldroid.network.oauth2.TraktTvAuthorizationCodeGrant; import de.rheinfabrik.heimdalldroid.network.oauth2.TraktTvOauth2AccessTokenManager; import de.rheinfabrik.heimdalldroid.utils.AlertDialogFactory; import java.util.Calendar; /** * Activity used to let the user login with his GitHub credentials. * You may want to move most of this code to your presenter class or view model. * For the sake of simplicity the code is inside the activity. */ public class LoginActivity extends RxAppCompatActivity { // Members @BindView(R.id.webView) protected WebView mWebView; // Activity lifecycle @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Set content view setContentView(R.layout.activity_login); // Inject views ButterKnife.bind(this); // Start authorization authorize(); } // Private Api private void authorize() { // Grab a new token manger TraktTvOauth2AccessTokenManager tokenManager = TraktTvOauth2AccessTokenManager.from(this); // Grab a new grant TraktTvAuthorizationCodeGrant grant = tokenManager.newAuthorizationCodeGrant(); // Listen for the authorization url and load it once needed grant.authorizationUri() .map(URL::toString) .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mWebView::loadUrl); // Sent loaded website to grant so it can check if we have an access token mWebView.setWebViewClient(new WebViewClient() { @Override public void onPageStarted(WebView view, String urlString, Bitmap favicon) { super.onPageStarted(view, urlString, favicon); try { URL url = new URL(urlString); grant.getOnUrlLoadedCommand().onNext(url); // Hide redirect page from user if (urlString.startsWith(grant.getRedirectUri())) { mWebView.setVisibility(View.GONE); } } catch (MalformedURLException ignored) { // Empty } } }); // Start authorization and listen for success tokenManager.grantNewAccessToken(grant, Calendar.getInstance()) .toObservable() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(x -> handleSuccess(), x -> handleError()); } // Set the result to ok and finish this activity private void handleSuccess() { setResult(RESULT_OK); finish(); } // Build an error dialog and show it private void handleError() { AlertDialogFactory.errorAlertDialog(this).show(); } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/actvities/MainActivity.java ================================================ package de.rheinfabrik.heimdalldroid.actvities; import android.app.Activity; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.webkit.CookieManager; import android.widget.Toolbar; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.trello.rxlifecycle3.components.support.RxAppCompatActivity; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import java.util.List; import butterknife.ButterKnife; import butterknife.BindView; import de.rheinfabrik.heimdalldroid.R; import de.rheinfabrik.heimdalldroid.adapter.TraktTvListsRecyclerViewAdapter; import de.rheinfabrik.heimdalldroid.network.TraktTvApiFactory; import de.rheinfabrik.heimdalldroid.network.models.TraktTvList; import de.rheinfabrik.heimdalldroid.network.oauth2.TraktTvOauth2AccessTokenManager; import de.rheinfabrik.heimdalldroid.utils.AlertDialogFactory; import de.rheinfabrik.heimdalldroid.utils.IntentFactory; import retrofit.RetrofitError; /** * Activity showing either the list of the user's repositories or the login screen. * You may want to move most of this code to your presenter class or view model. * For the sake of simplicity the code is inside the activity. */ public class MainActivity extends RxAppCompatActivity { // Constants private static final int AUTHORIZATION_REQUEST_CODE = 1; // Members @BindView(R.id.recyclerView) protected RecyclerView mRecyclerView; @BindView(R.id.toolbar) protected Toolbar mToolbar; @BindView(R.id.swipeRefreshLayout) protected SwipeRefreshLayout mSwipeRefreshLayout; private TraktTvOauth2AccessTokenManager mTokenManager; // Activity lifecycle @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Set content view setContentView(R.layout.activity_main); // Inject views ButterKnife.bind(this); // Setup toolbar setActionBar(mToolbar); // Setup swipe refresh layout mSwipeRefreshLayout.setOnRefreshListener(MainActivity.this::refresh); // Setup recycler view LinearLayoutManager layoutManager = new LinearLayoutManager(this); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); mRecyclerView.setLayoutManager(layoutManager); // Grab a new manager mTokenManager = TraktTvOauth2AccessTokenManager.from(this); // Check if we are logged in refresh(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); // Check if the request code is correct if (requestCode == AUTHORIZATION_REQUEST_CODE) { // Check if login was successful if (resultCode == Activity.RESULT_OK) { loadLists(); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.logout) { logout(); } return super.onOptionsItemSelected(item); } // Private Api private void loadLists() { // Load the lists Observable> listsObservable = mTokenManager /* Grab a valid access token (automatically refreshes the token if it is expired) */ .getValidAccessToken() /* Load lists */ .flatMapObservable(authorizationHeader -> TraktTvApiFactory.newApiService().getLists(authorizationHeader)); // Bind to lifecycle listsObservable .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(lists -> { if (lists == null || lists.isEmpty()) { handleEmptyList(); } else { handleSuccess(lists); } }, this::handleError); } // Start the LoginActivity. private void showLogin() { mSwipeRefreshLayout.setRefreshing(false); Intent loginIntent = IntentFactory.loginIntent(this); startActivityForResult(loginIntent, AUTHORIZATION_REQUEST_CODE); } // Shows a dialog saying that there were no lists. private void handleEmptyList() { mSwipeRefreshLayout.setRefreshing(false); AlertDialogFactory.noListsFoundDialog(this).show(); } // Show an error dialog private void handleError(Throwable error) { mSwipeRefreshLayout.setRefreshing(false); // Clear token and login if 401 if (error instanceof RetrofitError) { RetrofitError retrofitError = (RetrofitError) error; if (retrofitError.getResponse().getStatus() == 401) { mTokenManager.getStorage().removeAccessToken(); refresh(); } } else { AlertDialogFactory.errorAlertDialog(this).show(); } } // Update our recycler view private void handleSuccess(List traktTvLists) { mSwipeRefreshLayout.setRefreshing(false); mRecyclerView.setAdapter(new TraktTvListsRecyclerViewAdapter(traktTvLists)); } // Check if logged in and show either login or load lists private void refresh() { mSwipeRefreshLayout.setRefreshing(true); mTokenManager.getStorage().hasAccessToken() .toObservable() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(loggedIn -> { if (loggedIn) { loadLists(); } else { showLogin(); } }); } // Logs out the user private void logout() { // Ask token manager to revoke the token mTokenManager.logout() .toObservable() .compose(bindToLifecycle()) .subscribe(x -> showLogin()); // Clear webview cache CookieManager cookieManager = CookieManager.getInstance(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { cookieManager.removeAllCookies(null); } else { cookieManager.removeAllCookie(); } } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/adapter/TraktTvListsRecyclerViewAdapter.java ================================================ package de.rheinfabrik.heimdalldroid.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import de.rheinfabrik.heimdalldroid.R; import de.rheinfabrik.heimdalldroid.adapter.viewholder.TraktTvListViewHolder; import de.rheinfabrik.heimdalldroid.network.models.TraktTvList; /** * Simple adapter for showing a list of liked lists. */ public class TraktTvListsRecyclerViewAdapter extends RecyclerView.Adapter { // Members private final List mTraktTvLists; // Constructor public TraktTvListsRecyclerViewAdapter(List traktTvLists) { super(); mTraktTvLists = traktTvLists; } // RecyclerView.Adapter @Override public TraktTvListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view_trakt_tv_list, parent, false); return new TraktTvListViewHolder(itemView); } @Override public void onBindViewHolder(TraktTvListViewHolder holder, int position) { holder.bind(mTraktTvLists.get(position)); } @Override public int getItemCount() { if (mTraktTvLists != null) { return mTraktTvLists.size(); } return 0; } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/adapter/viewholder/TraktTvListViewHolder.java ================================================ package de.rheinfabrik.heimdalldroid.adapter.viewholder; import android.view.View; import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; import butterknife.ButterKnife; import butterknife.BindView; import de.rheinfabrik.heimdalldroid.R; import de.rheinfabrik.heimdalldroid.network.models.TraktTvList; /** * View holder used to display a liked list. */ public class TraktTvListViewHolder extends RecyclerView.ViewHolder { // Members @BindView(R.id.titleTextView) protected TextView mTitleTextView; @BindView(R.id.descriptionTextView) protected TextView mDescriptionTextView; @BindView(R.id.likeCountTextView) protected TextView mLikeCountTextView; // Constructor public TraktTvListViewHolder(View itemView) { super(itemView); // Inject views ButterKnife.bind(this, itemView); } // Public Api public void bind(TraktTvList traktTvList) { // Set title mTitleTextView.setText(traktTvList.name); // Set description mDescriptionTextView.setText(traktTvList.description); // Set like count mLikeCountTextView.setText(traktTvList.numberOfLikes + itemView.getContext().getString(R.string.likes_postfix)); } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/TraktTvApiFactory.java ================================================ package de.rheinfabrik.heimdalldroid.network; import com.google.gson.Gson; import de.rheinfabrik.heimdalldroid.TraktTvAPIConfiguration; import retrofit.RestAdapter; import retrofit.converter.GsonConverter; /** * Factory class to generate new TraktTvApiService instances. */ public class TraktTvApiFactory { // Constants private static final String API_ENDPOINT = "https://api-v2launch.trakt.tv"; // Public Api /** * Creates a new preconfigured TraktTvApiService instance. */ public static TraktTvApiService newApiService() { // Set up rest adapter RestAdapter.Builder restAdapterBuilder = new RestAdapter.Builder() .setConverter(new GsonConverter(new Gson())) .setRequestInterceptor(request -> { request.addHeader("Content-type", "application/json"); request.addHeader("trakt-api-version", "2"); request.addHeader("trakt-api-key", TraktTvAPIConfiguration.CLIENT_ID); }) .setLogLevel(RestAdapter.LogLevel.FULL) .setEndpoint(API_ENDPOINT); // Build raw api service return restAdapterBuilder.build().create(TraktTvApiService.class); } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/TraktTvApiService.java ================================================ package de.rheinfabrik.heimdalldroid.network; import io.reactivex.Observable; import java.util.List; import de.rheinfabrik.heimdall2.OAuth2AccessToken; import de.rheinfabrik.heimdalldroid.network.models.AccessTokenRequestBody; import de.rheinfabrik.heimdalldroid.network.models.RefreshTokenRequestBody; import de.rheinfabrik.heimdalldroid.network.models.RevokeAccessTokenBody; import de.rheinfabrik.heimdalldroid.network.models.TraktTvList; import retrofit.http.Body; import retrofit.http.GET; import retrofit.http.Header; import retrofit.http.POST; /** * Interface for communicating to the TraktTv API (http://docs.trakt.apiary.io/#). */ public interface TraktTvApiService { // POST @POST("/oauth/token") Observable grantNewAccessToken(@Body AccessTokenRequestBody body); @POST("/oauth/token") Observable refreshAccessToken(@Body RefreshTokenRequestBody body); @POST("/oauth/revoke") Observable revokeAccessToken(@Body RevokeAccessTokenBody body); // GET @GET("/users/me/lists") Observable> getLists(@Header("Authorization") String authorizationHeader); } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/AccessTokenRequestBody.java ================================================ package de.rheinfabrik.heimdalldroid.network.models; import com.google.gson.annotations.SerializedName; import java.io.Serializable; /** * Body object used to exchange a code with an access token. */ public class AccessTokenRequestBody implements Serializable { // Properties @SerializedName("code") public String code; @SerializedName("client_id") public String clientId; @SerializedName("client_secret") public String clientSecret; @SerializedName("redirect_uri") public String redirectUri; @SerializedName("grant_type") public String grantType; // Constructor public AccessTokenRequestBody(String code, String clientId, String redirectUri, String clientSecret, String grantType) { super(); this.code = code; this.clientId = clientId; this.redirectUri = redirectUri; this.clientSecret = clientSecret; this.grantType = grantType; } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/RefreshTokenRequestBody.java ================================================ package de.rheinfabrik.heimdalldroid.network.models; import com.google.gson.annotations.SerializedName; import java.io.Serializable; /** * Body used to refresh an access token. */ public class RefreshTokenRequestBody implements Serializable { // Properties @SerializedName("refresh_token") public String refreshToken; @SerializedName("client_id") public String clientId; @SerializedName("client_secret") public String clientSecret; @SerializedName("redirect_uri") public String redirectUri; @SerializedName("grant_type") public String grantType; // Constructor public RefreshTokenRequestBody(String refreshToken, String clientId, String clientSecret, String redirectUri, String grantType) { super(); this.refreshToken = refreshToken; this.clientId = clientId; this.clientSecret = clientSecret; this.redirectUri = redirectUri; this.grantType = grantType; } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/RevokeAccessTokenBody.java ================================================ package de.rheinfabrik.heimdalldroid.network.models; import com.google.gson.annotations.SerializedName; /** * Body used to revoke an access token. */ public class RevokeAccessTokenBody { // Properties @SerializedName("access_token") public String accessToken; // Constructor public RevokeAccessTokenBody(String accessToken) { super(); this.accessToken = accessToken; } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/TraktTvList.java ================================================ package de.rheinfabrik.heimdalldroid.network.models; import com.google.gson.annotations.SerializedName; import java.io.Serializable; /** * Model describing a TraktTvList. */ public class TraktTvList implements Serializable { // Properties @SerializedName("name") public String name; @SerializedName("description") public String description; @SerializedName("likes") public int numberOfLikes; } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/oauth2/TraktTvAuthorizationCodeGrant.java ================================================ package de.rheinfabrik.heimdalldroid.network.oauth2; import android.net.Uri; import de.rheinfabrik.heimdall2.grants.OAuth2AuthorizationCodeGrant; import io.reactivex.Observable; import java.net.URL; import de.rheinfabrik.heimdall2.OAuth2AccessToken; import de.rheinfabrik.heimdalldroid.network.TraktTvApiFactory; import de.rheinfabrik.heimdalldroid.network.models.AccessTokenRequestBody; /** * TraktTv authorization code grant as described in http://docs.trakt.apiary.io/#reference/authentication-oauth. */ public class TraktTvAuthorizationCodeGrant extends OAuth2AuthorizationCodeGrant { // Properties public String clientSecret; // OAuth2AuthorizationCodeGrant @Override public URL buildAuthorizationUrl() { try { return new URL( Uri.parse("https://trakt.tv/oauth/authorize") .buildUpon() .appendQueryParameter("client_id", getClientId()) .appendQueryParameter("redirect_uri", getRedirectUri()) .appendQueryParameter("response_type", OAuth2AuthorizationCodeGrant.RESPONSE_TYPE) .build() .toString() ); } catch (Exception ignored) { return null; } } @Override public Observable exchangeTokenUsingCode(String code) { AccessTokenRequestBody body = new AccessTokenRequestBody( code, getClientId(), getRedirectUri(), clientSecret, GRANT_TYPE ); return TraktTvApiFactory.newApiService().grantNewAccessToken(body); } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/oauth2/TraktTvOauth2AccessTokenManager.java ================================================ package de.rheinfabrik.heimdalldroid.network.oauth2; import android.content.Context; import android.content.SharedPreferences; import de.rheinfabrik.heimdall2.OAuth2AccessToken; import de.rheinfabrik.heimdall2.OAuth2AccessTokenManager; import de.rheinfabrik.heimdall2.OAuth2AccessTokenStorage; import de.rheinfabrik.heimdalldroid.TraktTvAPIConfiguration; import de.rheinfabrik.heimdalldroid.network.TraktTvApiFactory; import de.rheinfabrik.heimdalldroid.network.models.RevokeAccessTokenBody; import de.rheinfabrik.heimdalldroid.utils.SharedPreferencesOAuth2AccessTokenStorage; import io.reactivex.Single; /** * Token manger used to handle all your access token needs with the TraktTv API (http://docs.trakt.apiary.io/#). */ public final class TraktTvOauth2AccessTokenManager extends OAuth2AccessTokenManager { // Factory methods /** * Creates a new preconfigured TraktTvOauth2AccessTokenManager based of a context. */ public static TraktTvOauth2AccessTokenManager from(Context context) { // Define the shared preferences where we will save the access token SharedPreferences sharedPreferences = context.getSharedPreferences("TraktTvAccessTokenStorage", Context.MODE_PRIVATE); // Define the storage using the the previously defined preferences SharedPreferencesOAuth2AccessTokenStorage tokenStorage = new SharedPreferencesOAuth2AccessTokenStorage<>(sharedPreferences, OAuth2AccessToken.class); // Create the new TraktTvOauth2AccessTokenManager return new TraktTvOauth2AccessTokenManager(tokenStorage); } // Constructor public TraktTvOauth2AccessTokenManager(OAuth2AccessTokenStorage storage) { super(storage); } // Public Api /** * Creates a new preconfigured TraktTvAuthorizationCodeGrant. */ public TraktTvAuthorizationCodeGrant newAuthorizationCodeGrant() { TraktTvAuthorizationCodeGrant grant = new TraktTvAuthorizationCodeGrant(); grant.setClientId(TraktTvAPIConfiguration.CLIENT_ID); grant.clientSecret = TraktTvAPIConfiguration.CLIENT_SECRET; grant.setRedirectUri(TraktTvAPIConfiguration.REDIRECT_URI); return grant; } /** * Returns a valid authorization header string using a preconfigured TraktTvRefreshAccessTokenGrant. */ public Single getValidAccessToken() { TraktTvRefreshAccessTokenGrant grant = new TraktTvRefreshAccessTokenGrant(); grant.clientId = TraktTvAPIConfiguration.CLIENT_ID; grant.clientSecret = TraktTvAPIConfiguration.CLIENT_SECRET; grant.redirectUri = TraktTvAPIConfiguration.REDIRECT_URI; return super.getValidAccessToken(grant).map(token -> token.getTokenType() + " " + token.getAccessToken()); } /** * Logs out the user if he is logged in. */ public Single logout() { return getStorage().getStoredAccessToken() .toObservable() .filter(token -> token != null) .concatMap(accessToken -> { RevokeAccessTokenBody body = new RevokeAccessTokenBody(accessToken.getAccessToken()); return TraktTvApiFactory.newApiService().revokeAccessToken(body); }) .doOnNext(x -> getStorage().removeAccessToken()).singleOrError(); } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/oauth2/TraktTvRefreshAccessTokenGrant.java ================================================ package de.rheinfabrik.heimdalldroid.network.oauth2; import de.rheinfabrik.heimdall2.OAuth2AccessToken; import de.rheinfabrik.heimdall2.grants.OAuth2RefreshAccessTokenGrant; import de.rheinfabrik.heimdalldroid.network.TraktTvApiFactory; import de.rheinfabrik.heimdalldroid.network.models.RefreshTokenRequestBody; import io.reactivex.Single; /** * TraktTv refresh token grant as described in http://docs.trakt.apiary.io/#reference/authentication-oauth/token/exchange-refresh_token-for-access_token. */ public class TraktTvRefreshAccessTokenGrant extends OAuth2RefreshAccessTokenGrant { // Properties public String clientSecret; public String clientId; public String redirectUri; // OAuth2RefreshAccessTokenGrant @Override public Single grantNewAccessToken() { RefreshTokenRequestBody body = new RefreshTokenRequestBody(getRefreshToken(), clientId, clientSecret, redirectUri, GRANT_TYPE); return TraktTvApiFactory.newApiService().refreshAccessToken(body).singleOrError(); } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/utils/AlertDialogFactory.java ================================================ package de.rheinfabrik.heimdalldroid.utils; import android.app.AlertDialog; import android.content.Context; import de.rheinfabrik.heimdalldroid.R; /** * Factory used to create AlertDialogs. */ public class AlertDialogFactory { // Public Api /** * Creates an error dialog. */ public static AlertDialog errorAlertDialog(Context context) { return new AlertDialog.Builder(context) .setTitle(R.string.error_title) .setMessage(R.string.error_message) .setPositiveButton(R.string.ok, (dialog, which) -> dialog.dismiss()).create(); } /** * Creates an dialog informing the user that there are no liked lists found. */ public static AlertDialog noListsFoundDialog(Context context) { return new AlertDialog.Builder(context) .setTitle(R.string.error_title) .setMessage(R.string.no_liked_lists_message) .setPositiveButton(R.string.ok, (dialog, which) -> dialog.dismiss()).create(); } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/utils/IntentFactory.java ================================================ package de.rheinfabrik.heimdalldroid.utils; import android.content.Context; import android.content.Intent; import de.rheinfabrik.heimdalldroid.actvities.LoginActivity; /** * Factory used to create Intents. */ public class IntentFactory { // Public Api /** * Creates an intent for showing the LoginActivity */ public static Intent loginIntent(Context context) { return new Intent(context, LoginActivity.class); } } ================================================ FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/utils/SharedPreferencesOAuth2AccessTokenStorage.java ================================================ package de.rheinfabrik.heimdalldroid.utils; import android.content.SharedPreferences; import com.google.gson.Gson; import de.rheinfabrik.heimdall2.OAuth2AccessToken; import de.rheinfabrik.heimdall2.OAuth2AccessTokenStorage; import io.reactivex.Single; /** * A simple storage that saves the access token as plain text in the passed shared preferences. * It is recommend to set the access mode to MODE_PRIVATE. * * @param The access token type. */ public class SharedPreferencesOAuth2AccessTokenStorage implements OAuth2AccessTokenStorage { // Constants private static final String ACCESS_TOKEN_PREFERENCES_KEY = "OAuth2AccessToken"; // Members private final SharedPreferences mSharedPreferences; private final Class mTokenClass; // Constructor /** * Designated constructor. * * @param sharedPreferences The shared preferences used for saving the access token. * @param tokenClass The actual class of the access token. */ public SharedPreferencesOAuth2AccessTokenStorage(SharedPreferences sharedPreferences, Class tokenClass) { super(); if (tokenClass == null) { throw new RuntimeException("TokenClass MUST NOT be null."); } if (sharedPreferences == null) { throw new RuntimeException("SharedPreferences MUST NOT be null."); } mTokenClass = tokenClass; mSharedPreferences = sharedPreferences; } // OAuth2AccessTokenStorage @Override public Single getStoredAccessToken() { return Single .just(mSharedPreferences.getString(ACCESS_TOKEN_PREFERENCES_KEY, null)) .map(json -> (TAccessToken) new Gson().fromJson(json, mTokenClass)); } @Override public void storeAccessToken(OAuth2AccessToken token) { mSharedPreferences .edit() .putString(ACCESS_TOKEN_PREFERENCES_KEY, new Gson().toJson(token)) .apply(); } @Override public Single hasAccessToken() { return Single.just(mSharedPreferences.contains(ACCESS_TOKEN_PREFERENCES_KEY)); } @Override public void removeAccessToken() { mSharedPreferences .edit() .remove(ACCESS_TOKEN_PREFERENCES_KEY) .apply(); } } ================================================ FILE: sample/src/main/res/layout/activity_login.xml ================================================ ================================================ FILE: sample/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: sample/src/main/res/layout/item_view_trakt_tv_list.xml ================================================ ================================================ FILE: sample/src/main/res/menu/menu_main.xml ================================================ ================================================ FILE: sample/src/main/res/values/colors.xml ================================================ #FFFFFFFF #F9F9F9 #FFD32F2F #FFF44336 #FF282B2E ================================================ FILE: sample/src/main/res/values/strings.xml ================================================ Heimdall.droid Oooops There was an error. Please try again later. There are no lists on your account. Please create some over at http://trakt.tv! OK " Likes" Logout ================================================ FILE: sample/src/main/res/values/styles.xml ================================================ ================================================ FILE: settings.gradle ================================================ include ':sample', ':library'