Working code with everything squashed
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
a138a06de1
commit
b5396bab6f
41
.drone.yml
Normal file
41
.drone.yml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
workspace:
|
||||||
|
base: /src
|
||||||
|
path: .
|
||||||
|
|
||||||
|
pipeline:
|
||||||
|
build:
|
||||||
|
image: openjdk:8-jdk
|
||||||
|
environment:
|
||||||
|
- ANDROID_COMPILE_SDK=25
|
||||||
|
- ANDROID_BUILD_TOOLS=25.0.2
|
||||||
|
- ANDROID_SDK_TOOLS=3859397
|
||||||
|
commands:
|
||||||
|
- chmod +x ./gradlew
|
||||||
|
- export ANDROID_HOME=/src/android-sdk-linux
|
||||||
|
- export PATH=$${PATH}:$${ANDROID_HOME}/platform-tools/
|
||||||
|
- export GRADLE_USER_HOME=/src/.gradle
|
||||||
|
- apt-get --quiet update --yes
|
||||||
|
- apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
|
||||||
|
- wget --no-clobber --quiet --output-document=sdk-tools-linux-$${ANDROID_SDK_TOOLS}.zip https://dl.google.com/android/repository/sdk-tools-linux-$${ANDROID_SDK_TOOLS}.zip || true
|
||||||
|
- unzip -d $${ANDROID_HOME}/ sdk-tools-linux-$${ANDROID_SDK_TOOLS}.zip
|
||||||
|
- mkdir -p $${ANDROID_HOME}/licenses/
|
||||||
|
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > $${ANDROID_HOME}/licenses/android-sdk-license
|
||||||
|
- mkdir -p $${HOME}/.android
|
||||||
|
- touch $${HOME}/.android/repositories.cfg
|
||||||
|
- echo y | $${ANDROID_HOME}/tools/bin/sdkmanager --update
|
||||||
|
- echo y | $${ANDROID_HOME}/tools/bin/sdkmanager "platforms;android-$${ANDROID_COMPILE_SDK}"
|
||||||
|
- echo y | $${ANDROID_HOME}/tools/bin/sdkmanager "build-tools;$${ANDROID_BUILD_TOOLS}"
|
||||||
|
- echo y | $${ANDROID_HOME}/tools/bin/sdkmanager "extras;android;m2repository"
|
||||||
|
- echo y | $${ANDROID_HOME}/tools/bin/sdkmanager "extras;google;m2repository"
|
||||||
|
- echo y | $${ANDROID_HOME}/tools/bin/sdkmanager "extras;google;google_play_services"
|
||||||
|
- ./gradlew build
|
||||||
|
|
||||||
|
upload_debug:
|
||||||
|
image: vividboarder/drone-webdav
|
||||||
|
file: com.iamthefij.otbeta/build/outputs/apk/com.iamthefij.otbeta-debug.apk
|
||||||
|
destination: https://cloud.iamthefij.com/remote.php/dav/files/iamthefij/Android/Apks/
|
||||||
|
secrets:
|
||||||
|
- source: WEBDAV_USER
|
||||||
|
target: PLUGIN_USERNAME
|
||||||
|
- source: WEBDAV_PASSWORD
|
||||||
|
target: PLUGIN_PASSWORD
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -32,3 +32,11 @@ proguard/
|
|||||||
# Android Studio captures folder
|
# Android Studio captures folder
|
||||||
captures/
|
captures/
|
||||||
|
|
||||||
|
# Android Studio project files
|
||||||
|
*.iml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
|
|
||||||
|
# OSX things
|
||||||
|
.DS_Store
|
||||||
|
.tags
|
||||||
|
22
.idea/compiler.xml
generated
Normal file
22
.idea/compiler.xml
generated
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<option name="DEFAULT_COMPILER" value="Javac" />
|
||||||
|
<resourceExtensions />
|
||||||
|
<wildcardResourcePatterns>
|
||||||
|
<entry name="!?*.java" />
|
||||||
|
<entry name="!?*.form" />
|
||||||
|
<entry name="!?*.class" />
|
||||||
|
<entry name="!?*.groovy" />
|
||||||
|
<entry name="!?*.scala" />
|
||||||
|
<entry name="!?*.flex" />
|
||||||
|
<entry name="!?*.kt" />
|
||||||
|
<entry name="!?*.clj" />
|
||||||
|
</wildcardResourcePatterns>
|
||||||
|
<annotationProcessing>
|
||||||
|
<profile default="true" name="Default" enabled="false">
|
||||||
|
<processorPath useClasspath="true" />
|
||||||
|
</profile>
|
||||||
|
</annotationProcessing>
|
||||||
|
</component>
|
||||||
|
</project>
|
3
.idea/copyright/profiles_settings.xml
generated
Normal file
3
.idea/copyright/profiles_settings.xml
generated
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<component name="CopyrightManager">
|
||||||
|
<settings default="" />
|
||||||
|
</component>
|
8
.idea/dictionaries/iamthefij.xml
generated
Normal file
8
.idea/dictionaries/iamthefij.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="iamthefij">
|
||||||
|
<words>
|
||||||
|
<w>otbeta</w>
|
||||||
|
<w>iamthefij</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
6
.idea/encodings.xml
generated
Normal file
6
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="PROJECT" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
18
.idea/gradle.xml
generated
Normal file
18
.idea/gradle.xml
generated
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/com.iamthefij.otbeta" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
65
.idea/misc.xml
generated
Normal file
65
.idea/misc.xml
generated
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EntryPointsManager">
|
||||||
|
<entry_points version="2.0" />
|
||||||
|
<list size="1">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="org.junit.Test" />
|
||||||
|
</list>
|
||||||
|
</component>
|
||||||
|
<component name="NullableNotNullManager">
|
||||||
|
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||||
|
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||||
|
<option name="myNullables">
|
||||||
|
<value>
|
||||||
|
<list size="4">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="myNotNulls">
|
||||||
|
<value>
|
||||||
|
<list size="4">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
|
||||||
|
<OptionsSetting value="true" id="Add" />
|
||||||
|
<OptionsSetting value="true" id="Remove" />
|
||||||
|
<OptionsSetting value="true" id="Checkout" />
|
||||||
|
<OptionsSetting value="true" id="Update" />
|
||||||
|
<OptionsSetting value="true" id="Status" />
|
||||||
|
<OptionsSetting value="true" id="Edit" />
|
||||||
|
<ConfirmationsSetting value="0" id="Add" />
|
||||||
|
<ConfirmationsSetting value="0" id="Remove" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
<component name="masterDetails">
|
||||||
|
<states>
|
||||||
|
<state key="ProjectJDKs.UI">
|
||||||
|
<settings>
|
||||||
|
<last-edited>1.6</last-edited>
|
||||||
|
<splitter-proportions>
|
||||||
|
<option name="proportions">
|
||||||
|
<list>
|
||||||
|
<option value="0.27125505" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</splitter-proportions>
|
||||||
|
</settings>
|
||||||
|
</state>
|
||||||
|
</states>
|
||||||
|
</component>
|
||||||
|
</project>
|
9
.idea/modules.xml
generated
Normal file
9
.idea/modules.xml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/com.iamthefij.otbeta/com.iamthefij.otbeta.iml" filepath="$PROJECT_DIR$/com.iamthefij.otbeta/com.iamthefij.otbeta.iml" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/otbeta.iml" filepath="$PROJECT_DIR$/otbeta.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
12
.idea/runConfigurations.xml
generated
Normal file
12
.idea/runConfigurations.xml
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
2
LICENSE
2
LICENSE
@ -1,5 +1,5 @@
|
|||||||
MIT License
|
MIT License
|
||||||
Copyright (c) <year> <copyright holders>
|
Copyright (c) 2017 Ian Fijolek
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
11
README.md
11
README.md
@ -1,2 +1,13 @@
|
|||||||
# otbeta
|
# otbeta
|
||||||
|
|
||||||
|
Simple Open Source implementation of the OTBeat app for Android.
|
||||||
|
|
||||||
|
### Note
|
||||||
|
This is an Unofficial app. The API may change without notice and this client may break.
|
||||||
|
|
||||||
|
### TODO
|
||||||
|
- [x] Clean all personal info (names)
|
||||||
|
- [x] Add proper docstrings
|
||||||
|
- [ ] Squash down history
|
||||||
|
- [ ] Infinite scrolling to load more summaries
|
||||||
|
- [ ] Fix request caching or use a local state to persist data
|
||||||
|
24
build.gradle
Normal file
24
build.gradle
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// 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.3.0'
|
||||||
|
classpath 'de.mobilej.unmock:UnMockPlugin:0.6.0'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
1
com.iamthefij.otbeta/.gitignore
vendored
Normal file
1
com.iamthefij.otbeta/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
51
com.iamthefij.otbeta/build.gradle
Normal file
51
com.iamthefij.otbeta/build.gradle
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'de.mobilej.unmock'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 25
|
||||||
|
buildToolsVersion '25.0.2'
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.iamthefij.otbeta"
|
||||||
|
minSdkVersion 22
|
||||||
|
targetSdkVersion 25
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dexOptions {
|
||||||
|
javaMaxHeapSize "3g"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unMock {
|
||||||
|
keep 'android.location.Location'
|
||||||
|
keep 'android.util.SparseArray'
|
||||||
|
keep 'com.android.internal.util.ArrayUtils'
|
||||||
|
keep 'com.android.internal.util.GrowingArrayUtils'
|
||||||
|
keep 'org.json.JSON'
|
||||||
|
keep 'org.json.JSONObject'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
// https://mvnrepository.com/artifact/com.goebl/david-webb
|
||||||
|
compile group: 'com.goebl', name: 'david-webb', version: '1.3.0'
|
||||||
|
// https://mvnrepository.com/artifact/com.google.code.gson/gson
|
||||||
|
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.0'
|
||||||
|
// http://www.android-graphview.org/download-getting-started/
|
||||||
|
compile 'com.jjoe64:graphview:4.2.1'
|
||||||
|
|
||||||
|
compile 'com.android.support:appcompat-v7:25.2.0'
|
||||||
|
compile 'com.android.support:design:25.2.0'
|
||||||
|
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||||
|
compile 'com.android.support:cardview-v7:25.2.0'
|
||||||
|
compile 'com.android.support:support-v4:25.2.0'
|
||||||
|
compile 'com.android.support:recyclerview-v7:25.2.0'
|
||||||
|
testCompile 'junit:junit:4.12'
|
||||||
|
}
|
17
com.iamthefij.otbeta/proguard-rules.pro
vendored
Normal file
17
com.iamthefij.otbeta/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in $HOME/workspace/android-sdk-linux/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 *;
|
||||||
|
#}
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.iamthefij.otbeta;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.test.ApplicationTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||||
|
*/
|
||||||
|
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||||
|
public ApplicationTest() {
|
||||||
|
super(Application.class);
|
||||||
|
}
|
||||||
|
}
|
48
com.iamthefij.otbeta/src/main/AndroidManifest.xml
Normal file
48
com.iamthefij.otbeta/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.iamthefij.otbeta">
|
||||||
|
|
||||||
|
<!-- To auto-complete the email text field in the login form with the user's emails -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_PROFILE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.location.gps" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
android:fullBackupContent="@xml/backup_descriptor">
|
||||||
|
<activity
|
||||||
|
android:name=".SplashActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:noHistory="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity android:name=".LoginActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".WorkoutListActivity"
|
||||||
|
android:label="@string/title_workout_list"
|
||||||
|
android:theme="@style/AppTheme.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".WorkoutDetailActivity"
|
||||||
|
android:label="@string/title_workout_detail"
|
||||||
|
android:parentActivityName=".WorkoutListActivity"
|
||||||
|
android:theme="@style/AppTheme.NoActionBar">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="com.iamthefij.otbeta.WorkoutListActivity" />
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
BIN
com.iamthefij.otbeta/src/main/ic_launcher-web.png
Normal file
BIN
com.iamthefij.otbeta/src/main/ic_launcher-web.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
@ -0,0 +1,50 @@
|
|||||||
|
package com.iamthefij.otbeta;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.iamthefij.otbeta.api.Exerciser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store authenticated user information to device shared preferences
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ExerciserStore {
|
||||||
|
|
||||||
|
private static final String PREF_DEFAULT_STRING = "";
|
||||||
|
private static final String PREF_EXERCISER_KEY = "exerciser_json";
|
||||||
|
private static final String PREFS_NAME = ExerciserStore.class.getName();
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
public ExerciserStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Exerciser getExerciser() {
|
||||||
|
Exerciser exerciser = null;
|
||||||
|
String exerciserJson = getExerciserJsonString();
|
||||||
|
if (!exerciserJson.equals(PREF_DEFAULT_STRING)) {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
exerciser = gson.fromJson(exerciserJson, Exerciser.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
return exerciser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveExerciser(Exerciser exerciser) {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
String jsonSessionCookieString = gson.toJson(exerciser);
|
||||||
|
SharedPreferences.Editor editor = getPrefs().edit();
|
||||||
|
editor.putString(PREF_EXERCISER_KEY, jsonSessionCookieString);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getExerciserJsonString() {
|
||||||
|
return getPrefs().getString(PREF_EXERCISER_KEY, PREF_DEFAULT_STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SharedPreferences getPrefs() {
|
||||||
|
return mContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,609 @@
|
|||||||
|
package com.iamthefij.otbeta;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.location.LocationListener;
|
||||||
|
import android.location.LocationManager;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v4.app.ActivityCompat;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.app.LoaderManager.LoaderCallbacks;
|
||||||
|
|
||||||
|
import android.content.CursorLoader;
|
||||||
|
import android.content.Loader;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.ContactsContract;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.AutoCompleteTextView;
|
||||||
|
import android.widget.BaseAdapter;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.Spinner;
|
||||||
|
import android.widget.SpinnerAdapter;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.iamthefij.otbeta.api.Client;
|
||||||
|
import com.iamthefij.otbeta.api.Exerciser;
|
||||||
|
import com.iamthefij.otbeta.api.Location;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static android.Manifest.permission.READ_CONTACTS;
|
||||||
|
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A login screen that offers login via email/password.
|
||||||
|
*/
|
||||||
|
public class LoginActivity extends AppCompatActivity implements LoaderCallbacks<Cursor> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ids to identity permission requests.
|
||||||
|
*/
|
||||||
|
private static final int REQUEST_READ_CONTACTS = 0;
|
||||||
|
private static final int REQUEST_ACCESS_FINE_LOCATION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep track of the login task to ensure we can cancel it if requested.
|
||||||
|
*/
|
||||||
|
private UserLoginTask mAuthTask = null;
|
||||||
|
|
||||||
|
// UI references.
|
||||||
|
private Spinner mLocationView;
|
||||||
|
private AutoCompleteTextView mEmailView;
|
||||||
|
private EditText mPasswordView;
|
||||||
|
private View mProgressView;
|
||||||
|
private View mLoginFormView;
|
||||||
|
private Client mClient;
|
||||||
|
private Exerciser mExerciser;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_login);
|
||||||
|
|
||||||
|
// Initialize client
|
||||||
|
mClient = new Client(this);
|
||||||
|
|
||||||
|
// Set up the login form.
|
||||||
|
mLocationView = (Spinner) findViewById(R.id.location);
|
||||||
|
populateLocationList();
|
||||||
|
|
||||||
|
mEmailView = (AutoCompleteTextView) findViewById(R.id.email);
|
||||||
|
populateAutoComplete();
|
||||||
|
|
||||||
|
mPasswordView = (EditText) findViewById(R.id.password);
|
||||||
|
mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
|
||||||
|
if (id == R.id.login || id == EditorInfo.IME_NULL) {
|
||||||
|
attemptLogin();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button);
|
||||||
|
mEmailSignInButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
attemptLogin();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mLoginFormView = findViewById(R.id.login_form);
|
||||||
|
mProgressView = findViewById(R.id.login_progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateAutoComplete() {
|
||||||
|
if (!mayRequestContacts()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoaderManager().initLoader(0, null, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateLocationList() {
|
||||||
|
LocationListFetcher locationListFetcher = new LocationListFetcher();
|
||||||
|
locationListFetcher.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sortLocationList() {
|
||||||
|
if (!mayRequestLocation()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final LocationSpinnerAdapter locationSpinnerAdapter = (LocationSpinnerAdapter) mLocationView.getAdapter();
|
||||||
|
if (locationSpinnerAdapter != null && !locationSpinnerAdapter.isEmpty() && !locationSpinnerAdapter.isSorted()) {
|
||||||
|
final LocationManager locationManager = (LocationManager) getApplicationContext().getSystemService(LOCATION_SERVICE);
|
||||||
|
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, new BasicLocationListener(locationSpinnerAdapter, locationManager));
|
||||||
|
android.location.Location currentLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
|
||||||
|
if (currentLocation != null) {
|
||||||
|
locationSpinnerAdapter.sortByDistance(currentLocation.getLatitude(), currentLocation.getLongitude());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean mayRequestContacts() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (shouldShowRequestPermissionRationale(READ_CONTACTS)) {
|
||||||
|
Snackbar.make(mEmailView, R.string.contact_permission_rationale, Snackbar.LENGTH_INDEFINITE)
|
||||||
|
.setAction(android.R.string.ok, new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
|
public void onClick(View v) {
|
||||||
|
requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
requestPermissions(new String[]{READ_CONTACTS}, REQUEST_READ_CONTACTS);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean mayRequestLocation() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (checkSelfPermission(ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (shouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION)) {
|
||||||
|
Snackbar.make(mLocationView, R.string.location_permission_rationale, Snackbar.LENGTH_INDEFINITE)
|
||||||
|
.setAction(android.R.string.ok, new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
|
public void onClick(View v) {
|
||||||
|
requestPermissions(new String[]{ACCESS_FINE_LOCATION}, REQUEST_ACCESS_FINE_LOCATION);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
requestPermissions(new String[]{ACCESS_FINE_LOCATION}, REQUEST_ACCESS_FINE_LOCATION);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback received when a permissions request has been completed.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||||
|
@NonNull int[] grantResults) {
|
||||||
|
if (requestCode == REQUEST_READ_CONTACTS) {
|
||||||
|
if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
populateAutoComplete();
|
||||||
|
}
|
||||||
|
} else if (requestCode == REQUEST_ACCESS_FINE_LOCATION) {
|
||||||
|
if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
sortLocationList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to sign in or register the account specified by the login form.
|
||||||
|
* If there are form errors (invalid email, missing fields, etc.), the
|
||||||
|
* errors are presented and no actual login attempt is made.
|
||||||
|
*/
|
||||||
|
private void attemptLogin() {
|
||||||
|
if (mAuthTask != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset errors.
|
||||||
|
mEmailView.setError(null);
|
||||||
|
mPasswordView.setError(null);
|
||||||
|
|
||||||
|
// Store values at the time of the login attempt.
|
||||||
|
String email = mEmailView.getText().toString();
|
||||||
|
String password = mPasswordView.getText().toString();
|
||||||
|
Location location = (Location) mLocationView.getSelectedItem();
|
||||||
|
|
||||||
|
boolean cancel = false;
|
||||||
|
View focusView = null;
|
||||||
|
|
||||||
|
// Check for a valid password, if the user entered one.
|
||||||
|
if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) {
|
||||||
|
mPasswordView.setError(getString(R.string.error_invalid_password));
|
||||||
|
focusView = mPasswordView;
|
||||||
|
cancel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a valid email address.
|
||||||
|
if (TextUtils.isEmpty(email)) {
|
||||||
|
mEmailView.setError(getString(R.string.error_field_required));
|
||||||
|
focusView = mEmailView;
|
||||||
|
cancel = true;
|
||||||
|
} else if (!isEmailValid(email)) {
|
||||||
|
mEmailView.setError(getString(R.string.error_invalid_email));
|
||||||
|
focusView = mEmailView;
|
||||||
|
cancel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancel) {
|
||||||
|
// There was an error; don't attempt login and focus the first
|
||||||
|
// form field with an error.
|
||||||
|
focusView.requestFocus();
|
||||||
|
} else {
|
||||||
|
// Show a progress spinner, and kick off a background task to
|
||||||
|
// perform the user login attempt.
|
||||||
|
showProgress(true);
|
||||||
|
mAuthTask = new UserLoginTask(email, password, location);
|
||||||
|
mAuthTask.execute((Void) null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEmailValid(String email) {
|
||||||
|
return email.contains("@");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPasswordValid(String password) {
|
||||||
|
return password.length() > 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the progress UI and hides the login form.
|
||||||
|
*/
|
||||||
|
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
|
||||||
|
private void showProgress(final boolean show) {
|
||||||
|
// On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow
|
||||||
|
// for very easy animations. If available, use these APIs to fade-in
|
||||||
|
// the progress spinner.
|
||||||
|
int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime);
|
||||||
|
|
||||||
|
mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
|
||||||
|
mLoginFormView.animate().setDuration(shortAnimTime).alpha(
|
||||||
|
show ? 0 : 1).setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||||
|
mProgressView.animate().setDuration(shortAnimTime).alpha(
|
||||||
|
show ? 1 : 0).setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
|
||||||
|
return new CursorLoader(this,
|
||||||
|
// Retrieve data rows for the device user's 'profile' contact.
|
||||||
|
Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI,
|
||||||
|
ContactsContract.Contacts.Data.CONTENT_DIRECTORY), ProfileQuery.PROJECTION,
|
||||||
|
|
||||||
|
// Select only email addresses.
|
||||||
|
ContactsContract.Contacts.Data.MIMETYPE +
|
||||||
|
" = ?", new String[]{ContactsContract.CommonDataKinds.Email
|
||||||
|
.CONTENT_ITEM_TYPE},
|
||||||
|
|
||||||
|
// Show primary email addresses first. Note that there won't be
|
||||||
|
// a primary email address if the user hasn't specified one.
|
||||||
|
ContactsContract.Contacts.Data.IS_PRIMARY + " DESC");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
|
||||||
|
List<String> emails = new ArrayList<>();
|
||||||
|
cursor.moveToFirst();
|
||||||
|
while (!cursor.isAfterLast()) {
|
||||||
|
emails.add(cursor.getString(ProfileQuery.ADDRESS));
|
||||||
|
cursor.moveToNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
addEmailsToAutoComplete(emails);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoaderReset(Loader<Cursor> cursorLoader) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addEmailsToAutoComplete(List<String> emailAddressCollection) {
|
||||||
|
//Create adapter to tell the AutoCompleteTextView what to show in its dropdown list.
|
||||||
|
ArrayAdapter<String> adapter =
|
||||||
|
new ArrayAdapter<>(LoginActivity.this,
|
||||||
|
android.R.layout.simple_dropdown_item_1line, emailAddressCollection);
|
||||||
|
|
||||||
|
mEmailView.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ProfileQuery {
|
||||||
|
String[] PROJECTION = {
|
||||||
|
ContactsContract.CommonDataKinds.Email.ADDRESS,
|
||||||
|
ContactsContract.CommonDataKinds.Email.IS_PRIMARY,
|
||||||
|
};
|
||||||
|
|
||||||
|
int ADDRESS = 0;
|
||||||
|
int IS_PRIMARY = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an asynchronous login/registration task used to authenticate
|
||||||
|
* the user.
|
||||||
|
*/
|
||||||
|
public class UserLoginTask extends AsyncTask<Void, Void, Boolean> {
|
||||||
|
|
||||||
|
private final String mEmail;
|
||||||
|
private final String mPassword;
|
||||||
|
private final Location mLocation;
|
||||||
|
private Exerciser mTempExerciser;
|
||||||
|
|
||||||
|
UserLoginTask(String email, String password, Location location) {
|
||||||
|
mEmail = email;
|
||||||
|
mPassword = password;
|
||||||
|
mLocation = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
mTempExerciser = mClient.login(mEmail, mPassword, mLocation);
|
||||||
|
|
||||||
|
return (mTempExerciser != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(final Boolean success) {
|
||||||
|
mAuthTask = null;
|
||||||
|
showProgress(false);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
mExerciser = this.mTempExerciser;
|
||||||
|
launchMainActivity();
|
||||||
|
} else {
|
||||||
|
mPasswordView.setError(getString(R.string.error_incorrect_password));
|
||||||
|
mPasswordView.requestFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCancelled() {
|
||||||
|
mAuthTask = null;
|
||||||
|
showProgress(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the main activity after an exerciser is retrieved
|
||||||
|
*/
|
||||||
|
private void launchMainActivity() {
|
||||||
|
if (mExerciser != null) {
|
||||||
|
ExerciserStore exerciserStore = new ExerciserStore(this);
|
||||||
|
exerciserStore.saveExerciser(mExerciser);
|
||||||
|
Intent nextIntent = new Intent(LoginActivity.this, WorkoutListActivity.class);
|
||||||
|
startActivity(nextIntent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an asynchronous task to retrieve locations from the api and populate
|
||||||
|
* the location spinner
|
||||||
|
*/
|
||||||
|
private class LocationListFetcher extends AsyncTask<Void, Void, Boolean> {
|
||||||
|
|
||||||
|
private final android.location.Location mCurrentLocation;
|
||||||
|
private List<Location> mLocations;
|
||||||
|
|
||||||
|
public LocationListFetcher() {
|
||||||
|
mCurrentLocation = null;
|
||||||
|
mLocations = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocationListFetcher(android.location.Location currentLocation) {
|
||||||
|
mCurrentLocation = currentLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
mLocations = mClient.getLocationsWithDetail();
|
||||||
|
return (mLocations != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean success) {
|
||||||
|
super.onPostExecute(success);
|
||||||
|
if (success) {
|
||||||
|
LocationSpinnerAdapter locationSpinnerAdapter = new LocationSpinnerAdapter(mLocations);
|
||||||
|
mLocationView.setAdapter(locationSpinnerAdapter);
|
||||||
|
|
||||||
|
if (mCurrentLocation != null) {
|
||||||
|
locationSpinnerAdapter.sortByDistance(mCurrentLocation.getLatitude(), mCurrentLocation.getLongitude());
|
||||||
|
}
|
||||||
|
|
||||||
|
sortLocationList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter to show locations in spinner
|
||||||
|
*/
|
||||||
|
private class LocationSpinnerAdapter extends BaseAdapter implements SpinnerAdapter {
|
||||||
|
|
||||||
|
private final List<Location> mLocations;
|
||||||
|
private boolean mSorted;
|
||||||
|
|
||||||
|
public boolean isSorted() {
|
||||||
|
return mSorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocationSpinnerAdapter(List<Location> locations) {
|
||||||
|
mLocations = locations;
|
||||||
|
mSorted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return mLocations.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getItem(int position) {
|
||||||
|
return mLocations.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, ViewGroup parent) {
|
||||||
|
TextView text;
|
||||||
|
if (convertView != null) {
|
||||||
|
text = (TextView) convertView;
|
||||||
|
} else {
|
||||||
|
text = (TextView) getLayoutInflater().inflate(
|
||||||
|
android.R.layout.simple_dropdown_item_1line,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
text.setTextColor(Color.BLACK);
|
||||||
|
text.setText(mLocations.get(position).getName());
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts the locations in the list by distance from a given point
|
||||||
|
*
|
||||||
|
* @param lat latitude of current location to calculate distance from
|
||||||
|
* @param lon longitude of current location to calculate distance from
|
||||||
|
*/
|
||||||
|
public void sortByDistance(double lat, double lon) {
|
||||||
|
Collections.sort(mLocations, new LocationDistanceComparator(lat, lon));
|
||||||
|
mSorted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comparator that allows sorting of locations by distance from a point
|
||||||
|
*/
|
||||||
|
static class LocationDistanceComparator implements Comparator<Location> {
|
||||||
|
|
||||||
|
private final double mLat;
|
||||||
|
private final double mLon;
|
||||||
|
private final SparseArray<Float> mDistances;
|
||||||
|
|
||||||
|
public LocationDistanceComparator(double lat, double lon) {
|
||||||
|
mLat = lat;
|
||||||
|
mLon = lon;
|
||||||
|
mDistances = new SparseArray<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean locationHasCoords(Location l) {
|
||||||
|
return (l.hasAddress() && l.getAddress().hasLatitude() && l.getAddress().hasLongitude());
|
||||||
|
}
|
||||||
|
|
||||||
|
private float distanceToLocation(Location l) {
|
||||||
|
// Try to return from cache
|
||||||
|
float distance = mDistances.get(l.getId(), -1f);
|
||||||
|
if (distance > 0) {
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the distance between location and provided coordinates
|
||||||
|
float[] results = new float[2];
|
||||||
|
android.location.Location.distanceBetween(mLat, mLon, l.getAddress().getLatitude(), l.getAddress().getLongitude(), results);
|
||||||
|
distance = results[0];
|
||||||
|
// Store distance in cache
|
||||||
|
mDistances.put(l.getId(), distance);
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int compare(Location l1, Location l2) {
|
||||||
|
// TODO: Fix bug here. Getting "Comparison method violates its general contract!"
|
||||||
|
if (locationHasCoords(l1) && !locationHasCoords(l2)) {
|
||||||
|
return -1;
|
||||||
|
} else if (!locationHasCoords(l1) && locationHasCoords(l2)) {
|
||||||
|
return 1;
|
||||||
|
} else if (locationHasCoords(l1) && locationHasCoords(l2)){
|
||||||
|
float distance1 = distanceToLocation(l1);
|
||||||
|
float distance2 = distanceToLocation(l2);
|
||||||
|
|
||||||
|
if (distance1 > distance2) {
|
||||||
|
return 1;
|
||||||
|
} else if (distance1 < distance2) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All else equal, let's use name
|
||||||
|
if (l1.getName() != null && l2.getName() != null) {
|
||||||
|
return l1.getName().compareTo(l2.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BasicLocationListener implements LocationListener {
|
||||||
|
|
||||||
|
private static final String TAG = "LocationListener";
|
||||||
|
private final LocationSpinnerAdapter spinnerAdapter;
|
||||||
|
private final LocationManager locationManager;
|
||||||
|
|
||||||
|
public BasicLocationListener(LocationSpinnerAdapter spinnerAdapter, LocationManager locationManager) {
|
||||||
|
this.spinnerAdapter = spinnerAdapter;
|
||||||
|
this.locationManager = locationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLocationChanged(android.location.Location location) {
|
||||||
|
spinnerAdapter.sortByDistance(location.getLatitude(), location.getLongitude());
|
||||||
|
locationManager.removeUpdates(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStatusChanged(String s, int i, Bundle bundle) {
|
||||||
|
Log.d(TAG, "onStatusChanged: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProviderEnabled(String s) {
|
||||||
|
Log.d(TAG, "onProviderEnabled: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProviderDisabled(String s) {
|
||||||
|
Log.d(TAG, "onProviderDisabled: ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
|||||||
|
package com.iamthefij.otbeta;
|
||||||
|
/*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015 Lukas Zorich
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
|
import java.net.CookieManager;
|
||||||
|
import java.net.CookieStore;
|
||||||
|
import java.net.HttpCookie;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository for cookies. CookieManager will store cookies of every incoming HTTP response into
|
||||||
|
* CookieStore, and retrieve cookies for every outgoing HTTP request.
|
||||||
|
* <p/>
|
||||||
|
* Cookies are stored in {@link android.content.SharedPreferences} and will persist on the
|
||||||
|
* user's device between application session. {@link com.google.gson.Gson} is used to serialize
|
||||||
|
* the cookies into a json string in order to be able to save the cookie to
|
||||||
|
* {@link android.content.SharedPreferences}
|
||||||
|
* <p/>
|
||||||
|
* Created by lukas on 17-11-14.
|
||||||
|
*/
|
||||||
|
public class PersistentCookieStore implements CookieStore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default preferences string.
|
||||||
|
*/
|
||||||
|
private final static String PREF_DEFAULT_STRING = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The preferences name.
|
||||||
|
*/
|
||||||
|
private final static String PREFS_NAME = PersistentCookieStore.class.getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The preferences session cookie key.
|
||||||
|
*/
|
||||||
|
private final static String PREF_SESSION_COOKIE = "session_cookie";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session ID cookie name
|
||||||
|
*/
|
||||||
|
private final static String SESSION_COOKIE_NAME = "JSESSIONID";
|
||||||
|
|
||||||
|
private final CookieStore mStore;
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context The application context
|
||||||
|
*/
|
||||||
|
public PersistentCookieStore(Context context) {
|
||||||
|
// prevent context leaking by getting the application context
|
||||||
|
mContext = context.getApplicationContext();
|
||||||
|
|
||||||
|
// get the default in memory store and if there is a cookie stored in shared preferences,
|
||||||
|
// we added it to the cookie store
|
||||||
|
mStore = new CookieManager().getCookieStore();
|
||||||
|
String jsonSessionCookie = getJsonSessionCookieString();
|
||||||
|
if (!jsonSessionCookie.equals(PREF_DEFAULT_STRING)) {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
HttpCookie cookie = gson.fromJson(jsonSessionCookie, HttpCookie.class);
|
||||||
|
mStore.add(URI.create(cookie.getDomain()), cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(URI uri, HttpCookie cookie) {
|
||||||
|
if (cookie.getName().equals(SESSION_COOKIE_NAME)) {
|
||||||
|
// if the cookie that the cookie store attempt to add is a session cookie,
|
||||||
|
// we remove the older cookie and save the new one in shared preferences
|
||||||
|
remove(URI.create(cookie.getDomain()), cookie);
|
||||||
|
saveSessionCookie(cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
mStore.add(URI.create(cookie.getDomain()), cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<HttpCookie> get(URI uri) {
|
||||||
|
return mStore.get(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<HttpCookie> getCookies() {
|
||||||
|
return mStore.getCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<URI> getURIs() {
|
||||||
|
return mStore.getURIs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean remove(URI uri, HttpCookie cookie) {
|
||||||
|
return mStore.remove(uri, cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeAll() {
|
||||||
|
return mStore.removeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getJsonSessionCookieString() {
|
||||||
|
return getPrefs().getString(PREF_SESSION_COOKIE, PREF_DEFAULT_STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the HttpCookie to SharedPreferences as a json string.
|
||||||
|
*
|
||||||
|
* @param cookie The cookie to save in SharedPreferences.
|
||||||
|
*/
|
||||||
|
private void saveSessionCookie(HttpCookie cookie) {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
String jsonSessionCookieString = gson.toJson(cookie);
|
||||||
|
SharedPreferences.Editor editor = getPrefs().edit();
|
||||||
|
editor.putString(PREF_SESSION_COOKIE, jsonSessionCookieString);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SharedPreferences getPrefs() {
|
||||||
|
return mContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package com.iamthefij.otbeta;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import java.net.HttpCookie;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SplashActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_splash);
|
||||||
|
|
||||||
|
// Check for existing cookies
|
||||||
|
PersistentCookieStore cookieStore = new PersistentCookieStore(this);
|
||||||
|
List<HttpCookie> cookies = cookieStore.getCookies();
|
||||||
|
|
||||||
|
// Set next intent based on existing cookies
|
||||||
|
Intent nextIntent;
|
||||||
|
if (cookies.isEmpty()) {
|
||||||
|
nextIntent = new Intent(SplashActivity.this, LoginActivity.class);
|
||||||
|
} else {
|
||||||
|
nextIntent = new Intent(SplashActivity.this, WorkoutListActivity.class);
|
||||||
|
}
|
||||||
|
startActivity(nextIntent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package com.iamthefij.otbeta;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.support.v7.app.ActionBar;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An activity representing a single Workout detail screen. This
|
||||||
|
* activity is only used narrow width devices. On tablet-size devices,
|
||||||
|
* item details are presented side-by-side with a list of items
|
||||||
|
* in a {@link WorkoutListActivity}.
|
||||||
|
*/
|
||||||
|
public class WorkoutDetailActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_workout_detail);
|
||||||
|
Toolbar toolbar = (Toolbar) findViewById(R.id.detail_toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
|
// Show the Up button in the action bar.
|
||||||
|
ActionBar actionBar = getSupportActionBar();
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// savedInstanceState is non-null when there is fragment state
|
||||||
|
// saved from previous configurations of this activity
|
||||||
|
// (e.g. when rotating the screen from portrait to landscape).
|
||||||
|
// In this case, the fragment will automatically be re-added
|
||||||
|
// to its container so we don't need to manually add it.
|
||||||
|
// For more information, see the Fragments API guide at:
|
||||||
|
//
|
||||||
|
// http://developer.android.com/guide/components/fragments.html
|
||||||
|
//
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
// Create the detail fragment and add it to the activity
|
||||||
|
// using a fragment transaction.
|
||||||
|
Bundle arguments = new Bundle();
|
||||||
|
arguments.putString(WorkoutDetailFragment.ARG_EXERCISER_UUID,
|
||||||
|
getIntent().getStringExtra(WorkoutDetailFragment.ARG_EXERCISER_UUID));
|
||||||
|
arguments.putInt(WorkoutDetailFragment.ARG_WORKOUT_ID,
|
||||||
|
getIntent().getIntExtra(WorkoutDetailFragment.ARG_WORKOUT_ID, -1));
|
||||||
|
arguments.putString(WorkoutDetailFragment.ARG_WORKOUT_TITLE,
|
||||||
|
getIntent().getStringExtra(WorkoutDetailFragment.ARG_WORKOUT_TITLE));
|
||||||
|
WorkoutDetailFragment fragment = new WorkoutDetailFragment();
|
||||||
|
fragment.setArguments(arguments);
|
||||||
|
getSupportFragmentManager().beginTransaction()
|
||||||
|
.add(R.id.workout_detail_container, fragment)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
int id = item.getItemId();
|
||||||
|
if (id == android.R.id.home) {
|
||||||
|
// This ID represents the Home or Up button. In the case of this
|
||||||
|
// activity, the Up button is shown. For
|
||||||
|
// more details, see the Navigation pattern on Android Design:
|
||||||
|
//
|
||||||
|
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
|
||||||
|
//
|
||||||
|
navigateUpTo(new Intent(this, WorkoutListActivity.class));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,230 @@
|
|||||||
|
package com.iamthefij.otbeta;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.support.design.widget.CollapsingToolbarLayout;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.goebl.david.Response;
|
||||||
|
import com.goebl.david.WebbException;
|
||||||
|
import com.jjoe64.graphview.DefaultLabelFormatter;
|
||||||
|
import com.jjoe64.graphview.GraphView;
|
||||||
|
import com.jjoe64.graphview.GridLabelRenderer;
|
||||||
|
import com.jjoe64.graphview.ValueDependentColor;
|
||||||
|
import com.jjoe64.graphview.series.BarGraphSeries;
|
||||||
|
import com.jjoe64.graphview.series.DataPoint;
|
||||||
|
import com.jjoe64.graphview.series.LineGraphSeries;
|
||||||
|
import com.iamthefij.otbeta.api.Client;
|
||||||
|
import com.iamthefij.otbeta.api.Exerciser;
|
||||||
|
import com.iamthefij.otbeta.api.Interval;
|
||||||
|
import com.iamthefij.otbeta.api.Workout;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fragment representing a single Workout detail screen.
|
||||||
|
* This fragment is either contained in a {@link WorkoutListActivity}
|
||||||
|
* in two-pane mode (on tablets) or a {@link WorkoutDetailActivity}
|
||||||
|
* on handsets.
|
||||||
|
*/
|
||||||
|
public class WorkoutDetailFragment extends Fragment {
|
||||||
|
/**
|
||||||
|
* The fragment argument representing the item ID that this fragment
|
||||||
|
* represents.
|
||||||
|
*/
|
||||||
|
public static final String ARG_EXERCISER_UUID = "exerciser_uuid";
|
||||||
|
public static final String ARG_WORKOUT_ID = "workout_id";
|
||||||
|
public static final String ARG_WORKOUT_TITLE = "workout_TITLE";
|
||||||
|
|
||||||
|
private Client mClient;
|
||||||
|
private Exerciser mExerciser;
|
||||||
|
private Interval mInterval;
|
||||||
|
private Workout mWorkout;
|
||||||
|
private View mRootView;
|
||||||
|
private String mWorkoutTitle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||||
|
* fragment (e.g. upon screen orientation changes).
|
||||||
|
*/
|
||||||
|
public WorkoutDetailFragment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
mClient = new Client(this.getContext());
|
||||||
|
|
||||||
|
if (getArguments().containsKey(ARG_EXERCISER_UUID)) {
|
||||||
|
// TODO: In a real-world scenario, use a Loader
|
||||||
|
// to load content from a content provider.
|
||||||
|
mExerciser = new Exerciser(getArguments().getString(ARG_EXERCISER_UUID));
|
||||||
|
mInterval = new Interval(getArguments().getInt(ARG_WORKOUT_ID));
|
||||||
|
mWorkoutTitle = getArguments().getString(ARG_WORKOUT_TITLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
new WorkoutDetailGetter().execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState) {
|
||||||
|
mRootView = inflater.inflate(R.layout.workout_detail, container, false);
|
||||||
|
|
||||||
|
maybeSetRootView();
|
||||||
|
setTitle(mWorkoutTitle);
|
||||||
|
|
||||||
|
return mRootView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTitle(String workoutTitle) {
|
||||||
|
Activity activity = this.getActivity();
|
||||||
|
CollapsingToolbarLayout appBarLayout = (CollapsingToolbarLayout) activity.findViewById(R.id.toolbar_layout);
|
||||||
|
if (appBarLayout != null) {
|
||||||
|
appBarLayout.setTitle(workoutTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeSetRootView() {
|
||||||
|
if (mRootView != null && mWorkout != null) {
|
||||||
|
fillHeartRateZoneGraph((GraphView) mRootView.findViewById(R.id.workout_hr_zone_graph));
|
||||||
|
fillHeartRateGraph((GraphView) mRootView.findViewById(R.id.workout_hrm_graph));
|
||||||
|
fillStats(mRootView.findViewById(R.id.workout_detail_insert));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillStats(View view) {
|
||||||
|
TextView splatPointsView = (TextView) view.findViewById(R.id.workout_detail_splat_points);
|
||||||
|
splatPointsView.setText(String.valueOf(mWorkout.getPoints()));
|
||||||
|
|
||||||
|
TextView caloriesView = (TextView) view.findViewById(R.id.workout_detail_calories);
|
||||||
|
caloriesView.setText(String.valueOf(mWorkout.getCalories()));
|
||||||
|
|
||||||
|
TextView avgHrView = (TextView) view.findViewById(R.id.workout_detail_avg_hr);
|
||||||
|
avgHrView.setText(String.valueOf(mWorkout.getHeartRate().getAverage()));
|
||||||
|
|
||||||
|
TextView durationView = (TextView) view.findViewById(R.id.workout_detail_duration);
|
||||||
|
durationView.setText(String.valueOf(mWorkout.getDuration()/60));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillHeartRateZoneGraph(GraphView graphView) {
|
||||||
|
Workout.HeartRateZones zones = mWorkout.getHrm();
|
||||||
|
|
||||||
|
BarGraphSeries<DataPoint> series = new BarGraphSeries<>(
|
||||||
|
new DataPoint[] {
|
||||||
|
new DataPoint(0, zones.getGreyZoneSeconds()/60),
|
||||||
|
new DataPoint(1, zones.getBlueZoneSeconds()/60),
|
||||||
|
new DataPoint(2, zones.getGreenZoneSeconds()/60),
|
||||||
|
new DataPoint(3, zones.getOrangeZoneSeconds()/60),
|
||||||
|
new DataPoint(4, zones.getRedZoneSeconds()/60)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
series.setAnimated(true);
|
||||||
|
series.setDrawValuesOnTop(true);
|
||||||
|
series.setValuesOnTopColor(R.color.colorPrimary);
|
||||||
|
series.setValueDependentColor(new ValueDependentColor<DataPoint>() {
|
||||||
|
@Override
|
||||||
|
public int get(DataPoint data) {
|
||||||
|
switch ((int) data.getX()) {
|
||||||
|
case 0:
|
||||||
|
return Color.GRAY;
|
||||||
|
case 1:
|
||||||
|
return Color.BLUE;
|
||||||
|
case 2:
|
||||||
|
return Color.GREEN;
|
||||||
|
case 3:
|
||||||
|
return Color.rgb(255, 165, 0);
|
||||||
|
case 4:
|
||||||
|
return Color.RED;
|
||||||
|
|
||||||
|
}
|
||||||
|
return Color.DKGRAY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
graphView.getGridLabelRenderer().setHorizontalLabelsVisible(false);
|
||||||
|
graphView.getGridLabelRenderer().setGridStyle(GridLabelRenderer.GridStyle.HORIZONTAL);
|
||||||
|
graphView.getViewport().setXAxisBoundsManual(true);
|
||||||
|
graphView.getViewport().setMinX(-0.5);
|
||||||
|
graphView.getViewport().setMaxX(4.5);
|
||||||
|
graphView.addSeries(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillHeartRateGraph(GraphView graphView) {
|
||||||
|
Workout.Metric heartRate = mWorkout.getHeartRate();
|
||||||
|
List<Double> values = heartRate.getValues();
|
||||||
|
|
||||||
|
double time = 0.0;
|
||||||
|
int numIntervals = values.size();
|
||||||
|
int intervalLength = heartRate.getInterval();
|
||||||
|
int targetHeartRate = mWorkout.getHrm().getTargetHeartRate();
|
||||||
|
double percentOfTarget;
|
||||||
|
DataPoint[] dataPoints = new DataPoint[numIntervals];
|
||||||
|
for (int i = 0; i < numIntervals; i++) {
|
||||||
|
percentOfTarget = (values.get(i) / targetHeartRate) * 100;
|
||||||
|
dataPoints[i] = new DataPoint(time, percentOfTarget);
|
||||||
|
time += intervalLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
graphView.getGridLabelRenderer().setVerticalAxisTitle("Heart Rate");
|
||||||
|
graphView.getGridLabelRenderer().setHorizontalAxisTitle("Minutes");
|
||||||
|
graphView.getGridLabelRenderer().setLabelFormatter(new DefaultLabelFormatter() {
|
||||||
|
@Override
|
||||||
|
public String formatLabel(double value, boolean isValueX) {
|
||||||
|
if (isValueX) {
|
||||||
|
// Convert to minutes
|
||||||
|
value = value / 60;
|
||||||
|
}
|
||||||
|
return super.formatLabel(value, isValueX);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
graphView.addSeries(new LineGraphSeries<>(dataPoints));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an asynchronous task to retrieve workouts from the api and populate
|
||||||
|
* the recycle view adapter
|
||||||
|
*/
|
||||||
|
private class WorkoutDetailGetter extends AsyncTask<Void, Void, Boolean> {
|
||||||
|
|
||||||
|
private Workout mWorkoutTemp;
|
||||||
|
private boolean mInvalidSession = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
try {
|
||||||
|
mWorkoutTemp = mClient.getWorkout(mExerciser, mInterval);
|
||||||
|
} catch (WebbException ex) {
|
||||||
|
Response response = ex.getResponse();
|
||||||
|
if (response.getStatusCode() == 403) {
|
||||||
|
mInvalidSession = true;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean success) {
|
||||||
|
super.onPostExecute(success);
|
||||||
|
if (success) {
|
||||||
|
mWorkout = mWorkoutTemp;
|
||||||
|
maybeSetRootView();
|
||||||
|
} else if (mInvalidSession) {
|
||||||
|
Intent loginActivity = new Intent(getActivity(), LoginActivity.class);
|
||||||
|
startActivity(loginActivity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,262 @@
|
|||||||
|
package com.iamthefij.otbeta;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.http.HttpResponseCache;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
|
||||||
|
import com.goebl.david.Response;
|
||||||
|
import com.goebl.david.WebbException;
|
||||||
|
import com.iamthefij.otbeta.api.Client;
|
||||||
|
import com.iamthefij.otbeta.api.Exerciser;
|
||||||
|
import com.iamthefij.otbeta.api.Interval;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An activity representing a list of Workouts. This activity
|
||||||
|
* has different presentations for handset and tablet-size devices. On
|
||||||
|
* handsets, the activity presents a list of items, which when touched,
|
||||||
|
* lead to a {@link WorkoutDetailActivity} representing
|
||||||
|
* item details. On tablets, the activity presents the list of items and
|
||||||
|
* item details side-by-side using two vertical panes.
|
||||||
|
*/
|
||||||
|
public class WorkoutListActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final String TAG = "WorkoutListActivity";
|
||||||
|
/**
|
||||||
|
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
|
||||||
|
* device.
|
||||||
|
*/
|
||||||
|
private boolean mTwoPane;
|
||||||
|
private Client mClient;
|
||||||
|
private Exerciser mExerciser;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_workout_list);
|
||||||
|
|
||||||
|
// Install cache to prevent HTTP requests every time a view is revisited
|
||||||
|
installHttpCache();
|
||||||
|
|
||||||
|
// Initialize client
|
||||||
|
mClient = new Client(this);
|
||||||
|
|
||||||
|
// Get Exerciser from store
|
||||||
|
ExerciserStore exerciserStore = new ExerciserStore(this);
|
||||||
|
mExerciser = exerciserStore.getExerciser();
|
||||||
|
|
||||||
|
// Get
|
||||||
|
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.workout_list);
|
||||||
|
new WorkoutListGetter(recyclerView).execute();
|
||||||
|
|
||||||
|
// Set up the toolbar
|
||||||
|
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
toolbar.setTitle(getTitle());
|
||||||
|
|
||||||
|
// Determine if dual pane
|
||||||
|
if (findViewById(R.id.workout_detail_container) != null) {
|
||||||
|
// The detail container view will be present only in the
|
||||||
|
// large-screen layouts (res/values-w900dp).
|
||||||
|
// If this view is present, then the
|
||||||
|
// activity should be in two-pane mode.
|
||||||
|
mTwoPane = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
flushHttpCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installHttpCache() {
|
||||||
|
try {
|
||||||
|
File httpCacheDir = new File(this.getCacheDir(), "http");
|
||||||
|
long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
|
||||||
|
HttpResponseCache.install(httpCacheDir, httpCacheSize);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.i(TAG, "HTTP response cache installation failed:" + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushHttpCache() {
|
||||||
|
HttpResponseCache cache = HttpResponseCache.getInstalled();
|
||||||
|
if (cache != null) {
|
||||||
|
cache.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an asynchronous task to retrieve workouts from the api and populate
|
||||||
|
* the recycle view adapter
|
||||||
|
*/
|
||||||
|
private class WorkoutListGetter extends AsyncTask<Void, Void, Boolean> {
|
||||||
|
|
||||||
|
private final RecyclerView mRecyclerView;
|
||||||
|
private List<Interval> mWorkouts;
|
||||||
|
private boolean mInvalidSession = false;
|
||||||
|
|
||||||
|
WorkoutListGetter(RecyclerView recyclerView) {
|
||||||
|
mRecyclerView = recyclerView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
try {
|
||||||
|
mWorkouts = mClient.getWorkouts(mExerciser, 30);
|
||||||
|
} catch (WebbException ex) {
|
||||||
|
Response response = ex.getResponse();
|
||||||
|
if (response.getStatusCode() == 403) {
|
||||||
|
mInvalidSession = true;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mWorkouts != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean success) {
|
||||||
|
super.onPostExecute(success);
|
||||||
|
if (success) {
|
||||||
|
WorkoutRecyclerAdapter adapter = new WorkoutRecyclerAdapter(mWorkouts);
|
||||||
|
mRecyclerView.setAdapter(adapter);
|
||||||
|
} else if (mInvalidSession) {
|
||||||
|
Intent loginActivity = new Intent(WorkoutListActivity.this, LoginActivity.class);
|
||||||
|
startActivity(loginActivity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter to display workouts as a list of cards
|
||||||
|
*/
|
||||||
|
private class WorkoutRecyclerAdapter
|
||||||
|
extends RecyclerView.Adapter<WorkoutRecyclerAdapter.WorkoutViewHolder> {
|
||||||
|
|
||||||
|
private final List<Interval> mWorkouts;
|
||||||
|
|
||||||
|
WorkoutRecyclerAdapter(List<Interval> workouts) {
|
||||||
|
mWorkouts = workouts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorkoutViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.workout_card_view, parent, false);
|
||||||
|
return new WorkoutViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(final WorkoutViewHolder holder, int position) {
|
||||||
|
Interval workout = mWorkouts.get(position);
|
||||||
|
if (workout != null) {
|
||||||
|
holder.fillView(workout);
|
||||||
|
|
||||||
|
holder.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (mTwoPane) {
|
||||||
|
Bundle arguments = new Bundle();
|
||||||
|
arguments.putString(WorkoutDetailFragment.ARG_EXERCISER_UUID, mExerciser.getUuid());
|
||||||
|
arguments.putInt(WorkoutDetailFragment.ARG_WORKOUT_ID, holder.getWorkout().getId());
|
||||||
|
arguments.putString(WorkoutDetailFragment.ARG_WORKOUT_TITLE, holder.getTitle());
|
||||||
|
WorkoutDetailFragment fragment = new WorkoutDetailFragment();
|
||||||
|
fragment.setArguments(arguments);
|
||||||
|
getSupportFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.workout_detail_container, fragment)
|
||||||
|
.commit();
|
||||||
|
} else {
|
||||||
|
Context context = v.getContext();
|
||||||
|
Intent intent = new Intent(context, WorkoutDetailActivity.class);
|
||||||
|
intent.putExtra(WorkoutDetailFragment.ARG_EXERCISER_UUID, mExerciser.getUuid());
|
||||||
|
intent.putExtra(WorkoutDetailFragment.ARG_WORKOUT_ID, holder.getWorkout().getId());
|
||||||
|
intent.putExtra(WorkoutDetailFragment.ARG_WORKOUT_TITLE, holder.getTitle());
|
||||||
|
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return mWorkouts.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
class WorkoutViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
private final View mView;
|
||||||
|
private final TextView mWorkoutDateView;
|
||||||
|
private final TextView mWorkoutCaloriesView;
|
||||||
|
private final TextView mWorkoutMinutesView;
|
||||||
|
private final TextView mWorkoutPointsView;
|
||||||
|
private Interval mWorkout;
|
||||||
|
|
||||||
|
Interval getWorkout() {
|
||||||
|
return mWorkout;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkoutViewHolder(View view) {
|
||||||
|
super(view);
|
||||||
|
mView = view;
|
||||||
|
mWorkoutDateView = (TextView) mView.findViewById(R.id.workout_date);
|
||||||
|
mWorkoutCaloriesView = (TextView) mView.findViewById(R.id.workout_calories);
|
||||||
|
mWorkoutMinutesView = (TextView) mView.findViewById(R.id.workout_minutes);
|
||||||
|
mWorkoutPointsView = (TextView) mView.findViewById(R.id.workout_points);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
void fillView(Interval workout) {
|
||||||
|
mWorkout = workout;
|
||||||
|
|
||||||
|
String workoutDate = "Unknown";
|
||||||
|
String localIsoTime = workout.getStartDateLocal();
|
||||||
|
if (localIsoTime != null) {
|
||||||
|
SimpleDateFormat isoParser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US);
|
||||||
|
SimpleDateFormat formatter = new SimpleDateFormat("MMM d, yyyy", Locale.US);
|
||||||
|
try {
|
||||||
|
Date localIso = isoParser.parse(localIsoTime);
|
||||||
|
workoutDate = formatter.format(localIso);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mWorkoutDateView.setText(workoutDate);
|
||||||
|
mWorkoutCaloriesView.setText(String.format("%d Calories", workout.getTotalCalories()));
|
||||||
|
mWorkoutMinutesView.setText(String.format("%d minutes", workout.getTotalDuration()/60));
|
||||||
|
mWorkoutPointsView.setText(String.format("%d Splat Points", workout.getTotalPoints()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOnClickListener(View.OnClickListener listener) {
|
||||||
|
mView.setOnClickListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getTitle() {
|
||||||
|
return (String) mWorkoutDateView.getText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
package com.iamthefij.otbeta.api;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.http.HttpResponseCache;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.goebl.david.Webb;
|
||||||
|
import com.iamthefij.otbeta.PersistentCookieStore;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.net.CookieHandler;
|
||||||
|
import java.net.CookieManager;
|
||||||
|
import java.net.CookiePolicy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple client for communicating with netpulse
|
||||||
|
*/
|
||||||
|
public class Client {
|
||||||
|
private static final String BASE_URL = "https://orangetheoryfitness.netpulse.com/np";
|
||||||
|
private static final String TAG = "OTBeta";
|
||||||
|
private final Webb webb;
|
||||||
|
|
||||||
|
public Client(Context context) {
|
||||||
|
webb = Webb.create();
|
||||||
|
webb.setBaseUri(BASE_URL);
|
||||||
|
|
||||||
|
// Session id is stored in a cookie so this is used to persist the logged in session
|
||||||
|
CookieManager cookieManager = new CookieManager(new PersistentCookieStore(context), CookiePolicy.ACCEPT_ALL);
|
||||||
|
CookieHandler.setDefault(cookieManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all OT locations along with Uuids
|
||||||
|
*
|
||||||
|
* @return List of {@link Location} instances with minimal information
|
||||||
|
*/
|
||||||
|
public List<Location> getLocations() {
|
||||||
|
return getLocations("simple");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all OT locations along with Uuids and Address information
|
||||||
|
*
|
||||||
|
* @return List of {@link Location} with detailed address information
|
||||||
|
*/
|
||||||
|
public List<Location> getLocationsWithDetail() {
|
||||||
|
return getLocations("detail");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Location> getLocations(String responseType) {
|
||||||
|
JSONArray response = webb.get("/company/children")
|
||||||
|
.param("responseType", responseType)
|
||||||
|
.param("partnerAlias", "clubComq")
|
||||||
|
.ensureSuccess()
|
||||||
|
.asJsonArray()
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
JSONObject location;
|
||||||
|
List<Location> locations = new ArrayList<>(response.length());
|
||||||
|
for (int i = 0; i < response.length(); i++) {
|
||||||
|
location = response.optJSONObject(i);
|
||||||
|
if (location != null) {
|
||||||
|
locations.add(new Location(location));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(TAG, String.format("getLocations: Added %d locations to list", locations.size()));
|
||||||
|
return locations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates a user and returns their information
|
||||||
|
*
|
||||||
|
* User session is stored as a cookie
|
||||||
|
*
|
||||||
|
* @param username Email address of the user to authenticate
|
||||||
|
* @param password Password for the authenticating user
|
||||||
|
* @param homeClub {@link Location} instance of the users home club
|
||||||
|
* @return {@link Exerciser} representing the authenticated user or null
|
||||||
|
*/
|
||||||
|
public Exerciser login(String username, String password, Location homeClub) {
|
||||||
|
return login(username, password, homeClub.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Exerciser login(String username, String password, String homeClubUuid) {
|
||||||
|
JSONObject response = webb.post("/exerciser/login")
|
||||||
|
.param("username", username)
|
||||||
|
.param("password", password)
|
||||||
|
.param("homeClubUuid", homeClubUuid)
|
||||||
|
.ensureSuccess()
|
||||||
|
.asJsonObject()
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return new Exerciser(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the full profile for a given Exerciser
|
||||||
|
*
|
||||||
|
* @param exerciser usr to retrieve profile for
|
||||||
|
* @return ExerciserProfile instance with extended user data
|
||||||
|
*/
|
||||||
|
public ExerciserProfile getExerciserProfile(Exerciser exerciser) {
|
||||||
|
JSONObject response = webb.get("/exerciser/" + exerciser.getUuid())
|
||||||
|
.ensureSuccess()
|
||||||
|
.asJsonObject()
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return new ExerciserProfile(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a monthly summary for an exerciser for a given number of months from the current date
|
||||||
|
*
|
||||||
|
* @param exerciser {@link Exerciser} instance of a user to get summary for
|
||||||
|
* @param months max number of months to return
|
||||||
|
* @return list of {@link Interval} results containing the summaries
|
||||||
|
*/
|
||||||
|
public List<Interval> getMonthlyWorkoutSummary(Exerciser exerciser, int months) {
|
||||||
|
return getWorkouts(exerciser.getUuid(), null, null, months, "month");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets list of recent individual workout summaries for an exerciser from the current date
|
||||||
|
*
|
||||||
|
* @param exerciser {@link Exerciser} instance of a user to get summary for
|
||||||
|
* @param count max number of workouts to return
|
||||||
|
* @return list of {@link Interval} results containing the summaries
|
||||||
|
*/
|
||||||
|
public List<Interval> getWorkouts(Exerciser exerciser, int count) {
|
||||||
|
return getWorkouts(exerciser.getUuid(), null, null, count, "single");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Interval> getWorkouts(String exerciserUuid, String filter, String endDate, Integer intervalCount, String interval) {
|
||||||
|
// Set default values
|
||||||
|
if (filter == null) filter = "all";
|
||||||
|
if (endDate == null) endDate = "20450101"; // Arbitrarily future date
|
||||||
|
if (intervalCount == null) intervalCount = 10;
|
||||||
|
// "single" "month"
|
||||||
|
if (interval == null) filter = "single";
|
||||||
|
|
||||||
|
JSONObject response = webb.get("/exerciser/" + exerciserUuid + "/workouts")
|
||||||
|
.param("filter", filter)
|
||||||
|
.param("endDate", endDate)
|
||||||
|
.param("intervalCount", intervalCount)
|
||||||
|
.param("interval", interval)
|
||||||
|
.ensureSuccess()
|
||||||
|
.asJsonObject()
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
Interval.setSpeedUnit(response.optString("speedUnit"));
|
||||||
|
Interval.setDistanceUnit(response.optString("distanceUnit")) ;
|
||||||
|
JSONArray intervalResponse = response.optJSONArray("intervals");
|
||||||
|
if (intervalResponse != null) {
|
||||||
|
List<Interval> intervals = new ArrayList<>(intervalResponse.length());
|
||||||
|
for (int i = 0; i < intervalResponse.length(); i++) {
|
||||||
|
JSONObject intervalData = intervalResponse.optJSONObject(i);
|
||||||
|
if (intervalData != null) {
|
||||||
|
intervals.add(new Interval(intervalData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(TAG, String.format("getWorkouts: Added %d intervals to list", intervals.size()));
|
||||||
|
return intervals;
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "getWorkouts: Could not parse workout intervals");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all details for a given workout
|
||||||
|
*
|
||||||
|
* This is where most of the interesting data is including all metrics for HR
|
||||||
|
*
|
||||||
|
* @param exerciser {@link Exerciser} instance of a user workout for
|
||||||
|
* @param interval the {@link Interval} to retrieve details of
|
||||||
|
* @return a {@link Workout} instance containing all details and metrics
|
||||||
|
*/
|
||||||
|
public Workout getWorkout(Exerciser exerciser, Interval interval) {
|
||||||
|
return getWorkout(exerciser.getUuid(), interval.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Workout getWorkout(String exerciserUuid, int intervalId) {
|
||||||
|
JSONObject response = webb.get("/exerciser/" + exerciserUuid + "/workout/" + String.valueOf(intervalId))
|
||||||
|
.ensureSuccess()
|
||||||
|
.useCaches(true)
|
||||||
|
.asJsonObject()
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
HttpResponseCache cache = HttpResponseCache.getInstalled();
|
||||||
|
if (cache != null) {
|
||||||
|
Log.d(TAG, String.format(Locale.US, "getWorkout: Got location. Cache results: hit %d, total requests %d", cache.getHitCount(), cache.getRequestCount()));
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "getWorkout: Got location. No installed cache.");
|
||||||
|
}
|
||||||
|
return new Workout(response);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
package com.iamthefij.otbeta.api;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an Exerciser or user
|
||||||
|
*/
|
||||||
|
public class Exerciser {
|
||||||
|
|
||||||
|
private final String mUuid;
|
||||||
|
private final String mFirstName;
|
||||||
|
private final String mLastName;
|
||||||
|
private final Boolean mVerified;
|
||||||
|
private final String mHomeClubUuid;
|
||||||
|
private final String mHomeClubName;
|
||||||
|
private final String mChainUuid;
|
||||||
|
private final String mChainName;
|
||||||
|
private final String mTimezone;
|
||||||
|
private final Integer mTimezoneOffset;
|
||||||
|
private final Boolean mProfileCompleted;
|
||||||
|
private final String mMembershipType;
|
||||||
|
private final String mBarcode;
|
||||||
|
private final String mExternalAuthToken;
|
||||||
|
private final String mMeasurementUnit;
|
||||||
|
private final Boolean mHasMessages;
|
||||||
|
|
||||||
|
public String getUuid() {
|
||||||
|
return mUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFirstName() {
|
||||||
|
return mFirstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastName() {
|
||||||
|
return mLastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getVerified() {
|
||||||
|
return mVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHomeClubUuid() {
|
||||||
|
return mHomeClubUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHomeClubName() {
|
||||||
|
return mHomeClubName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getChainUuid() {
|
||||||
|
return mChainUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getChainName() {
|
||||||
|
return mChainName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTimezone() {
|
||||||
|
return mTimezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTimezoneOffset() {
|
||||||
|
return mTimezoneOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getProfileCompleted() {
|
||||||
|
return mProfileCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMembershipType() {
|
||||||
|
return mMembershipType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBarcode() {
|
||||||
|
return mBarcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExternalAuthToken() {
|
||||||
|
return mExternalAuthToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMeasurementUnit() {
|
||||||
|
return mMeasurementUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getHasMessages() {
|
||||||
|
return mHasMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Exerciser from a login response
|
||||||
|
*
|
||||||
|
* @param loginResponse {@link JSONObject} response from a login request
|
||||||
|
*/
|
||||||
|
public Exerciser(JSONObject loginResponse) {
|
||||||
|
mUuid = loginResponse.optString("uuid");
|
||||||
|
mFirstName = loginResponse.optString("firstName");
|
||||||
|
mLastName = loginResponse.optString("lastName");
|
||||||
|
mVerified = loginResponse.optBoolean("verified");
|
||||||
|
mHomeClubUuid = loginResponse.optString("homeClubUuid");
|
||||||
|
mHomeClubName = loginResponse.optString("homeClubName");
|
||||||
|
mChainUuid = loginResponse.optString("chainUuid");
|
||||||
|
mChainName = loginResponse.optString("chainName");
|
||||||
|
mTimezone = loginResponse.optString("timezone");
|
||||||
|
mTimezoneOffset = loginResponse.optInt("timezoneOffset");
|
||||||
|
mProfileCompleted = loginResponse.optBoolean("profileCompleted");
|
||||||
|
mMembershipType = loginResponse.optString("membershipType");
|
||||||
|
mBarcode = loginResponse.optString("barcode");
|
||||||
|
mExternalAuthToken = loginResponse.optString("externalAuthToken");
|
||||||
|
mMeasurementUnit = loginResponse.optString("measurementUnit");
|
||||||
|
mHasMessages = loginResponse.optBoolean("hasMessages");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal version of Exerciser that provides Uuid for REST calls
|
||||||
|
*
|
||||||
|
* @param uuid {@link String} Uuid of the Exerciser to be instantiated
|
||||||
|
*/
|
||||||
|
public Exerciser(String uuid) {
|
||||||
|
mUuid = uuid;
|
||||||
|
mFirstName = null;
|
||||||
|
mLastName = null;
|
||||||
|
mVerified = null;
|
||||||
|
mHomeClubUuid = null;
|
||||||
|
mHomeClubName = null;
|
||||||
|
mChainUuid = null;
|
||||||
|
mChainName = null;
|
||||||
|
mTimezone = null;
|
||||||
|
mTimezoneOffset = null;
|
||||||
|
mProfileCompleted = null;
|
||||||
|
mMembershipType = null;
|
||||||
|
mBarcode = null;
|
||||||
|
mExternalAuthToken = null;
|
||||||
|
mMeasurementUnit = null;
|
||||||
|
mHasMessages = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
package com.iamthefij.otbeta.api;
|
||||||
|
|
||||||
|
import android.location.Address;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full profile of an Exerciser or user
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class ExerciserProfile {
|
||||||
|
private final String mAboutMe;
|
||||||
|
private final Boolean mActive;
|
||||||
|
private Address mAddress;
|
||||||
|
private final String mBarcode;
|
||||||
|
private final String mBirthday;
|
||||||
|
private final String mClientLoginId;
|
||||||
|
private final String mCreatedAt;
|
||||||
|
private final String mEmail;
|
||||||
|
private EmailPreference mEmailPreference;
|
||||||
|
private final JSONObject mExternalServiceNames;
|
||||||
|
private final Boolean mFacebookAutoShare;
|
||||||
|
private final String mFirstName;
|
||||||
|
private final String mLastName;
|
||||||
|
private final String mGender;
|
||||||
|
private final Double mHeight;
|
||||||
|
private final String mHomeClubUuid;
|
||||||
|
private final String mMeasurementUnit;
|
||||||
|
private final String mNickname;
|
||||||
|
private final String mPasscode;
|
||||||
|
private final String mPhoneNumber;
|
||||||
|
private final String mPicturePassword;
|
||||||
|
private final String mPrivacy;
|
||||||
|
private final String mProfilePicture;
|
||||||
|
private final String mTimezone;
|
||||||
|
private final String mUpdatedAt;
|
||||||
|
private final String mUuid;
|
||||||
|
private final Boolean mVerified;
|
||||||
|
private final Double mWeight;
|
||||||
|
|
||||||
|
public ExerciserProfile(JSONObject response) {
|
||||||
|
mAboutMe = response.optString("aboutMe");
|
||||||
|
mActive = response.optBoolean("active");
|
||||||
|
mBarcode = response.optString("barcode");
|
||||||
|
mBirthday = response.optString("birthday");
|
||||||
|
mClientLoginId = response.optString("clientLoginId");
|
||||||
|
mCreatedAt = response.optString("createdAt");
|
||||||
|
mEmail = response.optString("email");
|
||||||
|
mExternalServiceNames = response.optJSONObject("externalServiceNames");
|
||||||
|
mFacebookAutoShare = response.optBoolean("facebookAutoShare");
|
||||||
|
mFirstName = response.optString("firstName");
|
||||||
|
mLastName = response.optString("lastName");
|
||||||
|
mGender = response.optString("gender");
|
||||||
|
mHomeClubUuid = response.optString("homeClubUuid");
|
||||||
|
mMeasurementUnit = response.optString("measurementUnit");
|
||||||
|
mNickname = response.optString("nickname");
|
||||||
|
mPasscode = response.optString("passcode");
|
||||||
|
mPhoneNumber = response.optString("phoneNumber");
|
||||||
|
mPicturePassword = response.optString("picturePassword");
|
||||||
|
mPrivacy = response.optString("privacy");
|
||||||
|
mProfilePicture = response.optString("profilePicture");
|
||||||
|
mTimezone = response.optString("timezone");
|
||||||
|
mUpdatedAt = response.optString("updatedAt");
|
||||||
|
mUuid = response.optString("uuid");
|
||||||
|
mVerified = response.optBoolean("verified");
|
||||||
|
mHeight = response.optDouble("height");
|
||||||
|
mWeight = response.optDouble("weight");
|
||||||
|
|
||||||
|
JSONObject address = response.optJSONObject("address");
|
||||||
|
if (address != null) {
|
||||||
|
mAddress = new Address(Locale.US);
|
||||||
|
mAddress.setAddressLine(1, address.optString("street1"));
|
||||||
|
mAddress.setAddressLine(2, address.optString("street2"));
|
||||||
|
mAddress.setLocality(address.optString("city"));
|
||||||
|
mAddress.setCountryCode(address.optString("country"));
|
||||||
|
mAddress.setPostalCode(address.optString("postalCode"));
|
||||||
|
mAddress.setAdminArea(address.optString("stateOrProvince"));
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject emailPreference = response.optJSONObject("emailPreference");
|
||||||
|
if (emailPreference != null) {
|
||||||
|
mEmailPreference = new EmailPreference(emailPreference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email preferences for a user
|
||||||
|
*/
|
||||||
|
public class EmailPreference {
|
||||||
|
private final Boolean mEmailApplause;
|
||||||
|
private final Boolean mEmailChallengeNotice;
|
||||||
|
private final Boolean mEmailComment;
|
||||||
|
private final Boolean mEmailGoalNotice;
|
||||||
|
private final Boolean mEmailMonthlyWorkout;
|
||||||
|
private final Boolean mEmailSingleWorkout;
|
||||||
|
private final Boolean mEmailSystemMessage;
|
||||||
|
private final Boolean mEmailTrainerMessage;
|
||||||
|
private final Boolean mEmailWeeklyWorkout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the preferences instance for a given user based on JSON response
|
||||||
|
* @param preference {@link JSONObject} to build preferences from
|
||||||
|
*/
|
||||||
|
public EmailPreference(JSONObject preference) {
|
||||||
|
mEmailApplause = preference.optBoolean("emailApplause");
|
||||||
|
mEmailChallengeNotice = preference.optBoolean("emailChallengeNotice");
|
||||||
|
mEmailComment = preference.optBoolean("emailComment");
|
||||||
|
mEmailGoalNotice = preference.optBoolean("emailGoalNotice");
|
||||||
|
mEmailMonthlyWorkout = preference.optBoolean("emailMonthlyWorkout");
|
||||||
|
mEmailSingleWorkout = preference.optBoolean("emailSingleWorkout");
|
||||||
|
mEmailSystemMessage = preference.optBoolean("emailSystemMessage");
|
||||||
|
mEmailTrainerMessage = preference.optBoolean("emailTrainerMessage");
|
||||||
|
mEmailWeeklyWorkout = preference.optBoolean("emailWeeklyWorkout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
package com.iamthefij.otbeta.api;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interval is a workout summary
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class Interval {
|
||||||
|
private static String sSpeedUnit;
|
||||||
|
private static String sDistanceUnit;
|
||||||
|
|
||||||
|
private Double mAvgHeartRate;
|
||||||
|
private Double mAvgResistance;
|
||||||
|
private Double mAvgSpeed;
|
||||||
|
private String mPointsLabel;
|
||||||
|
private String mStartDateLocal;
|
||||||
|
private Double mStartDateUtc;
|
||||||
|
private Integer mTotalCalories;
|
||||||
|
private Double mTotalDistance;
|
||||||
|
private Integer mTotalDuration;
|
||||||
|
private Integer mTotalPoints;
|
||||||
|
private Integer mTotalWorkout;
|
||||||
|
|
||||||
|
private String mDeviceType;
|
||||||
|
private String mEquipmentType;
|
||||||
|
private final Integer mId;
|
||||||
|
private Boolean mNoted;
|
||||||
|
private String mTimezone;
|
||||||
|
private String mWorkoutCategory;
|
||||||
|
private String mWorkoutSource;
|
||||||
|
|
||||||
|
public static String getSpeedUnit() {
|
||||||
|
return sSpeedUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setSpeedUnit(String sSpeedUnit) {
|
||||||
|
Interval.sSpeedUnit = sSpeedUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDistanceUnit() {
|
||||||
|
return sDistanceUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setDistanceUnit(String sDistanceUnit) {
|
||||||
|
Interval.sDistanceUnit = sDistanceUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getAvgHeartRate() {
|
||||||
|
return mAvgHeartRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getAvgResistance() {
|
||||||
|
return mAvgResistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getAvgSpeed() {
|
||||||
|
return mAvgSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPointsLabel() {
|
||||||
|
return mPointsLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStartDateLocal() {
|
||||||
|
return mStartDateLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getStartDateUtc() {
|
||||||
|
return mStartDateUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTotalCalories() {
|
||||||
|
return mTotalCalories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getTotalDistance() {
|
||||||
|
return mTotalDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTotalDuration() {
|
||||||
|
return mTotalDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTotalPoints() {
|
||||||
|
return mTotalPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTotalWorkout() {
|
||||||
|
return mTotalWorkout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDeviceType() {
|
||||||
|
return mDeviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEquipmentType() {
|
||||||
|
return mEquipmentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return mId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getNoted() {
|
||||||
|
return mNoted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTimezone() {
|
||||||
|
return mTimezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWorkoutCategory() {
|
||||||
|
return mWorkoutCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWorkoutSource() {
|
||||||
|
return mWorkoutSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal constructor for use in getting details
|
||||||
|
*
|
||||||
|
* @param intervalId int id of interval to be used
|
||||||
|
*/
|
||||||
|
public Interval(int intervalId) {
|
||||||
|
mId = intervalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an Interval with the response from any kind of workout list request
|
||||||
|
*
|
||||||
|
* @param interval {@link JSONObject} response from any interval request
|
||||||
|
*/
|
||||||
|
public Interval(JSONObject interval) {
|
||||||
|
mAvgHeartRate = interval.optDouble("avgHeartRate");
|
||||||
|
mAvgResistance = interval.optDouble("avgResistance");
|
||||||
|
mAvgSpeed = interval.optDouble("avgSpeed");
|
||||||
|
mPointsLabel = interval.optString("pointsLabel");
|
||||||
|
mStartDateLocal = interval.optString("startDateLocal");
|
||||||
|
mStartDateUtc = interval.optDouble("startDateUtc");
|
||||||
|
mTotalCalories = interval.optInt("totalCalories");
|
||||||
|
mTotalDistance = interval.optDouble("totalDistance");
|
||||||
|
mTotalDuration = interval.optInt("totalDuration");
|
||||||
|
mTotalPoints = interval.optInt("totalPoints");
|
||||||
|
mTotalWorkout = interval.optInt("totalWorkout");
|
||||||
|
|
||||||
|
mDeviceType = interval.optString("deviceType");
|
||||||
|
mEquipmentType = interval.optString("equipmentType");
|
||||||
|
mId = interval.optInt("id");
|
||||||
|
mNoted = interval.optBoolean("noted");
|
||||||
|
mTimezone = interval.optString("timezone");
|
||||||
|
mWorkoutCategory = interval.optString("workoutCategory");
|
||||||
|
mWorkoutSource = interval.optString("workoutSource");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
package com.iamthefij.otbeta.api;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a company location
|
||||||
|
*/
|
||||||
|
public class Location {
|
||||||
|
// Always present
|
||||||
|
private final Integer mId;
|
||||||
|
private final String mExternalMappingId;
|
||||||
|
private final String mFeatures;
|
||||||
|
private final String mMms;
|
||||||
|
private final String mName;
|
||||||
|
private final String mPhone;
|
||||||
|
private final String mStatus;
|
||||||
|
private final String mStatusTillDate;
|
||||||
|
private final String mUuid;
|
||||||
|
private String mTimezone;
|
||||||
|
// Details only
|
||||||
|
private Address mAddress;
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return mId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUuid() {
|
||||||
|
return mUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExternalMappingId() {
|
||||||
|
return mExternalMappingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTimezone() {
|
||||||
|
return mTimezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhone() {
|
||||||
|
return mPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFeatures() {
|
||||||
|
return mFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return mStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatusTillDate() {
|
||||||
|
return mStatusTillDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMms() {
|
||||||
|
return mMms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Address getAddress() {
|
||||||
|
return mAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAddress() {
|
||||||
|
return mAddress != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Location from a {@link JSONObject}
|
||||||
|
*/
|
||||||
|
public Location(JSONObject locationJSON) {
|
||||||
|
mId = locationJSON.optInt("id");
|
||||||
|
mUuid = locationJSON.optString("uuid");
|
||||||
|
mName = locationJSON.optString("name");
|
||||||
|
mExternalMappingId = locationJSON.optString("externalMappingId");
|
||||||
|
mTimezone = locationJSON.optString("timezone");
|
||||||
|
mPhone = locationJSON.optString("phone");
|
||||||
|
mFeatures = locationJSON.optString("features");
|
||||||
|
mStatus = locationJSON.optString("status");
|
||||||
|
mStatusTillDate = locationJSON.optString("statusTillDate");
|
||||||
|
mMms = locationJSON.optString("mms");
|
||||||
|
|
||||||
|
JSONObject address = locationJSON.optJSONObject("address");
|
||||||
|
if (address != null) {
|
||||||
|
mAddress = new Address();
|
||||||
|
mAddress.setAddressLine1(address.optString("addressLine1"));
|
||||||
|
mAddress.setAddressLine2(address.optString("addressLine2"));
|
||||||
|
mAddress.setLocality(address.optString("city"));
|
||||||
|
mAddress.setCountryCode(address.optString("country"));
|
||||||
|
mAddress.setPostalCode(address.optString("postalCode"));
|
||||||
|
mAddress.setAdminArea(address.optString("stateOrProvince"));
|
||||||
|
if (mTimezone == null) {
|
||||||
|
mTimezone = address.optString("timezone");
|
||||||
|
}
|
||||||
|
mAddress.setLatitude(address.optDouble("lat"));
|
||||||
|
mAddress.setLongitude(address.optDouble("lng"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Address {
|
||||||
|
String mAddressLine1;
|
||||||
|
String mAddressLine2;
|
||||||
|
String mLocality;
|
||||||
|
String mCountryCode;
|
||||||
|
String mPostalCode;
|
||||||
|
String mAdminArea;
|
||||||
|
Double mLatitude;
|
||||||
|
Double mLongitude;
|
||||||
|
|
||||||
|
public String getAddressLine1() {
|
||||||
|
return mAddressLine1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddressLine1(String addressLine) {
|
||||||
|
mAddressLine1 = addressLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddressLine2() {
|
||||||
|
return mAddressLine2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddressLine2(String addressLine) {
|
||||||
|
mAddressLine2 = addressLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocality() {
|
||||||
|
return mLocality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocality(String locality) {
|
||||||
|
mLocality = locality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCountryCode() {
|
||||||
|
return mCountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCountryCode(String countryCode) {
|
||||||
|
mCountryCode = countryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPostalCode() {
|
||||||
|
return mPostalCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPostalCode(String postalCode) {
|
||||||
|
mPostalCode = postalCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAdminArea() {
|
||||||
|
return mAdminArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdminArea(String adminArea) {
|
||||||
|
mAdminArea = adminArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getLatitude() {
|
||||||
|
return mLatitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasLatitude() {
|
||||||
|
return mLatitude != null && !mLatitude.isNaN();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLatitude(Double latitude) {
|
||||||
|
mLatitude = latitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getLongitude() {
|
||||||
|
return mLongitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasLongitude() {
|
||||||
|
return mLongitude != null && !mLongitude.isNaN();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLongitude(Double longitude) {
|
||||||
|
mLongitude = longitude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,234 @@
|
|||||||
|
package com.iamthefij.otbeta.api;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single workout with all metrics and details
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class Workout {
|
||||||
|
private HeartRateZones mHrm;
|
||||||
|
private final Integer mCalories;
|
||||||
|
private final Integer mDuration;
|
||||||
|
private final Integer mId;
|
||||||
|
private final Integer mPoints;
|
||||||
|
private final Integer mStartTimeUtc;
|
||||||
|
private Metric mDistance;
|
||||||
|
private Metric mHeartRate;
|
||||||
|
private Metric mResistance;
|
||||||
|
private Metric mSpeed;
|
||||||
|
private Metric mVertical;
|
||||||
|
private final String mDeviceType;
|
||||||
|
private final String mNotes;
|
||||||
|
private final String mPointsLabel;
|
||||||
|
private final String mSource;
|
||||||
|
private final String mSourceLogo;
|
||||||
|
private final String mStartTimeLocal;
|
||||||
|
private final String mTimezone;
|
||||||
|
|
||||||
|
public HeartRateZones getHrm() {
|
||||||
|
return mHrm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCalories() {
|
||||||
|
return mCalories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getDuration() {
|
||||||
|
return mDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return mId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPoints() {
|
||||||
|
return mPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getStartTimeUtc() {
|
||||||
|
return mStartTimeUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metric getDistance() {
|
||||||
|
return mDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metric getHeartRate() {
|
||||||
|
return mHeartRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metric getResistance() {
|
||||||
|
return mResistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metric getSpeed() {
|
||||||
|
return mSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metric getVertical() {
|
||||||
|
return mVertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDeviceType() {
|
||||||
|
return mDeviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNotes() {
|
||||||
|
return mNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPointsLabel() {
|
||||||
|
return mPointsLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSource() {
|
||||||
|
return mSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSourceLogo() {
|
||||||
|
return mSourceLogo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStartTimeLocal() {
|
||||||
|
return mStartTimeLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTimezone() {
|
||||||
|
return mTimezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Workout(JSONObject workout) {
|
||||||
|
mCalories = workout.optInt("calories");
|
||||||
|
mDuration = workout.optInt("duration");
|
||||||
|
mId = workout.optInt("id");
|
||||||
|
mPoints = workout.optInt("points");
|
||||||
|
mStartTimeUtc = workout.optInt("startTimeUtc");
|
||||||
|
mDeviceType = workout.optString("deviceType");
|
||||||
|
mNotes = workout.optString("notes");
|
||||||
|
mPointsLabel = workout.optString("pointsLabel");
|
||||||
|
mSource = workout.optString("source");
|
||||||
|
mSourceLogo = workout.optString("sourceLogo");
|
||||||
|
mStartTimeLocal = workout.optString("startTimeLocal");
|
||||||
|
mTimezone = workout.optString("timezone");
|
||||||
|
|
||||||
|
JSONObject distance = workout.optJSONObject("distance");
|
||||||
|
if (distance != null) {
|
||||||
|
mDistance = new Metric(distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject heartRate = workout.optJSONObject("heartRate");
|
||||||
|
if (heartRate != null) {
|
||||||
|
mHeartRate = new Metric(heartRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject resistance = workout.optJSONObject("resistance");
|
||||||
|
if (resistance != null) {
|
||||||
|
mResistance = new Metric(resistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject speed = workout.optJSONObject("speed");
|
||||||
|
if (speed != null) {
|
||||||
|
mSpeed = new Metric(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject vertical = workout.optJSONObject("vertical");
|
||||||
|
if (vertical != null) {
|
||||||
|
mVertical = new Metric(vertical);
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject hrm = workout.optJSONObject("hrm");
|
||||||
|
if (hrm != null) {
|
||||||
|
mHrm = new HeartRateZones(hrm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Metric {
|
||||||
|
private final Double mAverage;
|
||||||
|
private final Integer mInterval;
|
||||||
|
private final String mUnits;
|
||||||
|
private List<Double> mValues;
|
||||||
|
private final Double mValue;
|
||||||
|
|
||||||
|
public Double getAverage() {
|
||||||
|
return mAverage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getInterval() {
|
||||||
|
return mInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUnits() {
|
||||||
|
return mUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Double> getValues() {
|
||||||
|
return mValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getValue() {
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metric(JSONObject metric) {
|
||||||
|
mAverage = metric.optDouble("average");
|
||||||
|
mInterval = metric.optInt("interval");
|
||||||
|
mUnits = metric.optString("units");
|
||||||
|
mValue = metric.optDouble("value");
|
||||||
|
|
||||||
|
JSONArray values = metric.optJSONArray("values");
|
||||||
|
if (values != null) {
|
||||||
|
mValues = new ArrayList<>(values.length());
|
||||||
|
for (int i = 0; i < values.length(); i++) {
|
||||||
|
mValues.add(values.optDouble(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeartRateZones {
|
||||||
|
private final Integer mBlueZoneSeconds;
|
||||||
|
private final Integer mGreenZoneSeconds;
|
||||||
|
private final Integer mGreyZoneSeconds;
|
||||||
|
private final Integer mOrangeZoneSeconds;
|
||||||
|
private final Integer mRedZoneSeconds;
|
||||||
|
private final Integer mTargetHeartRate;
|
||||||
|
|
||||||
|
public Integer getBlueZoneSeconds() {
|
||||||
|
return mBlueZoneSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getGreenZoneSeconds() {
|
||||||
|
return mGreenZoneSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getGreyZoneSeconds() {
|
||||||
|
return mGreyZoneSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getOrangeZoneSeconds() {
|
||||||
|
return mOrangeZoneSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getRedZoneSeconds() {
|
||||||
|
return mRedZoneSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTargetHeartRate() {
|
||||||
|
return mTargetHeartRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HeartRateZones(JSONObject zones) {
|
||||||
|
mBlueZoneSeconds = zones.optInt("blueZoneSeconds");
|
||||||
|
mGreenZoneSeconds = zones.optInt("greenZoneSeconds");
|
||||||
|
mGreyZoneSeconds = zones.optInt("greyZoneSeconds");
|
||||||
|
mOrangeZoneSeconds = zones.optInt("orangeZoneSeconds");
|
||||||
|
mRedZoneSeconds = zones.optInt("redZoneSeconds");
|
||||||
|
mTargetHeartRate = zones.optInt("targetHeartRate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:divider="?android:attr/dividerHorizontal"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:showDividers="middle"
|
||||||
|
tools:context="com.iamthefij.otbeta.WorkoutListActivity">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This layout is a two-pane layout for the Workouts
|
||||||
|
master/detail flow.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/workout_list"
|
||||||
|
android:name="com.iamthefij.otbeta.WorkoutListFragment"
|
||||||
|
android:layout_width="@dimen/item_width"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layoutManager="LinearLayoutManager"
|
||||||
|
tools:context="com.iamthefij.otbeta.WorkoutListActivity"
|
||||||
|
tools:listitem="@layout/workout_list_content" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/workout_detail_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="3"
|
||||||
|
tools:ignore="InconsistentLayout"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
86
com.iamthefij.otbeta/src/main/res/layout/activity_login.xml
Normal file
86
com.iamthefij.otbeta/src/main/res/layout/activity_login.xml
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
|
tools:context="com.iamthefij.otbeta.LoginActivity">
|
||||||
|
|
||||||
|
<!-- Login progress -->
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/login_progress"
|
||||||
|
style="?android:attr/progressBarStyleLarge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/login_form"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/email_login_form"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/location" />
|
||||||
|
|
||||||
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<AutoCompleteTextView
|
||||||
|
android:id="@+id/email"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/prompt_email"
|
||||||
|
android:inputType="textEmailAddress"
|
||||||
|
android:maxLines="1" />
|
||||||
|
|
||||||
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/prompt_password"
|
||||||
|
android:imeActionId="@+id/login"
|
||||||
|
android:imeActionLabel="@string/action_sign_in_short"
|
||||||
|
android:imeOptions="actionUnspecified"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:maxLines="1" />
|
||||||
|
|
||||||
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/email_sign_in_button"
|
||||||
|
style="?android:textAppearanceSmall"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/action_sign_in"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</LinearLayout>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="com.iamthefij.otbeta.SplashActivity">
|
||||||
|
|
||||||
|
</android.support.constraint.ConstraintLayout>
|
@ -0,0 +1,43 @@
|
|||||||
|
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
tools:context="com.iamthefij.otbeta.WorkoutDetailActivity"
|
||||||
|
tools:ignore="MergeRootFrame">
|
||||||
|
|
||||||
|
<android.support.design.widget.AppBarLayout
|
||||||
|
android:id="@+id/app_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/app_bar_height"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||||
|
|
||||||
|
<android.support.design.widget.CollapsingToolbarLayout
|
||||||
|
android:id="@+id/toolbar_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
app:contentScrim="?attr/colorPrimary"
|
||||||
|
app:layout_scrollFlags="scroll|exitUntilCollapsed"
|
||||||
|
app:toolbarId="@+id/toolbar">
|
||||||
|
|
||||||
|
<android.support.v7.widget.Toolbar
|
||||||
|
android:id="@+id/detail_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:layout_collapseMode="pin"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
|
||||||
|
|
||||||
|
</android.support.design.widget.CollapsingToolbarLayout>
|
||||||
|
|
||||||
|
</android.support.design.widget.AppBarLayout>
|
||||||
|
|
||||||
|
<android.support.v4.widget.NestedScrollView
|
||||||
|
android:id="@+id/workout_detail_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
|
</android.support.design.widget.CoordinatorLayout>
|
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
tools:context="com.iamthefij.otbeta.WorkoutListActivity">
|
||||||
|
|
||||||
|
<android.support.design.widget.AppBarLayout
|
||||||
|
android:id="@+id/app_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:theme="@style/AppTheme.AppBarOverlay">
|
||||||
|
|
||||||
|
<android.support.v7.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:popupTheme="@style/AppTheme.PopupOverlay" />
|
||||||
|
|
||||||
|
</android.support.design.widget.AppBarLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/frameLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<include layout="@layout/workout_list" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</android.support.design.widget.CoordinatorLayout>
|
@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.v7.widget.CardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/text_margin_half"
|
||||||
|
android:layout_marginBottom="@dimen/text_margin_half"
|
||||||
|
android:layout_marginStart="@dimen/text_margin"
|
||||||
|
android:layout_marginEnd="@dimen/text_margin"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="@dimen/text_margin">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/workout_date"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_marginBottom="@dimen/text_margin_half"
|
||||||
|
android:text="March 10, 2017"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textAppearance="@android:style/TextAppearance.Material.Medium"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/workout_calories"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/workout_date"
|
||||||
|
android:layout_marginEnd="@dimen/text_margin_half"
|
||||||
|
android:text="600 Kc"
|
||||||
|
android:textAppearance="@android:style/TextAppearance.Material.Small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/workout_minutes"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/workout_date"
|
||||||
|
android:layout_marginEnd="@dimen/text_margin_half"
|
||||||
|
android:layout_toEndOf="@+id/workout_calories"
|
||||||
|
android:text="60 min"
|
||||||
|
android:textAppearance="@android:style/TextAppearance.Material.Small" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/workout_points"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/text_margin_half"
|
||||||
|
android:text="22 points"
|
||||||
|
android:layout_below="@+id/workout_date"
|
||||||
|
android:layout_toEndOf="@+id/workout_minutes"
|
||||||
|
android:textAppearance="@android:style/TextAppearance.Material.Small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</android.support.v7.widget.CardView>
|
133
com.iamthefij.otbeta/src/main/res/layout/workout_detail.xml
Normal file
133
com.iamthefij.otbeta/src/main/res/layout/workout_detail.xml
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<RelativeLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/workout_detail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="@dimen/text_margin_half"
|
||||||
|
>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/workout_hr_zone_graph_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:text="Time in Zones"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textAppearance="@android:style/TextAppearance.Material.Medium" />
|
||||||
|
|
||||||
|
<com.jjoe64.graphview.GraphView
|
||||||
|
android:id="@+id/workout_hr_zone_graph"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="200dip"
|
||||||
|
android:layout_below="@+id/workout_hr_zone_graph_title"
|
||||||
|
android:layout_marginBottom="@dimen/text_margin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/workout_detail_insert_wrapper"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/list_item_height"
|
||||||
|
android:layout_below="@id/workout_hr_zone_graph"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/workout_detail_insert_headers"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Splat Points"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Calories"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Avg. HR"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Duration"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/workout_detail_insert"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/workout_detail_insert_headers"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/workout_detail_splat_points"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="30"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/workout_detail_calories"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="600"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/workout_detail_avg_hr"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="120"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/workout_detail_duration"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="52"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
</LinearLayout>
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/workout_hrm_graph_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/workout_detail_insert_wrapper"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:text="Heart Rate"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textAppearance="@android:style/TextAppearance.Material.Medium" />
|
||||||
|
|
||||||
|
<com.jjoe64.graphview.GraphView
|
||||||
|
android:id="@+id/workout_hrm_graph"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="200dip"
|
||||||
|
android:layout_below="@+id/workout_hrm_graph_title"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
11
com.iamthefij.otbeta/src/main/res/layout/workout_list.xml
Normal file
11
com.iamthefij.otbeta/src/main/res/layout/workout_list.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/workout_list"
|
||||||
|
android:name="com.iamthefij.otbeta.WorkoutListFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layoutManager="LinearLayoutManager"
|
||||||
|
tools:context="com.iamthefij.otbeta.WorkoutListActivity"
|
||||||
|
tools:listitem="@layout/workout_list_content" />
|
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/id"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/text_margin"
|
||||||
|
android:textAppearance="?attr/textAppearanceListItem" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/text_margin"
|
||||||
|
android:textAppearance="?attr/textAppearanceListItem" />
|
||||||
|
</LinearLayout>
|
BIN
com.iamthefij.otbeta/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
com.iamthefij.otbeta/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
BIN
com.iamthefij.otbeta/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
com.iamthefij.otbeta/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
BIN
com.iamthefij.otbeta/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
com.iamthefij.otbeta/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
com.iamthefij.otbeta/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
com.iamthefij.otbeta/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
BIN
com.iamthefij.otbeta/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
com.iamthefij.otbeta/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
6
com.iamthefij.otbeta/src/main/res/values/colors.xml
Normal file
6
com.iamthefij.otbeta/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#f9a825</color>
|
||||||
|
<color name="colorPrimaryDark">#f57f17</color>
|
||||||
|
<color name="colorAccent">#42a5f5</color>
|
||||||
|
</resources>
|
10
com.iamthefij.otbeta/src/main/res/values/dimens.xml
Normal file
10
com.iamthefij.otbeta/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<resources>
|
||||||
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
|
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||||
|
<dimen name="app_bar_height">200dp</dimen>
|
||||||
|
<dimen name="item_width">200dp</dimen>
|
||||||
|
<dimen name="text_margin">16dp</dimen>
|
||||||
|
<dimen name="text_margin_half">8dp</dimen>
|
||||||
|
<dimen name="list_item_height">72dp</dimen>
|
||||||
|
</resources>
|
21
com.iamthefij.otbeta/src/main/res/values/strings.xml
Normal file
21
com.iamthefij.otbeta/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">OTBeta</string>
|
||||||
|
|
||||||
|
<!-- Strings related to login -->
|
||||||
|
<string name="prompt_email">Email</string>
|
||||||
|
<string name="prompt_password">Password (optional)</string>
|
||||||
|
<string name="action_sign_in">Sign in or register</string>
|
||||||
|
<string name="action_sign_in_short">Sign in</string>
|
||||||
|
<string name="error_invalid_email">This email address is invalid</string>
|
||||||
|
<string name="error_invalid_password">This password is too short</string>
|
||||||
|
<string name="error_incorrect_password">This password is incorrect</string>
|
||||||
|
<string name="error_field_required">This field is required</string>
|
||||||
|
<string name="contact_permission_rationale">"Contacts permissions are needed for providing email
|
||||||
|
completions."
|
||||||
|
</string>
|
||||||
|
<string name="location_permission_rationale">"Location permissions are needed for sorting the
|
||||||
|
list of studios."
|
||||||
|
</string>
|
||||||
|
<string name="title_workout_list">Workouts</string>
|
||||||
|
<string name="title_workout_detail">Workout Detail</string>
|
||||||
|
</resources>
|
20
com.iamthefij.otbeta/src/main/res/values/styles.xml
Normal file
20
com.iamthefij.otbeta/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||||
|
|
||||||
|
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
||||||
|
|
||||||
|
</resources>
|
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<full-backup-content>
|
||||||
|
<!-- Exclude specific shared preferences that contain GCM registration Id -->
|
||||||
|
</full-backup-content>
|
@ -0,0 +1,92 @@
|
|||||||
|
package com.iamthefij.otbeta;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.iamthefij.otbeta.api.Location;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple unit test for location sorting
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class LocationSortUnitTest {
|
||||||
|
private final double lat = 0.0;
|
||||||
|
private final double lng = 0.0;
|
||||||
|
private int locationId = 0;
|
||||||
|
|
||||||
|
private Location getLocation(String name, Double lat, Double lng) {
|
||||||
|
try {
|
||||||
|
JSONObject locationJSON = new JSONObject()
|
||||||
|
.put("id", locationId++)
|
||||||
|
.put("name", name);
|
||||||
|
if (lat != null || lng != null) {
|
||||||
|
JSONObject address = new JSONObject();
|
||||||
|
if (!lat.isNaN()) {
|
||||||
|
address.put("lat", lat);
|
||||||
|
}
|
||||||
|
if (!lng.isNaN()) {
|
||||||
|
address.put("lng", lng);
|
||||||
|
}
|
||||||
|
locationJSON.put("address", address);
|
||||||
|
}
|
||||||
|
return new Location(locationJSON);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location getLocationWithNaNAddress(String name) {
|
||||||
|
return getLocation(name, Double.NaN, Double.NaN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location getLocationWithNoAddress(String name) {
|
||||||
|
return getLocation(name, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location getLocationWithNearAddress(String name) {
|
||||||
|
return getLocation(name, lat, lng);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location getLocationWithFarAddress(String name) {
|
||||||
|
return getLocation(name, lat + 1.0, lng + 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginActivity.LocationDistanceComparator getComparator() {
|
||||||
|
return new LoginActivity.LocationDistanceComparator(lat, lng);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNaNAddress() throws Exception {
|
||||||
|
Location nan = getLocationWithNaNAddress("nan");
|
||||||
|
assertTrue("Is not NaN", nan.getAddress().getLatitude().isNaN());
|
||||||
|
|
||||||
|
assertFalse(nan.getAddress().getLatitude() == 0);
|
||||||
|
assertFalse(nan.getAddress().getLatitude() > 0);
|
||||||
|
assertFalse(nan.getAddress().getLatitude() < 0);
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
public void testLocationSort() throws Exception {
|
||||||
|
List<Location> locations = new ArrayList<>();
|
||||||
|
locations.add(getLocationWithNoAddress("no-adr"));
|
||||||
|
locations.add(getLocationWithNaNAddress("nan-adr"));
|
||||||
|
locations.add(getLocationWithNearAddress("near-adr"));
|
||||||
|
locations.add(getLocationWithFarAddress("far-adr"));
|
||||||
|
|
||||||
|
Collections.sort(locations, getComparator());
|
||||||
|
|
||||||
|
assertSame("near-adr", locations.get(0).getName());
|
||||||
|
assertSame("far-adr", locations.get(1).getName());
|
||||||
|
assertSame("nan-adr", locations.get(2).getName());
|
||||||
|
assertSame("no-adr", locations.get(3).getName());
|
||||||
|
}
|
||||||
|
}
|
18
gradle.properties
Normal file
18
gradle.properties
Normal file
@ -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=-Xmx3072m -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
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#Mon Feb 27 15:58:31 PST 2017
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
|
160
gradlew
vendored
Executable file
160
gradlew
vendored
Executable file
@ -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 "$@"
|
90
gradlew.bat
vendored
Normal file
90
gradlew.bat
vendored
Normal file
@ -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
|
1
settings.gradle
Normal file
1
settings.gradle
Normal file
@ -0,0 +1 @@
|
|||||||
|
include ':com.iamthefij.otbeta'
|
Loading…
x
Reference in New Issue
Block a user