From 6bf6c5ccac6bce736afe392f5223b3b6a593b933 Mon Sep 17 00:00:00 2001 From: patloew Date: Sat, 5 Mar 2016 00:24:39 +0100 Subject: [PATCH] Initial commit. --- .gitignore | 8 + LICENSE | 13 + NOTICE | 46 + README.md | 56 + build.gradle | 32 + gradle.properties | 18 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 ++ gradlew.bat | 90 + library/.gitignore | 1 + library/build.gradle | 65 + library/proguard-rules.pro | 17 + library/src/main/AndroidManifest.xml | 13 + .../com/patloew/rxfit/BaseObservable.java | 182 ++ .../rxfit/BleClaimDeviceObservable.java | 43 + .../BleListClaimedDevicesObservable.java | 53 + .../patloew/rxfit/BleStartScanObservable.java | 44 + .../patloew/rxfit/BleStopScanObservable.java | 30 + .../rxfit/BleUnclaimDeviceObservable.java | 43 + .../ConfigCreateCustomDataTypeObservable.java | 42 + .../rxfit/ConfigDisableFitObservable.java | 26 + .../rxfit/ConfigReadDataTypeObservable.java | 41 + .../rxfit/GoogleAPIClientObservable.java | 52 + .../rxfit/GoogleAPIConnectionException.java | 30 + ...GoogleAPIConnectionSuspendedException.java | 27 + .../rxfit/HistoryDeleteDataObservable.java | 30 + .../rxfit/HistoryInsertDataObservable.java | 30 + .../HistoryReadDailyTotalObservable.java | 42 + .../rxfit/HistoryReadDataObservable.java | 41 + .../rxfit/HistoryUpdateDataObservable.java | 30 + .../rxfit/PermissionRequiredException.java | 13 + .../RecordingListSubscriptionsObservable.java | 54 + .../rxfit/RecordingSubscribeObservable.java | 44 + .../rxfit/RecordingUnsubscribeObservable.java | 53 + .../com/patloew/rxfit/ResolutionActivity.java | 61 + .../main/java/com/patloew/rxfit/RxFit.java | 264 +++ .../SensorsAddDataPointIntentObservable.java | 33 + .../rxfit/SensorsDataPointObservable.java | 53 + .../SensorsFindDataSourcesObservable.java | 56 + ...ensorsRemoveDataPointIntentObservable.java | 30 + .../rxfit/SessionInsertObservable.java | 30 + .../patloew/rxfit/SessionReadObservable.java | 41 + .../rxfit/SessionRegisterObservable.java | 30 + .../patloew/rxfit/SessionStartObservable.java | 30 + .../patloew/rxfit/SessionStopObservable.java | 43 + .../rxfit/SessionUnregisterObservable.java | 30 + .../com/patloew/rxfit/StatusException.java | 29 + .../patloew/rxfit/StatusResultCallBack.java | 27 + library/src/main/res/values/strings.xml | 3 + .../java/com/patloew/rxfit/RxFitTest.java | 1511 +++++++++++++++++ sample/.gitignore | 1 + sample/build.gradle | 44 + sample/proguard-rules.pro | 17 + sample/src/main/AndroidManifest.xml | 20 + .../com/patloew/rxfitsample/MainActivity.java | 185 ++ sample/src/main/res/layout/activity_main.xml | 21 + .../res/layout/list_item_fitness_session.xml | 61 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes sample/src/main/res/values-w820dp/dimens.xml | 6 + sample/src/main/res/values/colors.xml | 6 + sample/src/main/res/values/dimens.xml | 5 + sample/src/main/res/values/strings.xml | 3 + sample/src/main/res/values/styles.xml | 11 + settings.gradle | 1 + 69 files changed, 4127 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 library/.gitignore create mode 100644 library/build.gradle create mode 100644 library/proguard-rules.pro create mode 100644 library/src/main/AndroidManifest.xml create mode 100644 library/src/main/java/com/patloew/rxfit/BaseObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/BleClaimDeviceObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/BleListClaimedDevicesObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/BleStartScanObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/BleStopScanObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/BleUnclaimDeviceObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/ConfigCreateCustomDataTypeObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/ConfigDisableFitObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/ConfigReadDataTypeObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/GoogleAPIClientObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/GoogleAPIConnectionException.java create mode 100644 library/src/main/java/com/patloew/rxfit/GoogleAPIConnectionSuspendedException.java create mode 100644 library/src/main/java/com/patloew/rxfit/HistoryDeleteDataObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/HistoryInsertDataObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/HistoryReadDailyTotalObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/HistoryReadDataObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/HistoryUpdateDataObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/PermissionRequiredException.java create mode 100644 library/src/main/java/com/patloew/rxfit/RecordingListSubscriptionsObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/RecordingSubscribeObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/RecordingUnsubscribeObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/ResolutionActivity.java create mode 100644 library/src/main/java/com/patloew/rxfit/RxFit.java create mode 100644 library/src/main/java/com/patloew/rxfit/SensorsAddDataPointIntentObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/SensorsDataPointObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/SensorsFindDataSourcesObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/SensorsRemoveDataPointIntentObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/SessionInsertObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/SessionReadObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/SessionRegisterObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/SessionStartObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/SessionStopObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/SessionUnregisterObservable.java create mode 100644 library/src/main/java/com/patloew/rxfit/StatusException.java create mode 100644 library/src/main/java/com/patloew/rxfit/StatusResultCallBack.java create mode 100644 library/src/main/res/values/strings.xml create mode 100644 library/src/test/java/com/patloew/rxfit/RxFitTest.java create mode 100644 sample/.gitignore create mode 100644 sample/build.gradle create mode 100644 sample/proguard-rules.pro create mode 100644 sample/src/main/AndroidManifest.xml create mode 100644 sample/src/main/java/com/patloew/rxfitsample/MainActivity.java create mode 100644 sample/src/main/res/layout/activity_main.xml create mode 100644 sample/src/main/res/layout/list_item_fitness_session.xml create mode 100644 sample/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 sample/src/main/res/values-w820dp/dimens.xml create mode 100644 sample/src/main/res/values/colors.xml create mode 100644 sample/src/main/res/values/dimens.xml create mode 100644 sample/src/main/res/values/strings.xml create mode 100644 sample/src/main/res/values/styles.xml create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..763cd2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2016 Patrick Löwenstein + +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. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..aecf397 --- /dev/null +++ b/NOTICE @@ -0,0 +1,46 @@ +RxFit +Copyright 2016 Patrick Löwenstein + +=========================== +Apache License, Version 2.0 +=========================== + +The following components are provided under the Apache License, Version 2.0. See project link for details. + +------ +RxJava +------ + +io.reactivex:rxjava +https://github.com/ReactiveX/RxJava + +Copyright 2013 Netflix, Inc. + +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. + + +------------------------ +Android-ReactiveLocation +------------------------ + +pl.charmas.android:android-reactive-location +https://github.com/mcharmas/Android-ReactiveLocation + + +Copyright (C) 2015 Michał Charmas (http://blog.charmas.pl) + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e078eae --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Reactive Fit API Library for Android + +This library wraps the Fit API in [RxJava](https://github.com/ReactiveX/RxJava) Observables. No more managing GoogleApiClients! Also, the authorization process for using fitness data is handled by the lib. + +# Usage + +Initialize RxFit once, preferably in your Application `onCreate()` via `RxFit.init(...)`. Make sure to include all the APIs and Scopes that you need for your app. The RxFit class is very similar to the Fitness class provided by the Fit API. Instead of `Fitness.HistoryApi.readData(apiClient, dataReadRequest)` you can use `RxFit.History.read(dataReadRequest)`. Make sure to have the Location and Body Sensors permission from Marshmallow on, if they are needed by your Fit API requests. + +Example: + +```java +RxFit.init( + context, + new Api[] { Fitness.HISTORY_API }, + new Scope[] { new Scope(Scopes.FITNESS_ACTIVITY_READ) } +); + +DataReadRequest dataReadRequest = new DataReadRequest.Builder() + .aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA) + .aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED) + .bucketBySession(1, TimeUnit.MINUTES) + .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS) + .build(); + +RxFit.History.read(dataReadRequest) + .flatMap(dataReadResult -> Observable.from(dataReadResult.getBuckets())) + .subscribe(bucket -> { + /* do something */ + }); +``` + +You can also obtain an `Observable`, which connects on subscribe and disconnects on unsubscribe via `GoogleAPIClientObservable.create(...)`. + +# Sample + +A basic sample app is available in the `sample` project. You need to create an OAuth 2.0 Client ID for the sample app, see the [guide in the Fit API docs](https://developers.google.com/fit/android/get-api-key). + +# Credits + +The code for managing the GoogleApiClient is taken from the [Android-ReactiveLocation](https://github.com/mcharmas/Android-ReactiveLocation) library by Michał Charmas, which is licensed under the Apache License, Version 2.0. + +# License + + Copyright 2016 Patrick Löwenstein + + 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. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..16a9eb1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,32 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.0.0-beta6' + + classpath 'me.tatarka:gradle-retrolambda:3.3.0-beta4' + classpath 'me.tatarka.retrolambda.projectlombok:lombok.ast:0.2.3.a2' + + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2' + classpath "com.github.dcendents:android-maven-gradle-plugin:1.3" + + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } + + configurations.classpath.exclude group: 'com.android.tools.external.lombok' +} + +allprojects { + repositories { + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1d3591c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# 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 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d0574d2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Dec 28 10:00:20 PST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/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 + +# 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\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +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"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # 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 "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@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 diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..c40da24 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,65 @@ +apply plugin: 'com.android.library' +apply plugin: 'com.jfrog.bintray' +apply plugin: 'com.github.dcendents.android-maven' + +group = 'com.patloew.rxfit' +version = '1.0.0' +project.archivesBaseName = 'rxfit' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 23 + versionCode 1 + versionName "1.0.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + testOptions { + unitTests.returnDefaultValues = true + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:23.2.0' + compile 'io.reactivex:rxjava:1.1.1' + compile 'com.google.android.gms:play-services-fitness:8.4.0' + + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile "org.powermock:powermock-module-junit4:1.6.4" + testCompile "org.powermock:powermock-module-junit4-rule:1.6.4" + testCompile "org.powermock:powermock-api-mockito:1.6.4" +} + +task generateSourcesJar(type: Jar) { + from android.sourceSets.main.java.srcDirs + classifier 'sources' +} + +task generateJavadocs(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath() + .join(File.pathSeparator)) +} + +task generateJavadocsJar(type: Jar) { + from generateJavadocs.destinationDir + classifier 'javadoc' +} + +generateJavadocsJar.dependsOn generateJavadocs + +artifacts { + archives generateJavadocsJar + archives generateSourcesJar +} + diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..1d26e0f --- /dev/null +++ b/library/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/patricklowenstein/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 *; +#} diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e944a2e --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/library/src/main/java/com/patloew/rxfit/BaseObservable.java b/library/src/main/java/com/patloew/rxfit/BaseObservable.java new file mode 100644 index 0000000..6209f0d --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/BaseObservable.java @@ -0,0 +1,182 @@ +package com.patloew.rxfit; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Scope; + +import java.util.ArrayList; +import java.util.List; + +import rx.Observable; +import rx.Observer; +import rx.Subscriber; +import rx.functions.Action0; +import rx.subscriptions.Subscriptions; + +/* Copyright (C) 2015 Michał Charmas (http://blog.charmas.pl) + * + * 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 MODIFIED by Patrick Löwenstein, 2016 + * + */ +public abstract class BaseObservable implements Observable.OnSubscribe { + private static final List observableList = new ArrayList<>(); + + private final Context ctx; + private final Api[] services; + private final Scope[] scopes; + private final boolean handleResolution; + private GoogleApiClient apiClient; + Subscriber subscriber; + + protected BaseObservable(@NonNull RxFit rxFit) { + this.ctx = rxFit.getContext(); + this.services = rxFit.getApis(); + this.scopes = rxFit.getScopes(); + handleResolution = true; + } + + protected BaseObservable(@NonNull Context ctx, @NonNull Api[] services, Scope[] scopes) { + this.ctx = ctx; + this.services = services; + this.scopes = scopes; + handleResolution = false; + } + + @Override + public void call(Subscriber subscriber) { + this.subscriber = subscriber; + + apiClient = createApiClient(subscriber); + + try { + apiClient.connect(); + } catch (Throwable ex) { + subscriber.onError(ex); + } + + subscriber.add(Subscriptions.create(new Action0() { + @Override + public void call() { + if (apiClient.isConnected() || apiClient.isConnecting()) { + onUnsubscribed(apiClient); + apiClient.disconnect(); + } + } + })); + } + + + protected GoogleApiClient createApiClient(Subscriber subscriber) { + + ApiClientConnectionCallbacks apiClientConnectionCallbacks = new ApiClientConnectionCallbacks(subscriber); + + GoogleApiClient.Builder apiClientBuilder = new GoogleApiClient.Builder(ctx); + + + for (Api service : services) { + apiClientBuilder.addApi(service); + } + + if(scopes != null) { + for (Scope scope : scopes) { + apiClientBuilder.addScope(scope); + } + } + + apiClientBuilder.addConnectionCallbacks(apiClientConnectionCallbacks); + apiClientBuilder.addOnConnectionFailedListener(apiClientConnectionCallbacks); + + GoogleApiClient apiClient = apiClientBuilder.build(); + + apiClientConnectionCallbacks.setClient(apiClient); + + return apiClient; + + } + + protected void onUnsubscribed(GoogleApiClient locationClient) { } + + protected abstract void onGoogleApiClientReady(GoogleApiClient apiClient, Observer observer); + + private class ApiClientConnectionCallbacks implements + GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener { + + final private Observer observer; + + private GoogleApiClient apiClient; + + private ApiClientConnectionCallbacks(Observer observer) { + this.observer = observer; + } + + @Override + public void onConnected(Bundle bundle) { + try { + onGoogleApiClientReady(apiClient, observer); + } catch (Throwable ex) { + observer.onError(ex); + } + } + + @Override + public void onConnectionSuspended(int cause) { + observer.onError(new GoogleAPIConnectionSuspendedException(cause)); + } + + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + if(handleResolution && connectionResult.hasResolution()) { + observableList.add(BaseObservable.this); + + if(!ResolutionActivity.isResolutionShown()) { + Intent intent = new Intent(ctx, ResolutionActivity.class); + intent.putExtra(ResolutionActivity.ARG_CONNECTION_RESULT, connectionResult); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + ctx.startActivity(intent); + } + } else { + observer.onError(new GoogleAPIConnectionException("Error connecting to GoogleApiClient.", connectionResult)); + } + } + + public void setClient(GoogleApiClient client) { + this.apiClient = client; + } + } + + static void onResolutionResult(int resultCode, ConnectionResult connectionResult) { + for(BaseObservable observable : observableList) { + if(!observable.subscriber.isUnsubscribed()) { + if (resultCode == Activity.RESULT_OK && observable.apiClient != null) { + observable.apiClient.connect(); + } else { + observable.subscriber.onError(new GoogleAPIConnectionException("Error connecting to GoogleApiClient, resolution was not successful.", connectionResult)); + } + } + } + + observableList.clear(); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/BleClaimDeviceObservable.java b/library/src/main/java/com/patloew/rxfit/BleClaimDeviceObservable.java new file mode 100644 index 0000000..2716acb --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/BleClaimDeviceObservable.java @@ -0,0 +1,43 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.BleDevice; + +import rx.Observable; +import rx.Observer; + +public class BleClaimDeviceObservable extends BaseObservable { + + private final BleDevice bleDevice; + private final String deviceAddress; + + static Observable create(@NonNull RxFit rxFit, @NonNull BleDevice bleDevice) { + return Observable.create(new BleClaimDeviceObservable(rxFit, bleDevice, null)); + } + + static Observable create(@NonNull RxFit rxFit, @NonNull String deviceAddress) { + return Observable.create(new BleClaimDeviceObservable(rxFit, null, deviceAddress)); + } + + BleClaimDeviceObservable(RxFit rxFit, BleDevice bleDevice, String deviceAddress) { + super(rxFit); + this.bleDevice = bleDevice; + this.deviceAddress = deviceAddress; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + ResultCallback resultCallback = new StatusResultCallBack(observer); + + if(bleDevice != null) { + Fitness.BleApi.claimBleDevice(apiClient, bleDevice).setResultCallback(resultCallback); + } else { + Fitness.BleApi.claimBleDevice(apiClient, deviceAddress).setResultCallback(resultCallback); + } + } +} diff --git a/library/src/main/java/com/patloew/rxfit/BleListClaimedDevicesObservable.java b/library/src/main/java/com/patloew/rxfit/BleListClaimedDevicesObservable.java new file mode 100644 index 0000000..fd0ab7b --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/BleListClaimedDevicesObservable.java @@ -0,0 +1,53 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.BleDevice; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.result.BleDevicesResult; + +import java.util.List; + +import rx.Observable; +import rx.Observer; + +public class BleListClaimedDevicesObservable extends BaseObservable> { + + private final DataType dataType; + + static Observable> create(@NonNull RxFit rxFit) { + return Observable.create(new BleListClaimedDevicesObservable(rxFit, null)); + } + + static Observable> create(@NonNull RxFit rxFit, DataType dataType) { + return Observable.create(new BleListClaimedDevicesObservable(rxFit, dataType)); + } + + BleListClaimedDevicesObservable(RxFit rxFit, DataType dataType) { + super(rxFit); + this.dataType = dataType; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer> observer) { + Fitness.BleApi.listClaimedBleDevices(apiClient).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull BleDevicesResult bleDevicesResult) { + if (!bleDevicesResult.getStatus().isSuccess()) { + observer.onError(new StatusException(bleDevicesResult.getStatus())); + } else { + if(dataType == null) { + observer.onNext(bleDevicesResult.getClaimedBleDevices()); + } else { + observer.onNext(bleDevicesResult.getClaimedBleDevices(dataType)); + } + + observer.onCompleted(); + } + } + }); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/BleStartScanObservable.java b/library/src/main/java/com/patloew/rxfit/BleStartScanObservable.java new file mode 100644 index 0000000..1d0045e --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/BleStartScanObservable.java @@ -0,0 +1,44 @@ +package com.patloew.rxfit; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.support.annotation.NonNull; +import android.support.annotation.RequiresPermission; +import android.support.v4.content.ContextCompat; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.request.StartBleScanRequest; + +import rx.Observable; +import rx.Observer; + +public class BleStartScanObservable extends BaseObservable { + + private final StartBleScanRequest startBleScanRequest; + + @RequiresPermission("android.permission.BLUETOOTH_ADMIN") + static Observable create(@NonNull RxFit rxFit, @NonNull StartBleScanRequest startBleScanRequest) { + if(ContextCompat.checkSelfPermission(rxFit.getContext(), Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED) { + return Observable.create(new BleStartScanObservable(rxFit, startBleScanRequest)); + } else { + return Observable.error(new PermissionRequiredException(Manifest.permission.BLUETOOTH_ADMIN)); + } + } + + BleStartScanObservable(RxFit rxFit, StartBleScanRequest startBleScanRequest) { + super(rxFit); + this.startBleScanRequest = startBleScanRequest; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + try { + //noinspection MissingPermission + Fitness.BleApi.startBleScan(apiClient, startBleScanRequest).setResultCallback(new StatusResultCallBack(observer)); + } catch(SecurityException e) { + observer.onError(new PermissionRequiredException(Manifest.permission.BLUETOOTH_ADMIN)); + } + } +} diff --git a/library/src/main/java/com/patloew/rxfit/BleStopScanObservable.java b/library/src/main/java/com/patloew/rxfit/BleStopScanObservable.java new file mode 100644 index 0000000..f3e46cb --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/BleStopScanObservable.java @@ -0,0 +1,30 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.request.BleScanCallback; + +import rx.Observable; +import rx.Observer; + +public class BleStopScanObservable extends BaseObservable { + + private final BleScanCallback bleScanCallback; + + static Observable create(@NonNull RxFit rxFit, @NonNull BleScanCallback bleScanCallback) { + return Observable.create(new BleStopScanObservable(rxFit, bleScanCallback)); + } + + BleStopScanObservable(RxFit rxFit, BleScanCallback bleScanCallback) { + super(rxFit); + this.bleScanCallback = bleScanCallback; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.BleApi.stopBleScan(apiClient, bleScanCallback).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/BleUnclaimDeviceObservable.java b/library/src/main/java/com/patloew/rxfit/BleUnclaimDeviceObservable.java new file mode 100644 index 0000000..23651c4 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/BleUnclaimDeviceObservable.java @@ -0,0 +1,43 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.BleDevice; + +import rx.Observable; +import rx.Observer; + +public class BleUnclaimDeviceObservable extends BaseObservable { + + private final BleDevice bleDevice; + private final String deviceAddress; + + static Observable create(@NonNull RxFit rxFit, @NonNull BleDevice bleDevice) { + return Observable.create(new BleUnclaimDeviceObservable(rxFit, bleDevice, null)); + } + + static Observable create(@NonNull RxFit rxFit, @NonNull String deviceAddress) { + return Observable.create(new BleUnclaimDeviceObservable(rxFit, null, deviceAddress)); + } + + BleUnclaimDeviceObservable(RxFit rxFit, BleDevice bleDevice, String deviceAddress) { + super(rxFit); + this.bleDevice = bleDevice; + this.deviceAddress = deviceAddress; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + ResultCallback resultCallback = new StatusResultCallBack(observer); + + if(bleDevice != null) { + Fitness.BleApi.unclaimBleDevice(apiClient, bleDevice).setResultCallback(resultCallback); + } else { + Fitness.BleApi.unclaimBleDevice(apiClient, deviceAddress).setResultCallback(resultCallback); + } + } +} diff --git a/library/src/main/java/com/patloew/rxfit/ConfigCreateCustomDataTypeObservable.java b/library/src/main/java/com/patloew/rxfit/ConfigCreateCustomDataTypeObservable.java new file mode 100644 index 0000000..734b851 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/ConfigCreateCustomDataTypeObservable.java @@ -0,0 +1,42 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.request.DataTypeCreateRequest; +import com.google.android.gms.fitness.result.DataTypeResult; + +import rx.Observable; +import rx.Observer; + +public class ConfigCreateCustomDataTypeObservable extends BaseObservable { + + private final DataTypeCreateRequest dataTypeCreateRequest; + + static Observable create(@NonNull RxFit rxFit, @NonNull DataTypeCreateRequest dataTypeCreateRequest) { + return Observable.create(new ConfigCreateCustomDataTypeObservable(rxFit, dataTypeCreateRequest)); + } + + ConfigCreateCustomDataTypeObservable(RxFit rxFit, DataTypeCreateRequest dataTypeCreateRequest) { + super(rxFit); + this.dataTypeCreateRequest = dataTypeCreateRequest; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.ConfigApi.createCustomDataType(apiClient, dataTypeCreateRequest).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull DataTypeResult dataTypeResult) { + if (!dataTypeResult.getStatus().isSuccess()) { + observer.onError(new StatusException(dataTypeResult.getStatus())); + } else { + observer.onNext(dataTypeResult.getDataType()); + observer.onCompleted(); + } + } + }); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/ConfigDisableFitObservable.java b/library/src/main/java/com/patloew/rxfit/ConfigDisableFitObservable.java new file mode 100644 index 0000000..b10f33f --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/ConfigDisableFitObservable.java @@ -0,0 +1,26 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; + +import rx.Observable; +import rx.Observer; + +public class ConfigDisableFitObservable extends BaseObservable { + + static Observable create(@NonNull RxFit rxFit) { + return Observable.create(new ConfigDisableFitObservable(rxFit)); + } + + ConfigDisableFitObservable(RxFit rxFit) { + super(rxFit); + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.ConfigApi.disableFit(apiClient).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/ConfigReadDataTypeObservable.java b/library/src/main/java/com/patloew/rxfit/ConfigReadDataTypeObservable.java new file mode 100644 index 0000000..4a97390 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/ConfigReadDataTypeObservable.java @@ -0,0 +1,41 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.result.DataTypeResult; + +import rx.Observable; +import rx.Observer; + +public class ConfigReadDataTypeObservable extends BaseObservable { + + private final String dataTypeName; + + static Observable create(@NonNull RxFit rxFit, @NonNull String dataTypeName) { + return Observable.create(new ConfigReadDataTypeObservable(rxFit, dataTypeName)); + } + + ConfigReadDataTypeObservable(RxFit rxFit, String dataTypeName) { + super(rxFit); + this.dataTypeName = dataTypeName; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.ConfigApi.readDataType(apiClient, dataTypeName).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull DataTypeResult dataTypeResult) { + if (!dataTypeResult.getStatus().isSuccess()) { + observer.onError(new StatusException(dataTypeResult.getStatus())); + } else { + observer.onNext(dataTypeResult.getDataType()); + observer.onCompleted(); + } + } + }); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/GoogleAPIClientObservable.java b/library/src/main/java/com/patloew/rxfit/GoogleAPIClientObservable.java new file mode 100644 index 0000000..fd386a8 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/GoogleAPIClientObservable.java @@ -0,0 +1,52 @@ +package com.patloew.rxfit; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Scope; + +import rx.Observable; +import rx.Observer; + +/* Copyright (C) 2015 Michał Charmas (http://blog.charmas.pl) + * + * 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 MODIFIED by Patrick Löwenstein, 2016 + * + */ +public class GoogleAPIClientObservable extends BaseObservable { + + @SafeVarargs + public static Observable create(@NonNull Context context, @NonNull Api... apis) { + return Observable.create(new GoogleAPIClientObservable(context, apis, null)); + } + + public static Observable create(@NonNull Context context, @NonNull Api[] apis, Scope[] scopes) { + return Observable.create(new GoogleAPIClientObservable(context, apis, scopes)); + } + + GoogleAPIClientObservable(Context ctx, Api[] apis, Scope[] scopes) { + super(ctx, apis, scopes); + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, Observer observer) { + observer.onNext(apiClient); + observer.onCompleted(); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/GoogleAPIConnectionException.java b/library/src/main/java/com/patloew/rxfit/GoogleAPIConnectionException.java new file mode 100644 index 0000000..0830fe0 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/GoogleAPIConnectionException.java @@ -0,0 +1,30 @@ +package com.patloew.rxfit; + +import com.google.android.gms.common.ConnectionResult; + +/* Copyright (C) 2015 Michał Charmas (http://blog.charmas.pl) + * + * 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. + */ +public class GoogleAPIConnectionException extends RuntimeException { + private final ConnectionResult connectionResult; + + GoogleAPIConnectionException(String detailMessage, ConnectionResult connectionResult) { + super(detailMessage); + this.connectionResult = connectionResult; + } + + public ConnectionResult getConnectionResult() { + return connectionResult; + } +} diff --git a/library/src/main/java/com/patloew/rxfit/GoogleAPIConnectionSuspendedException.java b/library/src/main/java/com/patloew/rxfit/GoogleAPIConnectionSuspendedException.java new file mode 100644 index 0000000..281ca37 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/GoogleAPIConnectionSuspendedException.java @@ -0,0 +1,27 @@ +package com.patloew.rxfit; + +/* Copyright (C) 2015 Michał Charmas (http://blog.charmas.pl) + * + * 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. + */ +public class GoogleAPIConnectionSuspendedException extends RuntimeException { + private final int cause; + + GoogleAPIConnectionSuspendedException(int cause) { + this.cause = cause; + } + + public int getErrorCause() { + return cause; + } +} diff --git a/library/src/main/java/com/patloew/rxfit/HistoryDeleteDataObservable.java b/library/src/main/java/com/patloew/rxfit/HistoryDeleteDataObservable.java new file mode 100644 index 0000000..9df1b54 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/HistoryDeleteDataObservable.java @@ -0,0 +1,30 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.request.DataDeleteRequest; + +import rx.Observable; +import rx.Observer; + +public class HistoryDeleteDataObservable extends BaseObservable { + + private final DataDeleteRequest dataDeleteRequest; + + static Observable create(@NonNull RxFit rxFit, @NonNull DataDeleteRequest dataDeleteRequest) { + return Observable.create(new HistoryDeleteDataObservable(rxFit, dataDeleteRequest)); + } + + HistoryDeleteDataObservable(RxFit rxFit, DataDeleteRequest dataDeleteRequest) { + super(rxFit); + this.dataDeleteRequest = dataDeleteRequest; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.HistoryApi.deleteData(apiClient, dataDeleteRequest).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/HistoryInsertDataObservable.java b/library/src/main/java/com/patloew/rxfit/HistoryInsertDataObservable.java new file mode 100644 index 0000000..7f81a7b --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/HistoryInsertDataObservable.java @@ -0,0 +1,30 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataSet; + +import rx.Observable; +import rx.Observer; + +public class HistoryInsertDataObservable extends BaseObservable { + + private final DataSet dataSet; + + static Observable create(@NonNull RxFit rxFit, @NonNull DataSet dataSet) { + return Observable.create(new HistoryInsertDataObservable(rxFit, dataSet)); + } + + HistoryInsertDataObservable(RxFit rxFit, DataSet dataSet) { + super(rxFit); + this.dataSet = dataSet; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.HistoryApi.insertData(apiClient, dataSet).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/HistoryReadDailyTotalObservable.java b/library/src/main/java/com/patloew/rxfit/HistoryReadDailyTotalObservable.java new file mode 100644 index 0000000..fdc4b34 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/HistoryReadDailyTotalObservable.java @@ -0,0 +1,42 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataSet; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.result.DailyTotalResult; + +import rx.Observable; +import rx.Observer; + +public class HistoryReadDailyTotalObservable extends BaseObservable { + + private final DataType dataType; + + static Observable create(@NonNull RxFit rxFit, @NonNull DataType dataType) { + return Observable.create(new HistoryReadDailyTotalObservable(rxFit, dataType)); + } + + HistoryReadDailyTotalObservable(RxFit rxFit, DataType dataType) { + super(rxFit); + this.dataType = dataType; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.HistoryApi.readDailyTotal(apiClient, dataType).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull DailyTotalResult dailyTotalResult) { + if (!dailyTotalResult.getStatus().isSuccess()) { + observer.onError(new StatusException(dailyTotalResult.getStatus())); + } else { + observer.onNext(dailyTotalResult.getTotal()); + observer.onCompleted(); + } + } + }); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/HistoryReadDataObservable.java b/library/src/main/java/com/patloew/rxfit/HistoryReadDataObservable.java new file mode 100644 index 0000000..68408ec --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/HistoryReadDataObservable.java @@ -0,0 +1,41 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.request.DataReadRequest; +import com.google.android.gms.fitness.result.DataReadResult; + +import rx.Observable; +import rx.Observer; + +public class HistoryReadDataObservable extends BaseObservable { + + private final DataReadRequest dataReadRequest; + + static Observable create(@NonNull RxFit rxFit, @NonNull DataReadRequest dataReadRequest) { + return Observable.create(new HistoryReadDataObservable(rxFit, dataReadRequest)); + } + + HistoryReadDataObservable(RxFit rxFit, DataReadRequest dataReadRequest) { + super(rxFit); + this.dataReadRequest = dataReadRequest; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.HistoryApi.readData(apiClient, dataReadRequest).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull DataReadResult dataReadResult) { + if (!dataReadResult.getStatus().isSuccess()) { + observer.onError(new StatusException(dataReadResult.getStatus())); + } else { + observer.onNext(dataReadResult); + observer.onCompleted(); + } + } + }); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/HistoryUpdateDataObservable.java b/library/src/main/java/com/patloew/rxfit/HistoryUpdateDataObservable.java new file mode 100644 index 0000000..aacbfa1 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/HistoryUpdateDataObservable.java @@ -0,0 +1,30 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.request.DataUpdateRequest; + +import rx.Observable; +import rx.Observer; + +public class HistoryUpdateDataObservable extends BaseObservable { + + private final DataUpdateRequest dataUpdateRequest; + + static Observable create(@NonNull RxFit rxFit, @NonNull DataUpdateRequest dataUpdateRequest) { + return Observable.create(new HistoryUpdateDataObservable(rxFit, dataUpdateRequest)); + } + + HistoryUpdateDataObservable(RxFit rxFit, DataUpdateRequest dataUpdateRequest) { + super(rxFit); + this.dataUpdateRequest = dataUpdateRequest; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.HistoryApi.updateData(apiClient, dataUpdateRequest).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/PermissionRequiredException.java b/library/src/main/java/com/patloew/rxfit/PermissionRequiredException.java new file mode 100644 index 0000000..af3690b --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/PermissionRequiredException.java @@ -0,0 +1,13 @@ +package com.patloew.rxfit; + +public class PermissionRequiredException extends Throwable { + private final String permission; + + PermissionRequiredException(String permission) { + this.permission = permission; + } + + public String getPermission() { + return permission; + } +} diff --git a/library/src/main/java/com/patloew/rxfit/RecordingListSubscriptionsObservable.java b/library/src/main/java/com/patloew/rxfit/RecordingListSubscriptionsObservable.java new file mode 100644 index 0000000..d558fc1 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/RecordingListSubscriptionsObservable.java @@ -0,0 +1,54 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.data.Subscription; +import com.google.android.gms.fitness.result.ListSubscriptionsResult; + +import java.util.List; + +import rx.Observable; +import rx.Observer; + +public class RecordingListSubscriptionsObservable extends BaseObservable> { + + private final DataType dataType; + + static Observable> create(@NonNull RxFit rxFit) { + return Observable.create(new RecordingListSubscriptionsObservable(rxFit, null)); + } + + static Observable> create(@NonNull RxFit rxFit, DataType dataType) { + return Observable.create(new RecordingListSubscriptionsObservable(rxFit, dataType)); + } + + RecordingListSubscriptionsObservable(RxFit rxFit, DataType dataType) { + super(rxFit); + this.dataType = dataType; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer> observer) { + ResultCallback resultCallback = new ResultCallback() { + @Override + public void onResult(@NonNull ListSubscriptionsResult listSubscriptionsResult) { + if(!listSubscriptionsResult.getStatus().isSuccess()) { + observer.onError(new StatusException(listSubscriptionsResult.getStatus())); + } else { + observer.onNext(listSubscriptionsResult.getSubscriptions()); + observer.onCompleted(); + } + } + }; + + if(dataType == null) { + Fitness.RecordingApi.listSubscriptions(apiClient).setResultCallback(resultCallback); + } else { + Fitness.RecordingApi.listSubscriptions(apiClient, dataType).setResultCallback(resultCallback); + } + } +} diff --git a/library/src/main/java/com/patloew/rxfit/RecordingSubscribeObservable.java b/library/src/main/java/com/patloew/rxfit/RecordingSubscribeObservable.java new file mode 100644 index 0000000..58474d8 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/RecordingSubscribeObservable.java @@ -0,0 +1,44 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; + +import rx.Observable; +import rx.Observer; + +public class RecordingSubscribeObservable extends BaseObservable { + + private final DataSource dataSource; + private final DataType dataType; + + static Observable create(@NonNull RxFit rxFit, @NonNull DataSource dataSource) { + return Observable.create(new RecordingSubscribeObservable(rxFit, dataSource, null)); + } + + static Observable create(@NonNull RxFit rxFit, @NonNull DataType dataType) { + return Observable.create(new RecordingSubscribeObservable(rxFit, null, dataType)); + } + + RecordingSubscribeObservable(RxFit rxFit, DataSource dataSource, DataType dataType) { + super(rxFit); + this.dataSource = dataSource; + this.dataType = dataType; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + ResultCallback resultCallback = new StatusResultCallBack(observer); + + if(dataSource != null) { + Fitness.RecordingApi.subscribe(apiClient, dataSource).setResultCallback(resultCallback); + } else { + Fitness.RecordingApi.subscribe(apiClient, dataType).setResultCallback(resultCallback); + } + } +} diff --git a/library/src/main/java/com/patloew/rxfit/RecordingUnsubscribeObservable.java b/library/src/main/java/com/patloew/rxfit/RecordingUnsubscribeObservable.java new file mode 100644 index 0000000..71f4dad --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/RecordingUnsubscribeObservable.java @@ -0,0 +1,53 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.data.Subscription; + +import rx.Observable; +import rx.Observer; + +public class RecordingUnsubscribeObservable extends BaseObservable { + + private final DataSource dataSource; + private final DataType dataType; + private final Subscription subscription; + + static Observable create(@NonNull RxFit rxFit, @NonNull DataSource dataSource) { + return Observable.create(new RecordingUnsubscribeObservable(rxFit, dataSource, null, null)); + } + + static Observable create(@NonNull RxFit rxFit, @NonNull DataType dataType) { + return Observable.create(new RecordingUnsubscribeObservable(rxFit, null, dataType, null)); + } + + static Observable create(@NonNull RxFit rxFit, @NonNull Subscription subscription) { + return Observable.create(new RecordingUnsubscribeObservable(rxFit, null, null, subscription)); + } + + RecordingUnsubscribeObservable(RxFit rxFit, DataSource dataSource, DataType dataType, Subscription subscription) { + super(rxFit); + this.dataSource = dataSource; + this.dataType = dataType; + this.subscription = subscription; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + ResultCallback resultCallback = new StatusResultCallBack(observer); + + if(dataSource != null) { + Fitness.RecordingApi.unsubscribe(apiClient, dataSource).setResultCallback(resultCallback); + } else if(dataType != null) { + Fitness.RecordingApi.unsubscribe(apiClient, dataType).setResultCallback(resultCallback); + } else { + Fitness.RecordingApi.unsubscribe(apiClient, subscription).setResultCallback(resultCallback); + } + } +} diff --git a/library/src/main/java/com/patloew/rxfit/ResolutionActivity.java b/library/src/main/java/com/patloew/rxfit/ResolutionActivity.java new file mode 100644 index 0000000..99cf6ff --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/ResolutionActivity.java @@ -0,0 +1,61 @@ +package com.patloew.rxfit; + +import android.app.Activity; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Bundle; + +import com.google.android.gms.common.ConnectionResult; + +public class ResolutionActivity extends Activity { + + protected static final String ARG_CONNECTION_RESULT = "connectionResult"; + + private static final int REQUEST_CODE_RESOLUTION = 123; + + private static boolean resolutionShown = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if(savedInstanceState == null) { + handleIntent(); + } + } + + @Override + protected void onNewIntent(Intent intent) { + setIntent(intent); + handleIntent(); + } + + private void handleIntent() { + try { + ConnectionResult connectionResult = getIntent().getParcelableExtra(ARG_CONNECTION_RESULT); + connectionResult.startResolutionForResult(this, REQUEST_CODE_RESOLUTION); + resolutionShown = true; + } catch (IntentSender.SendIntentException|NullPointerException e) { + setResolutionResultAndFinish(Activity.RESULT_CANCELED); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if(requestCode == REQUEST_CODE_RESOLUTION) { + setResolutionResultAndFinish(resultCode); + } else { + setResolutionResultAndFinish(Activity.RESULT_CANCELED); + } + } + + private void setResolutionResultAndFinish(int resultCode) { + resolutionShown = false; + BaseObservable.onResolutionResult(resultCode, (ConnectionResult) getIntent().getParcelableExtra(ARG_CONNECTION_RESULT)); + finish(); + } + + static boolean isResolutionShown() { + return resolutionShown; + } +} diff --git a/library/src/main/java/com/patloew/rxfit/RxFit.java b/library/src/main/java/com/patloew/rxfit/RxFit.java new file mode 100644 index 0000000..3ccf251 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/RxFit.java @@ -0,0 +1,264 @@ +package com.patloew.rxfit; + +import android.app.PendingIntent; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.RequiresPermission; + +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.Scope; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.data.BleDevice; +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.data.DataSet; +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.data.Session; +import com.google.android.gms.fitness.data.Subscription; +import com.google.android.gms.fitness.request.BleScanCallback; +import com.google.android.gms.fitness.request.DataDeleteRequest; +import com.google.android.gms.fitness.request.DataReadRequest; +import com.google.android.gms.fitness.request.DataSourcesRequest; +import com.google.android.gms.fitness.request.DataTypeCreateRequest; +import com.google.android.gms.fitness.request.DataUpdateRequest; +import com.google.android.gms.fitness.request.SensorRequest; +import com.google.android.gms.fitness.request.SessionInsertRequest; +import com.google.android.gms.fitness.request.SessionReadRequest; +import com.google.android.gms.fitness.request.StartBleScanRequest; +import com.google.android.gms.fitness.result.DataReadResult; +import com.google.android.gms.fitness.result.SessionReadResult; + +import java.util.List; + +import rx.Observable; + +/* Factory for Google Fit API observables. Make sure to include all the APIs + * and Scopes that you need for your app. Also make sure to have the Location + * and Body Sensors permission on Marshmallow, if they are needed by your + * Fit API requests. + */ +public class RxFit { + + private static RxFit instance = null; + + private final Context ctx; + private final Api[] apis; + private final Scope[] scopes; + + /* Initializes the singleton instance of RxFitProvider + * + * @param ctx Context. + * @param apis An array of Fitness APIs to be used in your app. + * @param scopes An array of the Scopes to be requested for your app. + */ + public static void init(@NonNull Context ctx, @NonNull Api[] apis, @NonNull Scope[] scopes) { + if(instance == null) { instance = new RxFit(ctx, apis, scopes); } + } + + /* Gets the singleton instance of RxFitProvider, after it was + * initialized. + */ + private static RxFit get() { + if(instance == null) { throw new IllegalStateException("RxFitProvider not initialized"); } + return instance; + } + + + private RxFit(@NonNull Context ctx, @NonNull Api[] apis, @NonNull Scope[] scopes) { + this.ctx = ctx.getApplicationContext(); + this.apis = apis; + this.scopes = scopes; + } + + Context getContext() { + return ctx; + } + + Api[] getApis() { + return apis; + } + + Scope[] getScopes() { + return scopes; + } + + + + public static class Ble { + + private Ble() { } + + public static Observable claimDevice(@NonNull BleDevice bleDevice) { + return BleClaimDeviceObservable.create(RxFit.get(), bleDevice); + } + + public static Observable claimDevice(@NonNull String deviceAddress) { + return BleClaimDeviceObservable.create(RxFit.get(), deviceAddress); + } + + public static Observable> getClaimedDeviceList() { + return BleListClaimedDevicesObservable.create(RxFit.get()); + } + + public static Observable> getClaimedDeviceList(DataType dataType) { + return BleListClaimedDevicesObservable.create(RxFit.get(), dataType); + } + + @RequiresPermission("android.permission.BLUETOOTH_ADMIN") + public static Observable startScan(@NonNull StartBleScanRequest startBleScanRequest) { + return BleStartScanObservable.create(RxFit.get(), startBleScanRequest); + } + + public static Observable stopScan(@NonNull BleScanCallback bleScanCallback) { + return BleStopScanObservable.create(RxFit.get(), bleScanCallback); + } + + public static Observable unclaimDevice(@NonNull BleDevice bleDevice) { + return BleUnclaimDeviceObservable.create(RxFit.get(), bleDevice); + } + + public static Observable unclaimDevice(@NonNull String deviceAddress) { + return BleUnclaimDeviceObservable.create(RxFit.get(), deviceAddress); + } + + } + + + public static class Config { + + private Config() { } + + public static Observable createCustomDataType(@NonNull DataTypeCreateRequest dataTypeCreateRequest) { + return ConfigCreateCustomDataTypeObservable.create(RxFit.get(), dataTypeCreateRequest); + } + + public static Observable disableFit() { + return ConfigDisableFitObservable.create(RxFit.get()); + } + + public static Observable readDataType(@NonNull String dataTypeName) { + return ConfigReadDataTypeObservable.create(RxFit.get(), dataTypeName); + } + + } + + + public static class History { + + private History() { } + + public static Observable delete(@NonNull DataDeleteRequest dataDeleteRequest) { + return HistoryDeleteDataObservable.create(RxFit.get(), dataDeleteRequest); + } + + public static Observable insert(@NonNull DataSet dataSet) { + return HistoryInsertDataObservable.create(RxFit.get(), dataSet); + } + + public static Observable readDailyTotal(@NonNull DataType dataType) { + return HistoryReadDailyTotalObservable.create(RxFit.get(), dataType); + } + + public static Observable read(@NonNull DataReadRequest dataReadRequest) { + return HistoryReadDataObservable.create(RxFit.get(), dataReadRequest); + } + + public static Observable update(@NonNull DataUpdateRequest dataUpdateRequest) { + return HistoryUpdateDataObservable.create(RxFit.get(), dataUpdateRequest); + } + + } + + + public static class Recording { + + private Recording() { } + + public static Observable> listSubscriptions() { + return RecordingListSubscriptionsObservable.create(RxFit.get()); + } + + public static Observable> listSubscriptions(DataType dataType) { + return RecordingListSubscriptionsObservable.create(RxFit.get(), dataType); + } + + public static Observable subscribe(@NonNull DataSource dataSource) { + return RecordingSubscribeObservable.create(RxFit.get(), dataSource); + } + + public static Observable subscribe(@NonNull DataType dataType) { + return RecordingSubscribeObservable.create(RxFit.get(), dataType); + } + + public static Observable unsubscribe(@NonNull DataSource dataSource) { + return RecordingUnsubscribeObservable.create(RxFit.get(), dataSource); + } + + public static Observable unsubscribe(@NonNull DataType dataType) { + return RecordingUnsubscribeObservable.create(RxFit.get(), dataType); + } + + public static Observable unsubscribe(@NonNull Subscription subscription) { + return RecordingUnsubscribeObservable.create(RxFit.get(), subscription); + } + + } + + + public static class Sensors { + + private Sensors() { } + + public static Observable addDataPointIntent(@NonNull SensorRequest sensorRequest, @NonNull PendingIntent pendingIntent) { + return SensorsAddDataPointIntentObservable.create(RxFit.get(), sensorRequest, pendingIntent); + } + + public static Observable removeDataPointIntent(@NonNull PendingIntent pendingIntent) { + return SensorsRemoveDataPointIntentObservable.create(RxFit.get(), pendingIntent); + } + + public static Observable getDataPoints(@NonNull SensorRequest sensorRequest) { + return SensorsDataPointObservable.create(RxFit.get(), sensorRequest); + } + + public static Observable> findDataSources(@NonNull DataSourcesRequest dataSourcesRequest) { + return SensorsFindDataSourcesObservable.create(RxFit.get(), dataSourcesRequest); + } + + public static Observable> findDataSources(@NonNull DataSourcesRequest dataSourcesRequest, DataType dataType) { + return SensorsFindDataSourcesObservable.create(RxFit.get(), dataSourcesRequest, dataType); + } + + } + + + public static class Sessions { + + private Sessions() { } + + public static Observable insert(@NonNull SessionInsertRequest sessionInsertRequest) { + return SessionInsertObservable.create(RxFit.get(), sessionInsertRequest); + } + + public static Observable read(@NonNull SessionReadRequest sessionReadRequest) { + return SessionReadObservable.create(RxFit.get(), sessionReadRequest); + } + + public static Observable registerForSessions(@NonNull PendingIntent pendingIntent) { + return SessionRegisterObservable.create(RxFit.get(), pendingIntent); + } + + public static Observable unregisterForSessions(@NonNull PendingIntent pendingIntent) { + return SessionUnregisterObservable.create(RxFit.get(), pendingIntent); + } + + public static Observable start(@NonNull Session session) { + return SessionStartObservable.create(RxFit.get(), session); + } + + public static Observable> stop(@NonNull String identifier) { + return SessionStopObservable.create(RxFit.get(), identifier); + } + } + +} diff --git a/library/src/main/java/com/patloew/rxfit/SensorsAddDataPointIntentObservable.java b/library/src/main/java/com/patloew/rxfit/SensorsAddDataPointIntentObservable.java new file mode 100644 index 0000000..e7046b8 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/SensorsAddDataPointIntentObservable.java @@ -0,0 +1,33 @@ +package com.patloew.rxfit; + +import android.app.PendingIntent; +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.request.SensorRequest; + +import rx.Observable; +import rx.Observer; + +public class SensorsAddDataPointIntentObservable extends BaseObservable { + + private final SensorRequest sensorRequest; + private final PendingIntent pendingIntent; + + static Observable create(@NonNull RxFit rxFit, @NonNull SensorRequest sensorRequest, @NonNull PendingIntent pendingIntent) { + return Observable.create(new SensorsAddDataPointIntentObservable(rxFit, sensorRequest, pendingIntent)); + } + + SensorsAddDataPointIntentObservable(RxFit rxFit, SensorRequest sensorRequest, PendingIntent pendingIntent) { + super(rxFit); + this.sensorRequest = sensorRequest; + this.pendingIntent = pendingIntent; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.SensorsApi.add(apiClient, sensorRequest, pendingIntent).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/SensorsDataPointObservable.java b/library/src/main/java/com/patloew/rxfit/SensorsDataPointObservable.java new file mode 100644 index 0000000..719d7b7 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/SensorsDataPointObservable.java @@ -0,0 +1,53 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.request.OnDataPointListener; +import com.google.android.gms.fitness.request.SensorRequest; + +import rx.Observable; +import rx.Observer; + +public class SensorsDataPointObservable extends BaseObservable { + + private final SensorRequest sensorRequest; + private OnDataPointListener dataPointListener = null; + + static Observable create(@NonNull RxFit rxFit, @NonNull SensorRequest sensorRequest) { + return Observable.create(new SensorsDataPointObservable(rxFit, sensorRequest)); + } + + SensorsDataPointObservable(RxFit rxFit, SensorRequest sensorRequest) { + super(rxFit); + this.sensorRequest = sensorRequest; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + dataPointListener = new OnDataPointListener() { + @Override + public void onDataPoint(DataPoint dataPoint) { + observer.onNext(dataPoint); + } + }; + + Fitness.SensorsApi.add(apiClient, sensorRequest, dataPointListener).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull Status status) { + if (!status.isSuccess()) { + observer.onError(new StatusException(status)); + } + } + }); + } + + @Override + protected void onUnsubscribed(GoogleApiClient apiClient) { + Fitness.SensorsApi.remove(apiClient, dataPointListener); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/SensorsFindDataSourcesObservable.java b/library/src/main/java/com/patloew/rxfit/SensorsFindDataSourcesObservable.java new file mode 100644 index 0000000..84f2bd9 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/SensorsFindDataSourcesObservable.java @@ -0,0 +1,56 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.request.DataSourcesRequest; +import com.google.android.gms.fitness.result.DataSourcesResult; + +import java.util.List; + +import rx.Observable; +import rx.Observer; + +public class SensorsFindDataSourcesObservable extends BaseObservable> { + + private final DataSourcesRequest dataSourcesRequest; + private final DataType dataType; + + static Observable> create(@NonNull RxFit rxFit, @NonNull DataSourcesRequest dataSourcesRequest) { + return Observable.create(new SensorsFindDataSourcesObservable(rxFit, dataSourcesRequest, null)); + } + + static Observable> create(@NonNull RxFit rxFit, @NonNull DataSourcesRequest dataSourcesRequest, DataType dataType) { + return Observable.create(new SensorsFindDataSourcesObservable(rxFit, dataSourcesRequest, dataType)); + } + + SensorsFindDataSourcesObservable(RxFit rxFit, DataSourcesRequest dataSourcesRequest, DataType dataType) { + super(rxFit); + this.dataSourcesRequest = dataSourcesRequest; + this.dataType = dataType; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer> observer) { + Fitness.SensorsApi.findDataSources(apiClient, dataSourcesRequest).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull DataSourcesResult dataSourcesResult) { + if (!dataSourcesResult.getStatus().isSuccess()) { + observer.onError(new StatusException(dataSourcesResult.getStatus())); + } else { + if(dataType == null) { + observer.onNext(dataSourcesResult.getDataSources()); + } else { + observer.onNext(dataSourcesResult.getDataSources(dataType)); + } + + observer.onCompleted(); + } + } + }); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/SensorsRemoveDataPointIntentObservable.java b/library/src/main/java/com/patloew/rxfit/SensorsRemoveDataPointIntentObservable.java new file mode 100644 index 0000000..b9e729d --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/SensorsRemoveDataPointIntentObservable.java @@ -0,0 +1,30 @@ +package com.patloew.rxfit; + +import android.app.PendingIntent; +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; + +import rx.Observable; +import rx.Observer; + +public class SensorsRemoveDataPointIntentObservable extends BaseObservable { + + private final PendingIntent pendingIntent; + + static Observable create(@NonNull RxFit rxFit, @NonNull PendingIntent pendingIntent) { + return Observable.create(new SensorsRemoveDataPointIntentObservable(rxFit, pendingIntent)); + } + + SensorsRemoveDataPointIntentObservable(RxFit rxFit, PendingIntent pendingIntent) { + super(rxFit); + this.pendingIntent = pendingIntent; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.SensorsApi.remove(apiClient, pendingIntent).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/SessionInsertObservable.java b/library/src/main/java/com/patloew/rxfit/SessionInsertObservable.java new file mode 100644 index 0000000..28f2e7a --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/SessionInsertObservable.java @@ -0,0 +1,30 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.request.SessionInsertRequest; + +import rx.Observable; +import rx.Observer; + +public class SessionInsertObservable extends BaseObservable { + + private final SessionInsertRequest sessionInsertRequest; + + static Observable create(@NonNull RxFit rxFit, @NonNull SessionInsertRequest sessionInsertRequest) { + return Observable.create(new SessionInsertObservable(rxFit, sessionInsertRequest)); + } + + SessionInsertObservable(RxFit rxFit, SessionInsertRequest sessionInsertRequest) { + super(rxFit); + this.sessionInsertRequest = sessionInsertRequest; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.SessionsApi.insertSession(apiClient, sessionInsertRequest).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/SessionReadObservable.java b/library/src/main/java/com/patloew/rxfit/SessionReadObservable.java new file mode 100644 index 0000000..94ab6a9 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/SessionReadObservable.java @@ -0,0 +1,41 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.request.SessionReadRequest; +import com.google.android.gms.fitness.result.SessionReadResult; + +import rx.Observable; +import rx.Observer; + +public class SessionReadObservable extends BaseObservable { + + private final SessionReadRequest sessionReadRequest; + + static Observable create(@NonNull RxFit rxFit, @NonNull SessionReadRequest sessionReadRequest) { + return Observable.create(new SessionReadObservable(rxFit, sessionReadRequest)); + } + + SessionReadObservable(RxFit rxFit, SessionReadRequest sessionReadRequest) { + super(rxFit); + this.sessionReadRequest = sessionReadRequest; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.SessionsApi.readSession(apiClient, sessionReadRequest).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull SessionReadResult sessionReadResult) { + if (!sessionReadResult.getStatus().isSuccess()) { + observer.onError(new StatusException(sessionReadResult.getStatus())); + } else { + observer.onNext(sessionReadResult); + observer.onCompleted(); + } + } + }); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/SessionRegisterObservable.java b/library/src/main/java/com/patloew/rxfit/SessionRegisterObservable.java new file mode 100644 index 0000000..fadd72b --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/SessionRegisterObservable.java @@ -0,0 +1,30 @@ +package com.patloew.rxfit; + +import android.app.PendingIntent; +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; + +import rx.Observable; +import rx.Observer; + +public class SessionRegisterObservable extends BaseObservable { + + private final PendingIntent pendingIntent; + + static Observable create(@NonNull RxFit rxFit, @NonNull PendingIntent pendingIntent) { + return Observable.create(new SessionRegisterObservable(rxFit, pendingIntent)); + } + + SessionRegisterObservable(RxFit rxFit, PendingIntent pendingIntent) { + super(rxFit); + this.pendingIntent = pendingIntent; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.SessionsApi.registerForSessions(apiClient, pendingIntent).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/SessionStartObservable.java b/library/src/main/java/com/patloew/rxfit/SessionStartObservable.java new file mode 100644 index 0000000..6d0a01e --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/SessionStartObservable.java @@ -0,0 +1,30 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.Session; + +import rx.Observable; +import rx.Observer; + +public class SessionStartObservable extends BaseObservable { + + private final Session session; + + static Observable create(@NonNull RxFit rxFit, @NonNull Session session) { + return Observable.create(new SessionStartObservable(rxFit, session)); + } + + SessionStartObservable(RxFit rxFit, Session session) { + super(rxFit); + this.session = session; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.SessionsApi.startSession(apiClient, session).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/SessionStopObservable.java b/library/src/main/java/com/patloew/rxfit/SessionStopObservable.java new file mode 100644 index 0000000..57c27cd --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/SessionStopObservable.java @@ -0,0 +1,43 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.Session; +import com.google.android.gms.fitness.result.SessionStopResult; + +import java.util.List; + +import rx.Observable; +import rx.Observer; + +public class SessionStopObservable extends BaseObservable> { + + private final String identifier; + + static Observable> create(@NonNull RxFit rxFit, @NonNull String identifier) { + return Observable.create(new SessionStopObservable(rxFit, identifier)); + } + + SessionStopObservable(RxFit rxFit, String identifier) { + super(rxFit); + this.identifier = identifier; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer> observer) { + Fitness.SessionsApi.stopSession(apiClient, identifier).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull SessionStopResult sessionStopResult) { + if (!sessionStopResult.getStatus().isSuccess()) { + observer.onError(new StatusException(sessionStopResult.getStatus())); + } else { + observer.onNext(sessionStopResult.getSessions()); + observer.onCompleted(); + } + } + }); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/SessionUnregisterObservable.java b/library/src/main/java/com/patloew/rxfit/SessionUnregisterObservable.java new file mode 100644 index 0000000..c9f331f --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/SessionUnregisterObservable.java @@ -0,0 +1,30 @@ +package com.patloew.rxfit; + +import android.app.PendingIntent; +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; + +import rx.Observable; +import rx.Observer; + +public class SessionUnregisterObservable extends BaseObservable { + + private final PendingIntent pendingIntent; + + static Observable create(@NonNull RxFit rxFit, @NonNull PendingIntent pendingIntent) { + return Observable.create(new SessionUnregisterObservable(rxFit, pendingIntent)); + } + + SessionUnregisterObservable(RxFit rxFit, PendingIntent pendingIntent) { + super(rxFit); + this.pendingIntent = pendingIntent; + } + + @Override + protected void onGoogleApiClientReady(GoogleApiClient apiClient, final Observer observer) { + Fitness.SessionsApi.unregisterForSessions(apiClient, pendingIntent).setResultCallback(new StatusResultCallBack(observer)); + } +} diff --git a/library/src/main/java/com/patloew/rxfit/StatusException.java b/library/src/main/java/com/patloew/rxfit/StatusException.java new file mode 100644 index 0000000..fb11edd --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/StatusException.java @@ -0,0 +1,29 @@ +package com.patloew.rxfit; + +import com.google.android.gms.common.api.Status; + +/* Copyright (C) 2015 Michał Charmas (http://blog.charmas.pl) + * + * 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. + */ +public class StatusException extends Throwable { + private final Status status; + + public StatusException(Status status) { + this.status = status; + } + + public Status getStatus() { + return status; + } +} diff --git a/library/src/main/java/com/patloew/rxfit/StatusResultCallBack.java b/library/src/main/java/com/patloew/rxfit/StatusResultCallBack.java new file mode 100644 index 0000000..4cf7c48 --- /dev/null +++ b/library/src/main/java/com/patloew/rxfit/StatusResultCallBack.java @@ -0,0 +1,27 @@ +package com.patloew.rxfit; + +import android.support.annotation.NonNull; + +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; + +import rx.Observer; + +public class StatusResultCallBack implements ResultCallback { + + private final Observer observer; + + public StatusResultCallBack(@NonNull Observer observer) { + this.observer = observer; + } + + @Override + public void onResult(@NonNull Status status) { + if (!status.isSuccess()) { + observer.onError(new StatusException(status)); + } else { + observer.onNext(status); + observer.onCompleted(); + } + } +} diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml new file mode 100644 index 0000000..851a9a9 --- /dev/null +++ b/library/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + RxFit + diff --git a/library/src/test/java/com/patloew/rxfit/RxFitTest.java b/library/src/test/java/com/patloew/rxfit/RxFitTest.java new file mode 100644 index 0000000..3de76e8 --- /dev/null +++ b/library/src/test/java/com/patloew/rxfit/RxFitTest.java @@ -0,0 +1,1511 @@ +package com.patloew.rxfit; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.pm.PackageManager; +import android.support.v4.content.ContextCompat; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Scope; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.BleApi; +import com.google.android.gms.fitness.ConfigApi; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.HistoryApi; +import com.google.android.gms.fitness.RecordingApi; +import com.google.android.gms.fitness.SensorsApi; +import com.google.android.gms.fitness.SessionsApi; +import com.google.android.gms.fitness.data.BleDevice; +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.data.DataSet; +import com.google.android.gms.fitness.data.DataSource; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.data.Session; +import com.google.android.gms.fitness.data.Subscription; +import com.google.android.gms.fitness.request.BleScanCallback; +import com.google.android.gms.fitness.request.DataDeleteRequest; +import com.google.android.gms.fitness.request.DataReadRequest; +import com.google.android.gms.fitness.request.DataSourcesRequest; +import com.google.android.gms.fitness.request.DataTypeCreateRequest; +import com.google.android.gms.fitness.request.DataUpdateRequest; +import com.google.android.gms.fitness.request.OnDataPointListener; +import com.google.android.gms.fitness.request.SensorRequest; +import com.google.android.gms.fitness.request.SessionInsertRequest; +import com.google.android.gms.fitness.request.SessionReadRequest; +import com.google.android.gms.fitness.request.StartBleScanRequest; +import com.google.android.gms.fitness.result.BleDevicesResult; +import com.google.android.gms.fitness.result.DailyTotalResult; +import com.google.android.gms.fitness.result.DataReadResult; +import com.google.android.gms.fitness.result.DataSourcesResult; +import com.google.android.gms.fitness.result.DataTypeResult; +import com.google.android.gms.fitness.result.ListSubscriptionsResult; +import com.google.android.gms.fitness.result.SessionReadResult; +import com.google.android.gms.fitness.result.SessionStopResult; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest; +import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import java.util.ArrayList; +import java.util.List; + +import rx.Observable; +import rx.Subscriber; +import rx.observers.TestSubscriber; + +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareOnlyThisForTest({ ContextCompat.class, Fitness.class, Status.class, ConnectionResult.class, DataType.class, DataSet.class, DataPoint.class }) +@SuppressStaticInitializationFor("com.google.android.gms.fitness.Fitness") +public class RxFitTest { + + @Mock Context ctx; + + @Mock GoogleApiClient apiClient; + @Mock Status status; + @Mock ConnectionResult connectionResult; + @Mock PendingResult pendingResult; + + @Mock DataType dataType; + @Mock DataSource dataSource; + @Mock DataSet dataSet; + @Mock BleDevice bleDevice; + @Mock Subscription subscription; + @Mock SensorRequest sensorRequest; + @Mock Session session; + + @Mock BleApi bleApi; + @Mock ConfigApi configApi; + @Mock HistoryApi historyApi; + @Mock RecordingApi recordingApi; + @Mock SensorsApi sensorsApi; + @Mock SessionsApi sessionsApi; + + @Mock RxFit rxFit; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + PowerMockito.mockStatic(Fitness.class); + Whitebox.setInternalState(Fitness.class, bleApi); + Whitebox.setInternalState(Fitness.class, configApi); + Whitebox.setInternalState(Fitness.class, historyApi); + Whitebox.setInternalState(Fitness.class, recordingApi); + Whitebox.setInternalState(Fitness.class, sensorsApi); + Whitebox.setInternalState(Fitness.class, sessionsApi); + + when(ctx.getApplicationContext()).thenReturn(ctx); + } + + ////////////////// + // UTIL METHODS // + ////////////////// + + @SuppressWarnings("unchecked") + // Mock GoogleApiClient connection success behaviour + private void setupBaseObservableSuccess(final BaseObservable baseObservable) { + doReturn(apiClient).when(baseObservable).createApiClient(Matchers.any()); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + baseObservable.onGoogleApiClientReady(apiClient, baseObservable.subscriber); + return null; + } + }).when(apiClient).connect(); + } + + @SuppressWarnings("unchecked") + private void setPendingResultValue(final Result result) { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ((ResultCallback)invocation.getArguments()[0]).onResult(result); + return null; + } + }).when(pendingResult).setResultCallback(Matchers.any()); + } + + private void assertError(TestSubscriber sub, Class errorClass) { + sub.assertError(errorClass); + sub.assertNoValues(); + sub.assertTerminalEvent(); + sub.assertUnsubscribed(); + } + + @SuppressWarnings("unchecked") + private void assertSingleValue(TestSubscriber sub, Object value) { + sub.assertNoErrors(); + sub.assertTerminalEvent(); + sub.assertUnsubscribed(); + sub.assertValue(value); + } + + + ////////////////////// + // OBSERVABLE TESTS // + ////////////////////// + + + // GoogleApiClientObservable + + @Test + public void GoogleAPIClientObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + GoogleAPIClientObservable observable = spy(new GoogleAPIClientObservable(ctx, new Api[] {}, new Scope[] {})); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, apiClient); + } + + @Test + public void GoogleAPIClientObservable_ConnectionException() { + TestSubscriber sub = new TestSubscriber<>(); + final GoogleAPIClientObservable observable = spy(new GoogleAPIClientObservable(ctx, new Api[] {}, new Scope[] {})); + + // Mock GoogleApiClient connection error behaviour + doReturn(apiClient).when(observable).createApiClient(Matchers.any()); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + observable.subscriber.onError(new GoogleAPIConnectionException("Error connecting to GoogleApiClient.", connectionResult)); + return null; + } + }).when(apiClient).connect(); + + Observable.create(observable).subscribe(sub); + + assertError(sub, GoogleAPIConnectionException.class); + } + + + /******* + * BLE * + *******/ + + // BleClaimDeviceObservable + + @Test + public void BleClaimDeviceObservable_BleDevice_Success() { + TestSubscriber sub = new TestSubscriber<>(); + BleDevice bleDevice = Mockito.mock(BleDevice.class); + BleClaimDeviceObservable observable = spy(new BleClaimDeviceObservable(rxFit, bleDevice, null)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(bleApi.claimBleDevice(apiClient, bleDevice)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void BleClaimDeviceObservable_DeviceAddress_Success() { + TestSubscriber sub = new TestSubscriber<>(); + String deviceAddress = "deviceAddress"; + BleClaimDeviceObservable observable = spy(new BleClaimDeviceObservable(rxFit, null, deviceAddress)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(bleApi.claimBleDevice(apiClient, deviceAddress)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void BleClaimDeviceObservable_BleDevice_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + BleDevice bleDevice = Mockito.mock(BleDevice.class); + BleClaimDeviceObservable observable = spy(new BleClaimDeviceObservable(rxFit, bleDevice, null)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(bleApi.claimBleDevice(apiClient, bleDevice)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + @Test + public void BleClaimDeviceObservable_DeviceAddress_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + String deviceAddress = "deviceAddress"; + BleClaimDeviceObservable observable = spy(new BleClaimDeviceObservable(rxFit, null, deviceAddress)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(bleApi.claimBleDevice(apiClient, deviceAddress)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + + // BleUnclaimDeviceObservable + + @Test + public void BleUnclaimDeviceObservable_BleDevice_Success() { + TestSubscriber sub = new TestSubscriber<>(); + BleDevice bleDevice = Mockito.mock(BleDevice.class); + BleUnclaimDeviceObservable observable = spy(new BleUnclaimDeviceObservable(rxFit, bleDevice, null)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(bleApi.unclaimBleDevice(apiClient, bleDevice)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void BleUnclaimDeviceObservable_DeviceAddress_Success() { + TestSubscriber sub = new TestSubscriber<>(); + String deviceAddress = "deviceAddress"; + BleUnclaimDeviceObservable observable = spy(new BleUnclaimDeviceObservable(rxFit, null, deviceAddress)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(bleApi.unclaimBleDevice(apiClient, deviceAddress)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void BleUnclaimDeviceObservable_BleDevice_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + BleDevice bleDevice = Mockito.mock(BleDevice.class); + BleUnclaimDeviceObservable observable = spy(new BleUnclaimDeviceObservable(rxFit, bleDevice, null)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(bleApi.unclaimBleDevice(apiClient, bleDevice)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + @Test + public void BleUnclaimDeviceObservable_DeviceAddress_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + String deviceAddress = "deviceAddress"; + BleUnclaimDeviceObservable observable = spy(new BleUnclaimDeviceObservable(rxFit, null, deviceAddress)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(bleApi.unclaimBleDevice(apiClient, deviceAddress)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // BleListClaimedDevicesObservable + + @Test + public void BleListClaimedDevicesObservable_WithDataType_Success() { + TestSubscriber> sub = new TestSubscriber<>(); + BleListClaimedDevicesObservable observable = spy(new BleListClaimedDevicesObservable(rxFit, dataType)); + + BleDevicesResult bleDevicesResult = Mockito.mock(BleDevicesResult.class); + + List bleDeviceList = new ArrayList<>(); + bleDeviceList.add(bleDevice); + + when(bleDevicesResult.getClaimedBleDevices(dataType)).thenReturn(bleDeviceList); + + setPendingResultValue(bleDevicesResult); + when(bleDevicesResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(true); + when(bleApi.listClaimedBleDevices(apiClient)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, bleDeviceList); + } + + @Test + public void BleListClaimedDevicesObservable_WithDataType_StatusException() { + TestSubscriber> sub = new TestSubscriber<>(); + BleListClaimedDevicesObservable observable = spy(new BleListClaimedDevicesObservable(rxFit, dataType)); + + BleDevicesResult bleDevicesResult = Mockito.mock(BleDevicesResult.class); + + List bleDeviceList = new ArrayList<>(); + bleDeviceList.add(bleDevice); + + when(bleDevicesResult.getClaimedBleDevices(dataType)).thenReturn(bleDeviceList); + + setPendingResultValue(bleDevicesResult); + when(bleDevicesResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(false); + when(bleApi.listClaimedBleDevices(apiClient)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + @Test + public void BleListClaimedDevicesObservable_Success() { + TestSubscriber> sub = new TestSubscriber<>(); + BleListClaimedDevicesObservable observable = spy(new BleListClaimedDevicesObservable(rxFit, null)); + + BleDevicesResult bleDevicesResult = Mockito.mock(BleDevicesResult.class); + + List bleDeviceList = new ArrayList<>(); + bleDeviceList.add(bleDevice); + + when(bleDevicesResult.getClaimedBleDevices()).thenReturn(bleDeviceList); + + setPendingResultValue(bleDevicesResult); + when(bleDevicesResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(true); + when(bleApi.listClaimedBleDevices(apiClient)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, bleDeviceList); + } + + @Test + public void BleListClaimedDevicesObservable_StatusException() { + TestSubscriber> sub = new TestSubscriber<>(); + BleListClaimedDevicesObservable observable = spy(new BleListClaimedDevicesObservable(rxFit, null)); + + BleDevicesResult bleDevicesResult = Mockito.mock(BleDevicesResult.class); + + List bleDeviceList = new ArrayList<>(); + bleDeviceList.add(bleDevice); + + when(bleDevicesResult.getClaimedBleDevices()).thenReturn(bleDeviceList); + + setPendingResultValue(bleDevicesResult); + when(bleDevicesResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(false); + when(bleApi.listClaimedBleDevices(apiClient)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // BleStartScanObservable + + @Test + public void BleStartScanObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + StartBleScanRequest startBleScanRequest = Mockito.mock(StartBleScanRequest.class); + BleStartScanObservable observable = spy(new BleStartScanObservable(rxFit, startBleScanRequest)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + //noinspection MissingPermission + when(bleApi.startBleScan(apiClient, startBleScanRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void BleStartScanObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + StartBleScanRequest startBleScanRequest = Mockito.mock(StartBleScanRequest.class); + BleStartScanObservable observable = spy(new BleStartScanObservable(rxFit, startBleScanRequest)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + //noinspection MissingPermission + when(bleApi.startBleScan(apiClient, startBleScanRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + @Test + public void BleStartScanObservable_PermissionRequiredException() throws Exception { + TestSubscriber sub = new TestSubscriber<>(); + + PowerMockito.mockStatic(ContextCompat.class); + PowerMockito.doReturn(PackageManager.PERMISSION_DENIED).when(ContextCompat.class, "checkSelfPermission", Matchers.any(Context.class), Matchers.anyString()); + + StartBleScanRequest startBleScanRequest = Mockito.mock(StartBleScanRequest.class); + //noinspection MissingPermission + BleStartScanObservable.create(rxFit, startBleScanRequest).subscribe(sub); + + assertError(sub, PermissionRequiredException.class); + } + + // BleStopScanObservable + + @Test + public void BleStopScanObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + BleScanCallback bleScanCallback = Mockito.mock(BleScanCallback.class); + BleStopScanObservable observable = spy(new BleStopScanObservable(rxFit, bleScanCallback)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + //noinspection MissingPermission + when(bleApi.stopBleScan(apiClient, bleScanCallback)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void BleStopScanObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + BleScanCallback bleScanCallback = Mockito.mock(BleScanCallback.class); + BleStopScanObservable observable = spy(new BleStopScanObservable(rxFit, bleScanCallback)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + //noinspection MissingPermission + when(bleApi.stopBleScan(apiClient, bleScanCallback)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + + /********** + * Config * + **********/ + + // ConfigCreateCustomDataTypeObservable + + @Test + public void ConfigCreateCustomDataTypeObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + DataTypeCreateRequest dataTypeCreateRequest = Mockito.mock(DataTypeCreateRequest.class); + DataTypeResult dataTypeResult = Mockito.mock(DataTypeResult.class); + ConfigCreateCustomDataTypeObservable observable = spy(new ConfigCreateCustomDataTypeObservable(rxFit, dataTypeCreateRequest)); + + setPendingResultValue(dataTypeResult); + when(dataTypeResult.getStatus()).thenReturn(status); + when(dataTypeResult.getDataType()).thenReturn(dataType); + when(status.isSuccess()).thenReturn(true); + when(configApi.createCustomDataType(apiClient, dataTypeCreateRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, dataType); + } + + @Test + public void ConfigCreateCustomDataTypeObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + DataTypeCreateRequest dataTypeCreateRequest = Mockito.mock(DataTypeCreateRequest.class); + DataTypeResult dataTypeResult = Mockito.mock(DataTypeResult.class); + ConfigCreateCustomDataTypeObservable observable = spy(new ConfigCreateCustomDataTypeObservable(rxFit, dataTypeCreateRequest)); + + setPendingResultValue(dataTypeResult); + when(dataTypeResult.getStatus()).thenReturn(status); + when(dataTypeResult.getDataType()).thenReturn(dataType); + when(status.isSuccess()).thenReturn(false); + when(configApi.createCustomDataType(apiClient, dataTypeCreateRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // ConfigDisableFitObservable + + @Test + public void ConfigDisableFitObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + ConfigDisableFitObservable observable = spy(new ConfigDisableFitObservable(rxFit)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(configApi.disableFit(apiClient)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void ConfigDisableFitObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + ConfigDisableFitObservable observable = spy(new ConfigDisableFitObservable(rxFit)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(configApi.disableFit(apiClient)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // ConfigReadDataTypeObservable + + @Test + public void ConfigReadDataTypeObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + String dataTypeName = "dataTypeName"; + DataTypeResult dataTypeResult = Mockito.mock(DataTypeResult.class); + ConfigReadDataTypeObservable observable = spy(new ConfigReadDataTypeObservable(rxFit, dataTypeName)); + + setPendingResultValue(dataTypeResult); + when(dataTypeResult.getStatus()).thenReturn(status); + when(dataTypeResult.getDataType()).thenReturn(dataType); + when(status.isSuccess()).thenReturn(true); + when(configApi.readDataType(apiClient, dataTypeName)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, dataType); + } + + @Test + public void ConfigReadDataTypeObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + String dataTypeName = "dataTypeName"; + DataTypeResult dataTypeResult = Mockito.mock(DataTypeResult.class); + ConfigReadDataTypeObservable observable = spy(new ConfigReadDataTypeObservable(rxFit, dataTypeName)); + + setPendingResultValue(dataTypeResult); + when(dataTypeResult.getStatus()).thenReturn(status); + when(dataTypeResult.getDataType()).thenReturn(dataType); + when(status.isSuccess()).thenReturn(false); + when(configApi.readDataType(apiClient, dataTypeName)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + /*********** + * History * + ***********/ + + // HistoryDeleteDataObservable + + @Test + public void HistoryDeleteDataObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + DataDeleteRequest dataDeleteRequest = Mockito.mock(DataDeleteRequest.class); + HistoryDeleteDataObservable observable = spy(new HistoryDeleteDataObservable(rxFit, dataDeleteRequest)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(historyApi.deleteData(apiClient, dataDeleteRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void HistoryDeleteDataObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + DataDeleteRequest dataDeleteRequest = Mockito.mock(DataDeleteRequest.class); + HistoryDeleteDataObservable observable = spy(new HistoryDeleteDataObservable(rxFit, dataDeleteRequest)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(historyApi.deleteData(apiClient, dataDeleteRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // HistoryInsertDataObservable + + @Test + public void HistoryInsertDataObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + HistoryInsertDataObservable observable = spy(new HistoryInsertDataObservable(rxFit, dataSet)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(historyApi.insertData(apiClient, dataSet)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void HistoryInsertDataObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + HistoryInsertDataObservable observable = spy(new HistoryInsertDataObservable(rxFit, dataSet)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(historyApi.insertData(apiClient, dataSet)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // HistoryReadDailyTotalObservable + + @Test + public void HistoryReadDailyTotalObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + DailyTotalResult dailyTotalResult = Mockito.mock(DailyTotalResult.class); + HistoryReadDailyTotalObservable observable = spy(new HistoryReadDailyTotalObservable(rxFit, dataType)); + + setPendingResultValue(dailyTotalResult); + when(dailyTotalResult.getStatus()).thenReturn(status); + when(dailyTotalResult.getTotal()).thenReturn(dataSet); + when(status.isSuccess()).thenReturn(true); + when(historyApi.readDailyTotal(apiClient, dataType)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, dataSet); + } + + @Test + public void HistoryReadDailyTotalObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + DailyTotalResult dailyTotalResult = Mockito.mock(DailyTotalResult.class); + HistoryReadDailyTotalObservable observable = spy(new HistoryReadDailyTotalObservable(rxFit, dataType)); + + setPendingResultValue(dailyTotalResult); + when(dailyTotalResult.getStatus()).thenReturn(status); + when(dailyTotalResult.getTotal()).thenReturn(dataSet); + when(status.isSuccess()).thenReturn(false); + when(historyApi.readDailyTotal(apiClient, dataType)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // HistoryReadDataObservable + + @Test + public void HistoryReadDataObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + DataReadRequest dataReadRequest = Mockito.mock(DataReadRequest.class); + DataReadResult dataReadResult = Mockito.mock(DataReadResult.class); + HistoryReadDataObservable observable = spy(new HistoryReadDataObservable(rxFit, dataReadRequest)); + + setPendingResultValue(dataReadResult); + when(dataReadResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(true); + when(historyApi.readData(apiClient, dataReadRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, dataReadResult); + } + + @Test + public void HistoryReadDataObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + DataReadRequest dataReadRequest = Mockito.mock(DataReadRequest.class); + DataReadResult dataReadResult = Mockito.mock(DataReadResult.class); + HistoryReadDataObservable observable = spy(new HistoryReadDataObservable(rxFit, dataReadRequest)); + + setPendingResultValue(dataReadResult); + when(dataReadResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(false); + when(historyApi.readData(apiClient, dataReadRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // HistoryUpdateDataObservable + + @Test + public void HistoryUpdateDataObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + DataUpdateRequest dataUpdateRequest = Mockito.mock(DataUpdateRequest.class); + HistoryUpdateDataObservable observable = spy(new HistoryUpdateDataObservable(rxFit, dataUpdateRequest)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(historyApi.updateData(apiClient, dataUpdateRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void HistoryUpdateDataObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + DataUpdateRequest dataUpdateRequest = Mockito.mock(DataUpdateRequest.class); + HistoryUpdateDataObservable observable = spy(new HistoryUpdateDataObservable(rxFit, dataUpdateRequest)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(historyApi.updateData(apiClient, dataUpdateRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + + /************* + * Recording * + *************/ + + // RecordingListSubscriptionsObservable + + @Test + public void RecordingListSubscriptionsObservable_Success() { + TestSubscriber> sub = new TestSubscriber<>(); + RecordingListSubscriptionsObservable observable = spy(new RecordingListSubscriptionsObservable(rxFit, null)); + + ListSubscriptionsResult listSubscriptionsResult = Mockito.mock(ListSubscriptionsResult.class); + + List subscriptionList = new ArrayList<>(); + subscriptionList.add(Mockito.mock(Subscription.class)); + + when(listSubscriptionsResult.getSubscriptions()).thenReturn(subscriptionList); + + setPendingResultValue(listSubscriptionsResult); + when(listSubscriptionsResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(true); + when(recordingApi.listSubscriptions(apiClient)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, subscriptionList); + } + + @Test + public void RecordingListSubscriptionsObservable_StatusException() { + TestSubscriber> sub = new TestSubscriber<>(); + RecordingListSubscriptionsObservable observable = spy(new RecordingListSubscriptionsObservable(rxFit, null)); + + ListSubscriptionsResult listSubscriptionsResult = Mockito.mock(ListSubscriptionsResult.class); + + List subscriptionList = new ArrayList<>(); + subscriptionList.add(Mockito.mock(Subscription.class)); + + when(listSubscriptionsResult.getSubscriptions()).thenReturn(subscriptionList); + + setPendingResultValue(listSubscriptionsResult); + when(listSubscriptionsResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(false); + when(recordingApi.listSubscriptions(apiClient)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + @Test + public void RecordingListSubscriptionsObservable_WithDataType_Success() { + TestSubscriber> sub = new TestSubscriber<>(); + RecordingListSubscriptionsObservable observable = spy(new RecordingListSubscriptionsObservable(rxFit, dataType)); + + ListSubscriptionsResult listSubscriptionsResult = Mockito.mock(ListSubscriptionsResult.class); + + List subscriptionList = new ArrayList<>(); + subscriptionList.add(Mockito.mock(Subscription.class)); + + when(listSubscriptionsResult.getSubscriptions()).thenReturn(subscriptionList); + + setPendingResultValue(listSubscriptionsResult); + when(listSubscriptionsResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(true); + when(recordingApi.listSubscriptions(apiClient, dataType)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, subscriptionList); + } + + @Test + public void RecordingListSubscriptionsObservable_WithDataType_StatusException() { + TestSubscriber> sub = new TestSubscriber<>(); + RecordingListSubscriptionsObservable observable = spy(new RecordingListSubscriptionsObservable(rxFit, dataType)); + + ListSubscriptionsResult listSubscriptionsResult = Mockito.mock(ListSubscriptionsResult.class); + + List subscriptionList = new ArrayList<>(); + subscriptionList.add(subscription); + + when(listSubscriptionsResult.getSubscriptions()).thenReturn(subscriptionList); + + setPendingResultValue(listSubscriptionsResult); + when(listSubscriptionsResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(false); + when(recordingApi.listSubscriptions(apiClient, dataType)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // RecordingSubscribeObservable + + @Test + public void RecordingSubscribeObservable_DataType_Success() { + TestSubscriber sub = new TestSubscriber<>(); + RecordingSubscribeObservable observable = spy(new RecordingSubscribeObservable(rxFit, null, dataType)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(recordingApi.subscribe(apiClient, dataType)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + + @Test + public void RecordingSubscribeObservable_DataType_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + RecordingSubscribeObservable observable = spy(new RecordingSubscribeObservable(rxFit, null, dataType)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(recordingApi.subscribe(apiClient, dataType)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + @Test + public void RecordingSubscribeObservable_DataSource_Success() { + TestSubscriber sub = new TestSubscriber<>(); + RecordingSubscribeObservable observable = spy(new RecordingSubscribeObservable(rxFit, dataSource, null)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(recordingApi.subscribe(apiClient, dataSource)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + + @Test + public void RecordingSubscribeObservable_DataSource_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + RecordingSubscribeObservable observable = spy(new RecordingSubscribeObservable(rxFit, dataSource, null)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(recordingApi.subscribe(apiClient, dataSource)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // RecordingSubscribeObservable + + @Test + public void RecordingUnsubscribeObservable_DataType_Success() { + TestSubscriber sub = new TestSubscriber<>(); + RecordingUnsubscribeObservable observable = spy(new RecordingUnsubscribeObservable(rxFit, null, dataType, null)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(recordingApi.unsubscribe(apiClient, dataType)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + + @Test + public void RecordingUnsubscribeObservable_DataType_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + RecordingUnsubscribeObservable observable = spy(new RecordingUnsubscribeObservable(rxFit, null, dataType, null)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(recordingApi.unsubscribe(apiClient, dataType)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + @Test + public void RecordingUnsubscribeObservable_DataSource_Success() { + TestSubscriber sub = new TestSubscriber<>(); + RecordingUnsubscribeObservable observable = spy(new RecordingUnsubscribeObservable(rxFit, dataSource, null, null)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(recordingApi.unsubscribe(apiClient, dataSource)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void RecordingUnsubscribeObservable_DataSource_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + RecordingUnsubscribeObservable observable = spy(new RecordingUnsubscribeObservable(rxFit, dataSource, null, null)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(recordingApi.unsubscribe(apiClient, dataSource)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + @Test + public void RecordingUnsubscribeObservable_Subscription_Success() { + TestSubscriber sub = new TestSubscriber<>(); + RecordingUnsubscribeObservable observable = spy(new RecordingUnsubscribeObservable(rxFit, null, null, subscription)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(recordingApi.unsubscribe(apiClient, subscription)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void RecordingUnsubscribeObservable_Subscription_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + RecordingUnsubscribeObservable observable = spy(new RecordingUnsubscribeObservable(rxFit, null, null, subscription)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(recordingApi.unsubscribe(apiClient, subscription)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + + /*********** + * Sensors * + ***********/ + + // SensorsAddDataPointIntentObservable + + @Test + public void SensorsAddDataPointIntentObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + PendingIntent pendingIntent = Mockito.mock(PendingIntent.class); + SensorsAddDataPointIntentObservable observable = spy(new SensorsAddDataPointIntentObservable(rxFit, sensorRequest, pendingIntent)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(sensorsApi.add(apiClient, sensorRequest, pendingIntent)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void SensorsAddDataPointIntentObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + PendingIntent pendingIntent = Mockito.mock(PendingIntent.class); + SensorsAddDataPointIntentObservable observable = spy(new SensorsAddDataPointIntentObservable(rxFit, sensorRequest, pendingIntent)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(sensorsApi.add(apiClient, sensorRequest, pendingIntent)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + + // SensorsRemoveDataPointIntentObservable + + @Test + public void SensorsRemoveDataPointIntentObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + PendingIntent pendingIntent = Mockito.mock(PendingIntent.class); + SensorsRemoveDataPointIntentObservable observable = spy(new SensorsRemoveDataPointIntentObservable(rxFit, pendingIntent)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(sensorsApi.remove(apiClient, pendingIntent)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void SensorsRemoveDataPointIntentObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + PendingIntent pendingIntent = Mockito.mock(PendingIntent.class); + SensorsRemoveDataPointIntentObservable observable = spy(new SensorsRemoveDataPointIntentObservable(rxFit, pendingIntent)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(sensorsApi.remove(apiClient, pendingIntent)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // SensorsDataPointObservable + + @Test + public void SensorsDataPointObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + DataPoint dataPoint = Mockito.mock(DataPoint.class); + SensorsDataPointObservable observable = spy(new SensorsDataPointObservable(rxFit, sensorRequest)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(sensorsApi.add(Matchers.any(GoogleApiClient.class), Matchers.any(SensorRequest.class), Matchers.any(OnDataPointListener.class))).thenReturn(pendingResult); + when(apiClient.isConnected()).thenReturn(true); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + observable.subscriber.onNext(dataPoint); + + verify(sensorsApi, never()).remove(Matchers.any(GoogleApiClient.class), Matchers.any(OnDataPointListener.class)); + sub.unsubscribe(); + verify(sensorsApi).remove(Matchers.any(GoogleApiClient.class), Matchers.any(OnDataPointListener.class)); + + sub.assertNoTerminalEvent(); + sub.assertValue(dataPoint); + } + + @Test + public void SensorsDataPointObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + SensorsDataPointObservable observable = spy(new SensorsDataPointObservable(rxFit, sensorRequest)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(sensorsApi.add(Matchers.any(GoogleApiClient.class), Matchers.any(SensorRequest.class), Matchers.any(OnDataPointListener.class))).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // SensorsFindDataSourcesObservable + + @Test + public void SensorsFindDataSourcesObservable_Success() { + TestSubscriber> sub = new TestSubscriber<>(); + DataSourcesRequest dataSourcesRequest = Mockito.mock(DataSourcesRequest.class); + DataSourcesResult dataSourcesResult = Mockito.mock(DataSourcesResult.class); + SensorsFindDataSourcesObservable observable = spy(new SensorsFindDataSourcesObservable(rxFit, dataSourcesRequest, null)); + + List dataSourceList = new ArrayList<>(); + dataSourceList.add(dataSource); + + when(dataSourcesResult.getDataSources()).thenReturn(dataSourceList); + + setPendingResultValue(dataSourcesResult); + when(dataSourcesResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(true); + when(sensorsApi.findDataSources(apiClient, dataSourcesRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, dataSourceList); + } + + @Test + public void SensorsFindDataSourcesObservable_StatusException() { + TestSubscriber> sub = new TestSubscriber<>(); + DataSourcesRequest dataSourcesRequest = Mockito.mock(DataSourcesRequest.class); + DataSourcesResult dataSourcesResult = Mockito.mock(DataSourcesResult.class); + SensorsFindDataSourcesObservable observable = spy(new SensorsFindDataSourcesObservable(rxFit, dataSourcesRequest, null)); + + List dataSourceList = new ArrayList<>(); + dataSourceList.add(dataSource); + + when(dataSourcesResult.getDataSources()).thenReturn(dataSourceList); + + setPendingResultValue(dataSourcesResult); + when(dataSourcesResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(false); + when(sensorsApi.findDataSources(apiClient, dataSourcesRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + @Test + public void SensorsFindDataSourcesObservable_WithDataType_Success() { + TestSubscriber> sub = new TestSubscriber<>(); + DataSourcesRequest dataSourcesRequest = Mockito.mock(DataSourcesRequest.class); + DataSourcesResult dataSourcesResult = Mockito.mock(DataSourcesResult.class); + SensorsFindDataSourcesObservable observable = spy(new SensorsFindDataSourcesObservable(rxFit, dataSourcesRequest, dataType)); + + List dataSourceList = new ArrayList<>(); + dataSourceList.add(dataSource); + + when(dataSourcesResult.getDataSources(dataType)).thenReturn(dataSourceList); + + setPendingResultValue(dataSourcesResult); + when(dataSourcesResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(true); + when(sensorsApi.findDataSources(apiClient, dataSourcesRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, dataSourceList); + } + + @Test + public void SensorsFindDataSourcesObservable_WithDataType_StatusException() { + TestSubscriber> sub = new TestSubscriber<>(); + DataSourcesRequest dataSourcesRequest = Mockito.mock(DataSourcesRequest.class); + DataSourcesResult dataSourcesResult = Mockito.mock(DataSourcesResult.class); + SensorsFindDataSourcesObservable observable = spy(new SensorsFindDataSourcesObservable(rxFit, dataSourcesRequest, dataType)); + + List dataSourceList = new ArrayList<>(); + dataSourceList.add(dataSource); + + when(dataSourcesResult.getDataSources(dataType)).thenReturn(dataSourceList); + + setPendingResultValue(dataSourcesResult); + when(dataSourcesResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(false); + when(sensorsApi.findDataSources(apiClient, dataSourcesRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + + /************ + * Sessions * + ************/ + + // SessionInsertObservable + + @Test + public void SessionInsertObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + SessionInsertRequest sessionInsertRequest = Mockito.mock(SessionInsertRequest.class); + SessionInsertObservable observable = spy(new SessionInsertObservable(rxFit, sessionInsertRequest)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(sessionsApi.insertSession(apiClient, sessionInsertRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void SessionInsertObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + SessionInsertRequest sessionInsertRequest = Mockito.mock(SessionInsertRequest.class); + SessionInsertObservable observable = spy(new SessionInsertObservable(rxFit, sessionInsertRequest)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(sessionsApi.insertSession(apiClient, sessionInsertRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // SessionRegisterObservable + + @Test + public void SessionRegisterObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + PendingIntent pendingIntent = Mockito.mock(PendingIntent.class); + SessionRegisterObservable observable = spy(new SessionRegisterObservable(rxFit, pendingIntent)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(sessionsApi.registerForSessions(apiClient, pendingIntent)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void SessionRegisterObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + PendingIntent pendingIntent = Mockito.mock(PendingIntent.class); + SessionRegisterObservable observable = spy(new SessionRegisterObservable(rxFit, pendingIntent)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(sessionsApi.registerForSessions(apiClient, pendingIntent)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // SessionUnregisterObservable + + @Test + public void SessionUnregisterObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + PendingIntent pendingIntent = Mockito.mock(PendingIntent.class); + SessionUnregisterObservable observable = spy(new SessionUnregisterObservable(rxFit, pendingIntent)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(sessionsApi.unregisterForSessions(apiClient, pendingIntent)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void SessionUnregisterObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + PendingIntent pendingIntent = Mockito.mock(PendingIntent.class); + SessionUnregisterObservable observable = spy(new SessionUnregisterObservable(rxFit, pendingIntent)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(sessionsApi.unregisterForSessions(apiClient, pendingIntent)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // SessionStartObservable + + @Test + public void SessionStartObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + Session session = Mockito.mock(Session.class); + SessionStartObservable observable = spy(new SessionStartObservable(rxFit, session)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(true); + when(sessionsApi.startSession(apiClient, session)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, status); + } + + @Test + public void SessionStartObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + SessionStartObservable observable = spy(new SessionStartObservable(rxFit, session)); + + setPendingResultValue(status); + when(status.isSuccess()).thenReturn(false); + when(sessionsApi.startSession(apiClient, session)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // SessionStopObservable + + @Test + public void SessionStopObservable_Success() { + TestSubscriber> sub = new TestSubscriber<>(); + String identifier = "identifier"; + SessionStopResult sessionStopResult = Mockito.mock(SessionStopResult.class); + SessionStopObservable observable = spy(new SessionStopObservable(rxFit, identifier)); + + List sessionList = new ArrayList<>(); + sessionList.add(session); + + when(sessionStopResult.getSessions()).thenReturn(sessionList); + + setPendingResultValue(sessionStopResult); + when(sessionStopResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(true); + when(sessionsApi.stopSession(apiClient, identifier)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, sessionList); + } + + @Test + public void SessionStopObservable_StatusException() { + TestSubscriber> sub = new TestSubscriber<>(); + String identifier = "identifier"; + SessionStopResult sessionStopResult = Mockito.mock(SessionStopResult.class); + SessionStopObservable observable = spy(new SessionStopObservable(rxFit, identifier)); + + List sessionList = new ArrayList<>(); + sessionList.add(session); + + when(sessionStopResult.getSessions()).thenReturn(sessionList); + + setPendingResultValue(sessionStopResult); + when(sessionStopResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(false); + when(sessionsApi.stopSession(apiClient, identifier)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + + // SessionReadObservable + + @Test + public void SessionReadObservable_Success() { + TestSubscriber sub = new TestSubscriber<>(); + SessionReadRequest sessionReadRequest = Mockito.mock(SessionReadRequest.class); + SessionReadResult sessionReadResult = Mockito.mock(SessionReadResult.class); + SessionReadObservable observable = spy(new SessionReadObservable(rxFit, sessionReadRequest)); + + setPendingResultValue(sessionReadResult); + when(sessionReadResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(true); + when(sessionsApi.readSession(apiClient, sessionReadRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertSingleValue(sub, sessionReadResult); + } + + @Test + public void SessionReadObservable_StatusException() { + TestSubscriber sub = new TestSubscriber<>(); + SessionReadRequest sessionReadRequest = Mockito.mock(SessionReadRequest.class); + SessionReadResult sessionReadResult = Mockito.mock(SessionReadResult.class); + SessionReadObservable observable = spy(new SessionReadObservable(rxFit, sessionReadRequest)); + + setPendingResultValue(sessionReadResult); + when(sessionReadResult.getStatus()).thenReturn(status); + when(status.isSuccess()).thenReturn(false); + when(sessionsApi.readSession(apiClient, sessionReadRequest)).thenReturn(pendingResult); + + setupBaseObservableSuccess(observable); + Observable.create(observable).subscribe(sub); + + assertError(sub, StatusException.class); + } + +} diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..5df7769 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,44 @@ +apply plugin: 'com.android.application' +apply plugin: 'me.tatarka.retrolambda' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + applicationId "com.patloew.rxfitsample" + minSdkVersion 9 + targetSdkVersion 23 + versionCode 1 + versionName "1.0.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +retrolambda { + javaVersion JavaVersion.VERSION_1_6 +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.2.0' + compile "com.android.support:design:23.2.0" + compile "com.android.support:recyclerview-v7:23.2.0" + compile "com.android.support:gridlayout-v7:23.2.0" + + compile project(':library') + + compile 'io.reactivex:rxjava:1.1.1' + + compile 'com.google.android.gms:play-services-fitness:8.4.0' +} diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 0000000..1d26e0f --- /dev/null +++ b/sample/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/patricklowenstein/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 *; +#} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bb9f1fc --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/com/patloew/rxfitsample/MainActivity.java b/sample/src/main/java/com/patloew/rxfitsample/MainActivity.java new file mode 100644 index 0000000..c15dbcf --- /dev/null +++ b/sample/src/main/java/com/patloew/rxfitsample/MainActivity.java @@ -0,0 +1,185 @@ +package com.patloew.rxfitsample; + +import android.os.Bundle; +import android.support.design.widget.Snackbar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.google.android.gms.common.Scopes; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.Scope; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.data.Field; +import com.google.android.gms.fitness.request.DataReadRequest; +import com.patloew.rxfit.RxFit; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import rx.Observable; +import rx.Subscription; + +public class MainActivity extends AppCompatActivity { + + RecyclerView recyclerView; + ProgressBar progressBar; + + ArrayList fitnessSessionDataList = new ArrayList<>(); + + Subscription subscription; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + recyclerView = (RecyclerView) findViewById(R.id.rv_main); + progressBar = (ProgressBar) findViewById(R.id.pb_main); + + recyclerView.setHasFixedSize(true); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + + RxFit.init(this, new Api[] { Fitness.SESSIONS_API, Fitness.HISTORY_API }, new Scope[] { new Scope(Scopes.FITNESS_ACTIVITY_READ) }); + + getFitnessData(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if(subscription != null && !subscription.isUnsubscribed()) { + subscription.unsubscribe(); + } + } + + private void getFitnessData() { + fitnessSessionDataList.clear(); + progressBar.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + + DataReadRequest dataReadRequest = new DataReadRequest.Builder() + .aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA) + .aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED) + .bucketBySession(1, TimeUnit.MINUTES) + // 3 months back + .setTimeRange(System.currentTimeMillis() - 7776000000L, System.currentTimeMillis(), TimeUnit.MILLISECONDS) + .build(); + + subscription = RxFit.History.read(dataReadRequest) + .flatMap(dataReadResult -> Observable.from(dataReadResult.getBuckets())) + .subscribe(bucket -> { + FitnessSessionData fitnessSessionData = new FitnessSessionData(); + fitnessSessionData.name = bucket.getSession().getName(); + fitnessSessionData.appName = bucket.getSession().getAppPackageName(); + fitnessSessionData.activity = bucket.getSession().getActivity(); + fitnessSessionData.start = new Date(bucket.getSession().getStartTime(TimeUnit.MILLISECONDS)); + fitnessSessionData.end = new Date(bucket.getSession().getEndTime(TimeUnit.MILLISECONDS)); + fitnessSessionData.steps = bucket.getDataSet(DataType.AGGREGATE_STEP_COUNT_DELTA).getDataPoints().get(0).getValue(Field.FIELD_STEPS).asInt(); + fitnessSessionData.calories = (int) bucket.getDataSet(DataType.AGGREGATE_CALORIES_EXPENDED).getDataPoints().get(0).getValue(Field.FIELD_CALORIES).asFloat(); + + fitnessSessionDataList.add(fitnessSessionData); + }, e -> { + progressBar.setVisibility(View.GONE); + Log.e("MainActivity", "Error reading fitness data", e); + Snackbar.make(recyclerView, "Error getting Fit data", Snackbar.LENGTH_INDEFINITE) + .setAction("Retry", v -> getFitnessData()) + .show(); + + }, () -> { + recyclerView.setAdapter(new FitnessSessionAdapter()); + progressBar.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + + if(fitnessSessionDataList.isEmpty()) { + Snackbar.make(recyclerView, "No sessions found", Snackbar.LENGTH_INDEFINITE).show(); + } + }); + } + + + private class FitnessSessionViewHolder extends RecyclerView.ViewHolder { + private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + + private TextView name; + private TextView dateTimeStart; + private TextView dateTimeStop; + private TextView activity; + private TextView calories; + private TextView steps; + private ImageView icon; + + public FitnessSessionViewHolder(View itemView) { + super(itemView); + + name = (TextView) itemView.findViewById(R.id.tv_name); + dateTimeStart = (TextView) itemView.findViewById(R.id.tv_datetime_start); + dateTimeStop = (TextView) itemView.findViewById(R.id.tv_datetime_stop); + activity = (TextView) itemView.findViewById(R.id.tv_activity); + calories = (TextView) itemView.findViewById(R.id.tv_calories); + steps = (TextView) itemView.findViewById(R.id.tv_steps); + icon = (ImageView) itemView.findViewById(R.id.iv_icon); + } + + public void bind(FitnessSessionData fitnessSessionData) { + dateTimeStart.setText("Start: " + DATE_FORMAT.format(fitnessSessionData.start)); + dateTimeStop.setText("Stop: " + DATE_FORMAT.format(fitnessSessionData.end)); + calories.setText(fitnessSessionData.calories + "kcal"); + steps.setText(fitnessSessionData.steps + " steps"); + activity.setText("Activity: " + fitnessSessionData.activity); + name.setText("Session" + (!TextUtils.isEmpty(fitnessSessionData.name) ? ": " + fitnessSessionData.name : "")); + + try { + icon.setVisibility(View.VISIBLE); + icon.setImageDrawable(getPackageManager().getApplicationIcon(fitnessSessionData.appName)); + } catch(Exception ignore) { + icon.setVisibility(View.GONE); + } + } + } + + private class FitnessSessionAdapter extends RecyclerView.Adapter { + + @Override + public FitnessSessionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_item_fitness_session, parent, false); + + return new FitnessSessionViewHolder(itemView); + } + + @Override + public void onBindViewHolder(FitnessSessionViewHolder holder, int position) { + holder.bind(fitnessSessionDataList.get(position)); + } + + @Override + public int getItemCount() { + return fitnessSessionDataList.size(); + } + } + + private static class FitnessSessionData { + public Date start; + public Date end; + public String name; + public String appName; + public String activity; + public int steps; + public int calories; + + } +} diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..0d186da --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/list_item_fitness_session.xml b/sample/src/main/res/layout/list_item_fitness_session.xml new file mode 100644 index 0000000..4816cb8 --- /dev/null +++ b/sample/src/main/res/layout/list_item_fitness_session.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde69bcccec65160d92116f20ffce4fce0b5245c GIT binary patch literal 3418 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 literal 0 HcmV?d00001 diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa42f0e7b91d006d22352c9ff2f134e504e3c1d GIT binary patch literal 4842 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 literal 0 HcmV?d00001 diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..324e72cdd7480cb983fa1bcc7ce686e51ef87fe7 GIT binary patch literal 7718 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 literal 0 HcmV?d00001 diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..aee44e138434630332d88b1680f33c4b24c70ab3 GIT binary patch literal 10486 zcmai4byOU|lb&5k+^GN3bv-?^>(QkVinb zlU9`mfQEQnq$S4VGrg6fmMQ=QFarQQ0ss(?uiys&;LQU7M-~7engIZmZaH5x#UC3m z-zvYBd&I}<`b3rPHj1tDgVv1x| zQss$ELI?W?E(!7PKk$lm@;7PwPX3o43{Ccd9@_BUsL4kQzSMa&=g{>4wj9#)9wgYw;=H@gH9KK{s?Be8N1_8W< z1Rh%Lm&PAfyYb*rGB%E#3q+}riOBB~+@@X<`9mgIiAex!QP8vg-XT>=+N&y*jC-f< zGihyr7XAly+G)|_e)qA?rnKZGG(x?=lLM7nrPk&93@5eX#7I_$g8kMX`0h=}l`HH) z=bpOkBCx=z*-fyr{yp7A9F=%o*qm93t_#tB2lAM@O{fX9ju%X#0~)nRUMvrXClh9w ze8|a0|0}JJg(_@$2wItI?LUY{zF78o(P2BR7;aC^@(jOp{8RE%U3m>MV5%Lu*46b@ zw*c?Nweu!TULS~}*9mi!ejNfNa=`po1*!jiYK)osxi%b59(thEyUZ>#lX@uEXSb_x?3)0kvB?8*TAh)7}IbzSm}5Ia;_?10{}M; z7vq-OS;Ayk8%_c-gg1Ee0FsrRU5phNs#H9Lp!1t+hwyK~9W0bWCxuG$LM~wQuumEw z=fbBD@sQE%1^j z`T@`PZLRVyWjX@*tjc7r;w$H~aW&7vu?|war?84^sg!{J*RH|mhq?KTsCVQBC1~fR z>99jeR=g-Q2b=d;pKwzXwYjrG>?pd3tFSsHN4in{usYLdK;01X2BdRLFI`cuB9yI) zI_ZX?7_(bz`MX2@^mCknx7 z*f}KV@}TBBc}CXMR8T_5yInD3p`KrNROSA;HoJJtlNG3weri%utO$eeY0 z+w-NEn;(;UCBk=OM$f%=%ma24wV7$idelqyNWI>sz1>BlGwr_3UugqVjY+UYyi9P) zxCB?&rPUetoZN?|*D%=hOOJ_${JU3GRjppY%&8Ws^G6>iokr^Bmv1&*@#2#5mXu05 zhPVXaQ`qe5i0lP-1^XL45x`ertKU5d-8b_?*1+tSU!qCeqD9gZP_>ZLq9p)RKtV(B zOh&^x>gV^eqb&c~Oi0|HgGG|gjpbR`9aRdZhOimvS2Y3e?eCFiw+L#_mi9j z;nU}gih+zTn{nv_|L}IllD1Dr3~@yitI}+4C&+;SR+cEfelqJ?eUjZ%&Qz)W8S750 z+vG8Lvo}xXz2C}S-m|9*uE?NWQWT#W+p@$DkH8wVn#=gLKa13M!Yva9qsfE(5Z#0V`A0pN)Ok zP*Eq0(~e$~m@iej0#Av_z703y-7|W6`UuGDS8fpy2rUgINZs#`33@@0(S%~%XUO5G zscEp&x^dU`8syC67USOswNLq>Z_}q#gLh2x`zR)0wvor72-IW@oDpnT0x zWn%LZ_yvR*7geY6<}MC~SViD+4`S9XC|L}N0ANpsUU;50sAjL zb5h>&s<-wcdf2>}P91QgeAu~ZnB7;;FkfKJp^8ne8!-`jK0+O(^`s~#RE0@)=IWiQ z@(vh6D^4jN5ih;*c4J48FMC9MwoN(cXk1Wiq55Vi-^X#p8R_(!y81}YDdMefwdl2F zNA0n}-!P4!FaCe-jnf{^I#?5W=%9T1C|$ z`+tq*x!rEx)Bkv-eO9$mWML9_yId)A_OltKIH-X=0eJ`Opqqj&s^T;PLIZXJ!pEi!=3ZLHPGi*~?<(L&m6;{M(636VC<08tan>&c6fW z%KEuUN9x|i7Wc^-0l&Vf20kI~_XfD4hEac=&}5n&MoYL`Xsx=1po#V*6wUpwB@pu* z*@2n|zglL~zr$9&uOd9_%)GWk&0UN`<&GAm8=Ba-@MT&TH*`NHlt+CMi2Ag;LgGpm zm+ybGL-!1Z$kBYk66=39zAsErw1}|-l1npj-?3g1LE#PXU%%_{8kO=5!W!6pQ?z&i zc_MuV(xKMXSA0ga@IsiwYspm&d4|n@L_zji`zUWxsM}|=@R}BFfT2P!uJcrQf81WG z;7~y_$uMK=ih(2hrfqIGOzb(81e}^7h$dQ*w9&zG_k*kV{ml>Dkn2!p9tb_+Sa82P zf!TC+{4a(i^7UC$53;w?sleb~lFWqeCjv5msi}#JQ!wJtA>=k~`WL0M{^a9PG3%vT z6x=jB0{7wX7$gs%H}xJ&s+hHnzrl#L*=KB8OZd%sPoxKs(`;%|I$(^;nFYa4Cg|3D zmbQ)m6I_Y@t)A~{YBRo!2sYI^n!q)$tPp|m&n1BkYVmX22Z+nY#4N{Bb0!Ko=DOhh z8)8*=>e(W&-%LSWUN;u45Wex{{R747!a~45S>12$wNc{9N95&r%gU+b#-B7PcF%`_ zbDPAsmvpVBsQpf}s{igh23+1)`QSj71!|zjij@kvxgob&J{E97Lwu==Z)RY-lujF1 zts{7+jfS(K5+clZ(CY~%ks(F!=cb)YtqEu(dp_7=A?O!zz8KONrrma{eU-54%}Dm| zMb0!-=YUH?S7JzBX|TVr;=fB(8}a+Mcip|v&=pAeFMCaHj_Nkl!sWeZSb#k<%oczm z#`lGsgJHo7RywsRYYQs4O`J_C=fARQ$)B1peZk)|&ULCaa#RJ45lrml54sxO!CCv< zACe-^PSoZc!)x$#iZa*NuMlS%Jd!_x9|UdgLzlGyF0cI$EUFG4O;L+8*+s;KNL-ld z?R+O)guOt(>{+*e-+_A{1MBbRn&>53j=33ngVZ*A9^^??x8!ww@-m%DVVPmliJh;B zA?gVg!0|Rs7)?hBD^!lSxbI8;-8Q65B4DKw29-K9_w0glvBA&vz=a(hBCWqSnbKS0 zUg%$!iEY%1jOqivHBW;uSX*e&(J!Yr7cborEc&_4TQAAt(Hs@99pynWwVQc-PD)!b zEAfVEq-cX>10nj+=mUt(v;j?>9`bLJayfOcTYEOojVJwg!qg=XHGMAonnJPa; zUJ!+pYTulTHW%^S;&|h~V3suNSc{q3^zg~L0z(5QQ;Fz}<5*7QiE`G{EY!_Bq6Tf3 z#Y6<%5EL^6+vT44<%^2!TOb&Drb?#eUqR@vqcvAd=l_6n*oWcLU38eLio z&XA9a$>+}PoZ&n7&1;j$MfqAp&SK~ziPsl|%{|CWXWM9wxyVKXe0%lk}rDC8g z8X@%6X|;SG;muLTK4d!cPgVxqjvaX=-$(Q65p5S*rI%=0cH7U(J{e1RPLJ7=nOmA) zMlRB`!r37ZXhzV+&X?quSyu}sbAn^a+S992*Te=%QW1izNzH-(Fc!u`0^%jIwx-q{ zjJ$P>vDS90xVX3yM??JQE(8|%*Ent^LOWJSOM1DpOGR5rG_7xH(O_SiI zQPhe?AtaSr$aWQDFB=s4vG}6A7sKS9#`*O?Gvb$VpNFveZ{M$e6gN?k zBAf6x8lMv8irB7O2F*?SxjQ+G9(Zzcf(-v6B#Che%7km*jk@ z)2}#vcILe$u75B8OqP#aD^OyEpX+8%bA;T*9+xPtBOA56r>VBH?W|l@4D*s*oHF7b zKiEI(=9Q&zzKDNu(c_-(iYp|O=RX90e|T*1D)Vi}F|XXxwzlFY%vI5oyr@gp+zfor zE{L0=4=<&pTg$Vb2&yaL(=zg-A=-V)<6G@}QKeym;mw^FzryGI(YX6E{x5!pKKNFb zX2wUTC}&?H`qv0{Ouyp!O!9>BD+&bp+x5*hFxlEJ|Jlx!dC36CiNWcOOOUw5NPT2n zckQz+nHS7$v`1`e33@@emu_-PmpnE%>A~wldBhO+8|uKd(CXF1LguU>p-iuo+6+#A(zwt<~}iz8;e zi$`F>cJ*M;o0PM7dMP=uB26set3i}BC!lE@>Gk`4oZQIG&&(O{wh_khwAz^jz zLMdgg*JfCk1{LlNW)C?WLX_!#5OsEIb3ZPWV7*KBWoBhmt&{(fw|eI)9LZTDrF;Cm zrRI0DXcArT*)L<`{Gy!R-`j)ca2)6Ks~48Jcl^Qg{XgWYyo6RpJj`Aq>-T>){#|lR zRPY`?<2vJ#s7v8mNz1zwnz@<9ofov5TnYTqj(PJN^Hv0N1N6rZY2Q2ixJ9IY`5B)j z?o!|2DLA8bc-{QD-^}@UP_JB`BjVr};f3o#5P`$++U2>eVvNM%RKxPV7J0hzme%(z zR7M~;#x=}vL&%^k)1dkFp)ApEinI%CXma_IcfN1= zghNTqbv$mD$mXwAWysU;hUAFR0^jhAYjE}TV=j$O0>v_@{)|7er^HCFN$j4D(Rxa+ zr>@Me?gS|zVlda*cn+sM7^g8|~YJlBlxK`p<| zo$B!mr$%Z4An3pBbh@BK4Hi-E7l^3GMOiG?^~~z1Oxn$0PAR&}&*9D$O)(_>aB04e z*{ihG%K2UZE9c%O@J$1R+qtuhVW+Li7>Bw~LBLxQ_2GJ6dWmr`sMzGzRfiKQrm?9I zR~`S8uz0=lw5lTY3!?lQ|2LJNx(Ly%0Hkj_Q0C+f8>^@`ot4vM)#Bo9*u)9;#4lPQ zkD$dnQJ;T3;cR_9pRiRuc^MkgYiS>6*;09uV{z*IYw3#i;TH$m(R{*3w>BS-cM7T<{u?6<8}o91iDU^B)<6wJwL{eG{=U+MNz z>#f)F`15Bnp|A(04!41E4ixt89MvouKW88SEk-A`6{3;V9M)Ips3VNFol3u5WiBmL ze0Uor5Z+x~NDGz=5gd!i#D5L)gN!7;`5bPc*8~;4hQOzIJ_RM07TD_cA!r1XISg_x z%9r&%6tsJq$>~|UQ1|7AZe{Oeu!2V&rjYX=>T-qb@S?3(7FC=Z^XOYf24G=+FJR;^ z&+s!YCtoncOWkA~zS!&wfYTiV$WJeR&@pINr7!v$Vw3}H92S?Mj>$ckH9eSoqhxli^L9 zl6?;LH$mT|@_S}#35}P!_7@h%=&u7n2PH0zl8K6L4SX!;*Nkxnnt~qhgVoG_|@w$t9uwee?p`9loMG zr|Qqo!ws?ZaVp;+zT!zH^@xtf^zzvEF*EJK-3hdBe&e4hTya+V7cwy9k?-&u+1W$J9MsjiXQu0{sN!(0)p=yn;5R~ zm8G1M$wClU4oHZeWuEucT>8fj9@#M0kY>Zjx}{F%fX>qa5#{2}lM>g}Xnjo}l|ew8 zkXA5h=I9hvEufUW_wOT8b^(DlBKCuM+=VI>J`Ua;1OioQTVInOmu*pv>=0&M>MOS| z%x%82SVXH|##aK|&I9wXCi2Kuz8@~`}P*VwE0=zPr%s5aHvFP`FsjEx2cBo)6ex*A zWp5GPoq0Vy74R>2aPlQP>~oZKw3$U(jAdy#E}=(clqiqe%$7=zb#t-GOC`@<-LJz{!m%n21KVT2lg4>F^Qyl9E2SvvZNE^Kq<8~8z*~izg_2G$e)DWZ z&r)^t$fjc4=0*E2GgW8V@;;-uQTLpkoe4G&6_Gi{=*bj1demc_{W*z@M)N3w-y!I2 zxt>0g2bLTSCr87lvU@@?w=y0(8-&vH2iDYp1oVatM3hj{k zTI09~y|)(A+XuR&rxolH&~6OyHuw;ulgO_ zPuTLyiVw)P|B03nB7klGZ1SdadQT)(_wcJpUd5Dw*Tl^3%=>G;G`B&%wwFm(MjZi# zMzuQuU>R1Zq8as9MkmM~4%8aV4m60Cl4X`?$zw27Nx(x@)C3hiNs$loyeJV|;3R`m z=2BoxiLeZq;~pUpKfO}+8=>;xkRT&Wh?xRT*$vA=e1-1-a(LQ&8&RQ!R;p| z0{dFY6Iuv97U8}VgGV$6PB!6w5}-jehsz>M8R?2d0-?1=c9Ek)8Yhh)!3TZPk1>d^py>9{d~my1NBGJ)ypHC;!FbEqzyVi zu?k`sqbi!2$c8~?{{=5xCd5}QNx$~UD2(hV0{VWx-}##X2uo*=a!4(~o_<3lOh;=1 zGWy!R&!cXBeOPdKzslPq+FOzt2P)Y6SL*2}8s1q7(#-PEp*Wm`{7r`W-T4WD{gKfb zL=!WtyH86@TGc=5%hW+QVgF5lmp6`bUz|y3kvDq8cEX#Zcon0xK`W6icDQ>?Gb=4k zx9`mayKC`XvhQ;fwwljzxg#~7>oUV^PafLCvQ3GNmYh3%udW9gpP}zdP01_?V#F|} zu+6A+v$!2@w>!LQS}Htz#xrDTMCHF(viHn9B@`r*AN^Uh^K1dYX%OU(L;QO-NS7sm zB}n&5G=+cvZdostKMXC?^Pljs93+p|U_TbCD$_YFH_al)C6D--qOJJg^-4S{e(_Bh(hqonQpIAR3 zLn22yQovcP8^(~lYa;Iw1iN45bC1LAyPgyMn!Us#kC~Od)l{8iBF=vyb{%q5Uo|At z`GioU@7{~W>87(`5`y7oUan|z+y9y6kLnnMdpTsuWXtd+^OE@Rc1&DlS#6q{VJQ~^2R25csGlWAI6%1)G(k1hy(%a6 zP8;j(?t{iGcAAzn*N4^9x1BG`9YQD?lsKuJE}E(!LRb-C04hKL&@?*uDt+rmq#F+E zy;MAG%p~MH`3$_n9%+YIg%-3+vV)5OcqKaeQuCmrhtqvaxZ!JAr|$dSF%)+`Yvoou zOSNuZL?Y9b&gUmyj|pfc5HOzcO#wTn_4)qhXWH?-2h*_V$bXFzOAO}R;U0Utm6jK1 zARXYF88&Au<4|bU zjIqU6CietjeFXz>A`VLxAln~?Tc3Z$!7ZUwvHhxe6;yAIYyV5DChijA_*mxgWa1Hf zpMe^m_ zi=Br9$|jmRXy`ALU7%BL%h!;kp0u2jEG>Y(3_SumS4~Ap=R2K`FOb*E9xFaK2xw@q5)FC9ki5__UGG^ChH* zg8T@CWK(2ZAhn)tl(@xrQ|@?sJZYbg?wPRykjvXSzBgO!5l;~}n=Vx=*>!3~hpG!QO_vZ7nOf(H%X8Zyf5zQI9<;&VgO`J^g!d%ci*Gayzi9E zzV{ggWXFUOwfXv^Cu9g;LXloZZQq$>osapDJ&dlE+FA zOAq0EeuKAV6~J_=V4ai?3X&T(A2S-Y-bb`Ai`xZ-D`VrnQ>pAdiPR0)l-S!eWp};M zhdf*YpjTWa+F;wAvaF(x6TW7LroZ>f%xX1B>ku{kHy23f4Gr*{SyBzch&H417J0V$b=yDLEIl7<2;YbKQ&{=ZOVvMR0}AxP zsmR+tme$kQHP;7Yn9&3eFJljv567buHH|D~F|nOk<45BcE*rk)#MT#RvWplVxMlzpi*dmU?7Pzz{?ICX{O>V+&4<<0nM?7@q6?=qp|+- z^F2j+>w(o9IZ#i9MKt?we*u>AF^=)GwlEo-<8)ZNsl`DO9Ts^3mN?;` zpu-&&=Gn~8C2og^of_Emg!Z)!`}l6?zCnvZ2)$RRO7E_te3B9iY#R5%#LUxR2a$64 zRNuv={A!3W0>=Vd9-Gygqi!GqnO4Wu*hSIx$FOH*78(*CzB@93|C9L^)cR86oytQX zz(VBa;uz&eA4;0&+0T7h>1okMFU4QmpaK8N1A2wlN0S5ncCO%AcYgA${c!kFQ+TiA zSE{2T+HSjei*$%Ai4A}4W1S3}-mXNa1B^jTL+Biw<*SD;pmpz7SdmFu%Z231W zkED`=rBr|FkuV%mCW~b>XQTCw%K0Clxj&QGIm4o%6lpuc4OgwWW^N>I z$CiUaixkCEQf)R*DBF6P&%z|)%AGchvGhBH3v_5YPKL6o6gDG~@`ZoTScT$`HQPz7 zQiqtq$|yTKXN%7 zSaCG2Ucn>50Z`>XxJnz6%(tPlqY9dGm@zHtV2!nWMmS!~Ac!e66nI-(6fh>Qh>8n)+v%wQv>T#tc54h zB%~5--xs;qRhX+bIms&XJP;?K$K2_5H1EpFn-*GyZaD5sGDZ&n5P~FndmWj1xxfxb zSocm{R9OVmD?CfFE;Oebf@%V^7{ZETZUhZ?GM(@uT|gImuIH#AeMtxlE^*teXWH`b z$LnM8?Q_|vjv^u(kO-Y$cB1?ICmH@j5PY(q zaPxf3LgA{hO>D7{M2?XnUpAsX?0!P#eL3cHStcyY4^PB2N&Y`}U05UvjiREStj@u{ z|B)ET + + 64dp + diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/sample/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/sample/src/main/res/values/dimens.xml b/sample/src/main/res/values/dimens.xml new file mode 100644 index 0000000..47c8224 --- /dev/null +++ b/sample/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 0000000..5e0d8e8 --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + RxFit Sample + diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml new file mode 100644 index 0000000..5885930 --- /dev/null +++ b/sample/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..1c52b4c --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':sample', ':library' \ No newline at end of file