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.
# <img src="https://cloud.githubusercontent.com/assets/460060/8159821/b8bfeb32-136a-11e5-83ed-83b7fe01df3a.jpg" width="30" height="30"> 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).
[](https://jitpack.io/#trivago/Heimdall.droid)
[](https://travis-ci.org/trivago/Heimdall.droid)
[](http://android-arsenal.com/details/1/2016)
[](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<OAuth2AccessToken> storage = new SharedPreferencesOAuth2AccessTokenStorage<>(mySharedPreferences, OAuth2AccessToken.class);
OAuth2AccessTokenManager<> manager = new OAuth2AccessTokenManager<OAuth2AccessToken>(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<OAuth2AccessToken> {
// Constructor
@Override
public Observable<OAuth2AccessToken> 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<OAuth2AccessToken> {
// Constructor
@Override
public Observable<OAuth2AccessToken> 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<OAuth2AccessToken> {
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<OAuth2AccessToken> 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<OAuth2AccessToken> =
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<OAuth2AccessToken> =
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 <TAccessToken> The access token type.
*/
interface OAuth2AccessTokenStorage {
// Public API
/**
* Queries the stored access token.
*
* @return - An Observable emitting the stored access token.
*/
fun getStoredAccessToken(): Single<OAuth2AccessToken>
/**
* 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<Boolean>
/**
* 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 <TAccessToken> 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<URL>()
// Private Members
private val mAuthorizationUrlSubject = BehaviorSubject.create<URL>()
// 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<OAuth2AccessToken>
// Public API
/**
* Observable emitting the authorization Uri.
*/
fun authorizationUri() = mAuthorizationUrlSubject
override fun grantNewAccessToken(): Single<OAuth2AccessToken> {
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<String, MutableList<String>> {
val queryParams = linkedMapOf<String, MutableList<String>>()
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 <TAccessToken> 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<OAuth2AccessToken>
}
================================================
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 <TAccessToken> 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<OAuth2AccessToken>().apply {
whenever(isExpired()).thenReturn(false)
}
// and a token manager with a valid storage and token
val storage = mock<OAuth2AccessTokenStorage>().apply {
whenever(getStoredAccessToken()).thenReturn(Single.just(accessToken))
}
val grant = mock<OAuth2RefreshAccessTokenGrant>().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<OAuth2AccessToken>().apply {
whenever(isExpired()).thenReturn(true)
}
// and a token manager
val tokenManager = OAuth2AccessTokenManager(
mStorage = mock<OAuth2AccessTokenStorage>().apply {
whenever(getStoredAccessToken()).thenReturn(Single.just(accessToken))
}
)
// and a OAuth2RefreshAccessTokenGrant
val grant = mock<OAuth2RefreshAccessTokenGrant>()
// 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<OAuth2AccessTokenStorage>().apply {
whenever(getStoredAccessToken()).thenReturn(Single.just(accessToken))
}
)
// and a grant
val grant = mock<OAuth2RefreshAccessTokenGrant>().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<OAuth2Grant>().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<OAuth2Grant>().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<OAuth2Grant>().apply {
whenever(grantNewAccessToken()).thenReturn(Single.just(accessToken))
}
// and a token manager
val storage = mock<OAuth2AccessTokenStorage>()
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<OAuth2AccessToken>()
val accessTokenStorage = mock<OAuth2AccessTokenStorage>()
whenever(accessTokenStorage.getStoredAccessToken()).thenReturn(Single.just(validToken))
// and a refresh access token grant
val refreshAccessTokenGrant = mock<OAuth2RefreshAccessTokenGrant>()
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
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.rheinfabrik.heimdalldroid">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Application -->
<application
android:allowBackup="true"
android:appComponentFactory="whateverString"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppThemeNoActionBar"
tools:replace="android:appComponentFactory">
<!-- Main Activity -->
<activity
android:name="de.rheinfabrik.heimdalldroid.actvities.MainActivity"
android:screenOrientation="portrait">
<!--Intent filter -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Login Activity -->
<activity
android:name="de.rheinfabrik.heimdalldroid.actvities.LoginActivity"
android:screenOrientation="portrait" />
</application>
</manifest>
================================================
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<List<TraktTvList>> 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<TraktTvList> 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<TraktTvListViewHolder> {
// Members
private final List<TraktTvList> mTraktTvLists;
// Constructor
public TraktTvListsRecyclerViewAdapter(List<TraktTvList> 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<OAuth2AccessToken> grantNewAccessToken(@Body AccessTokenRequestBody body);
@POST("/oauth/token")
Observable<OAuth2AccessToken> refreshAccessToken(@Body RefreshTokenRequestBody body);
@POST("/oauth/revoke")
Observable<Void> revokeAccessToken(@Body RevokeAccessTokenBody body);
// GET
@GET("/users/me/lists")
Observable<List<TraktTvList>> 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<OAuth2AccessToken> 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<OAuth2AccessToken> 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<String> 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<Void> 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<OAuth2AccessToken> 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 <TAccessToken> The access token type.
*/
public class SharedPreferencesOAuth2AccessTokenStorage<TAccessToken extends OAuth2AccessToken> 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<OAuth2AccessToken> 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<Boolean> 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
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- WebView -->
<WebView android:id="@+id/webView"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
================================================
FILE: sample/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- Toolbar -->
<android.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/red_orange"
android:elevation="1dp"
android:theme="@style/AppThemeNoActionBar" />
<!-- Swipe refresh layout -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/hint_of_red" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
================================================
FILE: sample/src/main/res/layout/item_view_trakt_tv_list.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Card view -->
<android.support.v7.widget.CardView
android:id="@+id/search_result_card"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
card_view:cardCornerRadius="4dp">
<!-- Container -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp">
<!-- Title -->
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:textColor="@color/bunker"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Title"/>
<!-- Description -->
<TextView
android:id="@+id/descriptionTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/titleTextView"
android:layout_toLeftOf="@+id/likeCountTextView"
android:textColor="@color/bunker"
android:textSize="14sp"
tools:text="Description"/>
<!-- Number of likes -->
<TextView
android:id="@+id/likeCountTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:textColor="@color/red_orange"
android:textSize="14sp"
tools:text="100 Likes"/>
</RelativeLayout>
</android.support.v7.widget.CardView>
================================================
FILE: sample/src/main/res/menu/menu_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<!-- Logout -->
<item
android:id="@+id/logout"
android:title="@string/menu_action_logout"
app:showAsAction="always"/>
</menu>
================================================
FILE: sample/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="white">#FFFFFFFF</color>
<color name="hint_of_red">#F9F9F9</color>
<color name="persian_red">#FFD32F2F</color>
<color name="red_orange">#FFF44336</color>
<color name="bunker">#FF282B2E</color>
</resources>
================================================
FILE: sample/src/main/res/values/strings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- The Application name -->
<string name="app_name">Heimdall.droid</string>
<!-- Dialogs -->
<string name="error_title">Oooops</string>
<string name="error_message">There was an error. Please try again later.</string>
<string name="no_liked_lists_message">There are no lists on your account. Please create some
over at http://trakt.tv!
</string>
<string name="ok">OK</string>
<!-- Item views -->
<string name="likes_postfix">" Likes"</string>
<string name="menu_action_logout">Logout</string>
</resources>
================================================
FILE: sample/src/main/res/values/styles.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppThemeNoActionBar" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Colors -->
<item name="colorPrimary">@color/red_orange</item>
<item name="colorPrimaryDark">@color/persian_red</item>
<item name="android:textColorPrimary">@color/white</item>
</style>
</resources>
================================================
FILE: settings.gradle
================================================
include ':sample', ':library'
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
SYMBOL INDEX (62 symbols across 17 files)
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/TraktTvAPIConfiguration.java
class TraktTvAPIConfiguration (line 3) | public class TraktTvAPIConfiguration {
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/actvities/LoginActivity.java
class LoginActivity (line 28) | public class LoginActivity extends RxAppCompatActivity {
method onCreate (line 37) | @Override
method authorize (line 53) | private void authorize() {
method handleSuccess (line 99) | private void handleSuccess() {
method handleError (line 105) | private void handleError() {
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/actvities/MainActivity.java
class MainActivity (line 37) | public class MainActivity extends RxAppCompatActivity {
method onCreate (line 58) | @Override
method onActivityResult (line 86) | @Override
method onCreateOptionsMenu (line 100) | @Override
method onOptionsItemSelected (line 108) | @Override
method loadLists (line 119) | private void loadLists() {
method showLogin (line 144) | private void showLogin() {
method handleEmptyList (line 152) | private void handleEmptyList() {
method handleError (line 159) | private void handleError(Throwable error) {
method handleSuccess (line 176) | private void handleSuccess(List<TraktTvList> traktTvLists) {
method refresh (line 183) | private void refresh() {
method logout (line 200) | private void logout() {
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/adapter/TraktTvListsRecyclerViewAdapter.java
class TraktTvListsRecyclerViewAdapter (line 17) | public class TraktTvListsRecyclerViewAdapter extends RecyclerView.Adapte...
method TraktTvListsRecyclerViewAdapter (line 25) | public TraktTvListsRecyclerViewAdapter(List<TraktTvList> traktTvLists) {
method onCreateViewHolder (line 33) | @Override
method onBindViewHolder (line 39) | @Override
method getItemCount (line 44) | @Override
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/adapter/viewholder/TraktTvListViewHolder.java
class TraktTvListViewHolder (line 15) | public class TraktTvListViewHolder extends RecyclerView.ViewHolder {
method TraktTvListViewHolder (line 30) | public TraktTvListViewHolder(View itemView) {
method bind (line 39) | public void bind(TraktTvList traktTvList) {
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/TraktTvApiFactory.java
class TraktTvApiFactory (line 12) | public class TraktTvApiFactory {
method newApiService (line 23) | public static TraktTvApiService newApiService() {
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/TraktTvApiService.java
type TraktTvApiService (line 19) | public interface TraktTvApiService {
method grantNewAccessToken (line 23) | @POST("/oauth/token")
method refreshAccessToken (line 26) | @POST("/oauth/token")
method revokeAccessToken (line 29) | @POST("/oauth/revoke")
method getLists (line 34) | @GET("/users/me/lists")
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/AccessTokenRequestBody.java
class AccessTokenRequestBody (line 10) | public class AccessTokenRequestBody implements Serializable {
method AccessTokenRequestBody (line 31) | public AccessTokenRequestBody(String code, String clientId, String red...
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/RefreshTokenRequestBody.java
class RefreshTokenRequestBody (line 10) | public class RefreshTokenRequestBody implements Serializable {
method RefreshTokenRequestBody (line 31) | public RefreshTokenRequestBody(String refreshToken, String clientId, S...
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/RevokeAccessTokenBody.java
class RevokeAccessTokenBody (line 8) | public class RevokeAccessTokenBody {
method RevokeAccessTokenBody (line 17) | public RevokeAccessTokenBody(String accessToken) {
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/TraktTvList.java
class TraktTvList (line 10) | public class TraktTvList implements Serializable {
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/oauth2/TraktTvAuthorizationCodeGrant.java
class TraktTvAuthorizationCodeGrant (line 16) | public class TraktTvAuthorizationCodeGrant extends OAuth2AuthorizationCo...
method buildAuthorizationUrl (line 24) | @Override
method exchangeTokenUsingCode (line 41) | @Override
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/oauth2/TraktTvOauth2AccessTokenManager.java
class TraktTvOauth2AccessTokenManager (line 18) | public final class TraktTvOauth2AccessTokenManager extends OAuth2AccessT...
method from (line 25) | public static TraktTvOauth2AccessTokenManager from(Context context) {
method TraktTvOauth2AccessTokenManager (line 39) | public TraktTvOauth2AccessTokenManager(OAuth2AccessTokenStorage storag...
method newAuthorizationCodeGrant (line 48) | public TraktTvAuthorizationCodeGrant newAuthorizationCodeGrant() {
method getValidAccessToken (line 60) | public Single<String> getValidAccessToken() {
method logout (line 72) | public Single<Void> logout() {
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/network/oauth2/TraktTvRefreshAccessTokenGrant.java
class TraktTvRefreshAccessTokenGrant (line 12) | public class TraktTvRefreshAccessTokenGrant extends OAuth2RefreshAccessT...
method grantNewAccessToken (line 22) | @Override
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/utils/AlertDialogFactory.java
class AlertDialogFactory (line 11) | public class AlertDialogFactory {
method errorAlertDialog (line 18) | public static AlertDialog errorAlertDialog(Context context) {
method noListsFoundDialog (line 28) | public static AlertDialog noListsFoundDialog(Context context) {
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/utils/IntentFactory.java
class IntentFactory (line 11) | public class IntentFactory {
method loginIntent (line 18) | public static Intent loginIntent(Context context) {
FILE: sample/src/main/java/de/rheinfabrik/heimdalldroid/utils/SharedPreferencesOAuth2AccessTokenStorage.java
class SharedPreferencesOAuth2AccessTokenStorage (line 17) | public class SharedPreferencesOAuth2AccessTokenStorage<TAccessToken exte...
method SharedPreferencesOAuth2AccessTokenStorage (line 36) | public SharedPreferencesOAuth2AccessTokenStorage(SharedPreferences sha...
method getStoredAccessToken (line 53) | @Override
method storeAccessToken (line 60) | @Override
method hasAccessToken (line 68) | @Override
method removeAccessToken (line 73) | @Override
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (103K chars).
[
{
"path": ".gitignore",
"chars": 646,
"preview": "# Built application files\n*.apk\n*.ap_\nbin/\ngen/\nclasses/\ngen-external-apklibs/\n\n# Eclipse project files\n.classpath\n.proj"
},
{
"path": ".travis.yml",
"chars": 827,
"preview": "language: android\nsudo: required\njdk: oraclejdk8\n\nenv:\n global:\n - ANDROID_API_LEVEL=30\n - ANDROID_BUILD_TOOLS_VE"
},
{
"path": "LICENSE.txt",
"chars": 11339,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 6561,
"preview": "# Deprecated Project\n\n⚠️ This project is no longer maintained\n\nThis repository is deprecated and will no longer receive "
},
{
"path": "build.gradle",
"chars": 470,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n r"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 233,
"preview": "#Tue Oct 24 09:19:37 CEST 2017\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER"
},
{
"path": "gradle.properties",
"chars": 909,
"preview": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will o"
},
{
"path": "gradlew",
"chars": 5080,
"preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n## Gradle start "
},
{
"path": "gradlew.bat",
"chars": 2404,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
},
{
"path": "library/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "library/build.gradle",
"chars": 976,
"preview": "apply plugin: 'kotlin'\napply plugin: 'groovy'\napply plugin: 'maven'\n\nbuildscript {\n ext.kotlin_version = '1.4.10'\n\n "
},
{
"path": "library/proguard-rules.pro",
"chars": 662,
"preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /U"
},
{
"path": "library/src/main/java/de/rheinfabrik/heimdall2/OAuth2AccessToken.kt",
"chars": 1728,
"preview": "package de.rheinfabrik.heimdall2\n\nimport com.google.gson.annotations.SerializedName\nimport java.io.Serializable\nimport j"
},
{
"path": "library/src/main/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenManager.kt",
"chars": 2289,
"preview": "package de.rheinfabrik.heimdall2\n\nimport de.rheinfabrik.heimdall2.grants.OAuth2Grant\nimport de.rheinfabrik.heimdall2.gra"
},
{
"path": "library/src/main/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenStorage.kt",
"chars": 936,
"preview": "package de.rheinfabrik.heimdall2\n\nimport io.reactivex.Single\n\n/**\n * Interface used to define how to store and retrieve "
},
{
"path": "library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2AuthorizationCodeGrant.kt",
"chars": 3882,
"preview": "package de.rheinfabrik.heimdall2.grants\n\nimport de.rheinfabrik.heimdall2.OAuth2AccessToken\nimport io.reactivex.Observabl"
},
{
"path": "library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2ClientCredentialsGrant.kt",
"chars": 468,
"preview": "package de.rheinfabrik.heimdall2.grants\n\nabstract class OAuth2ClientCredentialsGrant(\n /**\n * OPTIONAL\n * The"
},
{
"path": "library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2Grant.kt",
"chars": 537,
"preview": "package de.rheinfabrik.heimdall2.grants\n\nimport de.rheinfabrik.heimdall2.OAuth2AccessToken\nimport io.reactivex.Single\n\n/"
},
{
"path": "library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2ImplicitGrant.kt",
"chars": 1340,
"preview": "package de.rheinfabrik.heimdall2.grants\n\n/**\n * Class representing the Implicit Grant as described in https://tools.ietf"
},
{
"path": "library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2RefreshAccessTokenGrant.kt",
"chars": 576,
"preview": "package de.rheinfabrik.heimdall2.grants\n\nabstract class OAuth2RefreshAccessTokenGrant(\n /**\n * REQUIRED\n * Th"
},
{
"path": "library/src/main/java/de/rheinfabrik/heimdall2/grants/OAuth2ResourceOwnerPasswordCredentialsGrant.kt",
"chars": 590,
"preview": "package de.rheinfabrik.heimdall2.grants\n\nabstract class OAuth2ResourceOwnerPasswordCredentialsGrant(\n /**\n * REQU"
},
{
"path": "library/src/test/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenIsExpiredTest.kt",
"chars": 1541,
"preview": "package de.rheinfabrik.heimdall2\n\nimport org.junit.Assert.assertEquals\nimport org.junit.Test\nimport java.util.*\n\nclass O"
},
{
"path": "library/src/test/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenManagerGetValidAccessTokenTest.kt",
"chars": 3225,
"preview": "package de.rheinfabrik.heimdall2\n\nimport com.nhaarman.mockitokotlin2.mock\nimport com.nhaarman.mockitokotlin2.verify\nimpo"
},
{
"path": "library/src/test/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenManagerGrantNewAccessTokenTest.kt",
"chars": 3219,
"preview": "package de.rheinfabrik.heimdall2\n\nimport com.nhaarman.mockitokotlin2.mock\nimport com.nhaarman.mockitokotlin2.verify\nimpo"
},
{
"path": "library/src/test/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenManagerTest.kt",
"chars": 1205,
"preview": "package de.rheinfabrik.heimdall2\n\nimport com.nhaarman.mockitokotlin2.mock\nimport com.nhaarman.mockitokotlin2.whenever\nim"
},
{
"path": "library/src/test/java/de/rheinfabrik/heimdall2/OAuth2AccessTokenSerializationTest.kt",
"chars": 2396,
"preview": "package de.rheinfabrik.heimdall2\n\nimport com.google.gson.Gson\nimport org.junit.Assert.assertEquals\nimport org.junit.Befo"
},
{
"path": "library/src/test/java/de/rheinfabrik/heimdall2/grants/OAuth2AuthorizationCodeGrantTest.kt",
"chars": 660,
"preview": "package de.rheinfabrik.heimdall2.grants\n\nimport org.junit.Assert.assertEquals\nimport org.junit.Test\n\nclass OAuth2Authori"
},
{
"path": "library/src/test/java/de/rheinfabrik/heimdall2/grants/OAuth2ClientCredentialsGrantTest.kt",
"chars": 528,
"preview": "package de.rheinfabrik.heimdall2.grants\n\nimport org.junit.Assert.assertEquals\nimport org.junit.Test\n\nclass OAuth2ClientC"
},
{
"path": "library/src/test/java/de/rheinfabrik/heimdall2/grants/OAuth2ImplicitGrantTest.kt",
"chars": 496,
"preview": "package de.rheinfabrik.heimdall2.grants\n\nimport org.junit.Assert.assertEquals\nimport org.junit.Test\n\nclass OAuth2Implici"
},
{
"path": "library/src/test/java/de/rheinfabrik/heimdall2/grants/OAuth2RefreshAccessTokenGrantTest.kt",
"chars": 449,
"preview": "package de.rheinfabrik.heimdall2.grants\n\nimport org.junit.Assert.assertEquals\nimport org.junit.Test\n\nclass OAuth2Refresh"
},
{
"path": "library/src/test/java/de/rheinfabrik/heimdall2/grants/OAuth2ResourceOwnerPasswordCredentialsGrantTest.kt",
"chars": 465,
"preview": "package de.rheinfabrik.heimdall2.grants\n\nimport org.junit.Assert.assertEquals\nimport org.junit.Test\n\nclass OAuth2Resourc"
},
{
"path": "library/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker",
"chars": 17,
"preview": "mock-maker-inline"
},
{
"path": "sample/build.gradle",
"chars": 1362,
"preview": "apply plugin: 'com.android.application'\n\nbuildscript {\n repositories {\n mavenCentral()\n }\n}\n\nandroid {\n "
},
{
"path": "sample/proguard-rules.pro",
"chars": 662,
"preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /U"
},
{
"path": "sample/src/main/AndroidManifest.xml",
"chars": 1230,
"preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:tools=\"http://schemas.android.com/tools\"\n"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/TraktTvAPIConfiguration.java",
"chars": 310,
"preview": "package de.rheinfabrik.heimdalldroid;\n\npublic class TraktTvAPIConfiguration {\n public static final String CLIENT_ID ="
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/actvities/LoginActivity.java",
"chars": 3450,
"preview": "package de.rheinfabrik.heimdalldroid.actvities;\n\nimport android.graphics.Bitmap;\nimport android.os.Bundle;\nimport androi"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/actvities/MainActivity.java",
"chars": 6819,
"preview": "package de.rheinfabrik.heimdalldroid.actvities;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport andr"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/adapter/TraktTvListsRecyclerViewAdapter.java",
"chars": 1408,
"preview": "package de.rheinfabrik.heimdalldroid.adapter;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport andr"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/adapter/viewholder/TraktTvListViewHolder.java",
"chars": 1268,
"preview": "package de.rheinfabrik.heimdalldroid.adapter.viewholder;\n\nimport android.view.View;\nimport android.widget.TextView;\n\nimp"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/network/TraktTvApiFactory.java",
"chars": 1252,
"preview": "package de.rheinfabrik.heimdalldroid.network;\n\nimport com.google.gson.Gson;\n\nimport de.rheinfabrik.heimdalldroid.TraktTv"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/network/TraktTvApiService.java",
"chars": 1170,
"preview": "package de.rheinfabrik.heimdalldroid.network;\n\nimport io.reactivex.Observable;\nimport java.util.List;\n\nimport de.rheinfa"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/AccessTokenRequestBody.java",
"chars": 953,
"preview": "package de.rheinfabrik.heimdalldroid.network.models;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.io"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/RefreshTokenRequestBody.java",
"chars": 976,
"preview": "package de.rheinfabrik.heimdalldroid.network.models;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.io"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/RevokeAccessTokenBody.java",
"chars": 420,
"preview": "package de.rheinfabrik.heimdalldroid.network.models;\n\nimport com.google.gson.annotations.SerializedName;\n\n/**\n * Body us"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/network/models/TraktTvList.java",
"chars": 431,
"preview": "package de.rheinfabrik.heimdalldroid.network.models;\n\nimport com.google.gson.annotations.SerializedName;\n\nimport java.io"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/network/oauth2/TraktTvAuthorizationCodeGrant.java",
"chars": 1668,
"preview": "package de.rheinfabrik.heimdalldroid.network.oauth2;\n\nimport android.net.Uri;\n\nimport de.rheinfabrik.heimdall2.grants.OA"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/network/oauth2/TraktTvOauth2AccessTokenManager.java",
"chars": 3365,
"preview": "package de.rheinfabrik.heimdalldroid.network.oauth2;\n\nimport android.content.Context;\nimport android.content.SharedPrefe"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/network/oauth2/TraktTvRefreshAccessTokenGrant.java",
"chars": 1049,
"preview": "package de.rheinfabrik.heimdalldroid.network.oauth2;\n\nimport de.rheinfabrik.heimdall2.OAuth2AccessToken;\nimport de.rhein"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/utils/AlertDialogFactory.java",
"chars": 1042,
"preview": "package de.rheinfabrik.heimdalldroid.utils;\n\nimport android.app.AlertDialog;\nimport android.content.Context;\n\nimport de."
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/utils/IntentFactory.java",
"chars": 455,
"preview": "package de.rheinfabrik.heimdalldroid.utils;\n\nimport android.content.Context;\nimport android.content.Intent;\n\nimport de.r"
},
{
"path": "sample/src/main/java/de/rheinfabrik/heimdalldroid/utils/SharedPreferencesOAuth2AccessTokenStorage.java",
"chars": 2412,
"preview": "package de.rheinfabrik.heimdalldroid.utils;\n\nimport android.content.SharedPreferences;\n\nimport com.google.gson.Gson;\n\nim"
},
{
"path": "sample/src/main/res/layout/activity_login.xml",
"chars": 253,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!-- WebView -->\n<WebView android:id=\"@+id/webView\"\n xmlns:android=\"http"
},
{
"path": "sample/src/main/res/layout/activity_main.xml",
"chars": 1128,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n and"
},
{
"path": "sample/src/main/res/layout/item_view_trakt_tv_list.xml",
"chars": 2191,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!-- Card view -->\n<android.support.v7.widget.CardView\n android:id=\"@+id/sear"
},
{
"path": "sample/src/main/res/menu/menu_main.xml",
"chars": 329,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:ap"
},
{
"path": "sample/src/main/res/values/colors.xml",
"chars": 293,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<resources>\n\n <color name=\"white\">#FFFFFFFF</color>\n <color name=\"hint_of_"
},
{
"path": "sample/src/main/res/values/strings.xml",
"chars": 616,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<resources>\n\n <!-- The Application name -->\n <string name=\"app_name\">Heimd"
},
{
"path": "sample/src/main/res/values/styles.xml",
"chars": 414,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<resources>\n\n <!-- Base application theme. -->\n <style name=\"AppThemeNoAct"
},
{
"path": "settings.gradle",
"chars": 30,
"preview": "include ':sample', ':library'\n"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the rheinfabrik/Heimdall.droid GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (91.4 KB), approximately 24.0k tokens, and a symbol index with 62 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.