ResearchStack Tutorial: Getting Started
Learn how to build an Android app with ResearchStack, the open source framework similar to ResearchKit on iOS that empowers medical research. By Tom Blankenship.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
ResearchStack Tutorial: Getting Started
30 mins
Collecting a Consent Signature
Next, you need to define the ConsentSignature
object. This is comparable to ORKConsentSignature object in ResearchKit.
Add the following before return document;
in createConsentDocument()
:
ConsentSignature signature = new ConsentSignature();
signature.setRequiresName(true);
signature.setRequiresSignatureImage(true);
document.addSignature(signature);
You create a new ConsentSignature object requiring a name and signature. You then add the signature to the document.
Adding the Review Content
The last piece needed in the document is the summary content. ResearchKit has a built-in ConsentReviewStep to help with this. In ResearchStack you define this view yourself.
Add the following before return document;
in createConsentDocument()
:
document.setHtmlReviewContent("<div style=\"padding: 10px;\" class=\"header\">" +
"<h1 style='text-align: center'>Review Consent!</h1></div>");
You add the review content to the document using full HTML syntax.
The Consent Task
Now that you have the consent document created, you will use it to construct a Task
. ResearchStack uses the class OrderedTask
to keep track of an ordered list of steps to show the user. This class coincides with ORKOrderedTask in ResearchKit.
You will add a VisualConsent
object for each document section. This is different from ResearchKit, where a single ORKVisualConsentStep uses the ORKConsentDocument to automatically create the individual screens.
Add the following method to MainActivity.java.
private List<Step> createConsentSteps(ConsentDocument document) {
List<Step> steps = new ArrayList<>();
for (ConsentSection section: document.getSections()) {
ConsentVisualStep visualStep = new ConsentVisualStep(section.getType().toString());
visualStep.setSection(section);
visualStep.setNextButtonString(getString(R.string.rsb_next));
steps.add(visualStep);
}
return steps;
}
You create an array of steps
and loop through all sections in ConsentDocument
to create a ConsentVisualStep
for each one. For each visualStep
, you set the title, section and a next button label. Finally, you add visualStep
to the list of steps
.
Note: You can pass in any unique identifier when creating steps. In this case, using the string representation of the ConsentSection
type is an easy way to supply the identifier.
Note: You can pass in any unique identifier when creating steps. In this case, using the string representation of the ConsentSection
type is an easy way to supply the identifier.
Next is the Consent Review step. In ResearchKit, RKConsentReviewStep includes the review step with the signature form. In ResearchStack, the signature form is a separate step.
Add the following before return steps;
in createConsentSteps()
:
ConsentDocumentStep documentStep = new ConsentDocumentStep("consent_doc");
documentStep.setConsentHTML(document.getHtmlReviewContent());
documentStep.setConfirmMessage(getString(R.string.rsb_consent_review_reason));
steps.add(documentStep);
Here, you create a new ConsentDocumentStep
and set the HTML content from the ConsentDocument
. You also set a confirmation message that displays in a dialog and add the step to the steps
list.
Next, add a step to capture the user’s full name. ResearchKit includes this in ORKConsentReviewStep.
Add the following code before return steps;
in createConsentSteps()
.
ConsentSignature signature = document.getSignature(0);
if (signature.requiresName()) {
TextAnswerFormat format = new TextAnswerFormat();
format.setIsMultipleLines(false);
QuestionStep fullName = new QuestionStep("consent_name_step", "Please enter your full name",
format);
fullName.setPlaceholder("Full name");
fullName.setOptional(false);
steps.add(fullName);
}
First, you retrieve the signature object from the consent document. If the signature name is required, you create a question step to ask for the user’s name and add the step to the steps
list.
Answer formats define how a question step will be formatted. You can explore other answer formats here.
Note: If you want to collect multiple questions on a single screen, use FormStep
with a series of QuestionSteps
.
Note: If you want to collect multiple questions on a single screen, use FormStep
with a series of QuestionSteps
.
Next, add a step to collect a signature from the user. ResearchKit includes this in ORKConsentReviewStep. Again, paste this before return steps;
in createConsentSteps()
.
if (signature.requiresSignatureImage()) {
ConsentSignatureStep signatureStep = new ConsentSignatureStep("signature_step");
signatureStep.setTitle(getString(R.string.rsb_consent_signature_title));
signatureStep.setText(getString(R.string.rsb_consent_signature_instruction));
signatureStep.setOptional(false);
signatureStep.setStepLayoutClass(ConsentSignatureStepLayout.class);
steps.add(signatureStep);
}
If the signature requires an image, you create a new signature step and assign the properties, set the step layout class and add the step to the steps
list.
Congratulations — you’ve finished the hard part! On to the UI.
Presenting the Consent Task
You will now create and present an OrderedTask
, using the steps you created in createConsentSteps()
.
The tutorial diverges from ResearchKit due to the different UI concepts between iOS and Android. ResearchKit uses a ORKTaskViewController, whereas ResearchStack uses a ViewTaskActivity
.
Add the following constant to the top of MainActivity
:
private static final int REQUEST_CONSENT = 0;
Add the following method to MainActivity
:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
}
onActivityResult()
is used to process the results of a task, using requestCode
as a unique task identifier. Normally this is where you would save the results or forward them to a web service.
Add the following to displayConsent()
in MainActivity
:
// 1
ConsentDocument document = createConsentDocument();
// 2
List<Step> steps = createConsentSteps(document);
// 3
Task consentTask = new OrderedTask("consent_task", steps);
// 4
Intent intent = ViewTaskActivity.newIntent(this, consentTask);
startActivityForResult(intent, REQUEST_CONSENT);
Here’s what’s going on in the code above:
- You call
createConsentDocument()
to create the ConsentDocument. - You pass
document
intocreateConsentSteps()
to create the consent steps. - You create a new
OrderedTask
, passing in a unique identifier along with the consent steps. - You create an intent with the task and launch the task activity.
Like any other Android activity, the ViewTaskActivity
must be added to the application manifest file. ResearchStack also uses a ViewWebDocumentActivity
when displaying Learn More content.
Add the following to AndroidManifest.xml within the Application
section:
<activity
android:name="org.researchstack.backbone.ui.ViewTaskActivity"
android:windowSoftInputMode="adjustResize"
/>
<activity
android:name="org.researchstack.backbone.ui.ViewWebDocumentActivity"
android:label="@string/app_name"
/>
Now is the big moment — your Consent task is ready to go!
Build and run the app. Tap the CONSENT button and work your way through the consent screens and notice the custom graphics for each section:
The Survey Module
Active Tasks
Where to Go From Here?
Instruction Step
Text Input Question
Text Choice Question
Color Choice Question
Summary Step
Presenting the Survey
Instruction Step
Custom Audio Step
Summary Step
Presenting the Audio Task
Note: If you’ve been paying close attention, you may be asking “Why is ConsentDocument
needed?” The answer is: it’s not! You could create the consent task and steps without a consent document. Using ConsentDocument separates the model from the presentation. A different consent workflow can be shown to the user by changing what is added to the consent document.
You are now ready to move onto the heart of the study: the Survey.
First, you’ll give the user some general instructions using an InstructionStep
. This equals ORKInstructionStep in ResearchKit.
Add the following to displaySurvey()
in MainActivity.java:
You create a list of steps for this task, then create a new instruction step and add it to the steps
list.
Next, you display the first question. This is covered by ORKQuestionStep in ResearchKit. You will format the question using TextAnswerFormat
which equals ORKTextAnswerFormat
in ResearchKit.
Add the following to the bottom of displaySurvey()
;
You create a text answer format with a maximum length of 20. You create a question step with the answer format and add it to the steps
list.
Next up is a question that lets the user choose from several predefined options. This also uses a QuestionStep
, but with a ChoiceAnswerFormat
which equals ORKTextChoiceAnswerFormat in ResearchKit.
Add the following to the bottom of displaySurvey
:
You create a choice–answer format using a .SingleChoice
style and pass in three choices. You then create a QuestionStep
with the answer format and add it to the steps
list.
Note: ChoiceAnswerFormat also allows multiple choice answers using the .MultipleChoice
style.
The last question asks for the user’s favorite color. In ResearchKit, this is done with ORKImageChoiceAnswerFormat, but ResearchStack doesn’t have a built-in option for image choice format.
You could use ChoiceAnswerFormat
and display the colors as simple text labels, but how boring would that be? Instead, you’ll create a custom answer format that displays colors.
ChoiceAnswerFormat
has almost everything you need except for color radio buttons. You’ll need a custom StepBody
and AnswerFormat
to override the default behavior.
Create a new class file named ImageChoiceAnswerFormat.java
and replace its contents with the following:
You inherit from ChoiceAnswerFormat
and implement the QuestionType
interface. You then override getQuestionType()
and return the current object. Finally, you implement getStepBodyClass()
and return the custom step body class.
Android Studio will complain that it cannot resolve ImageChoiceQuestionBody.class
. You’ll fix that now.
ResearchStack provides a class named SingleChoiceQuestionBody
that creates a radio group with buttons for the choices. You will extend this class and modify the radio group to provide custom radio buttons.
Create a new class file named ImageChoiceQuestionBody.java and replace the contents with the following:
You inherit from SingleChoiceQuestionBody
. The constructor calls the parent constructor and saves the answer choices. You then override getBodyView()
and call the base class to create the view. You finish off by looping through the radio buttons and replacing the button drawables using the resource values from mChoices
.
With that in place, you can create an ImageChoiceAnswerFormat
using resource IDs as the values for your choices.
Add the following to the bottom of displaySurvey()
in MainActivity.java
:
You create an ImageChoiceAnswerFormat
using a SingleChoice style and pass in the color choices, then create a question step with the answer format and add it to the steps
list.
Note: The color drawables are included in the starter project. These are standard XML selectors with images for checked and unchecked states.
The last step indicates that the survey is complete. ResearchKit uses ORKCompletionStep for this, but you will use ResearchStack's InstructionStep
.
Add the following to the bottom of displaySurvey()
:
Here, you simply create an instruction step and add it to the steps
list.
All that's left is to the display the survey. Add the following constant to the top of MainActivity
:
Add the following to the bottom of displaySurvey()
:
In the code above, you create a new OrderedTask
, pass in the steps and then launch the activity. You set REQUEST_SURVEY
as the request code.
Build and run the app; tap on the Survey button and work your way through the survey.
Besides surveys, you may want to collect active data as well. ResearchStack doesn’t have any active tasks built-in, so you will create your own with a custom step.
Your custom step will use the device's microphone to record audio samples. ResearchKit uses a built-in AudioTask.
The following diagram illustrates the main components involved in the custom audio task. You will build out the AudioStep
and AudioStepLayout
parts of the diagram.
Start by displaying some basic instructions. Add the following code to displayAudioTask()
in MainActivity.java:
Here you add the familiar instruction step to the steps
list.
To create a fully custom Step
, you need to create the following classes:
Create a new layout resource file named audio_step_layout.xml in the res/layout folder and add the following code:
Most of this is boilerplate code used by any custom step layout. The LinearLayout
section contains your custom layout UI. This provides a button for the user to start the recording and some labels to show progress.
You’ll now create a Layout
class to manage the layout.
Create a new file named AudioStepLayout.java and add the following:
Here you inherit from RelativeLayout
and implement the StepLayout
interface. You add standard layout constructors and overrides for the StepLayout
interface. You'll fill in the details for the overrides soon.
Next, you will create the custom audio step. Create a new file named AudioStep.java and add the following code:
Here you define a custom audio step with one property, mDuration
, to control the length of the audio recording. In the constructor, you call the base class, set the step as required and define the default layout class as AudioStepLayout.class
.
Now to finish AudioStepLayout
. Add the following to the top of the AudioStepLayout
class:
Add the following methods to AudioStepLayout.java:
Taking it comment-by-comment;
Now you will create a method to handle initialization. Add the following to AudioStepLayout.java:
Here you inflate the layout defined earlier and bind the UI elements to variables.
Now replace //TODO: set onClick listener
with the following:
There’s quite a few things going on here, so to break it down:
Now that initializeStep()
implements the core functionality for AudioStepLayout
, it's time to wire everything up.
Add the following code to initialize()
:
initialize()
is called when the step is about to be displayed. StepResult
will contain the user's previous answer if they have already visited this step, otherwise it is null
. You save the step along with the step result to member variables and then call the previously defined initializeStep()
.
Replace the contents of getLayout()
with the following:
getLayout
returns the view to be displayed. In this case, it is the current object.
Replace the contents of isBackEventConsumed()
with the following:
isBackEventConsumed
should return false
unless you have special handling in mind when the user tries to back up. This is your chance to save the results. You call onSaveStep()
to notify ResearchStack that it can save the results and perform the ACTION_PREV
action.
Replace the contents of setCallbacks()
method with the following:
setCallbacks
provides you with a callback object, and you save it for future use.
Great job! You have completed a custom step, and now you can finish the audio task.
Add the following to displayAudioTask()
in MainActivity.java:
You create the new audio step, set its title, text and duration and add it to the steps
list.
The final step display displays a summary. Add the following to displayAudioTask()
:
You create an instruction step and add it to the steps
list.
All that's left is to present the task!
Add the following constant to the top of MainActivity
:
Add the following to displayAudioTask()
:
You create a new OrderedTask
and pass in the steps
and then launch the activity and set REQUEST_AUDIO
as the request code.
Build and run the app. Tap the Microphone button and run through the task:
You can download the final project for this tutorial here.
While this tutorial covered three typical tasks you'll find in research studies — informed consent, surveys and active tasks – a real-world study will require some additional work. You may need to store or print results, send results to a server, schedule tasks to run periodically and deal with IRB approval.
For more information, visit the official project page for ResearchStack.
Take some time to look at the Skin module. It builds on top of Backbone and provides several advanced features, including:
The MoleMapper Open Source app available on GitHub provides a great example of a fully-featured research app created for both iOS and Android.
Thank you for reading this tutorial, and I hope you have enjoyed it. Please join the discussion below if you have any questions!
-
AudioStepLayout
: An Android Layout class that implements theStepLayout
interface. -
AudioStep
: A class that extends the baseStep
class.
- Additional built-in tasks such as ConsentTask and SmartSurveyTask.
- The ability to build out your studies using JSON files and use ResearchKit resources.
- Task scheduling system with notifications.
- You define
setDataToResult()
to assign the audio data returned fromgetBase64EncodedAudio()
to the step results.KEY_AUDIO
is a unique identifier that distinguishes the audio data from any other results. - You define
getBase64EncodedAudio()
to translate the raw audio to a Base64 string. - If the recording is complete, you open the recording file, use ResearchStack's
FileUtils.readAll()
to read in the data from the file and then useBase64.encodeToString()
to encode the data to a string and return it.
- You create an
onClick
listener on thebeginButton
to start recording when tapped. - You get a filename based on the app’s files directory and use the
AudioRecorder
object to start recording the file. AudioRecorder.java was included in the starter project and provides basic audio recording capabilities. - You hide the Begin button and display the recording countdown timer label.
- You start a
CountDownTimer
based on the requested recording duration. - The
CountDownTimer
callsonTick()
periodically. You’ll use this opportunity to update the countdown timer with the remaining time in seconds. -
CountDownTimer
callsonFinish()
when it is complete. You set themIsRecordingComplete
flag, stop the recording and save the results. You callmStepCallbacks.onSaveStep()
to automatically jump to the next step. - Finally, you start the countdown timer.
Note: If you’ve been paying close attention, you may be asking “Why is ConsentDocument
needed?” The answer is: it’s not! You could create the consent task and steps without a consent document. Using ConsentDocument separates the model from the presentation. A different consent workflow can be shown to the user by changing what is added to the consent document.
The Survey Module
You are now ready to move onto the heart of the study: the Survey.
Instruction Step
First, you’ll give the user some general instructions using an InstructionStep
. This equals ORKInstructionStep in ResearchKit.
Add the following to displaySurvey()
in MainActivity.java:
List<Step> steps = new ArrayList<>();
InstructionStep instructionStep = new InstructionStep("survey_instruction_step",
"The Questions Three",
"Who would cross the Bridge of Death must answer me these questions three, ere the other side they see.");
steps.add(instructionStep);
You create a list of steps for this task, then create a new instruction step and add it to the steps
list.
Text Input Question
Next, you display the first question. This is covered by ORKQuestionStep in ResearchKit. You will format the question using TextAnswerFormat
which equals ORKTextAnswerFormat
in ResearchKit.
Add the following to the bottom of displaySurvey()
;
TextAnswerFormat format = new TextAnswerFormat(20);
QuestionStep nameStep = new QuestionStep("name", "What is your name?", format);
nameStep.setPlaceholder("Name");
nameStep.setOptional(false);
steps.add(nameStep);
You create a text answer format with a maximum length of 20. You create a question step with the answer format and add it to the steps
list.
Text Choice Question
Next up is a question that lets the user choose from several predefined options. This also uses a QuestionStep
, but with a ChoiceAnswerFormat
which equals ORKTextChoiceAnswerFormat in ResearchKit.
Add the following to the bottom of displaySurvey
:
AnswerFormat questionFormat = new ChoiceAnswerFormat(AnswerFormat.ChoiceAnswerStyle
.SingleChoice,
new Choice<>("Create a ResearchKit App", 0),
new Choice<>("Seek the Holy Grail", 1),
new Choice<>("Find a shrubbery", 2));
QuestionStep questionStep = new QuestionStep("quest_step", "What is your quest?", questionFormat);
questionStep.setPlaceholder("Quest");
questionStep.setOptional(false);
steps.add(questionStep);
You create a choice–answer format using a .SingleChoice
style and pass in three choices. You then create a QuestionStep
with the answer format and add it to the steps
list.
Note: ChoiceAnswerFormat also allows multiple choice answers using the .MultipleChoice
style.
Color Choice Question
The last question asks for the user’s favorite color. In ResearchKit, this is done with ORKImageChoiceAnswerFormat, but ResearchStack doesn’t have a built-in option for image choice format.
You could use ChoiceAnswerFormat
and display the colors as simple text labels, but how boring would that be? Instead, you’ll create a custom answer format that displays colors.
ChoiceAnswerFormat
has almost everything you need except for color radio buttons. You’ll need a custom StepBody
and AnswerFormat
to override the default behavior.
Create a new class file named ImageChoiceAnswerFormat.java
and replace its contents with the following:
public class ImageChoiceAnswerFormat extends ChoiceAnswerFormat implements AnswerFormat.QuestionType
{
public ImageChoiceAnswerFormat(ChoiceAnswerStyle answerStyle, Choice... choices) {
super(answerStyle, choices);
}
@Override
public QuestionType getQuestionType()
{
return this;
}
@Override
public Class<?> getStepBodyClass() {
return ImageChoiceQuestionBody.class;
}
}
You inherit from ChoiceAnswerFormat
and implement the QuestionType
interface. You then override getQuestionType()
and return the current object. Finally, you implement getStepBodyClass()
and return the custom step body class.
Android Studio will complain that it cannot resolve ImageChoiceQuestionBody.class
. You’ll fix that now.
ResearchStack provides a class named SingleChoiceQuestionBody
that creates a radio group with buttons for the choices. You will extend this class and modify the radio group to provide custom radio buttons.
Create a new class file named ImageChoiceQuestionBody.java and replace the contents with the following:
public class ImageChoiceQuestionBody <T> extends SingleChoiceQuestionBody
{
private Choice[] mChoices;
public ImageChoiceQuestionBody(Step step, StepResult result) {
super(step, result);
QuestionStep questionStep = (QuestionStep)step;
ImageChoiceAnswerFormat format = (ImageChoiceAnswerFormat)questionStep.getAnswerFormat();
mChoices = format.getChoices();
}
@Override
public View getBodyView(int viewType, LayoutInflater inflater, ViewGroup parent)
{
RadioGroup group = (RadioGroup)super.getBodyView(viewType, inflater, parent);
for (int i=0; i<mChoices.length; i++) {
RadioButton button = (RadioButton)group.getChildAt(i);
button.setButtonDrawable(Integer.parseInt(mChoices[i].getValue().toString()));
}
return group;
}
}
You inherit from SingleChoiceQuestionBody
. The constructor calls the parent constructor and saves the answer choices. You then override getBodyView()
and call the base class to create the view. You finish off by looping through the radio buttons and replacing the button drawables using the resource values from mChoices
.
With that in place, you can create an ImageChoiceAnswerFormat
using resource IDs as the values for your choices.
Add the following to the bottom of displaySurvey()
in MainActivity.java
:
AnswerFormat colorAnswerFormat = new ImageChoiceAnswerFormat(AnswerFormat.ChoiceAnswerStyle
.SingleChoice,
new Choice<>("Red", R.drawable.red_selector),
new Choice<>("Orange", R.drawable.orange_selector),
new Choice<>("Yellow", R.drawable.yellow_selector),
new Choice<>("Green", R.drawable.green_selector),
new Choice<>("Blue", R.drawable.blue_selector),
new Choice<>("Purple", R.drawable.purple_selector));
QuestionStep colorStep = new QuestionStep("color_step", "What is your favorite color?",
colorAnswerFormat);
colorStep.setOptional(false);
steps.add(colorStep);
You create an ImageChoiceAnswerFormat
using a SingleChoice style and pass in the color choices, then create a question step with the answer format and add it to the steps
list.
Note: The color drawables are included in the starter project. These are standard XML selectors with images for checked and unchecked states.
Summary Step
The last step indicates that the survey is complete. ResearchKit uses ORKCompletionStep for this, but you will use ResearchStack's InstructionStep
.
Add the following to the bottom of displaySurvey()
:
InstructionStep summaryStep = new InstructionStep("survey_summary_step",
"Right. Off you go!",
"That was easy!");
steps.add(summaryStep);
Here, you simply create an instruction step and add it to the steps
list.
Presenting the Survey
All that's left is to the display the survey. Add the following constant to the top of MainActivity
:
private static final int REQUEST_SURVEY = 1;
Add the following to the bottom of displaySurvey()
:
OrderedTask task = new OrderedTask("survey_task", steps);
Intent intent = ViewTaskActivity.newIntent(this, task);
startActivityForResult(intent, REQUEST_SURVEY);
In the code above, you create a new OrderedTask
, pass in the steps and then launch the activity. You set REQUEST_SURVEY
as the request code.
Build and run the app; tap on the Survey button and work your way through the survey.
Active Tasks
Besides surveys, you may want to collect active data as well. ResearchStack doesn’t have any active tasks built-in, so you will create your own with a custom step.
Your custom step will use the device's microphone to record audio samples. ResearchKit uses a built-in AudioTask.
The following diagram illustrates the main components involved in the custom audio task. You will build out the AudioStep
and AudioStepLayout
parts of the diagram.
Instruction Step
Start by displaying some basic instructions. Add the following code to displayAudioTask()
in MainActivity.java:
List<Step> steps = new ArrayList<>();
InstructionStep instructionStep = new InstructionStep("audio_instruction_step",
"A sentence prompt will be given to you to read.",
"These are the last dying words of Joseph of Aramathea.");
steps.add(instructionStep);
Here you add the familiar instruction step to the steps
list.
Custom Audio Step
To create a fully custom Step
, you need to create the following classes:
-
AudioStepLayout
: An Android Layout class that implements theStepLayout
interface. -
AudioStep
: A class that extends the baseStep
class.
Create a new layout resource file named audio_step_layout.xml in the res/layout folder and add the following code:
<?xml version="1.0" encoding="utf-8"?>
<merge
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"
>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/image"
android:layout_marginLeft="@dimen/rsb_margin_left"
android:layout_marginRight="@dimen/rsb_margin_right"
android:layout_marginTop="20dp"
android:textColor="?attr/colorAccent"
android:textSize="20sp"
tools:text="@string/lorem_name"
/>
<TextView
android:id="@+id/summary"
style="@style/TextAppearance.AppCompat.Subhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/title"
android:layout_marginLeft="@dimen/rsb_margin_left"
android:layout_marginRight="@dimen/rsb_margin_right"
android:layout_marginTop="36dp"
tools:text="@string/lorem_medium"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/summary"
android:layout_centerHorizontal="true"
android:orientation="vertical"
>
<Button
android:id="@+id/begin_recording"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="36dp"
android:text="Begin"
/>
<TextView
android:id="@+id/countdown_title"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="36dp"
android:text="Recording"
android:visibility="gone"
/>
<TextView
android:id="@+id/countdown"
style="@style/TextAppearance.AppCompat.Subhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="36dp"
tools:text="@string/lorem_medium"
/>
</LinearLayout>
<org.researchstack.backbone.ui.views.SubmitBar
android:id="@+id/submit_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/layout_consent_review_signature_clear"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignTop="@+id/submit_bar"
android:text="@string/rsb_consent_signature_clear"
android:textColor="@color/rsb_submit_bar_negative"
/>
</merge>
Most of this is boilerplate code used by any custom step layout. The LinearLayout
section contains your custom layout UI. This provides a button for the user to start the recording and some labels to show progress.
You’ll now create a Layout
class to manage the layout.
Create a new file named AudioStepLayout.java and add the following:
public class AudioStepLayout extends RelativeLayout implements StepLayout
{
public AudioStepLayout(Context context)
{
super(context);
}
public AudioStepLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public AudioStepLayout(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}
@Override
public void initialize(Step step, StepResult result) {
}
@Override
public View getLayout() {
return null;
}
@Override
public boolean isBackEventConsumed() {
return false;
}
@Override
public void setCallbacks(StepCallbacks callbacks) {
}
}
Here you inherit from RelativeLayout
and implement the StepLayout
interface. You add standard layout constructors and overrides for the StepLayout
interface. You'll fill in the details for the overrides soon.
Next, you will create the custom audio step. Create a new file named AudioStep.java and add the following code:
public class AudioStep extends Step
{
private int mDuration;
public AudioStep(String identifier)
{
super(identifier);
setOptional(false);
setStepLayoutClass(AudioStepLayout.class);
}
public int getDuration() {
return mDuration;
}
public void setDuration(int duration) {
mDuration = duration;
}
}
Here you define a custom audio step with one property, mDuration
, to control the length of the audio recording. In the constructor, you call the base class, set the step as required and define the default layout class as AudioStepLayout.class
.
Now to finish AudioStepLayout
. Add the following to the top of the AudioStepLayout
class:
public static final String KEY_AUDIO = "AudioStep.Audio";
private StepCallbacks mStepCallbacks;
private AudioStep mStep;
private StepResult<String> mResult;
private boolean mIsRecordingComplete = false;
private String mFilename;
Add the following methods to AudioStepLayout.java:
// 1
private void setDataToResult()
{
mResult.setResultForIdentifier(KEY_AUDIO, getBase64EncodedAudio());
}
// 2
private String getBase64EncodedAudio()
{
if(mIsRecordingComplete)
{
// 3
File file = new File(mFilename);
try {
byte[] bytes = FileUtils.readAll(file);
String encoded = Base64.encodeToString(bytes, Base64.DEFAULT);
return encoded;
} catch (Exception e) {
return null;
}
}
else
{
return null;
}
}
Taking it comment-by-comment;
- You define
setDataToResult()
to assign the audio data returned fromgetBase64EncodedAudio()
to the step results.KEY_AUDIO
is a unique identifier that distinguishes the audio data from any other results. - You define
getBase64EncodedAudio()
to translate the raw audio to a Base64 string. - If the recording is complete, you open the recording file, use ResearchStack's
FileUtils.readAll()
to read in the data from the file and then useBase64.encodeToString()
to encode the data to a string and return it.
Now you will create a method to handle initialization. Add the following to AudioStepLayout.java:
private void initializeStep()
{
LayoutInflater.from(getContext())
.inflate(R.layout.audio_step_layout, this, true);
TextView title = (TextView) findViewById(R.id.title);
title.setText(mStep.getTitle());
TextView text = (TextView) findViewById(R.id.summary);
text.setText(mStep.getText());
final TextView countdown = (TextView) findViewById(R.id.countdown);
countdown.setText("Seconds remaining: " + Integer.toString(mStep.getDuration()));
final TextView countdown_title = (TextView) findViewById(R.id.countdown_title);
final Button beginButton = (Button) findViewById(R.id.begin_recording);
// TODO: set onClick listener
}
Here you inflate the layout defined earlier and bind the UI elements to variables.
Now replace //TODO: set onClick listener
with the following:
// 1
beginButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 2
mFilename = getContext().getFilesDir().getAbsolutePath();
mFilename += "/camelotaudiorecord.3gp";
final AudioRecorder audioRecorder = new AudioRecorder();
audioRecorder.startRecording(mFilename);
// 3
beginButton.setVisibility(GONE);
countdown_title.setVisibility(View.VISIBLE);
// 4
CountDownTimer Count = new CountDownTimer(mStep.getDuration()*1000, 1000) {
// 5
public void onTick(long millisUntilFinished) {
countdown.setText("Seconds remaining: " + millisUntilFinished / 1000);
}
// 6
public void onFinish() {
mIsRecordingComplete = true;
audioRecorder.stopRecording();
AudioStepLayout.this.setDataToResult();
mStepCallbacks.onSaveStep(StepCallbacks.ACTION_NEXT, mStep, mResult);
}
};
// 7
Count.start();
}
});
There’s quite a few things going on here, so to break it down:
- You create an
onClick
listener on thebeginButton
to start recording when tapped. - You get a filename based on the app’s files directory and use the
AudioRecorder
object to start recording the file. AudioRecorder.java was included in the starter project and provides basic audio recording capabilities. - You hide the Begin button and display the recording countdown timer label.
- You start a
CountDownTimer
based on the requested recording duration. - The
CountDownTimer
callsonTick()
periodically. You’ll use this opportunity to update the countdown timer with the remaining time in seconds. -
CountDownTimer
callsonFinish()
when it is complete. You set themIsRecordingComplete
flag, stop the recording and save the results. You callmStepCallbacks.onSaveStep()
to automatically jump to the next step. - Finally, you start the countdown timer.
Now that initializeStep()
implements the core functionality for AudioStepLayout
, it's time to wire everything up.
Add the following code to initialize()
:
this.mStep = (AudioStep)step;
this.mResult = result == null ? new StepResult<>(step) : result;
initializeStep();
initialize()
is called when the step is about to be displayed. StepResult
will contain the user's previous answer if they have already visited this step, otherwise it is null
. You save the step along with the step result to member variables and then call the previously defined initializeStep()
.
Replace the contents of getLayout()
with the following:
return this;
getLayout
returns the view to be displayed. In this case, it is the current object.
Replace the contents of isBackEventConsumed()
with the following:
setDataToResult();
mStepCallbacks.onSaveStep(StepCallbacks.ACTION_PREV, mStep, mResult);
return false;
isBackEventConsumed
should return false
unless you have special handling in mind when the user tries to back up. This is your chance to save the results. You call onSaveStep()
to notify ResearchStack that it can save the results and perform the ACTION_PREV
action.
Replace the contents of setCallbacks()
method with the following:
this.mStepCallbacks = callbacks;
setCallbacks
provides you with a callback object, and you save it for future use.
Great job! You have completed a custom step, and now you can finish the audio task.
Add the following to displayAudioTask()
in MainActivity.java:
AudioStep audioStep = new AudioStep("audio_step");
audioStep.setTitle("Repeat the following phrase:");
audioStep.setText("The Holy Grail can be found in the Castle of Aaaaaaaaaaah");
audioStep.setDuration(10);
steps.add(audioStep);
You create the new audio step, set its title, text and duration and add it to the steps
list.
Summary Step
The final step display displays a summary. Add the following to displayAudioTask()
:
InstructionStep summaryStep = new InstructionStep("audio_summary_step",
"Right. Off you go!",
"That was easy!");
steps.add(summaryStep);
You create an instruction step and add it to the steps
list.
All that's left is to present the task!
Presenting the Audio Task
Add the following constant to the top of MainActivity
:
private static final int REQUEST_AUDIO = 2;
Add the following to displayAudioTask()
:
OrderedTask task = new OrderedTask("audio_task", steps);
Intent intent = ViewTaskActivity.newIntent(this, task);
startActivityForResult(intent, REQUEST_AUDIO);
You create a new OrderedTask
and pass in the steps
and then launch the activity and set REQUEST_AUDIO
as the request code.
Build and run the app. Tap the Microphone button and run through the task:
Where to Go From Here?
You can download the final project for this tutorial here.
While this tutorial covered three typical tasks you'll find in research studies — informed consent, surveys and active tasks – a real-world study will require some additional work. You may need to store or print results, send results to a server, schedule tasks to run periodically and deal with IRB approval.
For more information, visit the official project page for ResearchStack.
Take some time to look at the Skin module. It builds on top of Backbone and provides several advanced features, including:
- Additional built-in tasks such as ConsentTask and SmartSurveyTask.
- The ability to build out your studies using JSON files and use ResearchKit resources.
- Task scheduling system with notifications.
The MoleMapper Open Source app available on GitHub provides a great example of a fully-featured research app created for both iOS and Android.
Thank you for reading this tutorial, and I hope you have enjoyed it. Please join the discussion below if you have any questions!
Note: If you’ve been paying close attention, you may be asking “Why is ConsentDocument
needed?” The answer is: it’s not! You could create the consent task and steps without a consent document. Using ConsentDocument separates the model from the presentation. A different consent workflow can be shown to the user by changing what is added to the consent document.
Note: ChoiceAnswerFormat also allows multiple choice answers using the .MultipleChoice
style.
Note: The color drawables are included in the starter project. These are standard XML selectors with images for checked and unchecked states.
Active Tasks
Besides surveys, you may want to collect active data as well. ResearchStack doesn’t have any active tasks built-in, so you will create your own with a custom step.
Your custom step will use the device's microphone to record audio samples. ResearchKit uses a built-in AudioTask.
The following diagram illustrates the main components involved in the custom audio task. You will build out the AudioStep
and AudioStepLayout
parts of the diagram.
Instruction Step
Start by displaying some basic instructions. Add the following code to displayAudioTask()
in MainActivity.java:
List<Step> steps = new ArrayList<>();
InstructionStep instructionStep = new InstructionStep("audio_instruction_step",
"A sentence prompt will be given to you to read.",
"These are the last dying words of Joseph of Aramathea.");
steps.add(instructionStep);
Here you add the familiar instruction step to the steps
list.
Custom Audio Step
To create a fully custom Step
, you need to create the following classes:
-
AudioStepLayout
: An Android Layout class that implements theStepLayout
interface. -
AudioStep
: A class that extends the baseStep
class.
Create a new layout resource file named audio_step_layout.xml in the res/layout folder and add the following code:
<?xml version="1.0" encoding="utf-8"?>
<merge
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"
>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/image"
android:layout_marginLeft="@dimen/rsb_margin_left"
android:layout_marginRight="@dimen/rsb_margin_right"
android:layout_marginTop="20dp"
android:textColor="?attr/colorAccent"
android:textSize="20sp"
tools:text="@string/lorem_name"
/>
<TextView
android:id="@+id/summary"
style="@style/TextAppearance.AppCompat.Subhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/title"
android:layout_marginLeft="@dimen/rsb_margin_left"
android:layout_marginRight="@dimen/rsb_margin_right"
android:layout_marginTop="36dp"
tools:text="@string/lorem_medium"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/summary"
android:layout_centerHorizontal="true"
android:orientation="vertical"
>
<Button
android:id="@+id/begin_recording"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="36dp"
android:text="Begin"
/>
<TextView
android:id="@+id/countdown_title"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="36dp"
android:text="Recording"
android:visibility="gone"
/>
<TextView
android:id="@+id/countdown"
style="@style/TextAppearance.AppCompat.Subhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="36dp"
tools:text="@string/lorem_medium"
/>
</LinearLayout>
<org.researchstack.backbone.ui.views.SubmitBar
android:id="@+id/submit_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/layout_consent_review_signature_clear"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignTop="@+id/submit_bar"
android:text="@string/rsb_consent_signature_clear"
android:textColor="@color/rsb_submit_bar_negative"
/>
</merge>
Most of this is boilerplate code used by any custom step layout. The LinearLayout
section contains your custom layout UI. This provides a button for the user to start the recording and some labels to show progress.
You’ll now create a Layout
class to manage the layout.
Create a new file named AudioStepLayout.java and add the following:
public class AudioStepLayout extends RelativeLayout implements StepLayout
{
public AudioStepLayout(Context context)
{
super(context);
}
public AudioStepLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public AudioStepLayout(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}
@Override
public void initialize(Step step, StepResult result) {
}
@Override
public View getLayout() {
return null;
}
@Override
public boolean isBackEventConsumed() {
return false;
}
@Override
public void setCallbacks(StepCallbacks callbacks) {
}
}
Here you inherit from RelativeLayout
and implement the StepLayout
interface. You add standard layout constructors and overrides for the StepLayout
interface. You'll fill in the details for the overrides soon.
Next, you will create the custom audio step. Create a new file named AudioStep.java and add the following code:
public class AudioStep extends Step
{
private int mDuration;
public AudioStep(String identifier)
{
super(identifier);
setOptional(false);
setStepLayoutClass(AudioStepLayout.class);
}
public int getDuration() {
return mDuration;
}
public void setDuration(int duration) {
mDuration = duration;
}
}
Here you define a custom audio step with one property, mDuration
, to control the length of the audio recording. In the constructor, you call the base class, set the step as required and define the default layout class as AudioStepLayout.class
.
Now to finish AudioStepLayout
. Add the following to the top of the AudioStepLayout
class:
public static final String KEY_AUDIO = "AudioStep.Audio";
private StepCallbacks mStepCallbacks;
private AudioStep mStep;
private StepResult<String> mResult;
private boolean mIsRecordingComplete = false;
private String mFilename;
Add the following methods to AudioStepLayout.java:
// 1
private void setDataToResult()
{
mResult.setResultForIdentifier(KEY_AUDIO, getBase64EncodedAudio());
}
// 2
private String getBase64EncodedAudio()
{
if(mIsRecordingComplete)
{
// 3
File file = new File(mFilename);
try {
byte[] bytes = FileUtils.readAll(file);
String encoded = Base64.encodeToString(bytes, Base64.DEFAULT);
return encoded;
} catch (Exception e) {
return null;
}
}
else
{
return null;
}
}
Taking it comment-by-comment;
- You define
setDataToResult()
to assign the audio data returned fromgetBase64EncodedAudio()
to the step results.KEY_AUDIO
is a unique identifier that distinguishes the audio data from any other results. - You define
getBase64EncodedAudio()
to translate the raw audio to a Base64 string. - If the recording is complete, you open the recording file, use ResearchStack's
FileUtils.readAll()
to read in the data from the file and then useBase64.encodeToString()
to encode the data to a string and return it.
Now you will create a method to handle initialization. Add the following to AudioStepLayout.java:
private void initializeStep()
{
LayoutInflater.from(getContext())
.inflate(R.layout.audio_step_layout, this, true);
TextView title = (TextView) findViewById(R.id.title);
title.setText(mStep.getTitle());
TextView text = (TextView) findViewById(R.id.summary);
text.setText(mStep.getText());
final TextView countdown = (TextView) findViewById(R.id.countdown);
countdown.setText("Seconds remaining: " + Integer.toString(mStep.getDuration()));
final TextView countdown_title = (TextView) findViewById(R.id.countdown_title);
final Button beginButton = (Button) findViewById(R.id.begin_recording);
// TODO: set onClick listener
}
Here you inflate the layout defined earlier and bind the UI elements to variables.
Now replace //TODO: set onClick listener
with the following:
// 1
beginButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 2
mFilename = getContext().getFilesDir().getAbsolutePath();
mFilename += "/camelotaudiorecord.3gp";
final AudioRecorder audioRecorder = new AudioRecorder();
audioRecorder.startRecording(mFilename);
// 3
beginButton.setVisibility(GONE);
countdown_title.setVisibility(View.VISIBLE);
// 4
CountDownTimer Count = new CountDownTimer(mStep.getDuration()*1000, 1000) {
// 5
public void onTick(long millisUntilFinished) {
countdown.setText("Seconds remaining: " + millisUntilFinished / 1000);
}
// 6
public void onFinish() {
mIsRecordingComplete = true;
audioRecorder.stopRecording();
AudioStepLayout.this.setDataToResult();
mStepCallbacks.onSaveStep(StepCallbacks.ACTION_NEXT, mStep, mResult);
}
};
// 7
Count.start();
}
});
There’s quite a few things going on here, so to break it down:
- You create an
onClick
listener on thebeginButton
to start recording when tapped. - You get a filename based on the app’s files directory and use the
AudioRecorder
object to start recording the file. AudioRecorder.java was included in the starter project and provides basic audio recording capabilities. - You hide the Begin button and display the recording countdown timer label.
- You start a
CountDownTimer
based on the requested recording duration. - The
CountDownTimer
callsonTick()
periodically. You’ll use this opportunity to update the countdown timer with the remaining time in seconds. -
CountDownTimer
callsonFinish()
when it is complete. You set themIsRecordingComplete
flag, stop the recording and save the results. You callmStepCallbacks.onSaveStep()
to automatically jump to the next step. - Finally, you start the countdown timer.
Now that initializeStep()
implements the core functionality for AudioStepLayout
, it's time to wire everything up.
Add the following code to initialize()
:
this.mStep = (AudioStep)step;
this.mResult = result == null ? new StepResult<>(step) : result;
initializeStep();
initialize()
is called when the step is about to be displayed. StepResult
will contain the user's previous answer if they have already visited this step, otherwise it is null
. You save the step along with the step result to member variables and then call the previously defined initializeStep()
.
Replace the contents of getLayout()
with the following:
return this;
getLayout
returns the view to be displayed. In this case, it is the current object.
Replace the contents of isBackEventConsumed()
with the following:
setDataToResult();
mStepCallbacks.onSaveStep(StepCallbacks.ACTION_PREV, mStep, mResult);
return false;
isBackEventConsumed
should return false
unless you have special handling in mind when the user tries to back up. This is your chance to save the results. You call onSaveStep()
to notify ResearchStack that it can save the results and perform the ACTION_PREV
action.
Replace the contents of setCallbacks()
method with the following:
this.mStepCallbacks = callbacks;
setCallbacks
provides you with a callback object, and you save it for future use.
Great job! You have completed a custom step, and now you can finish the audio task.
Add the following to displayAudioTask()
in MainActivity.java:
AudioStep audioStep = new AudioStep("audio_step");
audioStep.setTitle("Repeat the following phrase:");
audioStep.setText("The Holy Grail can be found in the Castle of Aaaaaaaaaaah");
audioStep.setDuration(10);
steps.add(audioStep);
You create the new audio step, set its title, text and duration and add it to the steps
list.
Summary Step
The final step display displays a summary. Add the following to displayAudioTask()
:
InstructionStep summaryStep = new InstructionStep("audio_summary_step",
"Right. Off you go!",
"That was easy!");
steps.add(summaryStep);
You create an instruction step and add it to the steps
list.
All that's left is to present the task!
Presenting the Audio Task
Add the following constant to the top of MainActivity
:
private static final int REQUEST_AUDIO = 2;
Add the following to displayAudioTask()
:
OrderedTask task = new OrderedTask("audio_task", steps);
Intent intent = ViewTaskActivity.newIntent(this, task);
startActivityForResult(intent, REQUEST_AUDIO);
You create a new OrderedTask
and pass in the steps
and then launch the activity and set REQUEST_AUDIO
as the request code.
Build and run the app. Tap the Microphone button and run through the task:
Where to Go From Here?
You can download the final project for this tutorial here.
While this tutorial covered three typical tasks you'll find in research studies — informed consent, surveys and active tasks – a real-world study will require some additional work. You may need to store or print results, send results to a server, schedule tasks to run periodically and deal with IRB approval.
For more information, visit the official project page for ResearchStack.
Take some time to look at the Skin module. It builds on top of Backbone and provides several advanced features, including:
- Additional built-in tasks such as ConsentTask and SmartSurveyTask.
- The ability to build out your studies using JSON files and use ResearchKit resources.
- Task scheduling system with notifications.
The MoleMapper Open Source app available on GitHub provides a great example of a fully-featured research app created for both iOS and Android.
Thank you for reading this tutorial, and I hope you have enjoyed it. Please join the discussion below if you have any questions!
Where to Go From Here?
You can download the final project for this tutorial here.
While this tutorial covered three typical tasks you'll find in research studies — informed consent, surveys and active tasks – a real-world study will require some additional work. You may need to store or print results, send results to a server, schedule tasks to run periodically and deal with IRB approval.
For more information, visit the official project page for ResearchStack.
Take some time to look at the Skin module. It builds on top of Backbone and provides several advanced features, including:
- Additional built-in tasks such as ConsentTask and SmartSurveyTask.
- The ability to build out your studies using JSON files and use ResearchKit resources.
- Task scheduling system with notifications.
The MoleMapper Open Source app available on GitHub provides a great example of a fully-featured research app created for both iOS and Android.
Thank you for reading this tutorial, and I hope you have enjoyed it. Please join the discussion below if you have any questions!
List<Step> steps = new ArrayList<>();
InstructionStep instructionStep = new InstructionStep("survey_instruction_step",
"The Questions Three",
"Who would cross the Bridge of Death must answer me these questions three, ere the other side they see.");
steps.add(instructionStep);
TextAnswerFormat format = new TextAnswerFormat(20);
QuestionStep nameStep = new QuestionStep("name", "What is your name?", format);
nameStep.setPlaceholder("Name");
nameStep.setOptional(false);
steps.add(nameStep);
AnswerFormat questionFormat = new ChoiceAnswerFormat(AnswerFormat.ChoiceAnswerStyle
.SingleChoice,
new Choice<>("Create a ResearchKit App", 0),
new Choice<>("Seek the Holy Grail", 1),
new Choice<>("Find a shrubbery", 2));
QuestionStep questionStep = new QuestionStep("quest_step", "What is your quest?", questionFormat);
questionStep.setPlaceholder("Quest");
questionStep.setOptional(false);
steps.add(questionStep);
public class ImageChoiceAnswerFormat extends ChoiceAnswerFormat implements AnswerFormat.QuestionType
{
public ImageChoiceAnswerFormat(ChoiceAnswerStyle answerStyle, Choice... choices) {
super(answerStyle, choices);
}
@Override
public QuestionType getQuestionType()
{
return this;
}
@Override
public Class<?> getStepBodyClass() {
return ImageChoiceQuestionBody.class;
}
}
public class ImageChoiceQuestionBody <T> extends SingleChoiceQuestionBody
{
private Choice[] mChoices;
public ImageChoiceQuestionBody(Step step, StepResult result) {
super(step, result);
QuestionStep questionStep = (QuestionStep)step;
ImageChoiceAnswerFormat format = (ImageChoiceAnswerFormat)questionStep.getAnswerFormat();
mChoices = format.getChoices();
}
@Override
public View getBodyView(int viewType, LayoutInflater inflater, ViewGroup parent)
{
RadioGroup group = (RadioGroup)super.getBodyView(viewType, inflater, parent);
for (int i=0; i<mChoices.length; i++) {
RadioButton button = (RadioButton)group.getChildAt(i);
button.setButtonDrawable(Integer.parseInt(mChoices[i].getValue().toString()));
}
return group;
}
}
AnswerFormat colorAnswerFormat = new ImageChoiceAnswerFormat(AnswerFormat.ChoiceAnswerStyle
.SingleChoice,
new Choice<>("Red", R.drawable.red_selector),
new Choice<>("Orange", R.drawable.orange_selector),
new Choice<>("Yellow", R.drawable.yellow_selector),
new Choice<>("Green", R.drawable.green_selector),
new Choice<>("Blue", R.drawable.blue_selector),
new Choice<>("Purple", R.drawable.purple_selector));
QuestionStep colorStep = new QuestionStep("color_step", "What is your favorite color?",
colorAnswerFormat);
colorStep.setOptional(false);
steps.add(colorStep);
InstructionStep summaryStep = new InstructionStep("survey_summary_step",
"Right. Off you go!",
"That was easy!");
steps.add(summaryStep);
private static final int REQUEST_SURVEY = 1;
OrderedTask task = new OrderedTask("survey_task", steps);
Intent intent = ViewTaskActivity.newIntent(this, task);
startActivityForResult(intent, REQUEST_SURVEY);
List<Step> steps = new ArrayList<>();
InstructionStep instructionStep = new InstructionStep("audio_instruction_step",
"A sentence prompt will be given to you to read.",
"These are the last dying words of Joseph of Aramathea.");
steps.add(instructionStep);
<?xml version="1.0" encoding="utf-8"?>
<merge
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"
>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/image"
android:layout_marginLeft="@dimen/rsb_margin_left"
android:layout_marginRight="@dimen/rsb_margin_right"
android:layout_marginTop="20dp"
android:textColor="?attr/colorAccent"
android:textSize="20sp"
tools:text="@string/lorem_name"
/>
<TextView
android:id="@+id/summary"
style="@style/TextAppearance.AppCompat.Subhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/title"
android:layout_marginLeft="@dimen/rsb_margin_left"
android:layout_marginRight="@dimen/rsb_margin_right"
android:layout_marginTop="36dp"
tools:text="@string/lorem_medium"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/summary"
android:layout_centerHorizontal="true"
android:orientation="vertical"
>
<Button
android:id="@+id/begin_recording"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="36dp"
android:text="Begin"
/>
<TextView
android:id="@+id/countdown_title"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="36dp"
android:text="Recording"
android:visibility="gone"
/>
<TextView
android:id="@+id/countdown"
style="@style/TextAppearance.AppCompat.Subhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="36dp"
tools:text="@string/lorem_medium"
/>
</LinearLayout>
<org.researchstack.backbone.ui.views.SubmitBar
android:id="@+id/submit_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/layout_consent_review_signature_clear"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignTop="@+id/submit_bar"
android:text="@string/rsb_consent_signature_clear"
android:textColor="@color/rsb_submit_bar_negative"
/>
</merge>
public class AudioStepLayout extends RelativeLayout implements StepLayout
{
public AudioStepLayout(Context context)
{
super(context);
}
public AudioStepLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public AudioStepLayout(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}
@Override
public void initialize(Step step, StepResult result) {
}
@Override
public View getLayout() {
return null;
}
@Override
public boolean isBackEventConsumed() {
return false;
}
@Override
public void setCallbacks(StepCallbacks callbacks) {
}
}
public class AudioStep extends Step
{
private int mDuration;
public AudioStep(String identifier)
{
super(identifier);
setOptional(false);
setStepLayoutClass(AudioStepLayout.class);
}
public int getDuration() {
return mDuration;
}
public void setDuration(int duration) {
mDuration = duration;
}
}
public static final String KEY_AUDIO = "AudioStep.Audio";
private StepCallbacks mStepCallbacks;
private AudioStep mStep;
private StepResult<String> mResult;
private boolean mIsRecordingComplete = false;
private String mFilename;
// 1
private void setDataToResult()
{
mResult.setResultForIdentifier(KEY_AUDIO, getBase64EncodedAudio());
}
// 2
private String getBase64EncodedAudio()
{
if(mIsRecordingComplete)
{
// 3
File file = new File(mFilename);
try {
byte[] bytes = FileUtils.readAll(file);
String encoded = Base64.encodeToString(bytes, Base64.DEFAULT);
return encoded;
} catch (Exception e) {
return null;
}
}
else
{
return null;
}
}
private void initializeStep()
{
LayoutInflater.from(getContext())
.inflate(R.layout.audio_step_layout, this, true);
TextView title = (TextView) findViewById(R.id.title);
title.setText(mStep.getTitle());
TextView text = (TextView) findViewById(R.id.summary);
text.setText(mStep.getText());
final TextView countdown = (TextView) findViewById(R.id.countdown);
countdown.setText("Seconds remaining: " + Integer.toString(mStep.getDuration()));
final TextView countdown_title = (TextView) findViewById(R.id.countdown_title);
final Button beginButton = (Button) findViewById(R.id.begin_recording);
// TODO: set onClick listener
}
// 1
beginButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 2
mFilename = getContext().getFilesDir().getAbsolutePath();
mFilename += "/camelotaudiorecord.3gp";
final AudioRecorder audioRecorder = new AudioRecorder();
audioRecorder.startRecording(mFilename);
// 3
beginButton.setVisibility(GONE);
countdown_title.setVisibility(View.VISIBLE);
// 4
CountDownTimer Count = new CountDownTimer(mStep.getDuration()*1000, 1000) {
// 5
public void onTick(long millisUntilFinished) {
countdown.setText("Seconds remaining: " + millisUntilFinished / 1000);
}
// 6
public void onFinish() {
mIsRecordingComplete = true;
audioRecorder.stopRecording();
AudioStepLayout.this.setDataToResult();
mStepCallbacks.onSaveStep(StepCallbacks.ACTION_NEXT, mStep, mResult);
}
};
// 7
Count.start();
}
});
this.mStep = (AudioStep)step;
this.mResult = result == null ? new StepResult<>(step) : result;
initializeStep();
return this;
setDataToResult();
mStepCallbacks.onSaveStep(StepCallbacks.ACTION_PREV, mStep, mResult);
return false;
this.mStepCallbacks = callbacks;
AudioStep audioStep = new AudioStep("audio_step");
audioStep.setTitle("Repeat the following phrase:");
audioStep.setText("The Holy Grail can be found in the Castle of Aaaaaaaaaaah");
audioStep.setDuration(10);
steps.add(audioStep);
InstructionStep summaryStep = new InstructionStep("audio_summary_step",
"Right. Off you go!",
"That was easy!");
steps.add(summaryStep);
private static final int REQUEST_AUDIO = 2;
OrderedTask task = new OrderedTask("audio_task", steps);
Intent intent = ViewTaskActivity.newIntent(this, task);
startActivityForResult(intent, REQUEST_AUDIO);