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.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 2 of this article. Click here to view the first page.

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.

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.

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:

  1. You call createConsentDocument() to create the ConsentDocument.
  2. You pass document into createConsentSteps() to create the consent steps.
  3. You create a new OrderedTask, passing in a unique identifier along with the consent steps.
  4. 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.

AudioStep 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 the StepLayout interface.
  • AudioStep: A class that extends the base Step 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.
  1. You define setDataToResult() to assign the audio data returned from getBase64EncodedAudio() to the step results. KEY_AUDIO is a unique identifier that distinguishes the audio data from any other results.
  2. You define getBase64EncodedAudio() to translate the raw audio to a Base64 string.
  3. 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 use Base64.encodeToString() to encode the data to a string and return it.
  1. You create an onClick listener on the beginButton to start recording when tapped.
  2. 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.
  3. You hide the Begin button and display the recording countdown timer label.
  4. You start a CountDownTimer based on the requested recording duration.
  5. The CountDownTimer calls onTick() periodically. You’ll use this opportunity to update the countdown timer with the remaining time in seconds.
  6. CountDownTimer calls onFinish() when it is complete. You set the mIsRecordingComplete flag, stop the recording and save the results. You call mStepCallbacks.onSaveStep() to automatically jump to the next step.
  7. Finally, you start the countdown timer.
Consent 1 Consent 2 Consent 3
Consent 4 Consent 5 Consent 6
Consent 7 Consent 8 Consent 9
Consent 10 Consent 11
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);
Survey 1 Survey 2 Survey 3
Survey 4 Survey 5
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);
Audio 1 Audio 2 Audio 3
Survey 1 Survey 2 Survey 3
Survey 4 Survey 5
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);
Audio 1 Audio 2 Audio 3
Audio 1 Audio 2 Audio 3
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);
Tom Blankenship

Contributors

Tom Blankenship

Author

A N Sreekumar

Tech Editor

Chris Belanger

Editor

Matt Luedke

Team Lead

Over 300 content creators. Join our team.