├── babbage-2017.1.0-SNAPSHOT
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── app
├── src
│ └── main
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ ├── drawable
│ │ │ └── edit_text_background.xml
│ │ └── layout
│ │ │ ├── review_fragment.xml
│ │ │ ├── form_row.xml
│ │ │ ├── registration_email_activity.xml
│ │ │ ├── list_activity.xml
│ │ │ ├── multi_page_form_activity.xml
│ │ │ ├── login_activity.xml
│ │ │ ├── registration_security_question_activity.xml
│ │ │ ├── registration_security_answer_activity.xml
│ │ │ ├── registration_password_activity.xml
│ │ │ ├── image_capture_activity.xml
│ │ │ ├── one_page_form_activity.xml
│ │ │ └── field_fragment.xml
│ │ ├── java
│ │ └── com
│ │ │ └── sample
│ │ │ └── appconnectsample
│ │ │ ├── ReviewFragment.java
│ │ │ ├── FieldPager.java
│ │ │ ├── RegistrationActivity.java
│ │ │ ├── RegistrationEmailActivity.java
│ │ │ ├── RegistrationPasswordActivity.java
│ │ │ ├── App.java
│ │ │ ├── RegistrationSecurityQuestionActivity.java
│ │ │ ├── LoginActivity.java
│ │ │ ├── RegistrationSecurityAnswerActivity.java
│ │ │ ├── ImageCaptureActivity.java
│ │ │ ├── ListActivity.java
│ │ │ ├── MultiPageFormActivity.java
│ │ │ ├── OnePageFormActivity.java
│ │ │ └── FieldFragment.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── gradlew
└── README.md
/babbage-2017.1.0-SNAPSHOT/build.gradle:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdsol/appconnect-android/develop/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdsol/appconnect-android/develop/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdsol/appconnect-android/develop/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdsol/appconnect-android/develop/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdsol/appconnect-android/develop/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdsol/appconnect-android/develop/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Apr 18 11:52:08 BST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #063FA8
4 | #121F42
5 | #B8D323
6 | #FFF
7 | #EAEAEA
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/edit_text_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | doc/
15 |
16 | # Local configuration file (sdk path, etc)
17 | local.properties
18 |
19 | # Windows thumbnail db
20 | Thumbs.db
21 |
22 | # OSX files
23 | .DS_Store
24 |
25 | # Eclipse project files
26 | .classpath
27 | .project
28 |
29 | # Android Studio
30 | *.iml
31 | .idea
32 | .gradle
33 | build/
34 |
35 | # NDK
36 | obj/
37 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/review_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/steve/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/form_row.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/ReviewFragment.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.os.Bundle;
4 | import android.support.v4.app.Fragment;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 |
9 | /**
10 | * A fragment used by {@link MultiPageFormActivity} to show the user that the
11 | * form is all filled out. For this sample we don't do anything but you could
12 | * have, for example, a summary of the user's answers that they can review.
13 | */
14 | public class ReviewFragment extends Fragment {
15 |
16 | @Override
17 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
18 | return inflater.inflate(R.layout.review_fragment, container, false);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/FieldPager.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.content.Context;
4 | import android.support.v4.view.ViewPager;
5 | import android.util.AttributeSet;
6 | import android.view.MotionEvent;
7 |
8 | /**
9 | * A simple subclass of ViewPager that disables the ability for the user to
10 | * swipe between pages using touch. Used by {@link MultiPageFormActivity}.
11 | */
12 | public class FieldPager extends ViewPager {
13 |
14 | public FieldPager(Context context, AttributeSet attributes) {
15 | super(context, attributes);
16 | }
17 |
18 | @Override
19 | public boolean onInterceptTouchEvent(MotionEvent ev) {
20 | return false;
21 | }
22 |
23 | @Override
24 | public boolean onTouchEvent(MotionEvent ev) {
25 | return false;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/app/src/main/res/layout/registration_email_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
27 |
28 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/RegistrationActivity.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.content.Intent;
4 | import android.support.v7.app.AppCompatActivity;
5 | import com.mdsol.babbage.net.RequestException;
6 |
7 | import java.util.HashMap;
8 |
9 | /**
10 | * Created by kbohlmann on 7/21/16.
11 | * Simple base class for the dialogue sequence of registration activities.
12 | */
13 | public abstract class RegistrationActivity extends AppCompatActivity {
14 |
15 | public static final int REGISTRATION_REQUEST = 1;
16 |
17 | public static final HashMap errorMap = createErrorMap();
18 |
19 | @Override
20 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
21 | if (requestCode == REGISTRATION_REQUEST) {
22 | setResult(resultCode, data);
23 | finish();
24 | }
25 | }
26 |
27 | private static HashMap createErrorMap() {
28 | HashMap errorsMessages = new HashMap();
29 | errorsMessages.put(RequestException.ErrorCause.EMAIL_ALREADY_EXISTS, "The user already exists in the study");
30 | errorsMessages.put(RequestException.ErrorCause.INVALID_PASSWORD, "Your password does not meet our requirements");
31 | return errorsMessages;
32 | }
33 |
34 | public String getErrorMessageFromException(RequestException exception) {
35 | String errorMessage = errorMap.get(exception.getErrorCause());
36 | if (errorMessage == null)
37 | errorMessage = exception.getMessage();
38 |
39 | return errorMessage;
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/list_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
27 |
28 |
34 |
35 |
36 |
37 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | Properties properties = new Properties()
4 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
5 |
6 | android {
7 | compileSdkVersion 23
8 |
9 | defaultConfig {
10 | applicationId "com.sample.appconnectsample"
11 | minSdkVersion 16
12 | targetSdkVersion 23
13 | versionCode 1
14 | versionName "1.0"
15 | buildConfigField "String", "DEFAULT_USERNAME", properties.getProperty('default.username', '""')
16 | buildConfigField "String", "DEFAULT_PASSWORD", properties.getProperty('default.password', '""')
17 | buildConfigField "boolean", "VALIDATION", properties.getProperty('default.isValidation', "false")
18 | }
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | }
26 |
27 | repositories {
28 | flatDir {
29 | dirs '/path/to/local/aar'
30 | }
31 | def artifactory_user = properties.getProperty('artifactory.username')
32 | def artifactory_pass = properties.getProperty('artifactory.password')
33 | def artifactory_server = properties.getProperty('artifactory.server')
34 |
35 | maven {
36 | credentials {
37 | username "${artifactory_user}"
38 | password "${artifactory_pass}"
39 | }
40 |
41 | url "https://${artifactory_server}/mdsol/p-cloud-release"
42 | }
43 | }
44 |
45 | dependencies {
46 | implementation fileTree(dir: 'libs', include: ['*.jar'])
47 | testImplementation 'junit:junit:4.12'
48 | implementation 'com.android.support:appcompat-v7:23.1.1'
49 | implementation 'com.amazonaws:aws-android-sdk-s3:2.2.13'
50 |
51 | // *** AppConnect ***
52 | implementation 'com.mdsol:babbage:2018.2.0.18@aar'
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/multi_page_form_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
22 |
23 |
30 |
31 |
38 |
39 |
40 |
41 |
46 |
47 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/login_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
27 |
28 |
33 |
34 |
42 |
43 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/registration_security_question_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
22 |
28 |
29 |
35 |
36 |
37 |
38 |
47 |
48 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/RegistrationEmailActivity.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.text.Editable;
6 | import android.text.TextWatcher;
7 | import android.view.View;
8 | import android.widget.Button;
9 | import android.widget.EditText;
10 |
11 | /**
12 | * The activity where the user can log in with a username and password.
13 | */
14 | public class RegistrationEmailActivity extends RegistrationActivity {
15 |
16 | private EditText emailField;
17 | private EditText emailConfirmationField;
18 | private Button submitButton;
19 |
20 | @Override
21 | protected void onCreate(Bundle savedInstanceState) {
22 | super.onCreate(savedInstanceState);
23 | setContentView(R.layout.registration_email_activity);
24 |
25 | emailField = (EditText)findViewById(R.id.registration_email_field);
26 | emailConfirmationField = (EditText)findViewById(R.id.registration_email_confirmation_field);
27 | submitButton = (Button)findViewById(R.id.registration_email_submit_button);
28 |
29 | validate();
30 |
31 | TextWatcher validationListener = new TextWatcher() {
32 | @Override
33 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
34 | }
35 |
36 | @Override
37 | public void onTextChanged(CharSequence s, int start, int before, int count) {
38 | }
39 |
40 | @Override
41 | public void afterTextChanged(Editable s) {
42 | validate();
43 | }
44 | };
45 |
46 | emailField.addTextChangedListener(validationListener);
47 | emailConfirmationField.addTextChangedListener(validationListener);
48 | }
49 |
50 | public void onSubmitButton(View source) {
51 | Intent intent = new Intent(RegistrationEmailActivity.this, RegistrationPasswordActivity.class);
52 | intent.putExtra("email", emailField.getText().toString());
53 | startActivityForResult(intent, REGISTRATION_REQUEST);
54 | }
55 |
56 | private void validate() {
57 | submitButton.setEnabled(false);
58 |
59 | String email = emailField.getText().toString();
60 | String emailConfirmation = emailConfirmationField.getText().toString();
61 |
62 | if (email.equals(emailConfirmation) && android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches())
63 | submitButton.setEnabled(true);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/registration_security_answer_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
22 |
28 |
29 |
35 |
36 |
37 |
38 |
44 |
45 |
54 |
55 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/RegistrationPasswordActivity.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.text.Editable;
6 | import android.text.TextWatcher;
7 | import android.view.View;
8 | import android.widget.Button;
9 | import android.widget.EditText;
10 |
11 | import java.util.regex.Pattern;
12 |
13 | /**
14 | * The activity where the user can log in with a username and password.
15 | */
16 | public class RegistrationPasswordActivity extends RegistrationActivity {
17 |
18 | private EditText passwordField;
19 | private EditText passwordConfirmationField;
20 | private Button submitButton;
21 |
22 | final String PASSWORD_PATTERN = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=\\S+$).{8,}$";
23 |
24 | @Override
25 | protected void onCreate(Bundle savedInstanceState) {
26 | super.onCreate(savedInstanceState);
27 | setContentView(R.layout.registration_password_activity);
28 |
29 | passwordField = (EditText)findViewById(R.id.registration_password_field);
30 | passwordConfirmationField = (EditText)findViewById(R.id.registration_password_confirmation_field);
31 | submitButton = (Button)findViewById(R.id.registration_submit_password_button);
32 |
33 | validate();
34 |
35 | TextWatcher validationListener = new TextWatcher() {
36 | @Override
37 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
38 | }
39 |
40 | @Override
41 | public void onTextChanged(CharSequence s, int start, int before, int count) {
42 | }
43 |
44 | @Override
45 | public void afterTextChanged(Editable s) {
46 | validate();
47 | }
48 | };
49 |
50 | passwordField.addTextChangedListener(validationListener);
51 | passwordConfirmationField.addTextChangedListener(validationListener);
52 | }
53 |
54 | public void onSubmitButton(View source) {
55 | Intent intent = new Intent(RegistrationPasswordActivity.this, RegistrationSecurityQuestionActivity.class);
56 | intent.putExtra("email", getIntent().getStringExtra("email"));
57 | intent.putExtra("password", passwordField.getText().toString());
58 | startActivityForResult(intent, REGISTRATION_REQUEST);
59 | }
60 |
61 | private void validate() {
62 | submitButton.setEnabled(false);
63 |
64 | String password = passwordField.getText().toString();
65 | String passwordConfirmation = passwordConfirmationField.getText().toString();
66 |
67 | if (Pattern.compile(PASSWORD_PATTERN).matcher(password).matches() && password.equals(passwordConfirmation))
68 | submitButton.setEnabled(true);
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/App.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.app.Activity;
4 | import android.app.Application;
5 | import com.mdsol.babbage.Babbage;
6 | import com.mdsol.babbage.model.Datastore;
7 | import com.mdsol.babbage.model.DatastoreFactory;
8 | import com.mdsol.babbage.net.Client;
9 | import com.mdsol.babbage.net.ClientFactory;
10 | import java.io.File;
11 |
12 | /**
13 | * The Application subclass that manages things needed by all activities.
14 | */
15 | public class App extends Application {
16 |
17 | private Client client;
18 | private Datastore UIDatastore;
19 |
20 | @Override
21 | public void onCreate() {
22 | super.onCreate();
23 |
24 | // *** AppConnect ***
25 | // Start AppConnect. This must be done as early as possible during the
26 | // lifetime of the app, so doing it in an Application subclass is ideal.
27 | // The passed directory is used to store the database. The key is used
28 | // to encrypt sensitive information in the database. It must be 32-bytes
29 | // long and must be the same between runs. It really should be stored
30 | // somewhere safe, and not created inline like here.
31 | File dir = getFilesDir();
32 | byte[] key = new byte[] {
33 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2};
34 | Babbage.start(this, Client.Environment.PRODUCTION, "API Token", dir, dir, key, null);
35 |
36 | // It is possible to communicate with other environments in the Medidata
37 | // Platform (although in most cases Production should be used).
38 | if (BuildConfig.VALIDATION) {
39 | Client.setEnvironment(Client.Environment.VALIDATION);
40 | }
41 |
42 | // *** AppConnect ***
43 | // The client that will be used to make requests to the backend can be
44 | // created once and reused as needed throughout the app
45 | client = ClientFactory.getInstance().getClient(Client.Type.HYBRID);
46 |
47 |
48 |
49 | // *** AppConnect ***
50 | // In order for object states to be consistent between views, all UI
51 | // code must get objects from the same datastore, so it's a good idea to
52 | // create it once and make it available to the rest of the app
53 | UIDatastore = DatastoreFactory.create();
54 | }
55 |
56 | @Override
57 | public void onTerminate() {
58 | super.onTerminate();
59 | UIDatastore.dispose();
60 | }
61 |
62 | public static Client getClient(Activity activity) {
63 | return ((App)activity.getApplication()).client;
64 | }
65 |
66 | public static Datastore getUIDatastore(Activity activity) {
67 | return ((App)activity.getApplication()).UIDatastore;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/registration_password_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
27 |
28 |
37 |
38 |
46 |
47 |
56 |
57 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/image_capture_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
30 |
31 |
38 |
39 |
46 |
47 |
54 |
55 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/one_page_form_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
18 |
19 |
27 |
28 |
34 |
35 |
43 |
44 |
50 |
51 |
59 |
60 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/RegistrationSecurityQuestionActivity.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.app.AlertDialog;
4 | import android.content.Intent;
5 | import android.os.AsyncTask;
6 | import android.os.Bundle;
7 | import android.util.Log;
8 | import android.view.View;
9 | import android.widget.AdapterView;
10 | import android.widget.ArrayAdapter;
11 | import android.widget.ListView;
12 | import android.widget.TextView;
13 |
14 | import com.mdsol.babbage.net.Client;
15 | import com.mdsol.babbage.net.RequestException;
16 |
17 | import java.util.HashMap;
18 | import java.util.Map;
19 |
20 | /**
21 | * The activity where the user can log in with a username and password.
22 | */
23 | public class RegistrationSecurityQuestionActivity extends RegistrationActivity {
24 |
25 | private ListView securityQuestionListView;
26 | private HashMap securityQuestionsById;
27 | private View progressBar;
28 | private TextView questionPromptView;
29 | private String[] questionsArray;
30 |
31 | private static final String TAG = "RegSecQuestionActivity";
32 |
33 | @Override
34 | protected void onCreate(Bundle savedInstanceState) {
35 | super.onCreate(savedInstanceState);
36 | setContentView(R.layout.registration_security_question_activity);
37 |
38 | progressBar = findViewById(R.id.questions_progress_bar);
39 | progressBar.setVisibility(View.GONE);
40 |
41 | questionPromptView = (TextView)findViewById(R.id.registration_security_question_prompt_view);
42 | questionPromptView.setVisibility(View.INVISIBLE);
43 |
44 | securityQuestionListView = (ListView)findViewById(R.id.registration_security_question_list_view);
45 |
46 | securityQuestionListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
47 | @Override
48 | public void onItemClick(AdapterView> parent, View view, int position, long id) {
49 | // find the item's id in the hash map of security questions.
50 | for (Map.Entry questionEntry : securityQuestionsById.entrySet()) {
51 | if (questionsArray[position].equals(questionEntry.getValue())) {
52 | onSecurityQuestionSelected(questionEntry.getKey(), questionEntry.getValue());
53 | return;
54 | }
55 | }
56 | }
57 | });
58 |
59 | new LoadSecurityQuestionsTask().execute();
60 | }
61 |
62 | public void onSecurityQuestionSelected(int questionId, String question) {
63 | Intent intent = new Intent(RegistrationSecurityQuestionActivity.this, RegistrationSecurityAnswerActivity.class);
64 | intent.putExtra("email", getIntent().getStringExtra("email"));
65 | intent.putExtra("password", getIntent().getStringExtra("password"));
66 | intent.putExtra("securityQuestionId", questionId);
67 | intent.putExtra("securityQuestion", question);
68 | startActivityForResult(intent, REGISTRATION_REQUEST);
69 | }
70 |
71 | public void populateList() {
72 | ArrayAdapter arrayAdapter = new ArrayAdapter<>(
73 | this, android.R.layout.simple_list_item_1, questionsArray);
74 | securityQuestionListView.setAdapter(arrayAdapter);
75 | }
76 |
77 | /**
78 | * An asynchronous task that registers a new user.
79 | */
80 | private class LoadSecurityQuestionsTask extends AsyncTask {
81 |
82 | private RequestException exception;
83 |
84 | @Override
85 | protected void onPreExecute() {
86 | progressBar.setVisibility(View.VISIBLE);
87 | }
88 |
89 | @Override
90 | protected Void doInBackground(String... params) {
91 | // *** AppConnect ***
92 | // Request the subject registration security questions and their ids.
93 | try {
94 | Client client = App.getClient(RegistrationSecurityQuestionActivity.this);
95 |
96 | securityQuestionsById = client.loadSecurityQuestions();
97 | questionsArray = new String[securityQuestionsById.size()];
98 | questionsArray = securityQuestionsById.values().toArray(questionsArray);
99 | }
100 | catch (RequestException ex) {
101 | exception = ex;
102 | }
103 |
104 | return null;
105 | }
106 |
107 | @Override
108 | protected void onPostExecute(Void aVoid) {
109 | progressBar.setVisibility(View.GONE);
110 |
111 | if (exception != null) {
112 | Log.e(TAG, "Failed to load security questions.", exception);
113 | new AlertDialog.Builder(RegistrationSecurityQuestionActivity.this).
114 | setTitle(R.string.registration_failed_title).
115 | setMessage(exception.getMessage()).
116 | setPositiveButton(R.string.ok_button, null).
117 | show();
118 | return;
119 | }
120 |
121 | questionPromptView.setVisibility(View.VISIBLE);
122 |
123 | populateList();
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/LoginActivity.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.app.AlertDialog;
4 | import android.content.Intent;
5 | import android.os.AsyncTask;
6 | import android.os.Bundle;
7 | import android.support.v7.app.AppCompatActivity;
8 | import android.util.Log;
9 | import android.view.View;
10 | import android.widget.Button;
11 | import android.widget.EditText;
12 | import com.mdsol.babbage.model.Datastore;
13 | import com.mdsol.babbage.model.DatastoreFactory;
14 | import com.mdsol.babbage.model.User;
15 | import com.mdsol.babbage.net.Client;
16 | import com.mdsol.babbage.net.RequestException;
17 |
18 | /**
19 | * The activity where the user can log in with a username and password.
20 | */
21 | public class LoginActivity extends AppCompatActivity {
22 |
23 | private static final String TAG = "LoginActivity";
24 |
25 | private EditText usernameField;
26 | private EditText passwordField;
27 | private Button logInButton;
28 |
29 | @Override
30 | protected void onCreate(Bundle savedInstanceState) {
31 | super.onCreate(savedInstanceState);
32 | setContentView(R.layout.login_activity);
33 |
34 | usernameField = (EditText)findViewById(R.id.login_username_field);
35 | passwordField = (EditText)findViewById(R.id.login_password_field);
36 | logInButton = (Button)findViewById(R.id.login_log_in_button);
37 |
38 | usernameField.setText(BuildConfig.DEFAULT_USERNAME);
39 | passwordField.setText(BuildConfig.DEFAULT_PASSWORD);
40 | }
41 |
42 | public void doLogInButton(View source) {
43 | String username = usernameField.getText().toString();
44 | String password = passwordField.getText().toString();
45 | new LogInTask().execute(username, password);
46 | }
47 |
48 | public void doRegisterButton(View source) {
49 | Intent intent = new Intent(LoginActivity.this, RegistrationEmailActivity.class);
50 | startActivityForResult(intent, RegistrationEmailActivity.REGISTRATION_REQUEST, null);
51 | }
52 |
53 | @Override
54 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
55 | if (resultCode == RESULT_OK && requestCode == RegistrationEmailActivity.REGISTRATION_REQUEST) {
56 | // The user registered successfully, set the email field, and clear password
57 | usernameField.setText(data.getStringExtra("email"));
58 | passwordField.setText("");
59 |
60 | passwordField.requestFocus();
61 |
62 | new AlertDialog.Builder(LoginActivity.this).
63 | setTitle(R.string.registration_succeeded_title).
64 | setMessage(R.string.registration_succeeded_message).
65 | setPositiveButton(R.string.ok_button, null).
66 | show();
67 | }
68 | }
69 |
70 | /**
71 | * An asynchronous task that logs in the user.
72 | */
73 | private class LogInTask extends AsyncTask {
74 |
75 | private long userID;
76 | private RequestException exception;
77 |
78 | @Override
79 | protected void onPreExecute() {
80 | logInButton.setEnabled(false);
81 | }
82 |
83 | @Override
84 | protected Void doInBackground(String... params) {
85 | String username = params[0];
86 | String password = params[1];
87 |
88 | // *** AppConnect ***
89 | // Each secondary thread must create its own datastore instance and
90 | // dispose of it when done
91 | Datastore datastore = null;
92 | try {
93 | datastore = DatastoreFactory.create();
94 | Client client = App.getClient(LoginActivity.this);
95 | User user = client.logIn(datastore, username, password);
96 |
97 | // Babbage objects can't be shared between threads so you must pass
98 | // them around by ID instead and the receiving code can get its own
99 | // copy from its own datastore
100 | userID = user.getID();
101 | }
102 | catch (RequestException ex) {
103 | exception = ex;
104 | }
105 | finally {
106 | if (datastore != null)
107 | datastore.dispose();
108 | }
109 | return null;
110 | }
111 |
112 | @Override
113 | protected void onPostExecute(Void aVoid) {
114 | logInButton.setEnabled(true);
115 |
116 | if (exception != null) {
117 | Log.e(TAG, "The log in task failed", exception);
118 | int message = R.string.login_failed_message;
119 |
120 | if (exception.getErrorCause() == RequestException.ErrorCause.USER_NOT_ASSOCIATED_WITH_TOKEN)
121 | message = R.string.login_user_not_associated_message;
122 |
123 | new AlertDialog.Builder(LoginActivity.this).
124 | setTitle(R.string.login_failed_title).
125 | setMessage(message).
126 | setPositiveButton(R.string.ok_button, null).
127 | show();
128 | }
129 | else {
130 | // Start the ListActivity to show the forms available for the
131 | // user who just logged in
132 | Intent intent = new Intent(LoginActivity.this, ListActivity.class);
133 | intent.putExtra(ListActivity.USER_ID_EXTRA, userID);
134 | startActivity(intent);
135 | }
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AppConnectSample
3 |
4 | OK
5 |
6 | Max length %1$s
7 | Choice %1$s: %2$s
8 | One of %1$s
9 | A value in the range %1$s–%2$s
10 |
11 | Field OID
12 | Type
13 | Header
14 | Number
15 | Label
16 | Format
17 | Problem
18 |
19 | Syncing…
20 |
21 | Previous
22 | Next
23 | Submit
24 | Step Back Failed
25 | You can\'t step back if there is no previous field.
26 | Step Forward Failed
27 | You can\'t step forward if the current response is invalid or if there is no next field.
28 | The response for field %1$s is invalid. Please review and try again.
29 | This form requires a signature, which is not supported by this sample app.
30 |
31 | Username
32 | Password
33 | Log In
34 | Sign Up
35 |
36 | Submit
37 | Sending Form…
38 | Form Error
39 | A field in the form is of an unexpected type.
40 | The \"%1$s\" field is not the correct format.
41 | There are more fields that need to be filled out in this form.
42 | This form requires a signature, which is not supported by this sample app.
43 |
44 | The form is ready to submit.
45 |
46 | Login Failed
47 | Please verify your credentials and try again.
48 | User is not associated with provided API token
49 |
50 | Submit Failed
51 | An error occurred while sending the form responses.
52 | Submit Succeeded
53 | The form responses were sent successfully!
54 |
55 | Load Failed
56 | An error occurred while loading the subjects and forms.
57 |
58 | Submit
59 | Registration Failed
60 |
61 | Email
62 | Email confirmation
63 |
64 | Enter and confirm a new password:
65 | Password
66 | Password confirmation
67 | Passwords must meet the below criteria:
68 | - At least 8 characters long\n - At least one upper-case letter\n - At least one lower-case letter\n - At least one numberic digit
69 |
70 | Select a security question:
71 | Loading security questions...
72 |
73 | Answer
74 | Create Account
75 | Registering subject...
76 | Subject registration successful
77 | Please enter your password to login.
78 |
79 | Save an image to AppConnect
80 | Submit
81 | Take Photo
82 | Select Image
83 | Image Capture Error
84 | Image Capture Failed
85 | Image Capture Succeeded
86 | Image successfully saved to AppConnect!
87 | Saving image to AppConnect...
88 |
89 |
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/RegistrationSecurityAnswerActivity.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.app.AlertDialog;
4 | import android.content.Intent;
5 | import android.os.AsyncTask;
6 | import android.os.Bundle;
7 | import android.text.Editable;
8 | import android.text.TextWatcher;
9 | import android.util.Log;
10 | import android.view.View;
11 | import android.widget.Button;
12 | import android.widget.EditText;
13 | import android.widget.TextView;
14 |
15 | import com.mdsol.babbage.net.Client;
16 | import com.mdsol.babbage.net.RequestException;
17 |
18 | /**
19 | * The activity where the user can log in with a username and password.
20 | */
21 | public class RegistrationSecurityAnswerActivity extends RegistrationActivity {
22 |
23 | private static final String TAG = "RegSecAnsActivity";
24 |
25 | private Button createAccountButton;
26 | private EditText securityAnswerField;
27 | private TextView securityQuestionView;
28 | private View progressBar;
29 |
30 | String emailToRegister;
31 | String passwordToRegister;
32 | int securityQuestionIdToRegister;
33 | String securityAnswerToRegister;
34 |
35 | @Override
36 | protected void onCreate(Bundle savedInstanceState) {
37 | super.onCreate(savedInstanceState);
38 | setContentView(R.layout.registration_security_answer_activity);
39 |
40 | securityAnswerField = (EditText)findViewById(R.id.registration_security_answer_field);
41 | securityQuestionView = (TextView)findViewById(R.id.registration_security_answer_prompt_view);
42 | createAccountButton = (Button)findViewById(R.id.registration_create_account_button);
43 |
44 | securityQuestionView.setText(getIntent().getStringExtra("securityQuestion"));
45 |
46 | progressBar = findViewById(R.id.registration_progress_bar);
47 | progressBar.setVisibility(View.GONE);
48 |
49 | validate();
50 |
51 | securityAnswerField.addTextChangedListener(new TextWatcher() {
52 | @Override
53 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
54 |
55 | @Override
56 | public void onTextChanged(CharSequence s, int start, int before, int count) {}
57 |
58 | @Override
59 | public void afterTextChanged(Editable s) {
60 | validate();
61 | }
62 | });
63 | }
64 |
65 | private void validate() {
66 | createAccountButton.setEnabled(false);
67 |
68 | // security answer must be at least 2 characters long.
69 | if (securityAnswerField.getText().toString().length() >= 2) {
70 | createAccountButton.setEnabled(true);
71 | }
72 | }
73 |
74 | public void onCreateAccountButton(View source) {
75 | emailToRegister = getIntent().getStringExtra("email");
76 | passwordToRegister = getIntent().getStringExtra("password");
77 | securityAnswerToRegister = securityAnswerField.getText().toString();
78 | securityQuestionIdToRegister = getIntent().getIntExtra("securityQuestionId", 0);
79 |
80 | new RegisterTask().execute();
81 | }
82 |
83 | /**
84 | * An asynchronous task that registers a new user.
85 | */
86 | private class RegisterTask extends AsyncTask {
87 |
88 | private RequestException exception;
89 |
90 | @Override
91 | protected void onPreExecute() {
92 | createAccountButton.setEnabled(false);
93 | progressBar.setVisibility(View.VISIBLE);
94 | }
95 |
96 | @Override
97 | protected Void doInBackground(String... params) {
98 | // *** AppConnect ***
99 | // Client call to register the subject. No exception thrown implies success.
100 | try {
101 | Client client = App.getClient(RegistrationSecurityAnswerActivity.this);
102 | client.registerSubject(emailToRegister, passwordToRegister, securityQuestionIdToRegister, securityAnswerToRegister);
103 | }
104 | catch (RequestException ex) {
105 | exception = ex;
106 | }
107 | return null;
108 | }
109 |
110 | @Override
111 | protected void onPostExecute(Void aVoid) {
112 | progressBar.setVisibility(View.GONE);
113 |
114 | if (exception != null) {
115 | createAccountButton.setEnabled(true);
116 | Log.e(TAG, "The registration task failed", exception);
117 | String errorMessage = "Unable to Register User";
118 |
119 | switch(exception.getErrorCause()) {
120 | case INVALID_REGISTRATION_TOKEN:
121 | errorMessage = "Invalid Registration Token";
122 | break;
123 | case EMAIL_ALREADY_EXISTS:
124 | errorMessage = "User Already Exists";
125 | break;
126 | case AUTHENTICATION_FAILURE:
127 | errorMessage = "iMedidata Authentication Error";
128 | break;
129 | default:
130 | break;
131 | }
132 |
133 | new AlertDialog.Builder(RegistrationSecurityAnswerActivity.this).
134 | setTitle(R.string.registration_failed_title).
135 | setMessage(errorMessage).
136 | setPositiveButton(R.string.ok_button, null).
137 | show();
138 | return;
139 | }
140 |
141 | // registration succeeded, will now unwind
142 | // the stack of registration activities back to the login screen,
143 | // and populate the login email field.
144 | Intent returnIntent = new Intent();
145 | returnIntent.putExtra("email", emailToRegister);
146 |
147 | setResult(RESULT_OK, returnIntent);
148 | finish();
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AppConnectSample for Android #
2 |
3 | ## Introduction ##
4 |
5 | This is a sample project for Android that demonstrates how to build a complete application using the AppConnect SDK.
6 |
7 | In particular, this project shows how to:
8 |
9 | 1. Add the Babbage library dependency.
10 | 2. Start Babbage when the application launches.
11 | 3. Log in a user.
12 | 4. Sync the subjects and forms.
13 | 5. Fill out and submit a form, either with:
14 | * All fields on one page, where you use the StepSequencer only at the end.
15 | * One field per page, where you use the StepSequencer to navigate from field to field.
16 |
17 | >See [Medidata AppConnect Home](https://learn.mdsol.com/api/appconnect/rave-appconnect-sdks-95295808.html) for more information.
18 |
19 | ## Prerequisites
20 |
21 | If you are running this application, it is assumed that:
22 |
23 | - You were provided Artifactory credentials by a Medidata representative.
24 | - You have a valid Rave installation with Patient Cloud functionality enabled.
25 |
26 | >You also need permission to access Rave studies and sites. If you do not have these permissions, contact your Medidata representative for more information.
27 |
28 | ## Building ##
29 |
30 | To get this app up and running:
31 |
32 | 1. Launch Android Studio and use the "Import project" option.
33 | 2. Add the following lines to your local.properties file, using the provided Artifactory credentials:
34 |
35 | ```gradle
36 | artifactory.username=yourusername
37 | artifactory.password=yourpassword
38 | artifactory.server=mdsol.jfrog.io
39 | ```
40 | 3. Build and run.
41 |
42 | ## Using the Sample CRF ##
43 |
44 | The sample app can be used to log in with any user and fill out and submit any form using the multi-page UI, with the fields presented sequentially, similarly to how Medidata Patient Cloud does it.
45 |
46 | This SDK also comes with a sample CRF that can be used to exercise the one-page UI, where all the fields are presented on one screen. To use it, follow these steps:
47 |
48 | 1. Import the accompanied CRF into Rave for a subject of your choosing.
49 | 2. Log in with the sample app using the credentials of the subject you chose.
50 | 3. You should see two forms, Form 1 and Form 2. The former is hardcoded to open as a one-page form. The latter will open as a multi-page form.
51 |
52 | # Using the API in your own application #
53 |
54 | This is a guide to the basics of Babbage - intialization, making network requests, and loading data from the datastore.
55 |
56 | ## Installation using Artifactory
57 | To install Babbage, include it in the `build.gradle` file, using the credentials provided to you. When you next sync your gradle project, the library will be downloaded and installed.
58 |
59 | ```groovy
60 | repositories {
61 | maven {
62 | credentials {
63 | username "myusername"
64 | password "mypassword"
65 | }
66 |
67 | url "https://${artifactory_server}/artifactory/p-cloud-release"
68 | }
69 | }
70 | ```
71 |
72 | An example of how to load these credentials from the `local.properties` file can be found in the sample app's [build.gradle](https://github.com/mdsol/appconnect-android/blob/develop/app/build.gradle) file.
73 |
74 | ## Installation using local files
75 |
76 | If the Babbage library was provided to you in a zip file, update your `build.gradle`, changing the path to point to the file on your local filesystem:
77 |
78 | ```groovy
79 | repositories {
80 | flatDir {
81 | dirs '/path/to/local/aar'
82 | }
83 | }
84 | ```
85 |
86 |
87 | ## Initialization
88 | Babbage must be initialized with four arguments:
89 | - The `Application` instance
90 | - The directory in which to store the data
91 | - The encryption key, which must be unique for each installation, and remain the same on each launch.
92 | - An instance of `Babbage.Listener` to handle migration events
93 |
94 | ```java
95 | // In App.Java
96 | File filesDir = getFilesDir();
97 |
98 | // Set up the encryption key used for data at rest
99 | byte[] key = BabbageKeyStore.getInstance().getKey(this);
100 |
101 | // Load the native Babbage library
102 | Babbage.start(this, filesDir, key, new MyCustomBabbageListener());
103 | ```
104 |
105 |
106 | ## Loading Data from the Datastore
107 | You can store and retrieve persistent data using the Datastore class.
108 |
109 | ```java
110 | Datastore datastore = DatastoreFactory.create()
111 | User user = datastore.getUser(username);
112 | ```
113 |
114 | >**Important Considerations:**
115 | - Although there can be multiple Datastore instances, they are all communicating with the same persistent store (a local SQlite database).
116 | - Datastore instances are not thread-safe. If you are creating a new thread - perhaps to make a network request asynchronously - then you should create a new Datastore to accompany it.
117 | - Instances loaded from a Datastore are not thread-safe. Instead of passing an instance to a separate thread, pass the instance's ID - for example, Java: `user.getID()`, Swift: `user.objectID` - and use a separate Datastore to load the instance.
118 |
119 |
120 | ## Network Requests
121 | Babbage talks to back-end services to retrieve all information, such as users, subjects, forms, and so on. A normal application flow goes something like this:
122 |
123 | 1. Log in using a username / password
124 | 2. Load subjects for the logged in user
125 | 3. Load forms and present them to the user
126 |
127 | The following code replicates this process:
128 | ```java
129 | User user = client.logIn(datastore, username, password);
130 | client.loadUserData(user);
131 | List subject = user.getSubjects();
132 | for (Subject subject : subjects) {
133 | client.loadStudyConfiguration(subject.getStudy(), user);
134 | }
135 |
136 | for (Subject subject : subjects) {
137 | client.loadForms(subject)
138 | }
139 | ```
140 |
141 | >**Important Considerations**
142 |
143 | - The preceding example assumes the user is associated with a single subject. In reality they may have multiple subjects associated with them.
144 | - The example assumes a best-case scenario where each request is successful. A robust application should have adequate error handling throughout the process.
145 | - To avoid interfering with the UI, make all requests asynchronously on a background thread.
146 | - The Java network requests are not synchronous and should be performed in a background thread.
147 |
148 | ## API Documentation ##
149 |
150 | Refer to the [AppConnect documentation](https://learn.mdsol.com/api/appconnect/rave-appconnect-sdks-95295808.html) for detailed instructions on how to use the various APIs.
151 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/ImageCaptureActivity.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.app.AlertDialog;
4 | import android.app.ProgressDialog;
5 | import android.content.DialogInterface;
6 | import android.content.Intent;
7 | import android.content.pm.PackageManager;
8 | import android.graphics.Bitmap;
9 | import android.graphics.BitmapFactory;
10 | import android.net.Uri;
11 | import android.os.AsyncTask;
12 | import android.os.Bundle;
13 | import android.provider.MediaStore;
14 | import android.support.v7.app.AppCompatActivity;
15 | import android.util.Log;
16 | import android.view.View;
17 | import android.widget.Button;
18 | import android.widget.ImageView;
19 |
20 | import com.mdsol.babbage.model.Datastore;
21 | import com.mdsol.babbage.model.DatastoreFactory;
22 | import com.mdsol.babbage.model.Subject;
23 |
24 | import java.io.ByteArrayOutputStream;
25 | import java.io.FileNotFoundException;
26 | import java.io.InputStream;
27 |
28 | /**
29 | * An activity to save an image to AppConnect
30 | */
31 | public class ImageCaptureActivity extends AppCompatActivity {
32 |
33 | public static final String SUBJECT_ID_EXTRA = "appconnectsample.imagecaptureactivity.intent.extra.SUBJECT_ID";
34 |
35 | private static final String TAG = "ImageCaptureActivity";
36 |
37 | private long subjectID;
38 | private Bitmap currentImage;
39 | private ImageView currentImageView;
40 | private Button submitButton;
41 | private Button takePictureButton;
42 |
43 | @Override
44 | protected void onCreate(Bundle savedInstanceState) {
45 | super.onCreate(savedInstanceState);
46 | setContentView(R.layout.image_capture_activity);
47 |
48 | currentImageView = (ImageView)findViewById(R.id.currentThumbnail);
49 | submitButton = (Button)findViewById(R.id.imageSubmitButton);
50 | takePictureButton = (Button)findViewById(R.id.takePictureButton);
51 |
52 | // Get the ID of the subject to save images for
53 | Intent intent = getIntent();
54 | subjectID = intent.getLongExtra(SUBJECT_ID_EXTRA, 0);
55 |
56 | if (!getApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY))
57 | takePictureButton.setEnabled(false);
58 |
59 | submitButton.setEnabled(false);
60 |
61 | }
62 |
63 | public void doSelectImageButton(View source) {
64 | dispatchSelectImageIntent();
65 | }
66 |
67 | public void doTakePictureButton(View source) {
68 | dispatchTakePictureIntent();
69 | }
70 |
71 | private static final int IMAGE_CAPTURE_INTENT = 1;
72 | private static final int SELECT_IMAGE_INTENT = 2;
73 |
74 | private void dispatchTakePictureIntent() {
75 | Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
76 | if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
77 | startActivityForResult(takePictureIntent, IMAGE_CAPTURE_INTENT);
78 | }
79 | }
80 |
81 | @Override
82 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
83 | if (resultCode == RESULT_OK) {
84 | if (requestCode == IMAGE_CAPTURE_INTENT) {
85 | Bundle extras = data.getExtras();
86 | currentImage = (Bitmap) extras.get("data");
87 |
88 | } else if (requestCode == SELECT_IMAGE_INTENT) {
89 | Uri selectedImage = data.getData();
90 | try {
91 | InputStream imageStream = getContentResolver().openInputStream(selectedImage);
92 | currentImage = BitmapFactory.decodeStream(imageStream);
93 |
94 | } catch (FileNotFoundException exception) {
95 | return;
96 | }
97 | }
98 | currentImageView.setImageBitmap(currentImage);
99 | submitButton.setEnabled(true);
100 | }
101 | }
102 |
103 | private void dispatchSelectImageIntent() {
104 | Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
105 | photoPickerIntent.setType("image/*");
106 | startActivityForResult(photoPickerIntent, SELECT_IMAGE_INTENT);
107 | }
108 |
109 | public void doSubmitButton(View source) {
110 | submitButton.setEnabled(false);
111 | new CollectAndSubmitTask().execute(currentImage);
112 | }
113 |
114 | /**
115 | * An asynchronous task that collects the responses and submits the form.
116 | */
117 | private class CollectAndSubmitTask extends AsyncTask {
118 |
119 | private ProgressDialog progressDialog;
120 | private Exception exception;
121 |
122 | @Override
123 | protected void onPreExecute() {
124 | progressDialog = new ProgressDialog(ImageCaptureActivity.this, ProgressDialog.STYLE_SPINNER);
125 | progressDialog.setMessage(getString(R.string.image_capture_saving_message));
126 | progressDialog.setCancelable(false);
127 | progressDialog.show();
128 | }
129 |
130 | @Override
131 | protected Void doInBackground(Bitmap... params) {
132 | // *** AppConnect ***
133 | // Each secondary thread must create its own datastore instance and
134 | // dispose of it when done
135 | Datastore datastore = null;
136 |
137 | try {
138 | datastore = DatastoreFactory.create();
139 | Subject subject = datastore.getSubject(subjectID);
140 |
141 | ByteArrayOutputStream stream = new ByteArrayOutputStream();
142 | currentImage.compress(Bitmap.CompressFormat.JPEG, 70, stream);
143 | byte[] byteArray = stream.toByteArray();
144 |
145 | // The API call to AppConnect to actually save the data:
146 | subject.collectData(byteArray, "sample metadata", "image/jpeg");
147 | }
148 | catch (Exception ex) {
149 | exception = ex;
150 | }
151 | finally {
152 | if (datastore != null)
153 | datastore.dispose();
154 | }
155 | return null;
156 | }
157 |
158 | @Override
159 | protected void onPostExecute(Void aVoid) {
160 | progressDialog.cancel();
161 |
162 | if (exception != null) {
163 | Log.e(TAG, "The submit task failed", exception);
164 | new AlertDialog.Builder(ImageCaptureActivity.this).
165 | setTitle(R.string.image_capture_failed_title).
166 | setMessage(exception.getMessage()).
167 | setPositiveButton(R.string.ok_button, null).
168 | show();
169 | return;
170 | }
171 |
172 | new AlertDialog.Builder(ImageCaptureActivity.this).
173 | setCancelable(false).
174 | setTitle(R.string.image_capture_succeeded_title).
175 | setMessage(R.string.image_capture_succeeded_message).
176 | setPositiveButton(R.string.ok_button, new DialogInterface.OnClickListener() {
177 | @Override
178 | public void onClick(DialogInterface dialog, int which) {
179 | currentImageView.setImageBitmap(null);
180 | currentImage = null;
181 | }
182 | }).
183 | show();
184 |
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/field_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
18 |
19 |
20 |
21 |
26 |
27 |
33 |
34 |
35 |
36 |
38 |
39 |
44 |
45 |
51 |
52 |
53 |
54 |
56 |
57 |
62 |
63 |
69 |
70 |
71 |
72 |
74 |
75 |
80 |
81 |
87 |
88 |
89 |
90 |
92 |
93 |
98 |
99 |
105 |
106 |
107 |
108 |
110 |
111 |
116 |
117 |
123 |
124 |
125 |
126 |
127 |
128 |
136 |
137 |
143 |
144 |
148 |
149 |
153 |
154 |
155 |
156 |
163 |
164 |
169 |
170 |
176 |
177 |
178 |
179 |
188 |
189 |
190 |
191 |
192 |
193 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sample/appconnectsample/ListActivity.java:
--------------------------------------------------------------------------------
1 | package com.sample.appconnectsample;
2 |
3 | import android.app.AlertDialog;
4 | import android.content.Intent;
5 | import android.os.AsyncTask;
6 | import android.support.v7.app.AppCompatActivity;
7 | import android.os.Bundle;
8 | import android.util.Log;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 | import android.widget.AdapterView;
12 | import android.widget.ArrayAdapter;
13 | import android.widget.ListView;
14 | import android.widget.TextView;
15 | import com.mdsol.babbage.model.Datastore;
16 | import com.mdsol.babbage.model.DatastoreFactory;
17 | import com.mdsol.babbage.model.Form;
18 | import com.mdsol.babbage.model.Subject;
19 | import com.mdsol.babbage.model.User;
20 | import com.mdsol.babbage.net.Client;
21 | import com.mdsol.babbage.net.RequestException;
22 | import java.util.ArrayList;
23 | import java.util.List;
24 |
25 | /**
26 | * The list activity retrieves available forms and displays them to the user.
27 | */
28 | public class ListActivity extends AppCompatActivity {
29 |
30 | public static final String USER_ID_EXTRA = "appconnectsample.listactivity.intent.extra.USER_ID";
31 |
32 | private static final String TAG = "ListActivity";
33 |
34 | private ListView formList;
35 | private FormAdapter formAdapter;
36 | private View progressBar;
37 | private long userID;
38 | private long subjectID = -1;
39 | private List