Working code with everything squashed
continuous-integration/drone/push Build is failing Details

This commit is contained in:
IamTheFij 2019-12-19 17:21:38 -08:00
parent a138a06de1
commit b5396bab6f
59 changed files with 3670 additions and 1 deletions

41
.drone.yml Normal file
View 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
View File

@ -32,3 +32,11 @@ proguard/
# Android Studio captures folder
captures/
# Android Studio project files
*.iml
/.idea/workspace.xml
/.idea/libraries
# OSX things
.DS_Store
.tags

22
.idea/compiler.xml Normal file
View 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>

View File

@ -0,0 +1,3 @@
<component name="CopyrightManager">
<settings default="" />
</component>

View 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 Normal file
View 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 Normal file
View 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 Normal file
View 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 Normal file
View 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>

View 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 Normal file
View 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>

View File

@ -1,5 +1,5 @@
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:

View File

@ -1,2 +1,13 @@
# 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
View 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
View File

@ -0,0 +1 @@
/build

View 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
View 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 *;
#}

View File

@ -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);
}
}

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -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);
}
}

View File

@ -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: ");
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}
}

View File

@ -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");
}
}
}

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View 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" />

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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
View 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

Binary file not shown.

View 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
View 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
View 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
View File

@ -0,0 +1 @@
include ':com.iamthefij.otbeta'