diff --git a/.travis.yml b/.travis.yml index e8e27f09b..0ff58ab2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: android # Required to run retrolambda jdk: - oraclejdk8 - + android: components: # Uncomment the lines below if you want to @@ -14,10 +14,14 @@ android: # The BuildTools version used by your project # per travis-ci issue #5036, add '-tools' to use build-tools-23.0.2 - tools - - build-tools-23.0.2 + - build-tools-28.0.2 + - build-tools-28.0.3 + - build-tools-27.0.3 + + # The SDK version used to compile your project - - android-23 + - android-28 # Additional components go here. - extra-android-support @@ -31,12 +35,7 @@ before_install: - export JAVA8_HOME=/usr/lib/jvm/java-8-oracle - export JAVA_HOME=$JAVA8_HOME -before_script: - # Tests crash if a local.properties file isn't present; this file is not kept - # under version control. Create an empty local.properties for testing. - - touch local.properties - -script: ./gradlew test +script: ./gradlew build # Uncomment line below to add post-test scripts. # after_script: diff --git a/CONTRIBUTORS b/CONTRIBUTORS index d5b1c497e..60002a53f 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -34,15 +34,13 @@ Then add the following entry to your dependencies in your app build.gradle: dependencies { ... compile 'org.researchstack:backbone:VERSION' - // or (if using Skin, you don't need Backbone since it is included) - compile 'org.researchstack:skin:VERSION' ... } ``` ## Running tests -Tests are located in the /backbone/src/test or /skin/src/test folder. Run the tests in Android Studio by right clicking on 'backbone' or 'skin' and clicking "Run 'All Tests'". +Tests are located in the /backbone/src/test folder. Run the tests in Android Studio by right clicking on 'backbone' or 'skin' and clicking "Run 'All Tests'". ## Code Style diff --git a/README.md b/README.md index a2bed7449..c74449188 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ Documentation is written and maintained using [Javadoc](http://www.oracle.com/te Add one or both to your app/build.gradle: ```groovy compile 'org.researchstack:backbone:1.1.1' -compile 'org.researchstack:skin:1.1.1' ``` You may also need to add the following source repos to your project's build.gradle: diff --git a/backbone/build.gradle b/backbone/build.gradle index c4cbecdf9..90c51f31f 100644 --- a/backbone/build.gradle +++ b/backbone/build.gradle @@ -1,20 +1,21 @@ apply plugin: 'com.android.library' -apply plugin: 'me.tatarka.retrolambda' -apply plugin: 'com.neenbedankt.android-apt' apply plugin: 'com.github.dcendents.android-maven' apply plugin: 'com.jfrog.bintray' +apply plugin: 'maven-publish' -version = '1.1.2' +version = '2.0.0-SNAPSHOT' android { - compileSdkVersion 23 - buildToolsVersion "23.0.2" + compileSdkVersion 28 defaultConfig { - minSdkVersion 16 - targetSdkVersion 23 + minSdkVersion 19 + targetSdkVersion 28 versionCode 6 versionName version + vectorDrawables.useSupportLibrary = true + + multiDexEnabled true } buildTypes { release { @@ -36,8 +37,13 @@ android { resourcePrefix 'rsb_' } +// Reading in data from local.properties is used here to grab key/value pairs used below in ext +// Originally, it read the local.properties, but this file should never be committed to vcs, +// So check if it exists first, because some projects may not care about it Properties properties = new Properties() -properties.load(project.rootProject.file('local.properties').newDataInputStream()) +if (project.rootProject.file('local.properties').exists()) { + properties.load(project.rootProject.file('local.properties').newDataInputStream()) +} ext { bintrayRepo = 'ResearchStack' @@ -54,6 +60,8 @@ ext { libraryVersion = version + // This grabs the key/value pairs from local.properties and assigns them to variables that + // can be used in gradle, and specifically, the mavenInstaller below userOrgName = 'researchstack' developerId = properties.getProperty("bintray.user") developerName = properties.getProperty("bintray.developerName") @@ -65,27 +73,57 @@ ext { } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:23.2.1' - compile 'com.android.support:cardview-v7:23.2.1' - compile 'com.android.support:preference-v14:23.2.1' - compile 'com.android.support:design:23.2.1' - compile 'com.google.code.gson:gson:2.4' - compile 'io.reactivex:rxjava:1.1.3' - compile 'io.reactivex:rxandroid:1.1.0' - compile 'com.jakewharton.rxbinding:rxbinding:0.2.0' - compile 'com.jakewharton.rxbinding:rxbinding-support-v4:0.2.0' - compile 'com.jakewharton.rxbinding:rxbinding-appcompat-v7:0.2.0' - compile 'com.jakewharton.rxbinding:rxbinding-design:0.2.0' - compile 'com.github.PhilJay:MPAndroidChart:v2.2.3' - - compile 'com.scottyab:aes-crypto:0.0.3' - compile 'co.touchlab.squeaky:squeaky-query:0.4.0.0' - apt 'co.touchlab.squeaky:squeaky-processor:0.4.0.0' - compile 'net.zetetic:android-database-sqlcipher:3.5.4@aar' - testCompile 'junit:junit:4.12' - testCompile 'org.robolectric:robolectric:3.0' - testCompile 'org.mockito:mockito-core:1.10.19' + implementation fileTree(dir: 'libs', include: ['*.jar']) + + // These are all support libraries that should be updated when Google releases new ones + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.legacy:legacy-preference-v14:1.0.0' + implementation 'androidx.annotation:annotation:1.0.0-alpha1' + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + + implementation 'androidx.multidex:multidex:2.0.0' + + api 'io.reactivex:rxjava:1.3.0' + implementation 'nl.littlerobots.rxlint:rxlint:1.6.1' + implementation 'com.google.code.gson:gson:2.8.2' + + implementation 'com.cronutils:cron-utils:3.1.2', { + exclude group: 'joda-time', module: 'joda-time' // this can cause dex errors due to duplicate joda times + } + api 'net.danlew:android.joda:2.9.9.4' // android specific joda time, jre version causes perf problems + testImplementation 'joda-time:joda-time:2.9.9' + + implementation 'com.squareup.retrofit2:retrofit:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.3.0' + implementation 'com.squareup.retrofit2:adapter-rxjava:2.3.0' + implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1' + + implementation 'io.reactivex:rxandroid:1.2.1' + + implementation 'com.jakewharton.rxbinding:rxbinding:0.4.0' + implementation 'com.jakewharton.rxbinding:rxbinding-support-v4:0.4.0' + implementation 'com.jakewharton.rxbinding:rxbinding-appcompat-v7:0.4.0' + implementation 'com.jakewharton.rxbinding:rxbinding-design:0.4.0' + + // Used to display UploadData and study data in various chart formats + api 'com.github.PhilJay:MPAndroidChart:v2.2.3' + + implementation 'com.scottyab:aes-crypto:0.0.3' + api 'co.touchlab.squeaky:squeaky-query:0.4.0.0' + annotationProcessor 'co.touchlab.squeaky:squeaky-processor:0.4.0.0' + api 'net.zetetic:android-database-sqlcipher:3.5.4@aar' + + // Libraries to help with unit testing + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.8.9' + testImplementation 'org.powermock:powermock-api-mockito2:1.7.1' + testImplementation 'org.powermock:powermock-module-junit4-rule-agent:1.7.1' + testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.1' + testImplementation 'org.powermock:powermock-module-junit4:1.7.1' + + testImplementation 'org.robolectric:robolectric:3.5' } group = publishedGroupId // Maven Group ID for the artifact @@ -137,6 +175,9 @@ if (project.hasProperty("android")) { // Android libraries task javadoc(type: Javadoc) { failOnError false source = android.sourceSets.main.java.srcDirs + // Exclude generated files + exclude '**/BuildConfig.java' + exclude '**/R.java' } afterEvaluate { diff --git a/backbone/lint.xml b/backbone/lint.xml new file mode 100644 index 000000000..c02b87092 --- /dev/null +++ b/backbone/lint.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backbone/src/main/AndroidManifest.xml b/backbone/src/main/AndroidManifest.xml index 228116260..4055b5fc0 100644 --- a/backbone/src/main/AndroidManifest.xml +++ b/backbone/src/main/AndroidManifest.xml @@ -1 +1,20 @@ - + + + + + + + + + + + + + + + + + + + diff --git a/skin/src/main/java/org/researchstack/skin/ActionItem.java b/backbone/src/main/java/org/researchstack/backbone/ActionItem.java similarity index 98% rename from skin/src/main/java/org/researchstack/skin/ActionItem.java rename to backbone/src/main/java/org/researchstack/backbone/ActionItem.java index 0b9834361..bddc07e3e 100644 --- a/skin/src/main/java/org/researchstack/skin/ActionItem.java +++ b/backbone/src/main/java/org/researchstack/backbone/ActionItem.java @@ -1,4 +1,4 @@ -package org.researchstack.skin; +package org.researchstack.backbone; import android.view.MenuItem; diff --git a/skin/src/main/java/org/researchstack/skin/AppPrefs.java b/backbone/src/main/java/org/researchstack/backbone/AppPrefs.java similarity index 65% rename from skin/src/main/java/org/researchstack/skin/AppPrefs.java rename to backbone/src/main/java/org/researchstack/backbone/AppPrefs.java index 5d1f888dd..d2ea4bd5d 100644 --- a/skin/src/main/java/org/researchstack/skin/AppPrefs.java +++ b/backbone/src/main/java/org/researchstack/backbone/AppPrefs.java @@ -1,17 +1,16 @@ -package org.researchstack.skin; - +package org.researchstack.backbone; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; -import org.researchstack.skin.ui.fragment.SettingsFragment; +import org.researchstack.backbone.ui.fragment.SettingsFragment; public class AppPrefs { //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Statics //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= private static final String KEY_ONBOARDING_COMPLETE = "settings_onboarding_complete"; - private static final String KEY_ONBOARDING_SKIPPED = "settings_onboarding_skipped"; + private static final String KEY_ONBOARDING_SKIPPED = "settings_onboarding_skipped"; private static AppPrefs instance; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -19,23 +18,42 @@ public class AppPrefs { //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= private final SharedPreferences prefs; - AppPrefs(Context context) { + AppPrefs(Context context) + { prefs = PreferenceManager.getDefaultSharedPreferences(context); } - public static synchronized AppPrefs getInstance(Context context) { - if (instance == null) { + public static void init(Context context) { + instance = new AppPrefs(context); + } + + @Deprecated + public static synchronized AppPrefs getInstance(Context context) + { + if(instance == null) + { instance = new AppPrefs(context); } return instance; } + public static AppPrefs getInstance() + { + if(instance == null) + { + throw new RuntimeException( + "AppPrefs instance is null. Make sure it is initialized in ResearchStack before calling."); + } + return instance; + } + /** * If auto lock is disabled, default the time to a year. * * @return time in milliseconds */ - public long getAutoLockTime() { + public long getAutoLockTime() + { boolean isAutoLocked = prefs.getBoolean(SettingsFragment.KEY_AUTO_LOCK_ENABLED, true); String time = prefs.getString(SettingsFragment.KEY_AUTO_LOCK_TIME, "1"); @@ -43,36 +61,48 @@ public long getAutoLockTime() { return autoLockMins * 60 * 1000; } - public void setSkippedOnboarding(boolean skipped) { + public void setSkippedOnboarding(boolean skipped) + { prefs.edit().putBoolean(KEY_ONBOARDING_SKIPPED, skipped).apply(); } - public boolean skippedOnboarding() { + public boolean skippedOnboarding() + { return prefs.getBoolean(KEY_ONBOARDING_SKIPPED, false); } - /** - * @return true if onboading is complete - */ - public boolean isOnboardingComplete() { - return prefs.getBoolean(KEY_ONBOARDING_COMPLETE, false); - } - /** * Method to set if onboarding is completed. This preference is used within Skin.SplashActivity * to see if we should show onboarding or proceed to MainActivity. * * @param complete true if onboading is complete */ - public void setOnboardingComplete(boolean complete) { + public void setOnboardingComplete(boolean complete) + { prefs.edit().putBoolean(KEY_ONBOARDING_COMPLETE, complete).apply(); } - public void setTaskReminderComplete(boolean enabled) { + /** + * @return true if onboading is complete + */ + public boolean isOnboardingComplete() + { + return prefs.getBoolean(KEY_ONBOARDING_COMPLETE, false); + } + + public void setTaskReminderComplete(boolean enabled) + { prefs.edit().putBoolean(SettingsFragment.KEY_REMINDERS, enabled).apply(); } - public boolean isTaskReminderEnabled() { + public boolean isTaskReminderEnabled() + { return prefs.getBoolean(SettingsFragment.KEY_REMINDERS, false); } + + public void clear() { + if (prefs != null) { + prefs.edit().clear().apply(); + } + } } \ No newline at end of file diff --git a/skin/src/main/java/org/researchstack/skin/DataProvider.java b/backbone/src/main/java/org/researchstack/backbone/DataProvider.java similarity index 50% rename from skin/src/main/java/org/researchstack/skin/DataProvider.java rename to backbone/src/main/java/org/researchstack/backbone/DataProvider.java index 3cbfd5a58..75936d254 100644 --- a/skin/src/main/java/org/researchstack/skin/DataProvider.java +++ b/backbone/src/main/java/org/researchstack/backbone/DataProvider.java @@ -1,28 +1,27 @@ -package org.researchstack.skin; +package org.researchstack.backbone; import android.app.Application; import android.content.Context; +import androidx.annotation.NonNull; +import org.researchstack.backbone.model.ConsentSignatureBody; +import org.researchstack.backbone.model.SchedulesAndTasksModel; +import org.researchstack.backbone.model.User; import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.storage.file.FileAccess; import org.researchstack.backbone.task.Task; -import org.researchstack.skin.model.SchedulesAndTasksModel; -import org.researchstack.skin.model.User; -import org.researchstack.skin.ui.EmailVerificationActivity; -import org.researchstack.skin.ui.MainActivity; -import org.researchstack.skin.ui.SplashActivity; -import org.researchstack.skin.ui.fragment.SettingsFragment; -import org.researchstack.skin.ui.layout.SignUpStepLayout; import rx.Observable; +import rx.Single; /** * Class used to as a buffer between the network layer and UI layer. The implementation allows the * framework to be backend-agnostic */ public abstract class DataProvider { - public final static String ERROR_NOT_AUTHENTICATED = "ERROR_NOT_AUTHENTICATED"; - public final static String ERROR_CONSENT_REQUIRED = "ERROR_CONSENT_REQUIRED"; + public static final String ERROR_NOT_AUTHENTICATED = "ERROR_NOT_AUTHENTICATED"; + public static final String ERROR_CONSENT_REQUIRED = "ERROR_CONSENT_REQUIRED"; + public static final String ERROR_APP_UPGRADE_REQUIRED = "ERROR_APP_UPGRADE_REQUIRED"; private static DataProvider instance; @@ -57,7 +56,7 @@ public static void init(DataProvider instance) { } /** - * Called in {@link SplashActivity} to initialize the state of the app. The state includes if + * Called to initialize the state of the app. The state includes if * the user is not signed in/up, not consented, etc.. * * @param context android context @@ -67,8 +66,11 @@ public static void init(DataProvider instance) { public abstract Observable initialize(Context context); /** - * Called in {@link SignUpStepLayout} to sign the user up to the backend service + * Called to sign the user up to the backend service * + * @param email the user's email + * @param username the user's username + * @param password the user's password * @param context android context * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} * returning true if signUp was successful @@ -76,16 +78,68 @@ public static void init(DataProvider instance) { public abstract Observable signUp(Context context, String email, String username, String password); /** - * Called in {@link SignUpStepLayout} to sign the user in to the backend service + * Called to sign the user in to the backend service * - * @param context android context + * @param context android context + * @param username the user's username + * @param password the user's password * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} * returning true if signIn was successful */ - public abstract Observable signIn(Context context, String username, String password); + public abstract Observable signIn(Context context, String username, String + password); + + /** + * Request for a link to sign in user + * + * @param username the user's username + * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} + * returning true if request for sign in link was successful + */ + public Observable requestSignInLink(String username) { + return Observable.just(new DataResponse(false, "Not implemented")); + } + + /** + * Request for an SMS for the user to login or sign up + * + * @param regionCode the user's region code + * @param phoneNumber the user's phone number + * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} + * returning true if request for phone sign in was successful + */ + public Observable requestPhoneSignIn(String regionCode, String phoneNumber) { + return Observable.just(new DataResponse(false, "Not implemented")); + } + /** - * Currently not used within the framework + * Sign in with email and token. This is used in conjunction with requestSignInLink. The + * link should be intercepted by the app and the sign in token extracted. + * + * @param username the user's username + * @param token sign in token + * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} + * returning true if request for sign in was successful + */ + public Observable signInWithEmailAndToken(String username, String token) { + return Observable.just(new DataResponse(false, "Not implemented")); + } + + /** + * Called to sign the user in using the user's external ID. + * + * @param context android context + * @param externalId the user's external ID + * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} + * returning true if signIn was successful + */ + public abstract Observable signInWithExternalId(Context context, String + externalId); + + /** + * Sign out the user. This will possibly involve a call to the server, + * and also clear all relevant local data that relates to the User, User Session, or Consent * * @param context android context * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} @@ -94,15 +148,29 @@ public static void init(DataProvider instance) { public abstract Observable signOut(Context context); /** - * Called in {@link EmailVerificationActivity} to alert the backend to resend a vertification + * Called to alert the backend to resend a vertification * email * + * @param email user's email * @param context android context * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} * returning true if signIn was successful */ public abstract Observable resendEmailVerification(Context context, String email); + /** + * Called to verify the user's email address + * Behind the scenes this calls signIn with securely stored email and passed in param password + * Afterwords, it the implementation must also upload the consent doc that was + * previously stored using saveLocalConsent + * + * @param context android context + * @param password the user's password + * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} + * returning true if verifyEmail was successful + */ + public abstract Observable verifyEmail(Context context, String password); + /** * Returns true if user is currently signed up * @@ -124,13 +192,23 @@ public static void init(DataProvider instance) { * * @param context android context * @return true if user is currently consented + * @deprecated use {@link #isConsented()} instead + */ + @Deprecated + public boolean isConsented(Context context) { + return isConsented(); + } + + /** + * @return true if user is currently consented to the study */ - public abstract boolean isConsented(Context context); + public abstract boolean isConsented(); /** - * Called in {@link SettingsFragment} to alert the backend that the user wants to withdraw from + * Called to alert the backend that the user wants to withdraw from * the study * + * @param reason the reason for withdrawal, can be any string * @param context android context * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} * returning true if withdrawl was successful @@ -139,22 +217,56 @@ public static void init(DataProvider instance) { /** * This method is responsible in uploading the user consent information (e.g. Name, Birthdate, - * Signature) to the backend + * Signature) to the backend. Usually, this is done by looking into the TaskResult + * object and filling up the ConsentSignature and then calling the method below this + * with the signature parameter * - * @param context android context + * @param context android context + * @param consentResult the TaskResult map containing hard-coded key/value data + * @deprecated use {@link #uploadConsent(Context, ConsentSignatureBody)} instead */ + @Deprecated public abstract void uploadConsent(Context context, TaskResult consentResult); + /** + * This method is responsible in uploading the user consent information (e.g. Name, Birthdate, + * Signature) to the backend. + * + * @param context android context + * @param signature Valid ConsentSignature object + * @return Observable of the result of the method, with {@link DataResponse#isSuccess()} if successful + */ + public abstract Observable uploadConsent(Context context, ConsentSignatureBody signature); + + /** + * Loads consent from local storage + * @param context android context + * @return null if no call has been made to saveLocalConsent, otherwise + * it will return the ConsentSignatureBody from the call to saveLocalConsent + */ + public abstract ConsentSignatureBody loadLocalConsent(Context context); + /** * This method is responsible in saving user consent information (e.g. Name, Birthdate, * Signature) locally. *

* Please use {@link FileAccess} class to encrypt user information when saving. * + * @param consentResult the TaskResult map containing hard-coded key/value data * @param context android context */ + @Deprecated // use saveLocalConsent(Context context, ConsentSignatureBody signature) instead public abstract void saveConsent(Context context, TaskResult consentResult); + /** + * This method is responsible in saving user consent information (e.g. Name, Birthdate, + * Signature) locally for use after the user successfully signs in + * + * @param context android context + * @param consentSignatureBody object which will be saved + */ + public abstract void saveLocalConsent(Context context, ConsentSignatureBody consentSignatureBody); + /** * Returns the user object that contains any sort of information. This information can be * collected in the inital survey and sorted using this object @@ -164,10 +276,17 @@ public static void init(DataProvider instance) { */ public abstract User getUser(Context context); + /** + * Saves the user object + * @param context android context + * @param user User object to save + */ + public abstract void setUser(Context context, User user); + /** * Gets the current sharing scope of the user. *

- * This scope can be:

  • sponsors_and_partners
  • all_quali5dfied_researchers
  • + * This scope can be:
    • sponsors_and_partners
    • all_quali5dfied_researchers
    * * @param context android context * @return the sharing scope of the user @@ -200,30 +319,28 @@ public static void init(DataProvider instance) { public abstract void uploadTaskResult(Context context, TaskResult taskResult); /** - * Loads the SchedulesAndTasksModel object, this should be used in conjunction with the {@link - * ResourceManager} if inflating the TaskAndSchedules object from assets folder + * Loads the SchedulesAndTasksModel object * * @param context android context * @return a SchedulesAndTasksModel object */ - public abstract SchedulesAndTasksModel loadTasksAndSchedules(Context context); + @NonNull + public abstract Single loadTasksAndSchedules(Context context); /** - * Loads a Task object, this should be used in conjunction with the {@link ResourceManager} if - * inflating a Task from assets folder + * Loads a Task object * * @param context android context * @param task the TaskScheduleModel model * @return a Task object with defined sub-steps */ - public abstract Task loadTask(Context context, SchedulesAndTasksModel.TaskScheduleModel task); + @NonNull + public abstract Single loadTask(Context context, SchedulesAndTasksModel.TaskScheduleModel task); /** * This initial task may include profile items such as height and weight that may need to be * processed differently than a normal task result. *

    - * This task is presented to the user when initially entering the {@link MainActivity} - * * @param context android context * @param taskResult initial TaskResult object to process */ @@ -239,4 +356,9 @@ public static void init(DataProvider instance) { * returning true if forgitpassword request was successful */ public abstract Observable forgotPassword(Context context, String email); + + /** + * @return the Study ID for the study + */ + public abstract String getStudyId(); } diff --git a/skin/src/main/java/org/researchstack/skin/DataResponse.java b/backbone/src/main/java/org/researchstack/backbone/DataResponse.java similarity index 93% rename from skin/src/main/java/org/researchstack/skin/DataResponse.java rename to backbone/src/main/java/org/researchstack/backbone/DataResponse.java index 05efef2d4..617bc1695 100644 --- a/skin/src/main/java/org/researchstack/skin/DataResponse.java +++ b/backbone/src/main/java/org/researchstack/backbone/DataResponse.java @@ -1,4 +1,4 @@ -package org.researchstack.skin; +package org.researchstack.backbone; public class DataResponse { private String message; diff --git a/skin/src/main/java/org/researchstack/skin/PermissionRequestManager.java b/backbone/src/main/java/org/researchstack/backbone/PermissionRequestManager.java similarity index 97% rename from skin/src/main/java/org/researchstack/skin/PermissionRequestManager.java rename to backbone/src/main/java/org/researchstack/backbone/PermissionRequestManager.java index 23c748e4e..d3a74d5a2 100644 --- a/skin/src/main/java/org/researchstack/skin/PermissionRequestManager.java +++ b/backbone/src/main/java/org/researchstack/backbone/PermissionRequestManager.java @@ -1,15 +1,15 @@ -package org.researchstack.skin; +package org.researchstack.backbone; import android.Manifest; import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.Intent; -import android.support.annotation.DrawableRes; -import android.support.annotation.StringRes; -import android.support.v4.content.ContextCompat; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; -import org.researchstack.skin.ui.layout.PermissionStepLayout; +import org.researchstack.backbone.ui.step.layout.PermissionStepLayout; import java.util.ArrayList; import java.util.Collection; diff --git a/skin/src/main/java/org/researchstack/skin/ResearchStack.java b/backbone/src/main/java/org/researchstack/backbone/ResearchStack.java similarity index 86% rename from skin/src/main/java/org/researchstack/skin/ResearchStack.java rename to backbone/src/main/java/org/researchstack/backbone/ResearchStack.java index 84b3500ea..da18998bb 100644 --- a/skin/src/main/java/org/researchstack/skin/ResearchStack.java +++ b/backbone/src/main/java/org/researchstack/backbone/ResearchStack.java @@ -1,14 +1,14 @@ -package org.researchstack.skin; +package org.researchstack.backbone; import android.app.Application; import android.content.Context; -import org.researchstack.backbone.StorageAccess; import org.researchstack.backbone.storage.database.AppDatabase; import org.researchstack.backbone.storage.file.EncryptionProvider; import org.researchstack.backbone.storage.file.FileAccess; import org.researchstack.backbone.storage.file.PinCodeConfig; -import org.researchstack.skin.notification.NotificationConfig; +import org.researchstack.backbone.notification.NotificationConfig; +import org.researchstack.backbone.onboarding.OnboardingManager; /** * Research stack is a singleton which controls all the major components of the ResearchStack @@ -47,6 +47,8 @@ public synchronized static ResearchStack getInstance() { public static void init(Context context, ResearchStack concreteResearchStack) { instance = concreteResearchStack; + AppPrefs.init(context); + ResourceManager.init(concreteResearchStack.createResourceManagerImplementation(context)); UiManager.init(concreteResearchStack.createUiManagerImplementation(context)); @@ -65,6 +67,10 @@ public static void init(Context context, ResearchStack concreteResearchStack) { PermissionRequestManager.init(concreteResearchStack.createPermissionRequestManagerImplementation( context)); + + // OnboardingManager is not a singleton, so that in the future, + // there can potentially be multiple onboarding flows depending + instance.createOnboardingManager(context); } /** @@ -91,6 +97,17 @@ public static void init(Context context, ResearchStack concreteResearchStack) { */ protected abstract EncryptionProvider getEncryptionProvider(Context context); + /** + * @return concrete implementation of {@link OnboardingManager} + */ + public abstract OnboardingManager getOnboardingManager(); + + /** + * Called within {@link #init(Context, ResearchStack)} to initialize {@link OnboardingManager} implementation + * @param context can be activity or application context, only used for resources + */ + public abstract void createOnboardingManager(Context context); + /** * Called within {@link #init(Context, ResearchStack)} to initialize {@link FileAccess} implementation * @@ -128,6 +145,8 @@ public static void init(Context context, ResearchStack concreteResearchStack) { * * @param context android Contenxt * @return concrete implementation of {@link TaskProvider} + * + * @deprecated use org.researchstack.backbone.onboarding.OnboardingManager instead */ protected abstract TaskProvider createTaskProviderImplementation(Context context); @@ -146,5 +165,4 @@ public static void init(Context context, ResearchStack concreteResearchStack) { * @return concrete implementation of {@link PermissionRequestManager} */ protected abstract PermissionRequestManager createPermissionRequestManagerImplementation(Context context); - } diff --git a/backbone/src/main/java/org/researchstack/backbone/ResourceManager.java b/backbone/src/main/java/org/researchstack/backbone/ResourceManager.java new file mode 100644 index 000000000..d18b63843 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ResourceManager.java @@ -0,0 +1,196 @@ +package org.researchstack.backbone; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class is responsible for returning paths of resources defined in the assets folder. This + * class has more structure and expects certain assets to be defined for use within the framework. + */ +public abstract class ResourceManager extends ResourcePathManager { + + /** + * The resource map is a way to inject your own resources into the ResourceManager + */ + private Map resourceMap = new HashMap<>(); + + /** + * Returns a singleton static instance of the ResourceManager class + * + * @return A singleton static instance of the ResourceManager class + */ + public static ResourceManager getInstance() { + return (ResourceManager) ResourcePathManager.getInstance(); + } + + /** + * Returns a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * StudyOverview file. + * + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * StudyOverview file. + */ + public abstract Resource getStudyOverview(); + + /** + * Returns a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing an + * HTML version of the Consent file. + * + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing an + * HTML version of the Consent file. + */ + public abstract Resource getConsentHtml(); + + /** + * This is currently unused but will be when share-pdf feature is implemented + * + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing an PDF + * version of the Consent file. + */ + public abstract Resource getConsentPDF(); + + /** + * The consent section differs from ResearchKit™ as ResearchStack includes extra + * documentProperties along with support for quiz steps + * + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing a + * consent section file + */ + public abstract Resource getConsentSections(); + + /** + * Returns a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing a + * learn-sections file + * + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing a + * learn-sections file + */ + public abstract Resource getLearnSections(); + + /** + * Returns a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * privacy policy + * + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * privacy policy + */ + public abstract Resource getPrivacyPolicy(); + + /** + * Returns a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * software notices + * + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * software notices + */ + public abstract Resource getSoftwareNotices(); + + /** + * Returns a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * tasks and schedules file + * + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * tasks and schedules file + */ + public abstract Resource getTasksAndSchedules(); + + /** + * Returns a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing a + * individual task file + * + * @param taskFileName the filename for th task + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing a + * individual task file + */ + public abstract Resource getTask(String taskFileName); + + /** + * Returns a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * inclusion criteria + * + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * inclusion criteria + */ + public abstract Resource getInclusionCriteria(); + + /** + * Returns a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * Onboarding sections + * + * @return a {@link org.researchstack.backbone.ResourcePathManager.Resource} representing the + * Onboarding sections + */ + public abstract Resource getOnboardingManager(); + + /** + * @param resourceName the name of the resource to find + * @return a Resource this manager provides, null if none exists + */ + public Resource getResource(String resourceName) { + Resource resource = resourceMap.get(resourceName); + // If we use the abstract methods (backwards compatibility) + // the resource may exist in one of the methods + if (resource == null) { + List resourceList = reflectToCreateResourceList(); + for (Resource resourceInList : resourceList) { + if (resourceInList.getName().equals(resourceName)) { + return resourceInList; + } + } + } + return resource; + } + + /** + * Uses reflection to find all methods that return "Resource" types and builds it into a list + * @return a list of Resource objects + */ + private List reflectToCreateResourceList() { + List resourceList = new ArrayList<>(); + // Look at all methods of ResourceManager to find ones that return type "Resource" + Method[] resourceMethods = ResourceManager.class.getDeclaredMethods(); + for (Method method : resourceMethods) { + if (method.getReturnType().equals(ResourcePathManager.Resource.class)) { + String errorMessage = null; + try { + Object resourceObj = method.invoke(ResourceManager.getInstance()); + if (resourceObj instanceof ResourcePathManager.Resource) { + resourceList.add((ResourcePathManager.Resource)resourceObj); + } + } catch (Exception e) { + errorMessage = e.getMessage(); + } + if (errorMessage != null) { + throw new IllegalStateException("You must define a method in ResourceManager " + + "that returns a Resource for the resourceName or add it to the" + + "ResourceManager with addResource()"); + } + } + } + return resourceList; + } + + /** + * @param resourceName the name of the resource that will be + * @param resource the full path, type, and name of the resource + * @return an instance of this class to chain adding resources + */ + public ResourceManager addResource(String resourceName, Resource resource) { + resourceMap.put(resourceName, resource); + return this; + } + + /** + * @return a duplicate copy of the ResourceManager's custom resource map + */ + public Map getResourceMap() { + return new HashMap<>(resourceMap); + } + + public void removeResource(String name) { + resourceMap.remove(name); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ResourcePathManager.java b/backbone/src/main/java/org/researchstack/backbone/ResourcePathManager.java index 3943deef6..e54935b5b 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ResourcePathManager.java +++ b/backbone/src/main/java/org/researchstack/backbone/ResourcePathManager.java @@ -28,6 +28,8 @@ * ResearchKit™ applications */ public abstract class ResourcePathManager { + // TODO: if we are going to go with a singleton instance of Gson, + // TODO: we need a simple way to inject type adapters into it - MDP 1/13/2017 private static Gson gson = new GsonBuilder().setDateFormat("MMM yyyy").create(); private static ResourcePathManager instance; @@ -64,7 +66,11 @@ public static ResourcePathManager getInstance() { * @return String representation of the file */ public static String getResourceAsString(Context context, String filePath) { - return new String(getResourceAsBytes(context, filePath), Charset.forName("UTF-8")); + byte[] fileBytes = getResourceAsBytes(context, filePath); + if (fileBytes == null) { + return null; + } + return new String(fileBytes, Charset.forName("UTF-8")); } /** @@ -76,6 +82,9 @@ public static String getResourceAsString(Context context, String filePath) { */ public static byte[] getResourceAsBytes(Context context, String filePath) { InputStream is = getResouceAsInputStream(context, filePath); + if (is == null) { + return null; + } ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); byte[] readBuffer = new byte[4 * 1024]; @@ -112,6 +121,9 @@ public static byte[] getResourceAsBytes(Context context, String filePath) { * @return InputStream representation of the asset */ public static InputStream getResouceAsInputStream(Context context, String filePath) { + if (context == null) { + return null; + } AssetManager assetManager = context.getAssets(); InputStream inputStream = null; try { @@ -124,6 +136,7 @@ public static InputStream getResouceAsInputStream(Context context, String filePa /** * Load resource from a file-path and turns contents to a objects, of type T, for consumption * + * @param object type * @param context android context * @param clazz the class of T * @param filePath relative file path @@ -258,6 +271,7 @@ public int getType() { * Create this Resource into an Object of type T. This method will only work for Json * files. * + * @param object type * @param context android context * @return object of type T */ diff --git a/backbone/src/main/java/org/researchstack/backbone/StorageAccess.java b/backbone/src/main/java/org/researchstack/backbone/StorageAccess.java index a810e15c0..024b777c9 100644 --- a/backbone/src/main/java/org/researchstack/backbone/StorageAccess.java +++ b/backbone/src/main/java/org/researchstack/backbone/StorageAccess.java @@ -2,9 +2,10 @@ import android.app.Application; import android.content.Context; +import android.content.SharedPreferences; import android.os.Handler; import android.os.Looper; -import android.support.annotation.MainThread; +import androidx.annotation.MainThread; import org.researchstack.backbone.storage.database.AppDatabase; import org.researchstack.backbone.storage.file.EncryptionProvider; @@ -40,6 +41,9 @@ public class StorageAccess { private static final boolean CHECK_THREADS = false; + private static final String SHARED_PREFS_KEY = "StorageAccessSharedPrefsKey"; + private static final String USES_FINGERPRINT_KEY = "UsesFingerprintKey"; + //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // Static Fields //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* @@ -126,7 +130,6 @@ public boolean hasPinCode(Context context) { return encryptionProvider.hasPinCode(context); } - //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // Storage Access request and notification //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* @@ -161,7 +164,7 @@ public void requestStorageAccess(Context context) { * Registers a listener. If you want to read/write data, you'll need to implement this to know * when file access is ready. * - * @param storageAccessListener + * @param storageAccessListener the listener to register */ @MainThread public final void register(StorageAccessListener storageAccessListener) { @@ -179,7 +182,7 @@ public final void register(StorageAccessListener storageAccessListener) { * Guess what this does. Yes, you'll need to call it if you called register or you'll have * memory leaks and possibly crashes on callbacks to dead clients. * - * @param storageAccessListener + * @param storageAccessListener the registered listener */ @MainThread public final void unregister(StorageAccessListener storageAccessListener) { @@ -256,7 +259,7 @@ public void logAccessTime() { * * @param context android context * @param pin string of the pin to attempt authentication - * @throws StorageAccessException + * @throws StorageAccessException if the authentication failed */ public void authenticate(Context context, String pin) { encryptionProvider.startWithPassphrase(context, pin); @@ -287,20 +290,62 @@ public void createPinCode(Context context, String pin) { * @param context android context * @param oldPin the old pin * @param newPin the new pin, which should already be validated (enter + confirm) - * @throws StorageAccessException + * @throws StorageAccessException if the pin code change failed */ public void changePinCode(Context context, String oldPin, String newPin) { encryptionProvider.changePinCode(context, oldPin, newPin); injectEncrypter(); } + /** + * @param context can be android or application + * @return true if pin code is backed by fingerprint, false if it is user created + */ + public boolean usesFingerprint(Context context) { + SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_KEY, Context.MODE_PRIVATE); + return prefs.getString(USES_FINGERPRINT_KEY, null) != null; + } + + /** + * @param context can be android or application + * @return the fingerprint data encrypted + */ + public String getFingerprint(Context context) { + SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_KEY, Context.MODE_PRIVATE); + return prefs.getString(USES_FINGERPRINT_KEY, null); + } + + /** + * Method must be called and set to true if the user registers their fingerprint + * @param context can be android or application + * @param encryptedKey the key used for storage, MUST BE ENCRYPTED and the encryption key for it backed by keystore + */ + public void setUsesFingerprint(Context context, String encryptedKey) { + SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_KEY, Context.MODE_PRIVATE); + prefs.edit().putString(USES_FINGERPRINT_KEY, encryptedKey).apply(); + } + + /** + * Removes any history of using fingerprint for authentication + * @param context can be android or application + */ + protected void removeSharedPreference(Context context) { + SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_KEY, Context.MODE_PRIVATE); + if (prefs != null) { + prefs.edit().clear().apply(); + } + } + /** * Removes the pin code if one exists. * * @param context android context */ public void removePinCode(Context context) { - encryptionProvider.removePinCode(context); + if (encryptionProvider != null) { + encryptionProvider.removePinCode(context); + } + removeSharedPreference(context); } private void injectEncrypter() { diff --git a/skin/src/main/java/org/researchstack/skin/TaskProvider.java b/backbone/src/main/java/org/researchstack/backbone/TaskProvider.java similarity index 88% rename from skin/src/main/java/org/researchstack/skin/TaskProvider.java rename to backbone/src/main/java/org/researchstack/backbone/TaskProvider.java index b3b9658ff..8d629522b 100644 --- a/skin/src/main/java/org/researchstack/skin/TaskProvider.java +++ b/backbone/src/main/java/org/researchstack/backbone/TaskProvider.java @@ -1,4 +1,4 @@ -package org.researchstack.skin; +package org.researchstack.backbone; import android.app.Application; @@ -7,6 +7,7 @@ /** * TaskProvider is used as a way for the Framework to get Tasks needed throughout the onboarding * process. This allows you to implement your own Tasks if needed. + * @deprecated use org.researchstack.backbone.onboarding.OnboardingManager instead */ public abstract class TaskProvider { /** @@ -37,6 +38,7 @@ public abstract class TaskProvider { * Application#onCreate()} method. * * @param manager an implementation of ResourcePathManager + * @deprecated use org.researchstack.backbone.onboarding.OnboardingManager instead */ public static void init(TaskProvider manager) { TaskProvider.instance = manager; @@ -57,10 +59,10 @@ public static TaskProvider getInstance() { } /** - * Used, in combination of {@link #put(String, Task)}, for task lookup and resuse + * Used, in combination of {@link #put(String, Task)}, for task lookup and reuse * * @param taskId the task id - * @return a task object with an id of {@param taskId} + * @return a task object with the provided id */ public abstract Task get(String taskId); diff --git a/skin/src/main/java/org/researchstack/skin/UiManager.java b/backbone/src/main/java/org/researchstack/backbone/UiManager.java similarity index 64% rename from skin/src/main/java/org/researchstack/skin/UiManager.java rename to backbone/src/main/java/org/researchstack/backbone/UiManager.java index c3849d8ff..c9b0f89d7 100644 --- a/skin/src/main/java/org/researchstack/skin/UiManager.java +++ b/backbone/src/main/java/org/researchstack/backbone/UiManager.java @@ -1,11 +1,15 @@ -package org.researchstack.skin; +package org.researchstack.backbone; import android.app.Application; import android.content.Context; +import androidx.fragment.app.Fragment; + import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.step.Step; -import org.researchstack.skin.notification.TaskNotificationReceiver; +import org.researchstack.backbone.utils.TextUtils; +import org.researchstack.backbone.notification.TaskNotificationReceiver; +import org.researchstack.backbone.ui.fragment.ShareFragment; import java.util.List; @@ -43,7 +47,7 @@ public static UiManager getInstance() { /** * All ActionItems returned by this method should define a title, icon, and class. These * ActionItems are used to populate an ActionBar in the main Activity, so the class should be - * that of activity (either SettingsActivity & LearnActivity or your own items) + * that of activity (either SettingsActivity & LearnActivity or your own items) * * @return a list of ActionItems for display in the MainActivity ActionBar */ @@ -52,31 +56,39 @@ public static UiManager getInstance() { /** * All ActionItems returned by this method should define a title, icon, and class. These items * are used to fill a pager in the MainActivity. The framework uses the class objects from this - * list to create a Fragments for tha pager. It is imperative that the defined classes be of - * instance {@link android.support.v4.app.Fragment}. + * list to create Fragments for tha pager. It is imperative that the defined classes be of + * instance {@link Fragment}. * * @return a list of ActionItems for display in the MainActivity ActionBar */ public abstract List getMainTabBarItems(); /** - * Includsion Criteria Step is one of the first Steps the user will come in contact with. It is + * Inclusion Criteria Step is one of the first Steps the user will come in contact with. It is * a question / form of questions whos result is used to see if the user elligible or - * inelligible for the study. That result in calculate and returned within {@link - * #isInclusionCriteriaValid(StepResult)} + * inelligible for the study. + * + * This method is now deprecated and Inclusion Criteria will now be loaded from a JSON file as + * defined {@link ResourceManager#getInclusionCriteria()}. * * @param context android context * @return a Step used for Eligibility within the onboarding process */ + @Deprecated public abstract Step getInclusionCriteriaStep(Context context); /** * Method used by the framework to show if the user the result of the {@link * #getInclusionCriteriaStep(Context)}. * + * This method is now deprecated and Inclusion Criteria will now be loaded from a JSON file as + * defined {@link ResourceManager#getInclusionCriteria()}. The JSON file + * contains expected answers which will be used to determine if the inclusion criteria is valid. + * * @param result StepResult object that contains the answers of the InclusionCriteria step - * @return true if the user is elligible for the study + * @return true if the user is elligible for the study */ + @Deprecated public abstract boolean isInclusionCriteriaValid(StepResult result); /** @@ -85,12 +97,22 @@ public static UiManager getInstance() { * All data will still be collected and uploaded when the user successfully signs up for the * first time Defaults to false. * + * * @return true if consent is skippable */ public boolean isConsentSkippable() { return false; } + /** + * Returns true if the password supplied by the user in the sign up step is valid. + * @param password the password to validate + * @return true if the password is valid, false otherwise + */ + public boolean isValidPassword(String password) { + return ! TextUtils.isEmpty(password); + } + /** * Returns the BroadCastReceiver class responsible for consuming alarms for triggering * Notifications @@ -100,4 +122,15 @@ public boolean isConsentSkippable() { public Class getTaskNotificationReceiver() { return TaskNotificationReceiver.class; } + + /** + * Return the ShareFragment to be used. Individual apps can extend and modify the fragment if they + * desire custom logic or presentation. + * + * @return The ShareFragment to use. + */ + public ShareFragment getShareFragment() + { + return new ShareFragment(); + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/AnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/AnswerFormat.java index d489242bd..a09e8e5ee 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/AnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/AnswerFormat.java @@ -4,10 +4,12 @@ import org.researchstack.backbone.ui.step.body.DecimalQuestionBody; import org.researchstack.backbone.ui.step.body.DurationQuestionBody; import org.researchstack.backbone.ui.step.body.FormBody; +import org.researchstack.backbone.ui.step.body.ImageChoiceBody; import org.researchstack.backbone.ui.step.body.IntegerQuestionBody; import org.researchstack.backbone.ui.step.body.MultiChoiceQuestionBody; import org.researchstack.backbone.ui.step.body.NotImplementedStepBody; import org.researchstack.backbone.ui.step.body.SingleChoiceQuestionBody; +import org.researchstack.backbone.ui.step.body.StepBody; import org.researchstack.backbone.ui.step.body.TextQuestionBody; import java.io.Serializable; @@ -23,10 +25,7 @@ * a {@link org.researchstack.backbone.ui.ViewTaskActivity}. */ public abstract class AnswerFormat implements Serializable { - /** - * Default constructor. The appropriate subclass of AnswerFormat should be used instead of this - * directly. - */ + /* Default constructor needed for serialization/deserialization of object */ public AnswerFormat() { } @@ -62,16 +61,17 @@ public enum Type implements QuestionType { TimeInterval(NotImplementedStepBody.class), Duration(DurationQuestionBody.class), Location(NotImplementedStepBody.class), - Form(FormBody.class); + Form(FormBody.class), + ImageChoice(ImageChoiceBody.class); - private Class stepBodyClass; + private Class stepBodyClass; - Type(Class stepBodyClass) { + Type(Class stepBodyClass) { this.stepBodyClass = stepBodyClass; } @Override - public Class getStepBodyClass() { + public Class getStepBodyClass() { return stepBodyClass; } @@ -108,6 +108,6 @@ public enum DateAnswerStyle { * org.researchstack.backbone.ui.step.body.StepBody} class. */ public interface QuestionType { - Class getStepBodyClass(); + Class getStepBodyClass(); } } diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/BirthDateAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/BirthDateAnswerFormat.java index a7c3a55f0..9950449ab 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/BirthDateAnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/BirthDateAnswerFormat.java @@ -1,48 +1,63 @@ package org.researchstack.backbone.answerformat; - import org.researchstack.backbone.R; import org.researchstack.backbone.ui.step.body.BodyAnswer; import java.util.Calendar; import java.util.Date; -public class BirthDateAnswerFormat extends DateAnswerFormat { +public class BirthDateAnswerFormat extends DateAnswerFormat +{ private final int minAge; private final int maxAge; - public BirthDateAnswerFormat(Date defaultDate, int minAge, int maxAge) { - super(DateAnswerStyle.Date, defaultDate, dateFromAge(maxAge), dateFromAge(minAge)); - this.minAge = minAge; - this.maxAge = maxAge; + /* Default constructor needed for serilization/deserialization of object */ + BirthDateAnswerFormat() + { + super(); + minAge = 0; + maxAge = Integer.MAX_VALUE; } - private static Date dateFromAge(int age) { + private static Date dateFromAge(int age) + { Calendar calendar = Calendar.getInstance(); - if (age != 0) { - calendar.add(Calendar.YEAR, -age); + if(age != 0) + { + calendar.add(Calendar.YEAR, - age); return calendar.getTime(); } return null; } + public BirthDateAnswerFormat(Date defaultDate, int minAge, int maxAge) + { + super(DateAnswerStyle.Date, defaultDate, dateFromAge(maxAge), dateFromAge(minAge)); + this.minAge = minAge; + this.maxAge = maxAge; + } + @Override - public BodyAnswer validateAnswer(Date resultDate) { + public BodyAnswer validateAnswer(Date resultDate) + { Date minDate = getMinimumDate(); Date maxDate = getMaximumDate(); - if (minDate != null && isOnOrBefore(resultDate, minDate)) { + if(minDate != null && isOnOrBefore(resultDate, minDate)) + { return new BodyAnswer(false, R.string.rsb_birth_date_too_old, String.valueOf(maxAge)); } - if (maxDate != null && !isOnOrBefore(resultDate, maxDate)) { + if(maxDate != null && ! isOnOrBefore(resultDate, maxDate)) + { return new BodyAnswer(false, R.string.rsb_birth_date_too_young, String.valueOf(minAge)); } return BodyAnswer.VALID; } - private boolean isOnOrBefore(Date inputDate, Date cutoffDate) { + private boolean isOnOrBefore(Date inputDate, Date cutoffDate) + { Calendar calendar = Calendar.getInstance(); calendar.setTime(inputDate); int year = calendar.get(Calendar.YEAR); @@ -53,4 +68,4 @@ private boolean isOnOrBefore(Date inputDate, Date cutoffDate) { return year < cutoffYear || (year == cutoffYear && dayOfYear <= cutoffDayOfYear); } -} +} \ No newline at end of file diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/BooleanAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/BooleanAnswerFormat.java index d4d40eae9..34c653fd5 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/BooleanAnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/BooleanAnswerFormat.java @@ -12,6 +12,20 @@ */ public class BooleanAnswerFormat extends ChoiceAnswerFormat { + public void setTextValues(String trueString, String falseString) { + setAnswerStyle(ChoiceAnswerStyle.SingleChoice); + setChoices(new Choice[] { + new Choice<>(trueString, true), + new Choice<>(falseString, false) + }); + } + + /* Default constructor needed for serilization/deserialization of object */ + public BooleanAnswerFormat() + { + super(); + } + /** * Constructs a single choice question with true/false values, using the specified strings to * represent those choices to the user. @@ -20,9 +34,8 @@ public class BooleanAnswerFormat extends ChoiceAnswerFormat { * @param falseString a string representing false ("No", "False", etc) */ public BooleanAnswerFormat(String trueString, String falseString) { - super(ChoiceAnswerStyle.SingleChoice, - new Choice<>(trueString, true), - new Choice<>(falseString, false)); + this(); + setTextValues(trueString, falseString); } @Override diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/ChoiceAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/ChoiceAnswerFormat.java index d87ae7296..de45af267 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/ChoiceAnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/ChoiceAnswerFormat.java @@ -9,7 +9,22 @@ */ public class ChoiceAnswerFormat extends AnswerFormat { private AnswerFormat.ChoiceAnswerStyle answerStyle; + public AnswerFormat.ChoiceAnswerStyle getAnswerStyle() { + return answerStyle; + } + public void setAnswerStyle(AnswerFormat.ChoiceAnswerStyle style) { + answerStyle = style; + } private Choice[] choices; + public void setChoices(Choice[] choices) { + this.choices = choices; + } + + /* Default constructor needed for serilization/deserialization of object */ + public ChoiceAnswerFormat() + { + super(); + } /** * Creates an answer format with the specified answerStyle(single or multichoice) and collection diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/DateAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/DateAnswerFormat.java index f4c06047c..ac636db4f 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/DateAnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/DateAnswerFormat.java @@ -1,5 +1,4 @@ package org.researchstack.backbone.answerformat; - import org.researchstack.backbone.R; import org.researchstack.backbone.ui.step.body.BodyAnswer; import org.researchstack.backbone.utils.FormatHelper; @@ -10,7 +9,8 @@ * The DateAnswerFormat class represents the answer format for questions that require users to enter * a date, or a date and time. */ -public class DateAnswerFormat extends AnswerFormat { +public class DateAnswerFormat extends AnswerFormat +{ private DateAnswerStyle style; @@ -20,11 +20,19 @@ public class DateAnswerFormat extends AnswerFormat { private Date maximumDate; - public DateAnswerFormat(DateAnswerStyle style) { + /* Default constructor needed for serilization/deserialization of object */ + DateAnswerFormat() + { + super(); + } + + public DateAnswerFormat(DateAnswerStyle style) + { this.style = style; } - public DateAnswerFormat(DateAnswerStyle style, Date defaultDate, Date minimumDate, Date maximumDate) { + public DateAnswerFormat(DateAnswerStyle style, Date defaultDate, Date minimumDate, Date maximumDate) + { this.style = style; this.defaultDate = defaultDate; this.minimumDate = minimumDate; @@ -36,7 +44,8 @@ public DateAnswerFormat(DateAnswerStyle style, Date defaultDate, Date minimumDat * * @return the style of the date entry */ - public DateAnswerStyle getStyle() { + public DateAnswerStyle getStyle() + { return style; } @@ -48,7 +57,8 @@ public DateAnswerStyle getStyle() { * * @return the default date for the date picker presented to the user, or null */ - public Date getDefaultDate() { + public Date getDefaultDate() + { return defaultDate; } @@ -59,7 +69,8 @@ public Date getDefaultDate() { * * @return returns the minimum allowed date, or null */ - public Date getMinimumDate() { + public Date getMinimumDate() + { return minimumDate; } @@ -70,33 +81,38 @@ public Date getMinimumDate() { * * @return returns the maximum allowed date, or null */ - public Date getMaximumDate() { + public Date getMaximumDate() + { return maximumDate; } @Override - public QuestionType getQuestionType() { - if (style == DateAnswerStyle.Date) return Type.Date; - if (style == DateAnswerStyle.DateAndTime) return Type.DateAndTime; - if (style == DateAnswerStyle.TimeOfDay) return Type.TimeOfDay; + public QuestionType getQuestionType() + { + if(style == DateAnswerStyle.Date) return Type.Date; + if(style == DateAnswerStyle.DateAndTime) return Type.DateAndTime; + if(style == DateAnswerStyle.TimeOfDay) return Type.TimeOfDay; return Type.None; } - public BodyAnswer validateAnswer(Date resultDate) { - if (minimumDate != null && resultDate.getTime() < minimumDate.getTime()) { + public BodyAnswer validateAnswer(Date resultDate) + { + if(minimumDate != null && resultDate.getTime() < minimumDate.getTime()) + { return new BodyAnswer(false, - R.string.rsb_invalid_answer_date_under, - FormatHelper.SIMPLE_FORMAT_DATE.format(minimumDate)); + R.string.rsb_invalid_answer_date_under, + FormatHelper.SIMPLE_FORMAT_DATE.format(minimumDate)); } - if (maximumDate != null && resultDate.getTime() > maximumDate.getTime()) { + if(maximumDate != null && resultDate.getTime() > maximumDate.getTime()) + { return new BodyAnswer(false, - R.string.rsb_invalid_answer_date_over, - FormatHelper.SIMPLE_FORMAT_DATE.format(maximumDate)); + R.string.rsb_invalid_answer_date_over, + FormatHelper.SIMPLE_FORMAT_DATE.format(maximumDate)); } return BodyAnswer.VALID; } -} +} \ No newline at end of file diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/DecimalAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/DecimalAnswerFormat.java index 5c7714f50..16c61f5e4 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/DecimalAnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/DecimalAnswerFormat.java @@ -16,6 +16,12 @@ public class DecimalAnswerFormat extends AnswerFormat { private float minValue; private float maxValue; + /* Default constructor needed for serilization/deserialization of object */ + DecimalAnswerFormat() + { + super(); + } + /** * Creates an answer format with the specified min and max values * diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/DurationAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/DurationAnswerFormat.java index 94a6c3fd5..73717bd01 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/DurationAnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/DurationAnswerFormat.java @@ -7,12 +7,17 @@ public class DurationAnswerFormat extends AnswerFormat { private String unit; private int step; + /* Default constructor needed for serilization/deserialization of object */ + DurationAnswerFormat() + { + super(); + } + public DurationAnswerFormat(int step, String unit) { this.step = step; this.unit = unit; } - @Override public QuestionType getQuestionType() { return Type.Duration; diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/EmailAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/EmailAnswerFormat.java index 5b7585b33..bfa496833 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/EmailAnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/EmailAnswerFormat.java @@ -1,13 +1,17 @@ package org.researchstack.backbone.answerformat; +import android.text.InputType; + import org.researchstack.backbone.utils.TextUtils; public class EmailAnswerFormat extends TextAnswerFormat { private static final int MAX_EMAIL_LENGTH = 255; + /* Default constructor needed for serilization/deserialization of object */ public EmailAnswerFormat() { super(MAX_EMAIL_LENGTH); + setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); } @Override diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/FormAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/FormAnswerFormat.java index dcd6fb85d..30381a089 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/FormAnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/FormAnswerFormat.java @@ -6,10 +6,9 @@ * answers. */ public class FormAnswerFormat extends AnswerFormat { - /** - * Default constructor - */ + /* Default constructor needed for serilization/deserialization of object */ public FormAnswerFormat() { + super(); } @Override diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/GenderAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/GenderAnswerFormat.java new file mode 100644 index 000000000..dea0ca19b --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/GenderAnswerFormat.java @@ -0,0 +1,51 @@ +package org.researchstack.backbone.answerformat; + +import android.content.Context; + +import org.researchstack.backbone.model.Choice; +import org.researchstack.backbone.model.UserHealth; + +/** + * Created by TheMDP on 1/4/17. + */ + +public class GenderAnswerFormat extends ChoiceAnswerFormat { + + /* Default constructor needed for serilization/deserialization of object */ + GenderAnswerFormat() + { + super(); + } + + public GenderAnswerFormat(Context context) { + super(ChoiceAnswerStyle.SingleChoice, createChoices(context)); + } + + /** + * @param context used to build strings for male, female, other + * @return Choice array for gender + */ + static Choice[] createChoices(Context context) { + String female = UserHealth.Gender.FEMALE.localizedTitle(context); + String male = UserHealth.Gender.MALE.localizedTitle(context); + String other = UserHealth.Gender.OTHER.localizedTitle(context); + + Choice[] genderChoices = new Choice[3]; + genderChoices[0] = new Choice(female, female); + genderChoices[1] = new Choice(male, male); + genderChoices[2] = new Choice(other, other); + + return genderChoices; + } + + /** + * Creates an answer format with the specified answerStyle(single or multichoice) and collection + * of choices. + * + * @param answerStyle either MultipleChoice or SingleChoice + * @param choices an array of {@link Choice} objects, all of the same type + */ + public GenderAnswerFormat(ChoiceAnswerStyle answerStyle, Choice... choices) { + super(answerStyle, choices); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/ImageChoiceAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/ImageChoiceAnswerFormat.java new file mode 100644 index 000000000..03800e06a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/ImageChoiceAnswerFormat.java @@ -0,0 +1,159 @@ +package org.researchstack.backbone.answerformat; + +import java.io.Serializable; +import java.util.List; + +/** + * Created by TheMDP on 3/14/17. + * + * The `ImageChoiceAnswerFormat` class represents an answer format that lets participants choose + * one image from a fixed set of images in a single choice question. + * + * For example, you might use the image choice answer format to represent a range of moods that range + * from very sad to very happy. + * + * The image choice answer format produces an `ChoiceQuestionResult` object. + */ + +public class ImageChoiceAnswerFormat extends AnswerFormat { + + /** + * An array of `ImageChoice` objects that represent the available choices. + * + * The text of the currently selected choice is displayed on screen. The text for + * each choice is spoken by VoiceOver when an image is highlighted. + */ + private List imageChoiceList; + + /* Default constructor needed for serialization/deserialization of object */ + public ImageChoiceAnswerFormat() { + super(); + } + + /** + * Returns an initialized image choice answer format using the specified array of images. + * + * @param imageChoiceList List of `ImageChoice` objects. + * + * @return An initialized image choice answer format. + */ + public ImageChoiceAnswerFormat(List imageChoiceList) { + this.imageChoiceList = imageChoiceList; + } + + public List getImageChoiceList() { + return imageChoiceList; + } + + public void setImageChoiceList(List imageChoiceList) { + this.imageChoiceList = imageChoiceList; + } + + @Override + public QuestionType getQuestionType() { + return Type.ImageChoice; + } + + /** + * The `ImageChoice` class defines a choice that can be included in an `ImageChoiceAnswerFormat` object. + * + * Typically, image choices are displayed in a horizontal row, so you need to use appropriate sizes. + * For example, when five image choices are displayed in an `ImageChoiceAnswerFormat`, image sizes + * of about 45 to 60 points allow the images to look good in apps that run on all versions of iPhone. + * + * The text that describes an image choice should be reasonably short. However, only the text for the + * currently selected image choice is displayed, so text that wraps to more than one line + * is supported. + */ + public static class ImageChoice implements Serializable { + + private String text; + + /** + * The image to display when the choice is not selected. + * + * The size of the unselected image depends on the number of choices you need to display. As a + * general rule, it's recommended that you start by creating an image that measures 44 x 44 points, + * and adjust it if necessary. + * + * String representation of a drawable resource + * for instance, if your image is R.drawable.image, this would just be "image" + */ + private String normalImageRes; + + /** + * The image to display when the choice is selected. + * + * For best results, the selected image should be the same size as the unselected image + * (that is, the value of the `normalImageRes` member variable). + * If you don't specify a selected image, the default tintColor is used to + * indicate the selection state of the item. + * + * String representation of a drawable resource + * for instance, if your image is R.drawable.image, this would just be "image" + */ + private String selectedImageRes; + + /** + * The value to return when the image is selected. + * The value of this variable is expected to be a scalar property list type, such as `Number` or + * `String`. If no value is provided, the index of the option as an 'Integer' + * in the `ImageChoiceAnswerFormat` options list is used. + */ + private Serializable value; + + /* Default constructor needed for serialization/deserialization of object */ + public ImageChoice() { + super(); + } + + /** + * Returns an image choice that includes the specified images and text. + * + * @param normalImageRes The image drawable resource name to display in the unselected state. + * @param selectedImageRes The image drawable resource name to display in the selected state. + * @param text The text to display when the image is selected. + * @param value The value to record in a result object when the image is selected. + * + * @return An image choice instance. + */ + public ImageChoice (String normalImageRes, String selectedImageRes, String text, Serializable value) { + this.normalImageRes = normalImageRes; + this.selectedImageRes = selectedImageRes; + this.text = text; + this.value = value; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getNormalImageRes() { + return normalImageRes; + } + + public void setNormalImageRes(String normalImageRes) { + this.normalImageRes = normalImageRes; + } + + public String getSelectedImageRes() { + return selectedImageRes; + } + + public void setSelectedImageRes(String selectedImageRes) { + this.selectedImageRes = selectedImageRes; + } + + public Serializable getValue() { + return value; + } + + public void setValue(Serializable value) { + this.value = value; + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/IntegerAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/IntegerAnswerFormat.java index 7ccaae7dd..ae5f1df1e 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/IntegerAnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/IntegerAnswerFormat.java @@ -1,8 +1,11 @@ package org.researchstack.backbone.answerformat; +import android.text.InputType; + import org.researchstack.backbone.R; import org.researchstack.backbone.ui.step.body.BodyAnswer; import org.researchstack.backbone.utils.TextUtils; +import org.w3c.dom.Text; /** * This class defines the attributes for an integer answer format that participants enter using a @@ -12,9 +15,21 @@ * the {@link org.researchstack.backbone.ui.step.body.IntegerQuestionBody} does not allow navigation * until the participant provides a value that is within the valid range. */ -public class IntegerAnswerFormat extends AnswerFormat { +public class IntegerAnswerFormat extends TextAnswerFormat { private int maxValue; + public void setMaxValue(int maxValue) { + this.maxValue = maxValue; + } private int minValue; + public void setMinValue(int minValue) { + this.minValue = minValue; + } + + /* Default constructor needed for serilization/deserialization of object */ + public IntegerAnswerFormat() { + super(); + commonInit(); + } /** * Creates an integer answer format with the specified min and max values. @@ -23,22 +38,27 @@ public class IntegerAnswerFormat extends AnswerFormat { * @param maxValue maximum allowed value, 0 if no max */ public IntegerAnswerFormat(int minValue, int maxValue) { + this(); this.minValue = minValue; this.maxValue = maxValue; } + private void commonInit() { + setInputType(InputType.TYPE_CLASS_NUMBER); + } + @Override public QuestionType getQuestionType() { return Type.Integer; } /** - * Returns the maximum allowed value for the question, 0 if no max + * Returns the maximum allowed value for the question, Integer.MAX_VALUE if maxValue is 0 * - * @return the max value, 0 if no max + * @return the max value, Integer.MAX_VALUE if maxValue is 0 */ public int getMaxValue() { - return maxValue; + return (maxValue == 0) ? Integer.MAX_VALUE : maxValue; } /** diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/MoodScaleAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/MoodScaleAnswerFormat.java new file mode 100644 index 000000000..97bc9bc8c --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/MoodScaleAnswerFormat.java @@ -0,0 +1,148 @@ +package org.researchstack.backbone.answerformat; + +import android.content.Context; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.utils.ResUtils; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * Created by TheMDP on 3/14/17. + */ + +public class MoodScaleAnswerFormat extends ImageChoiceAnswerFormat { + + public static final int MOOD_IMAGE_COUNT = 5; + + /* Default constructor needed for serialization/deserialization of object */ + public MoodScaleAnswerFormat() { + super(); + } + + /** + * Creates a MoodScaleAnswerFormat using the CUSTOM images, but with different hint text + * and step result values + * @param customTextChoices list of custom hint text that will be displayed + * when the corresponding mood image is selected + * @param customValues lost of custom values that will be saved to the step result + * when the corresponding mood image is selected + */ + public MoodScaleAnswerFormat(List customTextChoices, List customValues) { + if (customTextChoices.size() != MOOD_IMAGE_COUNT || + customValues.size() != MOOD_IMAGE_COUNT) + { + throw new IllegalStateException("Both customTextChoices and customValues must" + + "be length " + MOOD_IMAGE_COUNT + " to match the images"); + } + + createImageChoiceList(ResUtils.MoodSurvey.CUSTOM, customTextChoices, customValues); + } + + /** + * @param context Can be app or activity, used to grab string resources + * @param moodQuestionType the type of mood question to base images and text on + */ + public MoodScaleAnswerFormat(Context context, MoodQuestionType moodQuestionType) { + super(); + + String imageName = null; + List textChoices = new ArrayList<>(); + + switch (moodQuestionType) { + case CLARITY: + imageName = ResUtils.MoodSurvey.CLARITY; + textChoices = Arrays.asList( + context.getString(R.string.rsb_MOOD_CLARITY_GREAT), + context.getString(R.string.rsb_MOOD_CLARITY_GOOD), + context.getString(R.string.rsb_MOOD_CLARITY_AVERAGE), + context.getString(R.string.rsb_MOOD_CLARITY_BAD), + context.getString(R.string.rsb_MOOD_CLARITY_TERRIBLE)); + break; + case OVERALL: + imageName = ResUtils.MoodSurvey.OVERALL; + textChoices = Arrays.asList( + context.getString(R.string.rsb_MOOD_OVERALL_GREAT), + context.getString(R.string.rsb_MOOD_OVERALL_GOOD), + context.getString(R.string.rsb_MOOD_OVERALL_AVERAGE), + context.getString(R.string.rsb_MOOD_OVERALL_BAD), + context.getString(R.string.rsb_MOOD_OVERALL_TERRIBLE)); + break; + case PAIN: + imageName = ResUtils.MoodSurvey.PAIN; + textChoices = Arrays.asList( + context.getString(R.string.rsb_MOOD_PAIN_GREAT), + context.getString(R.string.rsb_MOOD_PAIN_GOOD), + context.getString(R.string.rsb_MOOD_PAIN_AVERAGE), + context.getString(R.string.rsb_MOOD_PAIN_BAD), + context.getString(R.string.rsb_MOOD_PAIN_TERRIBLE)); + break; + case SLEEP: + imageName = ResUtils.MoodSurvey.SLEEP; + textChoices = Arrays.asList( + context.getString(R.string.rsb_MOOD_SLEEP_GREAT), + context.getString(R.string.rsb_MOOD_SLEEP_GOOD), + context.getString(R.string.rsb_MOOD_SLEEP_AVERAGE), + context.getString(R.string.rsb_MOOD_SLEEP_BAD), + context.getString(R.string.rsb_MOOD_SLEEP_TERRIBLE)); + break; + case EXERCISE: + imageName = ResUtils.MoodSurvey.EXERCISE; + textChoices = Arrays.asList( + context.getString(R.string.rsb_MOOD_EXERCISE_GREAT), + context.getString(R.string.rsb_MOOD_EXERCISE_GOOD), + context.getString(R.string.rsb_MOOD_EXERCISE_AVERAGE), + context.getString(R.string.rsb_MOOD_EXERCISE_BAD), + context.getString(R.string.rsb_MOOD_EXERCISE_TERRIBLE)); + break; + case CUSTOM: + imageName = ResUtils.MoodSurvey.CUSTOM; + textChoices = Arrays.asList( + context.getString(R.string.rsb_MOOD_CUSTOM_GREAT), + context.getString(R.string.rsb_MOOD_CUSTOM_GOOD), + context.getString(R.string.rsb_MOOD_CUSTOM_AVERAGE), + context.getString(R.string.rsb_MOOD_CUSTOM_BAD), + context.getString(R.string.rsb_MOOD_CUSTOM_TERRIBLE)); + break; + } + + createImageChoiceList(imageName, textChoices, null); + } + + private void createImageChoiceList( + String imageName, List textChoices, List values) + { + List imageChoiceList = new ArrayList<>(); + int count = textChoices.size(); + for (int i = 0; i < count; i++) { + String normalNameRes = ResUtils.MoodSurvey.normal(imageName, (i + 1)); + Serializable value = Integer.valueOf(count - i); + if (values != null) { + value = values.get(i); + } + ImageChoice answerOption = new ImageChoice( + normalNameRes, + null, + textChoices.get(i), + value); + imageChoiceList.add(answerOption); + } + setImageChoiceList(imageChoiceList); + } + + /** + * Type of mood survey question. + */ + public enum MoodQuestionType { + CUSTOM, + CLARITY, + OVERALL, + PAIN, + SLEEP, + EXERCISE + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/PasswordAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/PasswordAnswerFormat.java new file mode 100644 index 000000000..0cbbb95a3 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/PasswordAnswerFormat.java @@ -0,0 +1,28 @@ +package org.researchstack.backbone.answerformat; + +import android.text.InputType; + +/** + * Created by TheMDP on 1/4/17. + */ + +public class PasswordAnswerFormat extends TextAnswerFormat { + + static final int DEFAULT_PASSWORD_MIN_LENGTH = 4; + static final int DEFAULT_PASSWORD_MAX_LENGTH = 16; + static final String PASSWORD_VALIDATION_REGEX = "^\\p{ASCII}*$"; + + /** + * Creates a TextAnswerFormat with no maximum length + * Also, default constructor needed for serilization/deserialization of object + */ + public PasswordAnswerFormat() + { + super(); + setMinumumLength(DEFAULT_PASSWORD_MIN_LENGTH); + setMaximumLength(DEFAULT_PASSWORD_MAX_LENGTH); + setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + setIsMultipleLines(false); + setValidationRegex(PASSWORD_VALIDATION_REGEX); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/answerformat/TextAnswerFormat.java b/backbone/src/main/java/org/researchstack/backbone/answerformat/TextAnswerFormat.java index 3cdda7726..e4885d9b8 100644 --- a/backbone/src/main/java/org/researchstack/backbone/answerformat/TextAnswerFormat.java +++ b/backbone/src/main/java/org/researchstack/backbone/answerformat/TextAnswerFormat.java @@ -1,6 +1,8 @@ package org.researchstack.backbone.answerformat; +import android.text.InputType; + /** * The TextAnswerFormat class represents the answer format for questions that collect a text * response from the user. @@ -8,11 +10,16 @@ public class TextAnswerFormat extends AnswerFormat { public static final int UNLIMITED_LENGTH = 0; private int maximumLength; + private int minimumLength = 0; private boolean isMultipleLines = false; + private int inputType = InputType.TYPE_CLASS_TEXT; + private String validationRegex = null; + private boolean disabled; /** * Creates a TextAnswerFormat with no maximum length + * Also, default constructor needed for serilization/deserialization of object */ public TextAnswerFormat() { this(UNLIMITED_LENGTH); @@ -36,6 +43,32 @@ public int getMaximumLength() { return maximumLength; } + /** + * @param maximumLength the maximum length for the answer, 0 if no maximum set + */ + public void setMaximumLength(int maximumLength) + { + this.maximumLength = maximumLength; + } + + /** + * Returns the minimum length for the answer, 0 if no minumum set + * + * @return the minumum + */ + public int getMinumumLength() + { + return minimumLength; + } + + /** + * @param minimumLength minimum length for the answer, 0 if no minumum set + */ + public void setMinumumLength(int minimumLength) + { + this.minimumLength = minimumLength; + } + @Override public QuestionType getQuestionType() { return Type.Text; @@ -59,6 +92,40 @@ public boolean isMultipleLines() { return isMultipleLines; } + /** + * @param validationRegex used to validate the text answer + */ + public void setValidationRegex(String validationRegex) + { + this.validationRegex = validationRegex; + } + + /** + * Returns whether validation regex for text answer + * + * @return String which can be null + */ + public String validationRegex() + { + return validationRegex; + } + + /** + * @return int indicating the input type of the text answer format + */ + public int getInputType() + { + return inputType; + } + + /** + * @param inputType indicating the input type used for this format + */ + public void setInputType(int inputType) + { + this.inputType = inputType; + } + /** * Returns a boolean indicating whether the passed in text is valid based on this answer format * @@ -66,7 +133,25 @@ public boolean isMultipleLines() { * @return a boolean indicating if the answer is valid */ public boolean isAnswerValid(String text) { - return text != null && text.length() > 0 && + boolean valid = text != null && text.length() >= minimumLength && (maximumLength == UNLIMITED_LENGTH || text.length() <= maximumLength); + + if (valid == false) { + return valid; + } + + if (validationRegex != null) { + valid = text.matches(validationRegex); + } + + return valid; + } + + public boolean isDisabled() { + return disabled; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; } } diff --git a/backbone/src/main/java/org/researchstack/backbone/factory/IntentFactory.java b/backbone/src/main/java/org/researchstack/backbone/factory/IntentFactory.java new file mode 100644 index 000000000..8945fe9ec --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/factory/IntentFactory.java @@ -0,0 +1,86 @@ +package org.researchstack.backbone.factory; + +import android.content.Context; +import android.content.Intent; +import androidx.annotation.NonNull; + +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.task.Task; +import org.researchstack.backbone.ui.ViewTaskActivity; + +/** + * This class encapsulates creating Intent instances. This is needed to enable unit tests, since + * Intents in the unit test environment will always throw. + */ +public class IntentFactory { + /** Singleton instance. */ + public static final IntentFactory INSTANCE = new IntentFactory(); + + /** + * Private constructor, to enforce the singleton property. This prevents creating additional + * instances, but the factory can still be mocked. + */ + private IntentFactory() { + } + + /** + * Creates an Intent for a task activity. + * + * @param context activity context + * @param clazz activity class + * @param task task activity + * @return an Intent for this task activity + */ + @NonNull + public Intent newTaskIntent(Context context, Class clazz, Task task) { + Intent intent = new Intent(context, clazz); + intent.putExtra(ViewTaskActivity.EXTRA_TASK, task); + return intent; + } + + /** + * Creates an Intent for a task activity with an existing task result + * + * @param context activity context + * @param clazz activity class + * @param task task activity + * @param result existing task result + * @return an Intent for this task activity + */ + @NonNull + public Intent newTaskIntent(Context context, Class clazz, Task task, TaskResult result) { + Intent intent = new Intent(context, clazz); + intent.putExtra(ViewTaskActivity.EXTRA_TASK, task); + if (result != null) { + intent.putExtra(ViewTaskActivity.EXTRA_TASK_RESULT, result); + } + return intent; + } + + /** + * Creates an Intent for a task activity with an existing task result + * + * @param context activity context + * @param clazz activity class + * @param task task activity + * @param result existing task result + * @param step the step the task should start on + * @return an Intent for this task activity + */ + @NonNull + public Intent newTaskIntent(Context context, + Class clazz, + Task task, TaskResult result, Step step) { + + Intent intent = new Intent(context, clazz); + intent.putExtra(ViewTaskActivity.EXTRA_TASK, task); + if (result != null) { + intent.putExtra(ViewTaskActivity.EXTRA_TASK_RESULT, result); + } + if (step != null) { + intent.putExtra(ViewTaskActivity.EXTRA_STEP, step); + } + return intent; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/factory/ObservableTransformerFactory.java b/backbone/src/main/java/org/researchstack/backbone/factory/ObservableTransformerFactory.java new file mode 100644 index 000000000..0da152f82 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/factory/ObservableTransformerFactory.java @@ -0,0 +1,27 @@ +package org.researchstack.backbone.factory; + +import androidx.annotation.NonNull; +import org.researchstack.backbone.utils.ObservableUtils; +import rx.Observable; + +/** + * This class encapsulates creating Observable.Transformer instances. This is needed to enable unit + * tests, since the default transformer calls through to Schedulers.io() and Android.mainThread(). + */ +public class ObservableTransformerFactory { + /** Singleton instance. */ + public static final ObservableTransformerFactory INSTANCE = new ObservableTransformerFactory(); + + /** + * Private constructor, to enforce the singleton property. This prevents creating additional + * instances, but the factory can still be mocked. + */ + private ObservableTransformerFactory() { + } + + /** Applies the default transformers. Used with Observable.compose(). */ + @NonNull + public Observable.Transformer defaultTransformer() { + return ObservableUtils.applyDefault(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/Choice.java b/backbone/src/main/java/org/researchstack/backbone/model/Choice.java index e4c820e9b..840d55ab2 100644 --- a/backbone/src/main/java/org/researchstack/backbone/model/Choice.java +++ b/backbone/src/main/java/org/researchstack/backbone/model/Choice.java @@ -1,5 +1,7 @@ package org.researchstack.backbone.model; +import com.google.gson.annotations.SerializedName; + import java.io.Serializable; /** @@ -7,15 +9,24 @@ * They typically have an integer or string value, always with a string text representation of the * choice for the user. * - * @param the type of value for the choice, usually Integer or String + * @param the type of value for the choice, usually Integer or String, but must implement Serializable */ public class Choice implements Serializable { + + @SerializedName(value="text", alternate = {"prompt"}) private String text; + @SerializedName("value") private T value; + @SerializedName("detailText") private String detailText; + /* Default constructor needed for serilization/deserialization of object */ + Choice() { + super(); + } + /** * Creates a choice object with the provided text and value, detailtext is null * diff --git a/backbone/src/main/java/org/researchstack/backbone/model/ConsentDocument.java b/backbone/src/main/java/org/researchstack/backbone/model/ConsentDocument.java index c83f713dc..a538d6550 100644 --- a/backbone/src/main/java/org/researchstack/backbone/model/ConsentDocument.java +++ b/backbone/src/main/java/org/researchstack/backbone/model/ConsentDocument.java @@ -1,6 +1,8 @@ package org.researchstack.backbone.model; -import android.support.annotation.StringRes; +import androidx.annotation.StringRes; + +import com.google.gson.annotations.SerializedName; import java.io.Serializable; import java.util.ArrayList; @@ -66,6 +68,13 @@ public class ConsentDocument implements Serializable { */ private String htmlReviewContent; + @SerializedName("documentProperties") + DocumentProperties documentProperties; + + /* Default constructor needed for serilization/deserialization of object */ + public ConsentDocument() { + super(); + } public void setTitle(String title) { this.title = title; @@ -114,4 +123,7 @@ public void setHtmlReviewContent(String htmlReviewContent) { this.htmlReviewContent = htmlReviewContent; } + public DocumentProperties getDocumentProperties() { + return documentProperties; + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/model/ConsentQuestionType.java b/backbone/src/main/java/org/researchstack/backbone/model/ConsentQuestionType.java new file mode 100644 index 000000000..2b3ca9dd5 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/ConsentQuestionType.java @@ -0,0 +1,27 @@ +package org.researchstack.backbone.model; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by TheMDP on 12/15/16. + */ + +@Deprecated // no longer needed since ConsentQuizModel is deprecated +public enum ConsentQuestionType { + + @SerializedName("boolean") + BOOLEAN("boolean"), + @SerializedName("singleChoiceText") + SINGLE_CHOICE_TEXT("singleChoiceText"), + @SerializedName("instruction") + INSTRUCTION("instruction"); + + ConsentQuestionType(String questionId) { + mIdentifier = questionId; + } + + String mIdentifier; + public String getIdentifier() { + return mIdentifier; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/ConsentQuizModel.java b/backbone/src/main/java/org/researchstack/backbone/model/ConsentQuizModel.java new file mode 100644 index 000000000..8324489ca --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/ConsentQuizModel.java @@ -0,0 +1,141 @@ +package org.researchstack.backbone.model; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; + +@Deprecated // Use NavigationFormStep or NavigationSubtaskStep instead +public class ConsentQuizModel implements Serializable +{ + private String failureTitle; + private String failureMessage; + private String successTitle; + private String successMessage; + private int allowedFailures; + private List questions; + + // fields with defaults + private String incorrectIcon = "rsb_quiz_retry"; + private String correctIcon = "rsb_ic_quiz_valid"; + + ConsentQuizModel() { + super(); + } + + public String getFailureTitle() + { + return failureTitle; + } + + public String getFailureMessage() + { + return failureMessage; + } + + public String getSuccessTitle() + { + return successTitle; + } + + public String getSuccessMessage() + { + return successMessage; + } + + public int getAllowedFailures() + { + return allowedFailures; + } + + public List getQuestions() + { + return questions; + } + + public String getIncorrectIcon() + { + return incorrectIcon; + } + + public String getCorrectIcon() + { + return correctIcon; + } + + public class QuizQuestion implements Serializable + { + private String identifier; + + /** iOS named it "title" but Android named it prompt, so allow for parsing of both */ + @SerializedName(value="prompt", alternate = {"title"}) + private String prompt; + + private ConsentQuestionType type; + private String expectedAnswer; + private String text; + private String positiveFeedback; + private String negativeFeedback; + + /** + * There are multiple ways you can provide the options for a quiz question: + * textChoices - a simple String list of choices for the user, the answer + * format will be the index in the array of the selected item + */ + private List textChoices; + /** + * There are multiple ways you can provide the options for a quiz question: + * items - a list of text / value pairs that can provide any type of answer value per text + */ + private List items; + + public String getIdentifier() + { + return identifier; + } + + public void setIdentifier(String identifier) + { + this.identifier = identifier; + } + + public String getPrompt() + { + return prompt; + } + + public ConsentQuestionType getType() + { + return type; + } + + public String getExpectedAnswer() + { + return expectedAnswer; + } + + public String getText() + { + return text; + } + + public List getTextChoices() + { + return textChoices; + } + + public List getItems() { + return items; + } + + public String getPositiveFeedback() + { + return positiveFeedback == null ? "" : positiveFeedback; + } + + public String getNegativeFeedback() + { + return negativeFeedback == null ? "" : negativeFeedback; + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/ConsentSection.java b/backbone/src/main/java/org/researchstack/backbone/model/ConsentSection.java index 13f232117..ea00bde60 100644 --- a/backbone/src/main/java/org/researchstack/backbone/model/ConsentSection.java +++ b/backbone/src/main/java/org/researchstack/backbone/model/ConsentSection.java @@ -1,4 +1,5 @@ package org.researchstack.backbone.model; +import androidx.annotation.StringRes; import com.google.gson.annotations.SerializedName; @@ -7,7 +8,8 @@ import java.io.Serializable; -public class ConsentSection implements Serializable { +public class ConsentSection implements Serializable +{ /** * The type of section. (read-only) @@ -15,8 +17,10 @@ public class ConsentSection implements Serializable { * The value of this property indicates whether a predefined image, title, and animation are * present. */ - @SerializedName("sectionType") - private Type type; + public static final String SECTION_TYPE_GSON = "sectionType"; + @SerializedName(SECTION_TYPE_GSON) + private Type type; + /** * The title of the consent section in a localized string. *

    @@ -102,75 +106,121 @@ public class ConsentSection implements Serializable { @SerializedName("sectionAnimationUrl") private String customAnimationURL; + /** + * Used for storing custom type identifer when type is CUSTOM eum + */ + transient String customTypeIdentifier; + + /* Default identifier for serilization/deserialization */ + ConsentSection() { + super(); + } + /** * Returns an initialized consent section using the specified type. * * @param type The consent section type. */ - public ConsentSection(Type type) { + public ConsentSection(Type type) + { this.type = type; this.summary = null; } - public String getTitle() { + public String getTitle() + { return title; } - public void setTitle(String title) { + public void setTitle(String title) + { this.title = title; } - public String getFormalTitle() { + public String getFormalTitle() + { return formalTitle; } - public Type getType() { + public Type getType() + { return type; } + void setType(Type type) { + this.type = type; + } - public String getHtmlContent() { + public String getHtmlContent() + { return htmlContent; } - public void setHtmlContent(String htmlContent) { + public void setHtmlContent(String htmlContent) + { this.htmlContent = htmlContent; } - public String getCustomImageName() { + public String getCustomImageName() + { return customImageName; } - public String getContent() { + void setCustomImageName(String imageName) { + customImageName = imageName; + } + + public String getContent() + { return content; } - public void setContent(String content) { + public void setContent(String content) + { this.content = content; this.escapedContent = null; } - public String getSummary() { + public String getSummary() + { return summary; } - public void setSummary(String summary) { + public void setSummary(String summary) + { this.summary = summary; } - public String getEscapedContent() { + public String getEscapedContent() + { // If its null, return that. If not, escape/replace chars in var content - if (TextUtils.isEmpty(content)) { + if(TextUtils.isEmpty(content)) + { return content; } return escapedContent; } - public String getCustomLearnMoreButtonTitle() { + public String getCustomLearnMoreButtonTitle() + { return customLearnMoreButtonTitle; } - public enum Type implements Serializable { + public void setCustomLearnMoreButtonTitle(String customLearnMoreButtonTitle) + { + this.customLearnMoreButtonTitle = customLearnMoreButtonTitle; + } + + public String getTypeIdentifier() { + if (type == Type.Custom) { + return customTypeIdentifier; + } + return type.getIdentifier(); + } + + public static final int UNDEFINED_RES = -1; + public enum Type implements Serializable + { /** * Overview of the informed consent process. *

    @@ -178,7 +228,11 @@ public enum Type implements Serializable { * general background information on the purpose of the study. */ @SerializedName("overview") - Overview(), + Overview( + "overview", + R.string.rsb_consent_section_welcome, + R.string.rsb_consent_section_more_info_welcome, + null), /** * A section informing the user that sensor data will be collected. @@ -187,7 +241,11 @@ public enum Type implements Serializable { * purpose. */ @SerializedName("dataGathering") - DataGathering(), + DataGathering( + "dataGathering", + R.string.rsb_consent_section_data_gathering, + R.string.rsb_consent_section_more_info_data_gathering, + "rsb_consent_section_data_gathering"), /** * A section describing the privacy policies for the study. @@ -198,7 +256,11 @@ public enum Type implements Serializable { * involved. */ @SerializedName("privacy") - Privacy(), + Privacy( + "privacy", + R.string.rsb_consent_section_privacy, + R.string.rsb_consent_section_more_info_privacy, + "rsb_consent_section_privacy"), /** * A section describing how the collected data will be used. @@ -208,7 +270,11 @@ public enum Type implements Serializable { * over the data after it is collected. */ @SerializedName("dataUse") - DataUse(), + DataUse( + "dataUse", + R.string.rsb_consent_section_data_use, + R.string.rsb_consent_section_more_info_data_use, + "rsb_consent_section_data_use"), /** * A section describing how much time is required for the study. @@ -216,7 +282,11 @@ public enum Type implements Serializable { * This content can help users understand what to expect as they participate in the study. */ @SerializedName("timeCommitment") - TimeCommitment(), + TimeCommitment( + "timeCommitment", + R.string.rsb_consent_section_time_commitment, + R.string.rsb_consent_section_more_info_time_commitment, + "rsb_consent_section_time_commitment"), /** * A section describing active task use in the study. @@ -225,7 +295,11 @@ public enum Type implements Serializable { * what purpose. Any risks that are involved can also be communicated in this section. */ @SerializedName("studyTasks") - StudyTasks(), + StudyTasks( + "studyTasks", + R.string.rsb_consent_section_study_tasks, + R.string.rsb_consent_section_more_info_study_tasks, + "rsb_consent_section_study_tasks"), /** * A section describing survey use in the study. @@ -234,7 +308,11 @@ public enum Type implements Serializable { * clear to what extent participation is optional. */ @SerializedName("studySurvey") - StudySurvey(), + StudySurvey( + "studySurvey", + R.string.rsb_consent_section_study_survey, + R.string.rsb_consent_section_more_info_study_survey, + "rsb_consent_section_study_survey"), /** * A section describing how to withdraw from the study. @@ -243,7 +321,11 @@ public enum Type implements Serializable { * to withdraw. */ @SerializedName("withdrawing") - Withdrawing(), + Withdrawing( + "withdrawing", + R.string.rsb_consent_section_withdrawing, + R.string.rsb_consent_section_more_info_withdrawing, + "rsb_consent_section_withdrawing"), /** * A custom section. @@ -252,7 +334,7 @@ public enum Type implements Serializable { * consent document may have as many or as few custom sections as needed. */ @SerializedName("custom") - Custom, + Custom("custom", UNDEFINED_RES, R.string.rsb_consent_section_more_info, null), /** * Document-only sections. @@ -262,75 +344,37 @@ public enum Type implements Serializable { * property). */ @SerializedName("onlyInDocument") - OnlyInDocument; + OnlyInDocument("onlyInDocument", UNDEFINED_RES, R.string.rsb_consent_section_more_info, null); + + Type( + String identifier, + @StringRes int titleRes, + @StringRes int moreInfoRes, + String imageName) + { + this.identifier = identifier; + this.titleRes = titleRes; + this.moreInfoRes = moreInfoRes; + this.imageName = imageName; + } + + String identifier; + String imageName; + @StringRes int titleRes; + @StringRes int moreInfoRes; public int getTitleResId() { - switch (this) { - case Overview: - return R.string.rsb_consent_section_welcome; - case DataGathering: - return R.string.rsb_consent_section_data_gathering; - case Privacy: - return R.string.rsb_consent_section_privacy; - case DataUse: - return R.string.rsb_consent_section_data_use; - case TimeCommitment: - return R.string.rsb_consent_section_time_commitment; - case StudySurvey: - return R.string.rsb_consent_section_study_survey; - case StudyTasks: - return R.string.rsb_consent_section_study_tasks; - case Withdrawing: - return R.string.rsb_consent_section_withdrawing; - default: - return -1; - } + return titleRes; } - public String getImageName() { - switch (this) { - case DataGathering: - return "rsb_consent_section_data_gathering"; - case Privacy: - return "rsb_consent_section_privacy"; - case DataUse: - return "rsb_consent_section_data_use"; - case TimeCommitment: - return "rsb_consent_section_time_commitment"; - case StudySurvey: - return "rsb_consent_section_study_survey"; - case StudyTasks: - return "rsb_consent_section_study_tasks"; - case Withdrawing: - return "rsb_consent_section_withdrawing"; - default: - return null; - } + return imageName; } - public int getMoreInfoResId() { - switch (this) { - case Overview: - return R.string.rsb_consent_section_more_info_welcome; - case DataGathering: - return R.string.rsb_consent_section_more_info_data_gathering; - case Privacy: - return R.string.rsb_consent_section_more_info_privacy; - case DataUse: - return R.string.rsb_consent_section_more_info_data_use; - case TimeCommitment: - return R.string.rsb_consent_section_more_info_time_commitment; - case StudySurvey: - return R.string.rsb_consent_section_more_info_study_survey; - case StudyTasks: - return R.string.rsb_consent_section_more_info_study_tasks; - case Withdrawing: - return R.string.rsb_consent_section_more_info_withdrawing; - default: - return R.string.rsb_consent_section_more_info; - } + return moreInfoRes; + } + private String getIdentifier() { + return identifier; } - } } diff --git a/backbone/src/main/java/org/researchstack/backbone/model/ConsentSectionAdapter.java b/backbone/src/main/java/org/researchstack/backbone/model/ConsentSectionAdapter.java new file mode 100644 index 000000000..4c47d90eb --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/ConsentSectionAdapter.java @@ -0,0 +1,79 @@ +package org.researchstack.backbone.model; + +import com.google.gson.Gson; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import org.researchstack.backbone.ResourceManager; +import org.researchstack.backbone.ResourcePathManager; +import org.researchstack.backbone.onboarding.OnboardingManager; + +import java.lang.reflect.Type; + +/** + * Created by TheMDP on 1/5/17. + */ + +public class ConsentSectionAdapter implements JsonDeserializer { + + /** + * Used to convert ConsentSections + */ + OnboardingManager.AdapterContextProvider adapterProvider; + + public ConsentSectionAdapter(OnboardingManager.AdapterContextProvider adapterProvider) { + this.adapterProvider = adapterProvider; + } + + @Override + public ConsentSection deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + + JsonObject jsonObject = json.getAsJsonObject(); + JsonElement typeJson = jsonObject.get(ConsentSection.SECTION_TYPE_GSON); + + ConsentSection.Type type = context.deserialize(typeJson, ConsentSection.Type.class); + // This was a custom ConsentSection Type + if (type == null) { + type = ConsentSection.Type.Custom; + } + + // This will avoid the infinite loop of using the param json context, + // Since this wont have the ConsentSectionAdapter registered + Gson gson = new Gson(); + ConsentSection consentSection = gson.fromJson(json, ConsentSection.class); + consentSection.setType(type); + + // If we have a non-custom type, we can auto-populate title, learn more, and image name + // if they weren't specifically provided by the JSON + if (type != ConsentSection.Type.Custom) { + if (adapterProvider != null && adapterProvider.getContext() != null) { + if (consentSection.getTitle() == null && type.getTitleResId() != ConsentSection.UNDEFINED_RES) { + consentSection.setTitle(adapterProvider.getContext().getString(type.getTitleResId())); + } + if (consentSection.getCustomLearnMoreButtonTitle() == null && type.getMoreInfoResId() != ConsentSection.UNDEFINED_RES) { + consentSection.setCustomLearnMoreButtonTitle(adapterProvider.getContext().getString(type.getMoreInfoResId())); + } + } + if (consentSection.getCustomImageName() == null) { + consentSection.setCustomImageName(type.getImageName()); + } + } else { + consentSection.customTypeIdentifier = typeJson.getAsString(); + } + + // Convert HTML content from filename to actual HTML content + if (consentSection.getHtmlContent() != null && + adapterProvider != null && adapterProvider.getContext() != null) + { + String htmlContentPath = ResourceManager.getInstance().generatePath( + ResourcePathManager.Resource.TYPE_HTML, consentSection.getHtmlContent()); + String htmlContent = ResourceManager.getResourceAsString(adapterProvider.getContext(), htmlContentPath); + consentSection.setHtmlContent(htmlContent); + } + + return consentSection; + } +} diff --git a/skin/src/main/java/org/researchstack/skin/model/ConsentSectionModel.java b/backbone/src/main/java/org/researchstack/backbone/model/ConsentSectionModel.java similarity index 77% rename from skin/src/main/java/org/researchstack/skin/model/ConsentSectionModel.java rename to backbone/src/main/java/org/researchstack/backbone/model/ConsentSectionModel.java index 092a091b7..18268050b 100644 --- a/skin/src/main/java/org/researchstack/skin/model/ConsentSectionModel.java +++ b/backbone/src/main/java/org/researchstack/backbone/model/ConsentSectionModel.java @@ -1,12 +1,10 @@ -package org.researchstack.skin.model; +package org.researchstack.backbone.model; import com.google.gson.annotations.SerializedName; -import org.researchstack.backbone.model.ConsentSection; -import org.researchstack.backbone.model.DocumentProperties; - import java.util.List; +@Deprecated // No longer needed with new OnboardingManager public class ConsentSectionModel { @SerializedName("documentProperties") diff --git a/backbone/src/main/java/org/researchstack/backbone/model/ConsentSignature.java b/backbone/src/main/java/org/researchstack/backbone/model/ConsentSignature.java index 1a27e7ea3..63f692a0b 100644 --- a/backbone/src/main/java/org/researchstack/backbone/model/ConsentSignature.java +++ b/backbone/src/main/java/org/researchstack/backbone/model/ConsentSignature.java @@ -1,11 +1,11 @@ package org.researchstack.backbone.model; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import java.io.Serializable; import java.util.UUID; -public class ConsentSignature implements Serializable, Cloneable { +public class ConsentSignature implements Serializable { /** * A Boolean value indicating whether the user needs to enter their name during consent review. @@ -68,6 +68,7 @@ public class ConsentSignature implements Serializable, Cloneable { */ private String signatureDateFormatString; + /* Default identifier for serilization/deserialization */ public ConsentSignature() { this.requiresName = true; this.requiresSignatureImage = true; diff --git a/backbone/src/main/java/org/researchstack/backbone/model/ConsentSignatureBody.java b/backbone/src/main/java/org/researchstack/backbone/model/ConsentSignatureBody.java new file mode 100644 index 000000000..9fca8f15b --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/ConsentSignatureBody.java @@ -0,0 +1,50 @@ +package org.researchstack.backbone.model; + +import java.util.Date; + +public class ConsentSignatureBody { + + public ConsentSignatureBody() { + // Default constructor + } + + /** + * The identifier for the study under which the user is signing in + */ + public String study; + + /** + * User's name + */ + public String name; + + /** + * User's birthdate + */ + public Date birthdate; + + /** + * User's signature image data + */ + public String imageData; + + /** + * User's signature image mime type + */ + public String imageMimeType; + + /** + * User's sharing scope choice + */ + public String scope; + + public ConsentSignatureBody(String study, String name, Date birthdate, String imageData, + String imageMimeType, String scope) { + this.study = study; + this.name = name; + this.birthdate = birthdate; + this.imageData = imageData; + this.imageMimeType = imageMimeType; + this.scope = scope; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/DocumentProperties.java b/backbone/src/main/java/org/researchstack/backbone/model/DocumentProperties.java index 45265aa63..ad4e66a44 100644 --- a/backbone/src/main/java/org/researchstack/backbone/model/DocumentProperties.java +++ b/backbone/src/main/java/org/researchstack/backbone/model/DocumentProperties.java @@ -2,7 +2,9 @@ import com.google.gson.annotations.SerializedName; -public class DocumentProperties { +import java.io.Serializable; + +public class DocumentProperties implements Serializable { @SerializedName("htmlDocument") private String htmlDocument; @@ -19,6 +21,11 @@ public class DocumentProperties { private boolean requiresName; private boolean requiresBirthdate; + /* Default identifier for serilization/deserialization */ + DocumentProperties() { + super(); + } + public String getHtmlDocument() { return htmlDocument; } diff --git a/backbone/src/main/java/org/researchstack/backbone/model/InclusionCriteriaModel.java b/backbone/src/main/java/org/researchstack/backbone/model/InclusionCriteriaModel.java new file mode 100644 index 000000000..a5dbb725b --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/InclusionCriteriaModel.java @@ -0,0 +1,82 @@ +package org.researchstack.backbone.model; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +@Deprecated // No longer needed with new OnboardingManager +public class InclusionCriteriaModel { + + public static final String INELIGIBLE_INSTRUCTION_IDENTIFIER = "ineligibleInstruction"; + public static final String ELIGIBLE_INSTRUCTION_IDENTIFIER = "eligibleInstruction"; + @SerializedName("steps") + public List steps; + + + public static class Step + { + @SerializedName("identifier") + public String identifier; + + @SerializedName("type") + public StepType type; + + @SerializedName("text") + public String text; + + @SerializedName("detailText") + public String detailText; + + @SerializedName("image") + public String image; + + @SerializedName("nextIdentifier") + public String nextIdentifier; + + @SerializedName("skipIdentifier") + public String skipIdentifier; + + @SerializedName("skipIfPassed") + public boolean skipIfPassed; + + @SerializedName("items") + public List items; + + } + + public enum StepType { + + @SerializedName("instruction") + INSTRUCTION("instruction"), + @SerializedName("compound") + COMPOUND("compound"), + @SerializedName("toggle") + TOGGLE("toggle"), + @SerializedName("share") + SHARE("share"); + + StepType(String type) { + type = type; + } + + String type; + public String getType() { + return type; + } + } + + public static class Item + { + @SerializedName("identifier") + public String identifier; + + @SerializedName("type") + public String type; + + @SerializedName("text") + public String text; + + @SerializedName("expectedAnswer") + public boolean expectedAnswer; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/ProfileInfoOption.java b/backbone/src/main/java/org/researchstack/backbone/model/ProfileInfoOption.java new file mode 100644 index 000000000..0710baf2a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/ProfileInfoOption.java @@ -0,0 +1,73 @@ +package org.researchstack.backbone.model; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 1/4/17. + * + * Used by several differnt Step types to designate re-usable QuestionStep types + * that collect user profile info + */ + +public enum ProfileInfoOption { + @SerializedName("email") + EMAIL("email"), + @SerializedName("password") + PASSWORD("password"), + @SerializedName("externalID") + EXTERNAL_ID("externalID"), + @SerializedName("name") + NAME("name"), + @SerializedName("birthdate") + BIRTHDATE("birthdate"), + @SerializedName("gender") + GENDER("gender"), + @SerializedName("bloodType") + BLOOD_TYPE("bloodType"), + @SerializedName("fitzpatrickSkinType") + FITZPATRICK_SKIN_TYPE("fitzpatrickSkinType"), + @SerializedName("wheelchairUse") + WHEEL_CHAIR_USE("wheelchairUse"), + @SerializedName("height") + HEIGHT("height"), + @SerializedName("weight") + WEIGHT("weight"), + @SerializedName("wakeTime") + WAKE_TIME("wakeTime"), + @SerializedName("sleepTime") + SLEEP_TIME("sleepTime"); + + static Gson gson; + + private final String identifier; + + ProfileInfoOption(String identifier) { + this.identifier = identifier; + } + + public String getIdentifier() { + return identifier; + } + + public static List toProfileInfoOptions(List identifiers) { + List options = new ArrayList<>(); + for(String identifier : identifiers) { + ProfileInfoOption option = toProfileInfoOption(identifier); + if (option != null) { + options.add(option); + } + } + return options; + } + + public static ProfileInfoOption toProfileInfoOption(String identifier) { + if (gson == null) { + gson = new Gson(); + } + return gson.fromJson(identifier, ProfileInfoOption.class); + } +} diff --git a/skin/src/main/java/org/researchstack/skin/model/SchedulesAndTasksModel.java b/backbone/src/main/java/org/researchstack/backbone/model/SchedulesAndTasksModel.java similarity index 55% rename from skin/src/main/java/org/researchstack/skin/model/SchedulesAndTasksModel.java rename to backbone/src/main/java/org/researchstack/backbone/model/SchedulesAndTasksModel.java index eed0eb6e9..cc1b52b20 100644 --- a/skin/src/main/java/org/researchstack/skin/model/SchedulesAndTasksModel.java +++ b/backbone/src/main/java/org/researchstack/backbone/model/SchedulesAndTasksModel.java @@ -1,8 +1,11 @@ -package org.researchstack.skin.model; +package org.researchstack.backbone.model; import com.google.gson.annotations.SerializedName; +import java.io.Serializable; +import java.util.Date; import java.util.List; +import java.util.Map; public class SchedulesAndTasksModel { public List schedules; @@ -11,6 +14,8 @@ public static class ScheduleModel { public String scheduleType; public String delay; public String scheduleString; + public Date scheduledOn; + public Date expiresOn; public List tasks; } @@ -19,7 +24,18 @@ public static class TaskScheduleModel { public String taskID; public String taskFileName; public String taskClassName; + public boolean taskIsOptional; + public String taskType; + public Date taskFinishedOn; + /** + * The GUID can distinguish between different instances of models with the same taskID + */ + public String taskGUID; + + /** + * The time it takes to complete the task + */ @SerializedName("taskCompletionTimeString") public String taskCompletionTime; } diff --git a/skin/src/main/java/org/researchstack/skin/model/SectionModel.java b/backbone/src/main/java/org/researchstack/backbone/model/SectionModel.java similarity index 83% rename from skin/src/main/java/org/researchstack/skin/model/SectionModel.java rename to backbone/src/main/java/org/researchstack/backbone/model/SectionModel.java index 7440737d6..33b152607 100644 --- a/skin/src/main/java/org/researchstack/skin/model/SectionModel.java +++ b/backbone/src/main/java/org/researchstack/backbone/model/SectionModel.java @@ -1,14 +1,28 @@ -package org.researchstack.skin.model; +package org.researchstack.backbone.model; import com.google.gson.annotations.SerializedName; import java.util.List; public class SectionModel { + public static final String SHARE_TYPE_DETAILS = "share"; + + @SerializedName("logo_name") + private String logoName; + + private String title; @SerializedName("items") private List

    sections; + public String getLogoName() { + return logoName; + } + + public String getTitle() { + return title; + } + public List
    getSections() { return sections; } diff --git a/skin/src/main/java/org/researchstack/skin/model/StudyOverviewModel.java b/backbone/src/main/java/org/researchstack/backbone/model/StudyOverviewModel.java similarity index 97% rename from skin/src/main/java/org/researchstack/skin/model/StudyOverviewModel.java rename to backbone/src/main/java/org/researchstack/backbone/model/StudyOverviewModel.java index 854346d13..1cfe8f609 100644 --- a/skin/src/main/java/org/researchstack/skin/model/StudyOverviewModel.java +++ b/backbone/src/main/java/org/researchstack/backbone/model/StudyOverviewModel.java @@ -1,4 +1,4 @@ -package org.researchstack.skin.model; +package org.researchstack.backbone.model; import com.google.gson.annotations.SerializedName; diff --git a/skin/src/main/java/org/researchstack/skin/model/TaskModel.java b/backbone/src/main/java/org/researchstack/backbone/model/TaskModel.java similarity index 98% rename from skin/src/main/java/org/researchstack/skin/model/TaskModel.java rename to backbone/src/main/java/org/researchstack/backbone/model/TaskModel.java index 59ea38a6b..412017ced 100644 --- a/skin/src/main/java/org/researchstack/skin/model/TaskModel.java +++ b/backbone/src/main/java/org/researchstack/backbone/model/TaskModel.java @@ -1,4 +1,4 @@ -package org.researchstack.skin.model; +package org.researchstack.backbone.model; import com.google.gson.annotations.SerializedName; diff --git a/backbone/src/main/java/org/researchstack/backbone/model/User.java b/backbone/src/main/java/org/researchstack/backbone/model/User.java new file mode 100644 index 000000000..5e7957e71 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/User.java @@ -0,0 +1,96 @@ +package org.researchstack.backbone.model; + +import java.io.Serializable; +import java.util.Date; + +/* + Created by bradleymcdermott on 10/22/15. + */ +public class User implements Serializable +{ + private String name; + + private String email; + + private Date birthDate; + + private UserHealth userHealth; + + /** + * See description above DataSharingScope inner enum below + */ + private DataSharingScope dataSharingScope; + + /** Default constructor for Serializable */ + public User() + { + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } + + public Date getBirthDate() + { + return birthDate; + } + + public void setBirthDate(Date birthDate) + { + this.birthDate = birthDate; + } + + public DataSharingScope getDataSharingScope() { + return dataSharingScope; + } + + public void setDataSharingScope(DataSharingScope dataSharingScope) { + this.dataSharingScope = dataSharingScope; + } + + public UserHealth getUserHealth() { + return userHealth; + } + + public void setUserHealth(UserHealth userHealth) { + this.userHealth = userHealth; + } + + /*! + * DataSharingScope is an enumeration of the choices for the scope of sharing collected data. + * NONE - The user has not consented to sharing their data. + * STUDY - The user has consented only to sharing their de-identified data with the sponsors and partners of the current research study. + * ALL - The user has consented to sharing their de-identified data for current and future research, which may or may not involve the same institutions or investigators. + */ + public enum DataSharingScope { + NONE("no_sharing"), + STUDY("sponsors_and_partners"), + ALL("all_qualified_researchers"); + + private String identifier; + + DataSharingScope(String identifier) { + this.identifier = identifier; + } + + public String getIdentifier() { + return identifier; + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/UserHealth.java b/backbone/src/main/java/org/researchstack/backbone/model/UserHealth.java new file mode 100644 index 000000000..31f6949f0 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/UserHealth.java @@ -0,0 +1,105 @@ +package org.researchstack.backbone.model; + +import android.content.Context; + +import org.researchstack.backbone.R; + +import java.io.Serializable; + +/** + * Created by TheMDP on 2/15/17. + * + * This should mimic the HealthKit values store on iOS + */ + +public class UserHealth implements Serializable { + + public static final float NO_VALUE = -1.0f; + + /** + * Weight in lbs + */ + private float weight = NO_VALUE; + + /** + * Height in inches + */ + private float height = NO_VALUE; + + /** + * Gender of the user + */ + private Gender gender = Gender.NOT_SET; + + /** Default constructor for Serializable */ + protected UserHealth() {} + + /** + * @return valid weight measurement if it has been entered by the user + * it will return -1 if no valid weight is available + */ + public float getWeight() { + return weight; + } + + /** + * @return true if a valid weight measurement has been entered by the user, false otherwise + */ + public boolean hasWeight() { + return !(weight < 0); + } + + public void setWeight(float weight) { + this.weight = weight; + } + + /** + * @return valid height measurement if it has been entered by the user + * it will return -1 if no valid height is available + */ + public float getHeight() { + return height; + } + + /** + * @return true if a valid height measurement has been entered by the user, false otherwise + */ + public boolean hasHeight() { + return !(height < 0); + } + + public void setHeight(float height) { + this.height = height; + } + + /** + * @return gender of the user, defaults to NOT_SET + */ + public Gender getGender() { + return gender; + } + + public void setGender(Gender gender) { + this.gender = gender; + } + + public enum Gender { + MALE, + FEMALE, + OTHER, + NOT_SET; + + public String localizedTitle(Context context) { + switch (this) { + case MALE: + return context.getString(R.string.rsb_GENDER_MALE); + case FEMALE: + return context.getString(R.string.rsb_GENDER_FEMALE); + case OTHER: + return context.getString(R.string.rsb_GENDER_OTHER); + default: + return NOT_SET.toString(); // no need to localize + } + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/ActiveStepSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/ActiveStepSurveyItem.java new file mode 100644 index 000000000..eb42ecfdf --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/ActiveStepSurveyItem.java @@ -0,0 +1,79 @@ +package org.researchstack.backbone.model.survey; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; + +/** + * Created by TheMDP on 12/31/16. + */ + +public class ActiveStepSurveyItem extends SurveyItem { + + @SerializedName("stepSpokenInstruction") + private String stepSpokenInstruction; + + @SerializedName("stepFinishedSpokenInstruction") + private String stepFinishedSpokenInstruction; + + @SerializedName(value="stepDuration", alternate={"duration"}) + private int stepDuration; + + /** + * A map of <"time_in_seconds_to_speak", "what_to_speak"> + */ + @SerializedName("spokenInstructions") + private Map spokenInstructionMap; + + /** + * A string representation of a raw file resource, + * it will play on start() of ActiveStepLayout + */ + @SerializedName("soundRes") + private String soundRes; + + /* Default constructor needed for serilization/deserialization of object */ + public ActiveStepSurveyItem() { + super(); + } + + public String getStepSpokenInstruction() { + return stepSpokenInstruction; + } + + public void setStepSpokenInstruction(String stepSpokenInstruction) { + this.stepSpokenInstruction = stepSpokenInstruction; + } + + public String getStepFinishedSpokenInstruction() { + return stepFinishedSpokenInstruction; + } + + public void setStepFinishedSpokenInstruction(String stepFinishedSpokenInstruction) { + this.stepFinishedSpokenInstruction = stepFinishedSpokenInstruction; + } + + public int getStepDuration() { + return stepDuration; + } + + public void setStepDuration(int stepDuration) { + this.stepDuration = stepDuration; + } + + public Map getSpokenInstructionMap() { + return spokenInstructionMap; + } + + public void setSpokenInstructionMap(Map spokenInstructions) { + spokenInstructionMap = spokenInstructions; + } + + public String getSoundRes() { + return soundRes; + } + + public void setSoundRes(String soundRes) { + this.soundRes = soundRes; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/BaseSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/BaseSurveyItem.java new file mode 100644 index 000000000..81ea99d1b --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/BaseSurveyItem.java @@ -0,0 +1,12 @@ +package org.researchstack.backbone.model.survey; + +/** + * Created by TheMDP on 1/2/17. + */ + +public class BaseSurveyItem extends SurveyItem { + /* Default constructor needed for serilization/deserialization of object */ + BaseSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/BooleanQuestionSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/BooleanQuestionSurveyItem.java new file mode 100644 index 000000000..768031906 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/BooleanQuestionSurveyItem.java @@ -0,0 +1,14 @@ +package org.researchstack.backbone.model.survey; + +import org.researchstack.backbone.model.Choice; + +/** + * Created by TheMDP on 1/3/17. + */ + +public class BooleanQuestionSurveyItem extends QuestionSurveyItem> { + /* Default constructor needed for serialization/deserialization of object */ + public BooleanQuestionSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/ChoiceQuestionSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/ChoiceQuestionSurveyItem.java new file mode 100644 index 000000000..5ea73892a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/ChoiceQuestionSurveyItem.java @@ -0,0 +1,14 @@ +package org.researchstack.backbone.model.survey; + +import org.researchstack.backbone.model.Choice; + +/** + * Created by TheMDP on 1/2/17. + */ + +public class ChoiceQuestionSurveyItem extends QuestionSurveyItem { + /* Default constructor needed for serilization/deserialization of object */ + ChoiceQuestionSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/ConsentReviewSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/ConsentReviewSurveyItem.java new file mode 100644 index 000000000..c91760504 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/ConsentReviewSurveyItem.java @@ -0,0 +1,12 @@ +package org.researchstack.backbone.model.survey; + +/** + * Created by TheMDP on 1/3/17. + */ + +public class ConsentReviewSurveyItem extends ProfileSurveyItem { + /* Default constructor needed for serilization/deserialization of object */ + ConsentReviewSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/ConsentSharingOptionsSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/ConsentSharingOptionsSurveyItem.java new file mode 100644 index 000000000..d234db000 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/ConsentSharingOptionsSurveyItem.java @@ -0,0 +1,23 @@ +package org.researchstack.backbone.model.survey; + +import com.google.gson.annotations.SerializedName; + +import org.researchstack.backbone.model.Choice; + +/** + * Created by TheMDP on 1/3/17. + */ + +public class ConsentSharingOptionsSurveyItem extends SurveyItem> { + @SerializedName("investigatorShortDescription") + public String investigatorShortDescription; + @SerializedName("investigatorLongDescription") + public String investigatorLongDescription; + @SerializedName("learnMoreHTMLContentURL") + public String learnMoreHTMLContentURL; + + /* Default constructor needed for serilization/deserialization of object */ + ConsentSharingOptionsSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/DateRangeSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/DateRangeSurveyItem.java new file mode 100644 index 000000000..b9946b034 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/DateRangeSurveyItem.java @@ -0,0 +1,14 @@ +package org.researchstack.backbone.model.survey; + +import java.util.Date; + +/** + * Created by TheMDP on 1/3/17. + */ + +public class DateRangeSurveyItem extends RangeSurveyItem { + /* Default constructor needed for serilization/deserialization of object */ + DateRangeSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/FloatRangeSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/FloatRangeSurveyItem.java new file mode 100644 index 000000000..ead88d96d --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/FloatRangeSurveyItem.java @@ -0,0 +1,12 @@ +package org.researchstack.backbone.model.survey; + +/** + * Created by TheMDP on 1/3/17. + */ + +public class FloatRangeSurveyItem extends RangeSurveyItem { + /* Default constructor needed for serilization/deserialization of object */ + FloatRangeSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/FormSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/FormSurveyItem.java new file mode 100644 index 000000000..9f49d168b --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/FormSurveyItem.java @@ -0,0 +1,31 @@ +package org.researchstack.backbone.model.survey; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by TheMDP on 1/3/17. + */ + +public class FormSurveyItem extends QuestionSurveyItem { + + /** + * When the expectedAnswer is this String, skipToStepIdentifier will be invoked + * If the StepResult does not contain any valid results, aka "Skip" button was clicked + */ + public static final String SKIP_BUTTON_TAPPED_ACTION_IDENTIFIER = "whenSkipButtonClicked"; + + @SerializedName("skipTitle") + public String skipTitle; + + /** + * If true, the first question body layout with an edittext will receive focus on load + * default is false and nothing will occur + */ + @SerializedName("autoFocusFirst") + public Boolean autoFocusFirstEditText; + + /* Default constructor needed for serilization/deserialization of object */ + public FormSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/InstructionSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/InstructionSurveyItem.java new file mode 100644 index 000000000..31b672fb8 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/InstructionSurveyItem.java @@ -0,0 +1,53 @@ +package org.researchstack.backbone.model.survey; + +import android.widget.ImageView; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by TheMDP on 12/31/16. + */ + +public class InstructionSurveyItem extends SurveyItem { + + @SerializedName("detailText") + public String detailText; + + @SerializedName("image") + public String image; + + @SerializedName("isImageAnimated") + public boolean isImageAnimated; + + @SerializedName("animationRepeatDuration") + public long animationRepeatDuration; + + @SerializedName("iconImage") + public String iconImage; + + /** + * Pointer to the next step to show after this one. If nil, then the next step + * is determined by the navigation rules setup by NavigableOrderedTask. + */ + @SerializedName("nextIdentifier") + public String nextIdentifier; + + @SerializedName("learnMoreHTMLContentURL") + public String learnMoreHTMLContentURL; + + /** + * Defaults to centerInside, but can be any scale type in the form of + * CENTER, CENTER_CROP, etc... + */ + @SerializedName("scaleType") + public ImageView.ScaleType scaleType; + + /* Default constructor needed for serialization/deserialization of object */ + public InstructionSurveyItem() { + super(); + } + + public boolean usesNavigation() { + return nextIdentifier != null; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/IntegerRangeSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/IntegerRangeSurveyItem.java new file mode 100644 index 000000000..ce87d29f0 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/IntegerRangeSurveyItem.java @@ -0,0 +1,18 @@ +package org.researchstack.backbone.model.survey; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by TheMDP on 1/3/17. + */ + +public class IntegerRangeSurveyItem extends RangeSurveyItem { + + @SerializedName("maxLength") + public Integer maxLength; + + /* Default constructor needed for serilization/deserialization of object */ + IntegerRangeSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/ProfileSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/ProfileSurveyItem.java new file mode 100644 index 000000000..19c6189ce --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/ProfileSurveyItem.java @@ -0,0 +1,12 @@ +package org.researchstack.backbone.model.survey; + +/** + * Created by TheMDP on 1/2/17. + */ + +public class ProfileSurveyItem extends SurveyItem { + /* Default constructor needed for serilization/deserialization of object */ + ProfileSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/QuestionSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/QuestionSurveyItem.java new file mode 100644 index 000000000..578011ae1 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/QuestionSurveyItem.java @@ -0,0 +1,55 @@ +package org.researchstack.backbone.model.survey; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +/** + * Created by TheMDP on 12/31/16. + */ + +public class QuestionSurveyItem extends SurveyItem { + + @SerializedName("questionStyle") + public boolean questionStyle; + @SerializedName("placeholderText") + public String placeholderText; + @SerializedName("optional") + public boolean optional; + @SerializedName("range") + public RangeSurveyItem range; + + @SerializedName(value="expectedAnswer", alternate={"matchingAnswer"}) + public Object expectedAnswer; + + @SerializedName("skipIdentifier") + public String skipIdentifier; + + @SerializedName("skipIfPassed") + public boolean skipIfPassed; + + /* Default constructor needed for serialization/deserialization of object */ + public QuestionSurveyItem() { + super(); + } + + public boolean isValidQuestionItem() { + return identifier != null && type.isQuestionSubtype(); + } + + public boolean isCompoundStep() { + return type == SurveyItemType.QUESTION_FORM; + } + + /** + * @return false by default, true if this question survey item + * can be used to create a QuestionStep that will implement + * an interface in NavigableOrderedTask + */ + public boolean usesNavigation() { + if (skipIdentifier != null || expectedAnswer != null) { + return true; + } + return false; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/RangeSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/RangeSurveyItem.java new file mode 100644 index 000000000..f64b2660a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/RangeSurveyItem.java @@ -0,0 +1,23 @@ +package org.researchstack.backbone.model.survey; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by TheMDP on 12/31/16. + */ + +public class RangeSurveyItem extends QuestionSurveyItem { + @SerializedName("min") + public T min; + @SerializedName("max") + public T max; + @SerializedName("defaultValue") + public T defaultValue; + @SerializedName("unit") + public String unit; + + /* Default constructor needed for serialization/deserialization of object */ + RangeSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/ScaleQuestionSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/ScaleQuestionSurveyItem.java new file mode 100644 index 000000000..ab370ef93 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/ScaleQuestionSurveyItem.java @@ -0,0 +1,19 @@ +package org.researchstack.backbone.model.survey; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by TheMDP on 1/3/17. + * + * Represents a slider that can go from integer min to integer high, at a certain step value + */ + +public class ScaleQuestionSurveyItem extends IntegerRangeSurveyItem { + @SerializedName("step") + public int step; + + /* Default constructor needed for serilization/deserialization of object */ + ScaleQuestionSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/SubtaskQuestionSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/SubtaskQuestionSurveyItem.java new file mode 100644 index 000000000..a253d89b5 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/SubtaskQuestionSurveyItem.java @@ -0,0 +1,13 @@ +package org.researchstack.backbone.model.survey; + +/** + * Created by TheMDP on 1/2/17. + */ + +public class SubtaskQuestionSurveyItem extends QuestionSurveyItem { + + /* Default constructor needed for serilization/deserialization of object */ + SubtaskQuestionSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/SurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/SurveyItem.java new file mode 100644 index 000000000..bde5c9b47 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/SurveyItem.java @@ -0,0 +1,126 @@ +package org.researchstack.backbone.model.survey; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.List; + +/** + * Created by TheMDP on 12/31/16. + * + * Generic Type "T" of survey item must also implement Serializable, + * Otherwise you will see a runtime exception + */ + +public class SurveyItem implements Serializable { + + public static final String IDENTIFIER_GSON = "identifier"; + @SerializedName(IDENTIFIER_GSON) + public String identifier; + + public static final String TYPE_GSON = "type"; + public static final String TYPE_GSON_2 = "dataType"; + @SerializedName(value=TYPE_GSON, alternate={TYPE_GSON_2}) + public SurveyItemType type; + + @SerializedName(value="title", alternate={"prompt"}) + public String title; + + @SerializedName("text") + public String text; + + @SerializedName("footnote") + public String footnote; + + @SerializedName(value="items", alternate={"choices", "inputFields"}) + public List items; + + /** + * This holds the original Json Object that was used to create this object + * Only is set if this object was created with the SurveyItemAdapter + */ + private transient String rawJson; + + /** + * This is simply used to keep track of state for the SurveyItemFactory, and will not be serialized + */ + private String customSurveyItemType; + + /* Default constructor needed for serilization/deserialization of object */ + protected SurveyItem() { + super(); + } + + public static class SurveyItemTypeComparator implements Comparator { + + @Override + public int compare(SurveyItemType lhs, SurveyItemType rhs) { + + return 0; + } + } + + public String getTypeIdentifier() { + if (isCustomStep()) { + return customSurveyItemType; + } + return type.getValue(); + } + + public String getIdentifier() { + if (identifier == null) { + return getTypeIdentifier(); + } + return identifier; + } + + @Override + public int hashCode() { + if (identifier == null) { + return super.hashCode(); + } + return identifier.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SurveyItem)) { + return false; + } + if (obj == this) { + return true; + } + + SurveyItem rhs = (SurveyItem) obj; + + if (identifier == null || rhs.identifier == null) { + return false; + } + + return identifier.equals(rhs.identifier); + } + + protected void setCustomTypeValue(String value) { + customSurveyItemType = value; + } + + public String getCustomTypeValue() { + return customSurveyItemType; + } + + public void setRawJson(String json) { + this.rawJson = json; + } + + public String getRawJson() { + return rawJson; + } + + /** + * @return true if the survey item type is custom, false otherwise + */ + public boolean isCustomStep() { + return customSurveyItemType != null; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/SurveyItemAdapter.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/SurveyItemAdapter.java new file mode 100644 index 000000000..23ef4f6a1 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/SurveyItemAdapter.java @@ -0,0 +1,155 @@ +package org.researchstack.backbone.model.survey; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; + +/** + * Created by TheMDP on 1/2/17. + * + * This class is the deserializer for SurveyItem classes + * It looks at the "type" field, attempts to map it to this library's pre-defined types + * and if it does not find it, creates a custom survey item + * the class of the custom survey item can easily be controlled by overriding this + * adapter, and overriding the method getCustomClass + * + * To go even further and change the mapping of the custom survey item to a custom step, + * you should override SurveyFactory's method public Step createCustomStep(SurveyItem item) + * which is the go to for converting a survey item to a step + */ + +public class SurveyItemAdapter implements JsonDeserializer { + + @Override + public SurveyItem deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + + JsonElement typeJson = jsonObject.get(SurveyItem.TYPE_GSON); + if (typeJson == null) { + typeJson = jsonObject.get(SurveyItem.TYPE_GSON_2); + } + SurveyItemType surveyItemType = context.deserialize(typeJson, SurveyItemType.class); + + // This was a custom survey item type + // For instance, "reconsent.instruction" is a subtask consent type + // That will be dealt with by a custom ConsentDocumentSurveyFactory + String customTypeString = null; + if (surveyItemType == null) { + surveyItemType = SurveyItemType.CUSTOM; + // Some JSON can be malformed and there is no type, only identifier + // In that case let's try and still parse it by setting the type as the identifier; + if (typeJson != null) { + customTypeString = typeJson.getAsString(); + } else { // use "identifier" string + customTypeString = jsonObject.get(SurveyItem.IDENTIFIER_GSON).getAsString(); + } + } + + SurveyItem item = null; + + switch (surveyItemType) { + case INSTRUCTION: + case INSTRUCTION_COMPLETION: + item = context.deserialize(json, InstructionSurveyItem.class); + break; + case SUBTASK: + item = context.deserialize(json, SubtaskQuestionSurveyItem.class); + break; + case QUESTION_FORM: + item = context.deserialize(json, FormSurveyItem.class); + break; + case QUESTION_BOOLEAN: + item = context.deserialize(json, BooleanQuestionSurveyItem.class); + break; + case QUESTION_DECIMAL: + item = context.deserialize(json, FloatRangeSurveyItem.class); + break; + case QUESTION_INTEGER: + item = context.deserialize(json, IntegerRangeSurveyItem.class); + break; + case QUESTION_DURATION: + break; + case QUESTION_SCALE: + item = context.deserialize(json, ScaleQuestionSurveyItem.class); + break; + case QUESTION_TEXT: + case QUESTION_EMAIL: + item = context.deserialize(json, TextfieldSurveyItem.class); + break; + case QUESTION_DATE: + case QUESTION_DATE_TIME: + case QUESTION_TIME: + item = context.deserialize(json, DateRangeSurveyItem.class); + break; + case QUESTION_MULTIPLE_CHOICE: + case QUESTION_SINGLE_CHOICE: + item = context.deserialize(json, ChoiceQuestionSurveyItem.class); + break; + case QUESTION_TIMING_RANGE: + item = context.deserialize(json, TimingRangeQuestionSurveyItem.class); + break; + case CONSENT_SHARING_OPTIONS: + item = context.deserialize(json, ConsentSharingOptionsSurveyItem.class); + break; + case CONSENT_REVIEW: + item = context.deserialize(json, ConsentReviewSurveyItem.class); + break; + case CONSENT_VISUAL: + break; + case RE_CONSENT: + item = context.deserialize(json, InstructionSurveyItem.class); + break; + case ACCOUNT_REGISTRATION: + case ACCOUNT_LOGIN: + case ACCOUNT_LOGIN_VIA_EMAIL: + case ACCOUNT_PROFILE: + case ACCOUNT_EXTERNAL_ID: + item = context.deserialize(json, ProfileSurveyItem.class); + break; + case ACCOUNT_COMPLETION: + case ACCOUNT_EMAIL_VERIFICATION: + item = context.deserialize(json, InstructionSurveyItem.class); + break; + case ACCOUNT_DATA_GROUPS: + case ACCOUNT_PERMISSIONS: + case PASSCODE: + break; + case SHARE_THE_APP: + item = context.deserialize(json, InstructionSurveyItem.class); + break; + case ACTIVE_STEP: + item = context.deserialize(json, ActiveStepSurveyItem.class); + break; + case CUSTOM: + item = context.deserialize(json, getCustomClass(customTypeString, json)); + item.type = surveyItemType; // need to set CUSTOM type for surveyItem, since it is a special case + item.setCustomTypeValue(customTypeString); + break; + } + + if (item == null) { + item = context.deserialize(json, BaseSurveyItem.class); + item.type = surveyItemType; + } + + item.setRawJson(json.toString()); + + return item; + } + + /** + * This can be overridden by subclasses to provide custom survey item deserialization + * the default deserialization is always an instruction survey item + * @param customType used to map to different types of survey items + * @param json if customType is not enough, you can use the JsonElement to determine how to parse + * it's contents by peeking at it's variables + * @return type of survey item to create from the custom class + */ + public Class getCustomClass(String customType, JsonElement json) { + return BaseSurveyItem.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/SurveyItemType.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/SurveyItemType.java new file mode 100644 index 000000000..ccaaa9e7a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/SurveyItemType.java @@ -0,0 +1,118 @@ +package org.researchstack.backbone.model.survey; + +import com.google.gson.annotations.SerializedName; + +import org.researchstack.backbone.model.survey.factory.ConsentDocumentFactory; + +/** + * Created by TheMDP on 12/31/16. + */ + +public enum SurveyItemType { + + CUSTOM(null), + // Subtask subtypes + @SerializedName("subtask") + SUBTASK ("subtask"), // SubtaskStep + // Instruction subtypes + @SerializedName("instruction") + INSTRUCTION ("instruction"), // InstructionStep + @SerializedName("completion") + INSTRUCTION_COMPLETION ("completion"), // CompletionStep + @SerializedName(value="compound", alternate={"form", "toggle"}) + QUESTION_FORM("compound"), // QuestionSteps > 1 + @SerializedName("boolean") + QUESTION_BOOLEAN ("boolean"), // ORKBooleanAnswerFormat + @SerializedName("singleChoiceText") + QUESTION_SINGLE_CHOICE ("singleChoiceText"), // ORKTextChoiceAnswerFormat of style SingleChoiceTextQuestion + @SerializedName("multipleChoiceText") + QUESTION_MULTIPLE_CHOICE ("multipleChoiceText"), // ORKTextChoiceAnswerFormat of style MultipleChoiceTextQuestion + @SerializedName("textfield") + QUESTION_TEXT ("textfield"), // ORKTextAnswerFormat + @SerializedName("emailTextField") + QUESTION_EMAIL ("emailTextField"), // EmailAnswerFormat + @SerializedName("datePicker") + QUESTION_DATE ("datePicker"), // ORKDateAnswerFormat of style Date + @SerializedName("timeAndDatePicker") + QUESTION_DATE_TIME ("timeAndDatePicker"), // ORKDateAnswerFormat of style DateTime + @SerializedName("timePicker") + QUESTION_TIME ("timePicker"), // ORKTimeOfDayAnswerFormat + @SerializedName("timeInterval") + QUESTION_DURATION ("timeInterval"), // ORKTimeIntervalAnswerFormat + @SerializedName(value="numericInteger", alternate={"integer"}) + QUESTION_INTEGER ("numericInteger"), // ORKNumericAnswerFormat of style Integer + @SerializedName("numericDecimal") + QUESTION_DECIMAL ("numericDecimal"), // ORKNumericAnswerFormat of style Decimal + @SerializedName("scaleInteger") + QUESTION_SCALE ("scaleInteger"), // ORKScaleAnswerFormat + @SerializedName("timingRange") + QUESTION_TIMING_RANGE ("timingRange"), // Timing Range: ORKTextChoiceAnswerFormat of style SingleChoiceTextQuestion + // Account subtypes + @SerializedName("registration") + ACCOUNT_REGISTRATION ("registration" ), // ProfileStep + @SerializedName("login") + ACCOUNT_LOGIN ("login" ), // LoginStep + @SerializedName("loginViaEmail") + ACCOUNT_LOGIN_VIA_EMAIL ("loginViaEmail" ), // LoginStep + @SerializedName("emailVerification") + ACCOUNT_EMAIL_VERIFICATION ("emailVerification" ), // EmailVerificationStep + @SerializedName("externalID") + ACCOUNT_EXTERNAL_ID ("externalID" ), // ExternalIDStep + @SerializedName("permissions") + ACCOUNT_PERMISSIONS ("permissions" ), // PermissionsStep + @SerializedName("onboardingCompletion") + ACCOUNT_COMPLETION ("onboardingCompletion"), // OnboardingCompletionStep + @SerializedName("dataGroups") + ACCOUNT_DATA_GROUPS ("dataGroups"), // DataGroupsStep + @SerializedName("profile") + ACCOUNT_PROFILE ("profile"), // ProfileQuestionStep or ProfileFormStep + @SerializedName("shareApp") + SHARE_THE_APP ("shareApp"), // ShareTheAppStep + // Passcode subtypes + @SerializedName(value="PASSCODE", alternate={"passcodeType4Digit", "passcodeType6Digit"}) + PASSCODE ("passcode"), // iOS has 6 digit too, but for now only support 4 digit + // Active Step + @SerializedName("active") + ACTIVE_STEP ("active"), // ActiveStep + + + // Consent subtypes + @SerializedName(ConsentDocumentFactory.CONSENT_SHARING_IDENTIFIER) + CONSENT_SHARING_OPTIONS (ConsentDocumentFactory.CONSENT_SHARING_IDENTIFIER), // ConsentSharingStep + @SerializedName(ConsentDocumentFactory.CONSENT_REVIEW_IDENTIFIER) + CONSENT_REVIEW (ConsentDocumentFactory.CONSENT_REVIEW_IDENTIFIER), // ConsentReviewStep + @SerializedName ("consentVisual") + CONSENT_VISUAL ("consentVisual"), // VisualConsentStep + @SerializedName ("reconsent.instruction") + RE_CONSENT ("reconsent.instruction"); // ReconsentInstructionStep + + SurveyItemType(String rawValue) { + value = rawValue; + } + + private String value; + public String getValue() { + return value; + } + + public boolean isQuestionSubtype() { + switch (this) { + case QUESTION_FORM: + case QUESTION_BOOLEAN: + case QUESTION_SINGLE_CHOICE: + case QUESTION_MULTIPLE_CHOICE: + case QUESTION_TEXT: + case QUESTION_DATE: + case QUESTION_DATE_TIME: + case QUESTION_TIME: + case QUESTION_DURATION: + case QUESTION_INTEGER: + case QUESTION_DECIMAL: + case QUESTION_SCALE: + case QUESTION_TIMING_RANGE: + return true; + default: + return false; + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/TextfieldSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/TextfieldSurveyItem.java new file mode 100644 index 000000000..e3d92c322 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/TextfieldSurveyItem.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.researchstack.backbone.model.survey; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by TheMDP on 11/17/17. + */ + +public class TextfieldSurveyItem extends QuestionSurveyItem { + + @SerializedName("inputType") + public Integer inputType; + + @SerializedName("validationRegex") + public String validationRegex; + + @SerializedName("disabled") + public Boolean disabled; + + @SerializedName("isMultipleLines") + public Boolean isMultipleLines; + + @SerializedName("maxLength") + public Integer maxLength; + + /* Default constructor needed for serialization/deserialization of object */ + public TextfieldSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/TimingRangeQuestionSurveyItem.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/TimingRangeQuestionSurveyItem.java new file mode 100644 index 000000000..e8592b8e3 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/TimingRangeQuestionSurveyItem.java @@ -0,0 +1,12 @@ +package org.researchstack.backbone.model.survey; + +/** + * Created by TheMDP on 3/25/17. + */ + +public class TimingRangeQuestionSurveyItem extends QuestionSurveyItem { + /* Default constructor needed for serialization/deserialization of object */ + public TimingRangeQuestionSurveyItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/factory/ConsentDocumentFactory.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/factory/ConsentDocumentFactory.java new file mode 100644 index 000000000..c4db492d1 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/factory/ConsentDocumentFactory.java @@ -0,0 +1,378 @@ +package org.researchstack.backbone.model.survey.factory; + +import android.content.Context; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.answerformat.AnswerFormat; +import org.researchstack.backbone.answerformat.ChoiceAnswerFormat; +import org.researchstack.backbone.model.Choice; +import org.researchstack.backbone.model.ConsentDocument; +import org.researchstack.backbone.model.ConsentSection; +import org.researchstack.backbone.model.ProfileInfoOption; +import org.researchstack.backbone.model.survey.ConsentReviewSurveyItem; +import org.researchstack.backbone.model.survey.ConsentSharingOptionsSurveyItem; +import org.researchstack.backbone.model.survey.InstructionSurveyItem; +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.onboarding.OnboardingSection; +import org.researchstack.backbone.onboarding.ReConsentInstructionStep; +import org.researchstack.backbone.step.ConsentDocumentStep; +import org.researchstack.backbone.step.ConsentReviewSubstepListStep; +import org.researchstack.backbone.step.ConsentSharingStep; +import org.researchstack.backbone.step.ConsentSignatureStep; +import org.researchstack.backbone.step.ConsentSubtaskStep; +import org.researchstack.backbone.step.ConsentVisualStep; +import org.researchstack.backbone.step.ProfileStep; +import org.researchstack.backbone.step.RegistrationStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.SubtaskStep; +import org.researchstack.backbone.step.NavigationSubtaskStep; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 12/29/16. + */ + +public class ConsentDocumentFactory extends SurveyFactory { + + public static final String CONSENT_SUBTASK_ID = "consent"; + public static final String CONSENT_SIGNATURE_IDENTIFIER = "consentSignature"; + public static final String CONSENT_REVIEW_PROFILE_IDENTIFIER = "consentReviewProfile"; + public static final String CONSENT_SHARING_IDENTIFIER = "consentSharingOptions"; + public static final String CONSENT_REVIEW_IDENTIFIER = "consentReview"; + + /** + * A list of steps that is used to track the state of Survey Factory so + * They can be used to create different types of Tasks + */ + private List stepList; + + private ConsentDocument consentDocument; + + public ConsentDocumentFactory() { + super(); + stepList = new ArrayList<>(); + } + + /** + * @param document consent document, already de-serialized + */ + public ConsentDocumentFactory(ConsentDocument document) { + this(); + consentDocument = document; + } + + /** + * @param document consent document, already deserialized + * @param customStepCreator override to control step creation from custom survey items + */ + public ConsentDocumentFactory( + ConsentDocument document, + CustomStepCreator customStepCreator) + { + this(); + consentDocument = document; + setCustomStepCreator(customStepCreator); + } + + @Override + public List createSurveySteps(Context context, List surveyItems) { + stepList = new ArrayList<>(); + return super.createSurveySteps(context, surveyItems); + } + + /** + * This is how we can assess our own types for consent + * @param item SurveyItem from JSON + * @return valid Step matching the SurveyItem + */ + @Override + public Step createSurveyStep(Context context, SurveyItem item, boolean isSubtaskStep) { + Step step = null; + switch (item.type) { + case CONSENT_REVIEW: + if (!(item instanceof ConsentReviewSurveyItem)) { + throw new IllegalStateException("Error in json parsing, CONSENT_REVIEW types must be ConsentReviewSurveyItem"); + } + if (consentDocument == null) { + throw new IllegalStateException("Consent document cannot be null!"); + } + step = createConsentReviewSteps(context, (ConsentReviewSurveyItem)item); + break; + case CONSENT_SHARING_OPTIONS: + if (!(item instanceof ConsentSharingOptionsSurveyItem)) { + throw new IllegalStateException("Error in json parsing, CONSENT_SHARING_OPTIONS types must be ConsentSharingOptionsSurveyItem"); + } + step = createConsentSharingStep(context, (ConsentSharingOptionsSurveyItem)item); + break; + case CONSENT_VISUAL: + if (consentDocument == null) { + throw new IllegalStateException("Consent document cannot be null!"); + } + step = createConsentVisualSteps(item, consentDocument.getSections()); + break; + case RE_CONSENT: + if (!(item instanceof InstructionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, RE_CONSENT types must be InstructionSurveyItem"); + } + step = createReConsentInstructionStep((InstructionSurveyItem)item); + break; + } + + if (step == null) { + step = super.createSurveyStep(context, item, isSubtaskStep); + } + + // Maintain a list that can then be used to create different tasks from this stepList + // Only add root steps, aka if they are not already in a subtask step + if (step != null && !isSubtaskStep) { + stepList.add(step); + } + + return step; + } + + /** + * Creates consent review steps, which can be a total of name, birthdate step, + * SignatureStep, but always a consent doc review step + * @param context can be any context, activity or application, used to access "R" resources + * @param item ConsentReviewSurveyItem used to create steps + * @return ConsentReviewSubstepListStep used for consent review + */ + public ConsentReviewSubstepListStep createConsentReviewSteps(Context context, ConsentReviewSurveyItem item) { + List stepList = new ArrayList<>(); + + if (item.items == null) { + // Default to name and birthdate + item.items = new ArrayList<>(); + item.items.add(ProfileInfoOption.NAME.getIdentifier()); + item.items.add(ProfileInfoOption.BIRTHDATE.getIdentifier()); + } + + // This is a deprecated way of supplying requiresName and requiresBirthdate + // But we are going to support it for now, but use "name", and "birthdate" moving forward + if (consentDocument.getDocumentProperties() != null) { + // Check if birthdate and name are required, and remove them if they are not + if (!consentDocument.getDocumentProperties().requiresName() && item.items != null) { + item.items.remove(ProfileInfoOption.NAME.getIdentifier()); + } + if (!consentDocument.getDocumentProperties().requiresBirthdate()) { + item.items.remove(ProfileInfoOption.BIRTHDATE.getIdentifier()); + } + } + + // Only create the profile step if there is at least one consent required item + if (item.items != null && !item.items.isEmpty()) { + String oldIdentifier = new String(item.identifier); + item.identifier = CONSENT_REVIEW_PROFILE_IDENTIFIER; + // This will create a profile step with name, and birthday, or w/e is in JSON + ProfileStep profileStep = super.createProfileStep(context, item); + profileStep.setOptional(false); + stepList.add(profileStep); + item.identifier = oldIdentifier; + } + + if (consentDocument.getDocumentProperties() != null && + consentDocument.getDocumentProperties().requiresSignature()) + { + // Add Consent Signature + stepList.add(createConsentSignatureStep(context, item)); + } + + // Find the html consent review doc, since it can come from a deprecated or modern place + String htmlConsentDoc = consentDocument.getHtmlReviewContent(); // deprecated way + if (htmlConsentDoc == null) { // more modern way + for (ConsentSection section : consentDocument.getSections()) { + if (section.getType() == ConsentSection.Type.OnlyInDocument) { + htmlConsentDoc = section.getHtmlContent(); + } + } + } + // Add Consent document review step here + ConsentDocumentStep docReviewStep = new ConsentDocumentStep(item.identifier); + docReviewStep.setConsentHTML(htmlConsentDoc); + stepList.add(docReviewStep); + + return new ConsentReviewSubstepListStep(item.identifier, stepList); + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item ConsentSharingOptionsSurveyItem that may have Sharing option choices + * @return ConsentSharingStep for designating how to share the user's data + */ + public ConsentSharingStep createConsentSharingStep(Context context, ConsentSharingOptionsSurveyItem item) { + AnswerFormat format = null; + if (item.items != null && !item.items.isEmpty()) { + Choice[] choices = item.items.toArray(new Choice[item.items.size()]); + format = new ChoiceAnswerFormat(AnswerFormat.ChoiceAnswerStyle.SingleChoice, choices); + } else if (item.investigatorLongDescription != null) { + String shareWidely = context.getString(R.string.rsb_consent_share_widely, item.investigatorLongDescription); + Choice shareWidelyChoice = new Choice<>(shareWidely, true); + String shareRestricted = context.getString(R.string.rsb_consent_share_only, item.investigatorLongDescription); + Choice shareRestrictedChoice = new Choice<>(shareRestricted, false); + Choice[] choices = new Choice[] { shareWidelyChoice, shareRestrictedChoice }; + format = new ChoiceAnswerFormat(AnswerFormat.ChoiceAnswerStyle.SingleChoice, choices); + } + ConsentSharingStep step; + if (format == null) { + step = new ConsentSharingStep(CONSENT_SHARING_IDENTIFIER); + } else { + step = new ConsentSharingStep(CONSENT_SHARING_IDENTIFIER, item.title, format); + } + + if (item.title == null) { + step.setTitle(context.getString(R.string.rsb_consent_share_title)); + } + + if (item.text != null) { + step.setText(item.text); + } else if (item.investigatorLongDescription != null) { + if (item.learnMoreHTMLContentURL != null) { + step.setText(context.getString(R.string.rsb_consent_share_description, + item.investigatorLongDescription, + item.learnMoreHTMLContentURL)); + } + } + + return step; + } + + /** + * @param item the survey item to be converted into a step + * @param sections used to create the ConsentVisualSteps + * @return Ordered list of ConsentVisualSteps + */ + public SubtaskStep createConsentVisualSteps(SurveyItem item, List sections) { + List stepList = new ArrayList<>(); + int customIdx = 1; + int sectionIdx = 0; + for (ConsentSection section : sections) { + // OnlyInDocument is used to create the ConsentDocumentStep later on + if (section.getType() != ConsentSection.Type.OnlyInDocument) { + ConsentVisualStep step; + if (section.getType() == ConsentSection.Type.Custom) { + step = createVisualStep(section.getTypeIdentifier() + customIdx, + sectionIdx, sections.size()); + customIdx++; + } else { + step = createVisualStep(section.getTypeIdentifier() + , sectionIdx, sections.size()); + } + step.setSection(section); + stepList.add(step); + sectionIdx++; + } + } + return new SubtaskStep(item.getTypeIdentifier(), stepList); + } + + /** + * This can be overridden by sub-classes to easily provide a custom ConsentVisualStep + * @param identifier the step identifier + * @param sectionIndex the index of this particular section within the sectionCount + * @param sectionCount the total number of consent visual sections + * @return a ConsentVisualStep to be included in the visual consent SubtaskStep + */ + protected ConsentVisualStep createVisualStep(String identifier, int sectionIndex, int sectionCount) { + return new ConsentVisualStep(identifier, sectionIndex, sectionCount); + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item used to create signature step + * @return ConsentSignatureStep + */ + public ConsentSignatureStep createConsentSignatureStep(Context context, ConsentReviewSurveyItem item) { + ConsentSignatureStep step = new ConsentSignatureStep(CONSENT_SIGNATURE_IDENTIFIER, item.title, item.text); + if (item.title == null) { + step.setTitle(context.getString(R.string.rsb_consent_signature_title)); + } + if (item.text == null) { + step.setTitle(context.getString(R.string.rsb_consent_signature_instruction)); + } + return step; + } + + /** + * @param item to make the reconsent step + * @return an instruction step that is shown with reconsent + */ + public ReConsentInstructionStep createReConsentInstructionStep(InstructionSurveyItem item) { + ReConsentInstructionStep step = new ReConsentInstructionStep(item.identifier, item.title, item.text); + fillInstructionStep(step, item); + return step; + } + + /** + * After all survey items have been processed into steps, use this method to get all + * ConsentVisualSteps + * @return list of all the ConsentVisualStep + */ + public List visualConsentSteps() { + List steps = new ArrayList<>(); + for (Step step : getSteps()) { + if (step instanceof ConsentVisualStep) { + steps.add(step); + } + } + return steps; + } + + public SubtaskStep reconsentStep() { + // Strip out the registration steps, and only leave the consent steps + List steps = new ArrayList<>(); + for (Step step : getSteps()) { + if (!isRegistrationStep(step)) { + steps.add(step); + } + } + return new NavigationSubtaskStep(OnboardingSection.CONSENT_IDENTIFIER, steps); + } + + boolean isRegistrationStep(Step step) { + // TODO: add ExternalIdStep here when it is created + return step instanceof RegistrationStep; // || step instanceof ExternalIdStep; + } + + /** + * @return subtask step with only the steps required for consent or reconsent on login + */ + public Step loginConsentStep() { + // Strip out the registration steps, and only leave the consent steps + List steps = new ArrayList<>(); + for (Step step : getSteps()) { + if (!isRegistrationStep(step)) { + steps.add(step); + } + } + // Consent subtask step will be skipped if user has already consented + return new ConsentSubtaskStep(OnboardingSection.CONSENT_IDENTIFIER, steps); + } + + /** + * Return subtask step with only the steps required for initial registration + * @return the consent step that is applicable during registration + */ + public Step registrationConsentStep() { + // If this is a step that conforms to the custom step protocol and the custom step type is + // a reconsent subtype, then this is not to be included in the registration steps + // Strip out the registration steps, and only leave the consent steps + List steps = new ArrayList<>(); + for (Step step : getSteps()) { + if (!(step instanceof ReConsentInstructionStep)) { + steps.add(step); + } + } + return new NavigationSubtaskStep(OnboardingSection.CONSENT_IDENTIFIER, steps); + } + + public void setConsentDocument(ConsentDocument consentDocument) { + this.consentDocument = consentDocument; + } + + private List getSteps() { + return stepList; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/survey/factory/SurveyFactory.java b/backbone/src/main/java/org/researchstack/backbone/model/survey/factory/SurveyFactory.java new file mode 100644 index 000000000..600e87a0a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/survey/factory/SurveyFactory.java @@ -0,0 +1,1064 @@ +package org.researchstack.backbone.model.survey.factory; + +import android.content.Context; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.annotation.VisibleForTesting; +import androidx.core.hardware.fingerprint.FingerprintManagerCompat; +import android.text.InputType; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.answerformat.AnswerFormat; +import org.researchstack.backbone.answerformat.BooleanAnswerFormat; +import org.researchstack.backbone.answerformat.ChoiceAnswerFormat; +import org.researchstack.backbone.answerformat.DateAnswerFormat; +import org.researchstack.backbone.answerformat.DecimalAnswerFormat; +import org.researchstack.backbone.answerformat.EmailAnswerFormat; +import org.researchstack.backbone.answerformat.GenderAnswerFormat; +import org.researchstack.backbone.answerformat.IntegerAnswerFormat; +import org.researchstack.backbone.answerformat.PasswordAnswerFormat; +import org.researchstack.backbone.answerformat.TextAnswerFormat; +import org.researchstack.backbone.model.Choice; +import org.researchstack.backbone.model.ProfileInfoOption; +import org.researchstack.backbone.model.TaskModel; +import org.researchstack.backbone.model.survey.ActiveStepSurveyItem; +import org.researchstack.backbone.model.survey.BooleanQuestionSurveyItem; +import org.researchstack.backbone.model.survey.ChoiceQuestionSurveyItem; +import org.researchstack.backbone.model.survey.FormSurveyItem; +import org.researchstack.backbone.model.survey.DateRangeSurveyItem; +import org.researchstack.backbone.model.survey.FloatRangeSurveyItem; +import org.researchstack.backbone.model.survey.IntegerRangeSurveyItem; +import org.researchstack.backbone.model.survey.ProfileSurveyItem; +import org.researchstack.backbone.model.survey.ScaleQuestionSurveyItem; +import org.researchstack.backbone.model.survey.InstructionSurveyItem; +import org.researchstack.backbone.model.survey.QuestionSurveyItem; +import org.researchstack.backbone.model.survey.SubtaskQuestionSurveyItem; +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.model.survey.SurveyItemType; +import org.researchstack.backbone.model.survey.TextfieldSurveyItem; +import org.researchstack.backbone.model.survey.TimingRangeQuestionSurveyItem; +import org.researchstack.backbone.onboarding.OnboardingSection; +import org.researchstack.backbone.step.CompletionStep; +import org.researchstack.backbone.step.EmailVerificationStep; +import org.researchstack.backbone.step.EmailVerificationSubStep; +import org.researchstack.backbone.step.FormStep; +import org.researchstack.backbone.step.InstructionStep; +import org.researchstack.backbone.step.LoginStep; +import org.researchstack.backbone.step.NavigationFormStep; +import org.researchstack.backbone.step.PasscodeStep; +import org.researchstack.backbone.step.PermissionsStep; +import org.researchstack.backbone.step.ProfileStep; +import org.researchstack.backbone.step.QuestionStep; +import org.researchstack.backbone.step.RegistrationStep; +import org.researchstack.backbone.step.ShareTheAppStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.SubtaskStep; +import org.researchstack.backbone.step.NavigationExpectedAnswerQuestionStep; +import org.researchstack.backbone.step.NavigationSubtaskStep; +import org.researchstack.backbone.step.active.ActiveStep; +import org.researchstack.backbone.task.SmartSurveyTask; +import org.researchstack.backbone.ui.ActiveTaskActivity; +import org.researchstack.backbone.ui.step.layout.PasscodeCreationStepLayout; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Created by TheMDP on 12/29/16. + * + * The SurveyFactory controls converting SurveyItem object to Step objects + * It accounts for all the variations specified in SurveyItemType when looping through each + * SurveyItem and storing the result in a field you can access using the getSteps method + * + * Note that SurveyItem objects should be create from JSON using the GSON library, + * and a SurveyItemAdapter class to do special de-serialization for the SurveyItems + */ + +public class SurveyFactory { + /** Singleton instance. */ + public static final SurveyFactory INSTANCE = new SurveyFactory(); + + // The rest of them use the toString of ProfileInfoOption + public static final String EMAIL_VERIFICATION_SUBSTEP_IDENTIFIER = "emailVerificationSubstep"; + public static final String PASSWORD_CONFIRMATION_IDENTIFIER = "confirmation"; + public static final String CONSENT_QUIZ_IDENTIFIER = "consentQuiz"; + + @VisibleForTesting + static final int EXTERNAL_ID_MAX_LENGTH = 128; + + private static final List EXTERNAL_ID_LOGIN_OPTIONS; + static { + List tempList = new ArrayList<>(); + tempList.add(ProfileInfoOption.EXTERNAL_ID); + EXTERNAL_ID_LOGIN_OPTIONS = Collections.unmodifiableList(tempList); + } + + // When set, this will be used + private CustomStepCreator customStepCreator; + + /** + * Can be used to make a SurveyFactor and take advantage of its SurveyItem to Step methods + */ + public SurveyFactory() { + super(); + // Default constructor, mainly used for subclasses + } + + /** + * Create a SmartSurveyTask for the given Context and TaskModel + * + * @param context activity context + * @param taskModel task model to create the survey task from + * @return created survey task + */ + @NonNull + public SmartSurveyTask createSmartSurveyTask( + @NonNull Context context, @NonNull TaskModel taskModel) { + return new SmartSurveyTask(context, taskModel); + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param surveyItems a list of survey items that will be transformed into Steps + * @return a list of steps + */ + public List createSurveySteps(Context context, List surveyItems) { + List steps = new ArrayList<>(); + if (surveyItems != null) { + for (SurveyItem item : surveyItems) { + Step step = createSurveyStep(context, item, false); + if (step != null) { + steps.add(step); + } + } + } + return steps; + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item the survey item to act upon + * @param isSubtaskStep true if this is within a subtask step already, false otherwise + * @return a step created from the item + */ + public Step createSurveyStep(Context context, SurveyItem item, boolean isSubtaskStep) { + + switch (item.type) { + case INSTRUCTION: + case INSTRUCTION_COMPLETION: + if (!(item instanceof InstructionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, INSTRUCTION types must be InstructionSurveyItems"); + } + if (item.type == SurveyItemType.INSTRUCTION_COMPLETION) { + return createInstructionCompletionStep((InstructionSurveyItem)item); + } + return createInstructionStep((InstructionSurveyItem)item); + case SUBTASK: + if (!(item instanceof SubtaskQuestionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, SUBTASK types must be SubtaskQuestionSurveyItem"); + } + return createSubtaskStep(context, ((SubtaskQuestionSurveyItem)item)); + case QUESTION_BOOLEAN: + case QUESTION_DATE: + case QUESTION_DATE_TIME: + case QUESTION_DECIMAL: + case QUESTION_DURATION: + case QUESTION_INTEGER: + case QUESTION_MULTIPLE_CHOICE: + case QUESTION_SCALE: + case QUESTION_SINGLE_CHOICE: + case QUESTION_TEXT: + case QUESTION_EMAIL: + case QUESTION_TIME: + case QUESTION_TIMING_RANGE: + if (!(item instanceof QuestionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, QUESTION_* types must be QuestionSurveyItem"); + } + return createQuestionStep(context, (QuestionSurveyItem)item); + case QUESTION_FORM: + if (!(item instanceof FormSurveyItem)) { + throw new IllegalStateException("Error in json parsing, QUESTION_FORM types must be FormSurveyItem"); + } + return createFormStep(context, (FormSurveyItem)item); + case ACCOUNT_REGISTRATION: + if (!(item instanceof ProfileSurveyItem)) { + throw new IllegalStateException("Error in json parsing, ACCOUNT_REGISTRATION types must be ProfileSurveyItem"); + } + return createRegistrationStep(context, (ProfileSurveyItem)item); + case ACCOUNT_PROFILE: + if (!(item instanceof ProfileSurveyItem)) { + throw new IllegalStateException("Error in json parsing, ACCOUNT_PROFILE types must be ProfileSurveyItem"); + } + return createProfileStep(context, (ProfileSurveyItem)item); + case ACCOUNT_LOGIN: + if (!(item instanceof ProfileSurveyItem)) { + throw new IllegalStateException("Error in json parsing, ACCOUNT_LOGIN types must be ProfileSurveyItem"); + } + return createLoginStep(context, (ProfileSurveyItem)item, defaultLoginOptions()); + case ACCOUNT_LOGIN_VIA_EMAIL: + if (!(item instanceof ProfileSurveyItem)) { + throw new IllegalStateException("Error in json parsing, ACCOUNT_LOGIN types must be ProfileSurveyItem"); + } + return createLoginStep(context, (ProfileSurveyItem)item, Arrays.asList + (ProfileInfoOption.EMAIL)); + case ACCOUNT_COMPLETION: + // TODO: finish the completion step layout, for now just use a simple instruction + // TODO: should show the cool check mark animation, see iOS + if (!(item instanceof InstructionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, ACCOUNT_COMPLETION types must be InstructionSurveyItem"); + } + return createInstructionStep((InstructionSurveyItem)item); + case ACCOUNT_EMAIL_VERIFICATION: + if (!(item instanceof InstructionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, ACCOUNT_EMAIL_VERIFICATION types must be InstructionSurveyItem"); + } + return createEmailVerificationStep(context, (InstructionSurveyItem)item); + case ACCOUNT_PERMISSIONS: + return createPermissionsStep(item); + case ACCOUNT_DATA_GROUPS: + return createNotImplementedStep(item); + case ACCOUNT_EXTERNAL_ID: + if (!(item instanceof ProfileSurveyItem)) { + throw new IllegalStateException("Error in json parsing, " + + "ACCOUNT_EXTERNAL_ID types must be ProfileSurveyItem"); + } + return createLoginStep(context, (ProfileSurveyItem)item, EXTERNAL_ID_LOGIN_OPTIONS); + case PASSCODE: + return createPasscodeStep(context, item); + case SHARE_THE_APP: + if (!(item instanceof InstructionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, SHARE_THE_APP types must be InstructionSurveyItem"); + } + return createShareTheAppStep(context, (InstructionSurveyItem)item); + case ACTIVE_STEP: + if (!(item instanceof ActiveStepSurveyItem)) { + throw new IllegalStateException("Error in json parsing, ACTIVE_STEP types must be ActiveStepSurveyItem"); + } + return createActiveStep(context, (ActiveStepSurveyItem)item); + case CUSTOM: + // To override a custom step from survey item mapping, + // You need to override the CustomStepCreator + if (customStepCreator != null) { + Step step = customStepCreator.createCustomStep(context, item, isSubtaskStep, this); + if (step != null) { + return step; + } + } + return createCustomStep(context, item, isSubtaskStep); + } + + return null; + } + + /** + * @param item InstructionSurveyItem from JSON + * @return valid InstructionStep matching the InstructionSurveyItem + */ + public InstructionStep createInstructionStep(InstructionSurveyItem item) { + InstructionStep step = new InstructionStep(item.identifier, item.title, item.text); + fillInstructionStep(step, item); + return step; + } + + /** + * @param item InstructionSurveyItem from JSON + * @return valid CompletionStep matching the InstructionSurveyItem + */ + public CompletionStep createInstructionCompletionStep(InstructionSurveyItem item) { + CompletionStep step = new CompletionStep(item.identifier, item.title, item.text); + fillInstructionStep(step, item); + return step; + } + + /** helper to display to dev that this step is not implemented yet */ + InstructionStep createNotImplementedStep(SurveyItem item) { + return new InstructionStep( + item.identifier, + "Not implemented", + "Type of step not implemented yet."); + } + + /** + * Helper method for instruction steps + * + * @param step the instruction step to fill with the item's info + * @param item the item to fil up the step with its fields + */ + public void fillInstructionStep(InstructionStep step, InstructionSurveyItem item) { + if (item.footnote != null) { + step.setFootnote(item.footnote); + } + if (item.nextIdentifier != null) { + step.setNextStepIdentifier(item.nextIdentifier); + } + if (item.detailText != null) { + step.setMoreDetailText(item.detailText); + } + if (item.image != null) { + step.setImage(item.image); + } + if (item.iconImage != null) { + step.setIconImage(item.iconImage); + } + if (item.scaleType != null) { + step.scaleType = item.scaleType; + } + if (item.isImageAnimated) { + step.setIsImageAnimated(true); + } + if (item.animationRepeatDuration > 0) { + step.setAnimationRepeatDuration(item.animationRepeatDuration); + } + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item SubtaskQuestionSurveyItem item from JSON that contains nested SurveyItems + * @return a subtask step by recursively calling createSurveyStep for inner subtask steps + */ + public SubtaskStep createSubtaskStep(Context context, SubtaskQuestionSurveyItem item) { + if (item.items == null || item.items.isEmpty()) { + throw new IllegalStateException("subtasks must have step items to proceed"); + } + + List substeps = new ArrayList<>(); + for (SurveyItem subItem : item.items) { + Step step = createSurveyStep(context, subItem, true); + substeps.add(step); + } + + SubtaskStep step; + if (item.usesNavigation()) { + NavigationSubtaskStep navStep = new NavigationSubtaskStep(item.identifier, substeps); + transferNavigationRules(item, navStep); + step = navStep; + } else { + step = new SubtaskStep(item.identifier, substeps); + } + + return step; + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item SubtaskQuestionSurveyItem item from JSON that contains nested SurveyItems + * @return a subtask step by recursively calling createSurveyStep for inner subtask steps + */ + public FormStep createFormStep(Context context, FormSurveyItem item) { + if (item.items == null || item.items.isEmpty()) { + throw new IllegalStateException("compound surveys must have step items to proceed"); + } + List questionSteps = formStepCreateQuestionSteps(context, item); + NavigationFormStep step = new NavigationFormStep(item.identifier, item.title, item.text, questionSteps); + fillNavigationFormStep(step, item); + return step; + } + + /** + * Helper method to re-use the logic of creating question steps for a form step + */ + protected List formStepCreateQuestionSteps(Context context, FormSurveyItem item) { + List questionSteps = new ArrayList<>(); + for (SurveyItem subItem : item.items) { + if (subItem instanceof QuestionSurveyItem) { + QuestionStep step = createQuestionStep(context, (QuestionSurveyItem)subItem); + questionSteps.add(step); + } + } + return questionSteps; + } + + /** + * Helper method to fill a navigation form step, but leave the base class out of it + */ + protected void fillNavigationFormStep(NavigationFormStep step, FormSurveyItem item) { + fillFormStep(step, item); + transferNavigationRules(item, step); + if (item.expectedAnswer != null) { + step.setExpectedAnswer(item.expectedAnswer); + } + } + + /** + * Helper method to fill a form step, but leave the base class out of it + */ + protected void fillFormStep(FormStep step, FormSurveyItem item) { + fillQuestionStep(item, step); + if (item.skipTitle != null) { + step.setSkipTitle(item.skipTitle); + // we can assume that if we set the skip title, we want to show the skip button + step.setOptional(true); + } + if (item.autoFocusFirstEditText != null) { + step.setAutoFocusFirstEditText(item.autoFocusFirstEditText); + } + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item QuestionSurveyItem from JSON + * @return QuestionStep converted from the item + */ + public QuestionStep createQuestionStep(Context context, QuestionSurveyItem item) { + AnswerFormat format = null; + switch (item.type) { + case QUESTION_BOOLEAN: + if (!(item instanceof BooleanQuestionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, QUESTION_BOOLEAN types must be BooleanQuestionSurveyItem"); + } + BooleanAnswerFormat boolFormat = new BooleanAnswerFormat(); + fillBooleanAnswerFormat(context, boolFormat, (BooleanQuestionSurveyItem)item); + format = boolFormat; + break; + case QUESTION_DATE: + case QUESTION_DATE_TIME: + if (!(item instanceof DateRangeSurveyItem)) { + throw new IllegalStateException("Error in json parsing, QUESTION_DATE types must be DateRangeSurveyItem"); + } + DateRangeSurveyItem dateSurveyItem = (DateRangeSurveyItem)item; + DateRangeSurveyItem dateItem = (DateRangeSurveyItem)item; + AnswerFormat.DateAnswerStyle style = AnswerFormat.DateAnswerStyle.Date; + if (dateSurveyItem.type == SurveyItemType.QUESTION_DATE_TIME) { + style = AnswerFormat.DateAnswerStyle.DateAndTime; + } + format = new DateAnswerFormat(style, dateItem.min, dateItem.max, dateItem.defaultValue); + break; + case QUESTION_TIME: + format = new DateAnswerFormat(AnswerFormat.DateAnswerStyle.TimeOfDay); + break; + case QUESTION_DECIMAL: + if (!(item instanceof FloatRangeSurveyItem)) { + throw new IllegalStateException("Error in json parsing, QUESTION_DECIMAL types must be FloatRangeSurveyItem"); + } + FloatRangeSurveyItem floatItem = (FloatRangeSurveyItem)item; + format = new DecimalAnswerFormat(floatItem.min, floatItem.max); + break; + case QUESTION_INTEGER: + if (!(item instanceof IntegerRangeSurveyItem)) { + throw new IllegalStateException("Error in json parsing, QUESTION_INTEGER types must be IntegerRangeSurveyItem"); + } + IntegerAnswerFormat integerFormat = new IntegerAnswerFormat(); + fillIntegerAnswerFormat(integerFormat, (IntegerRangeSurveyItem)item); + format = integerFormat; + break; + case QUESTION_DURATION: + // TODO: create DurationQuestionSurveyItem and also TimeIntervalAnswerFormat + format = new ChoiceAnswerFormat(AnswerFormat.ChoiceAnswerStyle.SingleChoice, + new Choice<>("TODO: Duration type not implemented", true)); + break; + case QUESTION_SCALE: + if (!(item instanceof ScaleQuestionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, QUESTION_SCALE types must be ScaleQuestionSurveyItem"); + } + ScaleQuestionSurveyItem scaleItem = (ScaleQuestionSurveyItem)item; + // TODO: create scale answer formats, see iOS versions + format = new ChoiceAnswerFormat(AnswerFormat.ChoiceAnswerStyle.SingleChoice, + new Choice<>("TODO: Scale (integer, continuous, text) survey not implemented", true)); + break; + case QUESTION_MULTIPLE_CHOICE: + case QUESTION_SINGLE_CHOICE: + { + if (!(item instanceof ChoiceQuestionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, this type must be ChoiceQuestionSurveyItem"); + } + ChoiceAnswerFormat choiceFormat = new ChoiceAnswerFormat(); + fillChoiceAnswerFormat(choiceFormat, (ChoiceQuestionSurveyItem)item); + format = choiceFormat; + break; + } + case QUESTION_TIMING_RANGE: // Single choice question, but with "Not Sure" as an added option + { + if (!(item instanceof TimingRangeQuestionSurveyItem)) { + throw new IllegalStateException("Error in json parsing, QUESTION_TIMING_RANGE type must be TimingRangeQuestionSurveyItem"); + } + if (item.items == null || item.items.isEmpty()) { + throw new IllegalStateException("TimingRangeQuestionSurveyItem must have Range items"); + } + List> choiceList = new ArrayList<>(); + TimingRangeQuestionSurveyItem subItem = (TimingRangeQuestionSurveyItem)item; + for (IntegerRangeSurveyItem integerRange : subItem.items) { + choiceList.add(convertToTextChoice(context, integerRange)); + } + String notSure = context.getString(R.string.rsb_not_sure); + choiceList.add(new Choice<>(notSure, notSure)); + Choice[] choices = choiceList.toArray(new Choice[choiceList.size()]); + format = new ChoiceAnswerFormat(AnswerFormat.ChoiceAnswerStyle.SingleChoice, choices); + break; + } + case QUESTION_TEXT: { + if (!(item instanceof TextfieldSurveyItem)) { + throw new IllegalStateException("Error in json parsing, QUESTION_TEXT type must be TextfieldSurveyItem"); + } + TextfieldSurveyItem textfieldSurveyItem = (TextfieldSurveyItem) item; + TextAnswerFormat textFormat = new TextAnswerFormat(); + fillTextAnswerFormat(textFormat, textfieldSurveyItem); + format = textFormat; + break; + } + case QUESTION_EMAIL: { + if (!(item instanceof TextfieldSurveyItem)) { + throw new IllegalStateException("Error in json parsing, QUESTION_EMAIL type must be TextfieldSurveyItem"); + } + TextfieldSurveyItem textfieldSurveyItem = (TextfieldSurveyItem) item; + EmailAnswerFormat emailFormat = new EmailAnswerFormat(); + fillTextAnswerFormat(emailFormat, textfieldSurveyItem); + format = emailFormat; + break; + } + default: + format = createCustomAnswerFormat(context, item); + break; + } + + QuestionStep step = null; + // Attach the navigation components to the step if there are any + if (item.usesNavigation()) { + NavigationExpectedAnswerQuestionStep navStep = new NavigationExpectedAnswerQuestionStep(item.identifier, item.title, format); + transferNavigationRules(item, navStep); + step = navStep; + } else { + step = new QuestionStep(item.identifier, item.title, format); + } + fillQuestionStep(item, step); + + return step; + } + + /** + * This method can be overridden by a subclass to provide your own AnswerFormat that + * can be used to customize the QuestionBody UI + * @param context can be android or app + * @param item QuestionSurveyItem with item.getCustomTypeValue() unknown to the base SurveyFactory + * @return the correct AnswerFormat to this QuestionStep + */ + public AnswerFormat createCustomAnswerFormat(Context context, QuestionSurveyItem item) { + return null; // to be implemented by subclass + } + + protected void fillTextAnswerFormat(TextAnswerFormat format, TextfieldSurveyItem item) { + if (item.inputType != null) { + format.setInputType(item.inputType); + } + if (item.validationRegex != null) { + format.setValidationRegex(item.validationRegex); + } + if (item.disabled != null && item.disabled) { + format.setDisabled(true); + } + if (item.isMultipleLines != null && item.isMultipleLines) { + format.setIsMultipleLines(true); + } + if (item.maxLength != null) { + format.setMaximumLength(item.maxLength); + } + } + + protected void fillIntegerAnswerFormat(IntegerAnswerFormat format, IntegerRangeSurveyItem item) { + format.setMaxValue((item.max == null) ? 0 : item.max); + format.setMinValue((item.min == null) ? 0 : item.min); + if (item.maxLength != null) { + format.setMaximumLength(item.maxLength); + } + } + + protected void fillChoiceAnswerFormat(ChoiceAnswerFormat format, ChoiceQuestionSurveyItem item) { + if (item.items == null || item.items.isEmpty()) { + throw new IllegalStateException("ChoiceQuestionSurveyItem must have choices"); + } + AnswerFormat.ChoiceAnswerStyle answerStyle = AnswerFormat.ChoiceAnswerStyle.SingleChoice; + if (item.type == SurveyItemType.QUESTION_MULTIPLE_CHOICE) { + answerStyle = AnswerFormat.ChoiceAnswerStyle.MultipleChoice; + } + format.setAnswerStyle(answerStyle); + Choice[] choices = item.items.toArray(new Choice[item.items.size()]); + format.setChoices(choices); + } + + protected void fillBooleanAnswerFormat(Context context, BooleanAnswerFormat format, BooleanQuestionSurveyItem item) { + String yes = null, no = null; + // First try and get the true / value choices from the BooleanQuestionSurveyItem + // Since it may sometimes, but not always provided + if (item.items != null && item.items.size() == 2) { + for (Choice choice : item.items) { + if (choice.getValue()) { + yes = choice.getText(); + } else { + no = choice.getText(); + } + } + } + // If they are not provided, use the default yes / no strings for true / false + if (yes == null) { + yes = context.getString(R.string.rsb_yes); + } + if (no == null) { + no = context.getString(R.string.rsb_no); + } + format.setTextValues(yes, no); + } + + protected void fillQuestionStep(QuestionSurveyItem item, QuestionStep step) { + step.setText(item.text); + step.setOptional(item.optional); + step.setPlaceholder(item.placeholderText); + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param profileInfoOptions type of profile item that should be included in profile form step + * @param addConfirmPasswordOption true if confirm password should be added with password, false otherwise + * @return a QuestionStep that can be used to get the correct data type for ProfileInfoOption + */ + public List createQuestionSteps( + Context context, + List profileInfoOptions, + boolean addConfirmPasswordOption) + { + List questionSteps = new ArrayList<>(); + for (ProfileInfoOption profileInfo : profileInfoOptions) { + switch (profileInfo) { + case EMAIL: + questionSteps.add(createEmailQuestionStep(context, profileInfo)); + break; + case PASSWORD: + questionSteps.add(createPasswordQuestionStep(context, profileInfo)); + // For password fields, we also need a "Confirm Password" field, if applicable + if (addConfirmPasswordOption) { + questionSteps.add(createConfirmPasswordQuestionStep(context)); + } + break; + case NAME: + questionSteps.add(createNameQuestionStep(context, profileInfo)); + break; + case BIRTHDATE: + questionSteps.add(createBirthdateQuestionStep(context, profileInfo)); + break; + case GENDER: + createGenderQuestionStep(context, profileInfo); + break; + case EXTERNAL_ID: + questionSteps.add(createExternalIdQuestionStep(context, profileInfo)); + break; + case BLOOD_TYPE: // ChoiceTextAnswerFormat, see HealthKit blood types + case FITZPATRICK_SKIN_TYPE: // ChoiceTextAnswerFormat + case WHEEL_CHAIR_USE: // boolean? + case HEIGHT: // ScaleAnswerFormat with units + case WEIGHT: // ScaleAnswerFormat with units + case WAKE_TIME: // DateAnswerFormat, but with Time value + case SLEEP_TIME: // DateAnswerFormat, but with Time value + // TODO: implement these when we need to + break; + } + } + return questionSteps; + } + + /** + * @param context used to generate title and placeholder title for step + * @param profileOption used to set step identifier + * @return QuestionStep used for gathering user's email + */ + public QuestionStep createEmailQuestionStep(Context context, ProfileInfoOption profileOption) { + QuestionStep emailStep = createGenericQuestionStep(context, + profileOption.getIdentifier(), + R.string.rsb_email, + R.string.rsb_email_placeholder, + new EmailAnswerFormat()); + emailStep.setOptional(false); + return emailStep; + } + + /** + * Create a question for External ID. + * + * @param context used to generate title and placeholder title for step + * @param profileOption used to set step identifier + * @return QuestionStep used for gathering user's external ID + */ + public QuestionStep createExternalIdQuestionStep( + Context context, ProfileInfoOption profileOption) { + QuestionStep externalIdStep = createGenericQuestionStep(context, + profileOption.getIdentifier(), + R.string.rsb_external_id, + R.string.rsb_external_id_placeholder, + new TextAnswerFormat(EXTERNAL_ID_MAX_LENGTH)); + externalIdStep.setOptional(false); + return externalIdStep; + } + + /** + * @param context used to generate title and placeholder title for step + * @param profileOption used to set step identifier + * @return QuestionStep used for gathering user's password + */ + public QuestionStep createPasswordQuestionStep(Context context, ProfileInfoOption profileOption) { + QuestionStep passwordStep = createGenericQuestionStep(context, + profileOption.getIdentifier(), + R.string.rsb_password, + R.string.rsb_password_placeholder, + new PasswordAnswerFormat()); + passwordStep.setOptional(false); + return passwordStep; + } + + /** + * @param context used to generate title and placeholder title for step + * @param profileOption used to set step identifier + * @return QuestionStep used for gathering user's password + */ + public QuestionStep createNameQuestionStep(Context context, ProfileInfoOption profileOption) { + TextAnswerFormat format = new TextAnswerFormat(); + format.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME); + format.setIsMultipleLines(false); + + return createGenericQuestionStep(context, + profileOption.getIdentifier(), + R.string.rsb_name, + R.string.rsb_name_placeholder, + format); + } + + /** + * @param context used to generate title and placeholder title for step + * @return QuestionStep used for gathering user's password + */ + public QuestionStep createConfirmPasswordQuestionStep(Context context) { + QuestionStep confirmPasswordStep = createGenericQuestionStep(context, + PASSWORD_CONFIRMATION_IDENTIFIER, + R.string.rsb_confirm_password, + R.string.rsb_confirm_password_placeholder, + new PasswordAnswerFormat()); + confirmPasswordStep.setOptional(false); + return confirmPasswordStep; + } + + /** + * @param context used to generate title and placeholder title for step + * @param profileOption that will be associated with this QuestionStep + * @return QuestionStep used for gathering user's password + */ + public QuestionStep createBirthdateQuestionStep(Context context, ProfileInfoOption profileOption) { + return createGenericQuestionStep(context, + profileOption.getIdentifier(), + R.string.rsb_birthdate, + R.string.rsb_birthdate_placeholder, + new DateAnswerFormat(AnswerFormat.DateAnswerStyle.Date)); + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param profileOption that will be associated with this QuestionStep + * @return QuestionStep used for gathering user's password + */ + public QuestionStep createGenderQuestionStep(Context context, ProfileInfoOption profileOption) { + return createGenericQuestionStep(context, + profileOption.getIdentifier(), + R.string.rsb_gender, + R.string.rsb_gender_placeholder, + new GenderAnswerFormat(context)); + } + + /** + * a helper method to make a re-usable generic method for creating question steps + * @param context can be any context, activity or application, used to access "R" resources + * @param identifier the identifier for the question step + * @param titleRes the string resource for the title of the question step + * @param placeholderRes the string resource for the placeholder title for the question step + * @param format the answer format for the question step + * @return QuestionStep with title, placeholder, and format all filled in + */ + public QuestionStep createGenericQuestionStep( + Context context, + String identifier, + @StringRes int titleRes, + @StringRes int placeholderRes, + AnswerFormat format) + { + String title = context.getString(titleRes); + QuestionStep step = new QuestionStep(identifier, title, format); + String placeholder = context.getString(placeholderRes); + step.setPlaceholder(placeholder); + step.setOptional(false); + return step; + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item ProfileSurveyItem from JSON + * @return valid RegistrationStep matching the ProfileSurveyItem + */ + public RegistrationStep createRegistrationStep(Context context, ProfileSurveyItem item) { + List options = createProfileInfoOptions(context, item, defaultRegistrationOptions()); + return new RegistrationStep( + item.identifier, item.title, item.text, + options, createQuestionSteps(context, options, true)); // true = create ConfirmPassword step + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item ProfileSurveyItem from JSON + * @return valid ProfileStep matching the ProfileSurveyItem + */ + public ProfileStep createProfileStep(Context context, ProfileSurveyItem item) { + List options = createProfileInfoOptions(context, item, defaultProfileOptions()); + return new ProfileStep( + item.identifier, item.title, item.text, + options, createQuestionSteps(context, options, false)); // false = dont create ConfirmPassword step + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item InstructionSurveyItem from JSON + * @param loginOptions A list of ProfileInfoOptions representing the fields needed for login. + * Can be defaultLoginOptions() for email/password, EXTERNAL_ID_LOGIN_OPTIONS + * @return valid EmailVerificationSubStep matching the InstructionSurveyItem + */ + public LoginStep createLoginStep( + Context context, ProfileSurveyItem item, List loginOptions) { + List options = createProfileInfoOptions(context, item, loginOptions); + return new LoginStep( + item.identifier, item.title, item.text, + options, createQuestionSteps(context, options, false)); // false = dont create ConfirmPassword step + } + + /** Helper for determining which profile info options to use */ + List createProfileInfoOptions( + Context context, + ProfileSurveyItem item, + List defaultOptions) + { + List options = defaultOptions; + // Use profile info options that were provided in the JSON if available + if (item.items != null && !item.items.isEmpty()) { + options = ProfileInfoOption.toProfileInfoOptions(item.items); + } + return options; + } + + List defaultLoginOptions() { + List profileInfo = new ArrayList<>(); + profileInfo.add(ProfileInfoOption.EMAIL); + profileInfo.add(ProfileInfoOption.PASSWORD); + return profileInfo; + } + + List defaultRegistrationOptions() { + List profileInfo = new ArrayList<>(); + profileInfo.add(ProfileInfoOption.NAME); + profileInfo.add(ProfileInfoOption.EMAIL); + profileInfo.add(ProfileInfoOption.PASSWORD); + return profileInfo; + } + + List defaultProfileOptions() { + List profileInfo = new ArrayList<>(); // blank for default profile + return profileInfo; + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item InstructionSurveyItem from JSON + * @return valid EmailVerificationStep matching the InstructionSurveyItem + */ + public EmailVerificationStep createEmailVerificationStep(Context context, InstructionSurveyItem item) { + EmailVerificationSubStep emailSubstep = new EmailVerificationSubStep( + EMAIL_VERIFICATION_SUBSTEP_IDENTIFIER, item.title, item.text); + fillInstructionStep(emailSubstep, item); + + String changeEmailTitle = context.getString(R.string.rsb_change_email_title); + RegistrationStep registrationStep = new RegistrationStep( + context, this, OnboardingSection.REGISTRATION_IDENTIFIER, changeEmailTitle, null); + + EmailVerificationStep emailVerificationStep = new EmailVerificationStep( + item.identifier, emailSubstep, registrationStep); + + return emailVerificationStep; + } + + /** + * @param item SurveyItem from JSON + * @return valid PermissionsStep matching the SurveyItem + */ + public PermissionsStep createPermissionsStep(SurveyItem item) { + return new PermissionsStep(item.identifier, item.title, item.text); + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item SurveyItem from JSON + * @return valid PasscodeStep matching the SurveyItem + */ + public Step createPasscodeStep(Context context, SurveyItem item) { + + PasscodeStep step = new PasscodeStep(item.identifier, item.title, item.text); + step.setStateOrdinal(PasscodeCreationStepLayout.State.CREATE.ordinal()); + + // Fingerprint API was added with api 23 + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(context); + + // This is by far the most secure way to store data, so if the user has it, + // we will make them take advantage of it; however, they can switch to passcode from the step layout + if (fingerprintManager.isHardwareDetected() && + fingerprintManager.hasEnrolledFingerprints()) + { + step.setUseFingerprint(true); + } + } + + return step; + } + + public ShareTheAppStep createShareTheAppStep(Context context, InstructionSurveyItem item) { + ShareTheAppStep step = new ShareTheAppStep(item.identifier, item.title, item.text); + fillInstructionStep(step, item); + + if (item.items != null && !item.items.isEmpty()) { + step.setShareTypeList(ShareTheAppStep.ShareType.toShareTypeList(item.items)); + } + + if (step.getTitle() == null) { + step.setTitle(context.getString(R.string.rsb_share_the_app_title)); + } + + if (step.getText() == null) { + step.setText(context.getString(R.string.rsb_share_the_app_text)); + } + + return step; + } + + public ActiveStep createActiveStep(Context context, ActiveStepSurveyItem item) { + ActiveStep step = new ActiveStep(item.identifier); + fillActiveStep(step, item); + return step; + } + + public void fillActiveStep(ActiveStep step, ActiveStepSurveyItem item) { + step.setActivityClazz(ActiveTaskActivity.class); + if (item.title != null) { + step.setTitle(item.title); + } + if (item.text != null) { + step.setText(item.text); + } + if (item.getStepDuration() > 0) { + step.setStepDuration(item.getStepDuration()); + } + if (item.getStepFinishedSpokenInstruction() != null) { + step.setFinishedSpokenInstruction(item.getStepFinishedSpokenInstruction()); + } + if (item.getStepSpokenInstruction() != null) { + step.setSpokenInstruction(item.getStepSpokenInstruction()); + } + if (item.getSpokenInstructionMap() != null) { + step.setSpokenInstructionMap(item.getSpokenInstructionMap()); + } + if (item.getSoundRes() != null) { + step.setSoundRes(item.getSoundRes()); + } + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item InstructionSurveyItem from JSON + * @param isSubtaskStep true if this is within a subtask step already, false otherwise + * @return valid CustomStep matching the InstructionSurveyItem + */ + public Step createCustomStep(Context context, SurveyItem item, boolean isSubtaskStep) { + return new InstructionStep(item.identifier, item.title, item.text); + } + + /* + * Transfers the QuestionSurveyItem nav properties over to NavigationStep + */ + protected void transferNavigationRules(QuestionSurveyItem item, NavigationExpectedAnswerQuestionStep toStep) { + toStep.setSkipIfPassed(item.skipIfPassed); + toStep.setSkipToStepIdentifier(item.skipIdentifier); + toStep.setExpectedAnswer(item.expectedAnswer); + } + + /* + * Transfers the QuestionSurveyItem nav properties over to NavigationStep + */ + protected void transferNavigationRules(QuestionSurveyItem item, NavigationFormStep toStep) { + toStep.setSkipIfPassed(item.skipIfPassed); + toStep.setSkipToStepIdentifier(item.skipIdentifier); + } + + /* + * Transfers the QuestionSurveyItem nav properties over to NavigationStep + */ + protected void transferNavigationRules(QuestionSurveyItem item, NavigationSubtaskStep toStep) { + toStep.setSkipIfPassed(item.skipIfPassed); + toStep.setSkipToStepIdentifier(item.skipIdentifier); + } + + public Choice convertToTextChoice(Context context, IntegerRangeSurveyItem item) { + String timeUnitStr; + switch (item.unit) { + case "minutes": + timeUnitStr = context.getString(R.string.rsb_time_minutes); + break; + case "hours": + timeUnitStr = context.getString(R.string.rsb_time_hours); + break; + case "days": + timeUnitStr = context.getString(R.string.rsb_time_days); + break; + case "weeks": + timeUnitStr = context.getString(R.string.rsb_time_weeks); + break; + case "months": + timeUnitStr = context.getString(R.string.rsb_time_months); + break; + case "years": + timeUnitStr = context.getString(R.string.rsb_time_years); + break; + default: + timeUnitStr = context.getString(R.string.rsb_time_seconds); + break; + } + + // Note: in all cases, the value is returned in English so that the localized + // values will result in the same answer in any table. It is up to the researcher to translate. + if (item.max != null) { + // We also have a min + if (item.min != null) { + String maxStr = String.format(Locale.getDefault(), "%d %s", item.max, timeUnitStr); + String maxText = String.format(context.getString(R.string.rsb_range_ago), maxStr); + + return new Choice<>( + String.format(Locale.getDefault(), "%d-%s", item.min, maxText), + String.format(Locale.US, "%d-%d %s ago", item.min, item.max, timeUnitStr)); + } else { // we just have a max + String maSxtr = String.format(Locale.getDefault(), "%d %s", item.max, timeUnitStr); + String text = String.format(context.getString(R.string.rsb_less_than_ago), maSxtr); + return new Choice<>(text, String.format(Locale.US, "Less than %d %s ago", item.max, timeUnitStr)); + } + } else { // does not have a max + String minStr = String.format(Locale.getDefault(), "%d %s", item.min, timeUnitStr); + String text = String.format(context.getString(R.string.rsb_more_than_ago), minStr); + return new Choice<>(text, String.format(Locale.US, "More than %d %s ago", item.min, timeUnitStr)); + } + } + + public void setCustomStepCreator(CustomStepCreator customStepCreator) { + this.customStepCreator = customStepCreator; + } + + public CustomStepCreator getCustomStepCreator() { + return customStepCreator; + } + + /** + * This can be used by another class to implement custom conversion from a CustomSurveyItem to a CustomStep + */ + public interface CustomStepCreator { + Step createCustomStep(Context context, SurveyItem item, boolean isSubtaskStep, SurveyFactory factory); + } +} \ No newline at end of file diff --git a/backbone/src/main/java/org/researchstack/backbone/model/taskitem/ActiveTaskItem.java b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/ActiveTaskItem.java new file mode 100644 index 000000000..25312653c --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/ActiveTaskItem.java @@ -0,0 +1,104 @@ +package org.researchstack.backbone.model.taskitem; + +import com.google.gson.annotations.SerializedName; + +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.task.factory.TaskExcludeOption; +import org.researchstack.backbone.utils.OptionSetUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Created by TheMDP on 3/7/17. + * + * An ActiveTaskItem represents a TaskItem that will be used to build a specific ResearchStack + * task like the ones defined in TaskItemType. Currently, these include the Tremor Task, + * Tapping Task, Audio Task, and several Walking Tasks. The list can and will continue to grow. + */ + +public class ActiveTaskItem extends TaskItem { + + /** + * localizedSteps are SurveyItem steps that contain fields to transfer to their + * corresponding steps after the Task is built + */ + @SerializedName("localizedSteps") + private List localizedSteps; + + /** + * This list of String identifiers can be used to remove steps with these identifiers + * from the task after the Task is built + */ + @SerializedName("removeSteps") + private List removeSteps; + + /** + * This is an OptionSet in iOS, but in Android we must treat it as a bit-masked int + * See TaskExcludeOption class for bit values which are converted by OptionSetUtils class + */ + @SerializedName("predefinedExclusions") + private int predefinedExclusions; + + @SerializedName("intendedUseDescription") + private String intendedUseDescription; + + /** + * The taskOptions contain and open-ended map that can be used to pass custom options from + * JSON to the task builder method in TaskItemFactory + */ + public static final String GSON_TASK_OPTIONS_NAME = "taskOptions"; + @SerializedName("taskOptions") + private Map taskOptions; + + public ActiveTaskItem() { + super(); + } + + public Map getTaskOptions() { + return taskOptions; + } + + public void setTaskOptions(Map taskOptions) { + this.taskOptions = taskOptions; + } + + public List createPredefinedExclusions() { + if (predefinedExclusions == 0) { + return new ArrayList<>(); + } + return OptionSetUtils.toEnumList(predefinedExclusions, TaskExcludeOption.values()); + } + + /** + * @param predefinedExclusions a bitmask representing a List, see OptionSetUtils + */ + public void setPredefinedExclusions(int predefinedExclusions) { + this.predefinedExclusions = predefinedExclusions; + } + + public List getLocalizedSteps() { + return localizedSteps; + } + + public void setLocalizedSteps(List localizedSteps) { + this.localizedSteps = localizedSteps; + } + + public List getRemoveSteps() { + return removeSteps; + } + + public void setRemoveSteps(List removeSteps) { + this.removeSteps = removeSteps; + } + + public String getIntendedUseDescription() { + return intendedUseDescription; + } + + public void setIntendedUseDescription(String intendedUseDescription) { + this.intendedUseDescription = intendedUseDescription; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/taskitem/BaseTaskItem.java b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/BaseTaskItem.java new file mode 100644 index 000000000..fb5770174 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/BaseTaskItem.java @@ -0,0 +1,14 @@ +package org.researchstack.backbone.model.taskitem; + +/** + * Created by TheMDP on 3/7/17. + * + * Only needed because TaskItemAdapter needs a base concrete implementation + * to avoid infinite loops of de-serializing TaskItem + */ + +public class BaseTaskItem extends TaskItem { + public BaseTaskItem() { + super(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/taskitem/TaskItem.java b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/TaskItem.java new file mode 100644 index 000000000..79beb9ed0 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/TaskItem.java @@ -0,0 +1,143 @@ +package org.researchstack.backbone.model.taskitem; + +import com.google.gson.annotations.SerializedName; + +import org.researchstack.backbone.model.survey.SurveyItem; + +import java.util.List; + +/** + * Created by TheMDP on 3/7/17. + * + * An TaskItem is the model for a JSON task that can be a number of specific + * tasks or even a custom one + */ + +public class TaskItem { + + /** + * If present, this will be used as the task id when the task is created + * Also, if taskType is missing, taskIdentifier will be used in it's place + */ + static final String TASK_IDENTIFIER_GSON = "taskIdentifier"; + @SerializedName(TASK_IDENTIFIER_GSON) + private String taskIdentifier; + + /** + * The schemaIdentifier describes the output format of the TaskResult + * It can be skipped unless you specifically use it when accessing your db storage + */ + @SerializedName("schemaIdentifier") + private String schemaIdentifier; + + /** + * If taskIsOptional is true, there will be a skip task button added to the first step of the task + */ + @SerializedName("optional") + private boolean taskIsOptional; + + static final String TASK_TYPE_GSON = "taskType"; + @SerializedName(TASK_TYPE_GSON) + private TaskItemType taskType; + + /** + * insertSteps is a list of steps that will be inserted into the Task after it is built + */ + @SerializedName("insertSteps") + private List insertSteps; + + /** + * taskSteps are a list of SurveyItems that will be used to build the Task + * it is similar to insertSteps, except the whole Task will be taskSteps instead of just + * inserting specific steps into a pre-constructed TaskItemType + */ + @SerializedName(value = "taskSteps", alternate = {"steps"}) + private List taskSteps; + + + private String customItemTypeIdentifier; + + private transient String rawJson; + + public TaskItem() { + super(); + } + + public String getTaskIdentifier() { + return taskIdentifier; + } + + public void setTaskIdentifier(String taskIdentifier) { + this.taskIdentifier = taskIdentifier; + } + + public String getSchemaIdentifier() { + return schemaIdentifier; + } + + public void setSchemaIdentifier(String schemaIdentifier) { + this.schemaIdentifier = schemaIdentifier; + } + + public boolean isTaskIsOptional() { + return taskIsOptional; + } + + public void setTaskIsOptional(boolean taskIsOptional) { + this.taskIsOptional = taskIsOptional; + } + + public List getInsertSteps() { + return insertSteps; + } + + public void setInsertSteps(List insertSteps) { + this.insertSteps = insertSteps; + } + + public List getTaskSteps() { + return taskSteps; + } + + public void setTaskSteps(List taskSteps) { + this.taskSteps = taskSteps; + } + + public String getTaskTypeIdentifier() { + if (isCustomTask()) { + return customItemTypeIdentifier; + } + return taskType.getValue(); + } + + public TaskItemType getTaskType() { + return taskType; + } + + public void setTaskType(TaskItemType type) { + this.taskType = type; + } + + protected void setCustomTypeValue(String value) { + customItemTypeIdentifier = value; + } + + public String getCustomTypeValue() { + return customItemTypeIdentifier; + } + + public void setRawJson(String json) { + this.rawJson = json; + } + + public String getRawJson() { + return rawJson; + } + + /** + * @return true if the survey item type is custom, false otherwise + */ + public boolean isCustomTask() { + return customItemTypeIdentifier != null; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/taskitem/TaskItemAdapter.java b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/TaskItemAdapter.java new file mode 100644 index 000000000..9ac292e64 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/TaskItemAdapter.java @@ -0,0 +1,93 @@ +package org.researchstack.backbone.model.taskitem; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.model.survey.factory.SurveyFactory; +import org.researchstack.backbone.model.taskitem.factory.TaskItemFactory; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +/** + * Created by TheMDP on 3/7/17. + * + * This class is the deserializer for TaskItem classes + * It looks at the "taskType" field, attempts to map it to this library's pre-defined types + * and if it does not find it, creates a custom task item + * the class of the custom task item can easily be controlled by overriding this + * adapter, and overriding the method getCustomClass + * + * To go even further and change the mapping of the custom tak item to a custom step, + * you should override TaskItemFactory's method public Task createCustomTask method + * which is the go to for converting a task item to a Task + */ + +public class TaskItemAdapter implements JsonDeserializer { + + @Override + public TaskItem deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + + JsonElement typeJson = jsonObject.get(TaskItem.TASK_TYPE_GSON); + TaskItemType itemType = context.deserialize(typeJson, TaskItemType.class); + + // This was a custom activity task item type + String customTypeString = null; + if (itemType == null) { + itemType = TaskItemType.CUSTOM; + // Some JSON can be malformed and there is no taskType, only taskIdentifier + // In that case let's try and still parse it by setting the task type as the task identifier + if (typeJson != null) { + customTypeString = typeJson.getAsString(); + } else { // use "taskIdentifier" string + customTypeString = jsonObject.get(TaskItem.TASK_IDENTIFIER_GSON).getAsString(); + } + } + + TaskItem item = null; + + switch (itemType) { + case WALKING: + case SHORT_WALK: + case VOICE: + case TAPPING: + case MOOD_SURVEY: + case TREMOR: + case MEMORY: + item = context.deserialize(json, ActiveTaskItem.class); + break; + case CUSTOM: + item = context.deserialize(json, getCustomClass(customTypeString, json)); + item.setTaskType(itemType); // need to set CUSTOM type for surveyItem, since it is a special case + item.setCustomTypeValue(customTypeString); + break; + } + + if (item == null) { + item = context.deserialize(json, BaseTaskItem.class); + item.setTaskType(itemType); + } + + item.setRawJson(json.toString()); + + return item; + } + + /** + * This can be overridden by subclasses to provide custom survey item deserialization + * the default deserialization is always a CustomTaskItem.class + * @param customType used to map to different types of survey items + * @param json if customType is not enough, you can use the JsonElement to determine how to parse + * it's contents by peeking at it's variables + * @return type of survey item to create from the custom class + */ + public Class getCustomClass(String customType, JsonElement json) { + return BaseTaskItem.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/taskitem/TaskItemType.java b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/TaskItemType.java new file mode 100644 index 000000000..6c4eb9c5c --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/TaskItemType.java @@ -0,0 +1,51 @@ +package org.researchstack.backbone.model.taskitem; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by TheMDP on 3/7/17. + */ + +public enum TaskItemType { + + // Custom Task + CUSTOM(null), + + // Active Tasks... + // Tapping Task + @SerializedName("tapping") + TAPPING ("tapping"), + + // Memory Task + @SerializedName("memory") + MEMORY ("memory"), + + // Voice Task + @SerializedName("voice") + VOICE ("voice"), + + // Walking Task + @SerializedName("walking") + WALKING ("walking"), + + // Short Walking Task + @SerializedName("shortWalk") + SHORT_WALK ("shortWalk"), + + // Tremor Task + @SerializedName("tremor") + TREMOR ("tremor"), + + // Mood Survey Task + @SerializedName("moodSurvey") + MOOD_SURVEY ("moodSurvey"); + + TaskItemType(String rawValue) { + value = rawValue; + } + + private String value; + String getValue() { + return value; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/model/taskitem/factory/TaskItemFactory.java b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/factory/TaskItemFactory.java new file mode 100644 index 000000000..68f5937d8 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/model/taskitem/factory/TaskItemFactory.java @@ -0,0 +1,472 @@ +package org.researchstack.backbone.model.taskitem.factory; + +import android.content.Context; + +import com.google.gson.reflect.TypeToken; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.model.survey.ActiveStepSurveyItem; +import org.researchstack.backbone.model.survey.InstructionSurveyItem; +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.model.taskitem.ActiveTaskItem; +import org.researchstack.backbone.model.taskitem.TaskItem; +import org.researchstack.backbone.model.survey.factory.SurveyFactory; +import org.researchstack.backbone.step.InstructionStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.SubtaskStep; +import org.researchstack.backbone.step.active.ActiveStep; +import org.researchstack.backbone.step.active.recorder.AudioRecorderSettings; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.task.OrderedTask; +import org.researchstack.backbone.task.Task; +import org.researchstack.backbone.task.factory.AudioTaskFactory; +import org.researchstack.backbone.task.factory.HandTaskOptions; +import org.researchstack.backbone.task.factory.TappingTaskFactory; +import org.researchstack.backbone.task.factory.TremorTaskFactory; +import org.researchstack.backbone.task.factory.WalkingTaskFactory; +import org.researchstack.backbone.utils.LogExt; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Created by TheMDP on 3/7/17. + * + * The TaskItemFactory controls converting TaskItem objects to Task objects + * It accounts for all the variations specified in TaskItemType when looping through each + * TaskItem and storing the result in a field you can access using the getTaskList method + * + * Note that TaskItem objects should be create from JSON using the GSON library, + * and a TaskItemAdapter class to do special de-serialization for the TaskItems + */ + +public class TaskItemFactory extends SurveyFactory { + + public static final int DEFAULT_DURATION = 10; // in seconds + public static final int DEFAULT_WALKING_DURATION = 30; // in seconds + public static final int DEFAULT_WALKING_REST_DURATION = 30; // in seconds + public static final int DEFAULT_STEPS_PER_LEG = 100; + + private static final String DURATION_KEY = "duration"; + private static final String WALK_DURATION_KEY = "walkDuration"; + private static final String REST_DURATION_KEY = "restDuration"; + private static final String HAND_OPTIONS_KEY = "handOptions"; + private static final String EXCLUDE_POSITIONS_KEY = "excludePositions"; + private static final String SPEECH_INSTRUCTIONS_KEY = "speechInstruction"; + private static final String SHORT_SPEECH_INSTRUCTIONS_KEY = "shortSpeechInstruction"; + private static final String RECORDING_SETTINGS_KEY = "recordingSettings"; + private static final String NUMBER_OF_STEPS_PER_LEG_KEY = "numberOfStepsPerLeg"; + + // When set, this will be used + private CustomTaskCreator customTaskCreator; + + /* + * Default constructor + */ + public TaskItemFactory() { + super(); + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param item TaskItem to transform into + * @return a Task created from the TaskItem object + */ + public Task createTask(Context context, TaskItem item) { + + Task task = null; + + switch (item.getTaskType()) { + case VOICE: + if (!(item instanceof ActiveTaskItem)) { + throw new IllegalStateException("Error in json parsing, VOICE type must be ActiveTaskItem"); + } + task = createVoiceTask(context, (ActiveTaskItem)item); + break; + case TAPPING: + if (!(item instanceof ActiveTaskItem)) { + throw new IllegalStateException("Error in json parsing, VOICE type must be ActiveTaskItem"); + } + task = createTappingTask(context, (ActiveTaskItem)item); + break; + case TREMOR: + if (!(item instanceof ActiveTaskItem)) { + throw new IllegalStateException("Error in json parsing, TREMOR type must be ActiveTaskItem"); + } + task = createTremorTask(context, (ActiveTaskItem)item); + break; + case WALKING: + if (!(item instanceof ActiveTaskItem)) { + throw new IllegalStateException("Error in json parsing, WALKING type must be ActiveTaskItem"); + } + task = createWalkingTask(context, (ActiveTaskItem)item); + break; + case SHORT_WALK: + if (!(item instanceof ActiveTaskItem)) { + throw new IllegalStateException("Error in json parsing, SHORT_WALK type must be ActiveTaskItem"); + } + task = createShortWalkTask(context, (ActiveTaskItem)item); + break; + case MOOD_SURVEY: + LogExt.e(getClass(), "Mood survey not implemented yet"); + task = null; + break; + case MEMORY: + LogExt.e(getClass(), "Memory task not implemented yet"); + task = null; + break; + case CUSTOM: + task = createCustomTask(context, item); + break; + } + + fillTaskWithDefaultTaskItemAdditions(context, task, item); + + return task; + } + + public void fillTaskWithDefaultTaskItemAdditions(Context context, Task task, TaskItem item) { + // Add submit bar negative action on first step to skip task if possible + if (item.isTaskIsOptional()) { + task = addSkipActionToTask(context, task); + } + + if (item.getInsertSteps() != null && !item.getInsertSteps().isEmpty()) { + if (task instanceof OrderedTask) { + // Insert steps are only supported for OrderedTask + OrderedTask orderedTask = (OrderedTask)task; + for (SurveyItem surveyItem : item.getInsertSteps()) { + // TODO: do insert steps, that can also be tasks + } + } else { + throw new IllegalStateException("insertSteps functionality only works with OrderedTasks"); + } + } + + // Special cases for active tasks that are also OrderedTasks + if (item instanceof ActiveTaskItem) { + ActiveTaskItem activeTaskItem = (ActiveTaskItem)item; + mapLocalizedSteps(activeTaskItem, task); + removeSteps(activeTaskItem, task); + } + } + + protected Task addSkipActionToTask(Context context, Task task) { + if (!(task instanceof OrderedTask)) { + LogExt.e(getClass(), "Map Localized Steps only available for OrderedTasks"); + return task; + } + + OrderedTask orderedTask = (OrderedTask)task; + + if (orderedTask.getSteps() == null || orderedTask.getSteps().isEmpty()) { + LogExt.e(getClass(), "Empty task will not have skip action applied"); + return task; + } + + Step introStep = orderedTask.getSteps().get(0); + if (!(introStep instanceof InstructionStep)) { + LogExt.e(getClass(), "Handling of an optional task is not implemented " + + "for tasks that do not start with InstructionStep"); + return task; + } + + Step lastStep = orderedTask.getSteps().get(orderedTask.getSteps().size()); + if (orderedTask.getSteps().size() <= 1 || !(lastStep instanceof InstructionStep)) { + LogExt.e(getClass(), "Handling of an optional task is not implemented " + + "for tasks that do not end with InstructionStep"); + return task; + } + + InstructionStep introInstructionStep = (InstructionStep)introStep; + + // Replace fields in the intro step with a direct navigation step that has a skip button + // to skip to the conclusion + String skipExplanation = context.getString(R.string.rsb_skip_activity_instruction); + String skipTitle = context.getString(R.string.rsb_skip_activity); + + if (introInstructionStep.getMoreDetailText() == null) { + introInstructionStep.setMoreDetailText(skipExplanation); + } else { + introInstructionStep.setMoreDetailText(String.format("%s\n%s\n", + introInstructionStep.getMoreDetailText(), skipExplanation)); + } + + introInstructionStep.setSubmitBarNegativeActionSkipRule( + task.getIdentifier(), skipTitle, lastStep.getIdentifier()); + + return new NavigableOrderedTask(task.getIdentifier(), orderedTask.getSteps()); + } + + /** + * This method maps some fields from a SurveyItem list stored in localizedSteps to edit + * the corresponding Steps in the Task + * @param activeTaskItem An ActiveTaskItem that contains localized steps + * @param task the task to edit + */ + protected void mapLocalizedSteps(ActiveTaskItem activeTaskItem, Task task) { + + if (!(task instanceof OrderedTask)) { + LogExt.e(getClass(), "Map Localized Steps only available for OrderedTasks"); + return; + } + + OrderedTask orderedTask = (OrderedTask)task; + + if (orderedTask.getSteps() == null || orderedTask.getSteps().isEmpty() || + activeTaskItem.getLocalizedSteps() == null || activeTaskItem.getLocalizedSteps().isEmpty()) + { + return; + } + + // Loop through all steps in a task, and see if any of them match the localized step + for (Step step : orderedTask.getSteps()) { + SurveyItem surveyItem = findSurveyItemIdentifierMatchToStep(step, activeTaskItem.getLocalizedSteps()); + if (surveyItem != null) { + + int indexOfStep = orderedTask.getSteps().indexOf(step); + + if (surveyItem.title != null) { + step.setTitle(surveyItem.title); + } + if (surveyItem.text != null) { + step.setText(surveyItem.text); + } + + if (step instanceof InstructionStep && surveyItem instanceof InstructionSurveyItem) { + InstructionStep substep = (InstructionStep)step; + InstructionSurveyItem subsurveyItem = (InstructionSurveyItem)surveyItem; + if (subsurveyItem.detailText != null) { + substep.setMoreDetailText(subsurveyItem.detailText); + } + } + + if (step instanceof ActiveStep && surveyItem instanceof ActiveStepSurveyItem) { + ActiveStep activeStep = (ActiveStep)step; + ActiveStepSurveyItem activeStepSurveyItem = (ActiveStepSurveyItem)surveyItem; + if (activeStepSurveyItem.getStepDuration() > 0) { + activeStep.setStepDuration(activeStepSurveyItem.getStepDuration()); + } + if (activeStepSurveyItem.getStepSpokenInstruction() != null) { + activeStep.setSpokenInstruction(activeStepSurveyItem.getStepSpokenInstruction()); + } + if (activeStepSurveyItem.getStepFinishedSpokenInstruction() != null) { + activeStep.setFinishedSpokenInstruction(activeStepSurveyItem.getStepFinishedSpokenInstruction()); + } + } + + // Must call this to make sure the changes stick + orderedTask.replaceStep(indexOfStep, step); + } + } + } + + /** + * Removes the corresponding steps from the Task + * @param activeTaskItem An ActiveTaskItem that contains removeSteps + * @param task the task to edit + */ + public void removeSteps(ActiveTaskItem activeTaskItem, Task task) { + if (!(task instanceof OrderedTask)) { + LogExt.e(getClass(), "Map Localized Steps only available for OrderedTasks"); + return; + } + + OrderedTask orderedTask = (OrderedTask)task; + + if (orderedTask.getSteps() == null || orderedTask.getSteps().isEmpty() || + activeTaskItem.getRemoveSteps() == null || activeTaskItem.getRemoveSteps().isEmpty()) + { + return; + } + + // Loop through all steps in a task, and see if any of them match the localized step + for (Step step : orderedTask.getSteps()) { + if (activeTaskItem.getRemoveSteps().contains(step.getIdentifier())) { + orderedTask.removeStep(orderedTask.getSteps().indexOf(step)); + } + } + } + + private SurveyItem findSurveyItemIdentifierMatchToStep(Step step, List surveyItemList) { + for (SurveyItem surveyItem : surveyItemList) { + if (step.getIdentifier().equals(surveyItem.getIdentifier())) { + return surveyItem; + } + } + return null; + } + + public Task createTappingTask(Context context, ActiveTaskItem item) { + + int duration = extractInt(DURATION_KEY, DEFAULT_DURATION, item.getTaskOptions()); + + String handOptionName = extractString(HAND_OPTIONS_KEY, HandTaskOptions.SERIALIZED_NAME_HAND_BOTH, item.getTaskOptions()); + HandTaskOptions.Hand handOption = HandTaskOptions.toHandOption(handOptionName); + + return TappingTaskFactory.twoFingerTappingIntervalTask( + context, + item.getSchemaIdentifier(), + item.getIntendedUseDescription(), + duration, + handOption, + item.createPredefinedExclusions()); + } + + public Task createVoiceTask(Context context, ActiveTaskItem item) { + + String speechInstruction = extractString(SPEECH_INSTRUCTIONS_KEY, null, item.getTaskOptions()); + String shortSpeechInstruction = extractString(SHORT_SPEECH_INSTRUCTIONS_KEY, null, item.getTaskOptions()); + int duration = extractInt(DURATION_KEY, DEFAULT_DURATION, item.getTaskOptions()); + + // TODO: implement RECORDING_SETTINGS_KEY specific to Android, for now use the default + if (item.getTaskOptions() != null && !item.getTaskOptions().isEmpty()) { + // Attempt to read rest duration from JSON + if (item.getTaskOptions().get(RECORDING_SETTINGS_KEY) != null) { + // Currently, all these String constants are specifically mapped to iOS AVAudioSettings + // so we would need to either change the key values completely, or make a map to Android + LogExt.e(getClass(), "TODO: Voice task recorder settings not implemented yet on Android"); + } + } + + return AudioTaskFactory.audioTask( + context, + item.getSchemaIdentifier(), + item.getIntendedUseDescription(), + speechInstruction, + shortSpeechInstruction, + duration, + AudioRecorderSettings.defaultSettings(), + true, + item.createPredefinedExclusions()); + } + + public Task createWalkingTask(Context context, ActiveTaskItem item) { + + // The walking activity is assumed to be walking back and forth rather than trying to walk down a long hallway. + int walkDuration = extractInt(WALK_DURATION_KEY, DEFAULT_WALKING_DURATION, item.getTaskOptions()); + int restDuration = extractInt(REST_DURATION_KEY, DEFAULT_WALKING_REST_DURATION, item.getTaskOptions()); + + return WalkingTaskFactory.walkBackAndForthTask( + context, + item.getSchemaIdentifier(), + item.getIntendedUseDescription(), + walkDuration, + restDuration, + item.createPredefinedExclusions()); // TODO: may need to be the same as iOS + } + + public Task createShortWalkTask(Context context, ActiveTaskItem item) { + + // The walking activity is assumed to be walking back and forth rather than trying to walk down a long hallway. + int restDuration = extractInt(REST_DURATION_KEY, DEFAULT_WALKING_REST_DURATION, item.getTaskOptions()); + int numberOfSteps = extractInt(NUMBER_OF_STEPS_PER_LEG_KEY, DEFAULT_STEPS_PER_LEG, item.getTaskOptions()); + + return WalkingTaskFactory.shortWalkTask( + context, + item.getSchemaIdentifier(), + item.getIntendedUseDescription(), + numberOfSteps, + restDuration, + item.createPredefinedExclusions()); + } + + public Task createTremorTask(Context context, ActiveTaskItem item) { + int duration = extractInt(DURATION_KEY, DEFAULT_DURATION, item.getTaskOptions()); + + String handOptionName = extractString(HAND_OPTIONS_KEY, HandTaskOptions.SERIALIZED_NAME_HAND_BOTH, item.getTaskOptions()); + HandTaskOptions.Hand handOption = HandTaskOptions.toHandOption(handOptionName); + + List excludeOptionList = new ArrayList<>(); + List serializedExcludeList = extractStringList(EXCLUDE_POSITIONS_KEY, new ArrayList<>(), item.getTaskOptions()); + for (String serialized : serializedExcludeList) { + excludeOptionList.add(TremorTaskFactory.toTremorExcludeOption(serialized)); + } + + return TremorTaskFactory.tremorTask( + context, + item.getSchemaIdentifier(), + item.getIntendedUseDescription(), + duration, + excludeOptionList, + handOption, + item.createPredefinedExclusions()); + } + + private int extractInt(String key, int defaultValue, Map options) { + if (options != null && !options.isEmpty()) { + if (options.get(key) != null && + options.get(key) instanceof Number) + { + return ((Number)options.get(key)).intValue(); + } + } + return defaultValue; + } + + private String extractString(String key, String defaultValue, Map options) { + if (options != null && !options.isEmpty()) { + Type listType = new TypeToken>() {}.getType(); + // Attempt to read key from JSON + if (options.get(key) != null && + options.get(key) instanceof String) + { + return (String)options.get(key); + } + } + return defaultValue; + } + + @SuppressWarnings("unchecked") // needed for unchecked String List generic type casting + private List extractStringList(String key, List defaultValue, Map options) { + if (options != null && !options.isEmpty()) { + // Attempt to read key from JSON, GSON stores any Lists as a Map + if (options.get(key) != null && + options.get(key) instanceof ArrayList) + { + ArrayList arrayList = (ArrayList) options.get(key); + List stringList = new ArrayList<>(); + for (Object listItem : arrayList) { + if (listItem instanceof String) { + stringList.add((String)listItem); + } + } + return stringList; + } + } + return defaultValue; + } + + /** + * Override to create your own custom Tasks + * @param context can be app or activity, used for resources + * @param item the task item to convert into a task + * @return a Task object made from the TaskItem + */ + public Task createCustomTask(Context context, TaskItem item) { + List steps = super.createSurveySteps(context, item.getTaskSteps()); + + if (steps.size() == 1 && steps.get(0) instanceof SubtaskStep) { + return ((SubtaskStep)steps.get(0)).getSubtask(); + } else { + return new NavigableOrderedTask(item.getSchemaIdentifier(), steps); + } + } + + public CustomTaskCreator getCustomTaskCreator() { + return customTaskCreator; + } + + public void setCustomTaskCreator(CustomTaskCreator customTaskCreator) { + this.customTaskCreator = customTaskCreator; + } + + /** + * This can be used by another class to implement custom conversion from a TaskItem to a Task + */ + public interface CustomTaskCreator { + Task createCustomTask(Context context, TaskItem item, TaskItemFactory factory); + } +} diff --git a/skin/src/main/java/org/researchstack/skin/notification/DeviceBootReceiver.java b/backbone/src/main/java/org/researchstack/backbone/notification/DeviceBootReceiver.java similarity index 89% rename from skin/src/main/java/org/researchstack/skin/notification/DeviceBootReceiver.java rename to backbone/src/main/java/org/researchstack/backbone/notification/DeviceBootReceiver.java index 8917474f7..f78d99d88 100644 --- a/skin/src/main/java/org/researchstack/skin/notification/DeviceBootReceiver.java +++ b/backbone/src/main/java/org/researchstack/backbone/notification/DeviceBootReceiver.java @@ -1,4 +1,4 @@ -package org.researchstack.skin.notification; +package org.researchstack.backbone.notification; import android.content.BroadcastReceiver; import android.content.Context; diff --git a/skin/src/main/java/org/researchstack/skin/notification/NotificationConfig.java b/backbone/src/main/java/org/researchstack/backbone/notification/NotificationConfig.java similarity index 94% rename from skin/src/main/java/org/researchstack/skin/notification/NotificationConfig.java rename to backbone/src/main/java/org/researchstack/backbone/notification/NotificationConfig.java index 0190423eb..ac624aea2 100644 --- a/skin/src/main/java/org/researchstack/skin/notification/NotificationConfig.java +++ b/backbone/src/main/java/org/researchstack/backbone/notification/NotificationConfig.java @@ -1,9 +1,9 @@ -package org.researchstack.skin.notification; +package org.researchstack.backbone.notification; import android.app.Application; import android.content.Context; -import android.support.annotation.ColorInt; -import android.support.annotation.DrawableRes; +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; /** * Configuration class for framework notifciations diff --git a/skin/src/main/java/org/researchstack/skin/notification/SimpleNotificationConfig.java b/backbone/src/main/java/org/researchstack/backbone/notification/SimpleNotificationConfig.java similarity index 82% rename from skin/src/main/java/org/researchstack/skin/notification/SimpleNotificationConfig.java rename to backbone/src/main/java/org/researchstack/backbone/notification/SimpleNotificationConfig.java index 9886e8de2..830c59665 100644 --- a/skin/src/main/java/org/researchstack/skin/notification/SimpleNotificationConfig.java +++ b/backbone/src/main/java/org/researchstack/backbone/notification/SimpleNotificationConfig.java @@ -1,13 +1,13 @@ -package org.researchstack.skin.notification; +package org.researchstack.backbone.notification; import android.content.Context; -import org.researchstack.skin.R; +import org.researchstack.backbone.R; public class SimpleNotificationConfig extends NotificationConfig { @Override public int getSmallIcon() { - return R.drawable.rss_ic_notification_24dp; + return R.drawable.rsb_ic_notification_24dp; } @Override diff --git a/skin/src/main/java/org/researchstack/skin/notification/TaskAlertReceiver.java b/backbone/src/main/java/org/researchstack/backbone/notification/TaskAlertReceiver.java similarity index 91% rename from skin/src/main/java/org/researchstack/skin/notification/TaskAlertReceiver.java rename to backbone/src/main/java/org/researchstack/backbone/notification/TaskAlertReceiver.java index 425ea40a6..1c47e6c40 100644 --- a/skin/src/main/java/org/researchstack/skin/notification/TaskAlertReceiver.java +++ b/backbone/src/main/java/org/researchstack/backbone/notification/TaskAlertReceiver.java @@ -1,4 +1,4 @@ -package org.researchstack.skin.notification; +package org.researchstack.backbone.notification; import android.app.AlarmManager; import android.app.PendingIntent; @@ -12,8 +12,8 @@ import org.researchstack.backbone.utils.FormatHelper; import org.researchstack.backbone.utils.LogExt; import org.researchstack.backbone.utils.ObservableUtils; -import org.researchstack.skin.UiManager; -import org.researchstack.skin.schedule.ScheduleHelper; +import org.researchstack.backbone.UiManager; +import org.researchstack.backbone.schedule.ScheduleHelper; import java.text.DateFormat; import java.util.Date; @@ -26,10 +26,10 @@ public class TaskAlertReceiver extends BroadcastReceiver { //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // Intent Actions //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* - public static final String ALERT_CREATE = "org.researchstack.skin.notification.ALERT_CREATE"; - public static final String ALERT_CREATE_ALL = "org.researchstack.skin.notification.ALERT_CREATE_ALL"; - public static final String ALERT_DELETE = "org.researchstack.skin.notification.ALERT_DELETE"; - public static final String ALERT_DELETE_ALL = "org.researchstack.skin.notification.ALERT_DELETE_ALL"; + public static final String ALERT_CREATE = "org.researchstack.backbone.notification.ALERT_CREATE"; + public static final String ALERT_CREATE_ALL = "org.researchstack.backbone.notification.ALERT_CREATE_ALL"; + public static final String ALERT_DELETE = "org.researchstack.backbone.notification.ALERT_DELETE"; + public static final String ALERT_DELETE_ALL = "org.researchstack.backbone.notification.ALERT_DELETE_ALL"; //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // Intent Keys @@ -37,6 +37,12 @@ public class TaskAlertReceiver extends BroadcastReceiver { public static final String KEY_NOTIFICATION = "CreateAlertReceiver.KEY_NOTIFICATION"; public static final String KEY_NOTIFICATION_ID = "CreateAlertReceiver.KEY_NOTIFICATION_ID"; + public static Intent createCreateIntent(TaskNotification notification) { + Intent createTaskIntent = new Intent(TaskAlertReceiver.ALERT_CREATE); + createTaskIntent.putExtra(TaskAlertReceiver.KEY_NOTIFICATION, notification); + return createTaskIntent; + } + public static Intent createDeleteIntent(int notificationId) { Intent deleteTaskIntent = new Intent(TaskAlertReceiver.ALERT_DELETE); deleteTaskIntent.putExtra(TaskAlertReceiver.KEY_NOTIFICATION_ID, notificationId); diff --git a/skin/src/main/java/org/researchstack/skin/notification/TaskNotificationReceiver.java b/backbone/src/main/java/org/researchstack/backbone/notification/TaskNotificationReceiver.java similarity index 93% rename from skin/src/main/java/org/researchstack/skin/notification/TaskNotificationReceiver.java rename to backbone/src/main/java/org/researchstack/backbone/notification/TaskNotificationReceiver.java index 6ae54463c..eefa9cefe 100644 --- a/skin/src/main/java/org/researchstack/skin/notification/TaskNotificationReceiver.java +++ b/backbone/src/main/java/org/researchstack/backbone/notification/TaskNotificationReceiver.java @@ -1,4 +1,4 @@ -package org.researchstack.skin.notification; +package org.researchstack.backbone.notification; import android.app.Notification; import android.app.NotificationManager; @@ -6,10 +6,10 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.support.v7.app.NotificationCompat; +import androidx.core.app.NotificationCompat; import android.util.Log; -import org.researchstack.skin.ui.MainActivity; +import org.researchstack.backbone.ui.MainActivity; public class TaskNotificationReceiver extends BroadcastReceiver { diff --git a/backbone/src/main/java/org/researchstack/backbone/onboarding/ConsentOnboardingSection.java b/backbone/src/main/java/org/researchstack/backbone/onboarding/ConsentOnboardingSection.java new file mode 100644 index 000000000..d9f9b8be2 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/onboarding/ConsentOnboardingSection.java @@ -0,0 +1,30 @@ +package org.researchstack.backbone.onboarding; + +import android.content.Context; + +import org.researchstack.backbone.model.ConsentDocument; +import org.researchstack.backbone.model.survey.factory.ConsentDocumentFactory; +import org.researchstack.backbone.model.survey.factory.SurveyFactory; + +/** + * Created by TheMDP on 1/5/17. + */ + +public class ConsentOnboardingSection extends OnboardingSection { + + // Serialization must be done manually in OnboardingSectionAdapter + protected ConsentDocument consentDocument; + + @Override + public SurveyFactory getDefaultOnboardingSurveyFactory( + Context context, + SurveyFactory.CustomStepCreator customStepCreator) + { + if (surveyFactory != null) { + return surveyFactory; + } + + surveyFactory = new ConsentDocumentFactory(consentDocument, customStepCreator); + return surveyFactory; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/onboarding/CustomOnboardingSection.java b/backbone/src/main/java/org/researchstack/backbone/onboarding/CustomOnboardingSection.java new file mode 100644 index 000000000..b5b851517 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/onboarding/CustomOnboardingSection.java @@ -0,0 +1,26 @@ +package org.researchstack.backbone.onboarding; + +/** + * Created by TheMDP on 1/12/17. + */ + +public class CustomOnboardingSection extends OnboardingSection { + + CustomOnboardingSection(String customOnboardingType) { + this.customOnboardingType = customOnboardingType; + } + + @Override + public String getOnboardingSectionIdentifier() { + if (onboardingType == OnboardingSectionType.CUSTOM) { + return customOnboardingType; + } + return super.getOnboardingSectionIdentifier(); + } + + /** + * Since Enums cannot have multiple instances of of an Enum with different member variables + * We must store the custom identifier elsewhere + */ + transient String customOnboardingType; +} diff --git a/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingManager.java b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingManager.java new file mode 100644 index 000000000..bdb97fedb --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingManager.java @@ -0,0 +1,372 @@ +package org.researchstack.backbone.onboarding; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.ResourcePathManager; +import org.researchstack.backbone.StorageAccess; +import org.researchstack.backbone.model.ConsentSection; +import org.researchstack.backbone.model.ConsentSectionAdapter; +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.model.survey.SurveyItemAdapter; +import org.researchstack.backbone.model.survey.factory.ConsentDocumentFactory; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.model.survey.factory.SurveyFactory; +import org.researchstack.backbone.ResourceManager; +import org.researchstack.backbone.ui.OnboardingTaskActivity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Created by TheMDP on 12/22/16. + * + * OnboardingManager is a more sophisticated version of TaskProvider + * It deserializes JSON from the onboarding JSON file, and converts it ultimately into Steps + * Most interactions with the OnboardingManager come from launchOnboarding + * which can launch onboarding of a certain type, and will provide Steps in the correct + * order in which the user will go through without much work from the app developer + */ + +public class OnboardingManager implements SurveyFactory.CustomStepCreator { + + static final String LOG_TAG = OnboardingManager.class.getCanonicalName(); + + /** + * Class used for easy deserialization of sections list + */ + class SectionsGsonHolder { + @SerializedName("sections") + List sections; + } + private SectionsGsonHolder mSectionsGsonHolder; + + /* + * Always initalize using this class + * @param Context used in reference to the ResourceManager to load JSON resources to construct + * the onboarding manager + * @return OnboardingManager set up using ResourceManager, so make sure it is initialized + */ + public OnboardingManager(Context context) { + ResourcePathManager.Resource onboarding = ResourceManager.getInstance().getOnboardingManager(); + String onboardingJson = ResourceManager.getResourceAsString(context, + ResourceManager.getInstance().generatePath(onboarding.getType(), onboarding.getName())); + + Gson gson = buildGson(context); // Do not store this gson as a member variable, it has a link to Context + mSectionsGsonHolder = gson.fromJson(onboardingJson, SectionsGsonHolder.class); + Collections.sort(mSectionsGsonHolder.sections, getSectionComparator()); + } + + public List getSections() { + return mSectionsGsonHolder.sections; + } + + /** + * Override to register custom SurveyItemAdapters, + * but make sure that the adapter extends from SurveyItemAdapter, and only overrides + * the method getCustomClass() + * @param builder the gson build to add the survey item adapter to + */ + public void registerSurveyItemAdapter(GsonBuilder builder) { + builder.registerTypeAdapter(SurveyItem.class, new SurveyItemAdapter()); + } + + /** + * Override to register custom ConsentSectionAdapters, + * but make sure that the adapter extends from ConsentSectionAdapter + * This can allow you to provide a custom ConsentDocumentFactory + * @param builder the gson builder to add to + * @param provider that is needed to create a ConsentSectionAdapter + */ + public void registerOnboardingSectionAdapter(GsonBuilder builder, AdapterContextProvider provider) { + builder.registerTypeAdapter(OnboardingSection.class, new OnboardingSectionAdapter(provider)); + } + + /** + * @param context should be activity context, just in case this is stored, but + * the results of this method should not be since it will store context + * @return a Gson to be used by the OnboardingManager + */ + private Gson buildGson(final Context context) { + // We give access to context through this interface so that once this method + // is over, we won't hold a reference to context + final AdapterContextProvider provider = () -> context; + + GsonBuilder onboardingGson = new GsonBuilder(); + registerSurveyItemAdapter(onboardingGson); + onboardingGson.registerTypeAdapter(ConsentSection.class, new ConsentSectionAdapter(provider)); + registerOnboardingSectionAdapter(onboardingGson, provider); + return onboardingGson.create(); + } + + // Override this to control OnboardingSection sort order + public Comparator getSectionComparator() { + return new SectionComparator(); + } + + /** + * When initializing an onboarding manager with a json file, + * the sections list will be sorted according to this class. By default, + * this sort function will ensure that the sections conforming to `OnboardingSectionType` field + * are ordered as needed to ensure the proper sequence of login, consent and registration according + * to their ordinal position. All custom sections are left in the order they were included in the + * original List. + **/ + static class SectionComparator implements Comparator { + @Override + public int compare(OnboardingSection lhs, OnboardingSection rhs) { + if (lhs == null && rhs == null) { + return 0; + } else if (lhs == null) { + return 1; + } else if (rhs == null) { + return -1; + } + + OnboardingSectionType lhsType = lhs.getOnboardingSectionType(); + OnboardingSectionType rhsType = rhs.getOnboardingSectionType(); + + // If there is a CUSTOM type, just return same, or 0, so it does not shift positions + if (lhsType == OnboardingSectionType.CUSTOM || rhsType == OnboardingSectionType.CUSTOM) { + return 0; + } + + // Passcode is a special case right now, since + + Integer lhsOrdinal = lhsType.ordinal(); + Integer rhsOrdinal = rhsType.ordinal(); + return lhsOrdinal.compareTo(rhsOrdinal); + } + } + + /** + * This is the method that developers should be calling to start off any onboarding process + * @param taskType the type of onboarding process that should be kicked off + * currently, it is either login, registration, or reconsent + * @param context used to transition app to the new onboarding activity + */ + public void launchOnboarding(OnboardingTaskType taskType, Context context) { + if (getSections() == null) { + Log.e(LOG_TAG, "Improper Onboarding json file, sections is null"); + return; + } + + if (getSections().isEmpty()) { + Log.e(LOG_TAG, "Improper Onboarding json file, sections are empty"); + return; + } + + List steps = new ArrayList<>(); + for (OnboardingSection section : getSections()) { + List subSteps = steps(context, section, taskType); + if (subSteps != null) { + setStepTitles(section, subSteps); + steps.addAll(subSteps); + } + } + + String identifier = taskType.toString(); + + NavigableOrderedTask task = createOnboardingTask(identifier, steps); + Intent onboardingTaskIntent = createOnboardingTaskActivityIntent(context, task); + context.startActivity(onboardingTaskIntent); + } + + /** + * Can be overridden by a sub-class to inject their own Task + * + * @param identifier Task Identifier + * @param stepList to make NavigableOrderedTask + * @return NavigableOrderedTask with step list + */ + public OnboardingManagerTask createOnboardingTask(String identifier, List stepList) { + return new OnboardingManagerTask(identifier, stepList); + } + + /** + * Intent must contain class of type OnboardingTaskActivity, otherwise it may not operate correctly + * + * @param context used to launch activity + * @param task to be sent to OnboardingTaskActivity or sub-class Activity + * + * @return an intent holding a reference to OnboardingTaskActivity or a sub-class of it + */ + public Intent createOnboardingTaskActivityIntent(Context context, NavigableOrderedTask task) { + return OnboardingTaskActivity.newIntent(context, task); + } + + /** + * Get the steps that should be included for a given `SBAOnboardingSection` and `SBAOnboardingTaskType`. + * By default, this will return the steps created using the default onboarding survey factory for that section + * or nil if the steps for that section should not be included for the given task. + * @param context used to determine if user is signed, registered, or consented + * @param section get the steps for this section + * @param taskType the type of task to control section steps + * @return step list for this onboarding section and the task type + */ + public List steps(final Context context, OnboardingSection section, OnboardingTaskType taskType) { + + // Check to see that the steps for this section should be included + if (!shouldInclude(context, section, taskType)) { + Log.d(LOG_TAG, "No sections for the task type " + taskType.ordinal()); + return null; + } + + // Get the default factory + SurveyFactory factory = section.getDefaultOnboardingSurveyFactory(context, this); + List stepList = factory.createSurveySteps(context, section.surveyItems); + + // For consent, need to filter out steps that should not be included and group the steps into a substep. + // This is to facilitate skipping re-consent for a user who is logging in where it is unknown whether + // or not the user needs to re-consent. Returned this way because the steps in a subclass of ORKOrderedTask + // are immutable but can be skipped using navigation rules. + if (factory instanceof ConsentDocumentFactory) { + ConsentDocumentFactory consentFactory = (ConsentDocumentFactory)factory; + switch (taskType) { + case REGISTRATION: + return Collections.singletonList(consentFactory.registrationConsentStep()); + case LOGIN: + return Collections.singletonList(consentFactory.loginConsentStep()); + default: // RE_CONSENT + return Collections.singletonList(consentFactory.reconsentStep()); + } + } + + // For all other cases, return the steps. + return stepList; + } + + /** + Define the rules for including a given section in a given task type. + @return `true` if the `SBAOnboardingSection` should be included for this `SBAOnboardingTaskType` + */ + protected boolean shouldInclude(Context context, OnboardingSection section, OnboardingTaskType taskType) { + switch (section.getOnboardingSectionType()) { + case LOGIN: + return taskType == OnboardingTaskType.LOGIN; + case CONSENT: + // All types *except* email verification include consent + return (taskType != OnboardingTaskType.REGISTRATION) || + !isRegistered(context); + case ELIGIBILITY: + case REGISTRATION: + // Intro, eligibility and registration are only included in registration + return (taskType == OnboardingTaskType.REGISTRATION) && + !isRegistered(context); + case PASSCODE: + // Passcode is included if it has not already been set + return !hasPasscode(context); + case EMAIL_VERIFICATION: + // Only registration where the login has not been verified includes verification + return (taskType == OnboardingTaskType.REGISTRATION) && + !isLoginVerified(context); + case PROFILE: + return (taskType == OnboardingTaskType.REGISTRATION); + case PERMISSIONS: + case COMPLETION: + // Permissions and completion are included for login and registration + return taskType == OnboardingTaskType.REGISTRATION || + taskType == OnboardingTaskType.LOGIN; + case CUSTOM: + return shouldIncludeCustomSection(context, section.getOnboardingSectionIdentifier(), taskType); + } + return false; + } + + /** + * @param context can be app or activity + * @param sectionIdentifier the custom section type identifier + * @param taskType the custom task type + * @return true if this section should be including for the taskType, false otherwise + */ + public boolean shouldIncludeCustomSection( + Context context, String sectionIdentifier, OnboardingTaskType taskType) { + return false; + } + + /** + * @param context used to access if login is verified + * @return true when the user has successfully signed in, false otherwise + * also, returns false on special case is if the user has signed up but not signed in yet + */ + boolean isLoginVerified(Context context) { + return DataProvider.getInstance().isSignedIn(context); + } + + /** + * @param context used to access if user is registered + * @return true when the user has successfully signed up, or signed in, false otherwise + */ + boolean isRegistered(Context context) { + return DataProvider.getInstance().isSignedUp(context); + } + + /** + * @param context used to access if user has made a pin code yet + * @return true if user has made a pin code yet, false otherwise + */ + boolean hasPasscode(Context context) { + return StorageAccess.getInstance().hasPinCode(context); + } + + @Override + public Step createCustomStep(Context context, SurveyItem item, boolean isSubtaskStep, SurveyFactory factory) { + return null; + } + + /** + * @param section that these steps belong to + * @param stepList the list of steps created from this section and the JSON + */ + public void setStepTitles(OnboardingSection section, List stepList) { + for (Step step : stepList) { + if (step.getStepTitle() == 0) { + switch (section.getOnboardingSectionType()) { + case LOGIN: + step.setStepTitle(R.string.rsb_login_step_title); + break; + case PASSCODE: + step.setStepTitle(R.string.rsb_passcode); + break; + case REGISTRATION: + step.setStepTitle(R.string.rsb_registration_step_title); + break; + case PERMISSIONS: + step.setStepTitle(R.string.rsb_permissions_step_title); + break; + case PROFILE: + step.setStepTitle(R.string.rsb_profile_step_title); + break; + case CONSENT: + step.setStepTitle(R.string.rsb_consent_step_title); + break; + case EMAIL_VERIFICATION: + step.setStepTitle(R.string.rsb_email_verification_step_title); + break; + case ELIGIBILITY: + step.setStepTitle(R.string.rsb_eligibility_step_title); + break; + case COMPLETION: + step.setStepTitle(R.string.rsb_completion_step_title); + break; + } + } + } + } + + public interface AdapterContextProvider { + Context getContext(); + } +} + + diff --git a/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingManagerTask.java b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingManagerTask.java new file mode 100644 index 000000000..3625211ba --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingManagerTask.java @@ -0,0 +1,51 @@ +package org.researchstack.backbone.onboarding; + +import android.content.Context; +import androidx.annotation.StringRes; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.model.survey.factory.ConsentDocumentFactory; +import org.researchstack.backbone.model.survey.factory.SurveyFactory; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.task.NavigableOrderedTask; + +import java.util.List; + +/** + * Created by TheMDP on 1/20/17. + */ + +public class OnboardingManagerTask extends NavigableOrderedTask { + + public OnboardingManagerTask(String identifier, List steps) { + super(identifier, steps); + } + + public OnboardingManagerTask(String identifier, Step... steps) { + super(identifier, steps); + } + + @Override + public String getTitleForStep(Context context, Step step) { + String title = null; + + @StringRes int stepTitleRes = -1; + + // All these are Subtasks, so identifier will be in the form of id.[question_id] + if (step.getStepTitle() != 0) { + stepTitleRes = step.getStepTitle(); + } else if (step.getIdentifier().contains(SurveyFactory.CONSENT_QUIZ_IDENTIFIER)) { + stepTitleRes = R.string.rsb_consent_quiz_step_title; + } else if (step.getIdentifier().contains(ConsentDocumentFactory.CONSENT_REVIEW_IDENTIFIER)) { + stepTitleRes = R.string.rsb_consent_review_step_title; + } else if (step.getIdentifier().contains(ConsentDocumentFactory.CONSENT_SHARING_IDENTIFIER)) { + stepTitleRes = R.string.rsb_consent_review_step_title; + } + + if (stepTitleRes != 0) { + return context.getString(stepTitleRes); + } + + return title; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingSection.java b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingSection.java new file mode 100644 index 000000000..443bd1831 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingSection.java @@ -0,0 +1,72 @@ +package org.researchstack.backbone.onboarding; + +import android.content.Context; + +import com.google.gson.annotations.SerializedName; + +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.model.survey.factory.SurveyFactory; + +import java.util.List; + +/** + * Created by TheMDP on 12/22/16. + */ + +public class OnboardingSection { + + /** + * These are pre-defined onboarding section identifiers + * that are used to create default helper enums for OnboardingSectionType + */ + public static final String LOGIN_IDENTIFIER = "login"; + public static final String ELIGIBILITY_IDENTIFIER = "eligibility"; + public static final String CONSENT_IDENTIFIER = "consent"; + public static final String REGISTRATION_IDENTIFIER = "registration"; + public static final String PASSCODE_IDENTIFIER = "passcode"; + public static final String EMAIL_VERIFICATION_IDENTIFIER = "emailVerification"; + public static final String PERMISSIONS_IDENTIFIER = "permissions"; + public static final String PROFILE_IDENTIFIER = "profile"; + public static final String COMPLETION_IDENTIFIER = "completion"; + + public OnboardingSection() { + super(); + } + + public OnboardingSection(OnboardingSectionType type) { + super(); + this.onboardingType = type; + } + + static final String ONBOARDING_TYPE_GSON = "onboardingType"; + @SerializedName(ONBOARDING_TYPE_GSON) + OnboardingSectionType onboardingType; + public OnboardingSectionType getOnboardingSectionType() { + return onboardingType; + } + + public String getOnboardingSectionIdentifier() { + return onboardingType.getIdentifier(); + } + + static final String ONBOARDING_SURVEY_ITEMS_GSON = "steps"; + @SerializedName(ONBOARDING_SURVEY_ITEMS_GSON) + public List surveyItems; + + // Isnt deserialized into a field, but is used in the deserialization process + static final String ONBOARDING_RESOURCE_NAME_GSON = "resourceName"; + + protected transient SurveyFactory surveyFactory; + public SurveyFactory getDefaultOnboardingSurveyFactory( + Context context, + SurveyFactory.CustomStepCreator customStepCreator) + { + if (surveyFactory != null) { + return surveyFactory; + } + + surveyFactory = new SurveyFactory(); + surveyFactory.setCustomStepCreator(customStepCreator); + return surveyFactory; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingSectionAdapter.java b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingSectionAdapter.java new file mode 100644 index 000000000..e27581a4b --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingSectionAdapter.java @@ -0,0 +1,100 @@ +package org.researchstack.backbone.onboarding; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; + +import org.researchstack.backbone.ResourceManager; +import org.researchstack.backbone.ResourcePathManager; +import org.researchstack.backbone.model.ConsentDocument; +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.model.survey.factory.ConsentDocumentFactory; + +import java.lang.reflect.Type; +import java.util.List; + +/** + * Created by TheMDP on 1/2/17. + */ + +public class OnboardingSectionAdapter implements JsonDeserializer { + + private OnboardingManager.AdapterContextProvider adapterProvider; + + /** + * @param adapterProvider should provide Gson and Context, to avoid storing them as member variables in this class + */ + public OnboardingSectionAdapter(OnboardingManager.AdapterContextProvider adapterProvider) { + this.adapterProvider = adapterProvider; + } + + @Override + public OnboardingSection deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + + JsonElement typeJson = json.getAsJsonObject().get(OnboardingSection.ONBOARDING_TYPE_GSON); + OnboardingSectionType type = context.deserialize(typeJson, OnboardingSectionType.class); + + // setup custom type + if (type == null) { + type = OnboardingSectionType.CUSTOM; + } + + JsonElement resourceName = json.getAsJsonObject().get(OnboardingSection.ONBOARDING_RESOURCE_NAME_GSON); + if (resourceName != null) { + // Android does not support spaces or uppercase letters for resource names + // So convert all of these before we request the resource name + String convertedResourceName = resourceName.getAsString().replace(" ", "_").toLowerCase(); + ResourcePathManager.Resource resource = ResourceManager.getInstance().getResource(convertedResourceName); + if (resource == null) { + throw new IllegalStateException("Could not find resource with name " + convertedResourceName + + "to load in onboarding JSON. Make sure you add it with ResourceManager.addResource(), so this" + + "class knows where to load it from"); + } + String resourceJson = ResourceManager.getResourceAsString(adapterProvider.getContext(), + ResourceManager.getInstance().generatePath(resource.getType(), resource.getName())); + JsonParser parser = new JsonParser(); + json = parser.parse(resourceJson); + } + + OnboardingSection section; + // Consent section also has a consent document with it, try and parse it if we have that type + if (type == OnboardingSectionType.CONSENT) { + ConsentOnboardingSection consentSection = createConsentOnboardingSection(); + consentSection.consentDocument = context.deserialize(json, ConsentDocument.class); + section = consentSection; + } else if (type == OnboardingSectionType.CUSTOM) { + section = new CustomOnboardingSection(typeJson.getAsString()); + } else { // otherwise make the base onboarding section class + section = createDefaultOnboardingSection(); + } + section.onboardingType = type; + + List surveyItems = context.deserialize( + json.getAsJsonObject().get(OnboardingSection.ONBOARDING_SURVEY_ITEMS_GSON), + new TypeToken>() {}.getType()); + section.surveyItems = surveyItems; + + return section; + } + + /** + * Can be overridden by a sub-class to provide a custom OnboardingSection + * which can provide a custom SurveyFactory + * @return a OnboardingSection class + */ + protected OnboardingSection createDefaultOnboardingSection() { + return new OnboardingSection(); + } + + /** + * Can be overridden by a sub-class to provide a custom ConsentOnboardingSection + * which can provide a custom ConsentDocumentFactory + * @return a ConsentOnboardingSection class + */ + protected ConsentOnboardingSection createConsentOnboardingSection() { + return new ConsentOnboardingSection(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingSectionType.java b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingSectionType.java new file mode 100644 index 000000000..783131c9d --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingSectionType.java @@ -0,0 +1,48 @@ +package org.researchstack.backbone.onboarding; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by TheMDP on 12/22/16. + */ + +public enum OnboardingSectionType { + // TODO: move passcode back to where it needs to be, after registration + // TODO: for now, a passcode is required to save any data locally, so keep it first, here, until + // TODO: we figure out how to fix the root cause in the flow. + @SerializedName(OnboardingSection.PASSCODE_IDENTIFIER) + PASSCODE(OnboardingSection.PASSCODE_IDENTIFIER), + + @SerializedName(OnboardingSection.LOGIN_IDENTIFIER) + LOGIN(OnboardingSection.LOGIN_IDENTIFIER), + @SerializedName(OnboardingSection.ELIGIBILITY_IDENTIFIER) + ELIGIBILITY(OnboardingSection.ELIGIBILITY_IDENTIFIER), + @SerializedName(OnboardingSection.CONSENT_IDENTIFIER) + CONSENT(OnboardingSection.CONSENT_IDENTIFIER), + @SerializedName(OnboardingSection.REGISTRATION_IDENTIFIER) + REGISTRATION(OnboardingSection.REGISTRATION_IDENTIFIER), + @SerializedName(OnboardingSection.EMAIL_VERIFICATION_IDENTIFIER) + EMAIL_VERIFICATION(OnboardingSection.EMAIL_VERIFICATION_IDENTIFIER), + @SerializedName(OnboardingSection.PERMISSIONS_IDENTIFIER) + PERMISSIONS(OnboardingSection.PERMISSIONS_IDENTIFIER), + @SerializedName(OnboardingSection.PROFILE_IDENTIFIER) + PROFILE(OnboardingSection.PROFILE_IDENTIFIER), + @SerializedName(OnboardingSection.COMPLETION_IDENTIFIER) + COMPLETION(OnboardingSection.COMPLETION_IDENTIFIER), + // Custom onboarding section, identifier should be set in OnboardingSection class + CUSTOM(null); + + OnboardingSectionType(String identifier) { + this.identifier = identifier; + } + + private String identifier; + + /** + * @return identifier for OnboardingSectionStep, only to be used for comparison + * outside of the OnboardingManager + */ + public String getIdentifier() { + return identifier; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingTaskType.java b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingTaskType.java new file mode 100644 index 000000000..0d1e2d04c --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/onboarding/OnboardingTaskType.java @@ -0,0 +1,16 @@ +package org.researchstack.backbone.onboarding; + +import com.google.gson.annotations.SerializedName; + +/** + * Created by TheMDP on 12/22/16. + */ + +public enum OnboardingTaskType { + @SerializedName("registration") + REGISTRATION, + @SerializedName("login") + LOGIN, + @SerializedName("reconsent") + RECONSENT; +} diff --git a/backbone/src/main/java/org/researchstack/backbone/onboarding/ReConsentInstructionStep.java b/backbone/src/main/java/org/researchstack/backbone/onboarding/ReConsentInstructionStep.java new file mode 100644 index 000000000..fbe3051d2 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/onboarding/ReConsentInstructionStep.java @@ -0,0 +1,18 @@ +package org.researchstack.backbone.onboarding; + +import org.researchstack.backbone.step.InstructionStep; + +/** + * Created by TheMDP on 3/25/17. + */ + +public class ReConsentInstructionStep extends InstructionStep { + /* Default constructor needed for serialization/deserialization of object */ + public ReConsentInstructionStep() { + super(); + } + + public ReConsentInstructionStep(String identifier, String title, String detailText) { + super(identifier, title, detailText); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/result/AudioResult.java b/backbone/src/main/java/org/researchstack/backbone/result/AudioResult.java new file mode 100644 index 000000000..6d1086e83 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/result/AudioResult.java @@ -0,0 +1,39 @@ +package org.researchstack.backbone.result; + +import java.io.File; + +/** + * Created by TheMDP on 2/26/17. + */ + +public class AudioResult extends FileResult { + + /** + * A value from 0.0 - 1.0 + * The rolling average of the audio data, + * can be used to get an overall picture of the audio's background noise + * For instance, you can not allow the user to do a step if this is > 0.45 + */ + private double rollingAverageOfVolume; + + /* Default identifier for serialization/deserialization */ + AudioResult() { + super(); + } + + public AudioResult(String identifier) { + super(identifier); + } + + public AudioResult(String identifier, File file, String contentType) { + super(identifier, file, contentType); + } + + public double getRollingAverageOfVolume() { + return rollingAverageOfVolume; + } + + public void setRollingAverageOfVolume(double rollingAverageOfVolume) { + this.rollingAverageOfVolume = rollingAverageOfVolume; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/result/FileResult.java b/backbone/src/main/java/org/researchstack/backbone/result/FileResult.java new file mode 100644 index 000000000..fecbf7aca --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/result/FileResult.java @@ -0,0 +1,83 @@ +package org.researchstack.backbone.result; + +import java.io.File; + +/** + * Created by TheMDP on 2/6/17. + * + * The `FileResult` class is a result that references the location of a file produced + * during a task. + * + * A file result is typically generated by the library as the task proceeds. When the task + * completes, it may be appropriate to serialize the linked file for transmission + * to the server. + * + * Active steps typically produce file results when Accelerometer or Gyroscope sensor data is + * serialized to disk using a data logger (`DataLogger`). Audio recording also produces a file + * result. + * + * When you write a custom step, use files to report results only when the data + * is likely to be too big to hold in memory for the duration of the task. For + * example, fitness tasks that use sensors can be quite long and can generate + * a large number of samples. To compensate for the length of the task, you can stream the samples to disk during + * the task, and return a `FileResult` object in the result hierarchy, usually as a + * child of an `StepResult` object. + */ + +public class FileResult extends Result { + + /** + * The MIME content type of the result. + * + * For example, "application/json". + */ + private String contentType; + + /** + * The File location of the file produced. + * + * It is the responsibility of the receiver of the result object to delete + * the file when it is no longer needed. + * + * The file is typically written to the output directory of the + * ViewTaskActivity, so it is common to manage the archiving or cleanup + * of these files by archiving or deleting the entire output directory. + */ + private File file; + + /* Default identifier for serialization/deserialization */ + FileResult() { + super(); + } + + public FileResult(String identifier) { + super(identifier); + } + + /** + * @param identifier identifier for this FileResult + * @param file the location of the file + * @param contentType the mime content type of the file + */ + public FileResult(String identifier, File file, String contentType) { + super(identifier); + setFile(file); + setContentType(contentType); + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public File getFile() { + return file; + } + + public void setFile(File file) { + this.file = file; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/result/Result.java b/backbone/src/main/java/org/researchstack/backbone/result/Result.java index b64ff5571..c5f62dc2f 100644 --- a/backbone/src/main/java/org/researchstack/backbone/result/Result.java +++ b/backbone/src/main/java/org/researchstack/backbone/result/Result.java @@ -1,5 +1,7 @@ package org.researchstack.backbone.result; +import org.researchstack.backbone.utils.ObjectUtils; + import java.io.Serializable; import java.util.Date; @@ -22,14 +24,16 @@ * subclass. */ public class Result implements Serializable { - private String identifier; + String identifier; private Date startDate; private Date endDate; - // unimplemented but exists in RK, implement or delete if not needed - private boolean saveable; + /* Default identifier for serialization/deserialization */ + Result() { + super(); + } /** * Returns an initialized result using the specified identifier. @@ -97,4 +101,23 @@ public void setEndDate(Date endDate) { this.endDate = endDate; } + /** + * @param newIdentifier new identifier for result + * @return a deep copy of this object, and its polymorphism, with a new identifier set + */ + public Result deepCopy(String newIdentifier) { + Result copy = (Result)ObjectUtils.clone(this); + copy.identifier = newIdentifier; + return copy; + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Result{"); + sb.append("identifier='").append(identifier).append('\''); + sb.append(", startDate=").append(startDate); + sb.append(", endDate=").append(endDate); + sb.append('}'); + return sb.toString(); + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/result/StepResult.java b/backbone/src/main/java/org/researchstack/backbone/result/StepResult.java index 603beb1db..095010aaf 100644 --- a/backbone/src/main/java/org/researchstack/backbone/result/StepResult.java +++ b/backbone/src/main/java/org/researchstack/backbone/result/StepResult.java @@ -3,9 +3,14 @@ import org.researchstack.backbone.answerformat.AnswerFormat; import org.researchstack.backbone.step.QuestionStep; import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.utils.ObjectUtils; +import java.io.Serializable; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; /** @@ -29,6 +34,12 @@ public class StepResult extends Result { private AnswerFormat answerFormat; + /* Default identifier for serilization/deserialization */ + StepResult() { + super(); + this.results = new LinkedHashMap<>(); + } + /** * Creates a StepResult from a {@link Step}. *

    @@ -39,7 +50,7 @@ public class StepResult extends Result { */ public StepResult(Step step) { super(step.getIdentifier()); - this.results = new HashMap<>(); + this.results = new LinkedHashMap<>(); if (step instanceof QuestionStep) { answerFormat = ((QuestionStep) step).getAnswerFormat(); @@ -108,4 +119,16 @@ public void setResultForIdentifier(String identifier, T result) { public AnswerFormat getAnswerFormat() { return answerFormat; } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("StepResult{"); + sb.append(", identifier='").append(identifier).append('\''); + sb.append(", answerFormat=").append(answerFormat); + sb.append(", startDate=").append(getStartDate()); + sb.append(", endDate=").append(getIdentifier()); + sb.append("results=").append(results); + sb.append('}'); + return sb.toString(); + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/result/TappingIntervalResult.java b/backbone/src/main/java/org/researchstack/backbone/result/TappingIntervalResult.java new file mode 100644 index 000000000..acf6c3b61 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/result/TappingIntervalResult.java @@ -0,0 +1,151 @@ +package org.researchstack.backbone.result; + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; +import java.util.Locale; + +/** + * Created by TheMDP on 2/23/17. + */ + +public class TappingIntervalResult extends Result { + + /** + * An array of collected samples, in which each item is an `ORKTappingSample` object that represents a + * tapping event. + */ + @SerializedName("TappingSamples") + private List samples; + + /** + * The size of the bounds of the step view containing the tap targets. + */ + @SerializedName("TappingViewSize") + private String stepViewSize; + + /** + * The frame of the left button, in points, relative to the step view bounds. + */ + @SerializedName("ButtonRectLeft") + private String buttonRectLeft; + + /** + * sThe frame of the right button, in points, relative to the step view bounds. + */ + @SerializedName("ButtonRectRight") + private String buttonRectRight; + + /* Default identifier for serialization/deserialization */ + TappingIntervalResult() { + super(); + } + + public TappingIntervalResult(String identifier) { + super(identifier); + } + + public List getSamples() { + return samples; + } + + public void setSamples(List samples) { + this.samples = samples; + } + + public void setStepViewSize(int width, int height) { + this.stepViewSize = String.format(Locale.getDefault(), "{%d, %d}", width, height); + } + + public void setButtonRect1(int x, int y, int width, int height) { + // This is the output format of iOS' NSStringFromCGRect, which is the expected format + this.buttonRectLeft = String.format(Locale.getDefault(), "{{%d, %d} {%d, %d}}", x, y, width, height); + } + + public void setButtonRect2(int x, int y, int width, int height) { + // This is the output format of iOS' NSStringFromCGRect, which is the expected format + this.buttonRectRight = String.format(Locale.getDefault(), "{{%d, %d} {%d, %d}}", x, y, width, height); + } + + public static class Sample implements Serializable { + /** + * A relative timestamp indicating the time of the tap event. + * + * The timestamp is relative to the value of `startDate` in the `Result` object that includes this + * sample. + */ + @SerializedName("TapTimeStamp") + private long timestamp; + + /** + * A duration of the tap event. + * + * The duration store time interval between touch down and touch release events. + */ + @SerializedName("duration") + private long duration; + + /** + * An enumerated value that indicates which button was tapped, if any. + * + * If the value of this property is `ORKTappingButtonIdentifierNone`, it indicates that the tap + * was near, but not inside, one of the target buttons. + */ + @SerializedName("TappedButtonId") + private TappingButtonIdentifier buttonIdentifier; + + /** + * The location of the tap within the step's view. + * + * The location coordinates are relative to a rectangle whose size corresponds to + * the `stepViewSize` in the enclosing `ORKTappingIntervalResult` object. + */ + @SerializedName("TapCoordinate") + private String location; + + /* Default identifier for serialization/deserialization */ + public Sample() { + super(); + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public long getDuration() { + return duration; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + public TappingButtonIdentifier getButtonIdentifier() { + return buttonIdentifier; + } + + public void setButtonIdentifier(TappingButtonIdentifier buttonIdentifier) { + this.buttonIdentifier = buttonIdentifier; + } + public void setLocation(int x, int y) { + this.location = String.format(Locale.getDefault(), "{%d, %d}", x, y); + } + } + + /** + Values that identify the button that was tapped in a tapping sample. + */ + public enum TappingButtonIdentifier { + // The touch landed outside of the two buttons. + TappedButtonNone, + // The touch landed in the left button. + TappedButtonLeft, + // The touch landed in the right button. + TappedButtonRight; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/result/TaskResult.java b/backbone/src/main/java/org/researchstack/backbone/result/TaskResult.java index f7abc5249..0988249f8 100644 --- a/backbone/src/main/java/org/researchstack/backbone/result/TaskResult.java +++ b/backbone/src/main/java/org/researchstack/backbone/result/TaskResult.java @@ -2,7 +2,15 @@ import android.net.Uri; +import org.researchstack.backbone.step.QuestionStep; +import org.researchstack.backbone.step.Step; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -17,18 +25,34 @@ *

    * The results property contains the step results for the task. */ -public class TaskResult extends Result { +public class TaskResult extends Result +{ private Map results; - // unimplemented but exists in RK, implement or delete if not needed - private UUID uuidTask; + /** Store extra details needed for parsing the result in the taskDetails */ + private Map taskDetails; - // unimplemented but exists in RK, implement or delete if not needed - private Uri outputDirectory; + /* Default identifier for serilization/deserialization */ + TaskResult() { + super(); + this.results = new LinkedHashMap<>(); + this.taskDetails = new HashMap<>(); + } - public TaskResult(String identifier) { + public TaskResult(String identifier) + { super(identifier); - this.results = new HashMap<>(); + this.results = new LinkedHashMap<>(); + this.taskDetails = new HashMap<>(); + } + + /** + * Set the Map of all of the StepResults in the task. + * @param newResults set the results object + */ + public void setResults(Map newResults) + { + results = newResults; } /** @@ -36,7 +60,8 @@ public TaskResult(String identifier) { * * @return a Map of the StepResults */ - public Map getResults() { + public Map getResults() + { return results; } @@ -44,9 +69,10 @@ public Map getResults() { * Returns a step result for the specified step identifier, if one exists. * * @param identifier The identifier for which to search. - * @return The result for the specified step, or {@link nil} for none. + * @return The result for the specified step, or null for none. */ - public StepResult getStepResult(String identifier) { + public StepResult getStepResult(String identifier) + { return results.get(identifier); } @@ -59,4 +85,12 @@ public StepResult getStepResult(String identifier) { public void setStepResultForStepIdentifier(String identifier, StepResult stepResult) { results.put(identifier, stepResult); } -} + + public Map getTaskDetails() { + return taskDetails; + } + + public void setTaskDetails(Map taskDetails) { + this.taskDetails = taskDetails; + } +} \ No newline at end of file diff --git a/backbone/src/main/java/org/researchstack/backbone/result/TimedWalkResult.java b/backbone/src/main/java/org/researchstack/backbone/result/TimedWalkResult.java new file mode 100644 index 000000000..aa279957c --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/result/TimedWalkResult.java @@ -0,0 +1,55 @@ +package org.researchstack.backbone.result; + +/** + * Created by TheMDP on 2/22/17. + */ + +public class TimedWalkResult extends Result { + /** + The timed walk distance in meters. + */ + private double distanceInMeters; + + /** + The time limit to complete the trials. + */ + private int timeLimit; + + /** + The trial duration (that is, the time taken to do the walk). + */ + private int duration; + + /* Default identifier for serilization/deserialization */ + TimedWalkResult() { + super(); + } + + public TimedWalkResult(String identifier) { + super(identifier); + } + + public double getDistanceInMeters() { + return distanceInMeters; + } + + public void setDistanceInMeters(double distanceInMeters) { + this.distanceInMeters = distanceInMeters; + } + + public int getTimeLimit() { + return timeLimit; + } + + public void setTimeLimit(int timeLimit) { + this.timeLimit = timeLimit; + } + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/result/logger/DataLogger.java b/backbone/src/main/java/org/researchstack/backbone/result/logger/DataLogger.java new file mode 100644 index 000000000..b269ea644 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/result/logger/DataLogger.java @@ -0,0 +1,133 @@ +package org.researchstack.backbone.result.logger; + +import org.researchstack.backbone.utils.LogExt; + +import java.io.File; +import java.io.UnsupportedEncodingException; + +/** + * Created by TheMDP on 2/6/17. + * + * The DataLogger class can be used to stream data to a file on another thread + * It should only be used when the data may become too large to store in app memory + */ + +public class DataLogger { + + protected static final String UTF_8 = "UTF-8"; + + /** + * The file that the data is being written to + */ + private File file; + + /** + * The Thread that performs the file writing + */ + private DataLoggerFileWriterThread dataLoggerWriterThread; + private DataWriteListener dataWriteListener; + + public DataLogger(File file, DataWriteListener listener) { + this.file = file; + this.dataWriteListener = listener; + } + + /** + * @param fileHeader the header of the file to write + * @param fileFooter the footer of the file to write + */ + public void start(String fileHeader, String fileFooter) { + if (dataLoggerWriterThread != null) { + throw new IllegalStateException("Thread was started while another was running, " + + "check your application logic, because this is not allowed"); + } + + dataLoggerWriterThread = new DataLoggerFileWriterThread( + file, fileHeader, fileFooter, + new DataWriteListener() + { + @Override + public void onWriteError(Throwable throwable) { + dataLoggerFailed(throwable); + } + + @Override + public void onWriteComplete(File file) { + DataLoggerManager.getInstance().dataLoggerTaskFinished(DataLogger.this, null); + dataWriteListener.onWriteComplete(file); + dataLoggerWriterThread = null; + } + }); + + DataLoggerManager.getInstance().startNewDataLoggerTask(this); + dataLoggerWriterThread.start(); + } + + /** + * Call when you are done writing to the data logger + */ + public void stop() { + if (dataLoggerWriterThread == null) { + throw new IllegalStateException("You need to call start() first"); + } + dataLoggerWriterThread.stop(); + } + + /** + * Call when you want the data logger file to immediately stop and the file deleted + */ + public void cancel() { + if (dataLoggerWriterThread != null) { + dataLoggerWriterThread.cancel(); + } + } + + /** + * Cancels the data logger because of an error that happened with a class using this class + * @param throwable the error to return to the listener, that happened above this class + */ + public void cancelDueToError(Throwable throwable) { + dataLoggerWriterThread.cancel(); + dataLoggerFailed(throwable); + } + + private void dataLoggerFailed(Throwable throwable) { + LogExt.e(getClass(), "Data logger failed ", throwable); + DataLoggerManager.getInstance().dataLoggerTaskFinished(DataLogger.this, throwable); + dataWriteListener.onWriteError(throwable); + dataLoggerWriterThread = null; + } + + /** + * @param data to append to the file on a different async task thread + */ + public void appendData(byte[] data) { + if (dataLoggerWriterThread != null) { + dataLoggerWriterThread.appendData(data); + } + } + + /** + * @param data string byte data to append to the file on a different async task thread + */ + public void appendData(String data) { + try { + appendData(data.getBytes(UTF_8)); + } catch (UnsupportedEncodingException e) { + dataWriteListener.onWriteError(e); + } + } + + public File getFile() { + return file; + } + + public void setFile(File file) { + this.file = file; + } + + public interface DataWriteListener { + void onWriteError(Throwable throwable); + void onWriteComplete(File file); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/result/logger/DataLoggerFileWriterThread.java b/backbone/src/main/java/org/researchstack/backbone/result/logger/DataLoggerFileWriterThread.java new file mode 100644 index 000000000..d707597f7 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/result/logger/DataLoggerFileWriterThread.java @@ -0,0 +1,227 @@ +package org.researchstack.backbone.result.logger; + +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import androidx.annotation.MainThread; + +import org.researchstack.backbone.utils.LogExt; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Created by TheMDP on 2/9/17. + * + * The DataLoggerFileWriterThread uses a HandlerThread to send byte[] data to another thread + * which writes it to a file asynchronously + */ + +public class DataLoggerFileWriterThread { + + private static final String BUNDLE_KEY_BYTE_DATA = "bytedata"; + + private static final int MSG_WRITE_REQUEST = 1; + private static final int MSG_STOP = 2; + private static final int MSG_CANCEL = 3; + + /** + * File output stream to write to the file + * this object SHOULD NOT be touched on the main thread, and only from within the Handler + */ + private FileOutputStream fileOutputStream; + + /** + * The listener for this class, will only be called from the main thread + */ + private final DataLogger.DataWriteListener writeListener; + + private final File file; + private final String fileHeader; + private final String fileFooter; + + private HandlerThread thread; + private FileWriterHandler threadHandler; + private Handler mainHandler; + + protected DataLoggerFileWriterThread( + File file, + String fileHeader, + String fileFooter, + DataLogger.DataWriteListener listener) + { + this.file = file; + this.fileHeader = fileHeader; + this.fileFooter = fileFooter; + this.writeListener = listener; + } + + private class FileWriterHandler extends Handler { + + private FileWriterHandler(Looper looper) { + super(looper); + } + private final AtomicBoolean isStopped = new AtomicBoolean(false); + + @Override + public void handleMessage(Message msg) { + try { + switch (msg.what) { + case MSG_WRITE_REQUEST: + if (isStopped.get()) { + LogExt.w(getClass(), "Data write request received after stopped or cancelled"); + return; + } + + openFileStreamIfNull(); + + Bundle bundle = msg.getData(); + if (bundle != null) { + byte[] bytesToWrite = bundle.getByteArray(BUNDLE_KEY_BYTE_DATA); + if (bytesToWrite != null) { + fileOutputStream.write(bytesToWrite); + } + } + + break; + case MSG_STOP: + if (isStopped.get()) { + LogExt.w(getClass(), "Stop request received after stopped or cancelled"); + return; + } + isStopped.set(true); + + // call openFileStreamIfNull to combat an edge case where no write requests were processed + openFileStreamIfNull(); + closeFileStream(); + writeCompleteFromThreadToMainThread(); + + break; + case MSG_CANCEL: + if (isStopped.get()) { + LogExt.w(getClass(), "Cancel request received after stopped or cancelled"); + return; + } + isStopped.set(true); + + closeFileStream(); + writeCanceledFromThreadToMainThread(); + + break; + } + } catch (final IOException e) { + if (fileOutputStream != null) { + try { + fileOutputStream.close(); + } catch (IOException closingException) { + closingException.printStackTrace(); + } + } + if (e.getMessage().contains("Stream Closed")) { + // Ignore this IOException, since the message can just be ignored safely + } else { + writeFailedFromThreadToMainThread(e); + } + } + } + + private void openFileStreamIfNull() throws IOException { + if (!file.exists()) { + file.createNewFile(); + } + if (fileOutputStream == null) { + fileOutputStream = new FileOutputStream(file, true); + // Write fileHeader if there is one + if (fileHeader != null) { + fileOutputStream.write(fileHeader.getBytes(DataLogger.UTF_8)); + } + } + } + + private void closeFileStream() throws IOException { + if (fileOutputStream != null) { + if (fileFooter != null) { + fileOutputStream.write(fileFooter.getBytes(DataLogger.UTF_8)); + } + fileOutputStream.close(); + } + } + } + + @MainThread + protected void start() { + if (thread != null) { + throw new IllegalStateException("Cannot call start while thread is running"); + } + + + thread = new HandlerThread(file.getName()); + mainHandler = new Handler(); + thread.start(); + threadHandler = new FileWriterHandler(thread.getLooper()); + } + + /** + * Will stop the thread which will soon after call the listener's complete callback + */ + @MainThread + protected void stop() { + threadHandler.sendEmptyMessage(MSG_STOP); + } + + @MainThread + protected void cancel() { + threadHandler.sendEmptyMessage(MSG_CANCEL); + } + + // Only should be called from thread handler + private void writeFailedFromThreadToMainThread(final Throwable throwable) { + thread.quit(); + mainHandler.post(new Runnable() { + @Override + public void run() { + writeListener.onWriteError(throwable); + } + }); + } + + // Only should be called from thread handler + private void writeCompleteFromThreadToMainThread() { + mainHandler.post(new Runnable() { + @Override + public void run() { + thread.quit(); + writeListener.onWriteComplete(file); + } + }); + } + + // Only should be called from thread handler + private void writeCanceledFromThreadToMainThread() { + mainHandler.post(new Runnable() { + @Override + public void run() { + thread.quit(); + boolean success = file.delete(); + if (!success) { + LogExt.d(getClass(), "Failed to delete file " + file.toString()); + } + } + }); + } + + /** + * @param data append data to the file, which will be sent to the thread for writing + */ + protected void appendData(byte[] data) { + final Bundle bundle = new Bundle(); + bundle.putByteArray(BUNDLE_KEY_BYTE_DATA, data); + Message message = Message.obtain(threadHandler, MSG_WRITE_REQUEST); + message.setData(bundle); + message.sendToTarget(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/result/logger/DataLoggerManager.java b/backbone/src/main/java/org/researchstack/backbone/result/logger/DataLoggerManager.java new file mode 100644 index 000000000..3aa8d4c0f --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/result/logger/DataLoggerManager.java @@ -0,0 +1,212 @@ +package org.researchstack.backbone.result.logger; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.annotation.MainThread; +import android.util.Log; + +import com.google.gson.Gson; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by TheMDP on 2/7/17. + * + * The DataLoggerManager class is central station for the DataLogger file's life cycle + * + * It works by associating a "FileStatus" with each DataLogger file that is created + * The FileStatus can be "dirty", which means it is either being created or has not reached + * the stage of attempted upload. Once the FileStatus is attempted upload, it will exist + * until it is uploaded. After it is uploaded, the necessary parts of the file needed by + * the app can be stored, but the underlying data will be removed. + */ + +public class DataLoggerManager { + + /** + * SharedPreferences are used to keep track of the DataLogger files' status + * They can be dirty while being written, completed, and uploaded + */ + private SharedPreferences sharedPrefs; + private static final String SHARED_PREFS_KEY = "DataLoggerManagerSharedPrefs"; + private Gson gson; // used to serialize/deserialize DataLoggerFileStatus + + private static DataLoggerManager instance; + + @MainThread + public static DataLoggerManager getInstance() { + if (instance == null) { + throw new IllegalStateException("DataLoggerManager must be initialized with context before it can be accessed"); + } + return instance; + } + + /** + * @return true if DataLoggerManager is initialized with context, false if not and it needs to be + */ + public static boolean isInitialized() { + return instance != null; + } + + @MainThread + public static void initialize(Context context) { + if (instance == null) { + instance = new DataLoggerManager(context); + } + } + + private DataLoggerManager(Context context) { + sharedPrefs = context.getSharedPreferences(SHARED_PREFS_KEY, Context.MODE_PRIVATE); + gson = new Gson(); + } + + @MainThread + protected void startNewDataLoggerTask(DataLogger dataLogger) { + createNewDataLoggerFileStatus(dataLogger.getFile()); + } + + @MainThread + protected void dataLoggerTaskFinished(DataLogger dataLogger, Throwable error) { + if (error != null) { + deleteFileStatus(dataLogger.getFile()); + } + } + + /** + * This creates a new status for the data logger that is is writing and considered "dirty" + * @param file to operate upon + */ + private void createNewDataLoggerFileStatus(File file) { + DataLoggerFileStatus fileStatus = new DataLoggerFileStatus( + fullFilePathAndName(file), true, false); + String sharedPrefsKey = fileStatus.getSharedPrefsKey(); + String statusJson = gson.toJson(fileStatus); + sharedPrefs.edit().putString(sharedPrefsKey, statusJson).apply(); + } + + /** + * This method deletes the file and also deletes it's status + * @param file to operate upon + */ + public void deleteFileStatus(File file) { + DataLoggerFileStatus fileStatus = new DataLoggerFileStatus( + fullFilePathAndName(file), true, false); + String sharedPrefsKey = fileStatus.getSharedPrefsKey(); + + sharedPrefs.edit().remove(sharedPrefsKey).apply(); + boolean success = file.delete(); + + if (!success) { + Log.e(getClass().getCanonicalName(), "Failed to delete data logger file"); + } + } + + /** + * This will loop through all the data logger files that exist in storage and + * remove any that are "dirty". A "dirty" file is any file that has not been zipped + * up and attempted to be uploaded yet. + * + * Examples of dirty data logger files are ones that were written before and app crashed + * or was force closed, or files that were written and then the user cancelled the active task + * that was responsible for creating them + */ + public void deleteAllDirtyFiles() { + Map fileStatusMap = sharedPrefs.getAll(); + + List prefKeysToDelete = new ArrayList<>(); + List filePathsToDelete = new ArrayList<>(); + + // Loop through all the potential file status and find ones that are dirty + for (String key : fileStatusMap.keySet()) { + Object fileStatusObj = fileStatusMap.get(key); + if (fileStatusObj instanceof String) { + DataLoggerFileStatus fileStatus = gson.fromJson((String)fileStatusObj, DataLoggerFileStatus.class); + if (fileStatus.isDirty) { + prefKeysToDelete.add(key); + filePathsToDelete.add(fileStatus.filePath); + } + } + } + + // remove all dirty files + for (String filePath : filePathsToDelete) { + File file = new File(filePath); + if (file.exists()) { + boolean success = file.delete(); + if (!success) { + Log.e(getClass().getCanonicalName(), "Failed to delete dirty file " + filePath); + } + } + } + + // remove dirty shared prefs + SharedPreferences.Editor editor = sharedPrefs.edit(); + for (String prefsKey : prefKeysToDelete) { + editor.remove(prefsKey); + } + editor.apply(); + } + + /** + * Removes the "dirty" status from the files, as they are now complete and should + * be kept around until they are successfully uploaded + * @param fileList list of files to change status of + */ + public void updateFileListToAttemptedUploadStatus(List fileList) { + Map fileStatusMap = sharedPrefs.getAll(); + Map updateMap = new HashMap<>(); + // Loop through all the potential file status and find ones that are dirty + for (String key : fileStatusMap.keySet()) { + Object fileStatusObj = fileStatusMap.get(key); + if (fileStatusObj instanceof String) { + DataLoggerFileStatus fileStatus = gson.fromJson((String)fileStatusObj, DataLoggerFileStatus.class); + for (File file : fileList) { + String filePath = fullFilePathAndName(file); + if (fileStatus.filePath.equals(filePath)) { + DataLoggerFileStatus updateStatus = new DataLoggerFileStatus(filePath, false, false); + updateMap.put(updateStatus.getSharedPrefsKey(), gson.toJson(updateStatus)); + } + } + } + } + + // Update the shared pref refs + SharedPreferences.Editor editor = sharedPrefs.edit(); + for (String prefsKey : updateMap.keySet()) { + editor.putString(prefsKey, updateMap.get(prefsKey)); + } + editor.apply(); + } + + private String fullFilePathAndName(File file) { + return file.getAbsolutePath() + file.getName(); + } + + /** + * Used to keep track of all the DataLogger files and their status + */ + protected class DataLoggerFileStatus { + protected boolean isDirty; + protected boolean isUploaded; + protected String filePath; + + DataLoggerFileStatus() {} + + DataLoggerFileStatus(String filePath, boolean isDirty, boolean isUploaded) { + this.filePath = filePath; + this.isDirty = isDirty; + this.isUploaded = isUploaded; + } + + /** + * @return a unique value that can be used to safely store this class in SharedPreferences + */ + protected String getSharedPrefsKey() { + return String.valueOf(filePath.hashCode()); + } + } +} diff --git a/skin/src/main/java/org/researchstack/skin/schedule/ScheduleHelper.java b/backbone/src/main/java/org/researchstack/backbone/schedule/ScheduleHelper.java similarity index 94% rename from skin/src/main/java/org/researchstack/skin/schedule/ScheduleHelper.java rename to backbone/src/main/java/org/researchstack/backbone/schedule/ScheduleHelper.java index 333dd3f85..6643bca12 100644 --- a/skin/src/main/java/org/researchstack/skin/schedule/ScheduleHelper.java +++ b/backbone/src/main/java/org/researchstack/backbone/schedule/ScheduleHelper.java @@ -1,4 +1,4 @@ -package org.researchstack.skin.schedule; +package org.researchstack.backbone.schedule; import com.cronutils.model.Cron; import com.cronutils.model.CronType; diff --git a/backbone/src/main/java/org/researchstack/backbone/step/AudioTooLoudStep.java b/backbone/src/main/java/org/researchstack/backbone/step/AudioTooLoudStep.java new file mode 100644 index 000000000..7f722e2dd --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/AudioTooLoudStep.java @@ -0,0 +1,79 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.step.active.recorder.AudioRecorder; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.utils.LogExt; + +import java.util.List; + +/** + * Created by TheMDP on 2/26/17. + * + * The AudioTooLoudStep is designed to be placed after an ActiveStep with identifier audioStepResultIdentifier + * The ActiveStep should also have an AudioRecorder that produces an AudioResult that can be + * analyzed in this state to see if this should show to the user that their background noise + * and the recording environment they are in, is too loud, and therefore unsatisfactory for + * the proceeding ActiveStep with AudioRecorder to collect accurate data + */ + +public class AudioTooLoudStep extends InstructionStep implements NavigableOrderedTask.NavigationSkipRule { + + /** + * The step identifier where the AudioResult object will be + */ + private String audioStepResultIdentifier; + + /** + * Must be a value from 0.0 - 1.0 + * Step will be skipped unless the loudness threshold is over this threshold + */ + private double loudnessThreshold; + + private boolean isSkippingStep = false; + + /* Default constructor needed for serialization/deserialization of object */ + AudioTooLoudStep() { + super(); + } + + public AudioTooLoudStep(String identifier, String title, String detailText) { + super(identifier, title, detailText); + } + + @Override + public boolean shouldSkipStep(TaskResult result, List additionalTaskResults) { + boolean isResultTooLoud = AudioRecorder.getLastTotalSampleAvg() > loudnessThreshold; + LogExt.i(getClass(), "Audio is " + (isResultTooLoud ? "" : "not") + + " too loud with value of " + AudioRecorder.getLastTotalSampleAvg()); + isSkippingStep = !isResultTooLoud; + return !isResultTooLoud; + } + + @Override + public String nextStepIdentifier(TaskResult result, List additionalTaskResults) { + // Only use the next step id if we didn't skip this step, otherwise, proceed as normal + if (!isSkippingStep) { + return nextStepIdentifier; + } else { + isSkippingStep = false; // revert back to original operation + return null; + } + } + + public String getAudioStepResultIdentifier() { + return audioStepResultIdentifier; + } + + public void setAudioStepResultIdentifier(String audioStepResultIdentifier) { + this.audioStepResultIdentifier = audioStepResultIdentifier; + } + + public double getLoudnessThreshold() { + return loudnessThreshold; + } + + public void setLoudnessThreshold(double loudnessThreshold) { + this.loudnessThreshold = loudnessThreshold; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/CompletionStep.java b/backbone/src/main/java/org/researchstack/backbone/step/CompletionStep.java new file mode 100644 index 000000000..ec859285e --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/CompletionStep.java @@ -0,0 +1,32 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.ui.step.layout.InstructionStepLayout; +import org.researchstack.backbone.utils.ResUtils; + +/** + * Created by TheMDP on 12/31/16. + */ + +public class CompletionStep extends InstructionStep { + + /* Default constructor needed for serilization/deserialization of object */ + public CompletionStep() { + super(); + commonInit(); + } + + public CompletionStep(String identifier, String title, String detailText) { + super(identifier, title, detailText); + commonInit(); + } + + private void commonInit() { + setImage(ResUtils.ANIMATED_CHECK_MARK_DELAYED); + setIsImageAnimated(true); + } + + @Override + public Class getStepLayoutClass() { + return InstructionStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/ConsentDocumentStep.java b/backbone/src/main/java/org/researchstack/backbone/step/ConsentDocumentStep.java index a587fe68b..97a6b65dc 100644 --- a/backbone/src/main/java/org/researchstack/backbone/step/ConsentDocumentStep.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/ConsentDocumentStep.java @@ -12,6 +12,11 @@ public class ConsentDocumentStep extends Step { private String confirmMessage; + /* Default constructor needed for serilization/deserialization of object */ + ConsentDocumentStep() { + super(); + } + public ConsentDocumentStep(String identifier) { super(identifier); } diff --git a/skin/src/main/java/org/researchstack/skin/step/ConsentQuizEvaluationStep.java b/backbone/src/main/java/org/researchstack/backbone/step/ConsentQuizEvaluationStep.java similarity index 79% rename from skin/src/main/java/org/researchstack/skin/step/ConsentQuizEvaluationStep.java rename to backbone/src/main/java/org/researchstack/backbone/step/ConsentQuizEvaluationStep.java index 778bd071c..6adc6d4a4 100644 --- a/skin/src/main/java/org/researchstack/skin/step/ConsentQuizEvaluationStep.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/ConsentQuizEvaluationStep.java @@ -1,10 +1,10 @@ -package org.researchstack.skin.step; +package org.researchstack.backbone.step; -import org.researchstack.backbone.step.Step; -import org.researchstack.skin.R; -import org.researchstack.skin.model.ConsentQuizModel; -import org.researchstack.skin.ui.layout.ConsentQuizEvaluationStepLayout; +import org.researchstack.backbone.R; +import org.researchstack.backbone.model.ConsentQuizModel; +import org.researchstack.backbone.ui.layout.ConsentQuizEvaluationStepLayout; +@Deprecated // use NavigationFormStep or NavigationSubtaskStep instead public class ConsentQuizEvaluationStep extends Step { private ConsentQuizModel quizModel; diff --git a/skin/src/main/java/org/researchstack/skin/step/ConsentQuizQuestionStep.java b/backbone/src/main/java/org/researchstack/backbone/step/ConsentQuizQuestionStep.java similarity index 69% rename from skin/src/main/java/org/researchstack/skin/step/ConsentQuizQuestionStep.java rename to backbone/src/main/java/org/researchstack/backbone/step/ConsentQuizQuestionStep.java index fb844a464..bf5978453 100644 --- a/skin/src/main/java/org/researchstack/skin/step/ConsentQuizQuestionStep.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/ConsentQuizQuestionStep.java @@ -1,10 +1,10 @@ -package org.researchstack.skin.step; +package org.researchstack.backbone.step; -import org.researchstack.backbone.step.Step; -import org.researchstack.skin.R; -import org.researchstack.skin.model.ConsentQuizModel; -import org.researchstack.skin.ui.layout.ConsentQuizQuestionStepLayout; +import org.researchstack.backbone.R; +import org.researchstack.backbone.model.ConsentQuizModel; +import org.researchstack.backbone.ui.layout.ConsentQuizQuestionStepLayout; +@Deprecated // Use NavigationFormStep or NavigationSubtaskStep instead public class ConsentQuizQuestionStep extends Step { private ConsentQuizModel.QuizQuestion question; diff --git a/backbone/src/main/java/org/researchstack/backbone/step/ConsentReviewSubstepListStep.java b/backbone/src/main/java/org/researchstack/backbone/step/ConsentReviewSubstepListStep.java new file mode 100644 index 000000000..c48c85893 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/ConsentReviewSubstepListStep.java @@ -0,0 +1,26 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.ui.step.layout.ConsentReviewSubstepListStepLayout; + +import java.util.List; + +/** + * Created by TheMDP on 1/16/17. + */ + +public class ConsentReviewSubstepListStep extends SubstepListStep { + + /* Default constructor needed for serilization/deserialization of object */ + ConsentReviewSubstepListStep() { + super(); + } + + public ConsentReviewSubstepListStep(String identifier, List stepList) { + super(identifier, stepList); + } + + @Override + public Class getStepLayoutClass() { + return ConsentReviewSubstepListStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/ConsentSharingStep.java b/backbone/src/main/java/org/researchstack/backbone/step/ConsentSharingStep.java index ca27ec0d1..8632ba82a 100644 --- a/backbone/src/main/java/org/researchstack/backbone/step/ConsentSharingStep.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/ConsentSharingStep.java @@ -1,5 +1,6 @@ package org.researchstack.backbone.step; +import org.researchstack.backbone.answerformat.AnswerFormat; import org.researchstack.backbone.ui.step.body.SingleChoiceQuestionBody; /** @@ -7,16 +8,23 @@ * how much they're willing to allow data to be shared after collection. */ public class ConsentSharingStep extends QuestionStep { + /* Default constructor needed for serilization/deserialization of object */ + ConsentSharingStep() { + super(); + } public ConsentSharingStep(String identifier) { super(identifier); setOptional(false); } + public ConsentSharingStep(String identifier, String title, AnswerFormat format) { + super(identifier, title, format); + setOptional(false); + } + @Override public Class getStepBodyClass() { return SingleChoiceQuestionBody.class; } - - } diff --git a/backbone/src/main/java/org/researchstack/backbone/step/ConsentSignatureStep.java b/backbone/src/main/java/org/researchstack/backbone/step/ConsentSignatureStep.java index 3cf36e23e..9e491fd74 100644 --- a/backbone/src/main/java/org/researchstack/backbone/step/ConsentSignatureStep.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/ConsentSignatureStep.java @@ -1,5 +1,7 @@ package org.researchstack.backbone.step; +import org.researchstack.backbone.ui.step.layout.ConsentSignatureStepLayout; + /** * This class represents the final step in the consent process, collecting the signature from the * study participant. @@ -7,11 +9,23 @@ public class ConsentSignatureStep extends Step { private String signatureDateFormat; + /* Default constructor needed for serilization/deserialization of object */ + ConsentSignatureStep() { + super(); + setOptional(false); + } + public ConsentSignatureStep(String identifier) { super(identifier); setOptional(false); } + public ConsentSignatureStep(String identifier, String title, String text) { + this(identifier); + setTitle(title); + setText(text); + } + /** * Returns the date format string to be used when producing a date string for the PDF or consent * review. @@ -37,4 +51,9 @@ public String getSignatureDateFormat() { public void setSignatureDateFormat(String signatureDateFormat) { this.signatureDateFormat = signatureDateFormat; } + + @Override + public Class getStepLayoutClass() { + return ConsentSignatureStepLayout.class; + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/step/ConsentSubtaskStep.java b/backbone/src/main/java/org/researchstack/backbone/step/ConsentSubtaskStep.java new file mode 100644 index 000000000..2d75b7abc --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/ConsentSubtaskStep.java @@ -0,0 +1,36 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.NavigableOrderedTask; + +import java.util.List; + +/** + * Created by TheMDP on 1/14/17. + */ + +public class ConsentSubtaskStep extends SubtaskStep implements NavigableOrderedTask.NavigationSkipRule { + + /* Default constructor needed for serilization/deserialization of object */ + ConsentSubtaskStep() { + super(); + } + + public ConsentSubtaskStep(String identifier) { + super(identifier); + } + + public ConsentSubtaskStep(String identifier, String title) { + super(identifier, title); + } + + public ConsentSubtaskStep(String identifier, List steps) { + super(identifier, steps); + } + + @Override + public boolean shouldSkipStep(TaskResult result, List additionalTaskResults) { + return DataProvider.getInstance().isConsented(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/ConsentVisualStep.java b/backbone/src/main/java/org/researchstack/backbone/step/ConsentVisualStep.java index 00ca0d6df..bb24317d8 100644 --- a/backbone/src/main/java/org/researchstack/backbone/step/ConsentVisualStep.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/ConsentVisualStep.java @@ -12,11 +12,27 @@ public class ConsentVisualStep extends Step { private ConsentSection section; + /** + * The index of the section within all the visual consent sections + */ + private int sectionIndex; + /** + * The total count of the visual consent sections + */ + private int sectionCount; + @Deprecated private String nextButtonString; - public ConsentVisualStep(String identifier) { + /* Default constructor needed for serilization/deserialization of object */ + protected ConsentVisualStep() { + super(); + } + + public ConsentVisualStep(String identifier, int index, int count) { super(identifier); + setSectionIndex(index); + setSectionCount(count); } @Override @@ -58,4 +74,20 @@ public String getNextButtonString() { public void setNextButtonString(String nextButtonString) { this.nextButtonString = nextButtonString; } + + public int getSectionIndex() { + return sectionIndex; + } + + public void setSectionIndex(int sectionIndex) { + this.sectionIndex = sectionIndex; + } + + public int getSectionCount() { + return sectionCount; + } + + public void setSectionCount(int sectionCount) { + this.sectionCount = sectionCount; + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/step/EmailVerificationStep.java b/backbone/src/main/java/org/researchstack/backbone/step/EmailVerificationStep.java new file mode 100644 index 000000000..9a7ad24de --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/EmailVerificationStep.java @@ -0,0 +1,32 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.ui.step.layout.EmailVerificationStepLayout; + +import java.util.Arrays; + +/** + * Created by TheMDP on 1/21/17. + * + * The EmailVerificationStep contains the substep of email verification and also + * the substep of registration, so that the user can change their email from this step + */ + +public class EmailVerificationStep extends SubstepListStep { + /* Default constructor needed for serilization/deserialization of object */ + EmailVerificationStep() { + super(); + } + + public EmailVerificationStep( + String identifier, + EmailVerificationSubStep verifySubstep, + RegistrationStep registrationStep) + { + super(identifier, Arrays.asList(verifySubstep, registrationStep)); + } + + @Override + public Class getStepLayoutClass() { + return EmailVerificationStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/EmailVerificationSubStep.java b/backbone/src/main/java/org/researchstack/backbone/step/EmailVerificationSubStep.java new file mode 100644 index 000000000..c07001386 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/EmailVerificationSubStep.java @@ -0,0 +1,23 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.ui.step.layout.EmailVerificationStepLayout; + +/** + * Created by TheMDP on 1/4/17. + */ + +public class EmailVerificationSubStep extends InstructionStep { + /* Default constructor needed for serilization/deserialization of object */ + EmailVerificationSubStep() { + super(); + } + + public EmailVerificationSubStep(String identifier, String title, String detailText) { + super(identifier, title, detailText); + } + + @Override + public Class getStepLayoutClass() { + return EmailVerificationStepLayout.SubStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/FormStep.java b/backbone/src/main/java/org/researchstack/backbone/step/FormStep.java index 94db97656..d3e224366 100644 --- a/backbone/src/main/java/org/researchstack/backbone/step/FormStep.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/FormStep.java @@ -1,7 +1,12 @@ package org.researchstack.backbone.step; - +import org.researchstack.backbone.answerformat.AnswerFormat; import org.researchstack.backbone.answerformat.FormAnswerFormat; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.ui.step.layout.FormStepLayout; +import org.researchstack.backbone.ui.step.layout.SurveyStepLayout; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -16,13 +21,39 @@ * includes a child StepResult object for each form item. */ public class FormStep extends QuestionStep { - private List formSteps; + List formSteps; + + private String skipTitle; + + /* Default constructor needed for serialization/deserialization of object */ + public FormStep() { + super(); + } public FormStep(String identifier, String title, String text) { super(identifier, title, new FormAnswerFormat()); setText(text); } + public FormStep(String identifier, String title, String text, List steps) { + this(identifier, title, text); + formSteps = steps; + } + + /** + * If true, the first question body layout with an edittext will receive focus on load + * default is false and nothing will occur + */ + private boolean autoFocusFirstEditText; + + public boolean isAutoFocusFirstEditText() { + return autoFocusFirstEditText; + } + + public void setAutoFocusFirstEditText(boolean autoFocusFirstEditText) { + this.autoFocusFirstEditText = autoFocusFirstEditText; + } + /** * Returns the list of items in the form. * @@ -39,4 +70,23 @@ public void setFormSteps(QuestionStep... formSteps) { public void setFormSteps(List formSteps) { this.formSteps = formSteps; } + + /** + * @return The title of the skip button, default will be localized "Skip" + */ + public String getSkipTitle() { + return skipTitle; + } + + /** + * @param skipTitle The title of the skip button, default will be localized "Skip" + */ + public void setSkipTitle(String skipTitle) { + this.skipTitle = skipTitle; + } + + @Override + public Class getStepLayoutClass() { + return FormStepLayout.class; + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/step/InstructionStep.java b/backbone/src/main/java/org/researchstack/backbone/step/InstructionStep.java index 8efbec31f..2858d50f9 100644 --- a/backbone/src/main/java/org/researchstack/backbone/step/InstructionStep.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/InstructionStep.java @@ -1,7 +1,18 @@ package org.researchstack.backbone.step; +import android.util.Log; +import android.widget.ImageView; + +import org.researchstack.backbone.model.survey.InstructionSurveyItem; +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.NavigableOrderedTask; import org.researchstack.backbone.ui.step.layout.InstructionStepLayout; +import java.util.List; +import java.util.Map; + /** * An InstructionStep object gives the participant instructions for a task. *

    @@ -9,8 +20,70 @@ * introductory content, instructions in the middle of a task, or a final message at the completion * of a task. */ -public class InstructionStep extends Step { - public InstructionStep(String identifier, String title, String detailText) { +public class InstructionStep extends Step implements NavigableOrderedTask.NavigationRule { + /* + * Additional detailed text to display + */ + String moreDetailText; + + /** + Additional text to display for the step in a localized string at the bottom of the view. + + The footnote is displayed in a smaller font below the continue button. It is intended to be used + in order to include disclaimer, copyright, etc. that is important to display in the step but + should not distract from the main purpose of the step. + */ + String footnote; + + /** + An image that provides visual context for the instruction. + + The image is displayed with aspect fit. Depending on the device, the screen area + available for this image can vary. For exact + metrics, see `ORKScreenMetricIllustrationHeight`. + */ + String image; + + /** + * True if this drawable should be loaded using AnimatedVectorDrawableCompat + * false, if this drawable should be loaded like any other image + */ + boolean isImageAnimated; + + /** + * The duration in between animation repeats in milliseconds + */ + long animationRepeatDuration; + + /** + Optional icon image to show above the title and text. + */ + String iconImage; + + /** + * Image scale type + */ + public ImageView.ScaleType scaleType; + + /** + * Pointer to the next step to show after this one. If nil, then the next step + * is determined by the navigation rules setup by NavigableOrderedTask. + */ + String nextStepIdentifier; + + /** + * If not null, the InstructionStepLayout will use this class to add a negative skip action + * that when pressed, will skip this step to another step specified by SubmitBarNegativeActionSkipRule + */ + private SubmitBarNegativeActionSkipRule submitBarSkipRule; + + /* Default constructor needed for serialization/deserialization of object */ + public InstructionStep() { + super(); + } + + public InstructionStep(String identifier, String title, String detailText) + { super(identifier, title); setText(detailText); setOptional(false); @@ -20,4 +93,94 @@ public InstructionStep(String identifier, String title, String detailText) { public Class getStepLayoutClass() { return InstructionStepLayout.class; } + + public void setMoreDetailText(String detailText) { + moreDetailText = detailText; + } + public String getMoreDetailText() { + return moreDetailText; + } + + public void setFootnote(String newFootnote) { + footnote = newFootnote; + } + public String getFootnote() { + return footnote; + } + + public void setIsImageAnimated(boolean isImageAnimated) { + this.isImageAnimated = isImageAnimated; + } + public boolean getIsImageAnimated() { + return isImageAnimated; + } + + public void setImage(String newImage) { + image = newImage; + } + public String getImage() { + return image; + } + + public void setIconImage(String image) { + iconImage = image; + } + public String getIconImage() { + return iconImage; + } + + public void setNextStepIdentifier(String identifier) { + nextStepIdentifier = identifier; + } + public String getNextStepIdentifier() { + return nextStepIdentifier; + } + + public void setAnimationRepeatDuration(long animationRepeatDuration) { + this.animationRepeatDuration = animationRepeatDuration; + } + public long getAnimationRepeatDuration() { + return animationRepeatDuration; + } + + public void setSubmitBarNegativeActionSkipRule(String taskIdentifier, String title, String skipIdentifier) { + submitBarSkipRule = new SubmitBarNegativeActionSkipRule(taskIdentifier, title, skipIdentifier); + } + + public SubmitBarNegativeActionSkipRule getSubmitBarNegativeActionSkipRule() { + return submitBarSkipRule; + } + + @Override + public String nextStepIdentifier(TaskResult result, List additionalTaskResults) { + return nextStepIdentifier; + } + + public static class SubmitBarNegativeActionSkipRule { + + public static final String SKIP_RESULT_KEY = "skip"; + + private String taskIdentifier; + private String title; + private String skipToStepIdentifier; + + public SubmitBarNegativeActionSkipRule(String taskIdentifier, String title, String skipToStepIdentifier) { + this.taskIdentifier = taskIdentifier; + this.title = title; + this.skipToStepIdentifier = skipToStepIdentifier; + } + + public void onNegativeActionClicked(InstructionStep step, StepResult stepResult) { + // Set the next step identifier + step.setNextStepIdentifier(skipToStepIdentifier); + + // add a result to this step view controller to mark that the task was skipped + Map stepResultMap = stepResult.getResults(); + stepResultMap.put(SKIP_RESULT_KEY, taskIdentifier); + } + + public String getTitle() { + return title; + } + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/step/LoginStep.java b/backbone/src/main/java/org/researchstack/backbone/step/LoginStep.java new file mode 100644 index 000000000..613ee020a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/LoginStep.java @@ -0,0 +1,30 @@ +package org.researchstack.backbone.step; + + +import org.researchstack.backbone.model.ProfileInfoOption; +import org.researchstack.backbone.ui.step.layout.LoginStepLayout; + +import java.util.List; + +/** + * Created by TheMDP on 1/4/17. + */ + +public class LoginStep extends ProfileStep { + + /* Default constructor needed for serilization/deserialization of object */ + protected LoginStep() { + super(); + setAutoFocusFirstEditText(true); + } + + public LoginStep(String identifier, String title, String text, List options, List steps) { + super(identifier, title, text, options, steps); + setAutoFocusFirstEditText(true); + } + + @Override + public Class getStepLayoutClass() { + return LoginStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/NavigationExpectedAnswerQuestionStep.java b/backbone/src/main/java/org/researchstack/backbone/step/NavigationExpectedAnswerQuestionStep.java new file mode 100644 index 000000000..4375134ee --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/NavigationExpectedAnswerQuestionStep.java @@ -0,0 +1,82 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.answerformat.AnswerFormat; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.utils.StepHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 12/31/16. + * + * This QuestionStep works by having a specific expected answer that must be correct or incorrect to + * move to the skipToStepIdentifier, with the correct/incorrect status depending on skipIfPassed + */ + +public class NavigationExpectedAnswerQuestionStep extends QuestionStep implements NavigableOrderedTask.NavigationRule { + + private static final String LOG_TAG = NavigationExpectedAnswerQuestionStep.class.getCanonicalName(); + + String skipToStepIdentifier; + boolean skipIfPassed; + + /** Expected answer for this QuestionStep, used by NavigableOrderedTask */ + private Object expectedAnswer; + + /* Default constructor needed for serilization/deserialization of object */ + NavigationExpectedAnswerQuestionStep() { + super(); + } + + public NavigationExpectedAnswerQuestionStep(String identifier) { + super(identifier); + } + + public NavigationExpectedAnswerQuestionStep(String identifier, String title) { + super(identifier, title); + } + + public NavigationExpectedAnswerQuestionStep(String identifier, String title, AnswerFormat format) { + super(identifier, title, format); + } + + public String getSkipToStepIdentifier() { + return skipToStepIdentifier; + } + + public void setSkipToStepIdentifier(String identifier) { + skipToStepIdentifier = identifier; + } + + public boolean getSkipIfPassed() { + return skipIfPassed; + } + + public void setSkipIfPassed(boolean skipIfPassed) { + this.skipIfPassed = skipIfPassed; + } + + /** + * @param expectedAnswer the expected answer for this QuestionStep + */ + public void setExpectedAnswer(Object expectedAnswer) { + this.expectedAnswer = expectedAnswer; + } + + /** + * @return expectedAnswer, which is usually null, but used with NavigableOrderedTask reads steps + */ + public Object getExpectedAnswer() { + return expectedAnswer; + } + + @Override + public String nextStepIdentifier(TaskResult result, List additionalTaskResults) { + List stepList = new ArrayList<>(); + stepList.add(this); + return StepHelper.navigationFormStepSkipIdentifier( + skipToStepIdentifier, skipIfPassed, stepList, result, additionalTaskResults); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/NavigationFormStep.java b/backbone/src/main/java/org/researchstack/backbone/step/NavigationFormStep.java new file mode 100644 index 000000000..3e3babb6d --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/NavigationFormStep.java @@ -0,0 +1,70 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.model.survey.FormSurveyItem; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.utils.StepHelper; + +import java.util.List; + +/** + * Created by TheMDP on 1/3/17. + */ + +public class NavigationFormStep extends FormStep implements NavigableOrderedTask.NavigationRule { + + private static final String LOG_TAG = NavigationFormStep.class.getSimpleName(); + + private String skipToStepIdentifier; + private boolean skipIfPassed; + private Object expectedAnswer; + + /* Default constructor needed for serilization/deserialization of object */ + public NavigationFormStep() { + super(); + } + + public NavigationFormStep(String identifier, String title, String text) { + super(identifier, title, text); + } + + public NavigationFormStep(String identifier, String title, String text, List steps) { + super(identifier, title, text, steps); + } + + public String getSkipToStepIdentifier() { + return skipToStepIdentifier; + } + + public void setSkipToStepIdentifier(String identifier) { + skipToStepIdentifier = identifier; + } + + public boolean getSkipIfPassed() { + return skipIfPassed; + } + + public void setSkipIfPassed(boolean skipIfPassed) { + this.skipIfPassed = skipIfPassed; + } + + @Override + public String nextStepIdentifier(TaskResult result, List additionalTaskResults) { + + // Special case SKIP_BUTTON_TAPPED_ACTION_IDENTIFIER will use the skip button being clicked + // to trigger a skip to a specific step identifier + if (expectedAnswer != null && + expectedAnswer.equals(FormSurveyItem.SKIP_BUTTON_TAPPED_ACTION_IDENTIFIER)) { + if (StepHelper.wasFormStepSkipped(this, result)) { + return skipToStepIdentifier; + } + } + + return StepHelper.navigationFormStepSkipIdentifier( + skipToStepIdentifier, skipIfPassed, formSteps, result, additionalTaskResults); + } + + public void setExpectedAnswer(Object expectedAnswer) { + this.expectedAnswer = expectedAnswer; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/NavigationQuestionStep.java b/backbone/src/main/java/org/researchstack/backbone/step/NavigationQuestionStep.java new file mode 100644 index 000000000..d21a9913d --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/NavigationQuestionStep.java @@ -0,0 +1,55 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.answerformat.AnswerFormat; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.utils.StepResultHelper; + +import java.util.List; + +/** + * Created by TheMDP on 2/5/17. + * + * The NavigationQuestionStep class allows the developer to implement any custom navigation rule + * by allowing them to pass in the interface themselves for determining the nextStepIdentifier + */ + +public class NavigationQuestionStep extends QuestionStep implements NavigableOrderedTask.NavigationRule { + + private List customRules; + + /* Default constructor needed for serilization/deserialization of object */ + NavigationQuestionStep() { + super(); + } + + public NavigationQuestionStep(String identifier) { + super(identifier); + } + + public NavigationQuestionStep(String identifier, String title) { + super(identifier, title); + } + + public NavigationQuestionStep(String identifier, String title, AnswerFormat format) { + super(identifier, title, format); + } + + public void setCustomRules(List customRules) { + this.customRules = customRules; + } + + @Override + public String nextStepIdentifier(TaskResult result, List additionalTaskResults) { + if (customRules != null) { + for (NavigableOrderedTask.ObjectEqualsNavigationRule rule : customRules) { + String nextIdentifier = rule.nextStepIdentifier(result, additionalTaskResults); + if (nextIdentifier != null) { + return nextIdentifier; + } + } + } + return null; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/NavigationSubtaskStep.java b/backbone/src/main/java/org/researchstack/backbone/step/NavigationSubtaskStep.java new file mode 100644 index 000000000..a90b56236 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/NavigationSubtaskStep.java @@ -0,0 +1,69 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.task.Task; +import org.researchstack.backbone.utils.StepHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 1/3/17. + */ + +public class NavigationSubtaskStep extends SubtaskStep implements NavigableOrderedTask.NavigationRule { + + String skipToStepIdentifier; + boolean skipIfPassed; + + /* Default constructor needed for serilization/deserialization of object */ + NavigationSubtaskStep() { + super(); + } + + public NavigationSubtaskStep(String identifier) { + super(identifier); + } + + public NavigationSubtaskStep(String identifier, String title) { + super(identifier, title); + } + + public NavigationSubtaskStep(String identifier, List steps) { + super(identifier, steps); + } + + public NavigationSubtaskStep(Task task) { + super(task); + } + + public String getSkipToStepIdentifier() { + return skipToStepIdentifier; + } + + public void setSkipToStepIdentifier(String identifier) { + skipToStepIdentifier = identifier; + } + + public boolean getSkipIfPassed() { + return skipIfPassed; + } + + public void setSkipIfPassed(boolean skipIfPassed) { + this.skipIfPassed = skipIfPassed; + } + + @Override + public String nextStepIdentifier(TaskResult result, List additionalTaskResults) { + List stepList = new ArrayList<>(); + for (String id : result.getResults().keySet()) { + Step step = getStepWithIdentifier(id); + if (step != null && step instanceof QuestionStep) { + stepList.add((QuestionStep) step); + } + } + return StepHelper.navigationFormStepSkipIdentifier( + skipToStepIdentifier, skipIfPassed, stepList, result, additionalTaskResults); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/OnboardingCompletionStep.java b/backbone/src/main/java/org/researchstack/backbone/step/OnboardingCompletionStep.java new file mode 100644 index 000000000..1b1b00e33 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/OnboardingCompletionStep.java @@ -0,0 +1,25 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.ui.step.layout.OnboardingCompletionStepLayout; + +/** + * Created by TheMDP on 1/18/17. + */ + +public class OnboardingCompletionStep extends InstructionStep { + + /* Default constructor needed for serilization/deserialization of object */ + OnboardingCompletionStep() { + super(); + } + + public OnboardingCompletionStep(String identifier, String title, String detailText) + { + super(identifier, title, detailText); + setOptional(false); + } + + public Class getStepLayoutClass() { + return OnboardingCompletionStepLayout.class; + } +} diff --git a/skin/src/main/java/org/researchstack/skin/step/PassCodeCreationStep.java b/backbone/src/main/java/org/researchstack/backbone/step/PassCodeCreationStep.java similarity index 76% rename from skin/src/main/java/org/researchstack/skin/step/PassCodeCreationStep.java rename to backbone/src/main/java/org/researchstack/backbone/step/PassCodeCreationStep.java index 0e819774e..af8539255 100644 --- a/skin/src/main/java/org/researchstack/skin/step/PassCodeCreationStep.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/PassCodeCreationStep.java @@ -1,7 +1,6 @@ -package org.researchstack.skin.step; +package org.researchstack.backbone.step; -import org.researchstack.backbone.step.Step; -import org.researchstack.skin.ui.layout.SignUpPinCodeCreationStepLayout; +import org.researchstack.backbone.ui.step.layout.SignUpPinCodeCreationStepLayout; public class PassCodeCreationStep extends Step { diff --git a/backbone/src/main/java/org/researchstack/backbone/step/PasscodeStep.java b/backbone/src/main/java/org/researchstack/backbone/step/PasscodeStep.java new file mode 100644 index 000000000..f93a7b41a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/PasscodeStep.java @@ -0,0 +1,60 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.ui.step.layout.FingerprintStepLayout; +import org.researchstack.backbone.ui.step.layout.PasscodeCreationStepLayout; +import org.researchstack.backbone.ui.step.layout.SignUpPinCodeCreationStepLayout; + +/** + * Created by TheMDP on 1/4/17. + */ + +public class PasscodeStep extends InstructionStep { + + private boolean useFingerprint = false; + + public int stateOrdinal = - 1; + + /* Default constructor needed for serilization/deserialization of object */ + PasscodeStep() { + super(); + } + + public PasscodeStep(String identifier, String title, String text) { + super(identifier, title, text); + } + + public int getStateOrdinal() + { + return stateOrdinal; + } + + public void setStateOrdinal(int stateOrdinal) + { + this.stateOrdinal = stateOrdinal; + } + + /** + * @return true if UI will be FingerprintStepLayout, false if UI will be PasscodeCreationStepLayout + */ + public boolean getUseFingerprint() + { + return useFingerprint; + } + + /** + * @param useFingerprint true if UI will be FingerprintStepLayout, false if UI will be PasscodeCreationStepLayout + */ + public void setUseFingerprint(boolean useFingerprint) + { + this.useFingerprint = useFingerprint; + } + + @Override + public Class getStepLayoutClass() { + if (!useFingerprint) { + return PasscodeCreationStepLayout.class; + } else { + return FingerprintStepLayout.class; + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/PermissionsStep.java b/backbone/src/main/java/org/researchstack/backbone/step/PermissionsStep.java new file mode 100644 index 000000000..a3cc6f984 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/PermissionsStep.java @@ -0,0 +1,36 @@ +package org.researchstack.backbone.step; + +import android.os.Build; + +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.ui.step.layout.PermissionStepLayout; + +import java.util.List; + +/** + * Created by TheMDP on 1/4/17. + */ + +public class PermissionsStep extends Step implements NavigableOrderedTask.NavigationSkipRule { + + /* Default constructor needed for serilization/deserialization of object */ + PermissionsStep() { + super(); + } + + public PermissionsStep(String identifier, String title, String text) { + super(identifier, title); + setText(text); + } + + @Override + public Class getStepLayoutClass() { + return PermissionStepLayout.class; + } + + @Override + public boolean shouldSkipStep(TaskResult result, List additionalTaskResults) { + return android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/ProfileStep.java b/backbone/src/main/java/org/researchstack/backbone/step/ProfileStep.java new file mode 100644 index 000000000..4e457dcbd --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/ProfileStep.java @@ -0,0 +1,76 @@ +package org.researchstack.backbone.step; + +import android.content.Context; +import androidx.annotation.MainThread; + +import org.researchstack.backbone.model.ProfileInfoOption; +import org.researchstack.backbone.model.survey.factory.SurveyFactory; +import org.researchstack.backbone.ui.step.layout.ProfileStepLayout; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 1/4/17. + * + * Registration step will collect a user's information that is needed to sign up with the server + * This usually includes name, email, password, etc + */ + +public class ProfileStep extends FormStep { + + private List profileInfoOptions = new ArrayList<>(); + public void setProfileInfoOptions(List options) { + profileInfoOptions = options; + } + public List getProfileInfoOptions() { + return profileInfoOptions; + } + + /* Default constructor needed for serilization/deserialization of object */ + protected ProfileStep() { + super(); + } + + /** + * @param identifier ProfileStep identifier + * @param title ProfileStep title + * @param text ProfileStep text + * @param options ProfileInfoOption list that must match up with steps param + * @param steps QuestionStep list that must match up with options param + */ + public ProfileStep( + String identifier, String title, String text, + List options, + List steps) + { + super(identifier, title, text, steps); + profileInfoOptions = options; + } + + /** + * @param context used by the surveyFactory to create QuestionSteps from ProfileInfoOptions + * @param surveyFactory if null, the default one will be used + * @param identifier ProfileStep identifier + * @param title ProfileStep title + * @param text ProfileStep text + * @param options ProfileInfoOption list to convert into a QuestionStep list for this FormStep subclass + * @param alsoAddConfirmPasswordOption if true, a confirm password step will be made with password step + */ + public ProfileStep( + Context context, + SurveyFactory surveyFactory, + String identifier, String title, String text, + List options, + boolean alsoAddConfirmPasswordOption) + { + this(identifier, title, text, options, + (surveyFactory == null ? new SurveyFactory() : surveyFactory) + .createQuestionSteps(context, options, alsoAddConfirmPasswordOption)); + } + + @Override + public Class getStepLayoutClass() { + return ProfileStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/QuestionStep.java b/backbone/src/main/java/org/researchstack/backbone/step/QuestionStep.java index 014541465..64420f938 100644 --- a/backbone/src/main/java/org/researchstack/backbone/step/QuestionStep.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/QuestionStep.java @@ -1,8 +1,14 @@ package org.researchstack.backbone.step; import org.researchstack.backbone.answerformat.AnswerFormat; +import org.researchstack.backbone.ui.step.body.StepBody; import org.researchstack.backbone.ui.step.layout.SurveyStepLayout; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** * The {@link QuestionStep} class is a concrete subclass of {@link Step} that represents a step in * which a single question is presented to the user. @@ -25,6 +31,11 @@ public class QuestionStep extends Step { private String placeholder; + /* Default constructor needed for serilization/deserialization of object */ + public QuestionStep() { + super(); + } + /** * Returns a new question step that includes the specified identifier. * @@ -87,7 +98,7 @@ public Class getStepLayoutClass() { * * @return the StepBody implementation for this question step. */ - public Class getStepBodyClass() { + public Class getStepBodyClass() { return answerFormat.getQuestionType().getStepBodyClass(); } diff --git a/backbone/src/main/java/org/researchstack/backbone/step/RegistrationStep.java b/backbone/src/main/java/org/researchstack/backbone/step/RegistrationStep.java new file mode 100644 index 000000000..2dc120a06 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/RegistrationStep.java @@ -0,0 +1,69 @@ +package org.researchstack.backbone.step; + +import android.content.Context; + +import org.researchstack.backbone.model.ProfileInfoOption; +import org.researchstack.backbone.model.survey.factory.SurveyFactory; +import org.researchstack.backbone.ui.step.layout.RegistrationStepLayout; + +import java.util.Arrays; +import java.util.List; + +/** + * Created by TheMDP on 1/4/17. + */ + +public class RegistrationStep extends ProfileStep { + + /* Default constructor needed for serilization/deserialization of object */ + RegistrationStep() { + super(); + } + + public RegistrationStep(String identifier, String title, String text, List options, List steps) { + super(identifier, title, text, options, steps); + } + + /** + * @param context used by the surveyFactory to create QuestionSteps from ProfileInfoOptions + * @param surveyFactory if null, the default one will be used + * @param identifier ProfileStep identifier + * @param title ProfileStep title + * @param text ProfileStep text + */ + public RegistrationStep( + Context context, + SurveyFactory surveyFactory, + String identifier, + String title, + String text) + { + this(context, surveyFactory, identifier, title, text, Arrays.asList( + ProfileInfoOption.EMAIL, + ProfileInfoOption.PASSWORD), true); // also create a Confirm Password option after Password + } + + /** + * @param context used by the surveyFactory to create QuestionSteps from ProfileInfoOptions + * @param surveyFactory if null, the default one will be used + * @param identifier ProfileStep identifier + * @param title ProfileStep title + * @param text ProfileStep text + * @param options ProfileInfoOption list to convert into a QuestionStep list for this FormStep subclass + * @param alsoAddConfirmPasswordOption if true, a confirm password step will be made with password step + */ + public RegistrationStep( + Context context, + SurveyFactory surveyFactory, + String identifier, String title, String text, + List options, + boolean alsoAddConfirmPasswordOption) + { + super (context, surveyFactory, identifier, title, text, options, alsoAddConfirmPasswordOption); + } + + @Override + public Class getStepLayoutClass() { + return RegistrationStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/RequireSystemFeatureStep.java b/backbone/src/main/java/org/researchstack/backbone/step/RequireSystemFeatureStep.java new file mode 100644 index 000000000..6ca83f7ca --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/RequireSystemFeatureStep.java @@ -0,0 +1,45 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.ui.step.layout.RequireSystemFeatureStepLayout; + +/** + * Created by TheMDP on 2/21/17. + * + * This step forces the user to turn on a system feature before proceeding in the task + * for now it is just GPS, but this could be expanded to cover Wifi, Cellular, NFC, etc + */ + +public class RequireSystemFeatureStep extends InstructionStep { + + private SystemFeature systemFeature; + + /* Default constructor needed for serilization/deserialization of object */ + RequireSystemFeatureStep() { + super(); + } + + public RequireSystemFeatureStep(SystemFeature systemFeature, String identifier, String title, String detailText) { + super(identifier, title, detailText); + this.systemFeature = systemFeature; + } + + public SystemFeature getSystemFeature() { + return systemFeature; + } + + public void setSystemFeature(SystemFeature systemFeature) { + this.systemFeature = systemFeature; + } + + /** + * System hardware feature that must be enabled + */ + public enum SystemFeature { + GPS + } + + @Override + public Class getStepLayoutClass() { + return RequireSystemFeatureStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/ShareTheAppStep.java b/backbone/src/main/java/org/researchstack/backbone/step/ShareTheAppStep.java new file mode 100644 index 000000000..af6b51892 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/ShareTheAppStep.java @@ -0,0 +1,89 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.ui.step.layout.ShareTheAppStepLayout; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Created by TheMDP on 1/26/17. + */ + +public class ShareTheAppStep extends InstructionStep { + + private static final String TWITTER_ID = "twitter"; + private static final String FACEBOOK_ID = "facebook"; + private static final String EMAIL_ID = "email"; + private static final String SMS_ID = "sms"; + + private List shareTypeList = new ArrayList<>(); + + /* Default constructor needed for serilization/deserialization of object */ + ShareTheAppStep() { + super(); + } + + /** + * Default share type list will be made from all the enum values + * @param identifier of step + * @param title of step + * @param text of step + */ + public ShareTheAppStep(String identifier, String title, String text) { + super(identifier, title, text); + shareTypeList = Arrays.asList(ShareType.values()); + } + + /** + * @param identifier of step + * @param title of step + * @param text of step + * @param shareTypeList list of share type to be included in step layout + */ + public ShareTheAppStep(String identifier, String title, String text, List shareTypeList) { + super(identifier, title, text); + this.shareTypeList = shareTypeList; + } + + public List getShareTypeList() { + return shareTypeList; + } + + public void setShareTypeList(List shareTypeList) { + this.shareTypeList = shareTypeList; + } + + @Override + public Class getStepLayoutClass() { + return ShareTheAppStepLayout.class; + } + + public enum ShareType { + + TWITTER (TWITTER_ID), + FACEBOOK(FACEBOOK_ID), + EMAIL (EMAIL_ID), + SMS (SMS_ID); + + private String identifier; + + ShareType(String identifier) { + this.identifier = identifier; + } + + public static List toShareTypeList(List idList) { + List shareTypeList = new ArrayList<>(); + if (idList != null) { + for (String id : idList) { + for (ShareType shareType : ShareType.values()) { + if (id.equals(shareType.identifier)) { + shareTypeList.add(shareType); + } + } + } + } + return shareTypeList; + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/Step.java b/backbone/src/main/java/org/researchstack/backbone/step/Step.java index af059ab52..1bc9728ab 100644 --- a/backbone/src/main/java/org/researchstack/backbone/step/Step.java +++ b/backbone/src/main/java/org/researchstack/backbone/step/Step.java @@ -1,6 +1,7 @@ package org.researchstack.backbone.step; import org.researchstack.backbone.task.Task; +import org.researchstack.backbone.utils.ObjectUtils; import java.io.Serializable; @@ -42,6 +43,11 @@ public class Step implements Serializable { private boolean allowsBackNavigation; private boolean useSurveyMode; + /* Default constructor needed for serilization/deserialization of object */ + public Step() { + super(); + } + /** * Returns a new step initialized with the specified identifier. * @@ -72,6 +78,8 @@ public Step(String identifier, String title) { *

    * In some cases, it can be useful to link the step identifier to a unique identifier in a * database; in other cases, it can make sense to make the identifier human readable. + * + * @return a short string that uniquely identifies the step */ public String getIdentifier() { return identifier; @@ -95,7 +103,7 @@ public boolean isOptional() { /** * Sets whether the step is skippable * - * @param optional + * @param optional a boolean indicating whether the step is skippable * @see #isOptional() */ public void setOptional(boolean optional) { @@ -131,7 +139,7 @@ public void setTitle(String title) { * display a long question, it can work well to keep the title short and put the additional * content in the text property. * - * @return + * @return the additional text to display */ public String getText() { return text; @@ -191,4 +199,40 @@ public Class getStepLayoutClass() { public void setStepLayoutClass(Class stepLayoutClass) { this.stepLayoutClass = stepLayoutClass; } + + /** + * @param newIdentifier to use instead of cloned step's identifier + * @return cloned step using Gson but with different identifier + */ + public Step deepCopy(String newIdentifier) { + Step clonedStep = (Step)ObjectUtils.clone(this); + clonedStep.identifier = newIdentifier; + return clonedStep; + } + + @Override + public int hashCode() { + if (identifier == null) { + return super.hashCode(); + } + return identifier.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Step)) { + return false; + } + if (obj == this) { + return true; + } + + Step rhs = (Step) obj; + + if (identifier == null || rhs.identifier == null) { + return false; + } + + return identifier.equals(rhs.identifier); + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/step/SubstepListStep.java b/backbone/src/main/java/org/researchstack/backbone/step/SubstepListStep.java new file mode 100644 index 000000000..b5daca78f --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/SubstepListStep.java @@ -0,0 +1,32 @@ +package org.researchstack.backbone.step; + +import org.researchstack.backbone.ui.step.layout.ViewPagerSubstepListStepLayout; + +import java.util.List; + +/** + * Created by TheMDP on 1/16/17. + */ + +public class SubstepListStep extends Step { + List stepList; + + /* Default constructor needed for serilization/deserialization of object */ + protected SubstepListStep() { + super(); + } + + public SubstepListStep(String identifier, List stepList) { + super(identifier); + this.stepList = stepList; + } + + public List getStepList() { + return stepList; + } + + @Override + public Class getStepLayoutClass() { + return ViewPagerSubstepListStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/SubtaskStep.java b/backbone/src/main/java/org/researchstack/backbone/step/SubtaskStep.java new file mode 100644 index 000000000..05d99ebea --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/SubtaskStep.java @@ -0,0 +1,171 @@ +package org.researchstack.backbone.step; + +import android.util.Log; + +import org.researchstack.backbone.result.Result; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.task.Task; +import org.researchstack.backbone.ui.step.layout.ViewPagerSubstepListStepLayout; +import org.researchstack.backbone.utils.ObjectUtils; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by TheMDP on 12/29/16. + */ + +public class SubtaskStep extends Step { + + private static final String LOG_TAG = SubtaskStep.class.getCanonicalName(); + + Task subtask; + public Task getSubtask() { + return subtask; + } + + /* Default constructor needed for serilization/deserialization of object */ + SubtaskStep() { + super(); + } + + public SubtaskStep(String identifier) { + super(identifier); + } + + public SubtaskStep(String identifier, String title) { + super(identifier, title); + } + + public SubtaskStep(String identifier, List steps) { + this(identifier); + subtask = new NavigableOrderedTask(identifier, steps); + } + + public SubtaskStep(Task task) { + this(task.getIdentifier()); + subtask = task; + } + + /** + * @param identifier full identifier, i.e. "consent.step1" + * @return identifier with subtask identifier stripped out, i.e. "step1" + */ + protected String substepIdentifier(String identifier) { + if (subtask == null) { + Log.e(LOG_TAG, "Subtask is null subtask step"); + return null; + } + + if (identifier == null || subtask.getIdentifier() == null) { + Log.e(LOG_TAG, "Identifier or subtask identifier is null in subtask step"); + return null; + } + + // Add a period to the end of the substep + String baseIdPrefix = subtask.getIdentifier() + "."; + int startIndex = identifier.indexOf(baseIdPrefix); + + if (startIndex < 0) { + return null; + } + + String substepId = identifier.substring(startIndex + baseIdPrefix.length()); + return substepId; + } + + private Step replacementStep(Step step) { + if (step == null) { + //Log.e(LOG_TAG, "Step is null in subtask step method"); + return null; + } + String replacementIdentifier = subtask.getIdentifier() + "." + step.getIdentifier(); + Step replacementStep = step.deepCopy(replacementIdentifier); + return replacementStep; + } + + protected TaskResult filteredTaskResult(TaskResult inputResult) { + // create a mutated copy of the results that includes only the subtask results + TaskResult subtaskResult = (TaskResult) ObjectUtils.clone(inputResult); + Map stepResults = subtaskResult.getResults(); + if (stepResults != null && !stepResults.keySet().isEmpty()) { + Map subtaskResults = filteredStepResults(stepResults); + subtaskResult.setResults(subtaskResults); + } + return subtaskResult; + } + + private Map filteredStepResults(Map inputResults) { + Map subtaskResults = new LinkedHashMap<>(); + String prefix = subtask.getIdentifier() + "."; + for (String identifier : inputResults.keySet()) { + if (identifier.startsWith(prefix)) { + + Map newResultMap = new LinkedHashMap<>(); + String newIdentifier = identifier.substring(prefix.length()); + if (inputResults.get(identifier) != null) { + StepResult stepResult = (StepResult) inputResults.get(identifier).deepCopy(newIdentifier); + + // Search results of the step for non-subtask identifiers as well + if (stepResult.getResults() != null) { + for (Object stepResultIdentifierObj : stepResult.getResults().keySet()) { + if (stepResultIdentifierObj instanceof String) { + String stepResultIdentifier = (String) stepResultIdentifierObj; + Object stepResultObject = stepResult.getResults().get(stepResultIdentifierObj); + if (stepResultObject instanceof Result) { + Result newResult = ((Result) stepResultObject).deepCopy(stepResultIdentifier); + newResultMap.put(stepResultIdentifier, newResult); + } else { + newResultMap.put(stepResultIdentifier, stepResultObject); + } + } + } + stepResult.setResults(newResultMap); + } + + subtaskResults.put(newIdentifier, stepResult); + } + } + } + return subtaskResults; + } + + public Step getStepWithIdentifier(String identifier) { + String substepIdentifier = substepIdentifier(identifier); + if (substepIdentifier == null) { + return null; + } + Step step = subtask.getStepWithIdentifier(substepIdentifier); + if (step == null) { + return null; + } + return replacementStep(step); + } + + public Step getStepAfterStep(Step step, TaskResult result) { + if (step == null) { + return replacementStep(subtask.getStepAfterStep(null, result)); + } + String substepIdentifier = substepIdentifier(step.getIdentifier()); + if (substepIdentifier == null) { + return null; + } + + Step substep = step.deepCopy(substepIdentifier); + TaskResult replacementTaskResult = filteredTaskResult(result); + Step nextStep = subtask.getStepAfterStep(substep, replacementTaskResult); + + // If the task result was mutated, need to add any changes back into the result set + StepResult thisStepResult = replacementTaskResult.getStepResult(substepIdentifier); + if (thisStepResult != null && result != null) { + StepResult parentStepResult = result.getStepResult(step.getIdentifier()); + parentStepResult.setResults(thisStepResult.getResults()); + } + + // And finally return the replacement step + return replacementStep(nextStep); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/ActiveStep.java b/backbone/src/main/java/org/researchstack/backbone/step/active/ActiveStep.java new file mode 100644 index 000000000..d4016c352 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/ActiveStep.java @@ -0,0 +1,409 @@ +package org.researchstack.backbone.step.active; + +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.recorder.RecorderConfig; +import org.researchstack.backbone.ui.ActiveTaskActivity; +import org.researchstack.backbone.ui.step.layout.ActiveStepLayout; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Created by TheMDP on 2/4/17. + * + * The `ORKActiveStep` class is the base class for steps in active tasks, which + * are steps that collect sensor data in a semi-controlled environment, as opposed + * to the more subjective data collected when users fill in surveys. + * + * In addition to the behaviors of `Step`, active steps have the concept of + * life cycle, which includes a defined start and finish. + * + * The ResearchStack library provides built-in behaviors that allow active steps + * to play voice prompts, speak a count down, and have a defined duration. + * + * To present an active step in your app, it's likely that you will subclass `ActiveStep` and + * `ActiveStepLayout` to present custom UI and custom + * prompts. For example subclasses, see `SpatialSpanMemoryStep` or `FitnessStep`. + * + * Active steps may also need `StepResult` subclasses to record their results + * if these don't come purely from recorders. + * + * If you develop a new active step subclass, consider contributing your + * code to the ResearchStack project so that it's available for others to use in + * their studies. + */ + +public class ActiveStep extends Step { + + /** + * The duration of the step in seconds. + * + * If the step duration is greater than zero, a built-in timer starts when the + * step starts. If `shouldStartTimerAutomatically` is set, the timer + * starts when the step's view appears. When the timer expires, a sound or + * vibration may be played. If `shouldContinueOnFinish` is set, the step + * automatically navigates forward when the timer expires. + * + * The default value of this property is `0`, which disables the built-in timer. + */ + private int stepDuration = 0; + + /** + * A Boolean value indicating whether to show a view with a default timer. + * + * The default timer UI is not used in any of the current predefined tasks, + * but it can be displayed in a simple active task that does not require custom + * UI and needs only a count down timer on screen during data collection. + * + * Note that this property is ignored if `stepDuration` is `0`. + * + * The default value of this property is true. + */ + private boolean shouldShowDefaultTimer = true; + + /** + * A Boolean value indicating whether to speak the last few seconds in the count down of the + * duration of a timed step. + * + * When the value of this property is `true`, `TextToSpeech` is used to synthesize the countdown. + * Note that this member variable is ignored if VoiceOver is enabled. + * + * The default value of this property is `false`. + */ + private boolean shouldSpeakCountDown = false; + + /** + * A Boolean value indicating whether to speak the halfway point in the count down of the + * duration of a timed step. + * + * When the value of this property is `true`, `TextToSpeech` is used to synthesize the countdown. + * Note that this member variable is ignored if VoiceOver is enabled. + * + * The default value of this property is `false`. + */ + private boolean shouldSpeakRemainingTimeAtHalfway = false; + + /** + * A Boolean value indicating whether to start the count down timer automatically when the step starts, or + * require the user to take some explicit action to start the step, such as tapping a button. + * + * Usually the explicit action needs to come from custom UI in an `ActiveStepLayout` subclass. + * + * The default value of this property is `false`. + */ + private boolean shouldStartTimerAutomatically = false; + + /** + * A Boolean value indicating whether to play a default sound when the step starts. + * + * The default value of this property is `false`. + */ + private boolean shouldPlaySoundOnStart = false; + + /** + * A Boolean value indicating whether to play a default sound when the step finishes. + * + * The default value of this property is `false`. + */ + private boolean shouldPlaySoundOnFinish = false; + + /** + * A Boolean value indicating whether to vibrate when the step starts. + * + * The default value of this property is `false`. + */ + private boolean shouldVibrateOnStart = false; + + /** + * A Boolean value indicating whether to vibrate when the step finishes. + * + * The default value of this property is `false`. + */ + private boolean shouldVibrateOnFinish = false; + + /** + * A Boolean value indicating whether the Next button should double as a skip action before + * the step finishes. + * + * When the value of this property is `true`, the ResearchStack library hides the skip button and + * makes the Next button function as a skip button when the step has not yet finished. + * + * The default value of this property is `false`. + */ + private boolean shouldUseNextAsSkipButton = false; + + /** + * A Boolean value indicating whether to transition automatically when the step finishes. + * + * When the value of this property is `true`, the active step layout automatically performs the + * continue action when the `[ActiveStepLayout finish]` method + * is called. + * + * The default value of this property is `false`. + */ + private boolean shouldContinueOnFinish = false; + + /** + * Localized text that represents an instructional voice prompt. + * + * Instructional speech begins when the step starts. If VoiceOver is active, + * the instruction is spoken by VoiceOver. + */ + private String spokenInstruction; + + /** + * Localized text that represents an instructional voice prompt for when the step finishes. + * + * Instructional speech begins when the step finishes. If VoiceOver is active, + * the instruction is spoken by VoiceOver. + */ + private String finishedSpokenInstruction; + + /** + * An array of recorder configurations that define the parameters for recorders to be + * run during a step to collect sensor or other data. + * + * If you want to collect data from sensors while the step is in progress, + * add one or more recorder configurations to the array. The active step view + * controller instantiates recorders and collates their results as children + * of the step result. + * + * The set of recorder configurations is scanned when populating the + * `requestedHealthKitTypesForReading` and `requestedPermissions` properties. + * + * See also: `ORKRecorderConfiguration` and `ORKRecorder`. + */ + private List recorderConfigurationList; + + /** + * An image to be displayed below the instructions for the step. + * + * It will be loaded from resources with the value of this variable + */ + private String imageResName; + + /** + * A map of <"time_in_seconds_to_speak", "what_to_speak"> + */ + private Map spokenInstructionMap; + + /** + * A string representation of a raw file resource + */ + private String soundRes; + + /** + * The recording UUID is a unique identifier used by the RecorderService + */ + private UUID recordingUuid; + + /** + * This can be increased to allow the ending spoken instruction to + * not get cut off if it is too long + */ + private int estimateTimeInMsToSpeakEndInstruction = 1000; // 1 second delay after finishing + + /** + * The class of the activity that will run this step + */ + private Class activityClazz = ActiveTaskActivity.class; + + /* Default constructor needed for serialization/deserialization of object */ + ActiveStep() { + super(); + } + + public ActiveStep(String identifier) { + super(identifier); + setOptional(false); + recordingUuid = UUID.randomUUID(); + } + + public ActiveStep(String identifier, String title, String detailText) { + super(identifier, title); + setText(detailText); + setOptional(false); + recordingUuid = UUID.randomUUID(); + } + + @Override + public Class getStepLayoutClass() { + return ActiveStepLayout.class; + } + + public void setStepDuration(int stepDuration) { + this.stepDuration = stepDuration; + } + + public int getStepDuration() { + return stepDuration; + } + + public void setShouldShowDefaultTimer(boolean shouldShowDefaultTimer) { + this.shouldShowDefaultTimer = shouldShowDefaultTimer; + } + + public boolean getShouldShowDefaultTimer() { + return shouldShowDefaultTimer; + } + + public void setShouldSpeakCountDown(boolean shouldSpeakCountDown) { + this.shouldSpeakCountDown = shouldSpeakCountDown; + } + + public boolean getShouldSpeakCountDown() { + return shouldSpeakCountDown; + } + + public void setShouldSpeakRemainingTimeAtHalfway(boolean shouldSpeakRemainingTimeAtHalfway) { + this.shouldSpeakRemainingTimeAtHalfway = shouldSpeakRemainingTimeAtHalfway; + } + + public boolean getShouldSpeakRemainingTimeAtHalfway() { + return shouldSpeakRemainingTimeAtHalfway; + } + + public void setShouldStartTimerAutomatically(boolean shouldStartTimerAutomatically) { + this.shouldStartTimerAutomatically = shouldStartTimerAutomatically; + } + + public boolean getShouldStartTimerAutomatically() { + return shouldStartTimerAutomatically; + } + + public boolean getShouldPlaySoundOnStart() { + return shouldPlaySoundOnStart; + } + + public void setShouldPlaySoundOnStart(boolean shouldPlaySoundOnStart) { + this.shouldPlaySoundOnStart = shouldPlaySoundOnStart; + } + + public boolean getShouldPlaySoundOnFinish() { + return shouldPlaySoundOnFinish; + } + + public void setShouldPlaySoundOnFinish(boolean shouldPlaySoundOnFinish) { + this.shouldPlaySoundOnFinish = shouldPlaySoundOnFinish; + } + + public boolean getShouldVibrateOnStart() { + return shouldVibrateOnStart; + } + + public void setShouldVibrateOnStart(boolean shouldVibrateOnStart) { + this.shouldVibrateOnStart = shouldVibrateOnStart; + } + + public boolean getShouldVibrateOnFinish() { + return shouldVibrateOnFinish; + } + + public void setShouldVibrateOnFinish(boolean shouldVibrateOnFinish) { + this.shouldVibrateOnFinish = shouldVibrateOnFinish; + } + + public boolean getShouldUseNextAsSkipButton() { + return shouldUseNextAsSkipButton; + } + + public void setShouldUseNextAsSkipButton(boolean shouldUseNextAsSkipButton) { + this.shouldUseNextAsSkipButton = shouldUseNextAsSkipButton; + } + + public boolean getShouldContinueOnFinish() { + return shouldContinueOnFinish; + } + + public void setShouldContinueOnFinish(boolean shouldContinueOnFinish) { + this.shouldContinueOnFinish = shouldContinueOnFinish; + } + + public String getSpokenInstruction() { + return spokenInstruction; + } + + public void setSpokenInstruction(String spokenInstruction) { + this.spokenInstruction = spokenInstruction; + } + + public String getFinishedSpokenInstruction() { + return finishedSpokenInstruction; + } + + public void setFinishedSpokenInstruction(String finishedSpokenInstruction) { + this.finishedSpokenInstruction = finishedSpokenInstruction; + } + + public boolean startsFinished() { + return stepDuration == 0; + } + + public boolean hasCountDown() { + return (stepDuration > 0) && shouldShowDefaultTimer; + } + + public boolean hasVoice() { + boolean hasSpokenInstruction = spokenInstruction != null && !spokenInstruction.isEmpty(); + boolean hasFinishedSpokenInstruction = finishedSpokenInstruction != null && !finishedSpokenInstruction.isEmpty(); + boolean hasValidSpeakingMap = spokenInstructionMap != null && !spokenInstructionMap.isEmpty(); + return (hasSpokenInstruction || hasFinishedSpokenInstruction || hasValidSpeakingMap); + } + + public List getRecorderConfigurationList() { + return recorderConfigurationList; + } + + public void setRecorderConfigurationList(List recorderConfigurationList) { + this.recorderConfigurationList = recorderConfigurationList; + } + + public String getImageResName() { + return imageResName; + } + + public void setImageResName(String imageResName) { + this.imageResName = imageResName; + } + + public Map getSpokenInstructionMap() { + return spokenInstructionMap; + } + + public void setSpokenInstructionMap(Map spokenInstructions) { + spokenInstructionMap = spokenInstructions; + } + + public int getEstimateTimeInMsToSpeakEndInstruction() { + return estimateTimeInMsToSpeakEndInstruction; + } + + public void setEstimateTimeInMsToSpeakEndInstruction(int estimateTimeInMsToSpeakEndInstruction) { + this.estimateTimeInMsToSpeakEndInstruction = estimateTimeInMsToSpeakEndInstruction; + } + + public UUID getRecordingUuid() { + return recordingUuid; + } + + public void setRecordingUuid(UUID recordingUuid) { + this.recordingUuid = recordingUuid; + } + + public Class getActivityClazz() { + return activityClazz; + } + + public void setActivityClazz(Class activityClazz) { + this.activityClazz = activityClazz; + } + + public String getSoundRes() { + return soundRes; + } + + public void setSoundRes(String soundRes) { + this.soundRes = soundRes; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/ActiveTaskAndResultListener.java b/backbone/src/main/java/org/researchstack/backbone/step/active/ActiveTaskAndResultListener.java new file mode 100644 index 000000000..587be2eb1 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/ActiveTaskAndResultListener.java @@ -0,0 +1,30 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.researchstack.backbone.step.active; + +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.Task; + +/** + * Created by TheMDP on 1/12/18. + */ + +public interface ActiveTaskAndResultListener { + Task activeTaskActivityGetTask(); + TaskResult activeTaskActivityResult(); +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/AudioStep.java b/backbone/src/main/java/org/researchstack/backbone/step/active/AudioStep.java new file mode 100644 index 000000000..3f6e8846d --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/AudioStep.java @@ -0,0 +1,35 @@ +package org.researchstack.backbone.step.active; + +import org.researchstack.backbone.ui.step.layout.AudioStepLayout; + +/** + * Created by TheMDP on 2/27/17. + */ + +public class AudioStep extends ActiveStep { + + /* Default constructor needed for serialization/deserialization of object */ + AudioStep() { + super(); + } + + public AudioStep(String identifier) { + super(identifier); + commonInit(); + } + + public AudioStep(String identifier, String title, String detailText) { + super(identifier, title, detailText); + commonInit(); + } + + protected void commonInit() { + setShouldShowDefaultTimer(false); + setShouldStartTimerAutomatically(true); + } + + @Override + public Class getStepLayoutClass() { + return AudioStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/CountdownStep.java b/backbone/src/main/java/org/researchstack/backbone/step/active/CountdownStep.java new file mode 100644 index 000000000..ea3555f2c --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/CountdownStep.java @@ -0,0 +1,39 @@ +package org.researchstack.backbone.step.active; + +import org.researchstack.backbone.ui.step.layout.CountdownStepLayout; + +/** + * Created by TheMDP on 2/4/17. + * + * The `CountdownStep` class represents a step that displays a label and a + * countdown for a time equal to its duration. + * + * To use the countdown step, set the `duration` property, incorporate it into a + * task, and present the task with ViewTaskActivity. + * + * The countdown step is used in most of ResearchStacks's predefined active tasks. + */ + +public class CountdownStep extends ActiveStep { + + public static final int DEFAULT_STEP_DURATION = 5; + + /* Default constructor needed for serilization/deserialization of object */ + CountdownStep() { + super(); + } + + public CountdownStep(String identifier) { + super(identifier); + setStepDuration(DEFAULT_STEP_DURATION); + setShouldStartTimerAutomatically(true); + setShouldShowDefaultTimer(false); + setShouldContinueOnFinish(true); + setEstimateTimeInMsToSpeakEndInstruction(0); // do not wait to proceed + } + + @Override + public Class getStepLayoutClass() { + return CountdownStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/FitnessStep.java b/backbone/src/main/java/org/researchstack/backbone/step/active/FitnessStep.java new file mode 100644 index 000000000..22d9d356f --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/FitnessStep.java @@ -0,0 +1,30 @@ +package org.researchstack.backbone.step.active; + +import org.researchstack.backbone.ui.step.layout.FitnessStepLayout; + +/** + * Created by TheMDP on 2/16/17. + */ + +public class FitnessStep extends ActiveStep { + + /* Default constructor needed for serilization/deserialization of object */ + FitnessStep() { + super(); + } + + public FitnessStep(String identifier) { + super(identifier); + setShouldShowDefaultTimer(false); + } + + public FitnessStep(String identifier, String title, String detailText) { + super(identifier, title, detailText); + setShouldShowDefaultTimer(false); + } + + @Override + public Class getStepLayoutClass() { + return FitnessStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/NavigationActiveStep.java b/backbone/src/main/java/org/researchstack/backbone/step/active/NavigationActiveStep.java new file mode 100644 index 000000000..ea14554e8 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/NavigationActiveStep.java @@ -0,0 +1,50 @@ +package org.researchstack.backbone.step.active; + +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.utils.StepHelper; +import org.researchstack.backbone.utils.StepResultHelper; + +import java.util.List; + +/** + * Created by TheMDP on 2/5/17. + * + * Class enabled an ActiveStep to be navigable with a custom NavigationRule + */ + +public class NavigationActiveStep extends ActiveStep implements NavigableOrderedTask.NavigationRule { + + private List customRules; + + /* Default constructor needed for serilization/deserialization of object */ + NavigationActiveStep() { + super(); + } + + public NavigationActiveStep(String identifier) { + super(identifier); + } + + public NavigationActiveStep(String identifier, String title, String detailText) { + super(identifier, title, detailText); + } + + public void setCustomRules(List customRules) { + this.customRules = customRules; + } + + @Override + public String nextStepIdentifier(TaskResult result, List additionalTaskResults) { + if (customRules != null) { + for (NavigableOrderedTask.ObjectEqualsNavigationRule rule : customRules) { + String nextIdentifier = rule.nextStepIdentifier(result, additionalTaskResults); + if (nextIdentifier != null) { + return nextIdentifier; + } + } + } + return null; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/RecorderService.java b/backbone/src/main/java/org/researchstack/backbone/step/active/RecorderService.java new file mode 100644 index 000000000..f98bf5cea --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/RecorderService.java @@ -0,0 +1,712 @@ +/* + * Copyright 2018 Sage Bionetworks + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.researchstack.backbone.step.active; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.ToneGenerator; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Vibrator; +import android.speech.tts.TextToSpeech; +import androidx.annotation.Nullable; +import androidx.annotation.RawRes; +import androidx.annotation.RequiresPermission; +import androidx.core.app.NotificationCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import android.util.Log; + +import com.google.gson.Gson; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.result.FileResult; +import org.researchstack.backbone.result.Result; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.step.active.recorder.Recorder; +import org.researchstack.backbone.step.active.recorder.RecorderConfig; +import org.researchstack.backbone.step.active.recorder.RecorderListener; +import org.researchstack.backbone.task.Task; +import org.researchstack.backbone.ui.ViewTaskActivity; +import org.researchstack.backbone.utils.LogExt; +import org.researchstack.backbone.utils.ResUtils; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.researchstack.backbone.ui.ViewTaskActivity.EXTRA_STEP; +import static org.researchstack.backbone.ui.ViewTaskActivity.EXTRA_TASK; +import static org.researchstack.backbone.ui.ViewTaskActivity.EXTRA_TASK_RESULT; + +/** + * Created by TheMDP on 1/10/18. + */ + +public class RecorderService extends Service implements RecorderListener, TextToSpeech.OnInitListener { + + public static final String RECORDER_PREFS_KEY = "RecorderServicePrefs"; + public static final String RECORDER_PREFS_RESULTS_KEY = "Results"; + public static final String RECORDER_PREFS_START_TIME_KEY = "StartTime"; + + public static final int DEFAULT_VIBRATION_AND_SOUND_DURATION = 500; // in milliseconds + private static final String NOTIFICATION_CHANNEL_ID = "RecorderService_NotificationChannel"; + private static final String NOTIFICATION_CHANNEL_TITLE = "Study in-activity progress tracker"; + private static final String NOTIFICATION_CHANNEL_DESC = "Records and shows your progress during an " + + "activity."; + + public static final String INTENT_ACTION_RECORDER_RESUME = "INTENT_ACTION_RECORDER_RESUME"; + + private static final String INTENT_KEY_OUTPUT_DIRECTORY = "RecorderOutputDirectory"; + + public static String ACTION_BROADCAST_RECORDER_COMPLETE = "RecorderService_RecordingComplete"; + public static String ACTION_BROADCAST_RECORDER_METRONOME = "RecorderService_MetronomeBroadcast"; + public static String ACTION_BROADCAST_RECORDER_SPOKEN_TEXT = "RecorderService_SpokenTextBroadcast"; + + public static String BROADCAST_RECORDER_METRONOME_CTR = "RecorderService_MetronomeCtr"; + public static String BROADCAST_RECORDER_SPOKEN_TEXT = "RecorderService_SpokenText"; + + // keys associated with spokenInstructions json recorder configs + public static final String TEXT_TO_SPEECH_END_KEY = "end"; + public static final String TEXT_TO_SPEECH_COUNTDOWN_KEY = "countdown"; + public static final String TEXT_TO_SPEECH_METRONOME_KEY = "metronome"; + + protected long startTime; + protected Handler mainHandler; + + protected List recorderList; + protected List resultList; + protected ActiveStep activeStep; + protected Task task; + protected TaskResult taskResult; + + protected Notification foregroundNotification; + + protected MediaPlayer mediaPlayer; + protected TextToSpeech tts; + protected String textToSpeakOnInit; + protected boolean isWaitingToComplete; + protected boolean isServiceRunning; + protected boolean shouldCancelRecordersOnDestroy; + + /** + * @param appContext + * @return the saved result list, if this active step has already completed recording + */ + public static ResultHolder consumeSavedResultList(Context appContext, ActiveStep activeStep) { + SharedPreferences prefs = appContext.getSharedPreferences(RECORDER_PREFS_KEY, MODE_PRIVATE); + String key = activeStep.getRecordingUuid().toString() + RECORDER_PREFS_RESULTS_KEY; + if (!prefs.contains(key)) { + return null; + } + String resultHolderJson = prefs.getString(key, null); + if (resultHolderJson == null) { + return null; + } + ResultHolder resultHolder = new Gson().fromJson(resultHolderJson, ResultHolder.class); + if (resultHolder == null) { + return null; + } + // This was a valid recorder result that was just read, so consume it + prefs.edit().remove(key).apply(); + return resultHolder; + } + + /** + * @param appContext + * @param activeStep to use to search for a previous start time + * @return null if the active step recording has not been started, null otherwise + */ + public static Long getStartTime(Context appContext, ActiveStep activeStep) { + SharedPreferences prefs = appContext.getSharedPreferences(RECORDER_PREFS_KEY, MODE_PRIVATE); + String key = activeStep.getRecordingUuid().toString() + RECORDER_PREFS_START_TIME_KEY; + if (!prefs.contains(key)) { + return null; + } + long startTime = prefs.getLong(key, -1); + if (startTime < 0) { + return null; + } + return startTime; + } + + protected static void setStartTime(Context appContext, ActiveStep activeStep, long startTime) { + LogExt.d(RecorderService.class, "setStartTime() " + startTime); + SharedPreferences prefs = appContext.getSharedPreferences(RECORDER_PREFS_KEY, MODE_PRIVATE); + String key = activeStep.getRecordingUuid().toString() + RECORDER_PREFS_START_TIME_KEY; + prefs.edit().putLong(key, startTime).apply(); + } + + protected static void removeStartTime(Context appContext, ActiveStep activeStep) { + LogExt.d(RecorderService.class, "removeStartTime()"); + SharedPreferences prefs = appContext.getSharedPreferences(RECORDER_PREFS_KEY, MODE_PRIVATE); + String key = activeStep.getRecordingUuid().toString() + RECORDER_PREFS_START_TIME_KEY; + prefs.edit().remove(key).apply(); + } + + /** + * @param appContext + * @return the saved result list, if this active step has already completed recording + */ + protected static void setSavedResultList(Context appContext, + ActiveStep activeStep, + ResultHolder resultHolder) { + + SharedPreferences prefs = appContext.getSharedPreferences(RECORDER_PREFS_KEY, MODE_PRIVATE); + String key = activeStep.getRecordingUuid().toString() + RECORDER_PREFS_RESULTS_KEY; + String resultListHolderJson = new Gson().toJson(resultHolder); + // commit needed since this may be read immediately + prefs.edit().putString(key, resultListHolderJson).commit(); + // Once we have stored the result, we should remove the startTime pref + removeStartTime(appContext, activeStep); + } + + /** + * @param appContext needed to create the intent + * @param activeStep currently being run, must contain a valid stepDuration + * @param outputDirectory for the recorders + * @param task the task for this activeStep, used in the Notification PendingIntent + * @param taskResult the current TaskResult, used in the Notification PendingIntent + * @return the intent to launch via "appContext.startService()" + */ + public static void startService(Context appContext, + File outputDirectory, + ActiveStep activeStep, + Task task, + TaskResult taskResult) { + + Intent intent = new Intent(appContext, RecorderService.class); + Bundle extras = new Bundle(); + + extras.putSerializable(EXTRA_STEP, activeStep); + extras.putSerializable(ViewTaskActivity.EXTRA_TASK, task); + extras.putSerializable(ViewTaskActivity.EXTRA_TASK_RESULT, taskResult); + extras.putSerializable(INTENT_KEY_OUTPUT_DIRECTORY, outputDirectory); + + intent.putExtras(extras); + appContext.startService(intent); + } + + @Override + public void onCreate() { + super.onCreate(); + LogExt.d(RecorderService.class, "onCreate"); + + // no-op, wait for onStartCommand + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + LogExt.d(RecorderService.class, "onStartCommand"); + + if (isServiceRunning) { + LogExt.e(RecorderService.class, "RecorderService already running, " + + "ignoring duplicate start command"); + return START_NOT_STICKY; + } + + isServiceRunning = true; + shouldCancelRecordersOnDestroy = true; + mainHandler = new Handler(); + resultList = new ArrayList<>(); + recorderList = new ArrayList<>(); + startTime = System.currentTimeMillis(); + isWaitingToComplete = false; + + // Starting with API 26, notifications must be contained in a channel + if (Build.VERSION.SDK_INT >= 26) { + NotificationManager notificationManager = + (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + NotificationChannel channel = new NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_TITLE, + NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(NOTIFICATION_CHANNEL_DESC); + notificationManager.createNotificationChannel(channel); + } + } + + File outputDir = null; + if (intent != null && intent.getExtras() != null) { + Bundle bundle = intent.getExtras(); + if (bundle.containsKey(EXTRA_STEP)) { + activeStep = (ActiveStep)bundle.getSerializable(EXTRA_STEP); + } + if (bundle.containsKey(EXTRA_TASK)) { + task = (Task)bundle.getSerializable(EXTRA_TASK); + } + if (bundle.containsKey(EXTRA_TASK_RESULT)) { + taskResult = (TaskResult)bundle.getSerializable(EXTRA_TASK_RESULT); + if (taskResult == null && task != null) { // it may be that there is no results yet + taskResult = new TaskResult(task.getIdentifier()); + } + } + if (bundle.containsKey(INTENT_KEY_OUTPUT_DIRECTORY)) { + outputDir = (File)bundle.getSerializable(INTENT_KEY_OUTPUT_DIRECTORY); + } + } + + if (activeStep != null && activeStep.getStepDuration() > 0 && + task != null && taskResult != null) { + + LogExt.d(RecorderService.class, "Valid active step found, starting service"); + + if (activeStep.hasVoice()) { + tts = new TextToSpeech(this, this); + } + + if (activeStep.getSoundRes() != null) { + @RawRes int soundRes = ResUtils + .getRawResourceId(this, activeStep.getSoundRes()); + if (soundRes == 0) { + LogExt.e(RecorderService.class, + "Error finding ActiveStep's sound " + activeStep.getSoundRes()); + } else { + mediaPlayer = MediaPlayer.create(this, soundRes); + mediaPlayer.start(); + } + } + + setStartTime(getApplicationContext(), activeStep, startTime); + + String notificationTitle = getString(R.string.rsb_recorder_notification_title); + // This will ensure that this service is never destroyed + showForegroundNotification(notificationTitle); + + // Start the recording process + if (activeStep.getRecorderConfigurationList() != null) { + for (RecorderConfig recorderConfig : activeStep.getRecorderConfigurationList()) { + Recorder recorder = recorderConfig.recorderForStep(activeStep, outputDir); + // recorder can be null if it requires custom setup, + // but that will require a custom RecorderService to handle + if (recorder != null) { + recorder.setRecorderListener(this); + recorderList.add(recorder); + recorder.start(getApplicationContext()); + LogExt.d(RecorderService.class, "recorder started " + recorder.getIdentifier()); + } + } + } + + // Start the delayed operations that will happen during the life-cycle of the service + startDelayedOperations(); + + } else { + String errorMessage = "ActiveStep is null or does not have a valid step duration"; + LogExt.e(RecorderService.class, errorMessage); + sendRecorderErrorBroadcast(errorMessage); + stopSelf(); + } + + return START_NOT_STICKY; + } + + private void startDelayedOperations() { + // Now allow the recorder to record for as long as the active step requires + mainHandler.postDelayed(this::onRecorderDurationFinished, + activeStep.getStepDuration() * 1000L); + + startSpeechToTextMap(); + } + + protected void startSpeechToTextMap() { + + Map speechToTextMap = activeStep.getSpokenInstructionMap(); + if (speechToTextMap != null) { + for (String speechKey : speechToTextMap.keySet()) { + + // Check for special case "end" key that speaks after the step duration + if (TEXT_TO_SPEECH_END_KEY.equals(speechKey)) { + final String endSpeechText = speechToTextMap.get(speechKey); + mainHandler.postDelayed(() -> speakTextAndUpdateNotification(endSpeechText), + activeStep.getStepDuration() * 1000L); + + // Check for special case "countdown" key that speaks a verbal seconds countdown + } else if (TEXT_TO_SPEECH_COUNTDOWN_KEY.equals(speechKey)) { + try { + int countdownTime = Integer.parseInt(speechToTextMap.get(speechKey)); + for (int i = countdownTime; i > 0; i--) { + final String countDownStr = String.valueOf(i); + mainHandler.postDelayed(() -> speakText(countDownStr), + (activeStep.getStepDuration() - i) * 1000L); + } + } catch (NumberFormatException e) { + LogExt.e(RecorderService.class, e.getLocalizedMessage()); + } + // All other cases will speak the text at the seconds time of the speechKey + } else if (TEXT_TO_SPEECH_METRONOME_KEY.equals(speechKey)) { + try { + double metronomeIntervalInSec = + Double.parseDouble(speechToTextMap.get(speechKey)); + long metronomeIntervalInMs = (long)(metronomeIntervalInSec * 1000L); + long stopTimeInMs = activeStep.getStepDuration() * 1000L; + long metronomeTimeInMs = metronomeIntervalInMs; + final ToneGenerator tockSound = + new ToneGenerator(AudioManager.STREAM_MUSIC, 60); + int metronomeCtr = 0; + while (metronomeTimeInMs < stopTimeInMs) { + final int metronomeCounter = metronomeCtr; + mainHandler.postDelayed(() -> { + tockSound.startTone(ToneGenerator.TONE_CDMA_PIP, 100); + sendMetronomeBroadcast(metronomeCounter); + }, metronomeTimeInMs); + metronomeTimeInMs += metronomeIntervalInMs; + metronomeCtr++; + } + } catch (NumberFormatException e) { + LogExt.e(RecorderService.class, e.getLocalizedMessage()); + } + // All other cases will speak the text at the seconds time of the speechKey + } else { + try { + double triggerTime = Double.parseDouble(speechKey); + final String speechText = speechToTextMap.get(speechKey); + mainHandler.postDelayed(() -> speakTextAndUpdateNotification(speechText), + (long)(triggerTime * 1000L)); + } catch (NumberFormatException e) { + LogExt.e(RecorderService.class, e.getLocalizedMessage()); + } + } + } + } + } + + protected void speakTextAndUpdateNotification(String message) { + speakText(message); + showForegroundNotification(message); + } + + /** + * This would never be called under normal operation while the recorders are running + * It will only be called when the user chooses to end the task and discard the results + * or in special very rare cases like user shut their phone off, serious memory issues, etc + * It should be treated like a recorder canceled scenario + */ + @Override + public void onDestroy() { + super.onDestroy(); + LogExt.d(RecorderService.class, "onDestroyed service is stopping"); + + shutDownTts(); + shutDownMediaPlayer(); + mainHandler.removeCallbacksAndMessages(null); + if (isServiceRunning && shouldCancelRecordersOnDestroy) { + LogExt.d(RecorderService.class, "cancelling all recorders"); + removeStartTime(getApplicationContext(), activeStep); + for (Recorder recorder : recorderList) { + recorder.cancel(); + } + } + isServiceRunning = false; + } + + @Override + public IBinder onBind(Intent intent) { + return null; // no need, pass everything through LocalBroadcastManager and Intents + } + + private void showForegroundNotification(String notificationMessage) { + + // Starting with API 26, notifications must be contained in a channel + if (Build.VERSION.SDK_INT >= 26) { + NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + NotificationChannel channel = new NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_TITLE, + NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(NOTIFICATION_CHANNEL_DESC); + notificationManager.createNotificationChannel(channel); + } + } + + LogExt.d(RecorderService.class, "showForegroundNotification(" + notificationMessage + ")"); + Intent notificationIntent = new Intent(this, activeStep.getActivityClazz()); + + // These will guarantee the activity is re-created at the same step as we were running + notificationIntent.putExtra(ViewTaskActivity.EXTRA_TASK, task); + notificationIntent.putExtra(ViewTaskActivity.EXTRA_TASK_RESULT, taskResult); + notificationIntent.putExtra(ViewTaskActivity.EXTRA_STEP, activeStep); + + notificationIntent.setAction(INTENT_ACTION_RECORDER_RESUME); + notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent pendingIntent = PendingIntent.getActivity( + this, 0, notificationIntent, 0); + + String msg = getString(R.string.rsb_recording); + + NotificationCompat.Builder notificationBuilder = + new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + .setContentTitle(task.getIdentifier() + " " + msg) + .setContentText(notificationMessage) + .setContentIntent(pendingIntent); + + // vector drawable crash + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + notificationBuilder.setSmallIcon(R.drawable.rsb_ic_recorder_notification); + } + foregroundNotification = notificationBuilder.build(); + startForeground(1, foregroundNotification); + } + + private void sendRecorderErrorBroadcast(String errorMessage) { + LogExt.d(RecorderService.class, "sendRecorderErrorBroadcast()"); + Intent broadcastIntent = new Intent(ACTION_BROADCAST_RECORDER_COMPLETE); + ResultHolder resultHolder = new ResultHolder(); + resultHolder.setErrorMessage(errorMessage); + resultHolder.setStartTime(startTime); + setSavedResultList(getApplicationContext(), activeStep, resultHolder); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcastIntent); + } + + private void sendRecorderCompleteBroadcast() { + LogExt.d(RecorderService.class, "sendRecorderCompleteBroadcast()"); + Intent broadcastIntent = new Intent(ACTION_BROADCAST_RECORDER_COMPLETE); + ResultHolder resultHolder = new ResultHolder(); + resultHolder.setResultList(resultList); + resultHolder.setStartTime(startTime); + setSavedResultList(getApplicationContext(), activeStep, resultHolder); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcastIntent); + } + + protected void sendMetronomeBroadcast(int ctr) { + Intent broadcastIntent = new Intent(ACTION_BROADCAST_RECORDER_METRONOME); + broadcastIntent.putExtra(BROADCAST_RECORDER_METRONOME_CTR, ctr); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcastIntent); + } + + protected void sendSpokenTextBroadcast(String spokenText) { + Intent broadcastIntent = new Intent(ACTION_BROADCAST_RECORDER_SPOKEN_TEXT); + broadcastIntent.putExtra(BROADCAST_RECORDER_SPOKEN_TEXT, spokenText); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcastIntent); + } + + @RequiresPermission(value = Manifest.permission.VIBRATE, conditional = true) + private void onRecorderDurationFinished() { + LogExt.d(RecorderService.class, "onRecorderDurationFinished()"); + isWaitingToComplete = true; + + if (activeStep.getShouldVibrateOnFinish()) { + vibrate(); + } + + if (activeStep.getShouldPlaySoundOnFinish()) { + playSound(); + } + + if (activeStep.getFinishedSpokenInstruction() != null) { + speakText(activeStep.getFinishedSpokenInstruction()); + } + + // copy list to avoid any concurrent modifications with calling stop on each recorder + List copiedList = new ArrayList<>(recorderList); + for (Recorder recorder : copiedList) { + recorder.stop(); // this will trigger a call to onComplete below + LogExt.d(RecorderService.class, "recorder stopped " + recorder.getIdentifier()); + } + // The continueOnFinishDelay allows the TTS to flush, + // and for the recorders to finish up before leaving the screen + mainHandler.postDelayed(() -> { + isWaitingToComplete = false; + if (!recorderList.isEmpty()) { + // continue to wait for recorders to finish before sending the complete broadcast + } else { + sendCompleteBroadcastAndFinish(); + } + }, activeStep.getEstimateTimeInMsToSpeakEndInstruction()); + } + + /** + * RecorderListener callback + * @param recorder The generating recorder object. + * @param result The generated result. + */ + @Override + public void onComplete(Recorder recorder, Result result) { + if (!(result instanceof FileResult)) { + // Due to the RecorderService having to store results in a SharedPreferences file + // We do not allow any Results other than FileResults + throw new IllegalStateException("RecorderService only works " + + "with Recorders that return FileResults"); + } + FileResult fileResult = (FileResult)result; + LogExt.d(RecorderService.class, "recorder onComplete() " + result.getIdentifier()); + recorderList.remove(recorder); + resultList.add(fileResult); + if (!isWaitingToComplete && recorderList.isEmpty()) { + sendCompleteBroadcastAndFinish(); + } + } + + protected void sendCompleteBroadcastAndFinish() { + shouldCancelRecordersOnDestroy = false; + sendRecorderCompleteBroadcast(); + stopSelf(); + } + + /** + * RecorderListener callback + * @param recorder The generating recorder object. + * @param error The error that occurred. + */ + @Override + public void onFail(Recorder recorder, Throwable error) { + shutDownTts(); + shutDownMediaPlayer(); + sendRecorderErrorBroadcast(error.getLocalizedMessage()); + stopSelf(); + } + + protected void shutDownTts() { + if (tts != null) { + if (tts.isSpeaking()) { + tts.stop(); + } + tts.shutdown(); + tts = null; + } + } + + protected void shutDownMediaPlayer() { + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } + } + + @Nullable + @Override + public Context getBroadcastContext() { + return this; + } + + // TextToSpeech initialization + @Override + public void onInit(int i) { + if (i == TextToSpeech.SUCCESS) { + int languageAvailable = tts.isLanguageAvailable(Locale.getDefault()); + // >= 0 means LANG_AVAILABLE, LANG_COUNTRY_AVAILABLE, or LANG_COUNTRY_VAR_AVAILABLE + if (languageAvailable >= 0) { + tts.setLanguage(Locale.getDefault()); + if (textToSpeakOnInit != null) { + speakText(textToSpeakOnInit); + } + } else { + tts = null; + } + } else { + Log.e(getClass().getCanonicalName(), "Failed to initialize TTS with error code " + i); + tts = null; + } + } + + protected void speakText(String text) { + // Setting this will guarantee the text gets spoken in the case that tts isn't set up yet + textToSpeakOnInit = text; + if (tts == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ttsGreater21(text); + } else { + ttsUnder20(text); + } + sendSpokenTextBroadcast(text); + } + + @SuppressWarnings("deprecation") + private void ttsUnder20(String text) { + HashMap map = new HashMap<>(); + map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "MessageId"); + tts.speak(text, TextToSpeech.QUEUE_FLUSH, map); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void ttsGreater21(String text) { + String utteranceId = String.valueOf(hashCode()); + tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId); + } + + @RequiresPermission(Manifest.permission.VIBRATE) + private void vibrate() { + Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + v.vibrate(DEFAULT_VIBRATION_AND_SOUND_DURATION); + } + + protected void playSound() { + ToneGenerator toneG = new ToneGenerator(AudioManager.STREAM_ALARM, 50); // 50 = half volume + // Play a low and high tone for 500 ms at full volume + toneG.startTone(ToneGenerator.TONE_CDMA_LOW_L, DEFAULT_VIBRATION_AND_SOUND_DURATION); + toneG.startTone(ToneGenerator.TONE_CDMA_HIGH_L, DEFAULT_VIBRATION_AND_SOUND_DURATION); + } + + /** + * Holder class for encapsulation serializable list data sent through intents + */ + public static class ResultHolder implements Serializable { + private long startTime; + private String errorMessage; + private List resultList; + + public ResultHolder() { + super(); + resultList = new ArrayList<>(); + } + + public List getResultList() { + return resultList; + } + + public void setResultList(List resultList) { + this.resultList = resultList; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public long getStartTime() { + return startTime; + } + + public void setStartTime(long startTime) { + this.startTime = startTime; + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/TappingIntervalStep.java b/backbone/src/main/java/org/researchstack/backbone/step/active/TappingIntervalStep.java new file mode 100644 index 000000000..6796d5d1b --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/TappingIntervalStep.java @@ -0,0 +1,35 @@ +package org.researchstack.backbone.step.active; + +import org.researchstack.backbone.ui.step.layout.TappingIntervalStepLayout; + +/** + * Created by TheMDP on 2/23/17. + */ + +public class TappingIntervalStep extends ActiveStep { + + /* Default constructor needed for serialization/deserialization of object */ + TappingIntervalStep() { + super(); + } + + public TappingIntervalStep(String identifier) { + super(identifier); + commonInit(); + } + + public TappingIntervalStep(String identifier, String title, String detailText) { + super(identifier, title, detailText); + commonInit(); + } + + private void commonInit() { + setShouldShowDefaultTimer(false); + setOptional(false); + } + + @Override + public Class getStepLayoutClass() { + return TappingIntervalStepLayout.class; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/TimedWalkStep.java b/backbone/src/main/java/org/researchstack/backbone/step/active/TimedWalkStep.java new file mode 100644 index 000000000..19d978e75 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/TimedWalkStep.java @@ -0,0 +1,50 @@ +package org.researchstack.backbone.step.active; + +import org.researchstack.backbone.ui.step.layout.ActiveStepLayout; + +/** + * Created by TheMDP on 2/22/17. + */ + +public class TimedWalkStep extends ActiveStep { + + private double distanceInMeters; + + /* Default constructor needed for serilization/deserialization of object */ + TimedWalkStep() { + super(); + } + + public TimedWalkStep(String identifier, double distanceInMeters) { + super(identifier); + commonInit(distanceInMeters); + } + + public TimedWalkStep(String identifier, String title, String detailText, double distanceInMeters) { + super(identifier, title, detailText); + commonInit(distanceInMeters); + } + + private void commonInit(double distanceInMeters) { + this.distanceInMeters = distanceInMeters; + setShouldStartTimerAutomatically(true); + setShouldShowDefaultTimer(false); + setShouldPlaySoundOnStart(true); + setShouldPlaySoundOnFinish(true); + setShouldVibrateOnStart(true); + setShouldVibrateOnFinish(true); + } + + @Override + public Class getStepLayoutClass() { + return ActiveStepLayout.class; + } + + public double getDistanceInMeters() { + return distanceInMeters; + } + + public void setDistanceInMeters(double distanceInMeters) { + this.distanceInMeters = distanceInMeters; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/WalkingTaskStep.java b/backbone/src/main/java/org/researchstack/backbone/step/active/WalkingTaskStep.java new file mode 100644 index 000000000..ad76ed099 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/WalkingTaskStep.java @@ -0,0 +1,46 @@ +package org.researchstack.backbone.step.active; + +import org.researchstack.backbone.ui.step.layout.ActiveStepLayout; +import org.researchstack.backbone.ui.step.layout.WalkingTaskStepLayout; + +/** + * Created by TheMDP on 2/15/17. + */ + +public class WalkingTaskStep extends ActiveStep { + + private int numberOfStepsPerLeg; + + /* Default constructor needed for serilization/deserialization of object */ + WalkingTaskStep() { + super(); + } + + /* Default constructor needed for serilization/deserialization of object */ + public WalkingTaskStep(String identifier) { + super(identifier); + commonInit(); + } + + public WalkingTaskStep(String identifier, String title, String detailText) { + super(identifier, title, detailText); + commonInit(); + } + + @Override + public Class getStepLayoutClass() { + return WalkingTaskStepLayout.class; + } + + private void commonInit() { + setShouldShowDefaultTimer(false); + } + + public int getNumberOfStepsPerLeg() { + return numberOfStepsPerLeg; + } + + public void setNumberOfStepsPerLeg(int numberOfStepsPerLeg) { + this.numberOfStepsPerLeg = numberOfStepsPerLeg; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AccelerometerRecorder.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AccelerometerRecorder.java new file mode 100644 index 000000000..283d8e673 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AccelerometerRecorder.java @@ -0,0 +1,29 @@ +package org.researchstack.backbone.step.active.recorder; + +import android.hardware.Sensor; + +import org.researchstack.backbone.step.Step; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +/** + * Created by TheMDP on 2/5/17. + * + * This class uses the JsonArrayDataRecorder class to save the Accelerometer sensor's data as + * an array of accelerometer json objects with timestamp, ax, ay, and az + */ +public class AccelerometerRecorder extends DeviceMotionRecorder { + AccelerometerRecorder(double frequency, String identifier, Step step, File outputDirectory) { + super(frequency, identifier, step, outputDirectory); + } + + @Override + protected List getSensorTypeList(List availableSensorList) { + if (hasAvailableType(availableSensorList, Sensor.TYPE_ACCELEROMETER)) { + return Collections.singletonList(Sensor.TYPE_ACCELEROMETER); + } + return Collections.emptyList(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AccelerometerRecorderConfig.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AccelerometerRecorderConfig.java new file mode 100644 index 000000000..b58730427 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AccelerometerRecorderConfig.java @@ -0,0 +1,40 @@ +package org.researchstack.backbone.step.active.recorder; + +import org.researchstack.backbone.step.Step; + +import java.io.File; + +/** + * Created by TheMDP on 2/5/17. + */ + +public class AccelerometerRecorderConfig extends RecorderConfig { + + /** + * The frequency of accelerometer data collection in samples per second (Hz). + */ + private double frequency; + + /** Default constructor used for serialization/deserialization */ + AccelerometerRecorderConfig() { + super(); + } + + public AccelerometerRecorderConfig(String identifier, double frequency) { + super(identifier); + this.frequency = frequency; + } + + @Override + public Recorder recorderForStep(Step step, File outputDirectory) { + return new AccelerometerRecorder(frequency, getIdentifier(), step, outputDirectory); + } + + public double getFrequency() { + return frequency; + } + + public void setFrequency(double frequency) { + this.frequency = frequency; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AccelerometerStepDetector.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AccelerometerStepDetector.java new file mode 100644 index 000000000..a1c654c39 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AccelerometerStepDetector.java @@ -0,0 +1,95 @@ +package org.researchstack.backbone.step.active.recorder; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorManager; +import android.util.Log; + +/** + * Created by TheMDP on 2/15/17. + * + * https://github.com/bagilevi/android-pedometer + * Accelerometer to Step Algorithm from link is distributed under a No restrictions license + * TODO: develop a better step detector method, this one has too many unknown constants + * TODO: it also seems to have a variance of about +-15% in my experiments + */ + +public class AccelerometerStepDetector { + + private final static String TAG = "StepDetector"; + + private OnStepTakenListener onStepTakenListener; + + private float mLimit = 10; + private float mLastValues[] = new float[3*2]; + private float mScale[] = new float[2]; + private float mYOffset; + + private float mLastDirections[] = new float[3*2]; + private float mLastExtremes[][] = { new float[3*2], new float[3*2] }; + private float mLastDiff[] = new float[3*2]; + private int mLastMatch = -1; + + public AccelerometerStepDetector() { + int h = 480; // TODO: remove this constant + mYOffset = h * 0.5f; + mScale[0] = - (h * 0.5f * (1.0f / (SensorManager.STANDARD_GRAVITY * 2))); + mScale[1] = - (h * 0.5f * (1.0f / (SensorManager.MAGNETIC_FIELD_EARTH_MAX))); + } + + public void setSensitivity(float sensitivity) { + mLimit = sensitivity; // 1.97 2.96 4.44 6.66 10.00 15.00 22.50 33.75 50.62 + } + + public void processAccelerometerData(SensorEvent sensorEvent) { + if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { + float vSum = 0; + for (int i = 0; i < 3; i++) { + final float v = mYOffset + sensorEvent.values[i] * mScale[1]; + vSum += v; + } + int k = 0; + float v = vSum / 3; + + float direction = (v > mLastValues[k] ? 1 : (v < mLastValues[k] ? -1 : 0)); + if (direction == -mLastDirections[k]) { + // Direction changed + int extType = (direction > 0 ? 0 : 1); // minumum or maximum? + mLastExtremes[extType][k] = mLastValues[k]; + float diff = Math.abs(mLastExtremes[extType][k] - mLastExtremes[1 - extType][k]); + + if (diff > mLimit) { + + boolean isAlmostAsLargeAsPrevious = diff > (mLastDiff[k] * 2 / 3); + boolean isPreviousLargeEnough = mLastDiff[k] > (diff / 3); + boolean isNotContra = (mLastMatch != 1 - extType); + + if (isAlmostAsLargeAsPrevious && isPreviousLargeEnough && isNotContra) { + Log.i(TAG, "step"); + onStepTaken(); + mLastMatch = extType; + } else { + mLastMatch = -1; + } + } + mLastDiff[k] = diff; + } + mLastDirections[k] = direction; + mLastValues[k] = v; + } + } + + private void onStepTaken() { + if (onStepTakenListener != null) { + onStepTakenListener.onStepTaken(); + } + } + + public void setOnStepTakenListener(OnStepTakenListener onStepTakenListener) { + this.onStepTakenListener = onStepTakenListener; + } + + public interface OnStepTakenListener { + void onStepTaken(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AudioRecorder.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AudioRecorder.java new file mode 100644 index 000000000..daf9206c0 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AudioRecorder.java @@ -0,0 +1,343 @@ +package org.researchstack.backbone.step.active.recorder; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.MediaRecorder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.webkit.MimeTypeMap; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.result.FileResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.utils.LogExt; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.Date; + +/** + * Created by TheMDP on 2/26/17. + * + * The AudioRecorder records audio in a format specified by the AudioRecorderSettings + * It bundles the result in an AudioResult object in which it returns in the recorder listener + */ + +public class AudioRecorder extends Recorder { + + private static final double VOLUME_CLAMP_IN_DECIBELS = 60.0; + public static final int MAX_VOLUME = 32767; + + /** + * This is the duration in between checking for the max sample amplitude of the audio recorder + * As of now, 100 ms is sufficient to check if the audio is "too loud" + */ + private static final int AVERAGE_MAX_VOLUME_DURATION = 100; + + public static final String BROADCAST_SAMPLE_ACTION = "AudioRecorder_BroadcastSample"; + private static final String BROADCAST_SAMPLE_KEY = "BroadcastSample"; + + /** + * Used to check amplitude of the sample at a desired frequency + */ + private Handler mainHandler; + private Runnable sampleMonitorRunnable; + + /** + * The AudioRecorder can give average sample callbacks at any desired frequency + * These variables keep track of the running average per callback window + */ + private long sampleSumSinceLastCallback; + private int samplesSinceLastCallback; + private long timeOfLastCallback; + private long msBetweenCallbacks; + + /** + * These keep track of the entire rolling average for the entire life-cycle of AudioRecorder + * It can be used to get an overall picture of the audio's background noise + */ + private long totalRollingAvg; + private int totalRollingAvgSampleCount; + + private AudioRecorderSettings settings; + private MediaRecorder mediaRecorder; + private long startTime; + + private static double sLastSampleAvg; + /** + * @return The last audio sample recorder, the total average volume from 0 - 1 + * The is most useful for determining if an area is too loud or noisy + */ + public static double getLastTotalSampleAvg() { + return sLastSampleAvg; + } + public static void setLastTotalSampleAvg(double totalAvg) { + sLastSampleAvg = totalAvg; + } + + AudioRecorder(AudioRecorderSettings settings, + String identifier, + Step step, + File outputDirectory) + { + super(identifier, step, outputDirectory); + this.settings = settings; + mainHandler = new Handler(); + } + + @Override + public void start(Context context) { + if (mediaRecorder != null) { + throw new IllegalStateException("Cannot start media recorder since it has already been started"); + } + + if (settings == null) { + throw new IllegalStateException("Cannot start media recorder since settings is null"); + } + + // In-app permissions were added in Android 6.0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PackageManager pm = context.getPackageManager(); + int hasPerm = pm.checkPermission(Manifest.permission.RECORD_AUDIO, context.getPackageName()); + if (hasPerm != PackageManager.PERMISSION_GRANTED) { + if (getRecorderListener() != null) { + String errorMsg = context.getString(R.string.rsb_permission_microphone_desc); + getRecorderListener().onFail(this, new IllegalStateException(errorMsg)); + } + return; + } + } + + mediaRecorder = new MediaRecorder(); + + mediaRecorder.setAudioSource(settings.getAudioSource()); + mediaRecorder.setOutputFormat(settings.getOutputFormat()); + mediaRecorder.setAudioEncoder(settings.getAudioEncoder()); + mediaRecorder.setAudioEncodingBitRate(settings.getSampleRate()); + mediaRecorder.setAudioChannels(settings.getAudioChannels()); + mediaRecorder.setAudioSamplingRate(settings.getSampleRate()); + mediaRecorder.setOutputFile(fullFilePath()); + + try { + mediaRecorder.prepare(); + } catch (IOException e) { + LogExt.e(getClass(), e.getMessage()); + onRecorderFailed(e); + mediaRecorder = null; + return; + } + + // Audio is recording + mediaRecorder.start(); + + // MediaRecorder doesn't support callbacks each time a sample is recorded, + // However, it does support querying for the most recent maximum amplitude + // So we will use a re-curring check to do a running average of the samples + startSampleMonitoring(); + + startTime = System.currentTimeMillis(); + + // Note: if the developer needs higher level audio analysis, they must either read + // the audio file afterwords, or use another method of audio recording + } + + private String fullFilePath() { + return getOutputDirectory() + File.separator + uniqueFilename + + getFileExtensionForOutputFormat(settings.getOutputFormat()); + } + + private void refreshCallbackVariables() { + sampleSumSinceLastCallback = 0; + samplesSinceLastCallback = 0; + timeOfLastCallback = System.currentTimeMillis(); + } + + private void startSampleMonitoring() { + totalRollingAvg = 0; + totalRollingAvgSampleCount = 0; + + refreshCallbackVariables(); + + sampleMonitorRunnable = new Runnable() { + @Override + public void run() { + if (mediaRecorder != null) { + // Every time you query getMaxAmplitude, it gets reset, + // so that the next time will be relative to the last time you called the method + int currentIntensity = mediaRecorder.getMaxAmplitude(); + + // Compute the new rolling average + addSampleToRollingAverages(currentIntensity); + + if (msBetweenCallbacks > 0) { + long now = System.currentTimeMillis(); + if ((now - timeOfLastCallback) > msBetweenCallbacks) { + int averageSample = (int) (sampleSumSinceLastCallback / samplesSinceLastCallback); + sendAverageSampleBroadcast(averageSample); + refreshCallbackVariables(); + } + } + } + mainHandler.postDelayed(sampleMonitorRunnable, AVERAGE_MAX_VOLUME_DURATION); + } + }; + // Smallest possible delay to get the most information about the audio recording + mainHandler.postDelayed(sampleMonitorRunnable, AVERAGE_MAX_VOLUME_DURATION); + } + + private void sendAverageSampleBroadcast(int averageSample) { + AverageSampleHolder sampleHolder = new AverageSampleHolder(); + sampleHolder.averageSampleVolume = averageSample; + sampleHolder.maxVolume = MAX_VOLUME; + Bundle bundle = new Bundle(); + bundle.putSerializable(BROADCAST_SAMPLE_KEY, sampleHolder); + Intent intent = new Intent(BROADCAST_SAMPLE_ACTION); + intent.putExtras(bundle); + sendBroadcast(intent); + } + + /** + * @param intent must have action of BROADCAST_SAMPLE_ACTION + * @return the AverageSampleHolder contained in the broadcast + */ + public static AverageSampleHolder getAverageSample(Intent intent) { + if (intent.getAction() == null || + !intent.getAction().equals(BROADCAST_SAMPLE_ACTION) || + intent.getExtras() == null || + !intent.getExtras().containsKey(BROADCAST_SAMPLE_KEY)) { + return null; + } + return (AverageSampleHolder) intent.getExtras().getSerializable(BROADCAST_SAMPLE_KEY); + } + + private void stopSampleMonitoring() { + mainHandler.removeCallbacksAndMessages(null); + } + + private void addSampleToRollingAverages(int sampleIntensity) { + LogExt.i(getClass(), "Sample intensity " + sampleIntensity); + + // Add to the running average for this period + sampleSumSinceLastCallback += sampleIntensity; + samplesSinceLastCallback++; + + totalRollingAvg += sampleIntensity; + totalRollingAvgSampleCount++; + + // iOS does this + // Convert to decibels and add it to the full normalized running average value +// double db = 20 * Math.log10(Math.abs(sampleIntensity) / MAX_VOLUME); +// double clampedValue = Math.max(db / VOLUME_CLAMP_IN_DECIBELS, -1) + 1; +// totalRollingAvgSampleCount++; +// totalRollingAvg = (totalRollingAvg * (totalRollingAvgSampleCount - 1) + clampedValue) / totalRollingAvgSampleCount; + } + + @Override + public void stop() { + stopSampleMonitoring(); + + if (mediaRecorder == null) { + throw new IllegalStateException("Cannot stop media recorder since it has not been started"); + } + + stopAndReleaseMediaRecorder(); + + // Build the file result and fill it with the collected data + String filepath = fullFilePath(); + File file = new File(filepath); + String mimeType = getMimeType(filepath); + FileResult fileResult = new FileResult(fileResultIdentifier(), file, mimeType); + fileResult.setStartDate(new Date(startTime)); + fileResult.setEndDate(new Date()); + + double totalAvgIntensityFrom0To1 = ((double)totalRollingAvg / + (double)totalRollingAvgSampleCount) / (double)MAX_VOLUME; + setLastTotalSampleAvg(totalAvgIntensityFrom0To1); + + // Return the result to the recorder listener + onRecorderCompleted(fileResult); + } + + @Override + public void cancel() { + stopSampleMonitoring(); + + if (mediaRecorder == null) { + return; // no reason to cancel anything, since the mediaRecorder never started + } + + stopAndReleaseMediaRecorder(); + + // Delete the file that was created + String filepath = getOutputDirectory() + File.separator + uniqueFilename; + File file = new File(filepath); + boolean deletedSuccessfully = file.delete(); + if (!deletedSuccessfully) { + LogExt.e(getClass(), "File was not deleted when recorder was cancelled " + file.toString()); + } + } + + private void stopAndReleaseMediaRecorder() { + // Stop the media recorder and release all the resources it was using + mediaRecorder.stop(); + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; + } + + private static String getMimeType(String filePath) { + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(filePath); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } + + private String getFileExtensionForOutputFormat(int outputFormat) { + switch (outputFormat) { + case MediaRecorder.OutputFormat.MPEG_4: + return ".m4a"; + default: + return ""; // needs implemented for other file types + } + } + + /** + * @param timeBetweenListenerCalls Duration cannot currently be less than 100 milliseconds + * the duration in milliseconds that will be in between calls to onAudioSampleRecorded + */ + public void setAudioRecorderBroadcastInterval(long timeBetweenListenerCalls) + { + this.msBetweenCallbacks = timeBetweenListenerCalls; + } + + public static class AverageSampleHolder implements Serializable { + private int averageSampleVolume; + private int maxVolume; + + public AverageSampleHolder() { + super(); + } + + public int getAverageSampleVolume() { + return averageSampleVolume; + } + + public void setAverageSampleVolume(int averageSampleVolume) { + this.averageSampleVolume = averageSampleVolume; + } + + public int getMaxVolume() { + return maxVolume; + } + + public void setMaxVolume(int maxVolume) { + this.maxVolume = maxVolume; + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AudioRecorderConfig.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AudioRecorderConfig.java new file mode 100644 index 000000000..b79d76799 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AudioRecorderConfig.java @@ -0,0 +1,60 @@ +package org.researchstack.backbone.step.active.recorder; + +import org.researchstack.backbone.step.Step; + +import java.io.File; + +/** + * Created by TheMDP on 2/26/17. + * + * The `AudioRecorderConfig` class represents a configuration that records + * audio data during an active step. + * + * An `AudioRecorderConfig` generates an `AudioRecorder` object. + * + * To use a recorder, include its configuration in the `recorderConfigurationList` property + * of an `ActiveStep` object, include that step in a task, and present it with + * an 'ActiveTaskActivity'. + * + */ + +public class AudioRecorderConfig extends RecorderConfig { + + private AudioRecorderSettings settings; + + private static final long DEFAULT_TIME_BETWEEN_BROADCASTS = 180; + private long timeBetweenAverageSampleBroadcasts = DEFAULT_TIME_BETWEEN_BROADCASTS; + + /** Default constructor used for serialization/deserialization */ + AudioRecorderConfig() { + super(); + } + + public AudioRecorderConfig(AudioRecorderSettings settings, String identifier) { + super(identifier); + this.settings = settings; + } + + @Override + public Recorder recorderForStep(Step step, File outputDirectory) { + AudioRecorder audioRecorder = new AudioRecorder(settings, identifier, step, outputDirectory); + audioRecorder.setAudioRecorderBroadcastInterval(timeBetweenAverageSampleBroadcasts); + return audioRecorder; + } + + public AudioRecorderSettings getSettings() { + return settings; + } + + public void setSettings(AudioRecorderSettings settings) { + this.settings = settings; + } + + public long getTimeBetweenAverageSampleBroadcasts() { + return timeBetweenAverageSampleBroadcasts; + } + + public void setTimeBetweenAverageSampleBroadcasts(long timeBetweenAverageSampleBroadcasts) { + this.timeBetweenAverageSampleBroadcasts = timeBetweenAverageSampleBroadcasts; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AudioRecorderSettings.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AudioRecorderSettings.java new file mode 100644 index 000000000..b33b3403b --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/AudioRecorderSettings.java @@ -0,0 +1,151 @@ +package org.researchstack.backbone.step.active.recorder; + +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; + +import java.io.Serializable; + +/** + * Created by TheMDP on 2/26/17. + * + * This class encapsulates the different audio recording settings that can be used + * with the AudioRecorder class + */ + +public class AudioRecorderSettings implements Serializable { + + /** + * The default sample rates ordered in priority from highest sample rates to lowest + */ + public static final int[] DEFAULT_SAMPLE_RATES = new int[] { 44100, 22050, 16000, 11025, 8000 }; + /** + * On Android, not all sample rates are available for all devices, + * so it is customary to provide a range of sample rates to choose from + */ + private int[] possibleSampleRate; + + public static final int AUDIO_CHANNELS_MONO = 1; + public static final int AUDIO_CHANNELS_STEREO = 2; + /** + * 1 for MONO, 2 for STEREO + */ + private int audioChannels; + + /** + * Can be any value in MediaRecorder.OutputFormat + */ + private int outputFormat; + + /** + * Can be any value in MediaRecorder.AudioSource + */ + private int audioSource; + + /** + * Can be any value in MediaRecorder.AudioEncoder + */ + private int audioEncoder; + + /** Default constructor used for serialization/deserialization */ + AudioRecorderSettings() { + super(); + } + + /** + * Create the settings that the audio recorder will use while recording audio + * + * @param audioSource The audio source value in MediaRecorder.AudioSource + * @param audioEncoder The audio source value in MediaRecorder.AudioEncoder + * @param outputFormat The audio source value in MediaRecorder.OutputFormat + * @param audioChannels The number of audio channels, use + * AUDIO_CHANNELS_MONO or AUDIO_CHANNELS_STEREO + * @param possibleSampleRates An array of possible sample rates that you desire, ordered by priority + * See DEFAULT_SAMPLE_RATES as an example + * On Android, not all sample rates are available for all devices, + * so it is customary to provide a range of sample rates to choose from + */ + public AudioRecorderSettings( + int audioSource, + int audioEncoder, + int outputFormat, + int audioChannels, + int[] possibleSampleRates) + { + this.audioSource = audioSource; + this.audioEncoder = audioEncoder; + this.outputFormat = outputFormat; + this.audioChannels = audioChannels; + this.possibleSampleRate = possibleSampleRates; + } + + /** + * @return default settings for recording audio, which is... + * MediaRecorder.AudioSource.MIC + * MediaRecorder.AudioEncoder.AAC + * MediaRecorder.OutputFormat.MPEG_4 + * AUDIO_CHANNELS_STEREO + * DEFAULT_SAMPLE_RATES - first priority 44.1k, 22k, 16k, 11k, 8k + */ + public static AudioRecorderSettings defaultSettings() { + return new AudioRecorderSettings( + MediaRecorder.AudioSource.MIC, + MediaRecorder.AudioEncoder.AAC, + MediaRecorder.OutputFormat.MPEG_4, + AUDIO_CHANNELS_STEREO, + DEFAULT_SAMPLE_RATES); + } + + private static int getDesiredSampleRate(int[] sampleRates) { + // add the rates you wish to check against, sticking with the main one + for (int rate : sampleRates) { + int bufferSize = AudioRecord.getMinBufferSize(rate, + AudioFormat.CHANNEL_IN_DEFAULT, AudioFormat.ENCODING_PCM_16BIT); + if (bufferSize > 0) { + // buffer size is valid, Sample rate supported + return rate; + } + } + return sampleRates[sampleRates.length-1]; + } + + public int getAudioChannels() { + return audioChannels; + } + + public void setAudioChannels(int audioChannels) { + this.audioChannels = audioChannels; + } + + public int getOutputFormat() { + return outputFormat; + } + + public void setOutputFormat(int outputFormat) { + this.outputFormat = outputFormat; + } + + public int getAudioSource() { + return audioSource; + } + + public void setAudioSource(int audioSource) { + this.audioSource = audioSource; + } + + public int getAudioEncoder() { + return audioEncoder; + } + + public void setAudioEncoder(int audioEncoder) { + this.audioEncoder = audioEncoder; + } + + public int getSampleRate() { + return getDesiredSampleRate(possibleSampleRate); + } + + public void setSampleRate(int[] possibleSampleRates) { + this.possibleSampleRate = possibleSampleRates; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/DeviceMotionRecorder.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/DeviceMotionRecorder.java new file mode 100644 index 000000000..bf0bed98c --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/DeviceMotionRecorder.java @@ -0,0 +1,340 @@ +package org.researchstack.backbone.step.active.recorder; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorManager; +import android.os.Build; +import androidx.annotation.VisibleForTesting; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.gson.JsonObject; + +import org.researchstack.backbone.step.Step; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Created by TheMDP on 2/5/17. + * + * The DeviceMotionRecorder incorporates a bunch of sensor fusion sensor readings + * together to paint a broad picture of the device's orientation and movement over time. + * + * This class is an attempt at recording data in a similar way as iOS' device motion recorder. + * + * @see + * Sensor values + * @see Sensor Types + * @see + * Position Sensors + * @see + * Motion Sensors + */ +public class DeviceMotionRecorder extends SensorRecorder { + private static final Logger logger = LoggerFactory.getLogger(DeviceMotionRecorder.class); + + public static final float GRAVITY_SI_CONVERSION = SensorManager.GRAVITY_EARTH; + + public static final String SENSOR_DATA_TYPE_KEY = "sensorType"; + public static final String SENSOR_DATA_SUBTYPE_KEY = "sensorAndroidType"; + public static final String SENSOR_EVENT_ACCURACY_KEY = "eventAccuracy"; + + public static final Map SENSOR_TYPE_TO_DATA_TYPE; + public static final Set ROTATION_VECTOR_TYPES; + + public static final String ROTATION_REFERENCE_COORDINATE_KEY = "referenceCoordinate"; + + static { + // build mapping for sensor type and its data type value + ImmutableMap.Builder sensorTypeMapBuilder = ImmutableMap.builder(); + // rotation/gyroscope + sensorTypeMapBuilder.put(Sensor.TYPE_GYROSCOPE, "rotationRate"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + sensorTypeMapBuilder.put(Sensor.TYPE_GYROSCOPE_UNCALIBRATED, "rotationRateUncalibrated"); + } + + // accelerometer + sensorTypeMapBuilder.put(Sensor.TYPE_ACCELEROMETER, "acceleration"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + sensorTypeMapBuilder.put( + Sensor.TYPE_ACCELEROMETER_UNCALIBRATED, "accelerationUncalibrated"); + } + + // gravity + sensorTypeMapBuilder.put(Sensor.TYPE_GRAVITY, "gravity"); + + // acceleration without gravity + sensorTypeMapBuilder.put(Sensor.TYPE_LINEAR_ACCELERATION, "userAcceleration"); + + // magnetic field + sensorTypeMapBuilder.put(Sensor.TYPE_MAGNETIC_FIELD, "magneticField"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + sensorTypeMapBuilder.put( + Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED, "magneticFieldUncalibrated"); + } + + // attitude + sensorTypeMapBuilder.put(Sensor.TYPE_ROTATION_VECTOR, "attitude"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + sensorTypeMapBuilder.put(Sensor.TYPE_GAME_ROTATION_VECTOR, "attitude"); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + sensorTypeMapBuilder.put(Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR, "attitude"); + } + SENSOR_TYPE_TO_DATA_TYPE = sensorTypeMapBuilder.build(); + + // build mappint for rotation type + ImmutableSet.Builder rotationTypeBuilder =ImmutableSet.builder(); + rotationTypeBuilder.add(Sensor.TYPE_ROTATION_VECTOR); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + rotationTypeBuilder.add(Sensor.TYPE_GAME_ROTATION_VECTOR); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + rotationTypeBuilder.add(Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR); + } + ROTATION_VECTOR_TYPES = rotationTypeBuilder.build(); + } + + public static final String X_KEY = "x"; + public static final String Y_KEY = "y"; + public static final String Z_KEY = "z"; + public static final String W_KEY = "w"; + public static final String ACCURACY_KEY = "estimatedAccuracy"; + + public static final String X_UNCALIBRATED_KEY = "xUncalibrated"; + public static final String Y_UNCALIBRATED_KEY = "yUncalibrated"; + public static final String Z_UNCALIBRATED_KEY = "zUncalibrated"; + public static final String X_BIAS_KEY = "xBias"; + public static final String Y_BIAS_KEY = "yBias"; + public static final String Z_BIAS_KEY = "zBias"; + + DeviceMotionRecorder(double frequency, String identifier, Step step, File outputDirectory) { + super(frequency, identifier, step, outputDirectory); + } + + @Override + public void start(Context context) { + super.start(context); + } + + @Override + protected List getSensorTypeList(List availableSensorList) { + List sensorTypeList = new ArrayList<>(); + + // Only add these sensors if the device has them + if (hasAvailableType(availableSensorList, Sensor.TYPE_ACCELEROMETER)) { + sensorTypeList.add(Sensor.TYPE_ACCELEROMETER); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + && hasAvailableType(availableSensorList, Sensor.TYPE_ACCELEROMETER_UNCALIBRATED)) { + sensorTypeList.add(Sensor.TYPE_ACCELEROMETER_UNCALIBRATED); + } + + if (hasAvailableType(availableSensorList, Sensor.TYPE_GRAVITY)) { + sensorTypeList.add(Sensor.TYPE_GRAVITY); + } + + if (hasAvailableType(availableSensorList, Sensor.TYPE_LINEAR_ACCELERATION)) { + sensorTypeList.add(Sensor.TYPE_LINEAR_ACCELERATION); + } + + if (hasAvailableType(availableSensorList, Sensor.TYPE_GYROSCOPE)) { + sensorTypeList.add(Sensor.TYPE_GYROSCOPE); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 + && hasAvailableType(availableSensorList, Sensor.TYPE_GYROSCOPE_UNCALIBRATED)) { + sensorTypeList.add(Sensor.TYPE_GYROSCOPE_UNCALIBRATED); + } + + if (hasAvailableType(availableSensorList, Sensor.TYPE_MAGNETIC_FIELD)) { + sensorTypeList.add(Sensor.TYPE_MAGNETIC_FIELD); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 + && hasAvailableType(availableSensorList, Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED)) { + sensorTypeList.add(Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED); + } + + if (hasAvailableType(availableSensorList, Sensor.TYPE_ROTATION_VECTOR)) { + sensorTypeList.add(Sensor.TYPE_ROTATION_VECTOR); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (hasAvailableType(availableSensorList, Sensor.TYPE_GAME_ROTATION_VECTOR)) { + sensorTypeList.add(Sensor.TYPE_GAME_ROTATION_VECTOR); + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (hasAvailableType(availableSensorList, Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR)) { + sensorTypeList.add(Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR); + } + } + + return sensorTypeList; + } + + + @Override + public void recordSensorEvent(SensorEvent sensorEvent, JsonObject jsonObject) { + int sensorType = sensorEvent.sensor.getType(); + String sensorTypeKey = SENSOR_TYPE_TO_DATA_TYPE.get(sensorType); + + if (Strings.isNullOrEmpty(sensorTypeKey)) { + logger.warn("Unable find type key for sensor type: " + + sensorType); + return; + } + + jsonObject.addProperty(SENSOR_DATA_TYPE_KEY, sensorTypeKey); + jsonObject.addProperty(SENSOR_EVENT_ACCURACY_KEY, sensorEvent.accuracy); + + switch (sensorType) { + case Sensor.TYPE_ACCELEROMETER: + recordAccelerometerEvent(sensorEvent, jsonObject); + break; + case Sensor.TYPE_GRAVITY: + recordGravityEvent(sensorEvent, jsonObject); + break; + case Sensor.TYPE_LINEAR_ACCELERATION: + recordLinearAccelerometerEvent(sensorEvent, jsonObject); + break; + case Sensor.TYPE_GYROSCOPE: + recordGyroscope(sensorEvent, jsonObject); + break; + case Sensor.TYPE_MAGNETIC_FIELD: + recordMagneticField(sensorEvent, jsonObject); + break; + case Sensor.TYPE_GYROSCOPE_UNCALIBRATED: + case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED: + case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED: + recordUncalibrated(sensorEvent, jsonObject); + break; + case Sensor.TYPE_GAME_ROTATION_VECTOR: + case Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR: + case Sensor.TYPE_ROTATION_VECTOR: + recordRotationVector(sensorEvent, jsonObject); + break; + default: + logger.warn("Unable to record sensor type: " + sensorType); + } + } + /** + * @see + * Sensor Types: Accelerometer + */ + @VisibleForTesting + void recordAccelerometerEvent(SensorEvent sensorEvent, JsonObject jsonObject) { + jsonObject.addProperty(X_KEY, sensorEvent.values[0] / GRAVITY_SI_CONVERSION); + jsonObject.addProperty(Y_KEY, sensorEvent.values[1] / GRAVITY_SI_CONVERSION); + jsonObject.addProperty(Z_KEY, sensorEvent.values[2] / GRAVITY_SI_CONVERSION); + } + + /** + * @see + * Sensor Types: Accelerometer + */ + @VisibleForTesting + void recordLinearAccelerometerEvent(SensorEvent sensorEvent, JsonObject jsonObject) { + // acceleration = gravity + linear-acceleration + jsonObject.addProperty(X_KEY, sensorEvent.values[0] / GRAVITY_SI_CONVERSION); + jsonObject.addProperty(Y_KEY, sensorEvent.values[1] / GRAVITY_SI_CONVERSION); + jsonObject.addProperty(Z_KEY, sensorEvent.values[2] / GRAVITY_SI_CONVERSION); + } + + /** + * Direction and magnitude of gravity. + * @see + * Sensor Types: Gravity + */ + @VisibleForTesting + void recordGravityEvent(SensorEvent sensorEvent, JsonObject jsonObject) { + jsonObject.addProperty(X_KEY, sensorEvent.values[0] / GRAVITY_SI_CONVERSION); + jsonObject.addProperty(Y_KEY, sensorEvent.values[1] / GRAVITY_SI_CONVERSION); + jsonObject.addProperty(Z_KEY, sensorEvent.values[2] / GRAVITY_SI_CONVERSION); + } + /** + * Sensor.TYPE_ROTATION_VECTOR relative to East-North-Up coordinate frame. + * Sensor.TYPE_GAME_ROTATION_VECTOR no magnetometer + * Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR similar to a rotation vector sensor but using a + * magnetometer and no gyroscope + * + * @see + * https://source.android.com/devices/sensors/sensor-types#rotation_vector + * https://source.android.com/devices/sensors/sensor-types#game_rotation_vector + * https://source.android.com/devices/sensors/sensor-types#geomagnetic_rotation_vector + */ + @VisibleForTesting + void recordRotationVector(SensorEvent sensorEvent, JsonObject jsonObject) { + // indicate android sensor subtype + int sensorType = sensorEvent.sensor.getType(); + if (Sensor.TYPE_ROTATION_VECTOR == sensorType) { + jsonObject.addProperty(SENSOR_DATA_SUBTYPE_KEY, "rotationVector"); + jsonObject.addProperty(ROTATION_REFERENCE_COORDINATE_KEY, "East-Up-North"); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 + && Sensor.TYPE_GAME_ROTATION_VECTOR == sensorType) { + jsonObject.addProperty(SENSOR_DATA_SUBTYPE_KEY, "gameRotationVector"); + jsonObject.addProperty(ROTATION_REFERENCE_COORDINATE_KEY, "zUp"); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + && Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR == sensorType) { + jsonObject.addProperty(SENSOR_DATA_SUBTYPE_KEY, "geomagneticRotationVector"); + jsonObject.addProperty(ROTATION_REFERENCE_COORDINATE_KEY, "East-Up-North"); + } + + // x = rot_axis.y * sin(theta/2) + jsonObject.addProperty(X_KEY, sensorEvent.values[0]); + // y = rot_axis.y * sin(theta/2) + jsonObject.addProperty(Y_KEY, sensorEvent.values[1]); + // z = rot_axis.z * sin(theta/2) + jsonObject.addProperty(Z_KEY, sensorEvent.values[2]); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + // w = cos(theta/2) + jsonObject.addProperty(W_KEY, sensorEvent.values[3]); + + // game rotation vector never provides accuracy, always returns zero + if (Sensor.TYPE_GAME_ROTATION_VECTOR!= sensorType) { + // estimated accuracy in radians, or -1 if unavailable + jsonObject.addProperty(ACCURACY_KEY, sensorEvent.values[4]); + } + } else if (sensorEvent.values.length > 3) { + // this value was optional before SDK Level 18 + // w = cos(theta/2) + jsonObject.addProperty(W_KEY, sensorEvent.values[3]); + } + } + + void recordGyroscope(SensorEvent sensorEvent, JsonObject jsonObject) { + jsonObject.addProperty(X_KEY, sensorEvent.values[0]); + jsonObject.addProperty(Y_KEY, sensorEvent.values[1]); + jsonObject.addProperty(Z_KEY, sensorEvent.values[2]); + } + + // used for uncalibrated gyroscope, uncalibrated accelerometer, and uncalibrated magnetic field + void recordUncalibrated(SensorEvent sensorEvent, JsonObject jsonObject) { + // conceptually: _uncalibrated = _calibrated + _bias. + jsonObject.addProperty(X_UNCALIBRATED_KEY, sensorEvent.values[0]); + jsonObject.addProperty(Y_UNCALIBRATED_KEY, sensorEvent.values[1]); + jsonObject.addProperty(Z_UNCALIBRATED_KEY, sensorEvent.values[2]); + + jsonObject.addProperty(X_BIAS_KEY, sensorEvent.values[3]); + jsonObject.addProperty(Y_BIAS_KEY, sensorEvent.values[4]); + jsonObject.addProperty(Z_BIAS_KEY, sensorEvent.values[5]); + } + + void recordMagneticField(SensorEvent sensorEvent, JsonObject jsonObject) { + jsonObject.addProperty(X_KEY, sensorEvent.values[0]); + jsonObject.addProperty(Y_KEY, sensorEvent.values[1]); + jsonObject.addProperty(Z_KEY, sensorEvent.values[2]); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int i) { + // no-op + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/DeviceMotionRecorderConfig.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/DeviceMotionRecorderConfig.java new file mode 100644 index 000000000..06831ecef --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/DeviceMotionRecorderConfig.java @@ -0,0 +1,40 @@ +package org.researchstack.backbone.step.active.recorder; + +import org.researchstack.backbone.step.Step; + +import java.io.File; + +/** + * Created by TheMDP on 2/5/17. + */ + +public class DeviceMotionRecorderConfig extends RecorderConfig { + + /** + * The frequency of sensor data collection in samples per second (Hz). + */ + private double frequency; + + /** Default constructor used for serialization/deserialization */ + DeviceMotionRecorderConfig() { + super(); + } + + public DeviceMotionRecorderConfig(String identifier, double frequency) { + super(identifier); + this.frequency = frequency; + } + + @Override + public Recorder recorderForStep(Step step, File outputDirectory) { + return new DeviceMotionRecorder(frequency, getIdentifier(), step, outputDirectory); + } + + public double getFrequency() { + return frequency; + } + + public void setFrequency(double frequency) { + this.frequency = frequency; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/JsonArrayDataRecorder.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/JsonArrayDataRecorder.java new file mode 100644 index 000000000..4c2b9eccb --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/JsonArrayDataRecorder.java @@ -0,0 +1,87 @@ +package org.researchstack.backbone.step.active.recorder; + +import com.google.gson.JsonObject; + +import org.researchstack.backbone.result.FileResult; +import org.researchstack.backbone.result.logger.DataLogger; +import org.researchstack.backbone.step.Step; + +import java.io.File; +import java.util.Date; + +/** + * Created by TheMDP on 2/7/17. + * + * The JsonArrayDataRecorder class is set up to be able to save a JsonArray to a DataLogger file + * It coordinates the file header and footer of "[" and "]" and injects a separator "," + * in between individual json object writes, so that the format of the file is correct + */ + +public abstract class JsonArrayDataRecorder extends Recorder { + + public static final String JSON_ITEMS_KEY = "items"; + public static final String JSON_MIME_CONTENT_TYPE = "application/json"; + public static final String JSON_FILE_SUFFIX = ".json"; + public static final String JSON_OBJECT_SEPARATOR = ","; + + protected boolean isFirstJsonObject; + + protected DataLogger dataLogger; + protected File dataLoggerFile; + + protected long startTime; + protected long endTime; + + public JsonArrayDataRecorder(String identifier, Step step, File outputDirectory) { + super(identifier, step, outputDirectory); + } + + @Override + public void cancel() { + if (dataLogger != null) { + dataLogger.cancel(); + } + } + + public void startJsonDataLogging() { + if (dataLoggerFile == null) { + dataLoggerFile = new File(getOutputDirectory(), uniqueFilename + JSON_FILE_SUFFIX); + dataLogger = new DataLogger(dataLoggerFile, new DataLogger.DataWriteListener() { + @Override + public void onWriteError(Throwable throwable) { + getRecorderListener().onFail(JsonArrayDataRecorder.this, throwable); + } + + @Override + public void onWriteComplete(File file) { + FileResult fileResult = new FileResult(fileResultIdentifier(), dataLoggerFile, JSON_MIME_CONTENT_TYPE); + fileResult.setContentType(JSON_MIME_CONTENT_TYPE); + fileResult.setStartDate(new Date(startTime)); + fileResult.setEndDate(new Date(endTime)); + getRecorderListener().onComplete(JsonArrayDataRecorder.this, fileResult); + } + }); + } + + setRecording(true); + startTime = System.currentTimeMillis(); + + // Since we are writing a JsonArray, have the header and footer be + dataLogger.start("[", "]"); + isFirstJsonObject = true; // will avoid comma separator on write object write + } + + public void stopJsonDataLogging() { + setRecording(false); + endTime = System.currentTimeMillis(); + dataLogger.stop(); + } + + public void writeJsonObjectToFile(JsonObject jsonObject) { + // append optional comma for array separation + String jsonString = (!isFirstJsonObject ? JSON_OBJECT_SEPARATOR : "") + + jsonObject.toString(); + dataLogger.appendData(jsonString); + isFirstJsonObject = false; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/LocationRecorder.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/LocationRecorder.java new file mode 100644 index 000000000..138c70fe6 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/LocationRecorder.java @@ -0,0 +1,344 @@ +package org.researchstack.backbone.step.active.recorder; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Build; +import android.os.Bundle; +import android.os.SystemClock; +import android.util.Log; + +import com.google.gson.JsonObject; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.utils.FormatHelper; + +import java.io.File; +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.util.Locale; + +import static android.content.Context.MODE_PRIVATE; + +/** + * Since not all apps using this SDK require this LocationRecorder functionality, + * we don't include it in the manifest and hence also suppress missing permissions warnings in the compiler + */ +@SuppressWarnings({"MissingPermission"}) +public class LocationRecorder extends JsonArrayDataRecorder implements LocationListener { + + private static final String TAG = LocationRecorder.class.getSimpleName(); + + private static final String SHARED_PREFS_KEY = "LocationRecorder"; + private static final String LAST_RECORDED_DIST_KEY = "LastRecordedTotalDistance"; + + public static final String COORDINATE_KEY = "coordinate"; + public static final String LONGITUDE_KEY = "longitude"; + public static final String LATITUDE_KEY = "latitude"; + public static final String ALTITUDE_KEY = "altitude"; + public static final String ACCURACY_KEY = "accuracy"; + public static final String COURSE_KEY = "course"; + public static final String RELATIVE_LATITUDE_KEY = "relativeLatitude"; + public static final String RELATIVE_LONGITUDE_KEY = "relativeLongitude"; + public static final String SPEED_KEY = "speed"; + public static final String TIMESTAMP_DATE_KEY = "timestampDate"; + public static final String TIMESTAMP_IN_SECONDS_KEY = "timestamp"; + public static final String UPTIME_IN_SECONDS_KEY = "uptime"; + + private JsonObject jsonObject; + private JsonObject coordinateJsonObject; + + private LocationManager locationManager = null; + private final long minTime; + private final float minDistance; + private final boolean usesRelativeCoordinates; + + private double totalDistance; + private Location firstLocation; + private Location lastLocation; + private long startTimeNanosSinceBoot; + + public static final String BROADCAST_LOCATION_UPDATE_ACTION = "LocationRecorder_BroadcastLocationUpdate"; + private static final String BROADCAST_LOCATION_UPDATE_KEY = "LocationUpdate"; + + private SharedPreferences locationRecorderPrefs; + + public static float getLastRecordedTotalDistance(Context context) { + SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_KEY, MODE_PRIVATE); + return prefs.getFloat(LAST_RECORDED_DIST_KEY, 0f); + } + + protected void setLastRecordedTotalDistance(float totalDistance) { + if (locationRecorderPrefs == null) { + return; + } + locationRecorderPrefs.edit().putFloat(LAST_RECORDED_DIST_KEY, totalDistance).apply(); + } + + /** + * @param minTime per Android doc, minimum time interval between location updates, in milliseconds + * @param minDistance per Android doc, minimum distance between location updates, in meters, no minimum if zero + * @param identifier the recorder's identifier + * @param step the step that contains this recorder + * @param outputDirectory the output directory of the file that will be written with location data + */ + LocationRecorder( + long minTime, float minDistance, boolean usesRelativeCoordinates, String identifier, + Step step, File outputDirectory) { + super(identifier, step, outputDirectory); + this.minTime = minTime; + this.minDistance = minDistance; + this.usesRelativeCoordinates = usesRelativeCoordinates; + } + + public long getMinTime() { + return minTime; + } + + public float getMinDistance() { + return minDistance; + } + + /** + * If this is set to true, the recorder will produce relative GPS coordinates, using the + * user's initial position as zero in the relative coordinate system. If this is set to + * false, the recorder will produce absolute GPS coordinates. + */ + public boolean getUsesRelativeCoordinates() { + return usesRelativeCoordinates; + } + + @Override + public void start(Context context) { + firstLocation = null; + lastLocation = null; + totalDistance = 0; + locationRecorderPrefs = context.getSharedPreferences(SHARED_PREFS_KEY, MODE_PRIVATE); + + if (locationManager == null) { + locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + } + + if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + if (getRecorderListener() != null) { + String errorMsg = context.getString(R.string.rsb_system_feature_gps_text); + getRecorderListener().onFail(this, new IllegalStateException(errorMsg)); + } + return; + } + + // In-app permissions were added in Android 6.0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PackageManager pm = context.getPackageManager(); + int hasPerm = pm.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION, context.getPackageName()); + if (hasPerm != PackageManager.PERMISSION_GRANTED) { + if (getRecorderListener() != null) { + String errorMsg = context.getString(R.string.rsb_permission_location_desc); + getRecorderListener().onFail(this, new IllegalStateException(errorMsg)); + } + return; + } + } + + // In Android, you can register for both network and gps location updates + // Let's just register for both and log all locations to the data file + // with their corresponding accuracy and other data associated + + try { + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, minTime, minDistance, this); + } catch (java.lang.SecurityException ex) { + Log.i(TAG, "fail to request location update, ignore", ex); + } catch (IllegalArgumentException ex) { + Log.d(TAG, "gps provider does not exist " + ex.getMessage()); + } + + coordinateJsonObject = new JsonObject(); + jsonObject = new JsonObject(); + startJsonDataLogging(); + } + + /** + * Ignore the missing permission, since user's of this SDK don't always need it, + * unless they are specifically doing a walking task test + */ + @Override + public void stop() { + if (locationManager != null) { + try { + locationManager.removeUpdates(this); + } catch (Exception ex) { + Log.i(TAG, "fail to remove location listners, ignore", ex); + } + } + stopJsonDataLogging(); + } + + // LocationListener methods + + @Override + public void onLocationChanged(Location location) { + if (location != null) { + if (firstLocation == null) { + // Initialize first location + firstLocation = location; + } + + // getElapsedReatimeNanos() is long nanoseconds since system boot time. + long locationNanos = getElapsedNanosSinceBootFromLocation(location); + if (startTimeNanosSinceBoot == 0) { + // Initialize start time. + startTimeNanosSinceBoot = locationNanos; + + // Add timestamp date, which is the ISO timestamp representing the activity start time. + // Location.getTime() is always epoch milliseconds, so we can use as is. + jsonObject.addProperty(TIMESTAMP_DATE_KEY, new SimpleDateFormat(FormatHelper.DATE_FORMAT_ISO_8601, + Locale.getDefault()).format(location.getTime())); + } else if (jsonObject.has(TIMESTAMP_DATE_KEY)) { + // Because we re-use the jsonObject, we need to clear the timestamp date key after the first iteration. + jsonObject.remove(TIMESTAMP_DATE_KEY); + } + + // Timestamps + // timestamp is seconds since start of the activity (locationNanos minus startTimeNanos, divided by a billion). + // uptime is a monotonically increasing timestamp in seconds, with any arbitrary zero. (We use + // getElapsedRealtimeNanos(), divided by a billion.) + jsonObject.addProperty(TIMESTAMP_IN_SECONDS_KEY, (locationNanos - startTimeNanosSinceBoot) * 1e-9); + jsonObject.addProperty(UPTIME_IN_SECONDS_KEY, locationNanos * 1e-9); + + // GPS coordinates + if (usesRelativeCoordinates) { + // Subtract from the firstLocation to get relative coordinates. + double relativeLatitude = location.getLatitude() - firstLocation.getLatitude(); + double relativeLongitude = location.getLongitude() - firstLocation.getLongitude(); + coordinateJsonObject.addProperty(RELATIVE_LATITUDE_KEY, relativeLatitude); + coordinateJsonObject.addProperty(RELATIVE_LONGITUDE_KEY, relativeLongitude); + } else { + // Use absolute coordinates given by the location. + coordinateJsonObject.addProperty(LONGITUDE_KEY, location.getLongitude()); + coordinateJsonObject.addProperty(LATITUDE_KEY, location.getLatitude()); + } + jsonObject.add(COORDINATE_KEY, coordinateJsonObject); + + if (location.hasAccuracy()) { + jsonObject.addProperty(ACCURACY_KEY, location.getAccuracy()); + } + + if (location.hasSpeed()) { + jsonObject.addProperty(SPEED_KEY, location.getSpeed()); + } + + if (location.hasAltitude()) { + jsonObject.addProperty(ALTITUDE_KEY, location.getAltitude()); + } + + if (location.hasBearing()) { + jsonObject.addProperty(COURSE_KEY, location.getBearing()); + } + + writeJsonObjectToFile(jsonObject); + + if (lastLocation != null) { + totalDistance += lastLocation.distanceTo(location); + } + sendLocationUpdateBroadcast( + location.getLongitude(), location.getLatitude(), totalDistance); + + lastLocation = location; + } + } + + // Wrapper method which encapsulates getting the elapsed realtime nanos, or falls back to elasped realtime (millis) + // for older OS versions. + // Package-scoped so this can be mocked for unit tests. + long getElapsedNanosSinceBootFromLocation(Location location) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return location.getElapsedRealtimeNanos(); + } else { + return (long) (SystemClock.elapsedRealtime() * 1e6); // millis to nanos + } + } + + protected void sendLocationUpdateBroadcast(double longitude, double latitude, double distance) { + setLastRecordedTotalDistance((float)distance); + LocationUpdateHolder locationHolder = new LocationUpdateHolder(); + locationHolder.setLongitude(longitude); + locationHolder.setLatitude(latitude); + locationHolder.setTotalDistance(distance); + Bundle bundle = new Bundle(); + bundle.putSerializable(BROADCAST_LOCATION_UPDATE_KEY, locationHolder); + Intent intent = new Intent(BROADCAST_LOCATION_UPDATE_ACTION); + intent.putExtras(bundle); + sendBroadcast(intent); + } + + /** + * @param intent must have action of BROADCAST_LOCATION_UPDATE_ACTION + * @return the LocationUpdateHolder contained in the broadcast + */ + public static LocationUpdateHolder getLocationUpdateHolder(Intent intent) { + if (intent.getAction() == null || + !intent.getAction().equals(BROADCAST_LOCATION_UPDATE_ACTION) || + intent.getExtras() == null || + intent.getExtras().containsKey(BROADCAST_LOCATION_UPDATE_KEY)) { + return null; + } + return (LocationUpdateHolder) intent.getExtras() + .getSerializable(BROADCAST_LOCATION_UPDATE_KEY); + } + + @Override + public void onStatusChanged(String s, int i, Bundle bundle) { + Log.i(TAG, s); + } + + @Override + public void onProviderEnabled(String s) { + Log.i(TAG, "onProviderEnabled " + s); + } + + @Override + public void onProviderDisabled(String s) { + Log.i(TAG, "onProviderDisabled " + s); + } + + public static class LocationUpdateHolder implements Serializable { + private double longitude; + private double latitude; + private double totalDistance; + + public LocationUpdateHolder() { + super(); + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getTotalDistance() { + return totalDistance; + } + + public void setTotalDistance(double distance) { + this.totalDistance = distance; + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/LocationRecorderConfig.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/LocationRecorderConfig.java new file mode 100644 index 000000000..cec05396a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/LocationRecorderConfig.java @@ -0,0 +1,87 @@ +package org.researchstack.backbone.step.active.recorder; + +import org.researchstack.backbone.step.Step; + +import java.io.File; + +public class LocationRecorderConfig extends RecorderConfig { + public static final long DEFAULT_MIN_TIME = 100; // 100 milliseconds minimal time change + public static final long DEFAULT_LOCATION_DISTANCE = 0; // no min distance + public static final boolean DEFAULT_USES_RELATIVE_COORDINATE = false; // default to absolute coordinates + + private long minTime; + private float minDistance; + private boolean usesRelativeCoordinates; + + /** Default constructor used for serialization/deserialization */ + LocationRecorderConfig() { + super(); + } + + public LocationRecorderConfig(String identifier) { + this(identifier, DEFAULT_MIN_TIME, DEFAULT_LOCATION_DISTANCE); + } + + /** + * @param minTime per Android doc, minimum time interval between location updates, in milliseconds + * @param minDistance per Android doc, minimum distance between location updates, in meters, no minimum if zero + * @param identifier the recorder's identifier + */ + public LocationRecorderConfig(String identifier, long minTime, float minDistance) { + this(identifier, minTime, minDistance, DEFAULT_USES_RELATIVE_COORDINATE); + } + + /** Private constructor, for use with builder. */ + private LocationRecorderConfig( + String identifier, long minTime, float minDistance, boolean usesRelativeCoordinates) { + super(identifier); + this.minTime = minTime; + this.minDistance = minDistance; + this.usesRelativeCoordinates = usesRelativeCoordinates; + } + + @Override + public Recorder recorderForStep(Step step, File outputDirectory) { + return new LocationRecorder(minTime, minDistance, usesRelativeCoordinates, getIdentifier(), + step, outputDirectory); + } + + /** LocationRecorderConfig builder. */ + public static class Builder { + private String identifier; + private long minTime = DEFAULT_MIN_TIME; + private float minDistance = DEFAULT_LOCATION_DISTANCE; + private boolean usesRelativeCoordinates = DEFAULT_USES_RELATIVE_COORDINATE; + + public Builder withIdentifier(String identifier) { + this.identifier = identifier; + return this; + } + + public Builder withMinTime(long minTime) { + this.minTime = minTime; + return this; + } + + public Builder withMinDistance(float minDistance) { + this.minDistance = minDistance; + return this; + } + + /** + * If this is set to true, the recorder will produce relative GPS coordinates, using the + * user's initial position as zero in the relative coordinate system. If this is set to + * false, the recorder will produce absolute GPS coordinates. + */ + public Builder withUsesRelativeCoordinates(boolean usesRelativeCoordinates) { + this.usesRelativeCoordinates = usesRelativeCoordinates; + return this; + } + + /** Builds the LocationRecorderConfig. */ + public LocationRecorderConfig build() { + return new LocationRecorderConfig(identifier, minTime, minDistance, + usesRelativeCoordinates); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/PedometerRecorder.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/PedometerRecorder.java new file mode 100644 index 000000000..dadd9ebcb --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/PedometerRecorder.java @@ -0,0 +1,190 @@ +package org.researchstack.backbone.step.active.recorder; + +import android.content.Context; +import android.content.Intent; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.MainThread; + +import com.google.gson.JsonObject; + +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.model.User; +import org.researchstack.backbone.model.UserHealth; +import org.researchstack.backbone.step.Step; + +import java.io.File; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +/** + * Created by TheMDP on 2/15/17. + */ + +public class PedometerRecorder extends SensorRecorder + implements AccelerometerStepDetector.OnStepTakenListener +{ + public static final String TIMESTAMP_KEY = "timestamp"; + public static final String END_DATE = "endDate"; + public static final String NUMBER_OF_STEPS = "numberOfSteps"; + public static final String DISTANCE = "totalDistance"; + + public static final String BROADCAST_PEDOMETER_UPDATE_ACTION = "LocationRecorder_BroadcastPedometerUpdate"; + private static final String BROADCAST_PEDOMETER_UPDATE_KEY = "PedometerUpdate"; + + /** + * This used to compute the totalDistance the user has traveled while recording the pedometer + * The default value is about half a meter, it will only be used when a user's height is unavailable + */ + public static final float DEFAULT_METERS_PER_STRIDE = 0.6f; // in meters + + /** + * This factor, when multiplied by a user's height, will determine an average stride length + */ + public static final float HEIGHT_FACTOR_FOR_STRIDE_LENGTH_MALE = 0.413f; + public static final float HEIGHT_FACTOR_FOR_STRIDE_LENGTH_FEMALE = 0.415f; + + private boolean useAccelerometerDetector; + private AccelerometerStepDetector accelerometerStepDetector; + private float strideLength; // in meters + + private int stepCounter; + private JsonObject jsonObject; + + PedometerRecorder(String identifier, Step step, File outputDirectory) { + super(MANUAL_JSON_FREQUENCY, identifier, step, outputDirectory); + } + + @Override + protected List getSensorTypeList(List availableSensorList) { + // Step detector is only available for OS kitkat and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // Not all devices even have the pedometer sensor + // I haven't found a full list yet but here are some that have it + // HTC One M8, Nexus 5x, Nexus 6p, Samsung S6 and S7 + if (hasAvailableType(availableSensorList, Sensor.TYPE_STEP_DETECTOR)) { + useAccelerometerDetector = false; + return Collections.singletonList(Sensor.TYPE_STEP_DETECTOR); + } + } + // do a custom pedometer algorithm + useAccelerometerDetector = true; + return Collections.singletonList(Sensor.TYPE_ACCELEROMETER); + } + + @Override + public void start(Context context) { + super.start(context); + stepCounter = 0; + jsonObject = new JsonObject(); + if (useAccelerometerDetector) { + accelerometerStepDetector = new AccelerometerStepDetector(); + accelerometerStepDetector.setOnStepTakenListener(this); + } + strideLength = computeStrideLength(context); + } + + /** + * @param context used to obtain the User's health data + * @return attempts to use the user's height and gender to compute an accurate average stride length + * default stride length used if the health data is not available + */ + private float computeStrideLength(Context context) { + float computedStride = DEFAULT_METERS_PER_STRIDE; + User user = DataProvider.getInstance().getUser(context); + if (user != null) { + UserHealth userHealth = user.getUserHealth(); + if (userHealth != null && userHealth.hasHeight()) { + float heightFactor = HEIGHT_FACTOR_FOR_STRIDE_LENGTH_FEMALE; + if (userHealth.getGender() == UserHealth.Gender.MALE) { + heightFactor = HEIGHT_FACTOR_FOR_STRIDE_LENGTH_MALE; + } + + computedStride = userHealth.getHeight() * heightFactor; + } + } + return computedStride; + } + + @MainThread + @Override + public void onStepTaken() { + stepCounter++; + jsonObject.addProperty(TIMESTAMP_KEY, System.currentTimeMillis()); + jsonObject.addProperty(END_DATE, System.currentTimeMillis()); + jsonObject.addProperty(NUMBER_OF_STEPS, stepCounter); + float distance = strideLength * stepCounter; + jsonObject.addProperty(DISTANCE, distance); + super.writeJsonObjectToFile(jsonObject); + sendPedometerUpdateBroadcast(stepCounter, distance); + } + + + @Override + public void recordSensorEvent(SensorEvent sensorEvent, JsonObject object) { + // Step detector is only available for OS kitkat and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && + sensorEvent.sensor.getType() == Sensor.TYPE_STEP_DETECTOR) + { + onStepTaken(); + } else if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { + accelerometerStepDetector.processAccelerometerData(sensorEvent); + } + //TODO: fix accelerometer data + } + + @Override + public void onAccuracyChanged(Sensor sensor, int i) { + // NO-OP + } + + protected void sendPedometerUpdateBroadcast(int stepCount, float totalDistance) { + PedometerUpdateHolder dataHolder = new PedometerUpdateHolder(); + dataHolder.setStepCount(stepCount); + dataHolder.setTotalDistance(totalDistance); + Bundle bundle = new Bundle(); + bundle.putSerializable(BROADCAST_PEDOMETER_UPDATE_KEY, dataHolder); + Intent intent = new Intent(BROADCAST_PEDOMETER_UPDATE_ACTION); + intent.putExtras(bundle); + sendBroadcast(intent); + } + + /** + * @param intent must have action of BROADCAST_PEDOMETER_UPDATE_ACTION + * @return the PedometerUpdateHolder contained in the broadcast + */ + public static PedometerUpdateHolder getPedometerUpdateHolder(Intent intent) { + if (intent.getAction() == null || + !intent.getAction().equals(BROADCAST_PEDOMETER_UPDATE_ACTION) || + intent.getExtras() == null || + intent.getExtras().containsKey(BROADCAST_PEDOMETER_UPDATE_KEY)) { + return null; + } + return (PedometerUpdateHolder) intent.getExtras() + .getSerializable(BROADCAST_PEDOMETER_UPDATE_KEY); + } + + public static class PedometerUpdateHolder implements Serializable { + private int stepCount; + private float totalDistance; + + public int getStepCount() { + return stepCount; + } + + public void setStepCount(int stepCount) { + this.stepCount = stepCount; + } + + public float getTotalDistance() { + return totalDistance; + } + + public void setTotalDistance(float totalDistance) { + this.totalDistance = totalDistance; + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/PedometerRecorderConfig.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/PedometerRecorderConfig.java new file mode 100644 index 000000000..a341ae6ee --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/PedometerRecorderConfig.java @@ -0,0 +1,26 @@ +package org.researchstack.backbone.step.active.recorder; + +import org.researchstack.backbone.step.Step; + +import java.io.File; + +/** + * Created by TheMDP on 2/15/17. + */ + +public class PedometerRecorderConfig extends RecorderConfig { + + /** Default constructor used for serialization/deserialization */ + PedometerRecorderConfig() { + super(); + } + + public PedometerRecorderConfig(String identifier) { + super(identifier); + } + + @Override + public Recorder recorderForStep(Step step, File outputDirectory) { + return new PedometerRecorder(getIdentifier(), step, outputDirectory); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/Recorder.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/Recorder.java new file mode 100644 index 000000000..5ee34b913 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/Recorder.java @@ -0,0 +1,213 @@ +package org.researchstack.backbone.step.active.recorder; + +import android.content.Context; +import android.content.Intent; +import androidx.annotation.MainThread; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.researchstack.backbone.result.Result; +import org.researchstack.backbone.step.Step; + +import java.io.File; +import java.util.UUID; + +/** + * Created by TheMDP on 2/5/17. + * + * A recorder is the runtime companion to an `RecorderConfiguration` object, and is + * usually generated by one. + * + * During active tasks, it is often useful to collect one or more pieces of data + * from sensors on the device. In research tasks, it's not always + * necessary to display that data, but it's important to record it in a controlled manner. + * + * An active step (`ActiveStep`) has an array of recorder configurations + * (`RecorderConfiguration`) that identify the types of data it needs to record + * for the duration of the step. When a step starts, the active step layout + * instantiates a recorder for each of the step's recorder configurations. + * The step layout starts the recorder when the active step is started, and stops the + * recorder when the active step is finished. + * + * The results of recording are typically written to a file specified by the value of the `outputDirectory` property. + * + * Usually, the `ActiveStepLayout` object is the recorder's delegate, and it + * receives callbacks when errors occur or when recording is complete. + */ + +public abstract class Recorder { + /** + * A short string that uniquely identifies the recorder (usually assigned by the recorder configuration). + * + * The identifier is reproduced in the results of a recorder created from this configuration. + * In fact, the only way to link a result + * (an `FileResult` object) to the recorder that generated it is to look at the value of + * `identifier`. To accurately identify recorder results, you need to ensure that recorder identifiers + * are unique within each step. + * + * In some cases, it can be useful to link the recorder identifier to a unique identifier in a + * database; in other cases, it can make sense to make the identifier human + * readable. + */ + private String identifier; + + /** + * The step that produced this recorder, configured during initialization. + */ + private Step step; + + /** + * The configuration that produced this recorder. + */ + private RecorderConfig config; + + /** + * The file URL of the output directory configured during initialization. + * + * Typically, you set the `outputDirectory` property for the `ViewTaskActivity` object + * before presenting the task. + */ + private File outputDirectory; + + /** + * A Boolean value indicating whether the recorder is currently recording. + * @return `true` if the recorder is recording; otherwise, `false`. + */ + private boolean isRecording; + + /** + * Used to communicate with the listener if the recording completed successfully or failed + */ + private RecorderListener recorderListener; + + /** + * a unique filename for this Recorder + */ + protected String uniqueFilename; + + /** Default constructor for serialization/deserialization */ + Recorder() { + super(); + } + + protected Recorder(String identifier, Step step, File outputDirectory) { + super(); + setIdentifier(identifier); + setStep(step); + setOutputDirectory(outputDirectory); + uniqueFilename = generateUniqueFileName(); + } + + /** + * Starts data recording. + * + * If an error occurs when recording starts, it is returned through the delegate. + * + * @param context can be app or activity, used for starting sensor + */ + @MainThread + public abstract void start(Context context); + + /** + * Stops data recording, which generally triggers the return of results. + * + * If an error occurs when stopping the recorder, it is returned through the delegate. + * Subclasses should call `finishRecordingWithError:` rather than calling super. + */ + @MainThread + public abstract void stop(); + + /** + * A cancel will cause this recorder to be immediately stopped, + * and the file it was writing will be deleted, + * + * Also, no callback will be invoked + */ + @MainThread + public abstract void cancel(); + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public boolean isRecording() { + return isRecording; + } + + protected void setRecording(boolean recording) { + isRecording = recording; + } + + public RecorderConfig getConfig() { + return config; + } + + protected void setConfig(RecorderConfig config) { + this.config = config; + } + + public Step getStep() { + return step; + } + + public void setStep(Step step) { + this.step = step; + } + + public File getOutputDirectory() { + return outputDirectory; + } + + public void setOutputDirectory(File outputDirectory) { + this.outputDirectory = outputDirectory; + } + + public RecorderListener getRecorderListener() { + return recorderListener; + } + + public void setRecorderListener(RecorderListener recorderListener) { + this.recorderListener = recorderListener; + } + + protected void onRecorderCompleted(Result result) { + if (recorderListener != null) { + recorderListener.onComplete(this, result); + } + } + + protected void onRecorderFailed(String error) { + if (recorderListener != null) { + recorderListener.onFail(this, new Throwable(error)); + } + } + + protected void onRecorderFailed(Throwable throwable) { + if (recorderListener != null) { + recorderListener.onFail(this, throwable); + } + } + + protected void sendBroadcast(Intent intent) { + if (recorderListener != null) { + Context context = recorderListener.getBroadcastContext(); + if (context != null) { + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + } + } + } + + private String generateUniqueFileName() { + return UUID.randomUUID().toString(); + } + + /** + * @return a step-specific identifier that can be used for the FileResult + */ + public String fileResultIdentifier() { + return identifier + "_" + step.getIdentifier(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/RecorderConfig.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/RecorderConfig.java new file mode 100644 index 000000000..3c3e22b9f --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/RecorderConfig.java @@ -0,0 +1,72 @@ +package org.researchstack.backbone.step.active.recorder; + +import org.researchstack.backbone.step.Step; + +import java.io.File; +import java.io.Serializable; + +/** + * Created by TheMDP on 2/4/17. + * + * /** + * The `RecorderConfig` class is the abstract base class for recorder configurations + * that can be attached to an active step (`ActiveStep`). + * + * Recorder configurations provide an easy way to collect + * sensor data into a serialized format during the duration of an active step. + * If you want to filter or process the data in real time, it is better to + * use the existing APIs directly. + * + * To use a recorder, include its configuration in the `recorderConfigurations` property + * of an `ORKActiveStep` object, include that step in a task, and present it with + * a 'ViewTaskActivity'. + * + * To add a new recorder, subclass both `RecorderConfig` and `Recorder`, + * and add the new `RecorderConfig` subclass to an `ActiveStep` object. + */ + +public abstract class RecorderConfig implements Serializable { + + /** + * A short string that uniquely identifies the recorder configuration within the step. + * + * The identifier is reproduced in the results of a recorder created from this configuration. + * In fact, the only way to link a result (an `FileResult` object) to the recorder + * that generated it is to look at the value of `identifier`. + * To accurately identify recorder results, you need to ensure that recorder identifiers + * are unique within each step. + * + * In some cases, it can be useful to link the recorder identifier to a unique identifier in a + * database; in other cases, it can make sense to make the identifier human + * readable. + */ + protected String identifier; + + /** Default constructor used for serialization/deserialization */ + public RecorderConfig() { + super(); + } + + public RecorderConfig(String identifier) { + super(); + this.identifier = identifier; + } + + /** + * Returns a recorder instance using this configuration. + * + * @param step The step for which this recorder is being created. + * @param outputDirectory The directory in which all output file data should be written (if producing `FileResult` instances). + * + * @return A configured recorder instance. + */ + public abstract Recorder recorderForStep(Step step, File outputDirectory); + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/RecorderListener.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/RecorderListener.java new file mode 100644 index 000000000..980c16aeb --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/RecorderListener.java @@ -0,0 +1,41 @@ +package org.researchstack.backbone.step.active.recorder; + +import android.content.Context; +import androidx.annotation.Nullable; + +import org.researchstack.backbone.result.Result; + +/** + * Created by TheMDP on 2/5/17. + * + * The `RecorderListener` interface defines methods that the delegate of an `Recorder` object + * should use to handle errors and log the completed results. + * + * This interface is implemented by `ActiveStepLayout`; your app should not need to implement it. + */ + +public interface RecorderListener { + /** + * Tells the listener that the recorder has completed with the specified result. + * Typically, this method is called once when recording is stopped. + * + * @param recorder The generating recorder object. + * @param result The generated result. + */ + void onComplete(Recorder recorder, Result result); + + /** + * Tells the listener that recording failed. + * Typically, this method is called once when the error occurred. + * + * @param recorder The generating recorder object. + * @param error The error that occurred. + */ + void onFail(Recorder recorder, Throwable error); + + /** + * @return a valid Context for the recorder to broadcast status, null if not available + */ + @Nullable + Context getBroadcastContext(); +} diff --git a/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/SensorRecorder.java b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/SensorRecorder.java new file mode 100644 index 000000000..0b6e51cd4 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/step/active/recorder/SensorRecorder.java @@ -0,0 +1,198 @@ +package org.researchstack.backbone.step.active.recorder; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Build; +import android.os.SystemClock; + +import com.google.gson.JsonObject; + +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.utils.FormatHelper; +import org.researchstack.backbone.utils.LogExt; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * Created by TheMDP on 2/7/17. + * + * The SensorRecorder is an abstract class that greatly reduces the amount of work required + * to write sensor data to a DataLogger json file + * + * Any Android sensor is compatible with this class as long as you correctly implement + * the two abstract methods, getSensorTypeList, and writeJsonData + */ + +abstract class SensorRecorder extends JsonArrayDataRecorder implements SensorEventListener { + + public static final float MANUAL_JSON_FREQUENCY = -1.0f; + + private static final long MICRO_SECONDS_PER_SEC = 1000000L; + + public static final String TIMESTAMP_IN_SECONDS_KEY = "timestamp"; + public static final String UPTIME_IN_SECONDS_KEY = "uptime"; + public static final String TIMESTAMP_DATE_KEY = "timestampDate"; + + /** + * The frequency of the sensor data collection in samples per second (Hz). + * Android Sensors do not allow exact frequency specifications, per their documentation, + * it is only a HINT, so we must manage it ourselves in a posted runnable with delay + */ + private double frequency; + + private SensorManager sensorManager; + private List sensorList; + + private long timestampZeroReferenceNanos = 0; + + SensorRecorder(double frequency, String identifier, Step step, File outputDirectory) { + super(identifier, step, outputDirectory); + this.frequency = frequency; + } + + /** + * @param availableSensorList the list of available sensors for the user's device + * @return a list of sensor types that should be listened to + * for example, if you only want accelerometer, you would return + * Collections.singletonList(Sensor.TYPE_ACCELEROMETER) + */ + protected abstract List getSensorTypeList(List availableSensorList); + + @Override + public void start(Context context) { + sensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + + sensorList = new ArrayList<>(); + List availableSensorList = sensorManager.getSensorList(Sensor.TYPE_ALL); + boolean anySucceeded = false; + for (int sensorType : getSensorTypeList(availableSensorList)) { + Sensor sensor = sensorManager.getDefaultSensor(sensorType); + if (sensor != null) { + sensorList.add(sensor); + boolean success; + if (isManualFrequency()) { + success = sensorManager.registerListener( + this, sensor, SensorManager.SENSOR_DELAY_FASTEST); + } else { + success = sensorManager.registerListener(this, sensor, + calculateDelayBetweenSamplesInMicroSeconds()); + } + anySucceeded |= success; + + if (!success) { + LogExt.i(SensorRecorder.class, "Failed to register sensor: " + sensor); + } + } + } + + if (!anySucceeded) { + super.onRecorderFailed("Failed to initialize any sensor"); + } else { + super.startJsonDataLogging(); + } + } + + @Override + public final void onSensorChanged(SensorEvent sensorEvent) { + + JsonObject jsonObject = new JsonObject(); + + if (timestampZeroReferenceNanos <= 0) { + // set timestamp reference, which timestamps are measured relative to + timestampZeroReferenceNanos = sensorEvent.timestamp; + + // record date equivalent of timestamp reference + long uptimeNanos; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + uptimeNanos = SystemClock.elapsedRealtimeNanos(); + } else { + uptimeNanos = (long) (SystemClock.elapsedRealtime() * 1e6); // millis to nanos + } + + long timestampReferenceMillis = System.currentTimeMillis() + + (long) ((timestampZeroReferenceNanos - uptimeNanos) * 1e-6d); + Date timestampReferenceDate = new Date(timestampReferenceMillis); + jsonObject.addProperty(TIMESTAMP_DATE_KEY, + new SimpleDateFormat(FormatHelper.DATE_FORMAT_ISO_8601, Locale.getDefault()) + .format(timestampReferenceDate)); + } + + // these values are doubles + jsonObject.addProperty(TIMESTAMP_IN_SECONDS_KEY, + (sensorEvent.timestamp - timestampZeroReferenceNanos) * 1e-9); + jsonObject.addProperty(UPTIME_IN_SECONDS_KEY, sensorEvent.timestamp * 1e-9); + + recordSensorEvent(sensorEvent, jsonObject); + + writeJsonObjectToFile(jsonObject); + } + + /*** + * This method receives a SensorEvent and a JsonObject and is expected to update the + * JsonObject with data to be written. + * @param sensorEvent + * @param object json object pre-populated with uptime and timestamp + */ + public abstract void recordSensorEvent(final SensorEvent sensorEvent, + final JsonObject object); + + @Override + public void stop() { + for (Sensor sensor : sensorList) { + sensorManager.unregisterListener(this, sensor); + } + stopJsonDataLogging(); + } + + @Override + public void cancel() { + super.cancel(); + for (Sensor sensor : sensorList) { + sensorManager.unregisterListener(this, sensor); + } + } + + /** + * @param availableSensorList the list of available sensors + * @param sensorType the sensor type to check if it is contained in the list + * @return true if that sensor type is available, false if it is not + */ + protected boolean hasAvailableType(List availableSensorList, int sensorType) { + for (Sensor sensor : availableSensorList) { + if (sensor.getType() == sensorType) { + return true; + } + } + return false; + } + + + protected int calculateDelayBetweenSamplesInMicroSeconds() { + return (int)((float)MICRO_SECONDS_PER_SEC / frequency); + } + + /** + * @return true if sensor frequency does not exist, and callbacks will be based on an event, like Step Detection + * false if the sensor frequency will come back at a desired frequency + */ + protected boolean isManualFrequency() { + return frequency < 0; + } + + public double getFrequency() { + return frequency; + } + + public void setFrequency(double frequency) { + this.frequency = frequency; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/storage/file/FileAccess.java b/backbone/src/main/java/org/researchstack/backbone/storage/file/FileAccess.java index 41945cf75..89b72c045 100644 --- a/backbone/src/main/java/org/researchstack/backbone/storage/file/FileAccess.java +++ b/backbone/src/main/java/org/researchstack/backbone/storage/file/FileAccess.java @@ -45,13 +45,14 @@ public interface FileAccess { * * @param context Can be Application context, but we'll be careful not to store, so don't worry too much. * @param path Path relative to the implementation's root store. Must start with '/'. No relative paths. + * @return The read Byte array data. */ byte[] readData(Context context, String path); /** - * @param context - * @param fromPath - * @param toPath + * @param context Can be Application context, but we'll be careful not to store, so don't worry too much. + * @param fromPath The current path relative to the implementation's root store. Must start with '/'. No relative paths. + * @param toPath The new path relative to the implementation's root store. Must start with '/'. No relative paths. */ void moveData(Context context, String fromPath, String toPath); diff --git a/backbone/src/main/java/org/researchstack/backbone/storage/file/KeystoreEncryptionHelper.java b/backbone/src/main/java/org/researchstack/backbone/storage/file/KeystoreEncryptionHelper.java new file mode 100644 index 000000000..e96cb73ac --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/storage/file/KeystoreEncryptionHelper.java @@ -0,0 +1,219 @@ +package org.researchstack.backbone.storage.file; + +import android.annotation.TargetApi; +import android.content.Context; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProperties; +import androidx.core.hardware.fingerprint.FingerprintManagerCompat; +import android.util.Base64; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +/** + * Created by TheMDP on 2/3/17. + * + * The KeystoreEncryptionHelper class aids in communicating with the Android Keystore + * This class can create a new keystore key, and use that to encrypt and decrypt unique secrets + * + * Currently, the keystore keys that are generated require user authentication with a + * fingerprint or the credential alert (password), and will throw exceptions if the user is not + * authenticated. However, that is up to the class that uses this class to make sure the user is authenticated + */ + +@TargetApi(android.os.Build.VERSION_CODES.M) // api 23, or Android 6.0 +public final class KeystoreEncryptionHelper { + + /** Reference to Android Key Store */ + private static final String ANDROID_KEYSTORE = "AndroidKeyStore"; + + /** Encryption type used for key generation */ + private static final String AES_MODE = + KeyProperties.KEY_ALGORITHM_AES + "/" + + KeyProperties.BLOCK_MODE_CBC + "/" + + KeyProperties.ENCRYPTION_PADDING_PKCS7; + + private static final String UTF_8 = "UTF-8"; + + /** Used when generating a random pincode */ + private static final int RANDOM_PINCODE_LENGTH = 32; + + /** + * @param fingerprintManager that has been initialized + * @return true if this device supports fingerprint, false otherwise + */ + public static boolean isFingerprintAuthAvailable(FingerprintManagerCompat fingerprintManager) { + return fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints(); + } + + /** + * Creates a symmetric key in the Android Key Store, which can only be used after the user has + * authenticated with fingerprint or password + * @param keyName the name of the key in the Android Keystore + */ + private static void createKey(String keyName) { + // The enrolling flow for fingerprint. This is where you ask the user to set up fingerprint + // for your flow. Use of keys is necessary if you need to know if the set of + // enrolled fingerprints has changed. + try { + KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE); + keyGenerator.init( + new KeyGenParameterSpec.Builder(keyName, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + // we know our pincode is securely random, since we generate it ourselves, + // so we do not need an Initialization Vector (IV) + .setRandomizedEncryptionRequired(false) + .setUserAuthenticationRequired(true) + .build()); + keyGenerator.generateKey(); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchProviderException e) { + throw new RuntimeException(e); + } + } + + /** + * This method will cause a crash and put the app in a bad state if a key with "keyName" has + * already been created + * + * @param keyName the key name to create and setup with the cipher + * @return {@code true} if initialization is successful, {@code false} if the lock screen has + * been disabled or reset after the key was generated, or if a fingerprint got enrolled after + * the key was generated + */ + public static Cipher initCipherForEncryption(String keyName) { + try { + // We need a new key generated for this encryption + createKey(keyName); + + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); + keyStore.load(null); + SecretKey key = (SecretKey) keyStore.getKey(keyName, null); + Cipher cipher = Cipher.getInstance(AES_MODE); + cipher.init(Cipher.ENCRYPT_MODE, key); + + return cipher; + } catch (KeyPermanentlyInvalidatedException e) { + // At this point the user has either removed the lock screen security, + // or they have added or changed the fingerprints they were previously using + return null; + } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException + | NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException e) { + throw new RuntimeException("Failed to init Cipher", e); + } + } + + /** + * @param context used to retrieve the Initialization Vector (IV) that was used to encrypt + * @param keyName the Android Keystore key name + * @param encryptedBase64Secret the encrypted secret that the cipher will be decrpting + * @param iv the Initialization Vector(IV) used in encryption, it is responsibility of the + * class that uses this to save the encryptor's IV, and feed it back here when decrypting + * It can be stored in SharedPreferences or wherever, it doesn't really matter + * @return {@code true} if initialization is successful, {@code false} if the lock screen has + * been disabled or reset after the key was generated, or if a fingerprint got enrolled after + * the key was generated + */ + public static Cipher initCipherForDecryption(Context context, String keyName, String encryptedBase64Secret, byte[] iv) { + try { + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); + keyStore.load(null); + SecretKey key = (SecretKey) keyStore.getKey(keyName, null); + Cipher cipher = Cipher.getInstance(AES_MODE); + + // Here we need to load the initialization vector that was used to encrypt the data + cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); + + return cipher; + } catch (KeyPermanentlyInvalidatedException e) { + // At this point the user has either removed the lock screen security, + // or they have added or changed the fingerprints they were previously using + return null; + } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException + | NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | + InvalidAlgorithmParameterException | NullPointerException e) { + return null; + } + } + + /** + * @param context can be android or app, used to store Initialization Vector (IV) in SharedPrefs + * @param cipher must be initialized and the user must be authenticated using fingerprint or password + * @param secret the secret to encrypt using the cipher + * @return a base 64 encoded String version of the securely encrypted secret + */ + public static String encryptSecret(Context context, Cipher cipher, String secret) { + byte[] encodedBytes; + try { + byte[] decodedBytes = secret.getBytes(UTF_8); + encodedBytes = cipher.doFinal(decodedBytes); + } catch (UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException e) { + throw new RuntimeException("Failed to do encryption", e); + } + String encryptedBase64EncodedSecret = Base64.encodeToString(encodedBytes, Base64.DEFAULT); + return encryptedBase64EncodedSecret; + } + + /** + * @return random string secret with length 32 + */ + public static String generateSecureRandomPin() { + // The AndroidKeyStore can be used to help secure sensitive data, + // but it doesn't actually store the sensitive data, so we need to + // generate and store the encrypted secret ourselves + RandomStringGenerator pinGenerator = new RandomStringGenerator(); + return pinGenerator.randomString(RANDOM_PINCODE_LENGTH); + } + + /** + * This method only works if your cipher has been initialized properly to decrypt, + * and the user has authenticated, using their fingerprint, or password + * + * @param cipher an initialized cipher with IV with mode DECRYPT + * @param encryptedSecret the secret we are trying to decrypt + * @return the decrypted secret + */ + public static String decryptPin(Cipher cipher, String encryptedSecret) { + try { + byte[] encrypted = Base64.decode(encryptedSecret, Base64.DEFAULT); + byte[] decodedBytes = cipher.doFinal(encrypted); + return new String(decodedBytes, UTF_8); + } catch (IOException | BadPaddingException | IllegalBlockSizeException e) { + throw new RuntimeException("Failed to do decryption " + e); + } + } + + /** + * Generates a securely random string using the characters in AB at whatever length is requested + */ + private static final class RandomStringGenerator { + static final String AB = "%+\\/'!#$^?:,.(){}[]~-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + SecureRandom rnd = new SecureRandom(); + + String randomString( int len ) { + StringBuilder sb = new StringBuilder( len ); + for( int i = 0; i < len; i++ ) + sb.append( AB.charAt( rnd.nextInt(AB.length()) ) ); + return sb.toString(); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/storage/file/PinCodeConfig.java b/backbone/src/main/java/org/researchstack/backbone/storage/file/PinCodeConfig.java index b3a4be7d5..954f0e290 100644 --- a/backbone/src/main/java/org/researchstack/backbone/storage/file/PinCodeConfig.java +++ b/backbone/src/main/java/org/researchstack/backbone/storage/file/PinCodeConfig.java @@ -1,5 +1,4 @@ package org.researchstack.backbone.storage.file; - import android.text.InputFilter; import android.text.InputType; import android.text.format.DateUtils; @@ -11,25 +10,145 @@ * This class allows you to customize the type/strength of the pin code that the user must create to * protect their data. */ -public class PinCodeConfig { - private static final String DIGITS_NUMERIC = "1234567890"; - private static final String DIGITS_ALPHABETIC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +public class PinCodeConfig +{ + private static final String DIGITS_NUMERIC = "1234567890"; + private static final String DIGITS_ALPHABETIC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; private static final String DIGITS_ALPHANUMERIC = DIGITS_ALPHABETIC + DIGITS_NUMERIC; + + /** + * The interface that the {@link PinCodeType} implements. Since you cannot extend an enum to add + * your own types, implement this interface as an alternative if you need your own PinCodeType. + */ + public interface Type + { + /** + * Returns the {@link InputType} that should be applied to the EditText during pincode + * entry. + * + * @return the input type for the EditText + */ + int getInputType(); + + /** + * Returns the id for the string resource representing the input character type. + *

    + * This will be inserted into the instructions for creating a pincode. For example, if you + * return 'digit', it will tell the user to enter a '4-digit code'. If you return 'letter', + * it will say '4-letter code'. + * + * @return the resource id for the string representing the input character type + */ + int getInputTypeStringId(); + + /** + * Returns the {@link InputFilter} for the EditText. Use this to limit the types of + * characters that may be used in the pin code type. + * + * @return the input filter + */ + InputFilter getInputFilter(); + + /** + * Returns the {@link InputType} that should be applied to the EditText based on whether the + * text is visible or not. + * + * @param visible a boolean indicating whether the text is visible or not + * + * @return the input type for the EditText + */ + int getVisibleVariationType(boolean visible); + } + + /** + * General {@link Type}s that should cover most desired pin code configs: Alpha, Numeric, and + * Alphanumberic. + */ + public enum PinCodeType implements Type + { + Alphabetic(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS, + InputType.TYPE_TEXT_VARIATION_NORMAL, + InputType.TYPE_TEXT_VARIATION_PASSWORD, + new TextUtils.AlphabeticFilter()), + + Numeric(InputType.TYPE_CLASS_NUMBER, + InputType.TYPE_NUMBER_VARIATION_NORMAL, + InputType.TYPE_NUMBER_VARIATION_PASSWORD, + new TextUtils.NumericFilter()), + + AlphaNumeric(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS, + InputType.TYPE_TEXT_VARIATION_NORMAL, + InputType.TYPE_TEXT_VARIATION_PASSWORD, + new TextUtils.AlphanumericFilter()); + + private int inputType; + private int inputTypeVisible; + private int inputTypeHidden; + private InputFilter filter; + + PinCodeType(int inputType, int inputTypeVisible, int inputTypeHidden, InputFilter filter) + { + this.inputType = inputType; + this.inputTypeVisible = inputTypeVisible; + this.inputTypeHidden = inputTypeHidden; + this.filter = filter; + } + + @Override + public int getInputType() + { + return inputType; + } + + @Override + public int getInputTypeStringId() + { + if(this == PinCodeType.Numeric) + { + return R.string.rsb_pincode_enter_digit; + } + else if(this == PinCodeType.Alphabetic) + { + return R.string.rsb_pincode_enter_letter; + } + else + { + return R.string.rsb_pincode_enter_character; + } + } + + @Override + public InputFilter getInputFilter() + { + return filter; + } + + @Override + public int getVisibleVariationType(boolean visible) + { + return visible ? inputTypeVisible : inputTypeHidden; + } + } + private Type type; private long autoLockTime; - private int length; + private int length; + /** * Constructs the default pin config, 4 digits and 5 minute lockout time */ - public PinCodeConfig() { + public PinCodeConfig() + { this(5 * DateUtils.MINUTE_IN_MILLIS); } + /** * Constructs a pin config with 4 digits and the provided lockout time * * @param autoLockTime the time before the user must re-enter their pin */ - public PinCodeConfig(long autoLockTime) { + public PinCodeConfig(long autoLockTime) + { this(PinCodeType.Numeric, 4, autoLockTime); } @@ -40,7 +159,8 @@ public PinCodeConfig(long autoLockTime) { * @param length the character length of the pin code * @param autoLockTime the time before the user must re-enter their pin */ - public PinCodeConfig(Type type, int length, long autoLockTime) { + public PinCodeConfig(Type type, int length, long autoLockTime) + { this.type = type; this.length = length; this.autoLockTime = autoLockTime; @@ -51,7 +171,8 @@ public PinCodeConfig(Type type, int length, long autoLockTime) { * * @return the pin type */ - public Type getPinType() { + public Type getPinType() + { return type; } @@ -60,7 +181,8 @@ public Type getPinType() { * * @return the pin length */ - public int getPinLength() { + public int getPinLength() + { return length; } @@ -70,7 +192,8 @@ public int getPinLength() { * * @return the lockout time */ - public long getPinAutoLockTime() { + public long getPinAutoLockTime() + { return autoLockTime; } @@ -82,107 +205,8 @@ public long getPinAutoLockTime() { * * @param pinAutoLockTime the lockout time */ - public void setPinAutoLockTime(long pinAutoLockTime) { + public void setPinAutoLockTime(long pinAutoLockTime) + { this.autoLockTime = pinAutoLockTime; } - - /** - * General {@link Type}s that should cover most desired pin code configs: Alpha, Numeric, and - * Alphanumberic. - */ - public enum PinCodeType implements Type { - Alphabetic(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS, - InputType.TYPE_TEXT_VARIATION_NORMAL, - InputType.TYPE_TEXT_VARIATION_PASSWORD, - new TextUtils.AlphabeticFilter()), - - Numeric(InputType.TYPE_CLASS_NUMBER, - InputType.TYPE_NUMBER_VARIATION_NORMAL, - InputType.TYPE_NUMBER_VARIATION_PASSWORD, - new TextUtils.NumericFilter()), - - AlphaNumeric(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS, - InputType.TYPE_TEXT_VARIATION_NORMAL, - InputType.TYPE_TEXT_VARIATION_PASSWORD, - new TextUtils.AlphanumericFilter()); - - private int inputType; - private int inputTypeVisible; - private int inputTypeHidden; - private InputFilter filter; - - PinCodeType(int inputType, int inputTypeVisible, int inputTypeHidden, InputFilter filter) { - this.inputType = inputType; - this.inputTypeVisible = inputTypeVisible; - this.inputTypeHidden = inputTypeHidden; - this.filter = filter; - } - - @Override - public int getInputType() { - return inputType; - } - - @Override - public int getInputTypeStringId() { - if (this == PinCodeType.Numeric) { - return R.string.rsb_pincode_enter_digit; - } else if (this == PinCodeType.Alphabetic) { - return R.string.rsb_pincode_enter_letter; - } else { - return R.string.rsb_pincode_enter_character; - } - } - - @Override - public InputFilter getInputFilter() { - return filter; - } - - @Override - public int getVisibleVariationType(boolean visible) { - return visible ? inputTypeVisible : inputTypeHidden; - } - } - - /** - * The interface that the {@link PinCodeType} implements. Since you cannot extend an enum to add - * your own types, implement this interface as an alternative if you need your own PinCodeType. - */ - public interface Type { - /** - * Returns the {@link InputType} that should be applied to the EditText during pincode - * entry. - * - * @return the input type for the EditText - */ - int getInputType(); - - /** - * Returns the id for the string resource representing the input character type. - *

    - * This will be inserted into the instructions for creating a pincode. For example, if you - * return 'digit', it will tell the user to enter a '4-digit code'. If you return 'letter', - * it will say '4-letter code'. - * - * @return the resource id for the string representing the input character type - */ - int getInputTypeStringId(); - - /** - * Returns the {@link InputFilter} for the EditText. Use this to limit the types of - * characters that may be used in the pin code type. - * - * @return the input filter - */ - InputFilter getInputFilter(); - - /** - * Returns the {@link InputType} that should be applied to the EditText based on whether the - * text is visible or not. - * - * @return the input type for the EditText - */ - int getVisibleVariationType(boolean visible); - } } diff --git a/backbone/src/main/java/org/researchstack/backbone/storage/file/SimpleFileAccess.java b/backbone/src/main/java/org/researchstack/backbone/storage/file/SimpleFileAccess.java index 550da5e85..9ce03d262 100644 --- a/backbone/src/main/java/org/researchstack/backbone/storage/file/SimpleFileAccess.java +++ b/backbone/src/main/java/org/researchstack/backbone/storage/file/SimpleFileAccess.java @@ -1,8 +1,8 @@ package org.researchstack.backbone.storage.file; import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.WorkerThread; +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; import org.researchstack.backbone.storage.file.aes.Encrypter; import org.researchstack.backbone.utils.FileUtils; diff --git a/backbone/src/main/java/org/researchstack/backbone/storage/file/aes/Encrypter.java b/backbone/src/main/java/org/researchstack/backbone/storage/file/aes/Encrypter.java index f62d147ce..65c098fbb 100644 --- a/backbone/src/main/java/org/researchstack/backbone/storage/file/aes/Encrypter.java +++ b/backbone/src/main/java/org/researchstack/backbone/storage/file/aes/Encrypter.java @@ -12,7 +12,7 @@ public interface Encrypter { * * @param data the byte array of data to be encrypted * @return the encrypted data - * @throws GeneralSecurityException + * @throws GeneralSecurityException if an exception occurs */ byte[] encrypt(byte[] data) throws GeneralSecurityException; @@ -21,7 +21,7 @@ public interface Encrypter { * * @param data the byte array of data to be decrypted * @return the decrypted data - * @throws GeneralSecurityException + * @throws GeneralSecurityException if an exception occurs */ byte[] decrypt(byte[] data) throws GeneralSecurityException; diff --git a/backbone/src/main/java/org/researchstack/backbone/storage/file/aes/PinProtectedProvider.java b/backbone/src/main/java/org/researchstack/backbone/storage/file/aes/PinProtectedProvider.java index ed94960af..088d4996d 100644 --- a/backbone/src/main/java/org/researchstack/backbone/storage/file/aes/PinProtectedProvider.java +++ b/backbone/src/main/java/org/researchstack/backbone/storage/file/aes/PinProtectedProvider.java @@ -1,7 +1,7 @@ package org.researchstack.backbone.storage.file.aes; import android.content.Context; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import com.tozny.crypto.android.AesCbcWithIntegrity; diff --git a/skin/src/main/java/org/researchstack/skin/task/ConsentTask.java b/backbone/src/main/java/org/researchstack/backbone/task/ConsentTask.java similarity index 95% rename from skin/src/main/java/org/researchstack/skin/task/ConsentTask.java rename to backbone/src/main/java/org/researchstack/backbone/task/ConsentTask.java index 34ecb7fbc..e136af9fb 100644 --- a/skin/src/main/java/org/researchstack/skin/task/ConsentTask.java +++ b/backbone/src/main/java/org/researchstack/backbone/task/ConsentTask.java @@ -1,4 +1,4 @@ -package org.researchstack.skin.task; +package org.researchstack.backbone.task; import android.content.Context; import android.content.res.Resources; @@ -22,21 +22,26 @@ import org.researchstack.backbone.step.FormStep; import org.researchstack.backbone.step.QuestionStep; import org.researchstack.backbone.step.Step; -import org.researchstack.backbone.task.OrderedTask; import org.researchstack.backbone.ui.step.layout.ConsentSignatureStepLayout; import org.researchstack.backbone.utils.LogExt; import org.researchstack.backbone.utils.TextUtils; -import org.researchstack.skin.R; -import org.researchstack.skin.ResourceManager; -import org.researchstack.skin.model.ConsentQuizModel; -import org.researchstack.skin.model.ConsentSectionModel; -import org.researchstack.skin.step.ConsentQuizEvaluationStep; -import org.researchstack.skin.step.ConsentQuizQuestionStep; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ResourceManager; +import org.researchstack.backbone.model.ConsentQuizModel; +import org.researchstack.backbone.model.ConsentSectionModel; +import org.researchstack.backbone.step.ConsentQuizEvaluationStep; +import org.researchstack.backbone.step.ConsentQuizQuestionStep; import java.util.ArrayList; import java.util.Calendar; import java.util.List; +@Deprecated +/** + * use OnboardingManager.getInstance().launchOnboarding(context, TaskType.REGISTRATION); + * use OnboardingManager.getInstance().launchOnboarding(context, TaskType.LOGIN); or... + * use OnboardingManager.getInstance().launchOnboarding(context, TaskType.RECONSENT); + */ public class ConsentTask extends OrderedTask { public static final String ID_VISUAL = "ID_VISUAL"; public static final String ID_FIRST_QUESTION = "question_1"; @@ -63,7 +68,7 @@ public static ConsentTask create(Context context, String taskId) { .getConsentSections() .create(context); - String participant = r.getString(R.string.rss_participant); + String participant = r.getString(R.string.rsb_participant); ConsentSignature signature = new ConsentSignature("participant", participant, null); DocumentProperties properties = data.getDocumentProperties(); @@ -150,7 +155,7 @@ private static void initVisualSteps(Context ctx, ConsentDocument doc, List section.setHtmlContent(ResourceManager.getResourceAsString(ctx, htmlFilePath)); } - ConsentVisualStep step = new ConsentVisualStep("consent_" + i); + ConsentVisualStep step = new ConsentVisualStep("consent_" + i, i, doc.getSections().size()); step.setSection(section); String nextString = ctx.getString(R.string.rsb_next); diff --git a/backbone/src/main/java/org/researchstack/backbone/task/NavigableOrderedTask.java b/backbone/src/main/java/org/researchstack/backbone/task/NavigableOrderedTask.java new file mode 100644 index 000000000..f401e7bea --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/NavigableOrderedTask.java @@ -0,0 +1,376 @@ +package org.researchstack.backbone.task; + +import android.util.Log; + +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.SubtaskStep; +import org.researchstack.backbone.utils.StepResultHelper; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 12/29/16. + * + * NavigableOrderedTask is meant for controlling a list of steps + * that can be SubtaskSteps, or any other normal step that may or may not + * implement and of the interfaces defined in this class... + * Which are NavigationRule, ConditionalRule, NavigationSkipRule + */ + +public class NavigableOrderedTask extends OrderedTask { + + static final String LOG_TAG = NavigableOrderedTask.class.getCanonicalName(); + + /* Default constructor needed for serilization/deserialization of object */ + public NavigableOrderedTask() { + super(); + } + + public NavigableOrderedTask(String identifier, List steps) { + super(identifier, steps); + orderedStepIdentifiers = new ArrayList<>(); + } + + public NavigableOrderedTask(String identifier, Step... steps) { + super(identifier, steps); + orderedStepIdentifiers = new ArrayList<>(); + } + + List additionalTaskResults; + ConditionalRule conditionalRule; + + private List orderedStepIdentifiers; + + private SubtaskStep subtaskStep(String identifier) { + // Look for a period in the range of the string + if (identifier == null) { + //Log.d(LOG_TAG, "Identifier is null, cannot find subtask step"); + return null; + } + + // Parse out the subtask identifier and look in super for a step with that identifier + int indexOfPeriod = identifier.indexOf("."); + if (indexOfPeriod < 0) { + //Log.d(LOG_TAG, "Identifier has no substep deliminator, aka a period"); + return null; + } + + String subtaskStepIdentifier = identifier.substring(0, indexOfPeriod); + Step subtaskStep = super.getStepWithIdentifier(subtaskStepIdentifier); + if (subtaskStep instanceof SubtaskStep) { + return (SubtaskStep)subtaskStep; + } + return null; // Wasnt an instance of SubtaskStep + } + + private Step superStepAfterStep(Step step, TaskResult result) { + // Check the conditional rule to see if it returns a next step for the given previous + // step and return that with an early exit if applicable. + if (conditionalRule != null) { + Step nextStep = conditionalRule.nextStep(step, null, result); + if (nextStep != null) { + return nextStep; + } + } + + Step returnStep; + Step previousStep = step; + boolean shouldSkip; + + do { + do { + + if (previousStep instanceof NavigationRule) { + NavigationRule navigableStep = (NavigationRule)previousStep; + String nextStepIdentifier = navigableStep.nextStepIdentifier(result, additionalTaskResults); + // If this is a step that conforms to the SBANavigableStep protocol and + // the next step identifier is non-nil then get the next step by looking within + // the steps associated with this task + if (nextStepIdentifier == null) { + returnStep = super.getStepAfterStep(previousStep, result); + } else { + returnStep = super.getStepWithIdentifier(nextStepIdentifier); + } + } else { + // If we've dropped through without setting the return step to something non-nil + // then look to super for the next step + returnStep = super.getStepAfterStep(previousStep, result); + } + + // Check if this is a skip-able step + if (returnStep instanceof NavigationSkipRule && + ((NavigationSkipRule)returnStep).shouldSkipStep(result, additionalTaskResults)) + { + shouldSkip = true; + previousStep = returnStep; + } else { + shouldSkip = false; + } + + } while (shouldSkip); + + // If the superclass returns a step of type subtask step, then get the first step from the subtask + // Since it is possible that the subtask will return an empty task (all steps are invalid) then + // need to also check that the return is non-nil + while (returnStep instanceof SubtaskStep) { + SubtaskStep subtaskStep = (SubtaskStep)returnStep; + Step subtaskReturnStep = subtaskStep.getStepAfterStep(null, result); + if (subtaskReturnStep != null) { + returnStep = subtaskReturnStep; + } else { + returnStep = super.getStepAfterStep(subtaskStep, result); + } + } + + // Check to see if this is a conditional step that *should* be skipped + if (conditionalRule != null) { + shouldSkip = conditionalRule.shouldSkip(returnStep, result); + } else { + shouldSkip = false; + } + + if (!shouldSkip && returnStep instanceof NavigationSkipRule) { + shouldSkip = ((NavigationSkipRule)returnStep).shouldSkipStep(result, additionalTaskResults); + } + + if (shouldSkip) { + previousStep = returnStep; + } + + } while (shouldSkip); + + // If there is a conditionalRule, then check to see if the step should be mutated or replaced + if (conditionalRule != null) { + returnStep = conditionalRule.nextStep(null, returnStep, result); + } + + return returnStep; + } + + // MARK: ORKOrderedTask overrides + /** + * Returns the next step immediately after the passed in step in the list of steps, or null + * + * @param step The reference step. Pass null to specify the first step. + * @param result A snapshot of the current set of results. + * @return the next step in steps after the passed in step, or null if at the end + */ + @Override + public Step getStepAfterStep(Step step, TaskResult result) + { + Step returnStep; + + String stepIdentifier = step != null ? step.getIdentifier() : null; + + // Look to see if this has a valid subtask step associated with this step + SubtaskStep subtaskStep = subtaskStep(stepIdentifier); + if (subtaskStep != null) { + returnStep = subtaskStep.getStepAfterStep(step, result); + if (returnStep == null) { + // If the subtask returns nil then it is at the last step + // Check super for more steps + returnStep = superStepAfterStep(subtaskStep, result); + } + } else { + // If this isn't a subtask step then look to super nav for the next step + returnStep = superStepAfterStep(step, result); + } + + // Look for step in the ordered steps and remove all items in the list after this one + String previousIdentifier = stepIdentifier; + int idx = -1; + if (previousIdentifier != null) { + idx = orderedStepIdentifiers.indexOf(previousIdentifier); + if (idx >= 0 && idx < (orderedStepIdentifiers.size() - 1)) { + orderedStepIdentifiers = new ArrayList<>(orderedStepIdentifiers.subList(0, idx + 1)); + } + } + + String identifier = null; + if (returnStep != null) { + identifier = returnStep.getIdentifier(); + } + + if (identifier != null) { + int indexOfId = orderedStepIdentifiers.indexOf(identifier); + if (indexOfId >= 0) { + orderedStepIdentifiers = new ArrayList<>(orderedStepIdentifiers.subList(0, indexOfId)); + } else { + orderedStepIdentifiers.add(identifier); + } + } + + return returnStep; + } + + /** + * Returns the next step immediately before the passed in step in the list of steps, or null + * + * @param step The reference step. + * @param result A snapshot of the current set of results. + * @return the next step in steps before the passed in step, or null if at the + * start + */ + @Override + public Step getStepBeforeStep(Step step, TaskResult result) { + if (step.getIdentifier() == null) { + //Log.e(LOG_TAG, "Found step with null identifier"); + return null; + } + + int idx = orderedStepIdentifiers.indexOf(step.getIdentifier()); + if (idx < 0) { + //Log.d(LOG_TAG, "Couldnt find step in orderedStepIdentifiers"); + return null; + } + + int prevIdx = idx - 1; + if (prevIdx >= 0) { + String previousIdentifier = orderedStepIdentifiers.get(prevIdx); + return getStepWithIdentifier(previousIdentifier); + } + + return null; + } + + @Override + public Step getStepWithIdentifier(String identifier) { + // Look for the step in the superclass + Step step = super.getStepWithIdentifier(identifier); + if (step != null) { + return step; + } + // If not found check to see if it is a substep + SubtaskStep subtaskStep = subtaskStep(identifier); + if (subtaskStep == null) { + Log.d(LOG_TAG, "No step with identifier found " + identifier); + return null; + } + return subtaskStep.getStepWithIdentifier(identifier); + } + + @Override + public TaskProgress getProgressOfCurrentStep(Step step, TaskResult result) { + // iOS has this return no progress ever because you truly can't predict it + // But, it will be helpful still as an estimate of progress + // so let's just estimate the progress by calling ordered task's progress + return super.getProgressOfCurrentStep(step, result); + } + + /** + * Validates that there are no duplicate identifiers in the list of steps + * @throws org.researchstack.backbone.task.Task.InvalidTaskException if parameters are invalid + */ + @Override + public void validateParameters() { + super.validateParameters(); + for (Step step : steps) { + // Check if the step is a subtask step and validate parameters + if (step instanceof SubtaskStep) { + ((SubtaskStep)step).getSubtask().validateParameters(); + } + } + } + +// TODO: may need this when we add audio support +// override open var providesBackgroundAudioPrompts: Bool { +// let superRet = super.providesBackgroundAudioPrompts +// if (superRet) { +// return true +// } +// for step in self.steps { +// // Check if the step is a subtask step and validate parameters +// if let subtaskStep = step as? SBASubtaskStep, +// let subRet = subtaskStep.subtask.providesBackgroundAudioPrompts , subRet { +// return true +// } +// } +// return false +// } + + /** + * Define the navigation rule as an interface to allow for protocol-oriented extention (multiple inheritance). + * Currently defined usage is to allow the SBANavigableOrderedTask to check if a step has a navigation rule. + */ + public interface NavigationRule { + String nextStepIdentifier(TaskResult result, List additionalTaskResults); + } + + /** + * A navigation skip rule applies to this step to allow that step to be skipped. + */ + public interface NavigationSkipRule { + boolean shouldSkipStep(TaskResult result, List additionalTaskResults); + } + + /** + * A conditional rule is appended to the navigable task to check a secondary source for whether or not the + * step should be displayed. + */ + public interface ConditionalRule { + boolean shouldSkip(Step step, TaskResult result); + Step nextStep(Step previousStep, Step nextStep, TaskResult result); + } + + /** + * This class is used to enable a simple navigation pattern for Steps + * The standard usage is to apply a TaskResult to it and look for the nextIdentifier + */ + public static class ObjectEqualsNavigationRule implements NavigationRule, Serializable { + private Object navigationResult; + private String navigationIdentifier; + private String resultIdentifier; + + /** Default constructor needed for serializable interface */ + ObjectEqualsNavigationRule() { + super(); + } + + /** + * @param navigationResult the expected result to enable navigation + * @param navigationIdentifier the navigation step identifier to go to if result is matched + * @param resultIdentifier the identifier of the result to find + */ + public ObjectEqualsNavigationRule( + Object navigationResult, + String navigationIdentifier, + String resultIdentifier) + { + this.navigationResult = navigationResult; + this.navigationIdentifier = navigationIdentifier; + this.resultIdentifier = resultIdentifier; + } + + public Object getNavigationResult() { + return navigationResult; + } + + public String getNavigationIdentifier() { + return navigationIdentifier; + } + + public String getResultIdentifier() { + return resultIdentifier; + } + + @Override + public String nextStepIdentifier(TaskResult result, List additionalTaskResults) { + if (navigationResult != null && navigationIdentifier != null && resultIdentifier != null) { + StepResult stepResult = StepResultHelper.findStepResult(result, resultIdentifier); + if (stepResult != null && + stepResult.getResult() != null && + stepResult.getResult().equals(navigationResult)) + { + return navigationIdentifier; + } + } + return null; + } + } +} + + diff --git a/skin/src/main/java/org/researchstack/skin/task/OnboardingTask.java b/backbone/src/main/java/org/researchstack/backbone/task/OnboardingTask.java similarity index 72% rename from skin/src/main/java/org/researchstack/skin/task/OnboardingTask.java rename to backbone/src/main/java/org/researchstack/backbone/task/OnboardingTask.java index 921f67aaf..2f1e928b7 100644 --- a/skin/src/main/java/org/researchstack/skin/task/OnboardingTask.java +++ b/backbone/src/main/java/org/researchstack/backbone/task/OnboardingTask.java @@ -1,18 +1,16 @@ -package org.researchstack.skin.task; +package org.researchstack.backbone.task; import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.step.Step; -import org.researchstack.backbone.task.Task; import org.researchstack.backbone.ui.step.body.NotImplementedStepBody; -import org.researchstack.skin.R; -import org.researchstack.skin.step.PassCodeCreationStep; -import org.researchstack.skin.ui.layout.PermissionStepLayout; -import org.researchstack.skin.ui.layout.SignInStepLayout; -import org.researchstack.skin.ui.layout.SignUpEligibleStepLayout; -import org.researchstack.skin.ui.layout.SignUpIneligibleStepLayout; -import org.researchstack.skin.ui.layout.SignUpStepLayout; +import org.researchstack.backbone.R; +import org.researchstack.backbone.step.PassCodeCreationStep; +import org.researchstack.backbone.ui.step.layout.PermissionStepLayout; +import org.researchstack.backbone.ui.layout.SignInStepLayout; +import org.researchstack.backbone.ui.layout.SignUpStepLayout; +@Deprecated // No longer needed with new OnboardingManager public abstract class OnboardingTask extends Task { public static final String SignUpInclusionCriteriaStepIdentifier = "InclusionCriteria"; public static final String SignUpEligibleStepIdentifier = "Eligible"; @@ -77,7 +75,7 @@ public boolean isEligible(TaskResult result) { public Step getSignInStep() { if (signInStep == null) { signInStep = new Step(SignInStepIdentifier); - signInStep.setStepTitle(R.string.rss_sign_in); + signInStep.setStepTitle(R.string.rsb_sign_in); signInStep.setStepLayoutClass(SignInStepLayout.class); } return signInStep; @@ -86,35 +84,17 @@ public Step getSignInStep() { public Step getThankyouStep() { if (thankyouStep == null) { thankyouStep = new Step(SignUpThankYouStepIdentifier); - thankyouStep.setStepTitle(R.string.rss_thank_you); + thankyouStep.setStepTitle(R.string.rsb_thank_you); thankyouStep.setStepLayoutClass(NotImplementedStepBody.class); // thankyouStep.setStepLayoutClass(SignUpThankYouStepLayout.class); } return thankyouStep; } - public Step getIneligibleStep() { - if (ineligibleStep == null) { - ineligibleStep = new Step(SignUpIneligibleStepIdentifier); - ineligibleStep.setStepTitle(R.string.rss_eligibility); - ineligibleStep.setStepLayoutClass(SignUpIneligibleStepLayout.class); - } - return ineligibleStep; - } - - public Step getEligibleStep() { - if (eligibleStep == null) { - eligibleStep = new Step(SignUpEligibleStepIdentifier); - eligibleStep.setStepTitle(R.string.rss_eligibility); - eligibleStep.setStepLayoutClass(SignUpEligibleStepLayout.class); - } - return eligibleStep; - } - public Step getPassCodeCreationStep() { if (passcodeCreationStep == null) { passcodeCreationStep = new PassCodeCreationStep(SignUpPassCodeCreationStepIdentifier, - R.string.rss_passcode); + R.string.rsb_passcode); } return passcodeCreationStep; } @@ -122,7 +102,7 @@ public Step getPassCodeCreationStep() { public Step getSignUpStep() { if (signUpStep == null) { signUpStep = new Step(SignUpStepIdentifier); - signUpStep.setStepTitle(R.string.rss_sign_up); + signUpStep.setStepTitle(R.string.rsb_sign_up); signUpStep.setStepLayoutClass(SignUpStepLayout.class); } return signUpStep; @@ -131,7 +111,7 @@ public Step getSignUpStep() { public Step getPermissionStep() { if (permissionsStep == null) { permissionsStep = new Step(SignUpPermissionsStepIdentifier); - permissionsStep.setStepTitle(R.string.rss_permissions); + permissionsStep.setStepTitle(R.string.rsb_permissions); permissionsStep.setStepLayoutClass(PermissionStepLayout.class); } return permissionsStep; diff --git a/backbone/src/main/java/org/researchstack/backbone/task/OrderedTask.java b/backbone/src/main/java/org/researchstack/backbone/task/OrderedTask.java index b2850676a..84b97d792 100644 --- a/backbone/src/main/java/org/researchstack/backbone/task/OrderedTask.java +++ b/backbone/src/main/java/org/researchstack/backbone/task/OrderedTask.java @@ -3,6 +3,7 @@ import android.content.Context; import org.researchstack.backbone.R; +import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.step.Step; import org.researchstack.backbone.utils.TextUtils; @@ -31,6 +32,11 @@ public class OrderedTask extends Task implements Serializable { protected List steps; + /* Default constructor needed for serilization/deserialization of object */ + public OrderedTask() { + super(); + } + /** * Returns an initialized ordered task using the specified identifier and array of steps. * @@ -132,7 +138,7 @@ public String getTitleForStep(Context context, Step step) { /** * Validates that there are no duplicate identifiers in the list of steps * - * @throws org.researchstack.backbone.task.Task.InvalidTaskException + * @throws org.researchstack.backbone.task.Task.InvalidTaskException if the task is invalid */ @Override public void validateParameters() { @@ -154,4 +160,33 @@ public void validateParameters() { public List getSteps() { return new ArrayList<>(steps); } + + /** + * Convenience method to replace a Step at a specific index + * This can be used to change the contents of a Step by calling getSteps(), + * changing the step, and then calling this method to make sure the changes stick + * + * @param index index of step to replace + * @param step to replace at index + */ + public void replaceStep(int index, Step step) { + steps.set(index, step); + } + + /** + * Convenience method to remove a Step from the Task + * @param index index of step to remove + */ + public void removeStep(int index) { + steps.remove(index); + } + + /** + * * Convenience method to add a Step to the Task + * @param index to add the step at + * @param step the step to add + */ + public void addStep(int index, Step step) { + steps.add(index, step); + } } diff --git a/skin/src/main/java/org/researchstack/skin/task/SignInTask.java b/backbone/src/main/java/org/researchstack/backbone/task/SignInTask.java similarity index 86% rename from skin/src/main/java/org/researchstack/skin/task/SignInTask.java rename to backbone/src/main/java/org/researchstack/backbone/task/SignInTask.java index 20d1812f5..49baefcea 100644 --- a/skin/src/main/java/org/researchstack/skin/task/SignInTask.java +++ b/backbone/src/main/java/org/researchstack/backbone/task/SignInTask.java @@ -1,13 +1,13 @@ -package org.researchstack.skin.task; +package org.researchstack.backbone.task; import android.content.Context; import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.step.Step; -import org.researchstack.skin.PermissionRequestManager; -import org.researchstack.skin.TaskProvider; - +import org.researchstack.backbone.PermissionRequestManager; +import org.researchstack.backbone.TaskProvider; +@Deprecated // use ResearchStack.getInstance().getOnboardingManager().launchOnboarding(OnboardingTaskType.LOGIN, this); public class SignInTask extends OnboardingTask { public static final int MINIMUM_STEPS = 0; public static final String ID_EMAIL = "ID_EMAIL"; diff --git a/backbone/src/main/java/org/researchstack/backbone/task/SignUpTask.java b/backbone/src/main/java/org/researchstack/backbone/task/SignUpTask.java new file mode 100644 index 000000000..a65e33edd --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/SignUpTask.java @@ -0,0 +1,254 @@ +package org.researchstack.backbone.task; + +import android.content.Context; + +import org.researchstack.backbone.answerformat.BooleanAnswerFormat; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.step.FormStep; +import org.researchstack.backbone.step.QuestionStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.utils.LogExt; +import org.researchstack.backbone.PermissionRequestManager; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ResourceManager; +import org.researchstack.backbone.TaskProvider; +import org.researchstack.backbone.UiManager; +import org.researchstack.backbone.model.InclusionCriteriaModel; +import org.researchstack.backbone.ui.layout.SignUpEligibleStepLayout; +import org.researchstack.backbone.ui.layout.SignUpIneligibleStepLayout; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Deprecated // use OnboardingManager.getInstance().launchOnboarding(context, TaskType.REGISTRATION); +public class SignUpTask extends OnboardingTask { + public static final int MINIMUM_STEPS = 2; + public static final String ID_EMAIL = "ID_EMAIL"; + public static final String ID_PASSWORD = "ID_PASSWORD"; + private boolean hasPasscode; + private Step inclusionCriteriaStep; + + private Map stepMap = new HashMap<>(); + private Map answerMap = new HashMap<>(); + + public SignUpTask(Context context) + { + super(TaskProvider.TASK_ID_SIGN_UP); + + initSteps(context); + + inclusionCriteriaStep = stepMap.get(SignUpInclusionCriteriaStepIdentifier); + + } + + /** + * Create steps as defined in the JSON file. + * + * @param context + */ + private void initSteps(Context context) { + InclusionCriteriaModel model = ResourceManager.getInstance() + .getInclusionCriteria() + .create(context); + + for(InclusionCriteriaModel.Step s: model.steps) { + // step can be null with addition of new OnboardingManager steps, so just ignore them + if (s != null && s.type != null) { + switch (s.type) { + case INSTRUCTION: + Step instruction = null; + switch (s.identifier) { + case InclusionCriteriaModel.INELIGIBLE_INSTRUCTION_IDENTIFIER: + instruction = new Step(SignUpIneligibleStepIdentifier, s.text); + instruction.setText(s.detailText); + instruction.setStepTitle(R.string.rsb_eligibility); + instruction.setStepLayoutClass(SignUpIneligibleStepLayout.class); + break; + case InclusionCriteriaModel.ELIGIBLE_INSTRUCTION_IDENTIFIER: + instruction = new Step(SignUpEligibleStepIdentifier, s.text); + instruction.setText(s.detailText); + instruction.setStepTitle(R.string.rsb_eligibility); + instruction.setStepLayoutClass(SignUpEligibleStepLayout.class); + break; + default: + instruction.setStepTitle(R.string.rsb_eligibility); + instruction = new Step(s.identifier, s.text); + instruction.setText(s.detailText); + } + + stepMap.put(instruction.getIdentifier(), instruction); + break; + // TODO: not sure what the differences are between compound/toggle or is compound obsolete? + case COMPOUND: + case TOGGLE: + FormStep form = new FormStep(SignUpInclusionCriteriaStepIdentifier, s.text, s.detailText); + List questions = new ArrayList<>(); + + if (s.items != null) { + // TODO: extend the json to include (yes/no)? + BooleanAnswerFormat booleanAnswerFormat = + new BooleanAnswerFormat(context.getString(R.string.rsb_yes), context.getString(R.string.rsb_no)); + for (InclusionCriteriaModel.Item item : s.items) { + QuestionStep question = new QuestionStep(item.identifier, item.text, booleanAnswerFormat); + answerMap.put(item.identifier, item.expectedAnswer); + questions.add(question); + } + form.setFormSteps(questions); + } + form.setStepTitle(R.string.rsb_eligibility); + form.setOptional(false); + stepMap.put(form.getIdentifier(), form); + break; + case SHARE: + Step step = new Step(s.identifier); + stepMap.put(step.getIdentifier(), step); + break; + default: + LogExt.i(getClass(), "Unrecognized InclusionCriteriaModel.Step: " + s.type); + } + } + } + } + + @Override + public Step getStepAfterStep(Step step, TaskResult result) { + Step nextStep = null; + + if (step == null) { + nextStep = inclusionCriteriaStep; + } else if (step.getIdentifier().equals(SignUpInclusionCriteriaStepIdentifier)) { + if (UiManager.getInstance().isInclusionCriteriaValid(result.getStepResult(step.getIdentifier()))) { + if(isInclusionCriteriaValid(result.getStepResult(step.getIdentifier()))) + nextStep = stepMap.get(SignUpEligibleStepIdentifier); + } else { + nextStep = stepMap.get(SignUpIneligibleStepIdentifier); + } + } else if (step.getIdentifier().equals(SignUpEligibleStepIdentifier)) { + if (!hasPasscode) { + nextStep = getPassCodeCreationStep(); + } else if (!PermissionRequestManager.getInstance().getPermissionRequests().isEmpty()) { + nextStep = getPermissionStep(); + } else { + nextStep = getSignUpStep(); + } + } else if (step.getIdentifier().equals(SignUpPassCodeCreationStepIdentifier)) { + if (!PermissionRequestManager.getInstance().getPermissionRequests().isEmpty()) { + nextStep = getPermissionStep(); + } else { + nextStep = getSignUpStep(); + } + } else if (step.getIdentifier().equals(SignUpPermissionsStepIdentifier)) { + nextStep = getSignUpStep(); + } + + return nextStep; + } + + @Override + public Step getStepBeforeStep(Step step, TaskResult result) { + Step prevStep = null; + + if (step.getIdentifier().equals(SignUpInclusionCriteriaStepIdentifier)) { + prevStep = null; + } else if (step.getIdentifier().equals(SignUpEligibleStepIdentifier)) { + prevStep = inclusionCriteriaStep; + + } else if (step.getIdentifier().equals(SignUpIneligibleStepIdentifier)) { + prevStep = inclusionCriteriaStep; + + } else if (step.getIdentifier().equals(SignUpPassCodeCreationStepIdentifier)) { + prevStep = stepMap.get(SignUpEligibleStepIdentifier); + } else if (step.getIdentifier().equals(SignUpPermissionsStepIdentifier)) { + if (hasPasscode) { + // Force user to create a new pin + prevStep = getPassCodeCreationStep(); + } else { + prevStep = stepMap.get(SignUpEligibleStepIdentifier); + } + } else if (step.getIdentifier().equals(SignUpStepIdentifier)) { + if (!PermissionRequestManager.getInstance().getPermissionRequests().isEmpty()) { + prevStep = getPermissionStep(); + } else if (hasPasscode) { + // Force user to create a new pin + prevStep = getPassCodeCreationStep(); + } else { + prevStep = stepMap.get(SignUpEligibleStepIdentifier); + } + } + + return prevStep; + } + + @Override + public TaskProgress getProgressOfCurrentStep(Step step, TaskResult result) { + int stepPosition = 0; + + if (step == null || step.getIdentifier().equals(SignUpInclusionCriteriaStepIdentifier)) { + stepPosition = 0; + } else if (step.getIdentifier().equals(SignUpEligibleStepIdentifier)) { + stepPosition = 1; + + } else if (step.getIdentifier().equals(SignUpIneligibleStepIdentifier)) { + stepPosition = 1; + + } else if (step.getIdentifier().equals(SignUpPassCodeCreationStepIdentifier)) { + stepPosition = 2; + + } else if (step.getIdentifier().equals(SignUpPermissionsStepIdentifier)) { + stepPosition = 3; + + } else if (step.getIdentifier().equals(SignUpStepIdentifier)) { + stepPosition = 4; + + } + + return new TaskProgress(stepPosition, MINIMUM_STEPS); + } + + public void setHasPasscode(boolean hasPasscode) { + this.hasPasscode = hasPasscode; + } + + private Boolean getBooleanAnswer(Map mapStepResult, String id) + { + StepResult stepResult = (StepResult)mapStepResult.get(id); + if (stepResult == null) return false; + Map mapResult = stepResult.getResults(); + if (mapResult == null) return false; + Boolean answer = (Boolean)mapResult.get(StepResult.DEFAULT_KEY); + if (answer == null || answer == false) + { + return false; + } + else + { + return true; + } + + } + + protected boolean isInclusionCriteriaValid(StepResult stepResult) + { + if(stepResult != null) + { + Map mapStepResult = stepResult.getResults(); + for(Object obj: mapStepResult.keySet()) + { + String id = (String)obj; + Boolean answer = getBooleanAnswer(mapStepResult, id); + Boolean expectedAnswer = answerMap.get(id); + if(answer.booleanValue() != expectedAnswer.booleanValue()) + { + return false; + } + } + + return true; + } + return false; + } + +} diff --git a/skin/src/main/java/org/researchstack/skin/task/SmartSurveyTask.java b/backbone/src/main/java/org/researchstack/backbone/task/SmartSurveyTask.java similarity index 97% rename from skin/src/main/java/org/researchstack/skin/task/SmartSurveyTask.java rename to backbone/src/main/java/org/researchstack/backbone/task/SmartSurveyTask.java index 88896590f..24b26d76e 100644 --- a/skin/src/main/java/org/researchstack/skin/task/SmartSurveyTask.java +++ b/backbone/src/main/java/org/researchstack/backbone/task/SmartSurveyTask.java @@ -1,7 +1,8 @@ -package org.researchstack.skin.task; +package org.researchstack.backbone.task; import android.content.Context; +import org.researchstack.backbone.R; import org.researchstack.backbone.answerformat.AnswerFormat; import org.researchstack.backbone.answerformat.BooleanAnswerFormat; import org.researchstack.backbone.answerformat.ChoiceAnswerFormat; @@ -16,10 +17,8 @@ import org.researchstack.backbone.step.InstructionStep; import org.researchstack.backbone.step.QuestionStep; import org.researchstack.backbone.step.Step; -import org.researchstack.backbone.task.Task; import org.researchstack.backbone.utils.LogExt; -import org.researchstack.skin.R; -import org.researchstack.skin.model.TaskModel; +import org.researchstack.backbone.model.TaskModel; import java.io.Serializable; import java.util.ArrayList; @@ -112,6 +111,8 @@ private AnswerFormat from(Context context, TaskModel.ConstraintsModel constraint ((TextAnswerFormat) answerFormat).setIsMultipleLines(multipleLines); } else if (type.equals("DateConstraints")) { answerFormat = new DateAnswerFormat(AnswerFormat.DateAnswerStyle.Date); + } else if (type.equals("DateTimeConstraints")) { + answerFormat = new DateAnswerFormat(AnswerFormat.DateAnswerStyle.DateAndTime); } else if (type.equals("DurationConstraints")) { answerFormat = new DurationAnswerFormat(constraints.step, constraints.durationUnit); } else { @@ -210,7 +211,7 @@ public Step getStepWithIdentifier(String identifier) { * * @param context for fetching resources * @param step the current step - * @return + * @return the title that should be displayed for this step */ @Override public String getTitleForStep(Context context, Step step) { diff --git a/backbone/src/main/java/org/researchstack/backbone/task/Task.java b/backbone/src/main/java/org/researchstack/backbone/task/Task.java index ea21b3e5a..714bdacbb 100644 --- a/backbone/src/main/java/org/researchstack/backbone/task/Task.java +++ b/backbone/src/main/java/org/researchstack/backbone/task/Task.java @@ -2,6 +2,7 @@ import android.content.Context; +import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.step.Step; import org.researchstack.backbone.ui.ViewTaskActivity; @@ -28,6 +29,11 @@ public abstract class Task implements Serializable { private String identifier; + /* Default constructor needed for serilization/deserialization of object */ + public Task() { + super(); + } + /** * Class constructor specifying a unique identifier. * @@ -147,7 +153,7 @@ public String getTitleForStep(Context context, Step step) { * This method is usually called by {@link org.researchstack.backbone.ui.ViewTaskActivity} when * its task is set. * - * @throws InvalidTaskException + * @throws InvalidTaskException if the task is invalid */ public abstract void validateParameters(); diff --git a/backbone/src/main/java/org/researchstack/backbone/task/TaskCreationManager.java b/backbone/src/main/java/org/researchstack/backbone/task/TaskCreationManager.java new file mode 100644 index 000000000..c5c9a560a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/TaskCreationManager.java @@ -0,0 +1,135 @@ +package org.researchstack.backbone.task; + +import android.content.Context; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import org.researchstack.backbone.ResourceManager; +import org.researchstack.backbone.ResourcePathManager; +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.model.survey.SurveyItemAdapter; +import org.researchstack.backbone.model.survey.factory.SurveyFactory; +import org.researchstack.backbone.model.taskitem.TaskItem; +import org.researchstack.backbone.model.taskitem.TaskItemAdapter; +import org.researchstack.backbone.model.taskitem.factory.TaskItemFactory; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.utils.LogExt; + +/** + * Created by TheMDP on 3/24/17. + * + * The TaskCreationManager follows a similar architecture as the OnboardingManager, + * Its job is to parse JSON resources and create Tasks from them + * It also is designed in a way that makes creating custom tasks and steps easy + * while still getting all the awesome features of the base Survey and TaskItem factories + */ + +public class TaskCreationManager implements TaskItemFactory.CustomTaskCreator, SurveyFactory.CustomStepCreator { + + private TaskItemFactory taskItemFactory; + + public TaskCreationManager() { + super(); + taskItemFactory = new TaskItemFactory(); + } + + /** + * @param context can be any context, activity or application, used to access "R" resources + * @param resourceName needs to be a resource that you define in the ResourceManager + * it is as simple as defining a method returning a resource with this name + * in your concreate implementation of ResourceManager + * @return a Task based on the contents of the resource + */ + public Task createTask(Context context, String resourceName) { + String taskItemJson = ResourceManager.getResourceAsString(context, + ResourceManager.getInstance().generatePath(ResourcePathManager.Resource.TYPE_JSON, resourceName)); + + if (taskItemJson == null) { + LogExt.e(getClass(), "Error finding resource with resource name " + resourceName + + ". Did you define a method that returns it in your concrete implementation " + + "of ResourceManager?"); + return null; + } + + Gson gson = buildGson(context); + TaskItem taskItem = gson.fromJson(taskItemJson, TaskItem.class); + if (taskItem == null) { + LogExt.e(getClass(), "Error creating TaskItem from json"); + return null; + } + + return getTaskItemFactory(taskItem).createTask(context, taskItem); + } + + /** + * This is a way for subclasses to inject their own task item factories per specific task items + * @param item the task item being operated on + * @return the task item factory that will be used to create the task from this task item + */ + public TaskItemFactory getTaskItemFactory(TaskItem item) { + return taskItemFactory; + } + + /** + * Override to register custom SurveyItemAdapters, + * but make sure that the adapter extends from SurveyItemAdapter, and only overrides + * the method getCustomClass() + * @param builder the gson build to add the survey item adapter to + */ + public void registerSurveyItemAdapter(GsonBuilder builder) { + builder.registerTypeAdapter(SurveyItem.class, new SurveyItemAdapter()); + } + + /** + * Override to register custom TaskItemAdapters, + * but make sure that the adapter extends from TaskItemAdapter, and only overrides + * the method getCustomClass() + * @param builder the gson build to add the task item adapter to + */ + public void registerTaskItemAdapter(GsonBuilder builder) { + builder.registerTypeAdapter(SurveyItem.class, new TaskItemAdapter()); + } + + /** + * @return a Gson to be used by the TaskCreationManager + */ + private Gson buildGson(Context context) { + GsonBuilder gsonBuilder = new GsonBuilder(); + registerSurveyItemAdapter(gsonBuilder); + registerTaskItemAdapter(gsonBuilder); + return gsonBuilder.create(); + } + + /** + * Override this to implement your own functionality for CustomStep creation + * You should also override registerSurveyItemAdapter to control the CustomSurveyItem data model + * @param item the custom survey item to create a custom step from + * @param factory the factory that created the custom survey item + * @param isSubtaskStep true if this is within a subtask step already, false otherwise + * @return a CustomStep that can be used in your app + */ + @Override + public Step createCustomStep(Context context, SurveyItem item, boolean isSubtaskStep, SurveyFactory factory) { + return factory.createCustomStep(context, item, isSubtaskStep); + } + + /** + * Override this to implement your own functionality for Custom Task creation + * You should also override registerTaskItemAdapter to control the CustomTaskItem data model + * @param item the custom task item to create a custom Task from + * @param factory the factory that created the custom task item + * @return a Task that can be used in your app + */ + @Override + public Task createCustomTask(Context context, TaskItem item, TaskItemFactory factory) { + return factory.createCustomTask(context, item); + } + + /** + * @return a basic task item factory that can be used to create tasks + */ + public TaskItemFactory getDefaultTaskItemFactory() { + return taskItemFactory; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/task/factory/AudioTaskFactory.java b/backbone/src/main/java/org/researchstack/backbone/task/factory/AudioTaskFactory.java new file mode 100644 index 000000000..070a8dc1d --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/factory/AudioTaskFactory.java @@ -0,0 +1,182 @@ +package org.researchstack.backbone.task.factory; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.step.AudioTooLoudStep; +import org.researchstack.backbone.step.InstructionStep; +import org.researchstack.backbone.step.PermissionsStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.AudioStep; +import org.researchstack.backbone.step.active.CountdownStep; +import org.researchstack.backbone.step.active.recorder.AudioRecorderConfig; +import org.researchstack.backbone.step.active.recorder.AudioRecorderSettings; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.utils.ResUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.researchstack.backbone.task.factory.TaskFactory.Constants.*; + +/** + * Created by TheMDP on 2/26/17. + */ + +public class AudioTaskFactory { + + /** + * Threshold can be anywhere from 0.0 - 1.0 + * This will determine if background noise is too loud and direct the user to + * try again in a quieter environment + */ + public static final double LOUDNESS_THRESHOLD = 0.45; + + public static final String AudioStepIdentifier = "audio"; + public static final String AudioTooLoudStepIdentifier = "audio.tooloud"; + public static final String MicrophonePermissionsStepIdentifier = "microphonepermission"; + + /** + * Returns a predefined task that enables an audio recording possibly with a check of the audio level. + * + * In an audio recording task, the participant is asked to make some kind of sound + * with their voice, and the audio data is collected. + * + * An audio task can be used to measure properties of the user's voice, such as + * frequency range, or the ability to pronounce certain sounds. + * + * If `checkAudioLevel == true` then a navigation rule is added to do a simple check of the background + * noise level. If the background noise is too loud, then the participant is instructed to move to a + * quieter location before trying again. + * + * Data collected in this task consists of audio information. + * + * @param context Can be app or activity, used for string and other resources + * @param identifier The task identifier to use for this task, appropriate to the study. + * @param intendedUseDescription A localized string describing the intended use of the data + * collected. If the value of this parameter is `null`, default + * localized text is used. + * @param speechInstruction Instructional content describing what the user needs to do when + * recording begins. If the value of this parameter is `null`, + * default localized text is used. + * @param shortSpeechInstruction Instructional content shown during audio recording. If the value of + * this parameter is `null`, default localized text is used. + * @param duration The length of the count down timer that runs while audio data is + * collected. + * @param recordingSettings See class for possible values, all based on MediaRecorder class + * @param checkAudioLevel If `true` then add navigational rules to check the background noise level. + * @param optionList Hand that affect the features of the predefined task. + * + * @return An active audio task that can be presented with an `ORKTaskViewController` object. + */ + public static NavigableOrderedTask audioTask( + Context context, + String identifier, + String intendedUseDescription, + String speechInstruction, + String shortSpeechInstruction, + int duration, + AudioRecorderSettings recordingSettings, + boolean checkAudioLevel, + List optionList) + { + List stepList = new ArrayList<>(); + + if (recordingSettings == null) { + recordingSettings = AudioRecorderSettings.defaultSettings(); + } + + if (optionList.contains(TaskExcludeOption.AUDIO)) { + throw new IllegalStateException("Audio collection cannot be excluded from audio task"); + } + + // In-app permissions were added in Android 6.0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // This isn't in iOS, but in Android we need to check for this so that microphone permission is granted + PackageManager pm = context.getPackageManager(); + int hasPerm = pm.checkPermission(Manifest.permission.RECORD_AUDIO, context.getPackageName()); + if (hasPerm != PackageManager.PERMISSION_GRANTED) { + // include a permission request step that requires microphone + stepList.add(new PermissionsStep(MicrophonePermissionsStepIdentifier, null, null)); + } + } + + if (!optionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + { + String title = context.getString(R.string.rsb_AUDIO_TASK_TITLE); + if (intendedUseDescription == null) { + intendedUseDescription = context.getString(R.string.rsb_AUDIO_INTENDED_USE); + } + InstructionStep step = new InstructionStep(Instruction0StepIdentifier, title, intendedUseDescription); + step.setImage(ResUtils.Audio.PHONE_WAVES); + stepList.add(step); + } + + { + String title = context.getString(R.string.rsb_AUDIO_TASK_TITLE); + String text = speechInstruction; + if (text == null) { + text = context.getString(R.string.rsb_AUDIO_INTRO_TEXT); + } + InstructionStep step = new InstructionStep(Instruction1StepIdentifier, title, text); + step.setMoreDetailText(context.getString(R.string.rsb_AUDIO_CALL_TO_ACTION)); + step.setImage(ResUtils.Audio.PHONE_SOUND_WAVES); + stepList.add(step); + } + } + + { + CountdownStep step = new CountdownStep(CountdownStepIdentifier); + + // Collect audio during the countdown step too, to provide a "too loud" baseline + step.setRecorderConfigurationList(Collections.singletonList(new AudioRecorderConfig( + AudioRecorderSettings.defaultSettings(), AudioRecorderIdentifier))); + + // If checking the sound level then add text indicating that's what is happening + if (checkAudioLevel) { + step.setText(context.getString(R.string.rsb_AUDIO_LEVEL_CHECK_LABEL)); + } + + stepList.add(step); + } + + if (checkAudioLevel) { + String text = context.getString(R.string.rsb_AUDIO_TOO_LOUD_MESSAGE); + AudioTooLoudStep step = new AudioTooLoudStep(AudioTooLoudStepIdentifier, null, text); + step.setMoreDetailText(context.getString(R.string.rsb_AUDIO_TOO_LOUD_ACTION_NEXT)); + + // Configure the step's Navigation Rules so that the NavigableOrderedTask + // can correctly direct the user based on the results of the audio recording + step.setLoudnessThreshold(LOUDNESS_THRESHOLD); + step.setAudioStepResultIdentifier(CountdownStepIdentifier); + step.setNextStepIdentifier(Instruction1StepIdentifier); + + stepList.add(step); + } + + { + AudioStep step = new AudioStep(AudioStepIdentifier, null, null); + if (shortSpeechInstruction == null) { + step.setTitle(context.getString(R.string.rsb_AUDIO_INSTRUCTION)); + } else { + step.setTitle(shortSpeechInstruction); + } + step.setRecorderConfigurationList(Collections.singletonList(new AudioRecorderConfig( + recordingSettings, AudioRecorderIdentifier))); + step.setStepDuration(duration); + step.setShouldContinueOnFinish(true); + + stepList.add(step); + } + + if (!optionList.contains(TaskExcludeOption.CONCLUSION)) { + stepList.add(TaskFactory.makeCompletionStep(context)); + } + + return new NavigableOrderedTask(identifier, stepList); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/task/factory/HandTaskOptions.java b/backbone/src/main/java/org/researchstack/backbone/task/factory/HandTaskOptions.java new file mode 100644 index 000000000..e7419e211 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/factory/HandTaskOptions.java @@ -0,0 +1,41 @@ +package org.researchstack.backbone.task.factory; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import org.researchstack.backbone.model.ProfileInfoOption; + +/** + * Created by TheMDP on 2/24/17. + * + * Values that identify the hand(s) to be used in an active task. + * + * By default, the participant will be asked to use their most affected hand. + */ +public class HandTaskOptions { + + public static final String SERIALIZED_NAME_HAND_LEFT = "left"; + public static final String SERIALIZED_NAME_HAND_RIGHT = "right"; + public static final String SERIALIZED_NAME_HAND_BOTH = "both"; + + static Gson gson; + + public enum Hand { + // Task should only test the left hand + @SerializedName(SERIALIZED_NAME_HAND_LEFT) + LEFT, + // Task should only test the right hand + @SerializedName(SERIALIZED_NAME_HAND_RIGHT) + RIGHT, + // Task should test both left and right hands + @SerializedName(SERIALIZED_NAME_HAND_BOTH) + BOTH; + } + + public static Hand toHandOption(String handSerializedName) { + if (gson == null) { + gson = new Gson(); + } + return gson.fromJson(handSerializedName, Hand.class); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/task/factory/MoodSurveyFactory.java b/backbone/src/main/java/org/researchstack/backbone/task/factory/MoodSurveyFactory.java new file mode 100644 index 000000000..784dee768 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/factory/MoodSurveyFactory.java @@ -0,0 +1,172 @@ +package org.researchstack.backbone.task.factory; + +import android.content.Context; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.answerformat.AnswerFormat; +import org.researchstack.backbone.answerformat.MoodScaleAnswerFormat; +import org.researchstack.backbone.step.InstructionStep; +import org.researchstack.backbone.step.QuestionStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.task.OrderedTask; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.researchstack.backbone.task.factory.TaskFactory.Constants.*; + +/** + * Created by TheMDP on 3/14/17. + */ + +public class MoodSurveyFactory { + + public static final String MoodSurveyIdentifier = "Mood Survey"; + public static final String MoodSurveyCustomQuestionStepIdentifier = "mood.custom"; + public static final String MoodSurveyClarityQuestionStepIdentifier = "mood.clarity"; + public static final String MoodSurveyOverallQuestionStepIdentifier = "mood.overall"; + public static final String MoodSurveySleepQuestionStepIdentifier = "mood.sleep"; + public static final String MoodSurveyExerciseQuestionStepIdentifier = "mood.exercise"; + public static final String MoodSurveyPainQuestionStepIdentifier = "mood.pain"; + + /** + * Returns a predefined survey that asks the user questions about their mood and general health. + * + * The mood survey includes questions about the daily or weekly mental and physical health status and + * includes asking about clarity of thinking, overall mood, pain, sleep and exercise. Additionally, + * the survey is setup to allow for an optional custom question that uses a similar-looking set of images + * as the other questions. + * + * @param context Can be app or activity, used to grab resources + * @param identifier The task identifier to use for this task, appropriate to the study. + * @param intendedUseDescription A localized string describing the intended use of the data + * collected. If the value of this parameter is `nil`, the default + * localized text is displayed. + * @param frequency How frequently the survey is asked (daily or weekly) + * @param customQuestionText A localized string to use for a custom question. If `nil`, this step + * is not included. + * @param optionList Options that affect the features of the predefined task. + * + * @return An mood survey that can be presented with an `ORKTaskViewController` object. + */ + public static OrderedTask moodSurvey( + Context context, + String identifier, + String intendedUseDescription, + MoodSurveyFrequency frequency, + String customQuestionText, + List optionList) + { + List stepList = new ArrayList<>(); + + if (!optionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + stepList.add(getIntroStep(context, frequency, intendedUseDescription)); + } + + // Custom + if (customQuestionText != null) { + stepList.add(getCustomQuestionStep(context, customQuestionText)); + } + + // Clarity + stepList.add(getClarityStep(context, frequency)); + + // Overall + stepList.add(getOverallStep(context, frequency)); + + // Pain + stepList.add(getPainStep(context, frequency)); + + // Sleep + stepList.add(getSleepStep(context, frequency)); + + // Exercise + stepList.add(getExerciseStep(context, frequency)); + + if (!optionList.contains(TaskExcludeOption.CONCLUSION)) { + stepList.add(TaskFactory.makeCompletionStep(context)); + } + + return new OrderedTask(identifier, stepList); + } + + protected static Step getIntroStep(Context context, MoodSurveyFrequency frequency, String intendedUseDescription) { + String title = (frequency == MoodSurveyFrequency.DAILY) ? + context.getString(R.string.rsb_MOOD_SURVEY_INTRO_DAILY_TITLE) : + context.getString(R.string.rsb_MOOD_SURVEY_INTRO_WEEKLY_TITLE); + String text = intendedUseDescription; + if (text == null) { + text = (frequency == MoodSurveyFrequency.DAILY) ? + context.getString(R.string.rsb_MOOD_SURVEY_INTRO_DAILY_TEXT) : + context.getString(R.string.rsb_MOOD_SURVEY_INTRO_WEEKLY_TEXT); + } + InstructionStep step = new InstructionStep(Instruction0StepIdentifier, title, text); + step.setMoreDetailText(context.getString(R.string.rsb_MOOD_SURVEY_INTRO_DETAIL)); + + return step; + } + + protected static Step getCustomQuestionStep(Context context, String customQuestionText) { + return moodQuestionStep(context, MoodSurveyCustomQuestionStepIdentifier, + customQuestionText, MoodScaleAnswerFormat.MoodQuestionType.CUSTOM); + } + + protected static Step getClarityStep(Context context, MoodSurveyFrequency frequency) { + String title = (frequency == MoodSurveyFrequency.DAILY) ? + context.getString(R.string.rsb_MOOD_CLARITY_DAILY_PROMPT) : + context.getString(R.string.rsb_MOOD_CLARITY_WEEKLY_PROMPT); + return moodQuestionStep(context, MoodSurveyClarityQuestionStepIdentifier, + title, MoodScaleAnswerFormat.MoodQuestionType.CLARITY); + } + + protected static Step getOverallStep(Context context, MoodSurveyFrequency frequency) { + String title = (frequency == MoodSurveyFrequency.DAILY) ? + context.getString(R.string.rsb_MOOD_OVERALL_DAILY_PROMPT) : + context.getString(R.string.rsb_MOOD_OVERALL_WEEKLY_PROMPT); + return moodQuestionStep(context, MoodSurveyOverallQuestionStepIdentifier, + title, MoodScaleAnswerFormat.MoodQuestionType.OVERALL); + } + + protected static Step getPainStep(Context context, MoodSurveyFrequency frequency) { + String title = (frequency == MoodSurveyFrequency.DAILY) ? + context.getString(R.string.rsb_MOOD_PAIN_DAILY_PROMPT) : + context.getString(R.string.rsb_MOOD_PAIN_WEEKLY_PROMPT); + return moodQuestionStep(context, MoodSurveyPainQuestionStepIdentifier, + title, MoodScaleAnswerFormat.MoodQuestionType.PAIN); + } + + protected static Step getSleepStep(Context context, MoodSurveyFrequency frequency) { + String title = (frequency == MoodSurveyFrequency.DAILY) ? + context.getString(R.string.rsb_MOOD_SLEEP_DAILY_PROMPT) : + context.getString(R.string.rsb_MOOD_SLEEP_WEEKLY_PROMPT); + return moodQuestionStep(context, MoodSurveySleepQuestionStepIdentifier, + title, MoodScaleAnswerFormat.MoodQuestionType.SLEEP); + } + + protected static Step getExerciseStep(Context context, MoodSurveyFrequency frequency) { + String title = (frequency == MoodSurveyFrequency.DAILY) ? + context.getString(R.string.rsb_MOOD_EXERCISE_DAILY_PROMPT) : + context.getString(R.string.rsb_MOOD_EXERCISE_WEEKLY_PROMPT); + return moodQuestionStep(context, MoodSurveyExerciseQuestionStepIdentifier, + title, MoodScaleAnswerFormat.MoodQuestionType.EXERCISE); + } + + /** + * @param context can be app or activity, used for string resources in MoodScaleAnswerFormat + * @param identifier identifier of the QuestionStep + * @param title title of the QuestionStep + * @param type MoodQuestionType for the MoodScaleAnswerFormat + * @return a QuestionStep for the Mood Survey + */ + protected static QuestionStep moodQuestionStep( + Context context, + String identifier, + String title, + MoodScaleAnswerFormat.MoodQuestionType type) + { + AnswerFormat answerFormat = new MoodScaleAnswerFormat(context, type); + return new QuestionStep(identifier, title, answerFormat); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/task/factory/MoodSurveyFrequency.java b/backbone/src/main/java/org/researchstack/backbone/task/factory/MoodSurveyFrequency.java new file mode 100644 index 000000000..470f2f9cc --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/factory/MoodSurveyFrequency.java @@ -0,0 +1,13 @@ +package org.researchstack.backbone.task.factory; + +/** + * Created by TheMDP on 3/14/17. + * + * Frequency with which the mood question is asked. + * Used by `MoodQuestionStep` to setup the question. + */ + +public enum MoodSurveyFrequency { + DAILY, + WEEKLY +} \ No newline at end of file diff --git a/backbone/src/main/java/org/researchstack/backbone/task/factory/TappingTaskFactory.java b/backbone/src/main/java/org/researchstack/backbone/task/factory/TappingTaskFactory.java new file mode 100644 index 000000000..9e8569b3e --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/factory/TappingTaskFactory.java @@ -0,0 +1,210 @@ +package org.researchstack.backbone.task.factory; + +import android.content.Context; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.step.InstructionStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.TappingIntervalStep; +import org.researchstack.backbone.step.active.recorder.AccelerometerRecorderConfig; +import org.researchstack.backbone.step.active.recorder.RecorderConfig; +import org.researchstack.backbone.task.OrderedTask; +import org.researchstack.backbone.utils.ResUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +import static org.researchstack.backbone.task.factory.TaskFactory.Constants.*; + +/** + * Created by TheMDP on 2/23/17. + */ + +public class TappingTaskFactory { + + /** + * Returns a predefined task that consists of two finger tapping (Optionally with a hand specified) + * + * In a two finger tapping task, the participant is asked to rhythmically and alternately tap two + * targets on the device screen. + * + * A two finger tapping task can be used to assess basic motor capabilities including speed, accuracy, + * and rhythm. + * + * Data collected in this task includes touch activity and accelerometer information. + * + * @param context App or Activity context, used for getting resources + * @param identifier The task identifier to use for this task, appropriate to the study. + * @param intendedUseDescription A localized string describing the intended use of the data + * collected. If the value of this parameter is `nil`, the default + * localized text will be displayed. + * @param duration The length of the count down timer that runs while touch data is + * collected. + * @param handOptions Hand for determining which hand(s) to test. + * @param optionList Hand that affect the features of the predefined task. + * + * @return An active two finger tapping task that can be presented with an `ActiveTaskActivity` object. + */ + public static OrderedTask twoFingerTappingIntervalTask( + Context context, + String identifier, + String intendedUseDescription, + int duration, + HandTaskOptions.Hand handOptions, + List optionList) + { + // Coin toss for which hand first (in case we're doing both + final boolean leftFirstIfDoingBoth = (new Random()).nextBoolean(); + + return twoFingerTappingIntervalTask( + context, identifier, intendedUseDescription, + duration, handOptions, optionList, leftFirstIfDoingBoth); + } + + // This method is separate mainly for unit testing purposes, to eliminate hand randomness + protected static OrderedTask twoFingerTappingIntervalTask( + Context context, + String identifier, + String intendedUseDescription, + int duration, + HandTaskOptions.Hand handOptions, + List optionList, + boolean leftFirstIfDoingBoth) + { + List stepList = new ArrayList<>(); + + String durationString = TaskFactory.convertDurationToString(context, duration); + + if (!optionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + String title = context.getString(R.string.rsb_TAPPING_TASK_TITLE); + InstructionStep step = new InstructionStep(Instruction0StepIdentifier, title, intendedUseDescription); + step.setMoreDetailText(context.getString(R.string.rsb_TAPPING_INTRO_TEXT)); + step.setImage(ResUtils.Tapping.PHONE_TAPPING_NO_TAP); + + stepList.add(step); + } + + // Setup which hand to start with and how many hands to add based on the handOptions parameter + // Hand order is randomly determined. + int handCount = handOptions == HandTaskOptions.Hand.BOTH ? 2 : 1; // 2 hands for both, 1 hand for right or left + boolean rightHand = false; + switch (handOptions) { + case LEFT: + rightHand = false; + break; + case RIGHT: + rightHand = true; + break; + case BOTH: + rightHand = !leftFirstIfDoingBoth; + break; + } + + // Obtain sensor frequency for Tapping Task recorders + double sensorFreq = context.getResources().getInteger(R.integer.rsb_sensor_frequency_tapping_task); + + // Make steps for one or both hands + for (int hand = 1; hand <= handCount; hand++) { + String handIdentifier = rightHand ? + stepIdentifierWithHandId(Instruction1StepIdentifier, ActiveTaskRightHandIdentifier) : + stepIdentifierWithHandId(Instruction1StepIdentifier, ActiveTaskLeftHandIdentifier) ; + + if (!optionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + InstructionStep step = new InstructionStep(handIdentifier, null, null); + + if (rightHand) { + step.setTitle(context.getString(R.string.rsb_TAPPING_TASK_TITLE_RIGHT)); + } else { + step.setTitle(context.getString(R.string.rsb_TAPPING_TASK_TITLE_LEFT)); + } + + // Set the instructions for the tapping test screen that is displayed prior to each hand test + String restText = context.getString(R.string.rsb_TAPPING_INTRO_TEXT_2_REST_PHONE); + String tappingTextFormat = context.getString(R.string.rsb_TAPPING_INTRO_TEXT_2_FORMAT); + String tappingText = String.format(tappingTextFormat, durationString); + String handText = null; + + if (hand == 1) { // first hand + if (rightHand) { + handText = context.getString(R.string.rsb_TAPPING_INTRO_TEXT_2_RIGHT_FIRST); + } else { + handText = context.getString(R.string.rsb_TAPPING_INTRO_TEXT_2_LEFT_FIRST); + } + } else { + if (rightHand) { + handText = context.getString(R.string.rsb_TAPPING_INTRO_TEXT_2_RIGHT_SECOND); + } else { + handText = context.getString(R.string.rsb_TAPPING_INTRO_TEXT_2_LEFT_SECOND); + } + } + + step.setText(String.format(Locale.getDefault(), "%s %s %s", restText, handText, tappingText)); + + // Continue button will be different from first hand and second hand + if (hand == 1) { + step.setMoreDetailText(context.getString(R.string.rsb_TAPPING_CALL_TO_ACTION)); + } else { + step.setMoreDetailText(context.getString(R.string.rsb_TAPPING_CALL_TO_ACTION_NEXT)); + } + + // Set the image + if (rightHand) { + step.setImage(ResUtils.Tapping.ANIMATED_TAPPING_RIGHT); + } else { + step.setImage(ResUtils.Tapping.ANIMATED_TAPPING_LEFT); + } + step.setIsImageAnimated(true); + // The ANIMATED_TAPPING assets repeat at this duration + long animDuration = 2 * context.getResources().getInteger(R.integer.rsb_config_tapping_duration_half); + step.setAnimationRepeatDuration(animDuration); + + stepList.add(step); + } + + // TAPPING STEP + { + List recorderConfigList = new ArrayList<>(); + if (!optionList.contains(TaskExcludeOption.ACCELEROMETER)) { + recorderConfigList.add(new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq)); + } + + String tappingHandIdentifier = rightHand ? + stepIdentifierWithHandId(TappingStepIdentifier, ActiveTaskRightHandIdentifier) : + stepIdentifierWithHandId(TappingStepIdentifier, ActiveTaskLeftHandIdentifier) ; + + TappingIntervalStep step = new TappingIntervalStep(tappingHandIdentifier); + + if (rightHand) { + step.setTitle(context.getString(R.string.rsb_TAPPING_INSTRUCTION_RIGHT)); + } else { + step.setTitle(context.getString(R.string.rsb_TAPPING_INSTRUCTION_LEFT)); + } + + step.setStepDuration(duration); + step.setShouldContinueOnFinish(true); + step.setRecorderConfigurationList(recorderConfigList); + step.setOptional(handCount == 2); + + stepList.add(step); + } + + // Flip to the other hand (ignored if handCount == 1) + rightHand = !rightHand; + } + + if (!optionList.contains(TaskExcludeOption.CONCLUSION)) { + stepList.add(TaskFactory.makeCompletionStep(context)); + } + + return new OrderedTask(identifier, stepList); + } + + public static String stepIdentifierWithHandId(String stepId, String handId) { + if (handId == null) { + return stepId; + } + return String.format("%s.%s", stepId, handId); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/task/factory/TaskExcludeOption.java b/backbone/src/main/java/org/researchstack/backbone/task/factory/TaskExcludeOption.java new file mode 100644 index 000000000..b069a7861 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/factory/TaskExcludeOption.java @@ -0,0 +1,33 @@ +package org.researchstack.backbone.task.factory; + +/** + * Created by TheMDP on 2/15/17. + */ + +/** + * The `TaskExcludeOption` enum lets you exclude particular behaviors from the predefined active + * tasks in the predefined category of `OrderedTask`. + * + * By default, all predefined tasks include instructions and conclusion steps, and may also include + * one or more data collection recorder configurations. Although not all predefined tasks include all + * of these data collection types, the predefined task enum flags can be used to explicitly specify + * that a task option not be included. + */ +public enum TaskExcludeOption { + // Exclude the initial instruction steps. + INSTRUCTIONS, + // Exclude the conclusion step. + CONCLUSION, + // Exclude accelerometer data collection. + ACCELEROMETER, + // Exclude device motion data collection. + DEVICE_MOTION, + // Exclude pedometer data collection. + PEDOMETER, + // Exclude location data collection. + LOCATION, + // Exclude heart rate data collection. + HEART_RATE, + // Exclude audio data collection. + AUDIO +} diff --git a/backbone/src/main/java/org/researchstack/backbone/task/factory/TaskFactory.java b/backbone/src/main/java/org/researchstack/backbone/task/factory/TaskFactory.java new file mode 100644 index 000000000..e02ca25ff --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/factory/TaskFactory.java @@ -0,0 +1,84 @@ +package org.researchstack.backbone.task.factory; + +import android.content.Context; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.step.CompletionStep; + +import java.util.Locale; + +import static org.researchstack.backbone.task.factory.TaskFactory.Constants.ConclusionStepIdentifier; + +/** + * Created by TheMDP on 2/15/17. + * + * Central location for the constants shared by the task factory + */ + +public class TaskFactory { + + public static class Constants { + // Recorder Config Identifiers + public static final String AccelerometerRecorderIdentifier = "accel"; + public static final String PedometerRecorderIdentifier = "pedometer"; + public static final String DeviceMotionRecorderIdentifier = "deviceMotion"; + public static final String LocationRecorderIdentifier = "location"; + public static final String AudioRecorderIdentifier = "audio"; + + // Step Identifiers for instructions + public static final String Instruction0StepIdentifier = "instruction"; + public static final String Instruction1StepIdentifier = "instruction1"; + public static final String Instruction2StepIdentifier = "instruction2"; + public static final String Instruction3StepIdentifier = "instruction3"; + public static final String Instruction4StepIdentifier = "instruction4"; + public static final String Instruction5StepIdentifier = "instruction5"; + public static final String Instruction6StepIdentifier = "instruction6"; + public static final String Instruction7StepIdentifier = "instruction7"; + + // Countdown identifiers + public static final String CountdownStepIdentifier = "countdown"; + public static final String Countdown1StepIdentifier = "countdown1"; + public static final String Countdown2StepIdentifier = "countdown2"; + public static final String Countdown3StepIdentifier = "countdown3"; + public static final String Countdown4StepIdentifier = "countdown4"; + public static final String Countdown5StepIdentifier = "countdown5"; + + // Tapping Identifiers + public static final String TappingStepIdentifier = "tapping"; + + // Conclusion Step Identifiers + public static final String ConclusionStepIdentifier = "conclusion"; + + // Active Task Steps Hand Identifier + public static final String ActiveTaskMostAffectedHandIdentifier = "mostAffected"; + public static final String ActiveTaskLeftHandIdentifier = "left"; + public static final String ActiveTaskRightHandIdentifier = "right"; + public static final String ActiveTaskSkipHandStepIdentifier = "skipHand"; + } + + public static CompletionStep makeCompletionStep(Context context) { + String title = context.getString(R.string.rsb_TASK_COMPLETE_TITLE); + String text = context.getString(R.string.rsb_TASK_COMPLETE_TEXT); + return new CompletionStep(ConclusionStepIdentifier, title, text); + } + + /** + * In iOS, this method turns duration into "for X minutes, Y seconds" but in Android, + * you can only localize a duration to be "in X minutes, Y seconds", so use that instead + * @param context can be app or activity, need for resources + * @param durationInSeconds the duration in seconds + * @return a string formatted to "in X minutes, Y seconds" where x & y are from durationInSeconds + */ + public static String convertDurationToString(Context context, int durationInSeconds) { + int minutes = durationInSeconds / 60; + int seconds = durationInSeconds - minutes * 60; + if (minutes > 0) { + return String.format(Locale.getDefault(), "%d %s, %d %s", + minutes, context.getString(R.string.rsb_time_minutes).toLowerCase(), + seconds, context.getString(R.string.rsb_time_seconds).toLowerCase()); + } else { + return String.format(Locale.getDefault(), "%d %s", + seconds, context.getString(R.string.rsb_time_seconds).toLowerCase()); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/task/factory/TremorTaskFactory.java b/backbone/src/main/java/org/researchstack/backbone/task/factory/TremorTaskFactory.java new file mode 100644 index 000000000..08c5ad82c --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/factory/TremorTaskFactory.java @@ -0,0 +1,552 @@ +package org.researchstack.backbone.task.factory; + +import android.content.Context; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.answerformat.AnswerFormat; +import org.researchstack.backbone.answerformat.ChoiceAnswerFormat; +import org.researchstack.backbone.model.Choice; +import org.researchstack.backbone.step.CompletionStep; +import org.researchstack.backbone.step.NavigationQuestionStep; +import org.researchstack.backbone.step.active.recorder.AccelerometerRecorderConfig; +import org.researchstack.backbone.step.active.ActiveStep; +import org.researchstack.backbone.step.active.CountdownStep; +import org.researchstack.backbone.step.InstructionStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.recorder.DeviceMotionRecorderConfig; +import org.researchstack.backbone.step.active.NavigationActiveStep; +import org.researchstack.backbone.task.NavigableOrderedTask; +import org.researchstack.backbone.utils.ResUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +import static org.researchstack.backbone.task.factory.TaskFactory.Constants.*; + +/** + * Created by TheMDP on 2/4/17. + * + * In iOS, they included a bunch of static methods for building OrderedTasks in the + * OrderedTask class. However, I think they belong in this Factory class + */ + +public class TremorTaskFactory { + + static Gson gson; + + // Tremor Step Identifiers + public static final String TremorTestInLapStepIdentifier = "tremor.handInLap"; + public static final String TremorTestExtendArmStepIdentifier = "tremor.handAtShoulderLength"; + public static final String TremorTestBendArmStepIdentifier = "tremor.handAtShoulderLengthWithElbowBent"; + public static final String TremorTestTouchNoseStepIdentifier = "tremor.handToNose"; + public static final String TremorTestTurnWristStepIdentifier = "tremor.handQueenWave"; + + /** + * Returns a predefined task that measures hand tremor. + * + * In a tremor assessment task, the participant is asked to hold the device with their most affected + * hand in various positions while accelerometer and motion data are captured. + * + * @param context can be app or activity, used for resources + * @param identifier The task identifier to use for this task, appropriate to the study. + * @param intendedUseDescription A localized string describing the intended use of the data + * collected. If the value of this parameter is null, none will be used + * @param activeStepDuration The duration for each active step in the task in seconds + * @param tremorOptionList Options that affect which active steps are presented for this task. + * @param handOption Options for determining which hand(s) to test. + * @param taskOptionList Options that affect the features of the predefined task, + * conclusion option will be ignored at this time. + * + * @return An active tremor test task that can be presented with an `ORKTaskViewController` object. + */ + public static NavigableOrderedTask tremorTask( + Context context, + String identifier, + String intendedUseDescription, + int activeStepDuration, + List tremorOptionList, + HandTaskOptions.Hand handOption, + List taskOptionList) + { + // Coin toss for which hand first (in case we're doing both) + final boolean leftFirstIfDoingBoth = (new Random()).nextBoolean(); + return tremorTask(context, identifier, intendedUseDescription, activeStepDuration, + tremorOptionList, handOption, taskOptionList, leftFirstIfDoingBoth); + } + + // This method is separate mainly for unit testing purposes, to eliminate hand randomness + protected static NavigableOrderedTask tremorTask( + Context context, + String identifier, + String intendedUseDescription, + int activeStepDuration, + List tremorOptionList, + HandTaskOptions.Hand handOption, + List taskOptionList, + boolean leftFirstIfDoingBoth) + { + List stepList = new ArrayList<>(); + + final boolean doingBoth = (handOption == HandTaskOptions.Hand.BOTH); + final boolean firstIsLeft = (leftFirstIfDoingBoth && doingBoth) || (!doingBoth && handOption == HandTaskOptions.Hand.LEFT); + + if (!taskOptionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + String title = context.getString(R.string.rsb_TREMOR_TEST_TITLE); + String detailText = context.getString(R.string.rsb_TREMOR_TEST_INTRO_1_DETAIL); + InstructionStep step = new InstructionStep(Instruction0StepIdentifier, title, intendedUseDescription); + step.setMoreDetailText(detailText); + step.setImage(ResUtils.Tremor.IN_HAND); + if (firstIsLeft) { + step.setImage(ResUtils.Tremor.IN_HAND_FLIPPED); + } + stepList.add(step); + } + + // Build the string for the detail texts + String[] detailStringForNumberOfTasks = new String[] { + context.getString(R.string.rsb_TREMOR_TEST_INTRO_2_DETAIL_1_TASK), + context.getString(R.string.rsb_TREMOR_TEST_INTRO_2_DETAIL_2_TASK), + context.getString(R.string.rsb_TREMOR_TEST_INTRO_2_DETAIL_3_TASK), + context.getString(R.string.rsb_TREMOR_TEST_INTRO_2_DETAIL_4_TASK), + context.getString(R.string.rsb_TREMOR_TEST_INTRO_2_DETAIL_5_TASK) + }; + + // Get the actual count for the end index based on the exclusion parameters + int actualTasksIndex = TremorTaskExcludeOption.values().length - tremorOptionList.size() - 1; + + String detailFormat = doingBoth ? + context.getString(R.string.rsb_tremor_test_skip_question_both_hands): + context.getString(R.string.rsb_tremor_test_intro_2_detail_default); + String detailText = String.format(detailFormat, detailStringForNumberOfTasks[actualTasksIndex]); + + NavigationQuestionStep handQuestionStep = null; + if (doingBoth) { + // If doing both hands then ask the user if they need to skip one of the hands + ChoiceAnswerFormat answerFormat = new ChoiceAnswerFormat( + AnswerFormat.ChoiceAnswerStyle.SingleChoice, + new Choice<>(context.getString(R.string.rsb_TREMOR_SKIP_RIGHT_HAND), ActiveTaskRightHandIdentifier), + new Choice<>(context.getString(R.string.rsb_TREMOR_SKIP_LEFT_HAND), ActiveTaskLeftHandIdentifier), + new Choice<>(context.getString(R.string.rsb_TREMOR_SKIP_NEITHER), "") + ); + + String title = context.getString(R.string.rsb_TREMOR_TEST_TITLE); + handQuestionStep = new NavigationQuestionStep(ActiveTaskSkipHandStepIdentifier, title, answerFormat); + handQuestionStep.setText(detailText); + handQuestionStep.setOptional(false); + + stepList.add(handQuestionStep); + } + + // right or most-affected hand + List rightSteps = new ArrayList<>(); + if (handOption == HandTaskOptions.Hand.BOTH || handOption == HandTaskOptions.Hand.RIGHT) { + rightSteps = stepsForOneHandTremorTest(context, identifier, + activeStepDuration, tremorOptionList, firstIsLeft, false, + ActiveTaskRightHandIdentifier, detailText, taskOptionList); + } + + List leftSteps = new ArrayList<>(); + if (handOption == HandTaskOptions.Hand.BOTH || handOption == HandTaskOptions.Hand.LEFT) { + leftSteps = stepsForOneHandTremorTest(context, identifier, + activeStepDuration, tremorOptionList, !firstIsLeft, true, + ActiveTaskLeftHandIdentifier, detailText, taskOptionList); + } + + if (firstIsLeft && !leftSteps.isEmpty()) { + stepList.addAll(leftSteps); + } + + if (!rightSteps.isEmpty()) { + stepList.addAll(rightSteps); + } + + if (!firstIsLeft && !leftSteps.isEmpty()) { + stepList.addAll(leftSteps); + } + + // iOS has the conclusion step optional, but we can't since we don't support step modifiers + // However, there should always be a conclusion step, so this really isn't an issue + CompletionStep completionStep = TaskFactory.makeCompletionStep(context); + stepList.add(completionStep); + final String completionStepId = completionStep.getIdentifier(); + + NavigableOrderedTask task = new NavigableOrderedTask(identifier, stepList); + + // Setup rules for skipping all the steps in either the left or right hand if called upon to do so. + if (doingBoth) { + List firstHandStepList = firstIsLeft ? leftSteps : rightSteps; + List secondHandStepList = firstIsLeft ? rightSteps : leftSteps; + final String secondHandStepId = secondHandStepList.get(0).getIdentifier(); + + // This step can be used to skip the second hand if we need to + final NavigationActiveStep lastStepOfFirstHands = (NavigationActiveStep)firstHandStepList.get(firstHandStepList.size()-1); + + // The question step can be used to skip the first steps if we need to + String handResultString = firstIsLeft ? ActiveTaskLeftHandIdentifier : ActiveTaskRightHandIdentifier; + handQuestionStep.setCustomRules(Collections.singletonList(new NavigableOrderedTask.ObjectEqualsNavigationRule( + handResultString, secondHandStepId, ActiveTaskSkipHandStepIdentifier))); + + // Next add a navigation rule to the end of the first set of hand steps to potentially skip the second steps + String lastStepResultString = firstIsLeft ? ActiveTaskRightHandIdentifier : ActiveTaskLeftHandIdentifier; + lastStepOfFirstHands.setCustomRules(Collections.singletonList(new NavigableOrderedTask.ObjectEqualsNavigationRule( + lastStepResultString, completionStepId, ActiveTaskSkipHandStepIdentifier))); + } + + return task; + } + + private static List stepsForOneHandTremorTest( + Context context, + String identifier, + int activeStepDuration, + List tremorOptionList, + boolean lastHand, + boolean leftHand, + String handIdentifier, + String detailText, + List taskOptionList) + { + List stepList = new ArrayList<>(); + + String stepFinishedInstruction = context.getString( + R.string.rsb_TREMOR_TEST_ACTIVE_STEP_FINISHED_INSTRUCTION); + + boolean rightHand = !leftHand && !ActiveTaskMostAffectedHandIdentifier.equals(handIdentifier); + + /********************************************************************************************* + * Intro Instruction Step + *********************************************************************************************/ + // Bracket blocks for variable encapsulation + { + String stepIdentifier = stepIdentifierWithHandId(Instruction1StepIdentifier, handIdentifier); + String title = context.getString(R.string.rsb_TREMOR_TEST_TITLE); + String text, stepDetailText = null; + if (ActiveTaskMostAffectedHandIdentifier.equals(identifier)) { + text = context.getString(R.string.rsb_TREMOR_TEST_INTRO_2_DEFAULT_TEXT); + stepDetailText = detailText; + } else { + if (leftHand) { + text = context.getString(R.string.rsb_TREMOR_TEST_INTRO_2_LEFT_HAND_TEXT); + } else { + text = context.getString(R.string.rsb_TREMOR_TEST_INTRO_2_RIGHT_HAND_TEXT); + } + } + InstructionStep step = new InstructionStep(stepIdentifier, title, text); + step.setMoreDetailText(stepDetailText); + + step.setImage(ResUtils.Tremor.IN_HAND_2); + if (leftHand) { + step.setImage(ResUtils.Tremor.IN_HAND_2_FLIPPED); + } + + stepList.add(step); + } + + // Obtain sensor frequency for Tremor Task recorders + double sensorFreq = context.getResources().getInteger(R.integer.rsb_sensor_frequency_tremor_task); + + /********************************************************************************************* + * Hand in lap + *********************************************************************************************/ + if (!tremorOptionList.contains(TremorTaskExcludeOption.HAND_IN_LAP)) { + if (!taskOptionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + String stepIdentifier = stepIdentifierWithHandId(Instruction2StepIdentifier, handIdentifier); + String title = context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_IN_LAP_INTRO); + String text = context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_INTRO_TEXT); + + InstructionStep step = new InstructionStep(stepIdentifier, title, text); + + step.setImage(ResUtils.Tremor.HAND_IN_LAP); + if (leftHand) { + step.setTitle(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_IN_LAP_INTRO)); + step.setImage(ResUtils.Tremor.HAND_IN_LAP_FLIPPED); + } else { + step.setTitle(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_IN_LAP_INTRO_RIGHT)); + } + stepList.add(step); + } + + { + String stepIdentifier = stepIdentifierWithHandId(Countdown1StepIdentifier, handIdentifier); + CountdownStep step = new CountdownStep(stepIdentifier); + stepList.add(step); + } + + { + String titleFormat = context.getString(R.string.rsb_tremor_test_active_step_in_lap_instruction_ld); + String stepIdentifier = stepIdentifierWithHandId(TremorTestInLapStepIdentifier, handIdentifier); + NavigationActiveStep step = new NavigationActiveStep(stepIdentifier); + step.setRecorderConfigurationList(Arrays.asList( + new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq), + new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq) + )); + String title = String.format(titleFormat, activeStepDuration); + step.setTitle(title); + step.setSpokenInstruction(title); + step.setFinishedSpokenInstruction(stepFinishedInstruction); + step.setStepDuration(activeStepDuration); + step.setShouldPlaySoundOnStart(true); + step.setShouldVibrateOnStart(true); + step.setShouldPlaySoundOnFinish(true); + step.setShouldVibrateOnFinish(true); + step.setShouldContinueOnFinish(false); + step.setShouldStartTimerAutomatically(true); + + stepList.add(step); + } + } + + /********************************************************************************************* + * Hand at shoulder height + *********************************************************************************************/ + if (!tremorOptionList.contains(TremorTaskExcludeOption.HAND_AT_SHOULDER_HEIGHT)) { + if (!taskOptionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + String stepIdentifier = stepIdentifierWithHandId(Instruction4StepIdentifier, handIdentifier); + String title = context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_EXTEND_ARM_INTRO); + String text = context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_INTRO_TEXT); + InstructionStep step = new InstructionStep(stepIdentifier, title, text); + step.setImage(ResUtils.Tremor.HAND_OUT); + if (leftHand) { + step.setTitle(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_EXTEND_ARM_INTRO_LEFT)); + step.setImage(ResUtils.Tremor.HAND_OUT_FLIPPED); + } else { + step.setTitle(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_EXTEND_ARM_INTRO_RIGHT)); + } + stepList.add(step); + } + + { + String stepIdentifier = stepIdentifierWithHandId(Countdown2StepIdentifier, handIdentifier); + CountdownStep step = new CountdownStep(stepIdentifier); + stepList.add(step); + } + + { + String titleFormat = context.getString(R.string.rsb_tremor_test_active_step_extend_arm_instruction_ld); + String stepIdentifier = stepIdentifierWithHandId(TremorTestExtendArmStepIdentifier, handIdentifier); + NavigationActiveStep step = new NavigationActiveStep(stepIdentifier); + step.setRecorderConfigurationList(Arrays.asList( + new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq), + new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq) + )); + String title = String.format(titleFormat, activeStepDuration); + step.setTitle(title); + step.setSpokenInstruction(title); + step.setFinishedSpokenInstruction(stepFinishedInstruction); + step.setStepDuration(activeStepDuration); + step.setShouldPlaySoundOnStart(true); + step.setShouldVibrateOnStart(true); + step.setShouldPlaySoundOnFinish(true); + step.setShouldVibrateOnFinish(true); + step.setShouldContinueOnFinish(false); + step.setShouldStartTimerAutomatically(true); + + stepList.add(step); + } + } + + /********************************************************************************************* + * Hand at shoulder height and elbow bent + *********************************************************************************************/ + if (!tremorOptionList.contains(TremorTaskExcludeOption.HAND_AT_SHOULDER_HEIGHT_ELBOW_BENT)) { + if (!taskOptionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + String stepIdentifier = stepIdentifierWithHandId(Instruction5StepIdentifier, handIdentifier); + String title = context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_BEND_ARM_INTRO); + String text = context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_INTRO_TEXT); + InstructionStep step = new InstructionStep(stepIdentifier, title, text); + step.setImage(ResUtils.Tremor.ELBOW_BENT); + if (leftHand) { + step.setTitle(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_BEND_ARM_INTRO_LEFT)); + step.setImage(ResUtils.Tremor.ELBOW_BENT_FLIPPED); + } else { + step.setTitle(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_BEND_ARM_INTRO_RIGHT)); + } + stepList.add(step); + } + + { + String stepIdentifier = stepIdentifierWithHandId(Countdown3StepIdentifier, handIdentifier); + CountdownStep step = new CountdownStep(stepIdentifier); + stepList.add(step); + } + + { + String titleFormat = context.getString(R.string.rsb_tremor_test_active_step_bend_arm_instruction_ld); + String stepIdentifier = stepIdentifierWithHandId(TremorTestBendArmStepIdentifier, handIdentifier); + NavigationActiveStep step = new NavigationActiveStep(stepIdentifier); + step.setRecorderConfigurationList(Arrays.asList( + new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq), + new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq) + )); + String title = String.format(titleFormat, activeStepDuration); + step.setTitle(title); + step.setSpokenInstruction(title); + step.setFinishedSpokenInstruction(stepFinishedInstruction); + step.setStepDuration(activeStepDuration); + step.setShouldPlaySoundOnStart(true); + step.setShouldVibrateOnStart(true); + step.setShouldPlaySoundOnFinish(true); + step.setShouldVibrateOnFinish(true); + step.setShouldContinueOnFinish(false); + step.setShouldStartTimerAutomatically(true); + + stepList.add(step); + } + } + + /********************************************************************************************* + * Hand to Nose + *********************************************************************************************/ + if (!tremorOptionList.contains(TremorTaskExcludeOption.HAND_TO_NOSE)) { + if (!taskOptionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + String stepIdentifier = stepIdentifierWithHandId(Instruction6StepIdentifier, handIdentifier); + String title = context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_TOUCH_NOSE_INTRO); + String text = context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_INTRO_TEXT); + InstructionStep step = new InstructionStep(stepIdentifier, title, text); + step.setImage(ResUtils.Tremor.HAND_TO_NOSE); + if (leftHand) { + step.setTitle(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_TOUCH_NOSE_INTRO_LEFT)); + step.setImage(ResUtils.Tremor.HAND_TO_NOSE_FLIPPED); + } else { + step.setTitle(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_TOUCH_NOSE_INTRO_RIGHT)); + } + stepList.add(step); + } + + { + String stepIdentifier = stepIdentifierWithHandId(Countdown4StepIdentifier, handIdentifier); + CountdownStep step = new CountdownStep(stepIdentifier); + stepList.add(step); + } + + { + String titleFormat = context.getString(R.string.rsb_tremor_test_active_step_touch_nose_instruction_ld); + String stepIdentifier = stepIdentifierWithHandId(TremorTestTouchNoseStepIdentifier, handIdentifier); + NavigationActiveStep step = new NavigationActiveStep(stepIdentifier); + step.setRecorderConfigurationList(Arrays.asList( + new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq), + new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq) + )); + String title = String.format(titleFormat, activeStepDuration); + step.setTitle(title); + step.setSpokenInstruction(title); + step.setFinishedSpokenInstruction(stepFinishedInstruction); + step.setStepDuration(activeStepDuration); + step.setShouldPlaySoundOnStart(true); + step.setShouldVibrateOnStart(true); + step.setShouldPlaySoundOnFinish(true); + step.setShouldVibrateOnFinish(true); + step.setShouldContinueOnFinish(false); + step.setShouldStartTimerAutomatically(true); + + stepList.add(step); + } + } + + /********************************************************************************************* + * Queen Wave + *********************************************************************************************/ + if (!tremorOptionList.contains(TremorTaskExcludeOption.QUEEN_WAVE)) { + if (!taskOptionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + String stepIdentifier = stepIdentifierWithHandId(Instruction7StepIdentifier, handIdentifier); + String title = context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_TURN_WRIST_INTRO); + String text = context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_INTRO_TEXT); + InstructionStep step = new InstructionStep(stepIdentifier, title, text); + step.setImage(ResUtils.Tremor.QUEEN_WAVE); + if (leftHand) { + step.setTitle(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_TURN_WRIST_INTRO_LEFT)); + step.setImage(ResUtils.Tremor.QUEEN_WAVE_FLIPPED); + } else { + step.setTitle(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_TURN_WRIST_INTRO_RIGHT)); + } + stepList.add(step); + } + + { + String stepIdentifier = stepIdentifierWithHandId(Countdown5StepIdentifier, handIdentifier); + CountdownStep step = new CountdownStep(stepIdentifier); + stepList.add(step); + } + + { + String titleFormat = context.getString(R.string.rsb_tremor_test_active_step_turn_wrist_instruction_ld); + String stepIdentifier = stepIdentifierWithHandId(TremorTestTurnWristStepIdentifier, handIdentifier); + NavigationActiveStep step = new NavigationActiveStep(stepIdentifier); + step.setRecorderConfigurationList(Arrays.asList( + new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq), + new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq) + )); + String title = String.format(titleFormat, activeStepDuration); + step.setTitle(title); + step.setSpokenInstruction(title); + step.setFinishedSpokenInstruction(stepFinishedInstruction); + step.setStepDuration(activeStepDuration); + step.setShouldPlaySoundOnStart(true); + step.setShouldVibrateOnStart(true); + step.setShouldPlaySoundOnFinish(true); + step.setShouldVibrateOnFinish(true); + step.setShouldContinueOnFinish(false); + step.setShouldStartTimerAutomatically(true); + + stepList.add(step); + } + } + + // fix the spoken instruction on the last included step, depending on which hand we're on + ActiveStep lastStep = (ActiveStep)stepList.get(stepList.size()-1); + if (lastHand) { + lastStep.setFinishedSpokenInstruction(context.getString(R.string.rsb_TREMOR_TEST_COMPLETED_INSTRUCTION)); + } else if (leftHand) { + lastStep.setFinishedSpokenInstruction(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_SWITCH_HANDS_RIGHT_INSTRUCTION)); + } else { + lastStep.setFinishedSpokenInstruction(context.getString(R.string.rsb_TREMOR_TEST_ACTIVE_STEP_SWITCH_HANDS_LEFT_INSTRUCTION)); + } + + return stepList; + } + + protected static String stepIdentifierWithHandId(String stepId, String handId) { + if (handId == null) { + return stepId; + } + return String.format("%s.%s", stepId, handId); + } + + /** + * The `TremorTaskExcludeOption` enum lets you exclude particular steps from the predefined active + * tasks in the predefined Tremor `OrderedTask`. + * + * By default, all predefined active tasks will be included. The tremor active task option flags can + * be used to explicitly specify that an active task is not to be included. + */ + public enum TremorTaskExcludeOption { + // Exclude the hand-in-lap steps. + @SerializedName("handInLap") + HAND_IN_LAP, + // Exclude the hand-extended-at-shoulder-height steps. + @SerializedName("handAtShoulderLength") + HAND_AT_SHOULDER_HEIGHT, + // Exclude the hand-extended-at-shoulder-height steps. + @SerializedName("elbowBent") + HAND_AT_SHOULDER_HEIGHT_ELBOW_BENT, + // Exclude the elbow-bent-touch-nose steps. + @SerializedName("touchNose") + HAND_TO_NOSE, + // Exclude the queen-wave steps. + @SerializedName("handQueenWave") + QUEEN_WAVE + } + + public static TremorTaskExcludeOption toTremorExcludeOption(String tremorSerializedName) { + if (gson == null) { + gson = new Gson(); + } + return gson.fromJson(tremorSerializedName, TremorTaskExcludeOption.class); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/task/factory/WalkingTaskFactory.java b/backbone/src/main/java/org/researchstack/backbone/task/factory/WalkingTaskFactory.java new file mode 100644 index 000000000..9780a3db2 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/task/factory/WalkingTaskFactory.java @@ -0,0 +1,573 @@ +package org.researchstack.backbone.task.factory; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.LocationManager; +import android.os.Build; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.answerformat.AnswerFormat; +import org.researchstack.backbone.answerformat.BooleanAnswerFormat; +import org.researchstack.backbone.answerformat.ChoiceAnswerFormat; +import org.researchstack.backbone.model.Choice; +import org.researchstack.backbone.step.FormStep; +import org.researchstack.backbone.step.InstructionStep; +import org.researchstack.backbone.step.PermissionsStep; +import org.researchstack.backbone.step.QuestionStep; +import org.researchstack.backbone.step.RequireSystemFeatureStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.TimedWalkStep; +import org.researchstack.backbone.step.active.recorder.AccelerometerRecorderConfig; +import org.researchstack.backbone.step.active.CountdownStep; +import org.researchstack.backbone.step.active.FitnessStep; +import org.researchstack.backbone.step.active.recorder.DeviceMotionRecorderConfig; +import org.researchstack.backbone.step.active.recorder.LocationRecorderConfig; +import org.researchstack.backbone.step.active.recorder.PedometerRecorderConfig; +import org.researchstack.backbone.step.active.recorder.RecorderConfig; +import org.researchstack.backbone.step.active.WalkingTaskStep; +import org.researchstack.backbone.task.OrderedTask; +import org.researchstack.backbone.utils.FormatHelper; +import org.researchstack.backbone.utils.ResUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import static org.researchstack.backbone.task.factory.TaskFactory.Constants.*; + +/** + * Created by TheMDP on 2/15/17. + * + * In iOS, they included a bunch of static methods for building OrderedTasks in the + * OrderedTask class. However, this class was created to furthur encapsulate the creation + * of Walking Tasks, specifically the timed walking task, walking back and forth task, and + * the short walking task. + */ + +public class WalkingTaskFactory { + + private static final float DEFAULT_STEP_DURATION_FALLBACK_FACTOR = 1.5f; + private static final int DEFAULT_COUNTDOWN_DURATION = 5; // in seconds + + private static final int IGNORE_NUMBER_OF_STEPS = Integer.MAX_VALUE; + private static final int SPEAK_WALK_DURATION_HALFWAY_THRESHOLD = 20; // in seconds + + public static final String GpsFeatureStepIdentifier = "gpsfeature"; + public static final String LocationPermissionsStepIdentifier = "locationpermission"; + public static final String ShortWalkOutboundStepIdentifier = "walking.outbound"; + public static final String ShortWalkReturnStepIdentifier = "walking.return"; + public static final String ShortWalkRestStepIdentifier = "walking.rest"; + public static final String TimedWalkFormStepIdentifier = "timed.walk.form"; + public static final String TimedWalkFormAFOStepIdentifier = "timed.walk.form.afo"; + public static final String TimedWalkFormAssistanceStepIdentifier = "timed.walk.form.assistance"; + public static final String TimedWalkTrial1StepIdentifier = "timed.walk.trial1"; + public static final String TimedWalkTurnAroundStepIdentifier = "timed.walk.turn.around"; + public static final String TimedWalkTrial2StepIdentifier = "timed.walk.trial2"; + + /** + * Returns a predefined task that consists of a short walk. + * + * In a short walk task, the participant is asked to walk a short distance, which may be indoors. + * Typical uses of the resulting data are to assess stride length, smoothness, sway, or other aspects + * of the participant's gait. + * + * The presentation of the short walk task differs from the fitness check task in that the distance is + * replaced by the number of steps taken, and the walk is split into a series of legs. After each leg, + * the user is asked to turn and reverse direction. + * + * The data collected by this task can include accelerometer, device motion, and pedometer data. + * + * @param context can be app or activity, used for resources + * @param identifier The task identifier to use for this task, appropriate to the study. + * @param intendedUseDescription A localized string describing the intended use of the data + * collected. If the value of this parameter is `nil`, the default + * localized text is displayed. + * @param numberOfStepsPerLeg The number of steps the participant is asked to walk. If the + * pedometer is unavailable, a distance is suggested and a suitable + * count down timer is displayed for each leg of the walk. + * @param restDuration The duration of the rest period in seconds. When the value of + * this parameter is nonzero, the user is asked to stand still + * for the specified rest period after the turn sequence + * has been completed, and baseline data is collected. + * @param optionList Options that affect the features of the predefined task. + * + * @return An active short walk task that can be presented with an `ActiveTaskActivity` object. + */ + public static OrderedTask shortWalkTask( + Context context, + String identifier, + String intendedUseDescription, + int numberOfStepsPerLeg, + int restDuration, + List optionList) + { + List stepList = new ArrayList<>(); + + // Obtain sensor frequency for Walking Task recorders + double sensorFreq = context.getResources().getInteger(R.integer.rsb_sensor_frequency_walking_task); + + if (!optionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + { + String title = context.getString(R.string.rsb_WALK_TASK_TITLE); + InstructionStep step = new InstructionStep(Instruction0StepIdentifier, title, intendedUseDescription); + step.setMoreDetailText(context.getString(R.string.rsb_WALK_INTRO_TEXT)); + stepList.add(step); + } + + { + String title = context.getString(R.string.rsb_WALK_TASK_TITLE); + String textFormat = context.getString(R.string.rsb_walk_intro_2_text_ld); + String text = String.format(textFormat, numberOfStepsPerLeg); + InstructionStep step = new InstructionStep(Instruction1StepIdentifier, title, text); + step.setMoreDetailText(context.getString(R.string.rsb_WALK_INTRO_2_DETAIL)); + step.setImage(ResUtils.PHONE_IN_POCKET); + stepList.add(step); + } + } + + { + CountdownStep step = new CountdownStep(CountdownStepIdentifier); + step.setStepDuration(DEFAULT_COUNTDOWN_DURATION); + stepList.add(step); + } + + { + { + List recorderConfigList = new ArrayList<>(); + if (!optionList.contains(TaskExcludeOption.PEDOMETER)) { + recorderConfigList.add(new PedometerRecorderConfig(PedometerRecorderIdentifier)); + } + if (!optionList.contains(TaskExcludeOption.ACCELEROMETER)) { + recorderConfigList.add(new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq)); + } + if (!optionList.contains(TaskExcludeOption.DEVICE_MOTION)) { + recorderConfigList.add(new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq)); + } + + { + WalkingTaskStep step = new WalkingTaskStep(ShortWalkOutboundStepIdentifier); + String titleFormat = context.getString(R.string.rsb_WALK_OUTBOUND_INSTRUCTION_FORMAT); + String title = String.format(titleFormat, numberOfStepsPerLeg); + step.setTitle(title); + step.setSpokenInstruction(step.getTitle()); + step.setRecorderConfigurationList(recorderConfigList); + step.setShouldContinueOnFinish(true); + step.setShouldStartTimerAutomatically(true); + step.setStepDuration(computeFallbackDuration(numberOfStepsPerLeg)); + step.setNumberOfStepsPerLeg(numberOfStepsPerLeg); + step.setShouldVibrateOnStart(true); + step.setShouldPlaySoundOnStart(true); + stepList.add(step); + } + } + + { + List recorderConfigList = new ArrayList<>(); + if (!optionList.contains(TaskExcludeOption.PEDOMETER)) { + recorderConfigList.add(new PedometerRecorderConfig(PedometerRecorderIdentifier)); + } + if (!optionList.contains(TaskExcludeOption.ACCELEROMETER)) { + recorderConfigList.add(new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq)); + } + if (!optionList.contains(TaskExcludeOption.DEVICE_MOTION)) { + recorderConfigList.add(new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq)); + } + + { + WalkingTaskStep step = new WalkingTaskStep(ShortWalkReturnStepIdentifier); + step.setTitle(context.getString(R.string.rsb_WALK_RETURN_INSTRUCTION_FORMAT)); + step.setSpokenInstruction(step.getTitle()); + step.setRecorderConfigurationList(recorderConfigList); + step.setShouldContinueOnFinish(true); + step.setShouldStartTimerAutomatically(true); + step.setStepDuration(computeFallbackDuration(numberOfStepsPerLeg)); + step.setNumberOfStepsPerLeg(numberOfStepsPerLeg); + step.setShouldVibrateOnStart(true); + step.setShouldPlaySoundOnStart(true); + stepList.add(step); + } + } + + if (restDuration > 0) { + if (restDuration > 0) { + List recorderConfigList = new ArrayList<>(); + if (!optionList.contains(TaskExcludeOption.ACCELEROMETER)) { + recorderConfigList.add(new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq)); + } + if (!optionList.contains(TaskExcludeOption.DEVICE_MOTION)) { + recorderConfigList.add(new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq)); + } + + FitnessStep step = new FitnessStep(ShortWalkRestStepIdentifier); + + String titleFormat = context.getString(R.string.rsb_WALK_STAND_INSTRUCTION_FORMAT); + String title = String.format(titleFormat, TaskFactory.convertDurationToString(context, restDuration)); + step.setTitle(title); + String voiceTitleFormat = context.getString(R.string.rsb_WALK_STAND_VOICE_INSTRUCTION_FORMAT); + String voiceTitle = String.format(voiceTitleFormat, TaskFactory.convertDurationToString(context, restDuration)); + step.setSpokenInstruction(voiceTitle); + step.setRecorderConfigurationList(recorderConfigList); + step.setShouldShowDefaultTimer(true); + step.setShouldContinueOnFinish(true); + step.setShouldStartTimerAutomatically(true); + step.setStepDuration(restDuration); + step.setShouldVibrateOnStart(true); + step.setShouldPlaySoundOnStart(true); + step.setShouldVibrateOnFinish(true); + step.setShouldPlaySoundOnFinish(true); + stepList.add(step); + } + } + } + + if (!optionList.contains(TaskExcludeOption.CONCLUSION)) { + stepList.add(TaskFactory.makeCompletionStep(context)); + } + + return new OrderedTask(identifier, stepList); + } + + /** + * Returns a predefined task that consists of a short walk back and forth. + * + * In a short walk task, the participant is asked to walk a short distance, which may be indoors. + * Typical uses of the resulting data are to assess stride length, smoothness, sway, or other aspects + * of the participant's gait. + * + * The presentation of the back and forth walk task differs from the short walk in that the participant + * is asked to walk back and forth rather than walking in a straight line for a certain number of steps. + * + * The participant is then asked to turn in a full circle and then stand still. + * + * This task is intended to allow the participant to walk in a confined space where the participant + * does not have access to a long hallway to walk in a continuous straight line. Additionally, by asking + * the participant to turn in a full circle and then stand still, the activity can access balance and + * concentration. + * + * The data collected by this task can include accelerometer, device motion, and pedometer data. + * + * @param context can be app or activity, used for resources + * @param identifier The task identifier to use for this task, appropriate to the study. + * @param intendedUseDescription A localized string describing the intended use of the data + * collected. If the value of this parameter is `nil`, the default + * localized text is displayed. + * @param walkDuration The duration of the walking period in seconds. + * @param restDuration The duration of the rest period in seconds. + * When the value of this parameter is + * nonzero, the user is asked to stand still for the specified rest + * period after the turn sequence has been completed, and baseline + * data is collected. + * @param optionList Options that affect the features of the predefined task. + * + * @return An active short walk task that can be presented with an `ActiveTaskActivity` object. + */ + public static OrderedTask walkBackAndForthTask( + Context context, + String identifier, + String intendedUseDescription, + int walkDuration, + int restDuration, + List optionList) + { + List stepList = new ArrayList<>(); + + // Obtain sensor frequency for Walking Task recorders + double sensorFreq = context.getResources().getInteger(R.integer.rsb_sensor_frequency_walking_task); + + if (!optionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + { + String title = context.getString(R.string.rsb_WALK_TASK_TITLE); + InstructionStep step = new InstructionStep(Instruction0StepIdentifier, title, intendedUseDescription); + step.setMoreDetailText(context.getString(R.string.rsb_WALK_INTRO_TEXT)); + stepList.add(step); + } + + { + String title = context.getString(R.string.rsb_WALK_TASK_TITLE); + String text = context.getString(R.string.rsb_WALK_INTRO_2_TEXT_BACK_AND_FORTH_INSTRUCTION); + InstructionStep step = new InstructionStep(Instruction1StepIdentifier, title, text); + step.setMoreDetailText(context.getString(R.string.rsb_WALK_INTRO_2_DETAIL_BACK_AND_FORTH_INSTRUCTION)); + step.setImage(ResUtils.PHONE_IN_POCKET); + stepList.add(step); + } + } + + { + CountdownStep step = new CountdownStep(CountdownStepIdentifier); + step.setStepDuration(DEFAULT_COUNTDOWN_DURATION); + stepList.add(step); + } + + { + { + List recorderConfigList = new ArrayList<>(); + if (!optionList.contains(TaskExcludeOption.PEDOMETER)) { + recorderConfigList.add(new PedometerRecorderConfig(PedometerRecorderIdentifier)); + } + if (!optionList.contains(TaskExcludeOption.ACCELEROMETER)) { + recorderConfigList.add(new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq)); + } + if (!optionList.contains(TaskExcludeOption.DEVICE_MOTION)) { + recorderConfigList.add(new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq)); + } + + { + WalkingTaskStep step = new WalkingTaskStep(ShortWalkOutboundStepIdentifier); + String titleFormat = context.getString(R.string.rsb_WALK_BACK_AND_FORTH_INSTRUCTION_FORMAT); + String title = String.format(titleFormat, TaskFactory.convertDurationToString(context, walkDuration)); + step.setTitle(title); + step.setSpokenInstruction(title); + step.setRecorderConfigurationList(recorderConfigList); + step.setShouldContinueOnFinish(true); + step.setShouldStartTimerAutomatically(true); + step.setStepDuration(walkDuration); + step.setNumberOfStepsPerLeg(IGNORE_NUMBER_OF_STEPS); + step.setShouldSpeakRemainingTimeAtHalfway(walkDuration > SPEAK_WALK_DURATION_HALFWAY_THRESHOLD); + step.setShouldVibrateOnStart(true); + step.setShouldPlaySoundOnStart(true); + stepList.add(step); + } + } + + if (restDuration > 0) { + if (restDuration > 0) { + List recorderConfigList = new ArrayList<>(); + if (!optionList.contains(TaskExcludeOption.ACCELEROMETER)) { + recorderConfigList.add(new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq)); + } + if (!optionList.contains(TaskExcludeOption.DEVICE_MOTION)) { + recorderConfigList.add(new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq)); + } + + FitnessStep step = new FitnessStep(ShortWalkRestStepIdentifier); + + String titleFormat = context.getString(R.string.rsb_WALK_BACK_AND_FORTH_STAND_INSTRUCTION_FORMAT); + String title = String.format(titleFormat, TaskFactory.convertDurationToString(context, restDuration)); + step.setTitle(title); + step.setSpokenInstruction(title); + step.setRecorderConfigurationList(recorderConfigList); + step.setShouldContinueOnFinish(true); + step.setShouldStartTimerAutomatically(true); + step.setStepDuration(restDuration); + step.setShouldVibrateOnStart(true); + step.setShouldPlaySoundOnStart(true); + step.setShouldVibrateOnFinish(true); + step.setShouldPlaySoundOnFinish(true); + step.setFinishedSpokenInstruction(context.getString(R.string.rsb_WALK_BACK_AND_FORTH_FINISHED_VOICE)); + step.setShouldSpeakRemainingTimeAtHalfway(restDuration > SPEAK_WALK_DURATION_HALFWAY_THRESHOLD); + stepList.add(step); + } + } + } + + if (!optionList.contains(TaskExcludeOption.CONCLUSION)) { + stepList.add(TaskFactory.makeCompletionStep(context)); + } + + return new OrderedTask(identifier, stepList); + } + + /** + * Returns a predefined task that consists of a timed walk. + * + * In a timed walk task, the participant is asked to walk for a specific distance as quickly as + * possible, but safely. The task is immediately administered again by having the patient walk back + * the same distance. + * A timed walk task can be used to measure lower extremity function. + * + * The presentation of the timed walk task differs from both the fitness check task and the short + * walk task in that the distance is fixed. After a first walk, the user is asked to turn and reverse + * direction. + * + * The data collected by this task can include accelerometer, device motion, pedometer data, + * and location where available. + * + * Data collected by the task is in the form of an `TimedWalkResult` object. + * + * @param context Can be app or activity, used to get resources + * @param identifier The task identifier to use for this task, appropriate to the study. + * @param intendedUseDescription A localized string describing the intended use of the data + * collected. If the value of this parameter is `nil`, the default + * localized text is displayed. + * @param distanceInMeters The timed walk distance in meters. + * @param timeLimit The time limit to complete the trials in seconds + * @param turnAroundTimeLimit The turn around time limit in seconds + * @param includeAssistiveDeviceForm A Boolean value that indicates whether to inlude the form step + * about the usage of an assistive device. + * @param optionList Options that affect the features of the predefined task. + * + * @return An active timed walk task that can be presented with an `ORKTaskViewController` object. + */ + public static OrderedTask timedWalkTask( + Context context, + String identifier, + String intendedUseDescription, + double distanceInMeters, + int timeLimit, + int turnAroundTimeLimit, + boolean includeAssistiveDeviceForm, + List optionList) + { + List stepList = new ArrayList<>(); + + // In-app permissions were added in Android 6.0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // This isn't in iOS, but in Android we need to check for this so that location permission is granted + PackageManager pm = context.getPackageManager(); + int hasPerm = pm.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION, context.getPackageName()); + if (hasPerm != PackageManager.PERMISSION_GRANTED) { + // include a permission request step that requires location + String title = context.getString(R.string.rsb_permission_location_title); + String text = context.getString(R.string.rsb_permission_location_desc); + stepList.add(new PermissionsStep(LocationPermissionsStepIdentifier, title, text)); + } + } + + // We also need to check if GPS is turned on, and turn it on if it is not + LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + String title = context.getString(R.string.rsb_system_feature_gps_title); + String text = context.getString(R.string.rsb_system_feature_gps_text); + stepList.add(new RequireSystemFeatureStep( + RequireSystemFeatureStep.SystemFeature.GPS, GpsFeatureStepIdentifier, title, text)); + } + + if (!optionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + String title = context.getString(R.string.rsb_TIMED_WALK_TITLE); + InstructionStep step = new InstructionStep(Instruction0StepIdentifier, title, intendedUseDescription); + step.setMoreDetailText(context.getString(R.string.rsb_TIMED_WALK_INTRO_DETAIL)); + stepList.add(step); + } + + if (includeAssistiveDeviceForm) { + + BooleanAnswerFormat answerFormat1 = new BooleanAnswerFormat( + context.getString(R.string.rsb_BOOL_YES), + context.getString(R.string.rsb_BOOL_NO)); + QuestionStep questionStep1 = new QuestionStep(TimedWalkFormAFOStepIdentifier, null, answerFormat1); + questionStep1.setText(context.getString(R.string.rsb_TIMED_WALK_QUESTION_TEXT)); + questionStep1.setOptional(false); + + String choice1Text = context.getString(R.string.rsb_TIMED_WALK_QUESTION_2_CHOICE); + Choice choice1 = new Choice<>(choice1Text, choice1Text); + + String choice2Text = context.getString(R.string.rsb_TIMED_WALK_QUESTION_2_CHOICE_2); + Choice choice2 = new Choice<>(choice2Text, choice2Text); + + String choice3Text = context.getString(R.string.rsb_TIMED_WALK_QUESTION_2_CHOICE_3); + Choice choice3 = new Choice<>(choice3Text, choice3Text); + + String choice4Text = context.getString(R.string.rsb_TIMED_WALK_QUESTION_2_CHOICE_4); + Choice choice4 = new Choice<>(choice4Text, choice4Text); + + String choice5Text = context.getString(R.string.rsb_TIMED_WALK_QUESTION_2_CHOICE_5); + Choice choice5 = new Choice<>(choice5Text, choice5Text); + + String choice6Text = context.getString(R.string.rsb_TIMED_WALK_QUESTION_2_CHOICE_6); + Choice choice6 = new Choice<>(choice6Text, choice6Text); + + ChoiceAnswerFormat answerFormat2 = new ChoiceAnswerFormat( + AnswerFormat.ChoiceAnswerStyle.SingleChoice, + choice1, choice2, choice3, choice4, choice5, choice6); + + QuestionStep questionStep2 = new QuestionStep(TimedWalkFormAssistanceStepIdentifier, null, answerFormat2); + questionStep2.setText(context.getString(R.string.rsb_TIMED_WALK_QUESTION_2_TITLE)); + questionStep2.setPlaceholder(context.getString(R.string.rsb_TIMED_WALK_QUESTION_2_TEXT)); + questionStep2.setOptional(false); + + String formStepTitle = context.getString(R.string.rsb_TIMED_WALK_FORM_TITLE); + String formStepText = context.getString(R.string.rsb_TIMED_WALK_FORM_TEXT); + + List questionStepList = Arrays.asList(questionStep1, questionStep2); + FormStep formStep = new FormStep(TimedWalkFormStepIdentifier, formStepTitle, formStepText, questionStepList); + + stepList.add(formStep); + } + + String formattedLength = FormatHelper.localizeDistance(context, distanceInMeters, Locale.getDefault()); + + if (!optionList.contains(TaskExcludeOption.INSTRUCTIONS)) { + String title = context.getString(R.string.rsb_TIMED_WALK_TITLE); + String textFormat = context.getString(R.string.rsb_timed_walk_intro_2_text); + String text = String.format(textFormat, formattedLength); + InstructionStep step = new InstructionStep(Instruction1StepIdentifier, title, text); + step.setMoreDetailText(context.getString(R.string.rsb_TIMED_WALK_INTRO_2_DETAIL)); + step.setImage(ResUtils.TIMER); + stepList.add(step); + } + + { + CountdownStep step = new CountdownStep(CountdownStepIdentifier); + step.setStepDuration(DEFAULT_COUNTDOWN_DURATION); + stepList.add(step); + } + + // Obtain sensor frequency for Walking Task recorders + double sensorFreq = context.getResources().getInteger(R.integer.rsb_sensor_frequency_walking_task); + + { + List recorderConfigList = new ArrayList<>(); + if (!optionList.contains(TaskExcludeOption.PEDOMETER)) { + recorderConfigList.add(new PedometerRecorderConfig(PedometerRecorderIdentifier)); + } + if (!optionList.contains(TaskExcludeOption.ACCELEROMETER)) { + recorderConfigList.add(new AccelerometerRecorderConfig(AccelerometerRecorderIdentifier, sensorFreq)); + } + if (!optionList.contains(TaskExcludeOption.DEVICE_MOTION)) { + recorderConfigList.add(new DeviceMotionRecorderConfig(DeviceMotionRecorderIdentifier, sensorFreq)); + } + if (!optionList.contains(TaskExcludeOption.LOCATION)) { + recorderConfigList.add(new LocationRecorderConfig(LocationRecorderIdentifier)); + } + + { + String titleFormat = context.getString(R.string.rsb_timed_walk_instruction); + String title = String.format(titleFormat, formattedLength); + String text = context.getString(R.string.rsb_TIMED_WALK_INSTRUCTION_TEXT); + TimedWalkStep step = new TimedWalkStep(TimedWalkTrial1StepIdentifier, title, text, distanceInMeters); + step.setSpokenInstruction(title); + step.setRecorderConfigurationList(recorderConfigList); + step.setStepDuration(timeLimit == 0 ? Integer.MAX_VALUE : timeLimit); + step.setImageResName(ResUtils.TimedWalking.MAN_OUTBOUND); + stepList.add(step); + } + + { + String title = context.getString(R.string.rsb_TIMED_WALK_INSTRUCTION_TURN); + String text = context.getString(R.string.rsb_TIMED_WALK_INSTRUCTION_TEXT); + TimedWalkStep step = new TimedWalkStep(TimedWalkTurnAroundStepIdentifier, title, text, 1); + step.setSpokenInstruction(title); + step.setRecorderConfigurationList(recorderConfigList); + step.setStepDuration(turnAroundTimeLimit == 0 ? Integer.MAX_VALUE : turnAroundTimeLimit); + step.setImageResName(ResUtils.TimedWalking.TURNAROUND); + stepList.add(step); + } + + { + String title = context.getString(R.string.rsb_TIMED_WALK_INSTRUCTION_2); + String text = context.getString(R.string.rsb_TIMED_WALK_INSTRUCTION_TEXT); + TimedWalkStep step = new TimedWalkStep(TimedWalkTrial2StepIdentifier, title, text, distanceInMeters); + step.setSpokenInstruction(title); + step.setRecorderConfigurationList(recorderConfigList); + step.setStepDuration(timeLimit == 0 ? Integer.MAX_VALUE : timeLimit); + step.setImageResName(ResUtils.TimedWalking.MAN_RETURN); + stepList.add(step); + } + } + + if (!optionList.contains(TaskExcludeOption.CONCLUSION)) { + stepList.add(TaskFactory.makeCompletionStep(context)); + } + + return new OrderedTask(identifier, stepList); + } + + /** + * @return a step duration value that can be used if number of steps takes too long + */ + private static int computeFallbackDuration(int numberOfStepsPerLeg) { + return (int)(numberOfStepsPerLeg * DEFAULT_STEP_DURATION_FALLBACK_FACTOR); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/ActiveTaskActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/ActiveTaskActivity.java new file mode 100644 index 000000000..81f1c91ad --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/ActiveTaskActivity.java @@ -0,0 +1,289 @@ +package org.researchstack.backbone.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import android.view.Surface; +import android.view.WindowManager; + +import org.researchstack.backbone.PermissionRequestManager; +import org.researchstack.backbone.R; +import org.researchstack.backbone.result.FileResult; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.result.logger.DataLoggerManager; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.ActiveStep; +import org.researchstack.backbone.step.active.ActiveTaskAndResultListener; +import org.researchstack.backbone.step.active.CountdownStep; +import org.researchstack.backbone.task.Task; +import org.researchstack.backbone.ui.callbacks.ActivityCallback; +import org.researchstack.backbone.ui.step.layout.ActiveStepLayout; +import org.researchstack.backbone.ui.step.layout.StepLayout; +import org.researchstack.backbone.ui.step.layout.StepPermissionRequest; +import org.researchstack.backbone.utils.LogExt; + +import java.io.File; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Created by TheMDP on 2/8/17. + * + * The ActiveTaskActivity is responsible for displaying any task with an ActiveStepLayout + * It will manage the DataLogger files that are created, make sure none of the dirty ones leak, + * and make sure that they are correctly bundled and uploaded at the end + */ + +public class ActiveTaskActivity extends ViewTaskActivity + implements ActivityCallback, ActiveTaskAndResultListener { + + public static final String ACTIVITY_TASK_RESULT_KEY = "ACTIVITY_TASK_RESULT_KEY"; + + protected boolean isBackButtonEnabled; + private boolean isPaused = false; + + public static Intent newIntent(Context context, Task task) { + Intent intent = new Intent(context, ActiveTaskActivity.class); + intent.putExtra(EXTRA_TASK, task); + return intent; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + init(); + } + + @Override + public void onResume() { + super.onResume(); + isPaused = false; + } + + @Override + public void onPause() { + super.onPause(); + this.isPaused = true; + if (currentStepLayout != null && currentStepLayout instanceof ActiveStepLayout) { + ((ActiveStepLayout)currentStepLayout).pauseActiveStepLayout(); + } + } + + public boolean getIsPaused() { + return isPaused; + } + + protected void init() { + if (!DataLoggerManager.isInitialized()) { + DataLoggerManager.initialize(this); + DataLoggerManager.getInstance().deleteAllDirtyFiles(); + } + } + + @Override + protected void discardResultsAndFinish() { + if (currentStepLayout != null && currentStepLayout instanceof ActiveStepLayout) { + ((ActiveStepLayout) currentStepLayout).pauseActiveStepLayout(); + // Pause may cause the currentStepLayout to change, so check again + if (currentStepLayout != null && currentStepLayout instanceof ActiveStepLayout) { + ((ActiveStepLayout) currentStepLayout).forceStop(); + } + } + currentStepLayout = null; + DataLoggerManager.getInstance().deleteAllDirtyFiles(); + super.discardResultsAndFinish(); + } + + @Override + public void showStep(Step step, boolean alwaysReplaceView) { + + LogExt.d(ActiveTaskActivity.class, + "showStep(" + step.getIdentifier() + ", " + alwaysReplaceView + ")"); + + // compute back button status while currentStep is actually the previousStep at this point + isBackButtonEnabled = + (!(step instanceof ActiveStep) || (step instanceof CountdownStep)) && + !(currentStep instanceof ActiveStep); // currentStep is one previously showing at this point + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(isBackButtonEnabled); + } + + if (isStepAndLayoutStillValid(step)) { + // The step was not killed, it is probably running with the RecorderService + // Instead of re-creating it, just signal to it that it should resume + ((ActiveStepLayout)currentStepLayout).resumeActiveStepLayout(); + } else { + super.showStep(step, alwaysReplaceView); + } + + // Active steps lock screen on and orientation so that the view is not unnecessarily + // destroyed and recreated while the data logger is recording + if (step instanceof ActiveStep) { + lockScreenOn(); + lockOrientation(); + } else { + unlockScreenOn(); + unlockOrientation(); + } + } + + @Override + protected void setupStepLayoutBeforeInitializeIsCalled(StepLayout stepLayout) { + super.setupStepLayoutBeforeInitializeIsCalled(stepLayout); + if (stepLayout instanceof ActiveStepLayout) { + ((ActiveStepLayout)stepLayout).setTaskAndResultListener(this); + } + } + + @Override + public void notifyStepOfBackPress() { + // intercept and block any back buttons + if (isBackButtonEnabled) { + super.notifyStepOfBackPress(); + } + } + + private boolean isStepAndLayoutStillValid(Step step) { + return step instanceof ActiveStep && step.equals(currentStep) && + currentStepLayout != null && currentStepLayout instanceof ActiveStepLayout; + } + + @Override + public void setRequestedOrientation(int requestedOrientation) { + super.setRequestedOrientation(requestedOrientation); + } + + @Override + protected void saveAndFinish() { + + // just in case we were locking these + unlockScreenOn(); + unlockOrientation(); + + taskResult.getTaskDetails().put(ACTIVITY_TASK_RESULT_KEY, true); + taskResult.setEndDate(new Date()); + + // Loop through and find all the FileResult files + List fileList = new ArrayList<>(); + Map stepResultMap = taskResult.getResults(); + for (String key : stepResultMap.keySet()) { + StepResult stepResult = stepResultMap.get(key); + if (stepResult != null) { + Map resultMap = stepResult.getResults(); + if (resultMap != null) { + for (Object resultKey : resultMap.keySet()) { + Object value = resultMap.get(resultKey); + if (value != null && value instanceof FileResult) { + FileResult fileResult = (FileResult)value; + fileList.add(fileResult.getFile()); + } + } + } + } + } + // Since we are now about to try and upload the files to the server, let's update their status + // These files will now stick around until we have successfully uploaded them + DataLoggerManager.getInstance().updateFileListToAttemptedUploadStatus(fileList); + super.saveAndFinish(); + } + + /** + * Active Steps lock screen to on so it can avoid any interruptions during data logging + */ + private void lockScreenOn() { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void unlockScreenOn() { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + /** + * Active Steps lock orientation so it can avoid any interruptions during data logging + */ + protected void lockOrientation() { + int orientation; + int rotation = ((WindowManager) getSystemService( + Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation(); + switch (rotation) { + case Surface.ROTATION_0: + orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + break; + case Surface.ROTATION_90: + orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + break; + case Surface.ROTATION_180: + orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; + break; + case Surface.ROTATION_270: + default: + orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + break; + } + setRequestedOrientation(orientation); + } + + protected void unlockOrientation() { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Override + protected void onExecuteStepAction(int action) { + // In this case, we cannot complete the Active Task, since one of the ActiveSteps + // Requested we end the task, because it couldn't complete for some reason + if (action == ACTION_END && currentStep instanceof ActiveStep) { + discardResultsAndFinish(); + } else { + super.onExecuteStepAction(action); + } + } + + @TargetApi(Build.VERSION_CODES.M) + @Override + public void onRequestPermission(String id) { + if (PermissionRequestManager.getInstance().isNonSystemPermission(id)) { + PermissionRequestManager.getInstance().onRequestNonSystemPermission(this, id); + } else { + requestPermissions(new String[] {id}, PermissionRequestManager.PERMISSION_REQUEST_CODE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if(requestCode == PermissionRequestManager.PERMISSION_REQUEST_CODE) { + updateStepLayoutForPermission(); + } + } + + protected void updateStepLayoutForPermission() { + StepLayout stepLayout = (StepLayout) findViewById(R.id.rsb_current_step); + if(stepLayout instanceof StepPermissionRequest) { + ((StepPermissionRequest) stepLayout).onUpdateForPermissionResult(); + } + } + + @Override + public void startConsentTask() { + // deprecated + } + + // Only called for ActiveStepLayouts + @Override + public TaskResult activeTaskActivityResult() { + return taskResult; + } + + // Only called for ActiveStepLayouts + @Override + public Task activeTaskActivityGetTask() { + return task; + } +} diff --git a/skin/src/main/java/org/researchstack/skin/ui/BaseActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/BaseActivity.java similarity index 58% rename from skin/src/main/java/org/researchstack/skin/ui/BaseActivity.java rename to backbone/src/main/java/org/researchstack/backbone/ui/BaseActivity.java index 95b662c3a..36104de58 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/BaseActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/BaseActivity.java @@ -1,43 +1,65 @@ -package org.researchstack.skin.ui; - +package org.researchstack.backbone.ui; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Color; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; -import android.support.v4.content.LocalBroadcastManager; +import android.net.Uri; +import com.google.android.material.snackbar.Snackbar; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import org.researchstack.backbone.StorageAccess; +import org.researchstack.backbone.onboarding.OnboardingTaskType; import org.researchstack.backbone.result.TaskResult; -import org.researchstack.backbone.task.Task; -import org.researchstack.backbone.ui.PinCodeActivity; -import org.researchstack.backbone.ui.ViewTaskActivity; import org.researchstack.backbone.utils.LogExt; import org.researchstack.backbone.utils.ObservableUtils; -import org.researchstack.skin.AppPrefs; -import org.researchstack.skin.DataProvider; -import org.researchstack.skin.R; -import org.researchstack.skin.TaskProvider; -import org.researchstack.skin.task.OnboardingTask; -import org.researchstack.skin.task.SignInTask; -import org.researchstack.skin.ui.layout.SignUpEligibleStepLayout; +import org.researchstack.backbone.AppPrefs; +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ResearchStack; +import org.researchstack.backbone.task.OnboardingTask; +import org.researchstack.backbone.task.SignInTask; +import org.researchstack.backbone.ui.layout.SignUpEligibleStepLayout; import rx.Observable; import rx.functions.Action1; -public class BaseActivity extends PinCodeActivity { +public class BaseActivity extends PinCodeActivity +{ + + @Override + protected void onResume() + { + super.onResume(); + + IntentFilter errorFilter = new IntentFilter(); + errorFilter.addAction(DataProvider.ERROR_CONSENT_REQUIRED); + errorFilter.addAction(DataProvider.ERROR_NOT_AUTHENTICATED); + errorFilter.addAction(DataProvider.ERROR_APP_UPGRADE_REQUIRED); - BroadcastReceiver errorBroadcastReceiver = new BroadcastReceiver() { + LocalBroadcastManager.getInstance(this) + .registerReceiver(errorBroadcastReceiver, errorFilter); + } + + @Override + protected void onPause() + { + super.onPause(); + LocalBroadcastManager.getInstance(this).unregisterReceiver(errorBroadcastReceiver); + } + + BroadcastReceiver errorBroadcastReceiver = new BroadcastReceiver() + { @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(Context context, Intent intent) + { LogExt.i(getClass(), "errorBroadcastReceiver()"); - if (AppPrefs.getInstance(context).skippedOnboarding()) { + if(AppPrefs.getInstance().skippedOnboarding()) + { // We don't want to bother a user that has skipped sign-up with the signed out // or consent messages. Short-circuiting until we have an approved message to show // a user that has skipped. @@ -48,43 +70,45 @@ public void onReceive(Context context, Intent intent) { int length = Snackbar.LENGTH_INDEFINITE; Action1 action = null; - switch (intent.getAction()) { + switch(intent.getAction()) + { case DataProvider.ERROR_CONSENT_REQUIRED: - messageText = getString(R.string.rss_network_error_consent); - actionText = getString(R.string.rss_network_error_consent_action); - action = v -> { - Task task = TaskProvider.getInstance().get(TaskProvider.TASK_ID_CONSENT); - Intent consentTask = ViewTaskActivity.newIntent(BaseActivity.this, task); - startActivityForResult(consentTask, SignUpEligibleStepLayout.CONSENT_REQUEST); - }; + messageText = getString(R.string.rsb_network_error_consent); + actionText = getString(R.string.rsb_network_error_consent_action); + action = v -> ResearchStack.getInstance().getOnboardingManager() + .launchOnboarding(OnboardingTaskType.RECONSENT, context); break; case DataProvider.ERROR_NOT_AUTHENTICATED: - messageText = getString(R.string.rss_network_error_sign_in); - actionText = getString(R.string.rss_network_error_sign_in_action); - action = v -> { - boolean hasPinCode = StorageAccess.getInstance() - .hasPinCode(BaseActivity.this); - SignInTask task = (SignInTask) TaskProvider.getInstance() - .get(TaskProvider.TASK_ID_SIGN_IN); - task.setHasPasscode(hasPinCode); - startActivityForResult(SignUpTaskActivity.newIntent(BaseActivity.this, - task), OnboardingActivity.REQUEST_CODE_SIGN_IN); - }; + messageText = getString(R.string.rsb_network_error_sign_in); + actionText = getString(R.string.rsb_network_error_sign_in_action); + action = v -> ResearchStack.getInstance().getOnboardingManager() + .launchOnboarding(OnboardingTaskType.LOGIN, context); + break; + + case DataProvider.ERROR_APP_UPGRADE_REQUIRED: + messageText = getString(R.string.rsb_network_error_upgrade_app); + actionText = getString(R.string.rsb_network_error_upgrade_app_action); + + Intent playStoreIntent = new Intent(Intent.ACTION_VIEW); + playStoreIntent.setData(Uri.parse("market://details?id=" + context.getPackageName())); + action = v -> startActivity(playStoreIntent); break; } // Throw up a Snackbar View root = findViewById(android.R.id.content); Snackbar snackbar = Snackbar.make(root, messageText, length); - if (action != null) { - snackbar.setAction(actionText, action::call); + if(action != null) + { + snackbar.setAction(actionText, action:: call); } snackbar.getView().setOnClickListener(v -> snackbar.dismiss()); snackbar.setActionTextColor(ContextCompat.getColor(BaseActivity.this, - R.color.rss_snackbar_action_color)); + R.color.rsb_snackbar_action_color)); TextView messageView = getSnackBarMessageView(snackbar); - if (messageView != null) { + if (messageView != null) + { messageView.setTextColor(Color.WHITE); } @@ -92,41 +116,25 @@ public void onReceive(Context context, Intent intent) { } }; - @Override - protected void onResume() { - super.onResume(); - - IntentFilter errorFilter = new IntentFilter(); - errorFilter.addAction(DataProvider.ERROR_CONSENT_REQUIRED); - errorFilter.addAction(DataProvider.ERROR_NOT_AUTHENTICATED); - - LocalBroadcastManager.getInstance(this) - .registerReceiver(errorBroadcastReceiver, errorFilter); - } - - @Override - protected void onPause() { - super.onPause(); - LocalBroadcastManager.getInstance(this).unregisterReceiver(errorBroadcastReceiver); - } - /** * Method is not safe and assumes tv-id or tv-index wont change. - * * @return Snackbar message TextView */ - private TextView getSnackBarMessageView(Snackbar snackbar) { + private TextView getSnackBarMessageView(Snackbar snackbar) + { // Try id for app level snackbar id - int id = org.researchstack.skin.R.id.snackbar_text; + int id = org.researchstack.backbone.R.id.snackbar_text; TextView tv = (TextView) snackbar.getView().findViewById(id); - if (tv != null) { + if (tv != null) + { return tv; } // Try id for lib level snackbar id - id = android.support.design.R.id.snackbar_text; + id = com.google.android.material.R.id.snackbar_text; tv = (TextView) snackbar.getView().findViewById(id); - if (tv != null) { + if (tv != null) + { return tv; } @@ -134,7 +142,8 @@ private TextView getSnackBarMessageView(Snackbar snackbar) { // action is a Button who's super-type is also TextView. ViewGroup snackBarContainer = (ViewGroup) snackbar.getView(); View childZero = snackBarContainer.getChildAt(0); - if (childZero.getClass() == TextView.class) { + if (childZero.getClass() == TextView.class) + { return (TextView) childZero; } @@ -142,21 +151,26 @@ private TextView getSnackBarMessageView(Snackbar snackbar) { } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == OnboardingActivity.REQUEST_CODE_SIGN_IN && resultCode == RESULT_OK) { + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + if(requestCode == OverviewActivity.REQUEST_CODE_SIGN_IN && resultCode == RESULT_OK) + { TaskResult result = (TaskResult) data.getSerializableExtra(ViewTaskActivity.EXTRA_TASK_RESULT); String email = (String) result.getStepResult(OnboardingTask.SignInStepIdentifier) .getResultForIdentifier(SignInTask.ID_EMAIL); String password = (String) result.getStepResult(OnboardingTask.SignInStepIdentifier) .getResultForIdentifier(SignInTask.ID_PASSWORD); - if (email != null && password != null) { + if(email != null && password != null) + { Intent intent = new Intent(this, EmailVerificationActivity.class); intent.putExtra(EmailVerificationActivity.EXTRA_EMAIL, email); intent.putExtra(EmailVerificationActivity.EXTRA_PASSWORD, password); startActivity(intent); } - } else if (requestCode == SignUpEligibleStepLayout.CONSENT_REQUEST && resultCode == RESULT_OK) { + } + else if(requestCode == SignUpEligibleStepLayout.CONSENT_REQUEST && resultCode == RESULT_OK) + { TaskResult consentResult = (TaskResult) data.getSerializableExtra(ViewTaskActivity.EXTRA_TASK_RESULT); Observable.fromCallable(() -> { @@ -164,8 +178,10 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { DataProvider.getInstance().uploadConsent(this, consentResult); return null; }).compose(ObservableUtils.applyDefault()).subscribe(); - } else { + } + else + { super.onActivityResult(requestCode, resultCode, data); } } -} +} \ No newline at end of file diff --git a/skin/src/main/java/org/researchstack/skin/ui/ConsentTaskActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/ConsentTaskActivity.java similarity index 86% rename from skin/src/main/java/org/researchstack/skin/ui/ConsentTaskActivity.java rename to backbone/src/main/java/org/researchstack/backbone/ui/ConsentTaskActivity.java index 5d7b5970d..d27e950f4 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/ConsentTaskActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/ConsentTaskActivity.java @@ -1,12 +1,12 @@ -package org.researchstack.skin.ui; +package org.researchstack.backbone.ui; import android.content.Context; import android.content.Intent; import org.researchstack.backbone.StorageAccess; import org.researchstack.backbone.task.Task; -import org.researchstack.backbone.ui.ViewTaskActivity; +@Deprecated // No longer needed since ConsentTask is deprecated public class ConsentTaskActivity extends ViewTaskActivity { public static Intent newIntent(Context context, Task task) { Intent intent = new Intent(context, ConsentTaskActivity.class); diff --git a/skin/src/main/java/org/researchstack/skin/ui/EmailVerificationActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/EmailVerificationActivity.java similarity index 89% rename from skin/src/main/java/org/researchstack/skin/ui/EmailVerificationActivity.java rename to backbone/src/main/java/org/researchstack/backbone/ui/EmailVerificationActivity.java index de49c50a7..4303037e3 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/EmailVerificationActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/EmailVerificationActivity.java @@ -1,10 +1,10 @@ -package org.researchstack.skin.ui; +package org.researchstack.backbone.ui; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; -import android.support.v7.widget.AppCompatTextView; -import android.support.v7.widget.Toolbar; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.appcompat.widget.Toolbar; import android.text.Html; import android.view.MenuItem; import android.view.View; @@ -16,16 +16,14 @@ import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.step.Step; import org.researchstack.backbone.task.OrderedTask; -import org.researchstack.backbone.ui.PinCodeActivity; -import org.researchstack.backbone.ui.ViewTaskActivity; import org.researchstack.backbone.ui.views.SubmitBar; import org.researchstack.backbone.utils.ObservableUtils; import org.researchstack.backbone.utils.ThemeUtils; -import org.researchstack.skin.DataProvider; -import org.researchstack.skin.R; -import org.researchstack.skin.task.OnboardingTask; -import org.researchstack.skin.task.SignUpTask; -import org.researchstack.skin.ui.layout.SignUpStepLayout; +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.R; +import org.researchstack.backbone.task.OnboardingTask; +import org.researchstack.backbone.task.SignUpTask; +import org.researchstack.backbone.ui.layout.SignUpStepLayout; public class EmailVerificationActivity extends PinCodeActivity { @@ -41,7 +39,7 @@ public class EmailVerificationActivity extends PinCodeActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.rss_activity_email_verification); + setContentView(R.layout.rsb_activity_email_verification); progress = findViewById(R.id.progress); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); @@ -81,7 +79,7 @@ private void updateEmailText() { String accentColorString = "#" + Integer.toHexString(Color.red(accentColor)) + Integer.toHexString(Color.green(accentColor)) + Integer.toHexString(Color.blue(accentColor)); - String formattedSummary = getString(R.string.rss_confirm_summary, + String formattedSummary = getString(R.string.rsb_confirm_summary, "" + email + ""); ((AppCompatTextView) findViewById(R.id.email_verification_body)).setText(Html.fromHtml( formattedSummary)); @@ -107,9 +105,9 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { private void changeEmail() { Step signUpStep = new Step(OnboardingTask.SignUpStepIdentifier); - signUpStep.setStepTitle(R.string.rss_sign_up); + signUpStep.setStepTitle(R.string.rsb_sign_up); signUpStep.setStepLayoutClass(SignUpStepLayout.class); - signUpStep.setTitle(getString(R.string.rss_change_email)); + signUpStep.setTitle(getString(R.string.rsb_change_email)); Intent intent = new Intent(this, ViewTaskActivity.class); intent.putExtra(ViewTaskActivity.EXTRA_TASK, new OrderedTask(CHANGE_EMAIL_ID, signUpStep)); @@ -165,14 +163,14 @@ private void attemptSignIn() { .alpha(0) .withEndAction(() -> progress.setVisibility(View.GONE)); Toast.makeText(EmailVerificationActivity.this, - R.string.rss_email_not_verified, Toast.LENGTH_LONG).show(); + R.string.rsb_email_not_verified, Toast.LENGTH_LONG).show(); } }, error -> { progress.animate() .alpha(0) .withEndAction(() -> progress.setVisibility(View.GONE)); Toast.makeText(EmailVerificationActivity.this, - R.string.rss_email_not_verified, Toast.LENGTH_LONG).show(); + R.string.rsb_email_not_verified, Toast.LENGTH_LONG).show(); }); } } diff --git a/skin/src/main/java/org/researchstack/skin/ui/LearnActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/LearnActivity.java similarity index 75% rename from skin/src/main/java/org/researchstack/skin/ui/LearnActivity.java rename to backbone/src/main/java/org/researchstack/backbone/ui/LearnActivity.java index 47e186804..1977a2c34 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/LearnActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/LearnActivity.java @@ -1,18 +1,17 @@ -package org.researchstack.skin.ui; +package org.researchstack.backbone.ui; import android.os.Bundle; -import android.support.v7.widget.Toolbar; +import androidx.appcompat.widget.Toolbar; import android.view.MenuItem; -import org.researchstack.backbone.ui.PinCodeActivity; -import org.researchstack.skin.R; -import org.researchstack.skin.ui.fragment.LearnFragment; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ui.fragment.LearnFragment; public class LearnActivity extends PinCodeActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.rss_activity_fragment); + setContentView(R.layout.rsb_activity_fragment); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); diff --git a/skin/src/main/java/org/researchstack/skin/ui/MainActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/MainActivity.java similarity index 57% rename from skin/src/main/java/org/researchstack/skin/ui/MainActivity.java rename to backbone/src/main/java/org/researchstack/backbone/ui/MainActivity.java index 057894633..2cdaf2404 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/MainActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/MainActivity.java @@ -1,30 +1,30 @@ -package org.researchstack.skin.ui; +package org.researchstack.backbone.ui; import android.content.Intent; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.support.design.widget.TabLayout; -import android.support.v4.view.ViewPager; -import android.support.v7.widget.Toolbar; +import com.google.android.material.tabs.TabLayout; +import androidx.viewpager.widget.ViewPager; +import androidx.appcompat.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; +import android.view.View; import org.researchstack.backbone.StorageAccess; import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.task.Task; -import org.researchstack.backbone.ui.ViewTaskActivity; import org.researchstack.backbone.ui.views.IconTabLayout; import org.researchstack.backbone.utils.LogExt; import org.researchstack.backbone.utils.ObservableUtils; import org.researchstack.backbone.utils.UiThreadContext; -import org.researchstack.skin.ActionItem; -import org.researchstack.skin.DataProvider; -import org.researchstack.skin.R; -import org.researchstack.skin.TaskProvider; -import org.researchstack.skin.UiManager; -import org.researchstack.skin.notification.TaskAlertReceiver; -import org.researchstack.skin.ui.adapter.MainPagerAdapter; +import org.researchstack.backbone.ActionItem; +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.R; +import org.researchstack.backbone.TaskProvider; +import org.researchstack.backbone.UiManager; +import org.researchstack.backbone.notification.TaskAlertReceiver; +import org.researchstack.backbone.ui.adapter.MainPagerAdapter; import java.util.List; @@ -34,7 +34,12 @@ public class MainActivity extends BaseActivity { private static final int REQUEST_CODE_INITIAL_TASK = 1010; - private MainPagerAdapter pagerAdapter; + protected MainPagerAdapter pagerAdapter; + + /** + * The view pager that holds the fragment tabs + */ + protected ViewPager viewPager; private boolean failedToFinishInitialTask; @@ -43,7 +48,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LogExt.d(getClass(), "onCreate"); - setContentView(R.layout.rss_activity_main); + setContentView(R.layout.rsb_activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); @@ -53,7 +58,7 @@ protected void onCreate(Bundle savedInstanceState) { List items = UiManager.getInstance().getMainTabBarItems(); pagerAdapter = new MainPagerAdapter(getSupportFragmentManager(), items); - ViewPager viewPager = (ViewPager) findViewById(R.id.pager); + viewPager = findViewById(R.id.pager); viewPager.setAdapter(pagerAdapter); viewPager.setPageMargin(1); viewPager.setPageMarginDrawable(new ColorDrawable(Color.LTGRAY)); @@ -67,14 +72,18 @@ public void onTabSelected(TabLayout.Tab tab) { } }); - for (ActionItem item : items) { - tabLayout.addIconTab( - item.getTitle(), - item.getIcon(), - items.indexOf(item) == 0, - // need real logic for this (show badge) - items.indexOf(item) == 0 - ); + if(items != null && items.size() > 1) { + for (ActionItem item : items) { + tabLayout.addIconTab( + item.getTitle(), + item.getIcon(), + items.indexOf(item) == 0, + // need real logic for this (show badge) + items.indexOf(item) == 0 + ); + } + } else { // If there is only one tab, hide the layout + tabLayout.setVisibility(View.GONE); } viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout)); @@ -175,6 +184,64 @@ public void onDataReady() { } }); } + + // TODO: integrate this into the Scheduled Activities + // TODO: for now, uncomment this to run/test the Tremor Task +// NavigableOrderedTask task = TremorTaskFactory.tremorTask( +// this, "tremorttaskid", "We collect sensor data to measure your hand tremor", 10, +// Arrays.asList(new TremorTaskFactory.TremorTaskExcludeOption[] {}), +// TremorTaskFactory.HandTaskOptions.BOTH, +// Arrays.asList(new TremorTaskFactory.TaskExcludeOption[] {})); +// +// Intent intent = ActiveTaskActivity.newIntent(this, task); +// startActivity(intent); + + // TODO: integrate this into the Scheduled Activities + // TODO: for now, uncomment this to run/test the Short Walk Task +// OrderedTask task = WalkingTaskFactory.shortWalkTask( +// this, "walkingtaskid", "intendedUseDescription", +// 30, 10, Arrays.asList(new TaskExcludeOption[] {})); +// +// Intent intent = ActiveTaskActivity.newIntent(this, task); +// startActivity(intent); + + // TODO: integrate this into the Scheduled Activities + // TODO: for now, uncomment this to run/test the Walk back and forth test +// OrderedTask task = WalkingTaskFactory.walkBackAndForthTask( +// this, "walkingtaskid", "intendedUseDescription", +// 30, 10, Arrays.asList(new TaskExcludeOption[] {})); +// +// Intent intent = ActiveTaskActivity.newIntent(this, task); +// startActivity(intent); + + // TODO: integrate this into the Scheduled Activities + // TODO: for now, uncomment this to run/test the timed walk task +// OrderedTask task = WalkingTaskFactory.timedWalkTask( +// this, "walkingtaskid", "intendedUseDescription", +// 50.0, 30, 10, true, Arrays.asList(new TaskExcludeOption[] {})); +// +// Intent intent = ActiveTaskActivity.newIntent(this, task); +// startActivity(intent); + + // TODO: integrate this into the Scheduled Activities + // TODO: for now, uncomment this to run/test the tapping task +// OrderedTask task = TappingTaskFactory.twoFingerTappingIntervalTask( +// this, "tappingtaskid", "intendedUseDescription", +// 30, HandTaskOptions.BOTH, Arrays.asList(new TaskExcludeOption[] {})); +// +// Intent intent = ActiveTaskActivity.newIntent(this, task); +// startActivity(intent); + + // TODO: integrate this into the Scheduled Activities + // TODO: for now, uncomment this to run/test the Audio task +// NavigableOrderedTask task = AudioTaskFactory.audioTask( +// this, "audiotaskid", "intendedUseDescription", +// "speech description", "short speech description", 5, +// AudioRecorderSettings.defaultSettings(), true, +// Arrays.asList(new TaskExcludeOption[] {})); +// +// Intent intent = ActiveTaskActivity.newIntent(this, task); +// startActivity(intent); } @Override diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/OnboardingTaskActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/OnboardingTaskActivity.java new file mode 100644 index 000000000..02f51f9c6 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/OnboardingTaskActivity.java @@ -0,0 +1,186 @@ +package org.researchstack.backbone.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; + +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.PermissionRequestManager; +import org.researchstack.backbone.StorageAccess; +import org.researchstack.backbone.model.ProfileInfoOption; +import org.researchstack.backbone.onboarding.OnboardingSection; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.EmailVerificationStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.task.Task; +import org.researchstack.backbone.ui.callbacks.ActivityCallback; +import org.researchstack.backbone.ui.step.layout.EmailVerificationStepLayout; +import org.researchstack.backbone.ui.step.layout.StepLayout; +import org.researchstack.backbone.ui.step.layout.StepPermissionRequest; +import org.researchstack.backbone.R; +import org.researchstack.backbone.utils.StepResultHelper; + +/** + * Created by TheMDP on 1/14/17. + * + * OnboardingTaskActivity serves as the root task activity during the onboarding process + * It is not much different than its base class ViewTaskActivity, except + * that it does not allow the pin code view to be shown + */ + +public class OnboardingTaskActivity extends ViewTaskActivity implements ActivityCallback { + + /** + * Used to maintain the step title from the previous step to show on the next step + * in the case that the next step does not have a valid step title + */ + protected String previousStepTitle; + + /** + * @param context used to create intent + * @param task any task will be displayed correctly + * @return launchable intent for OnboardingTaskActivity + */ + public static Intent newIntent(Context context, Task task) { + Intent intent = new Intent(context, OnboardingTaskActivity.class); + intent.putExtra(EXTRA_TASK, task); + return intent; + } + + @Override + public void onDataAuth() + { + // Onboarding tasks skip pin code data auth and go right to data ready + onDataReady(); + } + + @Override + protected StepLayout getLayoutForStep(Step step) + { + StepLayout superStepLayout = super.getLayoutForStep(step); + + // Onboarding Tasks will keep the previous step title if none is available + String title = task.getTitleForStep(this, step); + if (title == null) { + setActionBarTitle(previousStepTitle); + } else { + previousStepTitle = title; + } + + setupCustomStepLayouts(step, superStepLayout); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(shouldShowBackButton(step)); + } + + return superStepLayout; + } + + @Override + @SuppressWarnings("unchecked") // needed for unchecked StepResult generic type casting + public void onSaveStep(int action, Step step, StepResult result) { + if (step instanceof EmailVerificationStep) { + StepResult passwordResult = StepResultHelper + .findStepResult(result, ProfileInfoOption.PASSWORD.getIdentifier()); + + // If there is a new password from the EmailVerificationStep, + // that means that the user has changed their email and password, + // and we need to replace the password result from the previous RegistrationStep's step result + if (passwordResult != null) { + StepResult originalPasswordResult = StepResultHelper + .findStepResult(taskResult, ProfileInfoOption.PASSWORD.getIdentifier()); + if (originalPasswordResult != null) { + originalPasswordResult.setResult(passwordResult.getResult()); + } + } + } + + super.onSaveStep(action, step, result); + } + + /** + * Injects TaskResult information into StepLayouts that need more information + * @param step the step that is about to be displayed + * @param stepLayout step layout that has just been instantiated + */ + public void setupCustomStepLayouts(Step step, StepLayout stepLayout) { + // Check here for StepLayouts that need results fed into them + if (stepLayout instanceof EmailVerificationStepLayout) { + EmailVerificationStepLayout emailStepLayout = (EmailVerificationStepLayout)stepLayout; + // Try and find the password step result, but exclude the EmailVerificationStep + // as a source for the password, since it will be handled internally by that class + if (taskResult != null) { + StepResult emailStepResult = taskResult.getResults().get(step.getIdentifier()); + if (emailStepResult != null) { + taskResult.getResults().remove(step.getIdentifier()); + } + + StepResult passwordResult = StepResultHelper + .findStepResult(taskResult, ProfileInfoOption.PASSWORD.getIdentifier()); + if (passwordResult != null) { + emailStepLayout.setPassword((String) passwordResult.getResult()); + } + + // Re-add email step task result + if (emailStepResult != null) { + taskResult.getResults().put(step.getIdentifier(), emailStepResult); + } + } + } + } + + /** + * Clear out all the data that has been saved by this Activity + * And push user back to the Overview screen, or whatever screen was below this Activity + */ + @Override + protected void discardResultsAndFinish() { + DataProvider.getInstance().signOut(this); + StorageAccess.getInstance().removePinCode(this); + super.discardResultsAndFinish(); + } + + @TargetApi(Build.VERSION_CODES.M) + @Override + public void onRequestPermission(String id) { + if (PermissionRequestManager.getInstance().isNonSystemPermission(id)) { + PermissionRequestManager.getInstance().onRequestNonSystemPermission(this, id); + } else { + requestPermissions(new String[] {id}, PermissionRequestManager.PERMISSION_REQUEST_CODE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if(requestCode == PermissionRequestManager.PERMISSION_REQUEST_CODE) { + updateStepLayoutForPermission(); + } + } + + protected void updateStepLayoutForPermission() { + StepLayout stepLayout = (StepLayout) findViewById(R.id.rsb_current_step); + if(stepLayout instanceof StepPermissionRequest) { + ((StepPermissionRequest) stepLayout).onUpdateForPermissionResult(); + } + } + + @Override + @Deprecated + public void startConsentTask() { + // deprecated + } + + public boolean shouldShowBackButton(Step step) { + switch (step.getIdentifier()) { + case OnboardingSection.EMAIL_VERIFICATION_IDENTIFIER: + return false; + case OnboardingSection.REGISTRATION_IDENTIFIER: + return false; + } + return true; + } +} diff --git a/skin/src/main/java/org/researchstack/skin/ui/OnboardingActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/OverviewActivity.java similarity index 74% rename from skin/src/main/java/org/researchstack/skin/ui/OnboardingActivity.java rename to backbone/src/main/java/org/researchstack/backbone/ui/OverviewActivity.java index 9a3a1cda1..8cfa4df45 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/OnboardingActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/OverviewActivity.java @@ -1,13 +1,14 @@ -package org.researchstack.skin.ui; +package org.researchstack.backbone.ui; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.content.Intent; import android.os.Bundle; -import android.support.design.widget.TabLayout; -import android.support.v4.content.ContextCompat; -import android.support.v4.view.ViewPager; -import android.support.v7.widget.AppCompatButton; +import com.google.android.material.tabs.TabLayout; +import androidx.core.content.ContextCompat; +import androidx.viewpager.widget.ViewPager; +import androidx.appcompat.widget.AppCompatButton; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; @@ -15,27 +16,32 @@ import android.widget.LinearLayout; import android.widget.TextView; +import org.researchstack.backbone.DataProvider; import org.researchstack.backbone.StorageAccess; +import org.researchstack.backbone.onboarding.OnboardingTaskType; import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.task.OrderedTask; -import org.researchstack.backbone.ui.PinCodeActivity; -import org.researchstack.backbone.ui.ViewTaskActivity; import org.researchstack.backbone.utils.ResUtils; import org.researchstack.backbone.utils.TextUtils; -import org.researchstack.skin.AppPrefs; -import org.researchstack.skin.R; -import org.researchstack.skin.ResourceManager; -import org.researchstack.skin.TaskProvider; -import org.researchstack.skin.UiManager; -import org.researchstack.skin.model.StudyOverviewModel; -import org.researchstack.skin.step.PassCodeCreationStep; -import org.researchstack.skin.task.OnboardingTask; -import org.researchstack.skin.task.SignInTask; -import org.researchstack.skin.task.SignUpTask; -import org.researchstack.skin.ui.adapter.OnboardingPagerAdapter; - - -public class OnboardingActivity extends PinCodeActivity implements View.OnClickListener { +import org.researchstack.backbone.AppPrefs; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ResearchStack; +import org.researchstack.backbone.ResourceManager; +import org.researchstack.backbone.TaskProvider; +import org.researchstack.backbone.UiManager; +import org.researchstack.backbone.model.StudyOverviewModel; +import org.researchstack.backbone.onboarding.OnboardingManager; +import org.researchstack.backbone.step.PassCodeCreationStep; +import org.researchstack.backbone.task.OnboardingTask; +import org.researchstack.backbone.task.SignInTask; +import org.researchstack.backbone.task.SignUpTask; +import org.researchstack.backbone.ui.adapter.OnboardingPagerAdapter; + +/** + * OverviewActivity is the landing page for a user who is not signed up or signed in + * it gives an overview of the study, as well as buttons to sign in or sign up + */ +public class OverviewActivity extends PinCodeActivity implements View.OnClickListener { public static final int REQUEST_CODE_SIGN_UP = 21473; public static final int REQUEST_CODE_SIGN_IN = 31473; public static final int REQUEST_CODE_PASSCODE = 41473; @@ -49,7 +55,7 @@ public class OnboardingActivity extends PinCodeActivity implements View.OnClickL @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - super.setContentView(R.layout.rss_activity_onboarding); + super.setContentView(R.layout.rsb_activity_onboarding); ImageView logoView = (ImageView) findViewById(R.id.layout_studyoverview_landing_logo); TextView titleView = (TextView) findViewById(R.id.layout_studyoverview_landing_title); @@ -72,14 +78,14 @@ protected void onCreate(Bundle savedInstanceState) { // add Read Consent option to list and tabbed dialog if ("yes".equals(welcomeQuestion.getShowConsent())) { StudyOverviewModel.Question consent = new StudyOverviewModel.Question(); - consent.setTitle(getString(R.string.rss_read_consent_doc)); + consent.setTitle(getString(R.string.rsb_read_consent_doc)); consent.setDetails(ResourceManager.getInstance().getConsentHtml().getName()); model.getQuestions().add(0, consent); } for (int i = 0; i < model.getQuestions().size(); i++) { AppCompatButton button = (AppCompatButton) LayoutInflater.from(this) - .inflate(R.layout.rss_button_study_overview, linearLayout, false); + .inflate(R.layout.rsb_button_study_overview, linearLayout, false); button.setText(model.getQuestions().get(i).getTitle()); // set the index for opening the viewpager to the correct page on click button.setTag(i); @@ -88,10 +94,13 @@ protected void onCreate(Bundle savedInstanceState) { } signUp = (Button) findViewById(R.id.intro_sign_up); + signUp.setOnClickListener(this::onSignUpClicked); // lamba to call internal method, IDE threw warning until I switched to it signIn = (TextView) findViewById(R.id.intro_sign_in); + signIn.setOnClickListener(this::onSignInClicked); skip = (Button) findViewById(R.id.intro_skip); skip.setVisibility(UiManager.getInstance().isConsentSkippable() ? View.VISIBLE : View.GONE); + skip.setOnClickListener(this::onSkipClicked); int resId = ResUtils.getDrawableResourceId(this, model.getLogoName()); logoView.setImageResource(resId); @@ -124,7 +133,18 @@ public void onDataAuth() { } } - private StudyOverviewModel parseStudyOverviewModel() { + @Override + public void onResume() { + super.onResume(); + // We shouldn't be able to get here if the user is signed in and consented, + // Go to MainActivity instead + if (DataProvider.getInstance().isSignedIn(this) && DataProvider.getInstance().isConsented()) { + startActivity(new Intent(this, MainActivity.class)); + } + } + + private StudyOverviewModel parseStudyOverviewModel() + { return ResourceManager.getInstance().getStudyOverview().create(this); } @@ -193,11 +213,17 @@ public void onBackPressed() { public void onSignUpClicked(View view) { hidePager(); - boolean hasPin = StorageAccess.getInstance().hasPinCode(this); - - SignUpTask task = (SignUpTask) TaskProvider.getInstance().get(TaskProvider.TASK_ID_SIGN_UP); - task.setHasPasscode(hasPin); - startActivityForResult(SignUpTaskActivity.newIntent(this, task), REQUEST_CODE_SIGN_UP); + OnboardingManager onboardingManager = ResearchStack.getInstance().getOnboardingManager(); + if (onboardingManager != null) { + // TODO: make sure User is completely signed out + onboardingManager.launchOnboarding(OnboardingTaskType.REGISTRATION, this); + } else { + Log.e(getClass().getSimpleName(), "Old flow is deprecated, please implement OnboardingManager"); + boolean hasPin = StorageAccess.getInstance().hasPinCode(this); + SignUpTask task = (SignUpTask) TaskProvider.getInstance().get(TaskProvider.TASK_ID_SIGN_UP); + task.setHasPasscode(hasPin); + startActivityForResult(SignUpTaskActivity.newIntent(this, task), REQUEST_CODE_SIGN_UP); + } } public void onSkipClicked(View view) { @@ -205,7 +231,7 @@ public void onSkipClicked(View view) { boolean hasPasscode = StorageAccess.getInstance().hasPinCode(this); if (!hasPasscode) { PassCodeCreationStep step = new PassCodeCreationStep(OnboardingTask.SignUpPassCodeCreationStepIdentifier, - R.string.rss_passcode); + R.string.rsb_passcode); OrderedTask task = new OrderedTask("PasscodeTask", step); startActivityForResult(ConsentTaskActivity.newIntent(this, task), REQUEST_CODE_PASSCODE); @@ -216,11 +242,18 @@ public void onSkipClicked(View view) { public void onSignInClicked(View view) { hidePager(); - boolean hasPasscode = StorageAccess.getInstance().hasPinCode(this); - SignInTask task = (SignInTask) TaskProvider.getInstance().get(TaskProvider.TASK_ID_SIGN_IN); - task.setHasPasscode(hasPasscode); - startActivityForResult(SignUpTaskActivity.newIntent(this, task), REQUEST_CODE_SIGN_IN); + OnboardingManager onboardingManager = ResearchStack.getInstance().getOnboardingManager(); + if (onboardingManager != null) { + // TODO: make sure User is completely signed out + onboardingManager.launchOnboarding(OnboardingTaskType.LOGIN, this); + } else { + Log.e(getClass().getSimpleName(), "Old flow is deprecated, please implement OnboardingManager"); + boolean hasPasscode = StorageAccess.getInstance().hasPinCode(this); + SignInTask task = (SignInTask) TaskProvider.getInstance().get(TaskProvider.TASK_ID_SIGN_IN); + task.setHasPasscode(hasPasscode); + startActivityForResult(SignUpTaskActivity.newIntent(this, task), REQUEST_CODE_SIGN_IN); + } } @Override diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/PinCodeActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/PinCodeActivity.java index 08408b78d..a97b00927 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/PinCodeActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/PinCodeActivity.java @@ -1,13 +1,17 @@ package org.researchstack.backbone.ui; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.graphics.Color; import android.os.Bundle; import android.os.Handler; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.app.AppCompatActivity; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.core.view.ViewCompat; +import androidx.appcompat.app.AppCompatActivity; import android.view.ContextThemeWrapper; +import android.view.View; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; @@ -15,10 +19,17 @@ import com.jakewharton.rxbinding.widget.RxTextView; +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.DataResponse; import org.researchstack.backbone.R; import org.researchstack.backbone.StorageAccess; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.PasscodeStep; +import org.researchstack.backbone.step.Step; import org.researchstack.backbone.storage.file.PinCodeConfig; import org.researchstack.backbone.storage.file.StorageAccessListener; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; +import org.researchstack.backbone.ui.step.layout.FingerprintStepLayout; import org.researchstack.backbone.ui.views.PinCodeLayout; import org.researchstack.backbone.utils.LogExt; import org.researchstack.backbone.utils.ObservableUtils; @@ -27,10 +38,12 @@ import java.util.List; import rx.Observable; +import rx.android.schedulers.AndroidSchedulers; import rx.functions.Action1; public class PinCodeActivity extends AppCompatActivity implements StorageAccessListener { private PinCodeLayout pinCodeLayout; + private FingerprintStepLayout fingerprintLayout; private Action1 toggleKeyboardAction; @Override @@ -43,6 +56,17 @@ protected void onPause() { super.onPause(); LogExt.i(getClass(), "logAccessTime()"); StorageAccess.getInstance().logAccessTime(); + + storageAccessUnregister(); + if(pinCodeLayout != null && ViewCompat.isAttachedToWindow(pinCodeLayout)) { + getWindowManager().removeView(pinCodeLayout); + } + if(fingerprintLayout != null) { + fingerprintLayout.stopListening(); + if (ViewCompat.isAttachedToWindow(fingerprintLayout)) { + getWindowManager().removeView(fingerprintLayout); + } + } } @Override @@ -52,15 +76,6 @@ protected void onResume() { requestStorageAccess(); } - @Override - protected void onDestroy() { - super.onDestroy(); - storageAccessUnregister(); - if (pinCodeLayout != null) { - getWindowManager().removeView(pinCodeLayout); - } - } - protected void requestStorageAccess() { LogExt.i(getClass(), "requestStorageAccess()"); StorageAccess storageAccess = StorageAccess.getInstance(); @@ -133,6 +148,45 @@ public void onDataAuth() { LogExt.e(getClass(), "onDataAuth()"); storageAccessUnregister(); + if (StorageAccess.getInstance().usesFingerprint(this)) { + initFingerprintLayout(); + } else { + initPincodeLayout(); + } + } + + private void initFingerprintLayout() { + int theme = ThemeUtils.getPassCodeTheme(this); + fingerprintLayout = new FingerprintStepLayout(new ContextThemeWrapper(this, theme)); + fingerprintLayout.setBackgroundColor(Color.WHITE); + + PasscodeStep step = new PasscodeStep("FingerprintStep", null, null); + step.setUseFingerprint(true); + fingerprintLayout.initialize(step, null); + fingerprintLayout.setCallbacks(new StepCallbacks() { + @Override + public void onSaveStep(int action, Step step, StepResult result) { + // is the way the FingerprintStepLayout signals that we should end the activity + if (action == ACTION_END) { + finish(); + } else { + // Move to the next state, which signals a successful data auth + transitionToNextState(); + } + } + + @Override + public void onCancelStep() { + // the cancel step signals to the pin code activity the FingerprintStepLayout needs setup again + signOut(); + } + }); + + WindowManager.LayoutParams params = new WindowManager.LayoutParams(); + getWindowManager().addView(fingerprintLayout, params); + } + + private void initPincodeLayout() { // Show pincode layout PinCodeConfig config = StorageAccess.getInstance().getPinCodeConfig(); @@ -140,6 +194,9 @@ public void onDataAuth() { pinCodeLayout = new PinCodeLayout(new ContextThemeWrapper(this, theme)); pinCodeLayout.setBackgroundColor(Color.WHITE); + pinCodeLayout.getForgotPasscodeButton().setVisibility(View.VISIBLE); + pinCodeLayout.getForgotPasscodeButton().setOnClickListener(this::forgotPasscodeClicked); + int errorColor = getResources().getColor(R.color.rsb_error); TextView summary = (TextView) pinCodeLayout.findViewById(R.id.text); @@ -178,10 +235,7 @@ public void onDataAuth() { if (!success) { toggleKeyboardAction.call(true); } else { - getWindowManager().removeView(pinCodeLayout); - pinCodeLayout = null; - // authenticate() no longer calls notifyReady(), call this after auth - requestStorageAccess(); + transitionToNextState(); } }); @@ -191,4 +245,68 @@ public void onDataAuth() { // Show keyboard, needs to be delayed, not sure why pinCodeLayout.postDelayed(() -> toggleKeyboardAction.call(true), 300); } + + /** + * Since all data in the app is protected by a passcode, we must remove all the data + * that currently exists, so that we can set the user up with a new passcode + * + * This alert dialog should provide sufficient warning to the user before all their local data is removed + * + * @param v button that was tapped + */ + public void forgotPasscodeClicked(View v) { + new AlertDialog.Builder(this).setTitle(R.string.rsb_reset_passcode) + .setMessage(R.string.rsb_reset_passcode_message) + .setCancelable(false) + .setPositiveButton(R.string.rsb_log_out, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + signOut(); + } + }) + .setNegativeButton(R.string.rsb_cancel, null) + .show(); + } + + private void signOut() { + // Signs the user out of the app, so they have to start from scratch + // Only gives a callback to response on success, the rest is handled by StepLayoutHelper + DataProvider.getInstance().signOut(this).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1() { + @Override + public void call(DataResponse response) { + if (!PinCodeActivity.this.isFinishing()) { + transitionToNextState(); + } + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + if (!PinCodeActivity.this.isFinishing()) { + new AlertDialog.Builder(PinCodeActivity.this) + .setMessage(throwable.getLocalizedMessage()) + .setPositiveButton(getString(R.string.rsb_ok), null) + .create().show(); + } + } + }); + } + + /** + * By removing the pincode layout and re-requesting storage access, we force the + * activity to re-evaluate its pincode state and move on to the next screen + */ + private void transitionToNextState() { + if (pinCodeLayout != null) { + getWindowManager().removeView(pinCodeLayout); + pinCodeLayout = null; + } + + if (fingerprintLayout != null && ViewCompat.isAttachedToWindow(fingerprintLayout)) { + getWindowManager().removeView(fingerprintLayout); + fingerprintLayout = null; + } + + // authenticate() no longer calls notifyReady(), call this after auth + requestStorageAccess(); + } } diff --git a/skin/src/main/java/org/researchstack/skin/ui/SettingsActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/SettingsActivity.java similarity index 78% rename from skin/src/main/java/org/researchstack/skin/ui/SettingsActivity.java rename to backbone/src/main/java/org/researchstack/backbone/ui/SettingsActivity.java index 5615f0d10..1d6000073 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/SettingsActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/SettingsActivity.java @@ -1,17 +1,17 @@ -package org.researchstack.skin.ui; +package org.researchstack.backbone.ui; import android.os.Bundle; -import android.support.v7.widget.Toolbar; +import androidx.appcompat.widget.Toolbar; import android.view.MenuItem; -import org.researchstack.skin.R; -import org.researchstack.skin.ui.fragment.SettingsFragment; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ui.fragment.SettingsFragment; public class SettingsActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.rss_activity_fragment); + setContentView(R.layout.rsb_activity_fragment); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/ShareActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/ShareActivity.java new file mode 100644 index 000000000..b406fc83b --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/ShareActivity.java @@ -0,0 +1,46 @@ +package org.researchstack.backbone.ui; + +import android.os.Bundle; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; +import android.view.MenuItem; + +import org.researchstack.backbone.UiManager; + + +public class ShareActivity extends BaseActivity +{ + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(org.researchstack.backbone.R.layout.rsb_activity_fragment); + + Toolbar toolbar = (Toolbar) findViewById(org.researchstack.backbone.R.id.toolbar); + setSupportActionBar(toolbar); + + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + + if(savedInstanceState == null) + { + getSupportFragmentManager().beginTransaction() + .add(org.researchstack.backbone.R.id.container, UiManager.getInstance().getShareFragment()) + .commit(); + } + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + if(item.getItemId() == android.R.id.home) + { + onBackPressed(); + return true; + } + + return super.onOptionsItemSelected(item); + } + +} \ No newline at end of file diff --git a/skin/src/main/java/org/researchstack/skin/ui/SignUpTaskActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/SignUpTaskActivity.java similarity index 88% rename from skin/src/main/java/org/researchstack/skin/ui/SignUpTaskActivity.java rename to backbone/src/main/java/org/researchstack/backbone/ui/SignUpTaskActivity.java index 65e21d03c..6bf57f363 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/SignUpTaskActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/SignUpTaskActivity.java @@ -1,4 +1,4 @@ -package org.researchstack.skin.ui; +package org.researchstack.backbone.ui; import android.annotation.TargetApi; import android.app.Activity; @@ -11,18 +11,19 @@ import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.step.Step; import org.researchstack.backbone.task.Task; -import org.researchstack.backbone.ui.ViewTaskActivity; import org.researchstack.backbone.ui.callbacks.ActivityCallback; import org.researchstack.backbone.ui.step.layout.StepLayout; import org.researchstack.backbone.ui.step.layout.StepPermissionRequest; import org.researchstack.backbone.utils.TextUtils; -import org.researchstack.skin.DataProvider; -import org.researchstack.skin.PermissionRequestManager; -import org.researchstack.skin.R; -import org.researchstack.skin.TaskProvider; -import org.researchstack.skin.task.OnboardingTask; -import org.researchstack.skin.ui.layout.SignUpEligibleStepLayout; - +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.PermissionRequestManager; +import org.researchstack.backbone.R; +import org.researchstack.backbone.TaskProvider; +import org.researchstack.backbone.task.OnboardingTask; +import org.researchstack.backbone.task.SignUpTask; +import org.researchstack.backbone.ui.layout.SignUpEligibleStepLayout; + +@Deprecated // use OnboardingManager.getInstance().launchOnboarding(OnboardingTaskType.REGISTRATION, this); public class SignUpTaskActivity extends ViewTaskActivity implements ActivityCallback { TaskResult consentResult; @@ -54,6 +55,8 @@ public void onSaveStep(int action, Step step, StepResult result) { String pin = (String) result.getResult(); if (!TextUtils.isEmpty(pin)) { StorageAccess.getInstance().createPinCode(this, pin); + SignUpTask signUpTask = (SignUpTask)getTask(); + signUpTask.setHasPasscode(true); } if (consentResult != null) { diff --git a/skin/src/main/java/org/researchstack/skin/ui/SplashActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/SplashActivity.java similarity index 77% rename from skin/src/main/java/org/researchstack/skin/ui/SplashActivity.java rename to backbone/src/main/java/org/researchstack/backbone/ui/SplashActivity.java index 9098f1bbe..de18cd2e2 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/SplashActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/SplashActivity.java @@ -1,14 +1,13 @@ -package org.researchstack.skin.ui; +package org.researchstack.backbone.ui; import android.content.Intent; import android.os.Bundle; import org.researchstack.backbone.StorageAccess; -import org.researchstack.backbone.ui.PinCodeActivity; import org.researchstack.backbone.utils.ObservableUtils; -import org.researchstack.skin.AppPrefs; -import org.researchstack.skin.DataProvider; -import org.researchstack.skin.notification.TaskAlertReceiver; +import org.researchstack.backbone.AppPrefs; +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.notification.TaskAlertReceiver; public class SplashActivity extends PinCodeActivity { @@ -28,8 +27,9 @@ public void onDataReady() { .compose(ObservableUtils.applyDefault()) .subscribe(response -> { - if (AppPrefs.getInstance(this).isOnboardingComplete() || - DataProvider.getInstance().isSignedIn(this)) { + if(AppPrefs.getInstance().isOnboardingComplete() || + (DataProvider.getInstance().isSignedIn(this) && DataProvider.getInstance().isConsented())) + { launchMainActivity(); } else { launchOnboardingActivity(); @@ -58,7 +58,7 @@ public void onDataFailed() { protected void launchOnboardingActivity() { // TODO: this shouldnt be hardcoded // TODO: consider an OnboardingManager class like iOS - startActivity(new Intent(this, OnboardingActivity.class)); + startActivity(new Intent(this, OverviewActivity.class)); } protected void launchMainActivity() { diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/ViewTaskActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/ViewTaskActivity.java index 6f07e2134..79924dd9b 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/ViewTaskActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/ViewTaskActivity.java @@ -4,10 +4,12 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.Toolbar; +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; +import android.view.Menu; import android.view.MenuItem; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; @@ -16,127 +18,208 @@ import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.task.OrderedTask; import org.researchstack.backbone.task.Task; import org.researchstack.backbone.ui.callbacks.StepCallbacks; import org.researchstack.backbone.ui.step.layout.StepLayout; import org.researchstack.backbone.ui.views.StepSwitcher; +import org.researchstack.backbone.utils.LogExt; +import org.researchstack.backbone.utils.StepLayoutHelper; -import java.lang.reflect.Constructor; import java.util.Date; -public class ViewTaskActivity extends PinCodeActivity implements StepCallbacks { - public static final String EXTRA_TASK = "ViewTaskActivity.ExtraTask"; +public class ViewTaskActivity extends PinCodeActivity implements StepCallbacks +{ + public static final String EXTRA_TASK = "ViewTaskActivity.ExtraTask"; public static final String EXTRA_TASK_RESULT = "ViewTaskActivity.ExtraTaskResult"; - public static final String EXTRA_STEP = "ViewTaskActivity.ExtraStep"; + public static final String EXTRA_STEP = "ViewTaskActivity.ExtraStep"; - private StepSwitcher root; + protected StepSwitcher root; + protected Toolbar toolbar; - private Step currentStep; - private Task task; - private TaskResult taskResult; + protected StepLayout currentStepLayout; + public StepLayout getCurrentStepLayout() { + return currentStepLayout; + } + + protected Step currentStep; + protected Task task; + public Task getTask() { + return task; + } + protected TaskResult taskResult; - public static Intent newIntent(Context context, Task task) { + protected int currentStepAction; + + public static Intent newIntent(Context context, Task task) + { Intent intent = new Intent(context, ViewTaskActivity.class); intent.putExtra(EXTRA_TASK, task); return intent; } @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(Bundle savedInstanceState) + { super.onCreate(savedInstanceState); super.setResult(RESULT_CANCELED); - super.setContentView(R.layout.rsb_activity_step_switcher); + super.setContentView(getContentViewId()); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar = (Toolbar) findViewById(getToolbarResourceId()); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - root = (StepSwitcher) findViewById(R.id.container); + root = (StepSwitcher) findViewById(getViewSwitcherRootId()); + + if(savedInstanceState == null) + { + task = (Task) getIntent() .getSerializableExtra(EXTRA_TASK); + + // Grab the existing task result if it is available, otherwise make a new one + if (getIntent().hasExtra(EXTRA_TASK_RESULT)) { + taskResult = (TaskResult) getIntent().getSerializableExtra(EXTRA_TASK_RESULT); + } else { + taskResult = new TaskResult(task.getIdentifier()); + } + + if (getIntent().hasExtra(EXTRA_STEP)) { + currentStep = (Step)getIntent().getSerializableExtra(EXTRA_STEP); + } - if (savedInstanceState == null) { - task = (Task) getIntent().getSerializableExtra(EXTRA_TASK); - taskResult = new TaskResult(task.getIdentifier()); taskResult.setStartDate(new Date()); - } else { + } + else + { task = (Task) savedInstanceState.getSerializable(EXTRA_TASK); taskResult = (TaskResult) savedInstanceState.getSerializable(EXTRA_TASK_RESULT); currentStep = (Step) savedInstanceState.getSerializable(EXTRA_STEP); } + LogExt.d(ViewTaskActivity.class, "Received task: "+task.getIdentifier()); + task.validateParameters(); task.onViewChange(Task.ViewChangeType.ActivityCreate, this, currentStep); } + public @IdRes int getToolbarResourceId() { + return R.id.toolbar; + } + /** * Returns the actual current step being shown. - * * @return an instance of @Step */ - public Step getCurrentStep() { + public Step getCurrentStep() + { return currentStep; } - protected void showNextStep() { + public @LayoutRes int getContentViewId() { + return R.layout.rsb_activity_step_switcher; + } + + public @IdRes int getViewSwitcherRootId() { + return R.id.container; + } + + protected void showNextStep() + { + hideKeyboard(); Step nextStep = task.getStepAfterStep(currentStep, taskResult); - if (nextStep == null) { + if(nextStep == null) + { saveAndFinish(); - } else { + } + else + { showStep(nextStep); } } - protected void showPreviousStep() { + protected void showPreviousStep() + { Step previousStep = task.getStepBeforeStep(currentStep, taskResult); - if (previousStep == null) { - finish(); - } else { + if(previousStep == null) + { + discardResultsAndFinish(); + } + else + { showStep(previousStep); } } - private void showStep(Step step) { + protected void showStep(Step step) + { + showStep(step, false); + } + + protected void showStep(Step step, boolean alwaysReplaceView) + { int currentStepPosition = task.getProgressOfCurrentStep(currentStep, taskResult) .getCurrent(); int newStepPosition = task.getProgressOfCurrentStep(step, taskResult).getCurrent(); StepLayout stepLayout = getLayoutForStep(step); + + if (stepLayout == null) { + LogExt.e(ViewTaskActivity.class, "Trying to add a step layout with a null task result"); + return; + } + stepLayout.getLayout().setTag(R.id.rsb_step_layout_id, step.getIdentifier()); root.show(stepLayout, newStepPosition >= currentStepPosition ? StepSwitcher.SHIFT_LEFT - : StepSwitcher.SHIFT_RIGHT); + : StepSwitcher.SHIFT_RIGHT, alwaysReplaceView); currentStep = step; + currentStepLayout = stepLayout; + } + + protected void refreshCurrentStep() + { + showStep(currentStep, true); } - protected StepLayout getLayoutForStep(Step step) { + protected StepLayout getLayoutForStep(Step step) + { // Change the title on the activity String title = task.getTitleForStep(this, step); setActionBarTitle(title); + if (taskResult == null) { + LogExt.e(ViewTaskActivity.class, + "Trying to add a step layout with a null task result"); + return null; + } + // Get result from the TaskResult, can be null StepResult result = taskResult.getStepResult(step.getIdentifier()); // Return the Class & constructor - StepLayout stepLayout = createLayoutFromStep(step); + StepLayout stepLayout = StepLayoutHelper.createLayoutFromStep(step, this); + setupStepLayoutBeforeInitializeIsCalled(stepLayout); stepLayout.initialize(step, result); - stepLayout.setCallbacks(this); + + // Some step layouts need to know about the task result + if (stepLayout instanceof ResultListener) { + ((ResultListener)stepLayout).taskResult(this, taskResult); + } return stepLayout; } - @NonNull - private StepLayout createLayoutFromStep(Step step) { - try { - Class cls = step.getStepLayoutClass(); - Constructor constructor = cls.getConstructor(Context.class); - return (StepLayout) constructor.newInstance(this); - } catch (Exception e) { - throw new RuntimeException(e); + protected void setupStepLayoutBeforeInitializeIsCalled(StepLayout stepLayout) { + // can be implemented by sub-classes to set up the step layout before it's initialized + stepLayout.setCallbacks(this); + if (stepLayout instanceof OnActionListener) { + ((OnActionListener)stepLayout).onAction(currentStepAction, this); } } - private void saveAndFinish() { + protected void saveAndFinish() + { taskResult.setEndDate(new Date()); Intent resultIntent = new Intent(); resultIntent.putExtra(EXTRA_TASK_RESULT, taskResult); @@ -145,28 +228,44 @@ private void saveAndFinish() { } @Override - protected void onPause() { + protected void onPause() + { hideKeyboard(); super.onPause(); task.onViewChange(Task.ViewChangeType.ActivityPause, this, currentStep); + + // Some step layouts need to know about when the activity pauses + if (currentStepLayout != null && currentStepLayout instanceof ActivityPauseListener) { + ((ActivityPauseListener)currentStepLayout).onActivityPause(this); + } } @Override - protected void onResume() { + protected void onResume(){ super.onResume(); task.onViewChange(Task.ViewChangeType.ActivityResume, this, currentStep); } @Override - protected void onStop() { + protected void onStop(){ super.onStop(); task.onViewChange(Task.ViewChangeType.ActivityStop, this, currentStep); } + @Override + public boolean onCreateOptionsMenu(Menu menu){ + // Create Menu which has an "X" or cancel icon + getMenuInflater().inflate(R.menu.rsb_task_menu, menu); + return super.onCreateOptionsMenu(menu); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { + if(item.getItemId() == R.id.rsb_clear_menu_item) { + showConfirmExitDialog(); + return true; + } else if(item.getItemId() == android.R.id.home) { notifyStepOfBackPress(); return true; } @@ -174,29 +273,50 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } + /** + * Clear out all the data that has been saved by this Activity + * And push user back to the Overview screen, or whatever screen was below this Activity + */ + protected void discardResultsAndFinish() { + if (taskResult != null) { // taskResult can be null in a bad state + taskResult.getResults().clear(); + } else { + LogExt.d(ViewTaskActivity.class, + "Task result is already null when discarding results"); + } + taskResult = null; + setResult(Activity.RESULT_CANCELED); + finish(); + } + @Override - public void onBackPressed() { + public void onBackPressed() + { notifyStepOfBackPress(); } @Override - protected void onSaveInstanceState(Bundle outState) { + protected void onSaveInstanceState(Bundle outState) + { super.onSaveInstanceState(outState); outState.putSerializable(EXTRA_TASK, task); outState.putSerializable(EXTRA_TASK_RESULT, taskResult); outState.putSerializable(EXTRA_STEP, currentStep); } - private void notifyStepOfBackPress() { + protected void notifyStepOfBackPress() + { StepLayout currentStepLayout = (StepLayout) findViewById(R.id.rsb_current_step); currentStepLayout.isBackEventConsumed(); } @Override - public void onDataReady() { + public void onDataReady() + { super.onDataReady(); - if (currentStep == null) { + if(currentStep == null) + { currentStep = task.getStepAfterStep(null, taskResult); } @@ -204,65 +324,128 @@ public void onDataReady() { } @Override - public void onDataFailed() { + public void onDataFailed() + { super.onDataFailed(); Toast.makeText(this, R.string.rsb_error_data_failed, Toast.LENGTH_LONG).show(); finish(); } @Override - public void onSaveStep(int action, Step step, StepResult result) { + public void onSaveStep(int action, Step step, StepResult result) + { onSaveStepResult(step.getIdentifier(), result); onExecuteStepAction(action); } - protected void onSaveStepResult(String id, StepResult result) { - taskResult.setStepResultForStepIdentifier(id, result); + protected void onSaveStepResult(String id, StepResult result) + { + if (taskResult == null) { + LogExt.e(ViewTaskActivity.class, "In bad state, " + + "task result should never be null, skipping onSaveStepResult"); + return; + } + if (result != null) { + taskResult.setStepResultForStepIdentifier(id, result); + } else if (taskResult.getResults() != null) { + // result is null, make sure that is reflected in the results + taskResult.getResults().remove(id); + } } - protected void onExecuteStepAction(int action) { - if (action == StepCallbacks.ACTION_NEXT) { + protected void onExecuteStepAction(int action) + { + currentStepAction = action; + if(action == StepCallbacks.ACTION_NEXT) + { showNextStep(); - } else if (action == StepCallbacks.ACTION_PREV) { + } + else if(action == StepCallbacks.ACTION_PREV) + { showPreviousStep(); - } else if (action == StepCallbacks.ACTION_END) { + } + else if(action == StepCallbacks.ACTION_END) + { showConfirmExitDialog(); - } else if (action == StepCallbacks.ACTION_NONE) { + } + else if(action == StepCallbacks.ACTION_NONE) + { // Used when onSaveInstanceState is called of a view. No action is taken. - } else { + } + else if(action == StepCallbacks.ACTION_REFRESH) + { + refreshCurrentStep(); + } + else + { throw new IllegalArgumentException("Action with value " + action + " is invalid. " + "See StepCallbacks for allowable arguments"); } } - private void hideKeyboard() { + protected void hideKeyboard() + { InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE); - if (imm.isActive() && imm.isAcceptingText()) { - imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0); + if(imm.isActive() && imm.isAcceptingText()) + { + if (getCurrentFocus() != null && getCurrentFocus().getWindowToken() != null) { + imm.hideSoftInputFromInputMethod(getCurrentFocus().getWindowToken(), 0); + } } } - private void showConfirmExitDialog() { - AlertDialog alertDialog = new AlertDialog.Builder(this).setTitle( - "Are you sure you want to exit?") - .setMessage(R.string.lorem_medium) - .setPositiveButton("End Task", (dialog, which) -> finish()) - .setNegativeButton("Cancel", null) - .create(); - alertDialog.show(); + /** + * Make sure user is 100% wanting to cancel, since their data will be discarded + * This may choose to simply exit the activity if the user is on the first step of the OrderedTask + */ + public void showConfirmExitDialog() + { + boolean showConfigrmDialog = true; + // Do not show the "Are you sure?" dialog if we are on the first step + if (task instanceof OrderedTask) { + OrderedTask orderedTask = (OrderedTask)task; + if (currentStep != null && orderedTask.getSteps().indexOf(currentStep) == 0) { + showConfigrmDialog = false; + } + } + if (showConfigrmDialog) { + new AlertDialog.Builder(this) + .setTitle(R.string.rsb_are_you_sure) + .setPositiveButton(R.string.rsb_discard_results, (dialog, i) -> discardResultsAndFinish()) + .setNegativeButton(R.string.rsb_cancel, null).create().show(); + } else { + discardResultsAndFinish(); + } } @Override - public void onCancelStep() { - setResult(Activity.RESULT_CANCELED); - finish(); + public void onCancelStep() + { + discardResultsAndFinish(); } - public void setActionBarTitle(String title) { + public void setActionBarTitle(String title) + { ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { + if(actionBar != null) + { actionBar.setTitle(title); } } + + public interface ResultListener { + void taskResult(ViewTaskActivity activity, TaskResult taskResult); + } + + public interface ActivityPauseListener { + void onActivityPause(ViewTaskActivity activity); + } + + /** + * This interface allows StepLayouts to know what step action brought the user to them + */ + public interface OnActionListener { + void onAction(int action, ViewTaskActivity activity); + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/ViewVideoActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/ViewVideoActivity.java index a5db40bee..0c3bac113 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/ViewVideoActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/ViewVideoActivity.java @@ -4,7 +4,7 @@ import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.widget.MediaController; import org.researchstack.backbone.R; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/ViewWebDocumentActivity.java b/backbone/src/main/java/org/researchstack/backbone/ui/ViewWebDocumentActivity.java index 8c9bbaffe..206f60309 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/ViewWebDocumentActivity.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/ViewWebDocumentActivity.java @@ -3,9 +3,9 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import android.view.MenuItem; import org.researchstack.backbone.R; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/adapter/MainPagerAdapter.java b/backbone/src/main/java/org/researchstack/backbone/ui/adapter/MainPagerAdapter.java new file mode 100644 index 000000000..16182be28 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/adapter/MainPagerAdapter.java @@ -0,0 +1,55 @@ +package org.researchstack.backbone.ui.adapter; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import android.util.SparseArray; +import android.view.ViewGroup; + +import org.researchstack.backbone.utils.ViewUtils; +import org.researchstack.backbone.ActionItem; + +import java.util.List; + + +public class MainPagerAdapter extends FragmentPagerAdapter { + private List items; + + /** + * Keep a list of fragments so you can get access to one + */ + SparseArray registeredFragments = new SparseArray<>(); + + public MainPagerAdapter(FragmentManager fm, List items) { + super(fm); + this.items = items; + } + + @Override + public Fragment getItem(int position) { + ActionItem item = items.get(position); + return ViewUtils.createFragment(item.getClazz()); + } + + @Override + public int getCount() { + return items.size(); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + Fragment fragment = (Fragment) super.instantiateItem(container, position); + registeredFragments.put(position, fragment); + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + registeredFragments.remove(position); + super.destroyItem(container, position, object); + } + + public Fragment getRegisteredFragment(int position) { + return registeredFragments.get(position); + } +} diff --git a/skin/src/main/java/org/researchstack/skin/ui/adapter/OnboardingPagerAdapter.java b/backbone/src/main/java/org/researchstack/backbone/ui/adapter/OnboardingPagerAdapter.java similarity index 89% rename from skin/src/main/java/org/researchstack/skin/ui/adapter/OnboardingPagerAdapter.java rename to backbone/src/main/java/org/researchstack/backbone/ui/adapter/OnboardingPagerAdapter.java index e877604d0..554afca79 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/adapter/OnboardingPagerAdapter.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/adapter/OnboardingPagerAdapter.java @@ -1,8 +1,8 @@ -package org.researchstack.skin.ui.adapter; +package org.researchstack.backbone.ui.adapter; import android.content.Context; import android.content.Intent; -import android.support.v4.view.PagerAdapter; +import androidx.viewpager.widget.PagerAdapter; import android.text.Html; import android.view.LayoutInflater; import android.view.View; @@ -12,9 +12,9 @@ import org.researchstack.backbone.ui.ViewVideoActivity; import org.researchstack.backbone.ui.views.LocalWebView; import org.researchstack.backbone.utils.TextUtils; -import org.researchstack.skin.R; -import org.researchstack.skin.ResourceManager; -import org.researchstack.skin.model.StudyOverviewModel; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ResourceManager; +import org.researchstack.backbone.model.StudyOverviewModel; import java.util.List; @@ -55,7 +55,7 @@ public Object instantiateItem(ViewGroup container, int position) { StudyOverviewModel.Question item = items.get(position); if (!TextUtils.isEmpty(item.getVideoName())) { - View layout = inflater.inflate(R.layout.rss_layout_study_html, container, false); + View layout = inflater.inflate(R.layout.rsb_layout_study_html, container, false); container.addView(layout); StringBuilder builder = new StringBuilder("

    " + item.getTitle() + "

    "); @@ -63,7 +63,7 @@ public Object instantiateItem(ViewGroup container, int position) { TextView simpleView = (TextView) layout.findViewById(R.id.text); simpleView.setText(Html.fromHtml(builder.toString())); - simpleView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, R.drawable.rss_ic_video); + simpleView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, R.drawable.rsb_ic_video); simpleView.setOnClickListener(v -> { String videoPath = ResourceManager.getInstance() .generatePath(ResourceManager.Resource.TYPE_MP4, item.getVideoName()); diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/adapter/TaskAdapter.java b/backbone/src/main/java/org/researchstack/backbone/ui/adapter/TaskAdapter.java new file mode 100644 index 000000000..cee7fb0f3 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/adapter/TaskAdapter.java @@ -0,0 +1,172 @@ +package org.researchstack.backbone.ui.adapter; + +import android.content.Context; +import android.content.res.Resources; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.researchstack.backbone.model.SchedulesAndTasksModel; +import org.researchstack.backbone.utils.LogExt; +import org.researchstack.backbone.R; + +import java.util.ArrayList; +import java.util.List; + +import rx.subjects.PublishSubject; + +/** + * Created by rianhouston on 2/25/17. + */ + +public class TaskAdapter extends RecyclerView.Adapter { + private static final String LOG_TAG = TaskAdapter.class.getCanonicalName(); + + private static final int VIEW_TYPE_HEADER = 0; + private static final int VIEW_TYPE_ITEM = 1; + + @NonNull + protected final List tasks; + @NonNull + protected final LayoutInflater inflater; + + protected PublishSubject publishSubject = PublishSubject.create(); + + public TaskAdapter(@NonNull Context context) { + super(); + + tasks = new ArrayList<>(); + this.inflater = LayoutInflater.from(context); + } + + public PublishSubject getPublishSubject() { + return publishSubject; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + + if (viewType == VIEW_TYPE_HEADER) { + View view = inflater.inflate(R.layout.rsb_item_schedule_header, parent, false); + return new HeaderViewHolder(view); + } else { + View view = inflater.inflate(R.layout.rsb_item_schedule, parent, false); + return new ViewHolder(view); + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder hldr, int position) { + Object obj = tasks.get(position); + if(hldr instanceof ViewHolder) { + ViewHolder holder = (ViewHolder) hldr; + SchedulesAndTasksModel.TaskScheduleModel task = (SchedulesAndTasksModel.TaskScheduleModel)obj; + + Resources res = holder.itemView.getResources(); + int tintColor = getColorForTask(res, task.taskID); + holder.colorBar.setBackgroundColor(tintColor); + + holder.title.setText(task.taskTitle); + holder.subtitle.setText(task.taskCompletionTime); + + holder.itemView.setOnClickListener(v -> { + LogExt.d(LOG_TAG, "Item clicked: " + task.taskID + ", " + task.taskType); + publishSubject.onNext(task); + }); + } else { + HeaderViewHolder holder = (HeaderViewHolder) hldr; + Header header = (Header)obj; + holder.title.setText(header.title); + holder.message.setText(header.message); + } + } + + @Override + public int getItemCount() { + return tasks.size(); + } + + @Override + public int getItemViewType(int position) { + Object item = tasks.get(position); + return item instanceof Header ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM; + } + + // Clean all elements of the recycler + public void clear() { + tasks.clear(); + notifyDataSetChanged(); + } + + // Add a list of items + public void addAll(List list) { + tasks.addAll(list); + notifyDataSetChanged(); + } + + public static class HeaderViewHolder extends RecyclerView.ViewHolder { + + TextView title; + TextView message; + + public HeaderViewHolder(View itemView) { + super(itemView); + title = (TextView) itemView.findViewById(R.id.activity_header_title); + message = (TextView) itemView.findViewById(R.id.activity_header_message); + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + View colorBar; + ImageView dailyIndicator; + TextView title; + TextView subtitle; + + public ViewHolder(View itemView) { + super(itemView); + colorBar = itemView.findViewById(R.id.color_bar); + dailyIndicator = (ImageView) itemView.findViewById(R.id.daily_indicator); + title = (TextView) itemView.findViewById(R.id.task_title); + subtitle = (TextView) itemView.findViewById(R.id.task_subtitle); + } + } + + public static class Header { + String title; + String message; + + public Header(String t, String m) { + title = t; + message = m; + } + } + + // TODO: this should live somewhere else like ResourceManager? Cause it will be app specific + // and the constants defined somewhere? + private int getColorForTask(Resources resources, String taskId) { + int colorId = 0; + if(taskId != null) { + if (taskId.contains("APHTimedWalking")) { + colorId = resources.getColor(R.color.rsb_activity_yellow); + } else if (taskId.contains("APHPhonation")) { + colorId = resources.getColor(R.color.rsb_activity_blue); + } else if (taskId.contains("APHIntervalTapping")) { + colorId = resources.getColor(R.color.rsb_activity_purple); + } else if (taskId.contains("APHMedicationTracker")) { + colorId = resources.getColor(R.color.rsb_activity_red); + } else if (taskId.contains("APHTremor")) { + colorId = resources.getColor(R.color.rsb_colorPrimary); + } else { + colorId = resources.getColor(R.color.rsb_activity_default); + } + } else { + colorId = resources.getColor(R.color.rsb_activity_default); + } + + return colorId; + } +} diff --git a/skin/src/main/java/org/researchstack/skin/ui/views/TextWatcherAdapter.java b/backbone/src/main/java/org/researchstack/backbone/ui/adapter/TextWatcherAdapter.java similarity index 89% rename from skin/src/main/java/org/researchstack/skin/ui/views/TextWatcherAdapter.java rename to backbone/src/main/java/org/researchstack/backbone/ui/adapter/TextWatcherAdapter.java index af7cd9d38..6943ae2d1 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/views/TextWatcherAdapter.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/adapter/TextWatcherAdapter.java @@ -1,9 +1,10 @@ -package org.researchstack.skin.ui.views; +package org.researchstack.backbone.ui.adapter; import android.text.Editable; import android.text.TextWatcher; public class TextWatcherAdapter implements TextWatcher { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/callbacks/StepCallbacks.java b/backbone/src/main/java/org/researchstack/backbone/ui/callbacks/StepCallbacks.java index b18d79768..b0bd4cb75 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/callbacks/StepCallbacks.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/callbacks/StepCallbacks.java @@ -8,6 +8,7 @@ public interface StepCallbacks { int ACTION_NONE = 0; int ACTION_NEXT = 1; int ACTION_END = 2; + int ACTION_REFRESH = 3; // step layout should be refreshed void onSaveStep(int action, Step step, StepResult result); diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/fragment/ActivitiesFragment.java b/backbone/src/main/java/org/researchstack/backbone/ui/fragment/ActivitiesFragment.java new file mode 100644 index 000000000..246e63dc0 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/fragment/ActivitiesFragment.java @@ -0,0 +1,401 @@ +package org.researchstack.backbone.ui.fragment; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import org.joda.time.DateTime; +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.StorageAccess; +import org.researchstack.backbone.factory.IntentFactory; +import org.researchstack.backbone.factory.ObservableTransformerFactory; +import org.researchstack.backbone.model.SchedulesAndTasksModel; +import org.researchstack.backbone.model.survey.SurveyItem; +import org.researchstack.backbone.model.survey.SurveyItemAdapter; +import org.researchstack.backbone.model.taskitem.TaskItem; +import org.researchstack.backbone.model.taskitem.TaskItemAdapter; +import org.researchstack.backbone.model.taskitem.factory.TaskItemFactory; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.storage.file.StorageAccessListener; +import org.researchstack.backbone.task.Task; +import org.researchstack.backbone.task.factory.MoodSurveyFactory; +import org.researchstack.backbone.task.factory.MoodSurveyFrequency; +import org.researchstack.backbone.ui.ActiveTaskActivity; +import org.researchstack.backbone.ui.ViewTaskActivity; +import org.researchstack.backbone.utils.LogExt; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ui.adapter.TaskAdapter; +import org.researchstack.backbone.ui.views.DividerItemDecoration; + +import java.util.ArrayList; +import java.util.List; + +import rx.Subscription; +import rx.functions.Action1; + +public abstract class ActivitiesFragment extends Fragment implements StorageAccessListener { + private static final String LOG_TAG = ActivitiesFragment.class.getCanonicalName(); + public static final int REQUEST_TASK = 1492; + + private TaskAdapter adapter; + private IntentFactory intentFactory = IntentFactory.INSTANCE; + private ObservableTransformerFactory observableTransformerFactory = + ObservableTransformerFactory.INSTANCE; + protected RecyclerView recyclerView; + private Subscription subscription; + private SwipeRefreshLayout swipeContainer; + + public TaskAdapter getAdapter() { + return adapter; + } + + public void setAdapter(TaskAdapter adapter) { + this.adapter = adapter; + } + + /** Intent factory, made available for subclasses to create Intent instances. */ + public final IntentFactory getIntentFactory() { + return intentFactory; + } + + /** Intent factory setter, made available if subclasses' unit tests need to mock. */ + public final void setIntentFactory(@NonNull IntentFactory intentFactory) { + this.intentFactory = intentFactory; + } + + // To allow unit tests to mock. + @VisibleForTesting + void setObservableTransformerFactory( + @NonNull ObservableTransformerFactory observableTransformerFactory) { + this.observableTransformerFactory = observableTransformerFactory; + } + + public RecyclerView getRecyclerView() { + return recyclerView; + } + + public Subscription getRxSubscription() { + return subscription; + } + + public void setRxSubscription(Subscription subscription) { + this.subscription = subscription; + } + + public SwipeRefreshLayout getSwipeFreshLayout() { + return swipeContainer; + } + + // To allow unit tests to mock. + @VisibleForTesting + void setSwipeFreshLayout(@NonNull SwipeRefreshLayout swipeContainer) { + this.swipeContainer = swipeContainer; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.rsb_fragment_activities, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + recyclerView = view.findViewById(R.id.recycler_view); + swipeContainer = view.findViewById(R.id.swipe_container); + recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); + recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), + DividerItemDecoration.VERTICAL_LIST, 0, false)); + + createOrClearAdapter(); + + // TODO: might need to add logic to prevent multiple requests + swipeContainer.setOnRefreshListener(this::fetchData); + + super.onViewCreated(view, savedInstanceState); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + unsubscribe(); + } + + public void unsubscribe() { + if (subscription != null) { + subscription.unsubscribe(); + } + } + + /** + * Override this method to provide a customer adapter for your application. + * + * @return The adapter for displaying the list of tasks. + */ + protected TaskAdapter createTaskAdapter() { + return new TaskAdapter(getActivity()); + } + + public void fetchData() { + DataProvider.getInstance().loadTasksAndSchedules(getActivity()).toObservable() + .compose(observableTransformerFactory.defaultTransformer()) + .subscribe(new Action1() { + @Override + public void call(SchedulesAndTasksModel model) { + if (isFragmentValid()) { + refreshAdapterSuccess(model); + } + } + }, new Action1() { + @Override + public void call(Throwable throwable) { + if (isFragmentValid()) { + refreshAdapterFailure(throwable.getLocalizedMessage()); + } + } + }); + } + + protected boolean isFragmentValid() { + return getActivity() != null && isAdded(); + } + + protected void refreshAdapterFailure(String errorMessage) { + swipeContainer.setRefreshing(false); + new AlertDialog.Builder(getContext()) + .setMessage(errorMessage) + .setPositiveButton(R.string.rsb_BUTTON_OK, null) + .create().show(); + } + + /** + * @param model SchedulesAndTasksModel can be used to fill the adapter + */ + protected void refreshAdapterSuccess(SchedulesAndTasksModel model) { + swipeContainer.setRefreshing(false); + createOrClearAdapter(); + adapter.addAll(processResults(model)); + } + + protected void createOrClearAdapter() { + if (adapter == null) { + unsubscribe(); + adapter = createTaskAdapter(); + recyclerView.setAdapter(adapter); + + subscription = adapter.getPublishSubject().subscribe(task -> { + LogExt.d(LOG_TAG, "Publish subject subscribe clicked."); + taskSelected(task); + }); + } else { + adapter.clear(); + } + } + + public void taskSelected(SchedulesAndTasksModel.TaskScheduleModel task) { + // Load task attempts to load a survey task based, based on the data provider. + DataProvider.getInstance().loadTask(getContext(), task).subscribe(newTask -> { + if (newTask == null) { + // We were unable to load the survey task. This probably means it's one of these + // custom tasks, keyed off task ID. + startCustomTask(task); + } else { + // This is a survey task. + startActivityForResult(intentFactory.newTaskIntent(getContext(), + getDefaultViewTaskActivityClass(), newTask), REQUEST_TASK); + } + }); + } + + public Class getDefaultViewTaskActivityClass() { + return ViewTaskActivity.class; + } + + /** + *

    + * Start a custom task based on the given task schedule model. Generally used if the + * DataProvider cannot load the task for whatever reason. + *

    + *

    + * Apps should override this method to specify their own custom tasks. + *

    + * + * @param task + * task schedule model to trigger the custom task + */ + protected abstract void startCustomTask(SchedulesAndTasksModel.TaskScheduleModel task); + + /** + * Process the model to create section groups and section headers + * + * @param model SchedulesAndTasksModel object + * @return a list of section groups and section headers + */ + public List processResults(SchedulesAndTasksModel model) { + if (model == null || model.schedules == null) { + return Lists.newArrayList(); + } + List tasks = new ArrayList<>(); + + DateTime now = new DateTime(); + DateTime startOfDay = new DateTime().withTimeAtStartOfDay().minusSeconds(1); + DateTime startOfYesterday = new DateTime().minusDays(1).withTimeAtStartOfDay().minusSeconds(1); + DateTime startOfTomorrow = new DateTime().plusDays(1).withTimeAtStartOfDay().minusSeconds(1); + + List yesterdayTasks = new ArrayList<>(); + List todaysTasks = new ArrayList<>(); + List optionalTasks = new ArrayList<>(); + + for (SchedulesAndTasksModel.ScheduleModel schedule : model.schedules) { + DateTime scheduled = (schedule.scheduledOn != null) ? new DateTime(schedule.scheduledOn) : new DateTime(); + boolean today = (scheduled.isAfter(startOfDay) && scheduled.isBefore(startOfTomorrow)); + boolean yesterday = (scheduled.isAfter(startOfYesterday) && scheduled.isBefore(startOfDay)); + for (SchedulesAndTasksModel.TaskScheduleModel task : schedule.tasks) { + if (today && task.taskIsOptional) { + optionalTasks.add(task); + } else if (today) { + todaysTasks.add(task); + } else if (yesterday) { + yesterdayTasks.add(task); + } else { + // skipping task + LogExt.d(LOG_TAG, "Skipping task: " + task.taskID); + } + + } + } + + // todays tasks + tasks.add(new TaskAdapter.Header(getActivity().getString(R.string.rsb_activities_today_header_title, + now.dayOfWeek().getAsText(), + now.monthOfYear().getAsText(), + now.dayOfMonth().getAsText()), + getActivity().getString(R.string.rsb_activities_today_header_message))); + tasks.addAll(todaysTasks); + + // todays optional tasks + if (optionalTasks.size() > 0) { + tasks.add(new TaskAdapter.Header(getActivity().getString(R.string.rsb_activities_optional_header_title), + getActivity().getString(R.string.rsb_activities_optional_header_message))); + tasks.addAll(optionalTasks); + } + + // yesterdays tasks + if (yesterdayTasks.size() > 0) { + tasks.add(new TaskAdapter.Header(getActivity().getString(R.string.rsb_activities_yesterday_header_title), + getActivity().getString(R.string.rsb_activities_yesterday_header_message))); + tasks.addAll(yesterdayTasks); + } + + return tasks; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_TASK) { + LogExt.d(LOG_TAG, "Received task result from task activity"); + + TaskResult taskResult = (TaskResult) data.getSerializableExtra(ViewTaskActivity.EXTRA_TASK_RESULT); + StorageAccess.getInstance().getAppDatabase().saveTaskResult(taskResult); + DataProvider.getInstance().uploadTaskResult(getActivity(), taskResult); + + fetchData();; + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void onDataReady() { + LogExt.i(LOG_TAG, "onDataReady()"); + + fetchData(); + } + + @Override + public void onDataFailed() { + // Ignore + } + + @Override + public void onDataAuth() { + // Ignore, activity handles auth + } + + // TODO: remove the methods below once we finish task builder + private Gson createGson() { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(SurveyItem.class, new SurveyItemAdapter()); + builder.registerTypeAdapter(TaskItem.class, new TaskItemAdapter()); + return builder.create(); + } + + public void startCustomTappingTask() { + String taskItemJson = "{\"taskIdentifier\":\"2-APHIntervalTapping-7259AC18-D711-47A6-ADBD-6CFCECDED1DF\",\"schemaIdentifier\":\"TappingActivity\",\"taskType\":\"tapping\",\"intendedUseDescription\":\"Speed of finger tapping can reflect severity of motor symptoms in Parkinson disease. This activity measures your tapping speed for each hand. Your medical provider may measure this differently.\",\"taskOptions\":{\"duration\":20.0,\"handOptions\":\"both\"},\"localizedSteps\":[{\"identifier\":\"conclusion\",\"type\":\"instruction\",\"text\":\"Thank You!\"}]}"; + TaskItemFactory factory = new TaskItemFactory(); + Task task = factory.createTask(getContext(), createGson().fromJson(taskItemJson, TaskItem.class)); + startActivityForResult(ActiveTaskActivity.newIntent(getContext(), task), REQUEST_TASK); + } + + public void startCustomTremorTask() { + String taskItemJson = "{\"taskIdentifier\":\"1-APHTremor-108E189F-4B5B-48DC-BFD7-FA6796EEf439\",\"schemaIdentifier\":\"Tremor Activity\",\"taskType\":\"tremor\",\"taskOptions\":{\"duration\":10.0,\"handOptions\":\"both\",\"excludePositions\":[\"elbowBent\",\"handQueenWave\"]}}"; + startCustomTask(taskItemJson); + } + + public void startCustomVoiceTask() { + String taskItemJson = "{\"taskIdentifier\":\"3-APHPhonation-C614A231-A7B7-4173-BDC8-098309354292\",\"schemaIdentifier\":\"Voice Activity\",\"taskType\":\"voice\",\"intendedUseDescription\":\"This activitiy evaluates your voice by recording it with the microphone at the bottom of your phone.\",\"localizedSteps\":[{\"identifier\":\"instruction\",\"type\":\"instruction\",\"title\":\"Voice\"},{\"identifier\":\"instruction1\",\"type\":\"instruction\",\"title\":\"Voice\",\"text\":\"Take a deep breath and say “Aaaaah” into the microphone for as long as you can. Keep a steady volume so the audio bars remain blue.\",\"detailText\":\"Tap Get Started to begin the test.\"},{\"identifier\":\"countdown\",\"type\":\"instruction\",\"text\":\"Please wait while we check the ambient sound levels.\"}],\"taskOptions\":{\"duration\":10.0}}"; + startCustomTask(taskItemJson); + } + + public void startCustomWalkingTask() { + String taskItemJson = "{\"taskIdentifier\":\"4-APHTimedWalking-80F09109-265A-49C6-9C5D-765E49AAF5D9\",\"schemaIdentifier\":\"Walking Activity\",\"taskType\":\"shortWalk\",\"taskOptions\":{\"restDuration\":30.0,\"numberOfStepsPerLeg\":100.0},\"removeSteps\":[\"walking.return\"],\"localizedSteps\":[{\"identifier\":\"instruction\",\"type\":\"instruction\",\"text\":\"This activity measures your gait (walk) and balance, which can be affected by Parkinson disease.\",\"detailText\":\"Please do not continue if you cannot safely walk unassisted.\"},{\"identifier\":\"instruction1\",\"type\":\"instruction\",\"text\":\"\u2022 Please wear a comfortable pair of walking shoes and find a flat, smooth surface for walking.\n\n\u2022 Try to walk continuously by turning at the ends of your path, as if you are walking around a cone.\n\n\u2022 Importantly, walk at your normal pace. You do not need to walk faster than usual.\",\"detailText\":\"Put your phone in a pocket or bag and follow the audio instructions.\"},{\"identifier\":\"walking.outbound\",\"type\":\"active\",\"stepDuration\":30.0,\"title\":\"\",\"text\":\"Walk back and forth for 30 seconds.\",\"stepSpokenInstruction\":\"Walk back and forth for 30 seconds.\"},{\"identifier\":\"walking.rest\",\"type\":\"active\",\"stepDuration\":30.0,\"text\":\"Turn around 360 degrees, then stand still, with your feet about shoulder-width apart. Rest your arms at your side and try to avoid moving for 30 seconds.\",\"stepSpokenInstruction\":\"Turn around 360 degrees, then stand still, with your feet about shoulder-width apart. Rest your arms at your side and try to avoid moving for 30 seconds.\"}]}"; + startCustomTask(taskItemJson); + } + + public void startCustomMoodSurveyTask() { + Task task = MoodSurveyFactory.moodSurvey( + getContext(), + MoodSurveyFactory.MoodSurveyIdentifier, + getContext().getString(R.string.rsb_activities_mood_survey_intended_use), + MoodSurveyFrequency.DAILY, + "Today, my thinking is:", + new ArrayList<>()); + startCustomTask(task); + } + + //endregion + + //region Start Custom Task Helpers + + private void startCustomTask(String taskItemJson) { + TaskItem taskItem = createGson().fromJson(taskItemJson, TaskItem.class); + startCustomTask(taskItem); + } + + private void startCustomTask(TaskItem taskItem) { + TaskItemFactory factory = new TaskItemFactory(); + Task task = factory.createTask(getContext(), taskItem); + startCustomTask(task); + } + + private void startCustomTask(Task task) { + startActivityForResult(ActiveTaskActivity.newIntent(getContext(), task), REQUEST_TASK); + } + + //endregion +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/fragment/LearnFragment.java b/backbone/src/main/java/org/researchstack/backbone/ui/fragment/LearnFragment.java new file mode 100644 index 000000000..5186c8cb6 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/fragment/LearnFragment.java @@ -0,0 +1,200 @@ +package org.researchstack.backbone.ui.fragment; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.researchstack.backbone.ResourcePathManager; +import org.researchstack.backbone.ui.ViewWebDocumentActivity; +import org.researchstack.backbone.utils.LogExt; +import org.researchstack.backbone.utils.ResUtils; +import org.researchstack.backbone.utils.TextUtils; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ResourceManager; +import org.researchstack.backbone.model.SectionModel; +import org.researchstack.backbone.ui.ShareActivity; + +import java.util.ArrayList; +import java.util.List; + +public class LearnFragment extends Fragment { + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.rsb_fragment_learn, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + SectionModel model = loadSections(); + if(model == null) return; + + ImageView logoView = (ImageView) view.findViewById(R.id.learn_logo_view); + if(!TextUtils.isEmpty(model.getLogoName())) + { + int resId = ResUtils.getDrawableResourceId(view.getContext(), model.getLogoName()); + logoView.setImageResource(resId); + } + else + { + logoView.setVisibility(View.GONE); + } + + TextView titleView = (TextView) view.findViewById(R.id.learn_title_view); + if(!TextUtils.isEmpty(model.getTitle())) + { + titleView.setText(model.getTitle()); + } + else + { + titleView.setVisibility(View.GONE); + } + + RecyclerView recyclerView = (RecyclerView) view.findViewById(org.researchstack.backbone.R.id.recycler_view); + recyclerView.setAdapter(new LearnAdapter(getContext(), loadSections())); + recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); + + } + + private SectionModel loadSections() { + SectionModel model = null; + ResourcePathManager.Resource resource = ResourceManager.getInstance().getLearnSections(); + try + { + model = resource.create(getActivity()); + } + catch (RuntimeException re) + { + LogExt.e(getClass(), "Error loading SectionModel for Learn: " + re.getMessage()); + } + + return model; + } + + public static class LearnAdapter extends RecyclerView.Adapter { + + private static final int VIEW_TYPE_HEADER = 0; + private static final int VIEW_TYPE_ITEM = 1; + + private Context context; + private List items; + private LayoutInflater inflater; + + public LearnAdapter(Context ctx, SectionModel sections) { + super(); + context = ctx; + items = new ArrayList<>(); + for (SectionModel.Section section : sections.getSections()) { + if(!TextUtils.isEmpty(section.getTitle())) + { + items.add(section.getTitle()); + } + items.addAll(section.getItems()); + + } + this.inflater = LayoutInflater.from(context); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_HEADER) { + View view = inflater.inflate(R.layout.preference_category_material, parent, false); + return new HeaderViewHolder(view); + } else { + View view = inflater.inflate(R.layout.rsb_item_row_learn, parent, false); + return new ViewHolder(view); + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder hldr, int position) { + if (hldr instanceof ViewHolder) { + ViewHolder holder = (ViewHolder) hldr; + + //Offset for header + SectionModel.SectionRow item = (SectionModel.SectionRow) items.get(position); + + holder.title.setText(item.getTitle()); + + if(!TextUtils.isEmpty(item.getIconImage())) + { + holder.icon.setVisibility(View.VISIBLE); + int resId = ResUtils.getDrawableResourceId(context, item.getIconImage()); + holder.icon.setImageResource(resId); + int colorId = ResUtils.getColorResourceId(context, item.getTintColor()); + holder.icon.setColorFilter(ContextCompat.getColor(context, colorId)); + } + else + { + holder.icon.setVisibility(View.GONE); + } + + holder.itemView.setOnClickListener(v -> { + if(SectionModel.SHARE_TYPE_DETAILS.equals(item.getDetails())) + { + Intent intent = new Intent(v.getContext(), ShareActivity.class); + v.getContext().startActivity(intent); + } + else { + String path = ResourceManager.getInstance(). + generateAbsolutePath(ResourceManager.Resource.TYPE_HTML, item.getDetails()); + Intent intent = ViewWebDocumentActivity.newIntentForPath(v.getContext(), + item.getTitle(), + path); + v.getContext().startActivity(intent); + + } + }); + } else { + HeaderViewHolder holder = (HeaderViewHolder) hldr; + String title = (String) items.get(position); + holder.title.setText(title); + } + } + + @Override + public int getItemViewType(int position) { + Object item = items.get(position); + return item instanceof String ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM; + } + + @Override + public int getItemCount() { + // Size of items + header + return items.size(); + } + + public static class HeaderViewHolder extends RecyclerView.ViewHolder { + + TextView title; + + public HeaderViewHolder(View itemView) { + super(itemView); + title = (TextView) itemView; + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView title; + ImageView icon; + + public ViewHolder(View itemView) { + super(itemView); + title = (TextView) itemView.findViewById(R.id.learn_item_title); + icon = (ImageView) itemView.findViewById(R.id.learn_item_icon); + } + } + } +} diff --git a/skin/src/main/java/org/researchstack/skin/ui/fragment/SettingsFragment.java b/backbone/src/main/java/org/researchstack/backbone/ui/fragment/SettingsFragment.java similarity index 59% rename from skin/src/main/java/org/researchstack/skin/ui/fragment/SettingsFragment.java rename to backbone/src/main/java/org/researchstack/backbone/ui/fragment/SettingsFragment.java index 2ee0e0482..e922f3c8e 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/fragment/SettingsFragment.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/fragment/SettingsFragment.java @@ -1,4 +1,4 @@ -package org.researchstack.skin.ui.fragment; +package org.researchstack.backbone.ui.fragment; import android.app.Activity; import android.content.Intent; @@ -6,11 +6,11 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceCategory; -import android.support.v7.preference.PreferenceFragmentCompat; -import android.support.v7.preference.PreferenceScreen; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -31,70 +31,72 @@ import org.researchstack.backbone.utils.FormatHelper; import org.researchstack.backbone.utils.LogExt; import org.researchstack.backbone.utils.ObservableUtils; -import org.researchstack.skin.AppPrefs; -import org.researchstack.skin.DataProvider; -import org.researchstack.skin.R; -import org.researchstack.skin.ResourceManager; -import org.researchstack.skin.model.ConsentSectionModel; -import org.researchstack.skin.notification.TaskAlertReceiver; -import org.researchstack.skin.step.PassCodeCreationStep; -import org.researchstack.skin.task.ConsentTask; -import org.researchstack.skin.ui.OnboardingActivity; -import org.researchstack.skin.ui.layout.SignUpPinCodeCreationStepLayout; -import org.researchstack.skin.utils.ConsentFormUtils; +import org.researchstack.backbone.AppPrefs; +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ResourceManager; +import org.researchstack.backbone.model.ConsentSectionModel; +import org.researchstack.backbone.notification.TaskAlertReceiver; +import org.researchstack.backbone.step.PassCodeCreationStep; +import org.researchstack.backbone.task.ConsentTask; +import org.researchstack.backbone.ui.OverviewActivity; +import org.researchstack.backbone.ui.step.layout.SignUpPinCodeCreationStepLayout; +import org.researchstack.backbone.utils.ConsentFormUtils; import java.text.DateFormat; -import java.text.ParseException; import java.util.Date; import rx.Observable; public class SettingsFragment extends PreferenceFragmentCompat implements - SharedPreferences.OnSharedPreferenceChangeListener, StorageAccessListener { + SharedPreferences.OnSharedPreferenceChangeListener, StorageAccessListener +{ + private static final int REQUEST_CODE_SHARING_OPTIONS = 0; + private static final int REQUEST_CODE_CHANGE_PASSCODE = 1; + //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // Settings Keys. - // If you are adding / changing settings, make sure they are unique / match in rss_settings.xml + // If you are adding / changing settings, make sure they are unique / match in rsb_settings.xml //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // Profile - public static final String KEY_PROFILE = "rss_settings_profile"; - public static final String KEY_PROFILE_NAME = "rss_settings_profile_name"; - public static final String KEY_PROFILE_BIRTHDATE = "rss_settings_profile_birthdate"; + public static final String KEY_PROFILE = "rsb_settings_profile"; + public static final String KEY_PROFILE_NAME = "rsb_settings_profile_name"; + public static final String KEY_PROFILE_BIRTHDATE = "rsb_settings_profile_birthdate"; // Reminders - public static final String KEY_REMINDERS = "rss_settings_reminders"; + public static final String KEY_REMINDERS = "rsb_settings_reminders"; // Privacy - public static final String KEY_PRIVACY = "rss_settings_privacy"; - public static final String KEY_PRIVACY_POLICY = "rss_settings_privacy_policy"; - public static final String KEY_REVIEW_CONSENT = "rss_settings_privacy_review_consent"; - public static final String KEY_SHARING_OPTIONS = "rss_settings_privacy_sharing_options"; + public static final String KEY_PRIVACY = "rsb_settings_privacy"; + public static final String KEY_PRIVACY_POLICY = "rsb_settings_privacy_policy"; + public static final String KEY_REVIEW_CONSENT = "rsb_settings_privacy_review_consent"; + public static final String KEY_SHARING_OPTIONS = "rsb_settings_privacy_sharing_options"; // Security - public static final String KEY_AUTO_LOCK_ENABLED = "rss_settings_auto_lock_on_exit"; - public static final String KEY_AUTO_LOCK_TIME = "rss_settings_auto_lock_time"; - public static final String KEY_CHANGE_PASSCODE = "rss_settings_security_change_passcode"; + public static final String KEY_AUTO_LOCK_ENABLED = "rsb_settings_auto_lock_on_exit"; + public static final String KEY_AUTO_LOCK_TIME = "rsb_settings_auto_lock_time"; + public static final String KEY_CHANGE_PASSCODE = "rsb_settings_security_change_passcode"; // General - public static final String KEY_GENERAL = "rss_settings_general"; - public static final String KEY_SOFTWARE_NOTICES = "rss_settings_general_software_notices"; - public static final String KEY_LEAVE_STUDY = "rss_settings_general_leave_study"; - public static final String KEY_JOIN_STUDY = "rss_settings_general_join_study"; + public static final String KEY_GENERAL = "rsb_settings_general"; + public static final String KEY_SOFTWARE_NOTICES = "rsb_settings_general_software_notices"; + public static final String KEY_LEAVE_STUDY = "rsb_settings_general_leave_study"; + public static final String KEY_JOIN_STUDY = "rsb_settings_general_join_study"; // Other - public static final String KEY_VERSION = "rss_settings_version"; - public static final String PASSCODE = "passcode"; - private static final int REQUEST_CODE_SHARING_OPTIONS = 0; - private static final int REQUEST_CODE_CHANGE_PASSCODE = 1; + public static final String KEY_VERSION = "rsb_settings_version"; + public static final String PASSCODE = "passcode"; + //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // Preference Items //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* private PreferenceCategory profileCategory; private PreferenceCategory privacyCategory; - private Preference sharingScope; + private Preference sharingScope; private PreferenceCategory generalCategory; - private Preference leaveStudy; - private Preference joinStudy; + private Preference leaveStudy; + private Preference joinStudy; //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // Field Vars //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* private ConsentSectionModel data; - private View progress; + private View progress; /** * This boolean is responsible for keeping track of the UI on whether changes have been made @@ -104,8 +106,9 @@ public class SettingsFragment extends PreferenceFragmentCompat implements private boolean isInitializedForConsent = false; @Override - public void onCreatePreferences(Bundle bundle, String s) { - super.addPreferencesFromResource(R.xml.rss_settings); + public void onCreatePreferences(Bundle bundle, String s) + { + super.addPreferencesFromResource(R.xml.rsb_settings); // Get our screen which is created in Skin SettingsFragment PreferenceScreen screen = getPreferenceScreen(); @@ -124,13 +127,15 @@ public void onCreatePreferences(Bundle bundle, String s) { screen.findPreference(KEY_VERSION).setSummary(getVersionString()); } - private void initPreferenceForConsent() { + private void initPreferenceForConsent() + { boolean isSignedInAndConsented = DataProvider.getInstance().isSignedIn(getActivity()) && DataProvider.getInstance().isConsented(getActivity()); PreferenceScreen screen = getPreferenceScreen(); - if (!isSignedInAndConsented) { + if(! isSignedInAndConsented) + { screen.removePreference(profileCategory); privacyCategory.removePreference(sharingScope); generalCategory.removePreference(leaveStudy); @@ -138,56 +143,61 @@ private void initPreferenceForConsent() { // This method will be called if we leave the study. This means we need to add // "join study" back into the general-category as it was removed on the initial call of // this method - if (generalCategory.findPreference(KEY_JOIN_STUDY) == null) { + if(generalCategory.findPreference(KEY_JOIN_STUDY) == null) + { generalCategory.addPreference(joinStudy); } - } else { + } + else + { generalCategory.removePreference(joinStudy); Observable.defer(() -> Observable.just(DataProvider.getInstance() - .getUser(getActivity()))) + .getUser(getActivity()))) .compose(ObservableUtils.applyDefault()) .subscribe(profile -> { - if (profile == null) { + if(profile == null) + { getPreferenceScreen().removePreference(profileCategory); return; } Preference namePref = profileCategory.findPreference(KEY_PROFILE_NAME); - if (profile.getName() != null) { + if(profile.getName() != null) + { namePref.setSummary(profile.getName()); - } else { + } + else + { profileCategory.removePreference(namePref); } Preference birthdatePref = profileCategory.findPreference( KEY_PROFILE_BIRTHDATE); - if (profile.getBirthDate() != null) { - try { - // The incoming date is formated in "yyyy-MM-dd", clean it up to "MMM dd, yyyy" - Date birthdate = FormatHelper.SIMPLE_FORMAT_DATE.parse(profile.getBirthDate()); - DateFormat format = FormatHelper.getFormat(DateFormat.LONG, - FormatHelper.NONE); - birthdatePref.setSummary(format.format(birthdate)); - } catch (ParseException e) { - LogExt.e(SettingsFragment.class, e); - birthdatePref.setSummary(profile.getBirthDate()); - } - } else { + if(profile.getBirthDate() != null) + { + // The incoming date is formated in "yyyy-MM-dd", clean it up to "MMM dd, yyyy" + Date birthdate = profile.getBirthDate(); + DateFormat format = FormatHelper.getFormat(DateFormat.LONG, + FormatHelper.NONE); + birthdatePref.setSummary(format.format(birthdate)); + } + else + { profileCategory.removePreference(birthdatePref); } }); // Load Consent Data and set sharing scope Observable.defer(() -> Observable.just(ResourceManager.getInstance() - .getConsentSections() - .create(getActivity()))).flatMap((consentData) -> { + .getConsentSections() + .create(getActivity()))).flatMap((consentData) -> { this.data = (ConsentSectionModel) consentData; // Load and set sharing scope return Observable.just(DataProvider.getInstance() - .getUserSharingScope(getContext())); + .getUserSharingScope(getContext())); }).compose(ObservableUtils.applyDefault()).subscribe(scope -> { sharingScope.setSummary(formatSharingOption(scope)); }); @@ -197,7 +207,8 @@ private void initPreferenceForConsent() { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { FrameLayout settingsRoot = new FrameLayout(container.getContext()); ViewGroup v = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState); @@ -211,11 +222,14 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa } @Override - public boolean onPreferenceTreeClick(Preference preference) { + public boolean onPreferenceTreeClick(Preference preference) + { LogExt.i(getClass(), String.valueOf(preference.getTitle())); - if (preference.hasKey()) { - switch (preference.getKey()) { + if(preference.hasKey()) + { + switch(preference.getKey()) + { case KEY_PRIVACY_POLICY: showPrivacyPolicy(); return true; @@ -230,9 +244,9 @@ public boolean onPreferenceTreeClick(Preference preference) { case KEY_CHANGE_PASSCODE: PassCodeCreationStep step = new PassCodeCreationStep(PASSCODE, - R.string.rss_passcode_change_title); + R.string.rsb_passcode_change_title); step.setStateOrdinal(SignUpPinCodeCreationStepLayout.State.CHANGE.ordinal()); - OrderedTask passcodeTask = new OrderedTask("task_rss_settings_passcode", step); + OrderedTask passcodeTask = new OrderedTask("task_rsb_settings_passcode", step); Intent passcodeIntent = ViewTaskActivity.newIntent(getContext(), passcodeTask); startActivityForResult(passcodeIntent, REQUEST_CODE_CHANGE_PASSCODE); return true; @@ -249,28 +263,28 @@ public boolean onPreferenceTreeClick(Preference preference) { ConsentSharingStep sharingStep = new ConsentSharingStep(ConsentTask.ID_SHARING); sharingStep.setOptional(false); - sharingStep.setStepTitle(R.string.rss_settings_privacy_sharing_options); + sharingStep.setStepTitle(R.string.rsb_settings_privacy_sharing_options); String shareWidely = getString(R.string.rsb_consent_share_widely, - investigatorLongDesc); + investigatorLongDesc); Choice shareWidelyChoice = new Choice<>(shareWidely, - "sponsors_and_partners", - null); + "sponsors_and_partners", + null); String shareRestricted = getString(R.string.rsb_consent_share_only, - investigatorShortDesc); + investigatorShortDesc); Choice shareRestrictedChoice = new Choice<>(shareRestricted, - "all_qualified_researchers", - null); + "all_qualified_researchers", + null); sharingStep.setAnswerFormat(new ChoiceAnswerFormat(AnswerFormat.ChoiceAnswerStyle.SingleChoice, - shareWidelyChoice, - shareRestrictedChoice)); + shareWidelyChoice, + shareRestrictedChoice)); sharingStep.setTitle(getString(R.string.rsb_consent_share_title)); sharingStep.setText(getString(R.string.rsb_consent_share_description, - investigatorLongDesc, - localizedLearnMoreHTMLContent)); + investigatorLongDesc, + localizedLearnMoreHTMLContent)); Task task = new OrderedTask("SharingStepTask", sharingStep); Intent intent = ViewTaskActivity.newIntent(getContext(), task); @@ -278,49 +292,53 @@ public boolean onPreferenceTreeClick(Preference preference) { return true; case KEY_LEAVE_STUDY: - new AlertDialog.Builder(getActivity()).setTitle(R.string.rss_settings_general_leave_study) - .setMessage(R.string.rss_settings_dialog_leave_study) - .setPositiveButton(R.string.rss_settings_general_leave_study, - (dialog, which) -> { - - progress.setVisibility(View.VISIBLE); - - DataProvider.getInstance() - .withdrawConsent(getActivity(), null) - .subscribe(response -> { - progress.setVisibility(View.GONE); - - if (response.isSuccess()) { - Toast.makeText(getActivity(), - R.string.rss_network_result_consent_withdraw_success, - Toast.LENGTH_SHORT).show(); - - isInitializedForConsent = false; - initPreferenceForConsent(); - } else { - Toast.makeText(getActivity(), - R.string.rss_network_error_consent_withdraw_failed, - Toast.LENGTH_SHORT).show(); - } - - }, error -> { - LogExt.e(getClass(), error); - progress.setVisibility(View.GONE); - }); - }) + new AlertDialog.Builder(getActivity()).setTitle(R.string.rsb_settings_general_leave_study) + .setMessage(R.string.rsb_settings_dialog_leave_study) + .setPositiveButton(R.string.rsb_settings_general_leave_study, + (dialog, which) -> { + + progress.setVisibility(View.VISIBLE); + + DataProvider.getInstance() + .withdrawConsent(getActivity(), null) + .subscribe(response -> { + progress.setVisibility(View.GONE); + + if(response.isSuccess()) + { + Toast.makeText(getActivity(), + R.string.rsb_network_result_consent_withdraw_success, + Toast.LENGTH_SHORT).show(); + + isInitializedForConsent = false; + initPreferenceForConsent(); + } + else + { + Toast.makeText(getActivity(), + R.string.rsb_network_error_consent_withdraw_failed, + Toast.LENGTH_SHORT).show(); + } + + }, error -> { + LogExt.e(getClass(), error); + progress.setVisibility(View.GONE); + }); + }) .setNegativeButton(R.string.rsb_cancel, null) .show(); return true; case KEY_JOIN_STUDY: - startActivity(new Intent(getActivity(), OnboardingActivity.class)); + startActivity(new Intent(getActivity(), OverviewActivity.class)); getActivity().finish(); return true; case KEY_REMINDERS: SharedPreferences preferences = preference.getSharedPreferences(); boolean isRemindersEnabled = preferences.getBoolean(KEY_REMINDERS, true); - if (!isRemindersEnabled) { + if(! isRemindersEnabled) + { getActivity().sendBroadcast(new Intent(TaskAlertReceiver.ALERT_DELETE_ALL)); } return true; @@ -331,8 +349,10 @@ public boolean onPreferenceTreeClick(Preference preference) { } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_CODE_SHARING_OPTIONS && resultCode == Activity.RESULT_OK) { + public void onActivityResult(int requestCode, int resultCode, Intent data) + { + if(requestCode == REQUEST_CODE_SHARING_OPTIONS && resultCode == Activity.RESULT_OK) + { TaskResult taskResult = (TaskResult) data.getSerializableExtra(ViewTaskActivity.EXTRA_TASK_RESULT); String result = (String) taskResult.getStepResult(ConsentTask.ID_SHARING).getResult(); @@ -342,7 +362,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { }).compose(ObservableUtils.applyDefault()).subscribe(o -> { sharingScope.setSummary(formatSharingOption(result)); }); - } else if (requestCode == REQUEST_CODE_CHANGE_PASSCODE && resultCode == Activity.RESULT_OK) { + } + else if(requestCode == REQUEST_CODE_CHANGE_PASSCODE && resultCode == Activity.RESULT_OK) + { TaskResult result = (TaskResult) data.getSerializableExtra(ViewTaskActivity.EXTRA_TASK_RESULT); String oldPinCode = (String) result.getStepResult(PASSCODE) @@ -357,100 +379,122 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { return null; }).compose(ObservableUtils.applyDefault()).subscribe(o -> { Toast.makeText(getActivity(), - R.string.rss_local_result_passcode_changed, - Toast.LENGTH_SHORT).show(); + R.string.rsb_local_result_passcode_changed, + Toast.LENGTH_SHORT).show(); progress.setVisibility(View.GONE); }, e -> { Toast.makeText(getActivity(), - R.string.rss_local_error_passcode_failed, - Toast.LENGTH_SHORT).show(); + R.string.rsb_local_error_passcode_failed, + Toast.LENGTH_SHORT).show(); progress.setVisibility(View.GONE); }); - } else { + } + else + { super.onActivityResult(requestCode, resultCode, data); } } @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - switch (key) { + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) + { + switch(key) + { case KEY_AUTO_LOCK_ENABLED: case KEY_AUTO_LOCK_TIME: - long autoLockTime = AppPrefs.getInstance(getContext()).getAutoLockTime(); + long autoLockTime = AppPrefs.getInstance().getAutoLockTime(); StorageAccess.getInstance().getPinCodeConfig().setPinAutoLockTime(autoLockTime); break; } } - public String formatSharingOption(String option) { - if (option.equals("sponsors_and_partners")) { + public String formatSharingOption(String option) + { + if(option.equals("sponsors_and_partners")) + { String investigatorLongDesc = data.getDocumentProperties() .getInvestigatorLongDescription(); return getString(R.string.rsb_consent_share_widely, investigatorLongDesc); - } else if (option.equals("all_qualified_researchers")) { + } + else if(option.equals("all_qualified_researchers")) + { String investigatorShortDesc = data.getDocumentProperties() .getInvestigatorShortDescription(); return getString(R.string.rsb_consent_share_only, investigatorShortDesc); - } else if (option.equals("no_sharing")) { + } + else if(option.equals("no_sharing")) + { return getString(R.string.rsb_consent_share_no); - } else { + } + else + { // If you want to add another sharing option, feel free, you just need to override this // method in your SettingsFragment throw new RuntimeException("Sharing option " + option + " not supported"); } } - public String getVersionString() { + public String getVersionString() + { int versionCode; String versionName; PackageManager manager = getActivity().getPackageManager(); - try { + try + { PackageInfo info = manager.getPackageInfo(getActivity().getPackageName(), 0); versionCode = info.versionCode; versionName = info.versionName; - } catch (PackageManager.NameNotFoundException e) { + } + catch(PackageManager.NameNotFoundException e) + { LogExt.e(getClass(), "Could not find package version info"); versionCode = 0; - versionName = getString(R.string.rss_settings_version_unknown); + versionName = getString(R.string.rsb_settings_version_unknown); } - return getString(R.string.rss_settings_version, versionName, versionCode); + return getString(R.string.rsb_settings_version, versionName, versionCode); } - public void showPrivacyPolicy() { + public void showPrivacyPolicy() + { String path = ResourceManager.getInstance().getPrivacyPolicy().getAbsolutePath(); Intent intent = ViewWebDocumentActivity.newIntentForPath(getContext(), - getString(R.string.rss_settings_privacy_policy), - path); + getString(R.string.rsb_settings_privacy_policy), + path); startActivity(intent); } - public void showSoftwareNotices() { + public void showSoftwareNotices() + { String path = ResourceManager.getInstance().getSoftwareNotices().getAbsolutePath(); Intent intent = ViewWebDocumentActivity.newIntentForPath(getContext(), - getString(R.string.rss_settings_general_software_notices), - path); + getString(R.string.rsb_settings_general_software_notices), + path); startActivity(intent); } @Override - public void onDataReady() { + public void onDataReady() + { LogExt.i(getClass(), "onDataReady()"); - if (!isInitializedForConsent) { + if(! isInitializedForConsent) + { initPreferenceForConsent(); } } @Override - public void onDataFailed() { + public void onDataFailed() + { // Ignore } @Override - public void onDataAuth() { + public void onDataAuth() + { // Ignore, handled in activity } -} +} \ No newline at end of file diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/fragment/ShareFragment.java b/backbone/src/main/java/org/researchstack/backbone/ui/fragment/ShareFragment.java new file mode 100644 index 000000000..961e7797d --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/fragment/ShareFragment.java @@ -0,0 +1,268 @@ +package org.researchstack.backbone.ui.fragment; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.researchstack.backbone.utils.ResUtils; +import org.researchstack.backbone.utils.TextUtils; +import org.researchstack.backbone.utils.ThemeUtils; +import org.researchstack.backbone.R; +import java.util.ArrayList; +import java.util.List; + +@Deprecated // use ShareTheAppStep which loads ShareTheAppStepLayout instead +public class ShareFragment extends Fragment +{ + protected static final String FACEBOOK_SHARE_URL = "https://www.facebook.com/sharer/sharer.php?u="; + protected static final String TWITTER_SHARE_URL = "https://twitter.com/intent/tweet?source=webclient&text="; + protected static final String SMS_MIME_TYPE = "vnd.android-dir/mms-sms"; + protected static final String TEXT_MIME_TYPE = "text/plain"; + protected static final String SMS_BODY_KEY = "sms_body"; + protected static final String MAILTO_SCHEME = "mailto"; + + public static enum Type + { + TWITTER, + FACEBOOK, + SMS, + EMAIL + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + return inflater.inflate(R.layout.rsb_fragment_share, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + + ImageView logoView = (ImageView) view.findViewById(R.id.share_logo_view); + // look for a logo to show, otherwise hide it + int logoId = ResUtils.getDrawableResourceId(getActivity(), "logo_disease"); + if(logoId > 0) + { + logoView.setImageResource(logoId); + logoView.setVisibility(View.VISIBLE); + } + + RecyclerView recyclerView = (RecyclerView) view.findViewById(org.researchstack.backbone.R.id.share_recycler_view); + recyclerView.setAdapter(new ShareFragment.ShareAdapter(getContext(), loadItems())); + recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); + + } + + /** + * Return a list of Share Type Item objects. + * + * @return list of share items to be displayed in adapter + */ + protected List loadItems() { + List items = new ArrayList<>(); + + ShareItem twitter = new ShareItem(ResUtils.TWITTER_ICON, getString(R.string.rsb_share_twitter), Type.TWITTER); + items.add(twitter); + + ShareItem facebook = new ShareItem(ResUtils.FACEBOOK_ICON, getString(R.string.rsb_share_facebook), Type.FACEBOOK); + items.add(facebook); + + ShareItem sms = new ShareItem(ResUtils.SMS_ICON, getString(R.string.rsb_share_sms), Type.SMS); + items.add(sms); + + ShareItem email = new ShareItem(ResUtils.EMAIL_ICON, getString(R.string.rsb_share_email), Type.EMAIL); + items.add(email); + + return items; + } + + private class ShareItem { + public String icon; + public String text; + public Type type; + + public ShareItem(String i, String t, Type ty) { + icon = i; + text = t; + type = ty; + } + } + + public class ShareAdapter extends RecyclerView.Adapter + { + + private Context context; + private List items; + private LayoutInflater inflater; + + public ShareAdapter(Context ctx, List itemList) + { + super(); + context = ctx; + items = itemList; + this.inflater = LayoutInflater.from(context); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) + { + View view = inflater.inflate(R.layout.rsb_item_row_share, parent, false); + return new ShareFragment.ShareAdapter.ViewHolder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder hldr, int position) + { + + ShareFragment.ShareAdapter.ViewHolder holder = (ShareFragment.ShareAdapter.ViewHolder) hldr; + ShareItem item = items.get(position); + + holder.title.setText(item.text); + + int resId = ResUtils.getDrawableResourceId(context, item.icon); + holder.icon.setImageResource(resId); + + // use accent color for the icons + int colorId = ThemeUtils.getAccentColor(context); + holder.icon.setColorFilter(colorId, PorterDuff.Mode.SRC_IN); + + + + holder.itemView.setOnClickListener(v -> { + Intent intent = null; + String message = context.getString(R.string.rsb_share_the_app_message); + switch(item.type) + { + case TWITTER: + intent = getShareTwitterIntent(message); + break; + case FACEBOOK: + intent = getShareFacebookIntent(message); + break; + case SMS: + intent = getShareSmsIntent(message); + break; + case EMAIL: + intent = getShareEmailIntent(message); + break; + } + + v.getContext().startActivity(intent); + + }); + + } + + @Override + public int getItemCount() + { + return items.size(); + } + + public class ViewHolder extends RecyclerView.ViewHolder + { + TextView title; + ImageView icon; + + public ViewHolder(View itemView) + { + super(itemView); + title = (TextView) itemView.findViewById(R.id.share_item_title); + icon = (ImageView) itemView.findViewById(R.id.share_item_icon); + } + } + } + + /** + * Return an Intent for sharing by email. + * + * @param message The message to share. + * @return The share intent. + */ + protected Intent getShareEmailIntent(String message) { + Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts( + MAILTO_SCHEME, "", null)); + intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.rsb_share_email_subject)); + intent.putExtra(Intent.EXTRA_TEXT, message); + return intent; + } + + /** + * Return an intent for sharing by SMS. + * + * @param message The message to share. + * @return The share intent. + */ + protected Intent getShareSmsIntent(String message) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setType(SMS_MIME_TYPE); + intent.putExtra(SMS_BODY_KEY,message); + return intent; + } + + /** + * Return an Intent for sharing by Twitter. + * + * @param message The message to share. + * @return The share intent. + */ + protected Intent getShareTwitterIntent(String message) { + String url = TWITTER_SHARE_URL + TextUtils.urlEncode(message); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + return intent; + } + + /** + * Return an intent for sharing by Facebook. + * + * @param message The message to share. + * @return The share intent. + */ + protected Intent getShareFacebookIntent(String message) { + String urlToShare = getString(R.string.rsb_share_app_url); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(TEXT_MIME_TYPE); + intent.putExtra(Intent.EXTRA_TEXT, urlToShare); + + // See if official Facebook app is found + boolean facebookAppFound = false; + List matches = getActivity().getPackageManager().queryIntentActivities(intent, 0); + for (ResolveInfo info : matches) + { + String facebookKey = getString(R.string.rsb_share_facebook_key); + if (info.activityInfo.packageName.toLowerCase().contains(facebookKey) || + info.activityInfo.name.toLowerCase().contains(facebookKey)) + { + intent.setPackage(info.activityInfo.packageName); + facebookAppFound = true; + break; + } + } + + // As fallback, launch sharer.php in a browser + if (!facebookAppFound) + { + String sharerUrl = FACEBOOK_SHARE_URL + urlToShare; + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(sharerUrl)); + } + + return intent; + } + +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/graph/BarChartCard.java b/backbone/src/main/java/org/researchstack/backbone/ui/graph/BarChartCard.java index d683ba71b..d7a474779 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/graph/BarChartCard.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/graph/BarChartCard.java @@ -4,9 +4,9 @@ import android.content.res.TypedArray; import android.graphics.Typeface; import android.graphics.drawable.Drawable; -import android.support.annotation.StringRes; -import android.support.v4.graphics.drawable.DrawableCompat; -import android.support.v7.widget.CardView; +import androidx.annotation.StringRes; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.cardview.widget.CardView; import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/graph/EmptyView.java b/backbone/src/main/java/org/researchstack/backbone/ui/graph/EmptyView.java index 12c6c994d..b5ac6702a 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/graph/EmptyView.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/graph/EmptyView.java @@ -4,8 +4,8 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.support.annotation.Nullable; -import android.support.v4.graphics.drawable.DrawableCompat; +import androidx.annotation.Nullable; +import androidx.core.graphics.drawable.DrawableCompat; import android.util.AttributeSet; import android.view.LayoutInflater; import android.widget.FrameLayout; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/graph/LineChartCard.java b/backbone/src/main/java/org/researchstack/backbone/ui/graph/LineChartCard.java index edf0179f8..6e0341d3d 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/graph/LineChartCard.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/graph/LineChartCard.java @@ -4,9 +4,9 @@ import android.content.res.TypedArray; import android.graphics.Typeface; import android.graphics.drawable.Drawable; -import android.support.annotation.StringRes; -import android.support.v4.graphics.drawable.DrawableCompat; -import android.support.v7.widget.CardView; +import androidx.annotation.StringRes; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.cardview.widget.CardView; import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/graph/PieChartCard.java b/backbone/src/main/java/org/researchstack/backbone/ui/graph/PieChartCard.java index eba851a2e..78dad67a2 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/graph/PieChartCard.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/graph/PieChartCard.java @@ -3,8 +3,8 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Typeface; -import android.support.annotation.StringRes; -import android.support.v7.widget.CardView; +import androidx.annotation.StringRes; +import androidx.cardview.widget.CardView; import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/graph/ProgressChartCard.java b/backbone/src/main/java/org/researchstack/backbone/ui/graph/ProgressChartCard.java index b94d906f4..f888747d0 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/graph/ProgressChartCard.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/graph/ProgressChartCard.java @@ -4,9 +4,9 @@ import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Typeface; -import android.support.annotation.StringRes; -import android.support.design.widget.TabLayout; -import android.support.v7.widget.CardView; +import androidx.annotation.StringRes; +import com.google.android.material.tabs.TabLayout; +import androidx.cardview.widget.CardView; import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; diff --git a/skin/src/main/java/org/researchstack/skin/ui/layout/ConsentQuizEvaluationStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/layout/ConsentQuizEvaluationStepLayout.java similarity index 88% rename from skin/src/main/java/org/researchstack/skin/ui/layout/ConsentQuizEvaluationStepLayout.java rename to backbone/src/main/java/org/researchstack/backbone/ui/layout/ConsentQuizEvaluationStepLayout.java index f101095b4..7a4e95b58 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/layout/ConsentQuizEvaluationStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/layout/ConsentQuizEvaluationStepLayout.java @@ -1,4 +1,4 @@ -package org.researchstack.skin.ui.layout; +package org.researchstack.backbone.ui.layout; import android.content.Context; import android.util.AttributeSet; @@ -13,9 +13,10 @@ import org.researchstack.backbone.ui.views.FixedSubmitBarLayout; import org.researchstack.backbone.ui.views.SubmitBar; import org.researchstack.backbone.utils.ResUtils; -import org.researchstack.skin.R; -import org.researchstack.skin.step.ConsentQuizEvaluationStep; +import org.researchstack.backbone.R; +import org.researchstack.backbone.step.ConsentQuizEvaluationStep; +@Deprecated // use FormStepLayout instead public class ConsentQuizEvaluationStepLayout extends FixedSubmitBarLayout implements StepLayout { private ConsentQuizEvaluationStep step; @@ -44,13 +45,13 @@ public void initialize(Step step, StepResult result) { @Override public int getContentResourceId() { - return R.layout.rss_layout_consent_evaluation; + return R.layout.rsb_layout_consent_evaluation; } private void initializeStep() { - ImageView image = (ImageView) findViewById(R.id.rss_quiz_eval_image); - TextView title = (TextView) findViewById(R.id.rss_quiz_eval_title); - TextView summary = (TextView) findViewById(R.id.rss_quiz_eval_summary); + ImageView image = (ImageView) findViewById(R.id.rsb_quiz_eval_image); + TextView title = (TextView) findViewById(R.id.rsb_quiz_eval_title); + TextView summary = (TextView) findViewById(R.id.rsb_quiz_eval_summary); SubmitBar submitBar = (SubmitBar) findViewById(R.id.rsb_submit_bar); submitBar.getNegativeActionView().setVisibility(View.GONE); diff --git a/skin/src/main/java/org/researchstack/skin/ui/layout/ConsentQuizQuestionStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/layout/ConsentQuizQuestionStepLayout.java similarity index 66% rename from skin/src/main/java/org/researchstack/skin/ui/layout/ConsentQuizQuestionStepLayout.java rename to backbone/src/main/java/org/researchstack/backbone/ui/layout/ConsentQuizQuestionStepLayout.java index 48d3831b0..4189c4fae 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/layout/ConsentQuizQuestionStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/layout/ConsentQuizQuestionStepLayout.java @@ -1,12 +1,10 @@ -package org.researchstack.skin.ui.layout; - +package org.researchstack.backbone.ui.layout; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.support.v4.graphics.drawable.DrawableCompat; -import android.support.v7.widget.AppCompatRadioButton; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.appcompat.widget.AppCompatRadioButton; import android.text.Html; import android.util.AttributeSet; import android.view.LayoutInflater; @@ -19,56 +17,67 @@ import android.widget.Toast; import org.researchstack.backbone.model.Choice; +import org.researchstack.backbone.model.ConsentQuestionType; import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.step.Step; import org.researchstack.backbone.ui.callbacks.StepCallbacks; import org.researchstack.backbone.ui.step.layout.StepLayout; import org.researchstack.backbone.ui.views.SubmitBar; -import org.researchstack.skin.R; -import org.researchstack.skin.model.ConsentQuizModel; -import org.researchstack.skin.step.ConsentQuizQuestionStep; +import org.researchstack.backbone.utils.LogExt; +import org.researchstack.backbone.utils.ConsentQuizQuestionUtils; +import org.researchstack.backbone.R; +import org.researchstack.backbone.model.ConsentQuizModel; +import org.researchstack.backbone.step.ConsentQuizQuestionStep; -import java.util.ArrayList; import java.util.List; -public class ConsentQuizQuestionStepLayout extends LinearLayout implements StepLayout { +@Deprecated // Use FormStepLayout instead +public class ConsentQuizQuestionStepLayout extends LinearLayout implements StepLayout +{ + static final String LOG_TAG = ConsentQuizQuestionStepLayout.class.getCanonicalName(); + private ConsentQuizQuestionStep step; - private StepResult result; - private StepCallbacks callbacks; - - private TextView resultSummary; - private TextView resultTitle; - private RadioGroup radioGroup; - private SubmitBar submitBar; - private View radioItemBackground; + private StepResult result; + private StepCallbacks callbacks; + + private TextView resultSummary; + private TextView resultTitle; + private RadioGroup radioGroup; + private SubmitBar submitBar; + private View radioItemBackground; private Choice expectedChoice; - public ConsentQuizQuestionStepLayout(Context context) { + public ConsentQuizQuestionStepLayout(Context context) + { super(context); } - public ConsentQuizQuestionStepLayout(Context context, AttributeSet attrs) { + public ConsentQuizQuestionStepLayout(Context context, AttributeSet attrs) + { super(context, attrs); } - public ConsentQuizQuestionStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + public ConsentQuizQuestionStepLayout(Context context, AttributeSet attrs, int defStyleAttr) + { super(context, attrs, defStyleAttr); } @Override - public void initialize(Step step, StepResult result) { + public void initialize(Step step, StepResult result) + { this.step = (ConsentQuizQuestionStep) step; this.result = result == null ? new StepResult<>(step) : result; initializeStep(); } - public void initializeStep() { + public void initializeStep() + { setOrientation(VERTICAL); ConsentQuizModel.QuizQuestion question = step.getQuestion(); LayoutInflater inflater = LayoutInflater.from(getContext()); - inflater.inflate(R.layout.rss_layout_quiz_question, this, true); + inflater.inflate(R.layout.rsb_layout_quiz_question, this, true); ((TextView) findViewById(R.id.title)).setText(step.getTitle()); @@ -82,7 +91,8 @@ public void initializeStep() { radioItemBackground = findViewById(R.id.quiz_result_item_background); - if (question.getType().equals("instruction")) { + if(question.getType() == ConsentQuestionType.INSTRUCTION) + { TextView instructionText = (TextView) findViewById(R.id.instruction_text); instructionText.setText(question.getText()); instructionText.setVisibility(VISIBLE); @@ -90,51 +100,47 @@ public void initializeStep() { // instruction steps don't need submit, also always count as correct answer submitBar.setPositiveTitle(R.string.rsb_next); submitBar.setPositiveAction(v -> onNext(true)); - } else { + } + else + { submitBar.setPositiveTitle(R.string.rsb_submit); submitBar.setPositiveAction(v -> onSubmit()); - - for (Choice choice : getChoices(question)) { - AppCompatRadioButton button = (AppCompatRadioButton) inflater.inflate(R.layout.rss_item_radio_quiz, - radioGroup, - false); - button.setText(choice.getText()); - button.setTag(choice); - radioGroup.addView(button); - - if (question.getExpectedAnswer().equals(choice.getValue())) { - expectedChoice = choice; - } - } } - } - @NonNull - private List> getChoices(ConsentQuizModel.QuizQuestion question) { - List> choices = new ArrayList<>(); - - if (question.getType().equals("boolean")) { - // json expected answer is a string of either "true" or "false" - choices.add(new Choice<>(getContext().getString(R.string.rss_btn_true), "true")); - choices.add(new Choice<>(getContext().getString(R.string.rss_btn_false), "false")); - } else if (question.getType().equals("singleChoiceText")) { - // json expected answer is a string of the index ("0" for the first choice) - List textChoices = question.getTextChoices(); - for (int i = 0; i < textChoices.size(); i++) { - choices.add(new Choice<>(textChoices.get(i), String.valueOf(i))); + List choices = ConsentQuizQuestionUtils.createChoices(getContext(), question); + for(Choice choice : choices) + { + AppCompatRadioButton button = (AppCompatRadioButton) inflater.inflate(R.layout.rsb_item_radio_quiz, + radioGroup, + false); + button.setText(choice.getText()); + button.setTag(choice); + radioGroup.addView(button); + + if(question.getExpectedAnswer().equals(String.valueOf(choice.getValue()))) + { + expectedChoice = choice; } } - return choices; } - public void onSubmit() { - if (isAnswerValid()) { + public void onSubmit() + { + if(isAnswerValid()) + { int buttonId = radioGroup.getCheckedRadioButtonId(); RadioButton checkedRadioButton = (RadioButton) radioGroup.findViewById(buttonId); Choice selectedChoice = (Choice) checkedRadioButton.getTag(); + + if (expectedChoice == null) { + LogExt.e(LOG_TAG, "Check JSON and sure your expectedChoice is equal to the value of your choices"); + } + boolean answerCorrect = expectedChoice.equals(selectedChoice); - if (resultTitle.getVisibility() == View.GONE) { + if(resultTitle.getVisibility() == View.GONE) + { + // TODO: remove hardcoded colors int resultTextColor = answerCorrect ? 0xFF67bd61 : 0xFFc96677; int radioBackground = Color.argb(51, //20% alpha @@ -143,7 +149,8 @@ public void onSubmit() { Color.blue(resultTextColor)); // Disable buttons to prevent further action - for (int i = 0; i < radioGroup.getChildCount(); i++) { + for(int i = 0; i < radioGroup.getChildCount(); i++) + { radioGroup.getChildAt(i).setEnabled(false); } @@ -172,8 +179,8 @@ public void onSubmit() { //Set our Result-title String resultTitle = answerCorrect - ? getContext().getString(R.string.rss_quiz_evaluation_correct) - : getContext().getString(R.string.rss_quiz_evaluation_incorrect); + ? getContext().getString(R.string.rsb_quiz_evaluation_correct) + : getContext().getString(R.string.rsb_quiz_evaluation_incorrect); this.resultTitle.setVisibility(View.VISIBLE); this.resultTitle.setText(resultTitle); @@ -182,11 +189,14 @@ public void onSubmit() { //Build and set our result-summary String explanation; - if (answerCorrect) { - explanation = getContext().getString(R.string.rss_quiz_question_explanation_correct, + if(answerCorrect) + { + explanation = getContext().getString(R.string.rsb_quiz_question_explanation_correct, step.getQuestion().getPositiveFeedback()); - } else { - explanation = getContext().getString(R.string.rss_quiz_question_explanation_incorrect, + } + else + { + explanation = getContext().getString(R.string.rsb_quiz_question_explanation_incorrect, expectedChoice.getText(), step.getQuestion().getNegativeFeedback()); } @@ -196,27 +206,33 @@ public void onSubmit() { resultSummary.setVisibility(View.VISIBLE); setSubmitBarNext(); - } else { + } + else + { onNext(answerCorrect); } } } - private void onNext(boolean answerCorrect) { + private void onNext(boolean answerCorrect) + { // Save the result and go to the next question result.setResult(answerCorrect); callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, step, result); } - private void setSubmitBarNext() { + private void setSubmitBarNext() + { // Change the submit bar positive-title to "next" submitBar.setPositiveTitle(R.string.rsb_next); } - public boolean isAnswerValid() { - if (radioGroup.getCheckedRadioButtonId() == -1) { - Toast.makeText(getContext(), R.string.rss_error_select_answer, Toast.LENGTH_SHORT).show(); + public boolean isAnswerValid() + { + if(radioGroup.getCheckedRadioButtonId() == - 1) + { + Toast.makeText(getContext(), R.string.rsb_error_select_answer, Toast.LENGTH_SHORT).show(); return false; } @@ -224,18 +240,21 @@ public boolean isAnswerValid() { } @Override - public View getLayout() { + public View getLayout() + { return this; } @Override - public boolean isBackEventConsumed() { + public boolean isBackEventConsumed() + { callbacks.onSaveStep(StepCallbacks.ACTION_PREV, step, result); return false; } @Override - public void setCallbacks(StepCallbacks callbacks) { + public void setCallbacks(StepCallbacks callbacks) + { this.callbacks = callbacks; } diff --git a/skin/src/main/java/org/researchstack/skin/ui/layout/PermissionStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/layout/PermissionStepLayout.java similarity index 83% rename from skin/src/main/java/org/researchstack/skin/ui/layout/PermissionStepLayout.java rename to backbone/src/main/java/org/researchstack/backbone/ui/layout/PermissionStepLayout.java index 5a50e09ab..ffd031d7d 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/layout/PermissionStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/layout/PermissionStepLayout.java @@ -1,9 +1,9 @@ -package org.researchstack.skin.ui.layout; +package org.researchstack.backbone.ui.layout; import android.content.Context; import android.graphics.drawable.Drawable; -import android.support.v4.content.ContextCompat; -import android.support.v4.graphics.drawable.DrawableCompat; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -14,6 +14,7 @@ import com.jakewharton.rxbinding.view.RxView; +import org.researchstack.backbone.PermissionRequestManager; import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.step.Step; import org.researchstack.backbone.ui.callbacks.ActivityCallback; @@ -22,11 +23,11 @@ import org.researchstack.backbone.ui.step.layout.StepPermissionRequest; import org.researchstack.backbone.ui.views.SubmitBar; import org.researchstack.backbone.utils.ThemeUtils; -import org.researchstack.skin.PermissionRequestManager; -import org.researchstack.skin.R; +import org.researchstack.backbone.R; import java.util.List; +@Deprecated // No longer needed with new OnboardingManager public class PermissionStepLayout extends LinearLayout implements StepLayout, StepPermissionRequest { private Step step; private StepResult result; @@ -77,10 +78,10 @@ public void initializeStep() { LayoutInflater inflater = LayoutInflater.from(getContext()); // Inflate step UI - inflater.inflate(R.layout.rss_layout_permission, this, true); + inflater.inflate(R.layout.rsb_layout_permission, this, true); // Add Sub-items to our ScrollView - LinearLayout permissionContainer = (LinearLayout) findViewById(R.id.container_permission_items); + LinearLayout permissionContainer = (LinearLayout) findViewById(R.id.rsb_container_permission_items); List items = PermissionRequestManager.getInstance() .getPermissionRequests(); @@ -88,7 +89,7 @@ public void initializeStep() { for (PermissionRequestManager.PermissionRequest item : items) { boolean isGranted = PermissionRequestManager.getInstance().hasPermission(getContext(), item.getId()); - View child = inflater.inflate(R.layout.rss_item_permission_content, + View child = inflater.inflate(R.layout.rsb_item_permission_content, permissionContainer, false); @@ -99,19 +100,19 @@ public void initializeStep() { Drawable icon = ContextCompat.getDrawable(getContext(), item.getIcon()); icon = DrawableCompat.wrap(icon); DrawableCompat.setTint(icon, ThemeUtils.getAccentColor(getContext())); - ((ImageView) child.findViewById(R.id.permission_icon)).setImageDrawable(icon); + ((ImageView) child.findViewById(R.id.rsb_permission_icon)).setImageDrawable(icon); // Set title - ((TextView) child.findViewById(R.id.permission_title)).setText(item.getTitle()); + ((TextView) child.findViewById(R.id.rsb_permission_title)).setText(item.getTitle()); // Set details - ((TextView) child.findViewById(R.id.permission_details)).setText(item.getText()); + ((TextView) child.findViewById(R.id.rsb_permission_details)).setText(item.getText()); // Text action - TextView action = (TextView) child.findViewById(R.id.permission_button); + TextView action = (TextView) child.findViewById(R.id.rsb_permission_button); action.setText(isGranted - ? R.string.rss_granted - : item.isBlockingPermission() ? R.string.rss_allow : R.string.rss_optional); + ? R.string.rsb_granted + : item.isBlockingPermission() ? R.string.rsb_allow : R.string.rsb_optional); RxView.clicks(action).subscribe(o -> { permissionCallback.onRequestPermission(item.getId()); }); @@ -145,10 +146,10 @@ private void updatePermissionItems() { View parent = findViewWithTag(item.getId()); - TextView action = (TextView) parent.findViewById(R.id.permission_button); + TextView action = (TextView) parent.findViewById(R.id.rsb_permission_button); action.setText(isGranted - ? R.string.rss_granted - : item.isBlockingPermission() ? R.string.rss_allow : R.string.rss_optional); + ? R.string.rsb_granted + : item.isBlockingPermission() ? R.string.rsb_allow : R.string.rsb_optional); action.setEnabled(!isGranted); } } @@ -169,7 +170,7 @@ public boolean isAnswerValid() { if (!isGranted && item.isBlockingPermission()) { String permissionName = getResources().getString(item.getTitle()); String formattedError = getResources().getString( - R.string.rss_permission_continue_invalid, permissionName.toLowerCase()); + R.string.rsb_permission_continue_invalid, permissionName.toLowerCase()); Toast.makeText(getContext(), formattedError, Toast.LENGTH_SHORT).show(); return false; } diff --git a/skin/src/main/java/org/researchstack/skin/ui/layout/SignInStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/layout/SignInStepLayout.java similarity index 92% rename from skin/src/main/java/org/researchstack/skin/ui/layout/SignInStepLayout.java rename to backbone/src/main/java/org/researchstack/backbone/ui/layout/SignInStepLayout.java index d76dcae2d..f1d8a7818 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/layout/SignInStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/layout/SignInStepLayout.java @@ -1,7 +1,7 @@ -package org.researchstack.skin.ui.layout; +package org.researchstack.backbone.ui.layout; import android.content.Context; -import android.support.v7.widget.AppCompatEditText; +import androidx.appcompat.widget.AppCompatEditText; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -20,11 +20,12 @@ import org.researchstack.backbone.ui.views.SubmitBar; import org.researchstack.backbone.utils.ObservableUtils; import org.researchstack.backbone.utils.TextUtils; -import org.researchstack.skin.DataProvider; -import org.researchstack.skin.R; -import org.researchstack.skin.task.SignInTask; -import org.researchstack.skin.ui.adapter.TextWatcherAdapter; +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.R; +import org.researchstack.backbone.task.SignInTask; +import org.researchstack.backbone.ui.adapter.TextWatcherAdapter; +@Deprecated // No longer needed with new OnboardingManager public class SignInStepLayout extends RelativeLayout implements StepLayout { private View progress; private AppCompatEditText email; @@ -51,7 +52,7 @@ public void initialize(Step step, StepResult result) { this.step = step; this.result = result == null ? new StepResult<>(step) : result; - View layout = LayoutInflater.from(getContext()).inflate(R.layout.rss_layout_sign_in, this, true); + View layout = LayoutInflater.from(getContext()).inflate(R.layout.rsb_layout_sign_in, this, true); progress = layout.findViewById(R.id.progress); @@ -86,7 +87,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { forgotPassword = (TextView) layout.findViewById(R.id.forgot_password); RxView.clicks(forgotPassword).subscribe(v -> { if (!isEmailValid()) { - Toast.makeText(getContext(), R.string.rss_error_invalid_email, Toast.LENGTH_SHORT) + Toast.makeText(getContext(), R.string.rsb_error_invalid_email, Toast.LENGTH_SHORT) .show(); return; } @@ -150,11 +151,11 @@ private void handleError(String message, String username, String password) { public boolean isAnswerValid() { if (!isEmailValid()) { - email.setError(getResources().getString(R.string.rss_error_invalid_email)); + email.setError(getResources().getString(R.string.rsb_error_invalid_email)); } if (!isPasswordValid()) { - password.setError(getResources().getString(R.string.rss_error_invalid_password)); + password.setError(getResources().getString(R.string.rsb_error_invalid_password)); } return TextUtils.isEmpty(email.getError()) && TextUtils.isEmpty(password.getError()); diff --git a/skin/src/main/java/org/researchstack/skin/ui/layout/SignUpEligibleStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/layout/SignUpEligibleStepLayout.java similarity index 82% rename from skin/src/main/java/org/researchstack/skin/ui/layout/SignUpEligibleStepLayout.java rename to backbone/src/main/java/org/researchstack/backbone/ui/layout/SignUpEligibleStepLayout.java index 80dd361df..d13808167 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/layout/SignUpEligibleStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/layout/SignUpEligibleStepLayout.java @@ -1,10 +1,11 @@ -package org.researchstack.skin.ui.layout; +package org.researchstack.backbone.ui.layout; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.RelativeLayout; +import android.widget.TextView; import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.step.Step; @@ -12,8 +13,9 @@ import org.researchstack.backbone.ui.callbacks.StepCallbacks; import org.researchstack.backbone.ui.step.layout.StepLayout; import org.researchstack.backbone.ui.views.SubmitBar; -import org.researchstack.skin.R; +import org.researchstack.backbone.R; +@Deprecated // No longer needed with new OnboardingManager public class SignUpEligibleStepLayout extends RelativeLayout implements StepLayout { public static final int CONSENT_REQUEST = 1001; @@ -48,11 +50,17 @@ public void initialize(Step step, StepResult result) { } private void initializeStep() { - LayoutInflater.from(getContext()).inflate(R.layout.rss_layout_eligible, this, true); + LayoutInflater.from(getContext()).inflate(R.layout.rsb_layout_eligible, this, true); SubmitBar submitBar = (SubmitBar) findViewById(R.id.submit_bar); submitBar.setPositiveAction((v) -> startConsentActivity()); submitBar.getNegativeActionView().setVisibility(GONE); + + TextView text = (TextView) findViewById(R.id.eligible_text); + TextView detailText = (TextView) findViewById(R.id.eligible_desc); + + text.setText(step.getTitle()); + detailText.setText(step.getText()); } private void startConsentActivity() { diff --git a/skin/src/main/java/org/researchstack/skin/ui/layout/SignUpIneligibleStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/layout/SignUpIneligibleStepLayout.java similarity index 73% rename from skin/src/main/java/org/researchstack/skin/ui/layout/SignUpIneligibleStepLayout.java rename to backbone/src/main/java/org/researchstack/backbone/ui/layout/SignUpIneligibleStepLayout.java index 0abb6bc18..46e071f43 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/layout/SignUpIneligibleStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/layout/SignUpIneligibleStepLayout.java @@ -1,17 +1,19 @@ -package org.researchstack.skin.ui.layout; +package org.researchstack.backbone.ui.layout; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; +import android.widget.TextView; import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.step.Step; import org.researchstack.backbone.ui.callbacks.StepCallbacks; import org.researchstack.backbone.ui.step.layout.StepLayout; -import org.researchstack.skin.R; +import org.researchstack.backbone.R; +@Deprecated // use OnboardingManager.getInstance().launchOnboarding(OnboardingTaskType.REGISTRATION, this); public class SignUpIneligibleStepLayout extends LinearLayout implements StepLayout { private StepCallbacks callbacks; private Step step; @@ -36,7 +38,13 @@ public void initialize(Step step, StepResult result) { } private void initializeStep() { - LayoutInflater.from(getContext()).inflate(R.layout.rss_layout_ineligible, this, true); + LayoutInflater.from(getContext()).inflate(R.layout.rsb_layout_ineligible, this, true); + + TextView text = (TextView) findViewById(R.id.ineligible_text); + TextView detailText = (TextView) findViewById(R.id.ineligible_detail); + + text.setText(step.getTitle()); + detailText.setText(step.getText()); } @Override diff --git a/skin/src/main/java/org/researchstack/skin/ui/layout/SignUpStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/layout/SignUpStepLayout.java similarity index 89% rename from skin/src/main/java/org/researchstack/skin/ui/layout/SignUpStepLayout.java rename to backbone/src/main/java/org/researchstack/backbone/ui/layout/SignUpStepLayout.java index b2e23c00a..f67f7b9f4 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/layout/SignUpStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/layout/SignUpStepLayout.java @@ -1,7 +1,7 @@ -package org.researchstack.skin.ui.layout; +package org.researchstack.backbone.ui.layout; import android.content.Context; -import android.support.v7.widget.AppCompatEditText; +import androidx.appcompat.widget.AppCompatEditText; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -17,11 +17,13 @@ import org.researchstack.backbone.ui.views.SubmitBar; import org.researchstack.backbone.utils.ObservableUtils; import org.researchstack.backbone.utils.TextUtils; -import org.researchstack.skin.DataProvider; -import org.researchstack.skin.R; -import org.researchstack.skin.task.SignUpTask; -import org.researchstack.skin.ui.adapter.TextWatcherAdapter; +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.R; +import org.researchstack.backbone.UiManager; +import org.researchstack.backbone.task.SignUpTask; +import org.researchstack.backbone.ui.adapter.TextWatcherAdapter; +@Deprecated // No longer needed with new OnboardingManager public class SignUpStepLayout extends RelativeLayout implements StepLayout { private StepCallbacks callbacks; @@ -50,7 +52,7 @@ public void initialize(Step step, StepResult result) { this.result = result == null ? new StepResult<>(step) : result; View layout = LayoutInflater.from(getContext()) - .inflate(R.layout.rss_layout_sign_up, this, true); + .inflate(R.layout.rsb_layout_sign_up, this, true); progress = layout.findViewById(R.id.progress); @@ -130,11 +132,11 @@ private void handleError(String message) { public boolean isAnswerValid() { if (!isEmailValid()) { - email.setError(getResources().getString(R.string.rss_error_invalid_email)); + email.setError(getResources().getString(R.string.rsb_error_invalid_email)); } if (!isPasswordValid()) { - password.setError(getResources().getString(R.string.rss_error_invalid_password)); + password.setError(getResources().getString(R.string.rsb_error_invalid_password)); } return TextUtils.isEmpty(email.getError()) && TextUtils.isEmpty(password.getError()); @@ -146,8 +148,7 @@ public boolean isEmailValid() { } public boolean isPasswordValid() { - CharSequence target = password.getText(); - return !TextUtils.isEmpty(target); + return UiManager.getInstance().isValidPassword(password.getText().toString()); } @Override diff --git a/skin/src/main/java/org/researchstack/skin/ui/preference/TextColorPreference.java b/backbone/src/main/java/org/researchstack/backbone/ui/preference/TextColorPreference.java similarity index 90% rename from skin/src/main/java/org/researchstack/skin/ui/preference/TextColorPreference.java rename to backbone/src/main/java/org/researchstack/backbone/ui/preference/TextColorPreference.java index 8f92609f1..1cddfea76 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/preference/TextColorPreference.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/preference/TextColorPreference.java @@ -1,14 +1,14 @@ -package org.researchstack.skin.ui.preference; +package org.researchstack.backbone.ui.preference; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceViewHolder; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.widget.TextView; -import org.researchstack.skin.R; +import org.researchstack.backbone.R; public class TextColorPreference extends Preference { private TextView titleView; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/BodyAnswer.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/BodyAnswer.java index 08c14361d..e26882957 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/BodyAnswer.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/BodyAnswer.java @@ -1,7 +1,7 @@ package org.researchstack.backbone.ui.step.body; import android.content.Context; -import android.support.annotation.StringRes; +import androidx.annotation.StringRes; import org.researchstack.backbone.R; @@ -37,7 +37,7 @@ public String getString(Context context) { if (getParams().length == 0) { return context.getString(getReason()); } else { - return context.getString(getReason(), getParams()); + return context.getString(getReason(), (Object[]) getParams()); } } } diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/DateQuestionBody.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/DateQuestionBody.java index fcdb5fd73..cb7f51149 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/DateQuestionBody.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/DateQuestionBody.java @@ -3,10 +3,12 @@ import android.app.DatePickerDialog; import android.app.TimePickerDialog; import android.content.res.Resources; -import android.support.v7.view.ContextThemeWrapper; +import android.text.InputType; +import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; @@ -82,8 +84,9 @@ public View getBodyView(int viewType, LayoutInflater inflater, ViewGroup parent) title.setVisibility(View.GONE); } - TextView textView = (TextView) view.findViewById(R.id.value); + EditText textView = view.findViewById(R.id.value); textView.setSingleLine(true); + textView.setInputType(InputType.TYPE_NULL); if (step.getPlaceholder() != null) { textView.setHint(step.getPlaceholder()); } else { diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/FormBody.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/FormBody.java index d6c85f7e5..2fecf756d 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/FormBody.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/FormBody.java @@ -1,6 +1,6 @@ package org.researchstack.backbone.ui.step.body; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/ImageChoiceBody.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/ImageChoiceBody.java new file mode 100644 index 000000000..928293c4a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/ImageChoiceBody.java @@ -0,0 +1,205 @@ +package org.researchstack.backbone.ui.step.body; + +import android.graphics.drawable.Drawable; +import androidx.core.content.ContextCompat; +import androidx.core.content.res.ResourcesCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.answerformat.ImageChoiceAnswerFormat; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.QuestionStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.utils.ResUtils; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 3/14/17. + * + * The ImageChoiceBody displays "X" number of images horizontally in a LinearLayout, + * these images act like RadioButtons, except, all of them can be deselected + * + * Also, hint text below the images changes based on which one is selected + */ + +public class ImageChoiceBody implements StepBody { + + //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* + // Constructor Fields + //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* + private QuestionStep step; + private ImageChoiceAnswerFormat answerFormat; + private StepResult result; + + //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* + // View Fields + //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* + private LinearLayout linearLayout; + private List imageViewList; + private TextView hintTextView; + + private static final float DEFAULT_UNSELECTED_ALPHA = 0.5f; + private float hintTextViewUnselectedAlpha = DEFAULT_UNSELECTED_ALPHA; + + private boolean hasSetSelectedColor; + private int selectedColorWhenSelectedImageIsNull; + + private static final int NO_SELECTION = -1; + private int selectedIndex; + + public ImageChoiceBody(Step step, StepResult result) { + this.step = (QuestionStep) step; + this.result = (result == null) ? new StepResult<>(step) : result; + } + + @Override + public View getBodyView(int viewType, LayoutInflater inflater, ViewGroup parent) { + View body = inflater.inflate(R.layout.rsb_step_body_image_choice, parent, false); + + linearLayout = (LinearLayout)body.findViewById(R.id.rsb_step_body_image_choice_layout); + hintTextView = (TextView) body.findViewById(R.id.rsb_step_body_image_choice_text_hint); + hintTextView.setText(R.string.rsb_PLACEHOLDER_IMAGE_CHOICES); + hintTextView.setAlpha(hintTextViewUnselectedAlpha); + + if (!(step.getAnswerFormat() instanceof ImageChoiceAnswerFormat)) { + throw new IllegalStateException("ImageChoiceBody must have ImageChoiceAnswerFormat as the QuestionStep AnswerFormat"); + } + + answerFormat = (ImageChoiceAnswerFormat)step.getAnswerFormat(); + + imageViewList = new ArrayList<>(); + selectedIndex = NO_SELECTION; + linearLayout.setWeightSum(answerFormat.getImageChoiceList().size()); + for (int i = 0; i < answerFormat.getImageChoiceList().size(); i++) { + ImageChoiceAnswerFormat.ImageChoice imageChoice = answerFormat.getImageChoiceList().get(i); + ImageButton imageButton = (ImageButton) + inflater.inflate(R.layout.rsb_item_image_button, linearLayout, false); + setNormalImage(imageButton, imageChoice); + LinearLayout.LayoutParams params = + new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1); + linearLayout.addView(imageButton, params); + imageViewList.add(imageButton); + setupImageViewClickListener(i, imageButton, imageChoice); + } + + // Loop through and apply the step result if it exists + if (result.getResult() != null) { + for (int i = 0; i < answerFormat.getImageChoiceList().size(); i++) { + ImageChoiceAnswerFormat.ImageChoice imageChoice = answerFormat.getImageChoiceList().get(i); + if (imageChoice.getValue().equals(result.getResult())) { + imageViewList.get(i).callOnClick(); + } + } + } + + return body; + } + + protected void setupImageViewClickListener(final int index, + final ImageButton button, + final ImageChoiceAnswerFormat.ImageChoice imageChoice) { + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (selectedIndex != NO_SELECTION) { + ImageButton oldButton = imageViewList.get(selectedIndex); + setNormalImage(oldButton, answerFormat.getImageChoiceList().get(selectedIndex)); + } + // The previous selectedIndex was selected again, so deselect it + if (selectedIndex == index) { + selectedIndex = NO_SELECTION; + setNormalImage(button, imageChoice); + hintTextView.setText(R.string.rsb_PLACEHOLDER_IMAGE_CHOICES); + hintTextView.setAlpha(hintTextViewUnselectedAlpha); + result.setResult(null); + } else { // otherwise select the new image + selectedIndex = index; + setSelectedImage(button, imageChoice); + hintTextView.setText(imageChoice.getText()); + hintTextView.setAlpha(1.0f); + result.setResult(imageChoice.getValue()); + } + } + }); + } + + protected void setNormalImage(ImageButton button, ImageChoiceAnswerFormat.ImageChoice imageChoice) { + int drawableInt = ResUtils.getDrawableResourceId(button.getContext(), imageChoice.getNormalImageRes()); + if (drawableInt != 0) { + button.setImageResource(drawableInt); + } + } + + protected void setSelectedImage(ImageButton button, ImageChoiceAnswerFormat.ImageChoice imageChoice) { + int drawableInt = ResUtils.getDrawableResourceId(button.getContext(), imageChoice.getSelectedImageRes()); + if (drawableInt != 0) { + button.setImageResource(drawableInt); + } else { + // If we do not have a selected image, we will just tint the current one to the primary color + drawableInt = ResUtils.getDrawableResourceId(button.getContext(), imageChoice.getNormalImageRes()); + if (drawableInt != 0) { + Drawable drawable = DrawableCompat.wrap(ContextCompat.getDrawable(button.getContext(), drawableInt)); + // Wrap the drawable so that future tinting calls work on pre-v21 devices. Always use the returned drawable. + drawable = DrawableCompat.wrap(drawable); + + int color; + if (hasSetSelectedColor) { + color = selectedColorWhenSelectedImageIsNull; + } else { + color = ResourcesCompat.getColor(button.getResources(), R.color.rsb_colorPrimary, null); + } + + // We can now set a tint, the "mutate()" is important as it makes sure the normal drawable remains untinted + DrawableCompat.setTint(drawable.mutate(), color); + button.setImageDrawable(drawable); + } + } + } + + @Override + public StepResult getStepResult(boolean skipped) { + if (skipped) { + result.setResult(null); + } + + return result; + } + + @Override + public BodyAnswer getBodyAnswerState() { + if (selectedIndex == NO_SELECTION) { + return BodyAnswer.INVALID; + } + + return BodyAnswer.VALID; + } + + public float getHintTextViewUnselectedAlpha() { + return hintTextViewUnselectedAlpha; + } + + public void setHintTextViewUnselectedAlpha(float hintTextViewUnselectedAlpha) { + this.hintTextViewUnselectedAlpha = hintTextViewUnselectedAlpha; + if (hintTextView != null && selectedIndex == NO_SELECTION) { + hintTextView.setAlpha(hintTextViewUnselectedAlpha); + } + } + + public int getSelectedColorWhenSelectedImageIsNull() { + return selectedColorWhenSelectedImageIsNull; + } + + public void setSelectedColorWhenSelectedImageIsNull(int selectedColorWhenSelectedImageIsNull) { + hasSetSelectedColor = true; + this.selectedColorWhenSelectedImageIsNull = selectedColorWhenSelectedImageIsNull; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/IntegerQuestionBody.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/IntegerQuestionBody.java index d06575237..796f7ca40 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/IntegerQuestionBody.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/IntegerQuestionBody.java @@ -13,6 +13,7 @@ import org.researchstack.backbone.R; import org.researchstack.backbone.answerformat.IntegerAnswerFormat; +import org.researchstack.backbone.answerformat.TextAnswerFormat; import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.step.QuestionStep; import org.researchstack.backbone.step.Step; @@ -23,15 +24,15 @@ public class IntegerQuestionBody implements StepBody { //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // Constructor Fields //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* - private QuestionStep step; - private StepResult result; - private IntegerAnswerFormat format; + protected QuestionStep step; + protected StepResult result; + protected IntegerAnswerFormat format; //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // View Fields //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* - private int viewType; - private EditText editText; + protected int viewType; + protected EditText editText; public IntegerQuestionBody(Step step, StepResult result) { this.step = (QuestionStep) step; @@ -52,6 +53,12 @@ public View getBodyView(int viewType, LayoutInflater inflater, ViewGroup parent) layoutParams.rightMargin = res.getDimensionPixelSize(R.dimen.rsb_margin_right); view.setLayoutParams(layoutParams); + if (format.getMaximumLength() > TextAnswerFormat.UNLIMITED_LENGTH) { + InputFilter.LengthFilter maxLengthFilter = new InputFilter.LengthFilter(format.getMaximumLength()); + InputFilter[] filters = ViewUtils.addFilter(editText.getFilters(), maxLengthFilter); + editText.setFilters(filters); + } + return view; } @@ -65,14 +72,14 @@ private View getViewForType(int viewType, LayoutInflater inflater, ViewGroup par } } - private View initViewDefault(LayoutInflater inflater, ViewGroup parent) { + protected View initViewDefault(LayoutInflater inflater, ViewGroup parent) { editText = (EditText) inflater.inflate(R.layout.rsb_item_edit_text, parent, false); setFilters(parent.getContext()); return editText; } - private View initViewCompact(LayoutInflater inflater, ViewGroup parent) { + protected View initViewCompact(LayoutInflater inflater, ViewGroup parent) { View formItemView = inflater.inflate(R.layout.rsb_item_edit_text_compact, parent, false); TextView title = (TextView) formItemView.findViewById(R.id.label); @@ -84,7 +91,7 @@ private View initViewCompact(LayoutInflater inflater, ViewGroup parent) { return formItemView; } - private void setFilters(Context context) { + protected void setFilters(Context context) { editText.setSingleLine(true); final int minValue = format.getMinValue(); // allow any positive int if no max value is specified diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/MultiChoiceQuestionBody.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/MultiChoiceQuestionBody.java index 2499de7aa..91a46b2f1 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/MultiChoiceQuestionBody.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/MultiChoiceQuestionBody.java @@ -1,8 +1,8 @@ package org.researchstack.backbone.ui.step.body; import android.content.res.Resources; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.AppCompatCheckBox; +import androidx.core.content.ContextCompat; +import androidx.appcompat.widget.AppCompatCheckBox; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/SingleChoiceQuestionBody.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/SingleChoiceQuestionBody.java index d7a2ced1c..a2b3d777c 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/SingleChoiceQuestionBody.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/SingleChoiceQuestionBody.java @@ -1,7 +1,7 @@ package org.researchstack.backbone.ui.step.body; import android.content.res.Resources; -import android.support.v4.content.ContextCompat; +import androidx.core.content.ContextCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -21,11 +21,11 @@ public class SingleChoiceQuestionBody implements StepBody { //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* // Constructor Fields //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* - private QuestionStep step; - private StepResult result; - private ChoiceAnswerFormat format; - private Choice[] choices; - private T currentSelected; + protected QuestionStep step; + protected StepResult result; + protected ChoiceAnswerFormat format; + protected Choice[] choices; + protected T currentSelected; public SingleChoiceQuestionBody(Step step, StepResult result) { this.step = (QuestionStep) step; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/TextQuestionBody.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/TextQuestionBody.java index ad4a83505..799830c71 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/body/TextQuestionBody.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/body/TextQuestionBody.java @@ -1,12 +1,13 @@ package org.researchstack.backbone.ui.step.body; -import android.content.res.Resources; +import androidx.annotation.DimenRes; +import androidx.annotation.LayoutRes; import android.text.InputFilter; +import android.text.InputType; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; -import android.widget.LinearLayout; import android.widget.TextView; import com.jakewharton.rxbinding.widget.RxTextView; @@ -30,15 +31,25 @@ public class TextQuestionBody implements StepBody { // View Fields //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* private EditText editText; + public EditText getEditText() { + return editText; + } public TextQuestionBody(Step step, StepResult result) { this.step = (QuestionStep) step; this.result = result == null ? new StepResult<>(step) : result; } + public @LayoutRes int getBodyViewRes() { + return R.layout.rsb_item_edit_text_compact; + } + @Override public View getBodyView(int viewType, LayoutInflater inflater, ViewGroup parent) { - View body = inflater.inflate(R.layout.rsb_item_edit_text_compact, parent, false); + View body = inflater.inflate(getBodyViewRes(), parent, false); + + // Format EditText from TextAnswerFormat + TextAnswerFormat format = (TextAnswerFormat) step.getAnswerFormat(); editText = (EditText) body.findViewById(R.id.value); if (step.getPlaceholder() != null) { @@ -46,9 +57,14 @@ public View getBodyView(int viewType, LayoutInflater inflater, ViewGroup parent) } else { editText.setHint(R.string.rsb_hint_step_body_text); } + editText.setEnabled(true); + if (format.isDisabled()) { + editText.setEnabled(false); + } TextView title = (TextView) body.findViewById(R.id.label); + // TODO: naming is confusing... compact means less, but this adds a view -MDP if (viewType == VIEW_TYPE_COMPACT) { title.setText(step.getTitle()); } else { @@ -66,10 +82,14 @@ public View getBodyView(int viewType, LayoutInflater inflater, ViewGroup parent) result.setResult(text.toString()); }); - // Format EditText from TextAnswerFormat - TextAnswerFormat format = (TextAnswerFormat) step.getAnswerFormat(); - - editText.setSingleLine(!format.isMultipleLines()); + if(format.isMultipleLines()) { + editText.setSingleLine(false); + editText.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE); + editText.setHorizontallyScrolling(false); + editText.setLines(5); + } else { + editText.setSingleLine(false); + } if (format.getMaximumLength() > TextAnswerFormat.UNLIMITED_LENGTH) { InputFilter.LengthFilter maxLengthFilter = new InputFilter.LengthFilter(format.getMaximumLength()); @@ -77,17 +97,11 @@ public View getBodyView(int viewType, LayoutInflater inflater, ViewGroup parent) editText.setFilters(filters); } - Resources res = parent.getResources(); - LinearLayout.MarginLayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - layoutParams.leftMargin = res.getDimensionPixelSize(R.dimen.rsb_margin_left); - layoutParams.rightMargin = res.getDimensionPixelSize(R.dimen.rsb_margin_right); - body.setLayoutParams(layoutParams); + editText.setInputType(format.getInputType()); return body; } - @Override public StepResult getStepResult(boolean skipped) { if (skipped) { diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ActiveStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ActiveStepLayout.java new file mode 100644 index 000000000..0c4da0945 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ActiveStepLayout.java @@ -0,0 +1,606 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.Manifest; +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.media.ToneGenerator; +import android.os.Build; +import android.os.Handler; +import android.os.Vibrator; +import android.speech.tts.TextToSpeech; +import androidx.annotation.CallSuper; +import androidx.annotation.RequiresPermission; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.result.FileResult; +import org.researchstack.backbone.result.Result; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.ActiveStep; +import org.researchstack.backbone.step.active.ActiveTaskAndResultListener; +import org.researchstack.backbone.step.active.RecorderService; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; +import org.researchstack.backbone.ui.views.FixedSubmitBarLayout; +import org.researchstack.backbone.utils.LogExt; +import org.researchstack.backbone.utils.ResUtils; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; + +import static org.researchstack.backbone.step.active.RecorderService.DEFAULT_VIBRATION_AND_SOUND_DURATION; + +/** + * Created by TheMDP on 2/4/17. + * + * /** + * The `ActiveStepLayout` class is the base class for displaying `ActiveStep` + * subclasses. The predefined active tasks defined in `TremorTaskFactory` all make use + * of subclasses of `ActiveStep`, paired with `ActiveStepLayout` subclasses. + * + * Active steps generally include some form of sensor-driven data collection, + * or involve some highly interactive content, such as a cognitive task or game. + * + * Examples of active step layout subclasses include `WalkingTaskStepLayout`, + * `CountdownStepLayout`, `SpatialSpanMemoryLayout`, `FitnessStepLayout`, and `AudioStepLayout`. + * + * The primary feature that active step layouts enable is recorder life cycle. + * After an active step is presented, it can be started to start a timer. When the timer expires, the + * step is considered finished. Some steps may have the concept of suspend and resume, such as when + * the app is put in the background, and during which data recording is temporarily paused. + * These life cycle methods generally apply to any recorders being used to record + * data from the device's sensors, but they should also be applied to any UI + * being displayed to clearly indicate when data is being collected + * for the task. + * + * When you develop a new active step, you should subclass `ActiveStepLayout` + * and define your specific UI. When subclassing, pay special attention to the life cycle + * methods, `start`, `finish`, `suspend`, and `resume`. Also, be sure to test for + * the expected behavior when the user suspends and resumes the app, during task + * save and restore, and during UIKit's UI state restoration. + */ + +public class ActiveStepLayout extends FixedSubmitBarLayout + implements StepLayout, TextToSpeech.OnInitListener { + + /** + * When this is true, files will be saved externally so you can read them + * since reading internal files requires root access. + * You must add WRITE_EXTERNAL_STORAGE permission to manifest as well + */ + private static final boolean DEBUG_SAVE_FILES_EXTERNALLY = false; + + private TextToSpeech tts; + protected String textToSpeakOnInit; + + protected StepCallbacks callbacks; + + private BroadcastReceiver recorderServiceReceiver; + + protected StepResult stepResult; + protected Handler mainHandler; + protected Runnable animationRunnable; + protected long startTime; + + protected int secondsLeft; + + protected ActiveStep activeStep; + protected LinearLayout activeStepLayout; + protected boolean isDetached; + + public LinearLayout getActiveStepLayout() { + return activeStepLayout; + } + protected TextView titleTextview; + protected TextView textTextview; + protected TextView timerTextview; + protected ProgressBar progressBar; + protected ProgressBar progressBarHorizontal; + + protected ImageView imageView; + + protected ActiveTaskAndResultListener taskAndResultListener; + public void setTaskAndResultListener(ActiveTaskAndResultListener listener) { + taskAndResultListener = listener; + } + + public ActiveStepLayout(Context context) { + super(context); + } + + public ActiveStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ActiveStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public ActiveStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public int getContentResourceId() { + return R.layout.rsb_step_layout_active_step; + } + + @Override + public void initialize(Step step, StepResult result) { + validateStep(step); + + mainHandler = new Handler(); + setupActiveViews(); + setupSubmitBar(); + + // We don't allow this activeStep to have it's state saved + stepResult = new StepResult<>(step); + + if (activeStep.hasVoice()) { + tts = new TextToSpeech(getContext(), this); + } + + if (!checkForAndLoadExistingState()) { + if (activeStep.getShouldStartTimerAutomatically()) { + start(); + } + } + } + + /** + * This is called when the activity containing this active step layout + * has previously moved to onPause and is coming back to the foreground with onResume + * This will only happen when the user actually leaves the app, or shuts off the screen, then returns + */ + public void resumeActiveStepLayout() { + if (isDetached) { // we can only resume if this view were previously detached + checkForAndLoadExistingState(); + } + } + + /** + * This is called when the activity containing this active step layout + * has moved to onPause and we should stop responding to broadcasts and anything else UI related + */ + public void pauseActiveStepLayout() { + LogExt.d(ActiveStepLayout.class, "pauseActiveStepLayout()"); + removeUiRelatedItemsAndCallbacks(); + } + + protected void removeUiRelatedItemsAndCallbacks() { + LogExt.d(ActiveStepLayout.class, "removeUiRelatedItemsAndCallbacks()"); + isDetached = true; + mainHandler.removeCallbacksAndMessages(null); + if (tts != null) { + tts.shutdown(); + tts = null; + } + unregisterRecorderBroadcastReceivers(); + } + + protected boolean checkForAndLoadExistingState() { + isDetached = false; + Context appContext = getContext().getApplicationContext(); + + // Check if this view is being re-created after it was destroyed while recording finished + RecorderService.ResultHolder resultHolder = + RecorderService.consumeSavedResultList(appContext, activeStep); + if (resultHolder != null) { + // This view was destroyed while we were running the recorders in + // the background in the RecorderService, and now we are being re-created + // after the recorder has finished + stepLayoutWasResumedInFinishedState(resultHolder); + return true; + } + + // Check if this view was destroyed and re-created while the recorder was still running + Long recorderStartTime = RecorderService.getStartTime(appContext, activeStep); + if (recorderStartTime != null) { + stepLayoutWasResumedInRecordingState(recorderStartTime); + return true; + } + return false; + } + + protected void setupSubmitBar() { + if (submitBar == null) { + return; // some custom UI implementations don't use the submit bar + } + + if (activeStep.isOptional()) { + submitBar.getNegativeActionView().setVisibility(View.VISIBLE); + submitBar.setNegativeAction(o -> skip()); + } else { + submitBar.getNegativeActionView().setVisibility(View.GONE); + } + + if (activeStep.getShouldStartTimerAutomatically()) { + submitBar.setPositiveActionViewEnabled(false); + } else { + submitBar.setPositiveTitle(R.string.rsb_BUTTON_GET_STARTED); + submitBar.setPositiveAction(o -> { + submitBar.setPositiveTitle(R.string.rsb_BUTTON_NEXT); + start(); + }); + } + } + + /** + * This method will be called when we were recording in the background with RecorderService + * and this step layout was destroyed and then re-created before the recording finished + */ + protected void stepLayoutWasResumedInRecordingState(long recordingStartTime) { + LogExt.d(ActiveStepLayout.class, "stepLayoutWasResumedInRecordingState()"); + // can be implemented by sub-class to resume it's UI + startTime = recordingStartTime; + startAnimation(); + + // Since we are not finished recording, we should re-register for recorder broadcasts + Context appContext = getContext().getApplicationContext(); + registerRecorderBroadcastReceivers(appContext); + } + + /** + * Should be implemented by sub-class to resume it's UI in the finished state + * This method will be called when we were recording in the background with RecorderService + * and this step layout was destroyed by the OS and then re-created after recording has finished + */ + protected void stepLayoutWasResumedInFinishedState(RecorderService.ResultHolder resultHolder) { + LogExt.d(ActiveStepLayout.class, "stepLayoutWasCreatedInFinishedState()"); + processRecorderServiceResults(resultHolder, true); + } + + protected void registerRecorderBroadcastReceivers(Context appContext) { + LogExt.d(ActiveStepLayout.class, "registerRecorderBroadcastReceivers()"); + recorderServiceReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || intent.getAction() == null) { + return; + } + if (RecorderService.ACTION_BROADCAST_RECORDER_COMPLETE.equals(intent.getAction())) { + LogExt.d(ActiveStepLayout.class, "RecorderService complete broadcast received"); + RecorderService.ResultHolder resultHolder = + RecorderService.consumeSavedResultList(appContext, activeStep); + if (resultHolder == null) { + showOkAlertDialog("Critical error, no recorder results", (dialogInterface, i) -> { + callbacks.onSaveStep(StepCallbacks.ACTION_END, activeStep, null); + }); + } else { + processRecorderServiceResults(resultHolder, false); + } + } else if (RecorderService.ACTION_BROADCAST_RECORDER_METRONOME.equals(intent.getAction())) { + if (intent.hasExtra(RecorderService.BROADCAST_RECORDER_METRONOME_CTR)) { + recorderServiceMetronomeAction(intent.getIntExtra( + RecorderService.BROADCAST_RECORDER_METRONOME_CTR, 0)); + } + } else if (RecorderService.ACTION_BROADCAST_RECORDER_SPOKEN_TEXT.equals(intent.getAction())) { + if (intent.hasExtra(RecorderService.BROADCAST_RECORDER_SPOKEN_TEXT)) { + recorderServiceSpokeText(intent.getStringExtra( + RecorderService.BROADCAST_RECORDER_SPOKEN_TEXT)); + } + } + } + }; + IntentFilter intentFilter = new IntentFilter(RecorderService.ACTION_BROADCAST_RECORDER_COMPLETE); + intentFilter.addAction(RecorderService.ACTION_BROADCAST_RECORDER_METRONOME); + intentFilter.addAction(RecorderService.ACTION_BROADCAST_RECORDER_SPOKEN_TEXT); + LocalBroadcastManager.getInstance(appContext) + .registerReceiver(recorderServiceReceiver, intentFilter); + } + + protected void unregisterRecorderBroadcastReceivers() { + LogExt.d(ActiveStepLayout.class, "unregisterRecorderBroadcastReceivers()"); + // Remove the recorder receiver, we will check if it completed when this view is re-created + if (recorderServiceReceiver != null) { + Context appContext = getContext().getApplicationContext(); + LocalBroadcastManager.getInstance(appContext).unregisterReceiver(recorderServiceReceiver); + } + } + + @CallSuper + @RequiresPermission(value = Manifest.permission.VIBRATE, conditional = true) + public void start() { + LogExt.d(ActiveStepLayout.class, "start()"); + if (activeStep.startsFinished()) { + return; + } + + if (activeStep.getShouldVibrateOnStart()) { + vibrate(); + } + + if (activeStep.getShouldPlaySoundOnStart()) { + playSound(); + } + + if (activeStep.getStepDuration() > 0) { + startTime = System.currentTimeMillis(); + startAnimation(); + } + + startBackgroundRecorderService(); + } + + protected void startBackgroundRecorderService() { + LogExt.d(ActiveStepLayout.class, "startBackgroundRecorderService()"); + Context appContext = getContext().getApplicationContext(); + if (taskAndResultListener == null) { + throw new IllegalStateException("taskAndResultListener cant be null +" + + "this should be set through the ActiveTaskActivity"); + } + RecorderService.startService( + appContext, getOutputDirectory(appContext), activeStep, + taskAndResultListener.activeTaskActivityGetTask(), + taskAndResultListener.activeTaskActivityResult()); + registerRecorderBroadcastReceivers(appContext); + } + + protected void stopRecordingService() { + LogExt.d(ActiveStepLayout.class, "stopRecordingService()"); + getContext().stopService(new Intent(getContext(), RecorderService.class)); + } + + protected void processRecorderServiceResults( + final RecorderService.ResultHolder resultHolder, boolean delayOperation) { + + long delay = delayOperation ? 100 : 0; + // We need to delay the callback.onSaveStep() to proceed to the next step slightly, + // this gives the activity time to finish setting up this StepLayout before moving to the next + mainHandler.postDelayed(() -> { + if (resultHolder.getErrorMessage() != null) { + LogExt.d(ActiveStepLayout.class, "RecorderService complete error message received"); + showOkAlertDialog(resultHolder.getErrorMessage(), (dialogInterface, i) -> { + callbacks.onSaveStep(StepCallbacks.ACTION_END, activeStep, null); + }); + } else { + LogExt.d(ActiveStepLayout.class, "RecorderService complete success"); + List recorderResults = resultHolder.getResultList(); + for (Result result : recorderResults) { + stepResult.setResultForIdentifier(result.getIdentifier(), result); + } + stop(); + stepResultFinished(); + } + }, delay); + } + + /** + * @param ctx can be app or activity, used to get files directory + * @return directory for outputting data logger files + */ + public static File getOutputDirectory(Context ctx) { + File outputDir = ctx.getFilesDir(); + if (DEBUG_SAVE_FILES_EXTERNALLY) { + outputDir = ctx.getExternalFilesDir(null); + } + return outputDir; + } + + @RequiresPermission(value = Manifest.permission.VIBRATE, conditional = true) + public void stop() { + LogExt.d(ActiveStepLayout.class, "stop()"); + mainHandler.removeCallbacksAndMessages(null); + if (!activeStep.getShouldContinueOnFinish()) { + if (submitBar != null) { + submitBar.setPositiveActionViewEnabled(true); + submitBar.setPositiveAction(o -> + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, activeStep, stepResult)); + } + } else { + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, activeStep, stepResult); + } + } + + /** + * A force stop should be called when this step layout is being cancelled + */ + public void forceStop() { + LogExt.d(ActiveStepLayout.class, "forceStop()"); + stopRecordingService(); + } + + public void skip() { + LogExt.d(ActiveStepLayout.class, "skip()"); + pauseActiveStepLayout(); + forceStop(); + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, activeStep, null); + } + + protected void startAnimation() { + LogExt.d(ActiveStepLayout.class, "startAnimation()"); + // Start animation may not be called when the recording starts + // so calculate how many seconds have gone by + long durationInMs = activeStep.getStepDuration() * 1000L; + long elapsedTimeInMs = System.currentTimeMillis() - startTime; + secondsLeft = (int)((durationInMs - elapsedTimeInMs) / 1000); + + animationRunnable = () -> { + doUIAnimationPerSecond(); + + secondsLeft--; + + // These calculations will remove any lag from the seconds timer + long timeToPast = (activeStep.getStepDuration() - secondsLeft) * 1000; + long nextSecond = startTime + timeToPast; + long timeUntilNextSecond = nextSecond - System.currentTimeMillis(); + + if (secondsLeft >= 0) { + mainHandler.postDelayed(animationRunnable, timeUntilNextSecond); + } + }; + mainHandler.removeCallbacks(animationRunnable); + mainHandler.post(animationRunnable); + } + + public void doUIAnimationPerSecond() { + if (timerTextview != null) { + timerTextview.setText(toMinuteSecondsString(secondsLeft)); + } + } + + private String toMinuteSecondsString(int seconds) { + int mins = seconds / 60; + int secs = seconds - mins * 60; + return String.format(Locale.getDefault(), "%02d:%02d", mins, secs); + } + + public void setupActiveViews() { + titleTextview = contentContainer.findViewById(R.id.rsb_active_step_layout_title); + if (titleTextview != null) { + titleTextview.setText(activeStep.getTitle()); + titleTextview.setVisibility(activeStep.getTitle() == null ? View.GONE : View.VISIBLE); + } + + textTextview = contentContainer.findViewById(R.id.rsb_active_step_layout_text); + if (textTextview != null) { + textTextview.setText(activeStep.getText()); + textTextview.setVisibility(activeStep.getText() == null ? View.GONE : View.VISIBLE); + } + + timerTextview = contentContainer.findViewById(R.id.rsb_active_step_layout_countdown); + + progressBar = contentContainer.findViewById(R.id.rsb_active_step_layout_progress); + progressBarHorizontal = contentContainer.findViewById(R.id.rsb_active_step_layout_progress_horizontal); + + imageView = contentContainer.findViewById(R.id.rsb_image_view); + if (imageView != null) { + if (activeStep.getImageResName() != null) { + int drawableInt = ResUtils.getDrawableResourceId(getContext(), activeStep.getImageResName()); + if (drawableInt != 0) { + imageView.setImageResource(drawableInt); + imageView.setVisibility(View.VISIBLE); + } + } else { + imageView.setVisibility(View.GONE); + } + } + + activeStepLayout = contentContainer.findViewById(R.id.rsb_step_layout_active_layout); + + if (timerTextview != null) { + if (activeStep.hasCountDown()) { + timerTextview.setVisibility(View.VISIBLE); + } else { + timerTextview.setVisibility(View.GONE); + } + } + } + + protected void validateStep(Step step) { + if (!(step instanceof ActiveStep)) { + throw new IllegalStateException("ActiveStepLayout must have an ActiveStep"); + } + activeStep = (ActiveStep)step; + } + + @Override + public View getLayout() { + return this; + } + + @Override + public boolean isBackEventConsumed() { + // You cannot go back during an active activeStep, you can only cancel + return true; + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + LogExt.d(ActiveStepLayout.class, "onDetachedFromWindow()"); + removeUiRelatedItemsAndCallbacks(); + } + + @Override + public void setCallbacks(StepCallbacks callbacks) { + this.callbacks = callbacks; + } + + @RequiresPermission(Manifest.permission.VIBRATE) + private void vibrate() { + Vibrator v = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); + v.vibrate(DEFAULT_VIBRATION_AND_SOUND_DURATION); + } + + protected void playSound() { + ToneGenerator toneG = new ToneGenerator(AudioManager.STREAM_ALARM, 50); // 50 = half volume + // Play a low and high tone for 500 ms at full volume + toneG.startTone(ToneGenerator.TONE_CDMA_LOW_L, DEFAULT_VIBRATION_AND_SOUND_DURATION); + toneG.startTone(ToneGenerator.TONE_CDMA_HIGH_L, DEFAULT_VIBRATION_AND_SOUND_DURATION); + } + + protected void speakText(String text) { + // Setting this will guarantee the text gets spoken in the case that tts isn't set up yet + textToSpeakOnInit = text; + if (tts == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ttsGreater21(text); + } else { + ttsUnder20(text); + } + } + + @SuppressWarnings("deprecation") + private void ttsUnder20(String text) { + HashMap map = new HashMap<>(); + map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "MessageId"); + tts.speak(text, TextToSpeech.QUEUE_FLUSH, map); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void ttsGreater21(String text) { + String utteranceId = String.valueOf(hashCode()); + tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId); + } + + protected void stepResultFinished() { + // To be implemented by sub-classes that need to save more info to step result + } + + // TextToSpeech initialization + @Override + public void onInit(int i) { + if (i == TextToSpeech.SUCCESS) { + int languageAvailable = tts.isLanguageAvailable(Locale.getDefault()); + // >= 0 means LANG_AVAILABLE, LANG_COUNTRY_AVAILABLE, or LANG_COUNTRY_VAR_AVAILABLE + if (languageAvailable >= 0) { + tts.setLanguage(Locale.getDefault()); + if (ActiveStepLayout.this.activeStep.getSpokenInstruction() != null) { + speakText(ActiveStepLayout.this.activeStep.getSpokenInstruction()); + } else if (textToSpeakOnInit != null) { + speakText(textToSpeakOnInit); + } + } else { + tts = null; + } + } else { + Log.e(getClass().getCanonicalName(), "Failed to initialize TTS with error code " + i); + tts = null; + } + } + + protected void recorderServiceMetronomeAction(int metronomeCtr) { + // Can be implemented by sub-class to do UI events on metronome sound + } + + protected void recorderServiceSpokeText(String spokenText) { + // Can be implemented by sub-class to also show text in the UI + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/AudioStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/AudioStepLayout.java new file mode 100644 index 000000000..4983c905f --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/AudioStepLayout.java @@ -0,0 +1,114 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.AudioStep; +import org.researchstack.backbone.step.active.recorder.AudioRecorder; +import org.researchstack.backbone.ui.views.AudioGraphView; + +/** + * Created by TheMDP on 2/27/17. + * + * The AudioStepLayout class shows a graph in real-time of the user's microphone data + * It does this by taking in data from the AudioRecorder and forwarding it to an AudioGraphView + */ + +public class AudioStepLayout extends ActiveStepLayout { + + protected AudioStep audioStep; + protected BroadcastReceiver audioUpdateReciever; + + protected RelativeLayout audioContentLayout; + protected AudioGraphView audioGraphView; + + 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); + } + + public AudioStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void setupActiveViews() { + super.setupActiveViews(); + + audioContentLayout = (RelativeLayout)layoutInflater + .inflate(R.layout.rsb_step_layout_audio, activeStepLayout, false); + + audioGraphView = audioContentLayout.findViewById(R.id.rsb_step_layout_audio_graph); + int primaryColor = ContextCompat.getColor(getContext(), R.color.rsb_colorPrimary); + audioGraphView.setGraphColor(primaryColor); + + timerTextview = audioContentLayout.findViewById(R.id.rsb_step_layout_audio_countdown); + timerTextview.setTextColor(primaryColor); + + activeStepLayout.addView(audioContentLayout, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + @Override + protected void validateStep(Step step) { + super.validateStep(step); + + if (!(step instanceof AudioStep)) { + throw new IllegalStateException("AudioStepLayout must have an AudioStep"); + } + audioStep = (AudioStep)step; + } + + @Override + protected void registerRecorderBroadcastReceivers(Context appContext) { + super.registerRecorderBroadcastReceivers(appContext); + audioUpdateReciever = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || intent.getAction() == null) { + return; + } + if (AudioRecorder.BROADCAST_SAMPLE_ACTION.equals(intent.getAction())) { + AudioRecorder.AverageSampleHolder sampleHolder = + AudioRecorder.getAverageSample(intent); + if (sampleHolder != null) { + if (audioGraphView == null) { + // graph not ready yet + return; + } + + audioGraphView.setMaxSampleValue(sampleHolder.getMaxVolume()); + audioGraphView.addSample(sampleHolder.getAverageSampleVolume()); + } + } + } + }; + IntentFilter intentFilter = new IntentFilter(AudioRecorder.BROADCAST_SAMPLE_ACTION); + LocalBroadcastManager.getInstance(appContext) + .registerReceiver(audioUpdateReciever, intentFilter); + } + + @Override + protected void unregisterRecorderBroadcastReceivers() { + super.unregisterRecorderBroadcastReceivers(); + Context appContext = getContext().getApplicationContext(); + LocalBroadcastManager.getInstance(appContext).unregisterReceiver(audioUpdateReciever); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentDocumentStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentDocumentStepLayout.java index fd1a297b1..b8d52cd95 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentDocumentStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentDocumentStepLayout.java @@ -1,7 +1,7 @@ package org.researchstack.backbone.ui.step.layout; import android.content.Context; -import android.support.v7.app.AlertDialog; +import androidx.appcompat.app.AlertDialog; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -46,6 +46,9 @@ public ConsentDocumentStepLayout(Context context, AttributeSet attrs, int defSty public void initialize(Step step, StepResult result) { this.step = (ConsentDocumentStep) step; this.confirmationDialogBody = ((ConsentDocumentStep) step).getConfirmMessage(); + if (confirmationDialogBody == null) { + confirmationDialogBody = getContext().getString(R.string.rsb_consent_document_review_message); + } this.htmlContent = ((ConsentDocumentStep) step).getConsentHTML(); this.stepResult = result; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentReviewSubstepListStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentReviewSubstepListStepLayout.java new file mode 100644 index 000000000..e9a17c433 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentReviewSubstepListStepLayout.java @@ -0,0 +1,143 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; + +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.DataResponse; +import org.researchstack.backbone.model.ConsentSignatureBody; +import org.researchstack.backbone.model.ProfileInfoOption; +import org.researchstack.backbone.model.User; +import org.researchstack.backbone.model.survey.factory.ConsentDocumentFactory; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.ui.ViewTaskActivity; +import org.researchstack.backbone.utils.ObservableUtils; +import org.researchstack.backbone.utils.StepHelper; +import org.researchstack.backbone.utils.StepLayoutHelper; +import org.researchstack.backbone.utils.StepResultHelper; + +import java.util.Date; +import java.util.Map; + +import rx.Observable; + +import static org.researchstack.backbone.model.survey.factory.ConsentDocumentFactory.CONSENT_SHARING_IDENTIFIER; +import static org.researchstack.backbone.model.survey.factory.ConsentDocumentFactory.CONSENT_SUBTASK_ID; + +/** + * Created by TheMDP on 1/16/17. + * + * Consent ReviewStep contains a number of steps + */ + +public class ConsentReviewSubstepListStepLayout extends ViewPagerSubstepListStepLayout implements ViewTaskActivity.ResultListener { + + public static final String LOG_TAG = ConsentReviewSubstepListStepLayout.class.getCanonicalName(); + + /** + * This is passed in by the ResultListener + */ + private String sharingScope; + + public ConsentReviewSubstepListStepLayout(Context context) { + super(context); + } + + public ConsentReviewSubstepListStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onComplete() { + ConsentSignatureBody consentSignatureBody = createConsentSignatureBody(stepResult); + if (DataProvider.getInstance().isSignedIn(getContext())) { + uploadConsent(consentSignatureBody); + } else { + DataProvider.getInstance().saveLocalConsent(getContext(), consentSignatureBody); + super.onComplete(); + } + } + + protected void uploadConsent(ConsentSignatureBody consentSignatureBody) { + Observable uploadConsent = DataProvider.getInstance() + .uploadConsent(getContext(), consentSignatureBody) + .compose(ObservableUtils.applyDefault()); + + // Only gives a callback to response on success, the rest is handled by StepLayoutHelper + StepLayoutHelper.safePerformWithAlerts(uploadConsent, this, response -> + super.onComplete() + ); + } + + /** + * @param stepResult The StepResult for the current step + * @return a completed ConsentSignatureBody model, if all the data is contained in either the + * the StepResult or the TaskResult + */ + protected ConsentSignatureBody createConsentSignatureBody(StepResult stepResult) { + + ConsentSignatureBody body = DataProvider.getInstance().loadLocalConsent(getContext()); + if (body == null) { + body = new ConsentSignatureBody(); + } + + // Grab signature from step result + StepResult signatureResult = StepResultHelper.findStepResult( + stepResult, ConsentDocumentFactory.CONSENT_SIGNATURE_IDENTIFIER); + + if (signatureResult != null) { + Map signatureData = signatureResult.getResults(); + for (String stepKey : signatureData.keySet()) { + switch (stepKey) { + case ConsentSignatureStepLayout.KEY_SIGNATURE: + body.imageData = (String)signatureData.get(stepKey); + body.imageMimeType = "image/png"; + break; + } + } + } + if (body.imageData == null) { + throw new IllegalStateException("Image data needs to be accessable at this point for StepLayout to work"); + } + + String usersName = StepResultHelper.findStringResult(ProfileInfoOption.NAME.getIdentifier(), stepResult); + if (usersName == null) { + throw new IllegalStateException("Names needs to be accessable at this point for StepLayout to work"); + } + body.name = usersName; + + Date usersBirthday = StepResultHelper.findDateResult(ProfileInfoOption.BIRTHDATE.getIdentifier(), stepResult); + if (usersBirthday == null) { + throw new IllegalStateException("Birthdate needs to be accessable at this point for StepLayout to work"); + } + body.birthdate = usersBirthday; + + // If one doesn't exist yet then set it to a blank string, otherwise the consent sig will have null value issues + if (sharingScope == null) { + if (body.scope == null) { + sharingScope = ""; + } + } else { + body.scope = sharingScope; + } + + // Save Consent Information + // User is not signed in yet, so we need to save consent info to disk for later upload + return body; + } + + @Override + public void taskResult(ViewTaskActivity activity, TaskResult taskResult) { + String identifier = StepResultHelper.subtaskIdentifier(CONSENT_SUBTASK_ID, CONSENT_SHARING_IDENTIFIER); + Boolean sharingScopeResult = StepResultHelper.findBooleanResult(identifier, taskResult); + if (sharingScopeResult != null) { + if (sharingScopeResult) { + sharingScope = User.DataSharingScope.ALL.getIdentifier(); + } else { + sharingScope = User.DataSharingScope.STUDY.getIdentifier(); + } + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentSignatureStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentSignatureStepLayout.java index 2c7ba7734..21bf1bc7a 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentSignatureStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentSignatureStepLayout.java @@ -14,6 +14,7 @@ import org.researchstack.backbone.R; import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; import org.researchstack.backbone.step.ConsentSignatureStep; import org.researchstack.backbone.step.Step; import org.researchstack.backbone.ui.callbacks.SignatureCallbacks; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentVisualStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentVisualStepLayout.java index 815c13095..547fea044 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentVisualStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ConsentVisualStepLayout.java @@ -4,7 +4,7 @@ import android.content.Intent; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.support.v4.graphics.drawable.DrawableCompat; +import androidx.core.graphics.drawable.DrawableCompat; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; @@ -27,8 +27,8 @@ public class ConsentVisualStepLayout extends FixedSubmitBarLayout implements StepLayout { - private StepCallbacks callbacks; - private ConsentVisualStep step; + protected StepCallbacks callbacks; + protected ConsentVisualStep step; public ConsentVisualStepLayout(Context context) { super(context); @@ -69,7 +69,7 @@ public int getContentResourceId() { return R.layout.rsb_step_layout_consent_visual; } - private void initializeStep() { + protected void initializeStep() { ConsentSection data = step.getSection(); // Set Image @@ -79,14 +79,8 @@ private void initializeStep() { int accentColor = a.getColor(0, 0); a.recycle(); - ImageView imageView = (ImageView) findViewById(R.id.image); - - String imageName = !TextUtils.isEmpty(data.getCustomImageName()) - ? data.getCustomImageName() - : data.getType().getImageName(); - - int imageResId = ResUtils.getDrawableResourceId(getContext(), imageName); - + ImageView imageView = findViewById(R.id.image); + int imageResId = getImageRes(); if (imageResId != 0) { Drawable drawable = getResources().getDrawable(imageResId); drawable = DrawableCompat.wrap(drawable); @@ -98,18 +92,15 @@ private void initializeStep() { } // Set Title - TextView titleView = (TextView) findViewById(R.id.title); - String title = TextUtils.isEmpty(data.getTitle()) ? getResources().getString(data.getType() - .getTitleResId()) : data.getTitle(); - titleView.setText(title); + TextView titleView = findViewById(R.id.title); + titleView.setText(getTitle()); // Set Summary - TextView summaryView = (TextView) findViewById(R.id.summary); - summaryView.setText(data.getSummary()); + TextView summaryView = findViewById(R.id.summary); + summaryView.setText(getSummary()); // Set more info - TextView moreInfoView = (TextView) findViewById(R.id.more_info); - + TextView moreInfoView = findViewById(R.id.more_info); if (!TextUtils.isEmpty(data.getHtmlContent())) { if (!TextUtils.isEmpty(data.getCustomLearnMoreButtonTitle())) { moreInfoView.setText(data.getCustomLearnMoreButtonTitle()); @@ -118,20 +109,66 @@ private void initializeStep() { } RxView.clicks(moreInfoView).subscribe(v -> { - String webTitle = getResources().getString(R.string.rsb_consent_section_more_info); - Intent webDoc = ViewWebDocumentActivity.newIntentForContent(getContext(), webTitle, - TextUtils.isEmpty(data.getContent()) ? data.getHtmlContent() : data.getContent()); - getContext().startActivity(webDoc); + moreInfoClicked(); }); } else { moreInfoView.setVisibility(View.GONE); } SubmitBar submitBar = (SubmitBar) findViewById(R.id.rsb_submit_bar); - submitBar.setPositiveTitle(step.getNextButtonString()); - submitBar.setPositiveAction(v -> callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, - step, - null)); + String nextButtonTitle = getContext().getString(R.string.rsb_next); + // Support for deprecated method + if (step.getNextButtonString() != null) { + nextButtonTitle = step.getNextButtonString(); + } + submitBar.setPositiveTitle(nextButtonTitle); + submitBar.setPositiveAction(v -> nextButtonClicked()); submitBar.getNegativeActionView().setVisibility(View.GONE); } + + protected void nextButtonClicked() { + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, step, null); + } + + protected void moreInfoClicked() { + ConsentSection data = step.getSection(); + String webTitle = getResources().getString(R.string.rsb_consent_section_more_info); + Intent webDoc = ViewWebDocumentActivity.newIntentForContent(getContext(), webTitle, + TextUtils.isEmpty(data.getContent()) ? data.getHtmlContent() : data.getContent()); + getContext().startActivity(webDoc); + } + + protected int getImageRes() { + ConsentSection data = step.getSection(); + String imageName = !TextUtils.isEmpty(data.getCustomImageName()) + ? data.getCustomImageName() + : data.getType().getImageName(); + + return ResUtils.getDrawableResourceId(getContext(), imageName); + } + + protected String getTitle() { + ConsentSection data = step.getSection(); + return TextUtils.isEmpty(data.getTitle()) ? getResources().getString(data.getType() + .getTitleResId()) : data.getTitle(); + } + + protected String getSummary() { + ConsentSection data = step.getSection(); + return data.getSummary(); + } + + /** + * @return The index of the section within all the visual consent sections + */ + public int getSectionIndex() { + return step.getSectionIndex(); + } + + /** + * @return The total count of the visual consent sections + */ + public int getSectionCount() { + return step.getSectionCount(); + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/CountdownStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/CountdownStepLayout.java new file mode 100644 index 000000000..1f49c51c4 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/CountdownStepLayout.java @@ -0,0 +1,156 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.annotation.TargetApi; +import android.content.Context; +import androidx.core.content.ContextCompat; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.CountdownStep; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; +import org.researchstack.backbone.ui.views.ArcDrawable; + +/** + * Created by TheMDP on 2/4/17. + * + * The CountdownStepLayout displays a simple countdown with a rotating arc that is full at the start + * and becomes smaller until the countdown is over + */ + +public class CountdownStepLayout extends ActiveStepLayout { + + private static final long ANIMATION_DELAY = (long)(1000.0 / 60.0); // ~60fps + private ArcDrawable arcDrawable; + protected Runnable fineAnimationRunnable; // runs once per ANIMATION_DELAY + + protected CountdownStep countdownStep; + protected boolean haveReachedEndOfStep; + + public CountdownStepLayout(Context context) { + super(context); + } + + public CountdownStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CountdownStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public CountdownStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void initialize(Step step, StepResult result) { + super.initialize(step, result); + setupCountdownViews(); + startArcDrawableAnimation(); + haveReachedEndOfStep = false; + } + + @Override + public void stop() { + haveReachedEndOfStep = true; + super.stop(); + } + + @Override + public void pauseActiveStepLayout() { + // Ignore pause activity (most likely user shutting off the screen) + // That is okay, we will proceed to the next step even if screen is off + // If we haven't reached the end of the activity yet + if (haveReachedEndOfStep) { + super.pauseActiveStepLayout(); + } + } + + @Override + public void forceStop() { + // Ok, it wasn't a screen turning off, we really must stop + super.pauseActiveStepLayout(); + super.forceStop(); + } + + private void setupCountdownViews() { + + int countdownSize = getContext().getResources() + .getDimensionPixelSize(R.dimen.rsb_step_layout_countdown_size); + // Increase the size of the timer text view to fit the arc drawable + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)timerTextview.getLayoutParams(); + params.width = countdownSize; + params.height = countdownSize; + timerTextview.setLayoutParams(params); + + arcDrawable = new ArcDrawable(); + arcDrawable.setColor(ContextCompat.getColor(getContext(), R.color.rsb_countdown_step_layout_timer_color)); + arcDrawable.setArchWidth(getContext().getResources() + .getDimensionPixelSize(R.dimen.rsb_step_layout_countdown_stroke_width)); + timerTextview.setBackground(arcDrawable); + timerTextview.setVisibility(View.VISIBLE); + + if (countdownStep.getTitle() == null) { + countdownStep.setTitle(getContext().getString(R.string.rsb_COUNTDOWN_LABEL)); + } + titleTextview.setText(countdownStep.getTitle()); + titleTextview.setGravity(Gravity.CENTER); + titleTextview.setPadding(0, 0, 0, 0); + titleTextview.setVisibility(View.VISIBLE); + } + + @Override + protected void validateStep(Step step) { + super.validateStep(step); + if (!(step instanceof CountdownStep)) { + throw new IllegalStateException("CountdownStepLayout must be passed a CountdownStep"); + } + + countdownStep = (CountdownStep)step; + + if (countdownStep.getStepDuration() < 3) { + throw new IllegalStateException("CountdownStep cannot have a activeStep duration less than 3 seconds"); + } + } + + @Override + public void doUIAnimationPerSecond() { + timerTextview.setText(String.valueOf(secondsLeft)); + } + + protected void startArcDrawableAnimation() { + fineAnimationRunnable = () -> { + long timeElapsedInMs = System.currentTimeMillis() - startTime; + float percentLeft = (float)timeElapsedInMs / + (float)(DateUtils.SECOND_IN_MILLIS * (activeStep.getStepDuration())); + float percentComplete = 1.0f - percentLeft; + + if (percentComplete < 0) { + arcDrawable.setSweepAngle(0.0f); + } else { + arcDrawable.setSweepAngle(ArcDrawable.FULL_SWEEPING_ANGLE * percentComplete); + mainHandler.postDelayed(fineAnimationRunnable, ANIMATION_DELAY); + } + }; + // All mainHandler callbacks are cancelled correctly by base class + mainHandler.postDelayed(fineAnimationRunnable, ANIMATION_DELAY); + } + + @Override + public View getLayout() { + return this; + } + + @Override + public boolean isBackEventConsumed() { + callbacks.onSaveStep(StepCallbacks.ACTION_PREV, activeStep, null); + return true; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/EmailVerificationStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/EmailVerificationStepLayout.java new file mode 100644 index 000000000..3abe751fc --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/EmailVerificationStepLayout.java @@ -0,0 +1,442 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Color; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; +import android.text.Html; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.jakewharton.rxbinding.view.RxView; + +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.DataResponse; +import org.researchstack.backbone.R; +import org.researchstack.backbone.answerformat.PasswordAnswerFormat; +import org.researchstack.backbone.model.ProfileInfoOption; +import org.researchstack.backbone.model.User; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.EmailVerificationStep; +import org.researchstack.backbone.step.EmailVerificationSubStep; +import org.researchstack.backbone.step.QuestionStep; +import org.researchstack.backbone.step.RegistrationStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; +import org.researchstack.backbone.ui.step.body.StepBody; +import org.researchstack.backbone.ui.views.FixedSubmitBarLayout; +import org.researchstack.backbone.ui.views.SubmitBar; +import org.researchstack.backbone.utils.ObservableUtils; +import org.researchstack.backbone.utils.StepLayoutHelper; +import org.researchstack.backbone.utils.StepResultHelper; +import org.researchstack.backbone.utils.ThemeUtils; + +import rx.Observable; + +/** + * Created by TheMDP on 1/21/17. + * + * The EmailVerificationStepLayout consists of the Email Verification screen, + * and a Registration screen ,which is used to change the user's email if it's not the correct one. + * + * Under the hood, this StepLayout is a SubstepListStep which uses a view pager to switch between + * screens. Having both screens in the same StepLayout allows them to safely exchange the + * user's password they set when changing email. + */ + +public class EmailVerificationStepLayout extends ViewPagerSubstepListStepLayout { + + private static final int EMAIL_VERIFY_SUBSTEP_INDEX = 0; + private static final int REGISTRATION_SUBSTEP_INDEX = 1; + + private String setPasswordOnLoad; + + public EmailVerificationStepLayout(Context context) { + super(context); + } + + public EmailVerificationStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void initialize(Step step, StepResult result) { + validateStep(step); + super.initialize(step, result); + } + + @Override + protected void onStepLayoutCreated(StepLayout stepLayout, int index) { + super.onStepLayoutCreated(stepLayout, index); + if (index == EMAIL_VERIFY_SUBSTEP_INDEX && stepLayout instanceof SubStepLayout) { + setupVerifyEmailSubstepLayout((SubStepLayout)stepLayout); + } else if (index == REGISTRATION_SUBSTEP_INDEX && stepLayout instanceof RegistrationStepLayout) { + setupRegistrationSubstepLayout((RegistrationStepLayout)stepLayout); + } + } + + protected void setupVerifyEmailSubstepLayout(SubStepLayout substepLayout) { + substepLayout.setSubstepListener(() -> { + viewPagerAdapter.moveNext(); + saveViewPagerIndex(); + }); + if (setPasswordOnLoad != null) { + substepLayout.setPassword(setPasswordOnLoad); + setPasswordOnLoad = null; + } + } + + protected void setupRegistrationSubstepLayout(RegistrationStepLayout registrationStepLayout) { + // Re-word the standard registration step layout submit bar buttons, and hook them + // into custom actions that make sense for this SubstepLayout + final Step registrationSubstep = substepListStep.getStepList().get(REGISTRATION_SUBSTEP_INDEX); + SubmitBar submitBar = registrationStepLayout.getSubmitBar(); + submitBar.setPositiveTitle(R.string.rsb_change); + submitBar.getNegativeActionView().setVisibility(View.VISIBLE); + submitBar.setNegativeTitle(R.string.rsb_cancel); + submitBar.setNegativeAction(v -> super.onSaveStep(ACTION_PREV, registrationSubstep, null)); + } + + @Override + public void onSaveStep(int action, Step step, StepResult result) { + int indexOfStep = substepListStep.getStepList().indexOf(step); + + // If the registration substep is trying to move next, + // That means that the email has been changed for the user + // and we need to update the EmailVerificationSubstep to reflect the changes + // and also send a previous movement to the subclass to animate the change + if (indexOfStep == REGISTRATION_SUBSTEP_INDEX && action == ACTION_NEXT) { + + // Update email layout to reflect the changes to email and password + StepResult passwordStepResult = StepResultHelper.findStepResult( + result, ProfileInfoOption.PASSWORD.getIdentifier()); + + if (passwordStepResult != null && passwordStepResult.getResult() instanceof String) { + SubStepLayout emailLayout = getEmailSubstepLayout(); + emailLayout.setPassword((String)passwordStepResult.getResult()); + emailLayout.updateEmailText(); + emailLayout.updatePasswordState(); + } + + // Move the view pager back to the email verification step layout + super.onSaveStep(ACTION_PREV, step, result); + + } else if (indexOfStep == EMAIL_VERIFY_SUBSTEP_INDEX && action == ACTION_NEXT) { + // This actually means a successfully verified email, so send user forward, + // which will skip the RegistrationStep + stepResult.getResults().put(step.getIdentifier(), result); + super.onComplete(); + + } else { + super.onSaveStep(action, step, result); + } + } + + /** + * @param password explicitly set password for EmailVerificationSubstepLayout + */ + public void setPassword(String password) { + // We may need to wait until the the StepLayout is created + SubStepLayout substepLayout = getEmailSubstepLayout(); + if (substepLayout != null) { + substepLayout.setPassword(password); + } else { + setPasswordOnLoad = password; + } + } + + protected void validateStep(Step step) { + if (!(step instanceof EmailVerificationStep)) { + throw new IllegalStateException("EmailVerificationStepLayout needs EmailVerificationStep"); + } + + EmailVerificationStep emailVerificationStep = (EmailVerificationStep)step; + if (!(emailVerificationStep.getStepList().get(EMAIL_VERIFY_SUBSTEP_INDEX) instanceof EmailVerificationSubStep)) { + throw new IllegalStateException("EmailVerificationStepLayout expects EmailVerificationSubStep at index " + EMAIL_VERIFY_SUBSTEP_INDEX); + } + if (!(emailVerificationStep.getStepList().get(REGISTRATION_SUBSTEP_INDEX) instanceof RegistrationStep)) { + throw new IllegalStateException("EmailVerificationStepLayout expects RegistrationStep at index " + EMAIL_VERIFY_SUBSTEP_INDEX); + } + substepListStep = emailVerificationStep; + } + + protected SubStepLayout getEmailSubstepLayout() { + return (SubStepLayout)super.getStepLayout(EMAIL_VERIFY_SUBSTEP_INDEX); + } + + protected RegistrationStepLayout getRegistrationSubstepLayout() { + return (RegistrationStepLayout) super.getStepLayout(REGISTRATION_SUBSTEP_INDEX); + } + + public static class SubStepLayout extends FixedSubmitBarLayout implements StepLayout { + + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + // Communicate w/ host + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + protected StepCallbacks callbacks; + protected EmailVerificationSubStep emailStep; + protected StepResult innerStepResult; + + private SubstepListener substepListener; + + /** + * If this is set, the EmailVerificationStepLayout will not create a field for the user + * to enter their password. + */ + @Nullable protected String password; + + /** + * This is only created if the user's app crashed or they forced closed the app + * while they were going through the sign up process + */ + @Nullable StepBody passwordStepBody; + @Nullable View passwordVerifyView; + + public SubStepLayout(Context context) { + super(context); + } + + public SubStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SubStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public SubStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public int getContentResourceId() { + return R.layout.rsb_layout_email_verification; + } + + @Override + public void initialize(Step step, StepResult result) { + validateStepAndResult(step, result); + + setupPasswordFromResult(); + + // Setup submit bar actions and titles + SubmitBar submitBar = (SubmitBar) findViewById(R.id.rsb_submit_bar); + submitBar.setPositiveTitle(getContext().getString(R.string.rsb_continue)); + submitBar.setPositiveAction(v -> signIn()); + submitBar.setNegativeAction(v -> resendVerificationEmail()); + submitBar.setNegativeTitle(getContext().getString(R.string.rsb_resend_email)); + + if (emailStep.getTitle() != null) { + ((TextView)findViewById(R.id.rsb_step_title)).setText(emailStep.getTitle()); + } + + RxView.clicks(findViewById(R.id.rsb_email_verification_wrong_email)).subscribe(v -> changeEmail()); + + updateEmailText(); + updatePasswordState(); + } + + private void setupPasswordFromResult() { + // This is an edge case for when user has changed their email, + // and the password result is directly accessible from this class + if (innerStepResult != null && innerStepResult.getResult() instanceof String) { + setPassword((String)innerStepResult.getResult()); + } + } + + public void updatePasswordState() { + // If the the password isn't in the TaskResult, we have to make the user re-enter their's + if (getPassword() == null) { + if (passwordVerifyView == null) { + createValidatePasswordStepBody(container()); + } else { + passwordVerifyView.setVisibility(View.VISIBLE); + } + } else if (passwordVerifyView != null) { + passwordVerifyView.setVisibility(View.GONE); + } + } + + public void updateEmailText() { + if (emailStep.getText() == null) { + int accentColor = ThemeUtils.getAccentColor(getContext()); + String accentColorString = "#" + Integer.toHexString(Color.red(accentColor)) + + Integer.toHexString(Color.green(accentColor)) + + Integer.toHexString(Color.blue(accentColor)); + final String email = getEmail(); + String formattedSummary = getContext().getString(R.string.rsb_confirm_summary, + "" + email + ""); + ((AppCompatTextView) findViewById(R.id.rsb_email_verification_body)).setText(Html.fromHtml( + formattedSummary)); + } else { + ((AppCompatTextView) findViewById(R.id.rsb_email_verification_body)).setText(emailStep.getText()); + } + } + + @Override + public Parcelable onSaveInstanceState() { + callbacks.onSaveStep(StepCallbacks.ACTION_NONE, emailStep, innerStepResult); + return super.onSaveInstanceState(); + } + + @SuppressWarnings("unchecked") // needed for unchecked StepResult generic type casting + protected void validateStepAndResult(Step step, StepResult result) { + if (!(step instanceof EmailVerificationSubStep)) { + throw new IllegalStateException( + "EmailVerificationStepLayout is only compatible with a EmailVerificationSubStep"); + } + + emailStep = (EmailVerificationSubStep) step; + + this.innerStepResult = result; + } + + @Override + public View getLayout() { + return this; + } + + /** + * Method allowing a step to consume a back event. + * + * @return a boolean indication whether the back event is consumed + */ + @Override + public boolean isBackEventConsumed() { + return true; // can't move backwards from this step layout + } + + public void setCallbacks(StepCallbacks callbacks) { + this.callbacks = callbacks; + } + + protected void changeEmail() { + // Send the user back to the Registration step, but we will let the listener control that + if (substepListener != null) { + substepListener.onChangeEmailRequested(); + } + } + + protected void resendVerificationEmail() { + + final String email = getEmail(); + + Observable resend = DataProvider.getInstance() + .resendEmailVerification(getContext(), email) + .compose(ObservableUtils.applyDefault()); + + // Only gives a callback to response on success, the rest is handled by StepLayoutHelper + StepLayoutHelper.safePerformWithAlerts(resend, this, response -> + { // loading dialog will dismiss indicating success + }); + } + + protected void signIn() { + final String password = getPassword(); + if (password == null || password.isEmpty()) { + Toast.makeText(getContext(), R.string.rsb_error_invalid_password, Toast.LENGTH_SHORT).show(); + return; + } + + Observable verify = DataProvider.getInstance() + .verifyEmail(getContext(), password) + .compose(ObservableUtils.applyDefault()); + + StepLayoutHelper.safePerformWithOnlyLoadingAlerts(verify, this, new StepLayoutHelper.WebCallback() { + @Override + public void onSuccess(DataResponse response) { + if(response.isSuccess()) { + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, emailStep, innerStepResult); + } else { + showOkAlertDialog(response.getMessage()); + } + } + + @Override + public void onFail(Throwable throwable) { + // TODO: fix this once the BridgeDataProvider is fixed + if (throwable.toString().toLowerCase().contains("ConsentRequired".toLowerCase())) { + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, emailStep, innerStepResult); + } else { + showOkAlertDialog(throwable.getMessage()); + } + } + }); + } + + protected RelativeLayout container() { + return (RelativeLayout)findViewById(R.id.rsb_email_verification_container); + } + + /** + * If the app has crashed, or user has force closed it, we will need them to re-enter their password + * Since they will be essentially signing in again + * @param container container to add the step body view to + */ + protected void createValidatePasswordStepBody(RelativeLayout container) { + // Create a verify password step + QuestionStep verifyPasswordStep = new QuestionStep(ProfileInfoOption.PASSWORD.getIdentifier()); + verifyPasswordStep.setAnswerFormat(new PasswordAnswerFormat()); + verifyPasswordStep.setPlaceholder(getContext().getString(R.string.rsb_password_placeholder)); + verifyPasswordStep.setTitle(getContext().getString(R.string.rsb_verify_password)); + + // Use FormStepLayout logic to create the StepLayout for verifyPasswordStep + passwordStepBody = SurveyStepLayout.createStepBody(verifyPasswordStep, null); + passwordVerifyView = FormStepLayout.initStepBodyHolder(layoutInflater, container, verifyPasswordStep, passwordStepBody); + + // Replace Space with password view + View oldPasswordSpace = findViewById(R.id.rsb_email_verify_reenter_password_space); + passwordVerifyView.setLayoutParams(oldPasswordSpace.getLayoutParams()); + container.removeView(oldPasswordSpace); + container.addView(passwordVerifyView); + } + + protected String getEmail() { + User user = DataProvider.getInstance().getUser(getContext()); + if (user == null || user.getEmail() == null) { + throw new IllegalStateException("Email must be set on user at this point"); + } + return user.getEmail(); + } + + public void setPassword(@Nullable String password) { + // If we had the enter password view, remove it now that we have a valid password + if (passwordVerifyView != null) { + container().removeView(passwordVerifyView); + passwordVerifyView = null; + passwordStepBody = null; + } + this.password = password; + + // Save this in our step result + StepResult passwordResult = new StepResult<>(emailStep); + passwordResult.setResult(password); + callbacks.onSaveStep(StepCallbacks.ACTION_NONE, emailStep, innerStepResult); + } + + protected String getPassword() { + if (passwordVerifyView == null || passwordVerifyView.getVisibility() == View.GONE) { + return password; + } else if (passwordStepBody != null) { + StepResult result = passwordStepBody.getStepResult(false); + return (String)result.getResult(); + } else { + return null; + } + } + + public void setSubstepListener(SubstepListener listener) { + substepListener = listener; + } + + public interface SubstepListener { + void onChangeEmailRequested(); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/FingerprintStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/FingerprintStepLayout.java new file mode 100644 index 000000000..d7aa43315 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/FingerprintStepLayout.java @@ -0,0 +1,442 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.DialogInterface; +import androidx.annotation.AnyThread; +import androidx.core.hardware.fingerprint.FingerprintManagerCompat; +import androidx.core.os.CancellationSignal; +import androidx.appcompat.app.AlertDialog; +import android.util.AttributeSet; +import android.util.Base64; +import android.view.View; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.StorageAccess; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.PasscodeStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.storage.file.KeystoreEncryptionHelper; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; +import org.researchstack.backbone.utils.ResUtils; + +import javax.crypto.Cipher; + +import rx.functions.Action1; + +/** + * Created by TheMDP on 1/31/17. + * + * This class can be used as a more convenient and secure method for creating + * an encryption key that will be used to protect the user's data + * + * It works with the FingerprintManager to create a secret key that is backed by the user's fingerprint + */ + +@TargetApi(android.os.Build.VERSION_CODES.M) // api 23, or Android 6.0 +public class FingerprintStepLayout extends InstructionStepLayout { + + /** Alias for our key in the Android Key Store */ + private static final String KEY_NAME = "fingerprint_researchstack_secret_key"; + + /** Shared Prefs to store the initialization vectors */ + private static final String FINGERPRINT_IV_SHARED_PREFS = "FINGERPRINT_IV_SHARED_PREFS"; + private static final String IV_SHARED_PREFS_KEY = "IV_SHARED_PREFS_KEY"; + + /** Animation durations can be customized int R.integer */ + private long animTimeFingerprintFrequency; + private long animTimeTooManyAttemptsDelay; + private long animTimeErrorMsgDelay; + + // Does not need customized + private static final long DEFAULT_EXIT_REGISTRATION_DELAY_MILLIS = 20; + + private PasscodeStep fingerprintStep; + + /** Hook into all things fingerprint */ + private FingerprintManagerCompat fingerprintManager; + /** Holds the cryptography methods for encrypting/decrypting pins */ + private FingerprintManagerCompat.CryptoObject cryptoObject; + private Cipher cipher; + + /** Used to cancel fingerprint scanning, when this view is detached or no longer valid for any reason */ + private CancellationSignal cancellationSignal; + boolean mSelfCancelled; + private Runnable fingerprintAnimationRunnable; + + public FingerprintStepLayout(Context context) { + super(context); + init(); + } + + public FingerprintStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public FingerprintStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(21) + public FingerprintStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + animTimeFingerprintFrequency = getContext().getResources().getInteger( + R.integer.rsb_config_anim_time_fingerprint_frequency); + animTimeTooManyAttemptsDelay = getContext().getResources().getInteger( + R.integer.rsb_config_anim_time_fingerprint_too_many_attempts_delay); + animTimeErrorMsgDelay = getContext().getResources().getInteger( + R.integer.rsb_config_anim_time_fingerprint_error_delay); + } + + @Override + public void initialize(Step step, StepResult result) { + validateAndSetStep(step); + + initFingerPrintManager(); + super.initialize(step, result); + setupSubmitBar(); + initInstructionStep(); + + startScanFingerprintAnimation(); + } + + private void initInstructionStep() { + titleTextView.setVisibility(View.VISIBLE); + titleTextView.setText(getContext().getString(R.string.rsb_fingerprint_title)); + textTextView.setVisibility(View.VISIBLE); + textTextView.setText(getContext().getString(R.string.rsb_fingerprint_text)); + } + + private void startScanFingerprintAnimation() { + refreshDetailText( + getContext().getString(R.string.rsb_fingerprint_hint), + getContext().getColor(R.color.rsb_hint)); + + fingerprintAnimationRunnable = new Runnable() { + @Override + public void run() { + refreshDetailText( + getContext().getString(R.string.rsb_fingerprint_hint), + getContext().getResources().getColor(R.color.rsb_hint, null)); + refreshImage(ResUtils.ANIMATED_FINGERPRINT, true); + postDelayed(fingerprintAnimationRunnable, animTimeFingerprintFrequency); + } + }; + post(fingerprintAnimationRunnable); + } + + private void setupSubmitBar() { + if (isCreationStep()) { + submitBar.getPositiveActionView().setVisibility(View.GONE); + submitBar.getNegativeActionView().setVisibility(View.VISIBLE); + submitBar.setNegativeTitle(R.string.rsb_use_passcode); + submitBar.setNegativeAction(new Action1() { + @Override + public void call(Object o) { + showUsePasscodeAlert(); + } + }); + } else { + submitBar.getPositiveActionView().setVisibility(View.GONE); + submitBar.getNegativeActionView().setVisibility(View.VISIBLE); + submitBar.setNegativeTitle(R.string.rsb_log_out); + submitBar.setNegativeAction(new Action1() { + @Override + public void call(Object o) { + showLogoutAlert(); + } + }); + } + } + + private void initFingerPrintManager() { + fingerprintManager = FingerprintManagerCompat.from(getContext()); + // We already have a fingerprint set, and this steplayout will be dismissed once + // the callbacks are set + if (isCreationStep() && StorageAccess.getInstance().hasPinCode(getContext())) { + return; + } + + // Create the correct cipher depending on if we are doing encryption or decryption + if (isCreationStep()) { + cipher = KeystoreEncryptionHelper.initCipherForEncryption(KEY_NAME); + } else { + String base64EncryptedPin = StorageAccess.getInstance().getFingerprint(getContext()); + cipher = KeystoreEncryptionHelper.initCipherForDecryption( + getContext(), KEY_NAME, base64EncryptedPin, loadIv()); + } + + if (cipher != null) { + cryptoObject = new FingerprintManagerCompat.CryptoObject(cipher); + startListening(cryptoObject); + } else { + // A delay is needed so that the Displaying task has time to complete it's + // view rendering, and can properly handle the callback state + postDelayed(new Runnable() { + @Override + public void run() { + showUnrecoverableEntryAlert(); + } + }, animTimeErrorMsgDelay); + } + } + + @Override + protected void validateAndSetStep(Step step) { + super.validateAndSetStep(step); + + if (!(step instanceof PasscodeStep)) { + throw new IllegalStateException("FingerprintStepLayout expects a FingerprintStep"); + } + + fingerprintStep = (PasscodeStep) step; + } + + protected FingerprintManagerCompat.AuthenticationCallback authCallbacks = new FingerprintManagerCompat.AuthenticationCallback() { + /** + * This is the error code for when the hardware shuts off due to too many attempts + * At this point the sensor will not work for some amount of time, + * so we should show the error message, and then kick the user from the screen + */ + static final int ERROR_CODE_TOO_MANY_ATTEMPTS = 7; + + /** This is called on another thread for some device manufacturers */ + @Override + @AnyThread + public void onAuthenticationError(final int errMsgId, final CharSequence errString) { + post(new Runnable() { + @Override + public void run() { + // Callback is not on the main thread, so send it to the main thread + removeCallbacks(fingerprintAnimationRunnable); + if (!mSelfCancelled) { + showError(errString); + } + if (errMsgId == ERROR_CODE_TOO_MANY_ATTEMPTS) { + // post delay to change the text back to hint text + removeCallbacks(fingerprintAnimationRunnable); + fingerprintAnimationRunnable = new Runnable() { + @Override + public void run() { + callbacks.onSaveStep(StepCallbacks.ACTION_END, fingerprintStep, null); + } + }; + postDelayed(fingerprintAnimationRunnable, animTimeTooManyAttemptsDelay); + } + } + }); + } + + /** This is called on another thread for some device manufacturers */ + @Override + @AnyThread + public void onAuthenticationHelp(final int helpMsgId, final CharSequence helpString) { + post(new Runnable() { + @Override + public void run() { + // Callback is not on the main thread, so send it to the main thread + removeCallbacks(fingerprintAnimationRunnable); + showError(helpString); + } + }); + } + + /** This is called on another thread for some device manufacturers */ + @Override + @AnyThread + public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { + post(new Runnable() { + @Override + public void run() { + // Callback is not on the main thread, so send it to the main thread + removeCallbacks(fingerprintAnimationRunnable); + refreshImage(ResUtils.ANIMATED_CHECK_MARK, true); + refreshDetailText( + getContext().getResources().getString(R.string.rsb_fingerprint_success), + getContext().getResources().getColor(R.color.rsb_success, null)); + + postDelayed(new Runnable() { + @Override + public void run() { + handleFingerprintSuccess(); + } + }, getResources().getInteger(R.integer.rsb_config_anim_time_check_mark)); + } + }); + } + + /** This is called on another thread for some device manufacturers */ + @Override + @AnyThread + public void onAuthenticationFailed() { + // Callback is not on the main thread, so send it to the main thread + post(new Runnable() { + @Override + public void run() { + removeCallbacks(fingerprintAnimationRunnable); + showError(getContext().getResources().getString(R.string.rsb_fingerprint_not_recognized)); + } + }); + } + }; + + protected void handleFingerprintSuccess() { + if (isCreationStep()) { + generateAndEncryptPin(); + saveIv(); // saves the IV for use in decryption + } else { + injectPin(); + } + stopListening(); + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, fingerprintStep, null); + } + + private void showUnrecoverableEntryAlert() { + // This is one of the only exceptions that means anything to us + // At this point we know that the user has changed or removed their fingerprint, + // And we will have to make them login again, same flow as forgot pincode + new AlertDialog.Builder(getContext()) + .setMessage(R.string.rsb_fingerprint_invalidated) + .setNegativeButton(R.string.rsb_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + callbacks.onCancelStep(); + } + }) + .create().show(); + } + + private void generateAndEncryptPin() { + String rawPin = KeystoreEncryptionHelper.generateSecureRandomPin(); + StorageAccess.getInstance().createPinCode(getContext(), rawPin); + + String encryptedBase64EncodedPin = KeystoreEncryptionHelper.encryptSecret(getContext(), cipher, rawPin); + StorageAccess.getInstance().setUsesFingerprint(getContext(), encryptedBase64EncodedPin); + } + + private void injectPin() { + String encryptedPincode = StorageAccess.getInstance().getFingerprint(getContext()); + String fingerprintPin = KeystoreEncryptionHelper.decryptPin(cipher, encryptedPincode); + StorageAccess.getInstance().authenticate(getContext(), fingerprintPin); + } + + public void startListening(FingerprintManagerCompat.CryptoObject cryptoObject) { + if (!KeystoreEncryptionHelper.isFingerprintAuthAvailable(fingerprintManager)) { + return; + } + cancellationSignal = new CancellationSignal(); + mSelfCancelled = false; + fingerprintManager.authenticate(cryptoObject, 0, cancellationSignal, authCallbacks, null); + } + + @Override + public void setCallbacks(StepCallbacks callbacks) { + super.setCallbacks(callbacks); + if (isCreationStep() && StorageAccess.getInstance().hasPinCode(getContext())) { + // A delay is needed so that the Displaying task has time to complete it's + // view rendering, and can properly handle the callback state + postDelayed(new Runnable() { + @Override + public void run() { + // We already have a pin code saved, do not let user create another, which will + // get everything in a bad state + callbacks.onSaveStep(StepCallbacks.ACTION_PREV, fingerprintStep, null); + } + }, DEFAULT_EXIT_REGISTRATION_DELAY_MILLIS); + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + removeCallbacks(fingerprintAnimationRunnable); + stopListening(); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + // Since we have created the fingerprint auth, it has been removed + if (StorageAccess.getInstance().usesFingerprint(getContext()) && + !KeystoreEncryptionHelper.isFingerprintAuthAvailable(fingerprintManager)) + { + showUnrecoverableEntryAlert(); + } + } + + public void stopListening() { + if (cancellationSignal != null) { + mSelfCancelled = true; + cancellationSignal.cancel(); + cancellationSignal = null; + } + } + + private void showError(CharSequence error) { + removeCallbacks(fingerprintAnimationRunnable); + refreshDetailText( + error.toString(), + getContext().getResources().getColor(R.color.rsb_error, null)); + refreshImage(ResUtils.ERROR_ICON, false); + + // post delay to change the text back to hint text + postDelayed(fingerprintAnimationRunnable, animTimeErrorMsgDelay); + } + + private void showUsePasscodeAlert() { + new AlertDialog.Builder(getContext()) + .setTitle(R.string.rsb_are_you_sure) + .setMessage(R.string.rsb_fingerprint_use_passcode) + .setNegativeButton(R.string.rsb_no, null) + .setPositiveButton(R.string.rsb_yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + fingerprintStep.setUseFingerprint(false); + callbacks.onSaveStep(StepCallbacks.ACTION_REFRESH, fingerprintStep, null); + } + }) + .create().show(); + } + + private void showLogoutAlert() { + new AlertDialog.Builder(getContext()) + .setTitle(R.string.rsb_are_you_sure) + .setNegativeButton(R.string.rsb_no, null) + .setPositiveButton(R.string.rsb_yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + callbacks.onCancelStep(); + } + }) + .create().show(); + } + + /** + * Should only be called after encryption cipher is initialized + */ + private void saveIv() { + String base64Iv = Base64.encodeToString(cipher.getIV(), Base64.DEFAULT); + getContext().getSharedPreferences(FINGERPRINT_IV_SHARED_PREFS, Context.MODE_PRIVATE) + .edit().putString(IV_SHARED_PREFS_KEY, base64Iv).apply(); + } + + /** + * Should only be used while decrypting + * @return the Initialization Vector (IV) used by the encryptor + */ + private byte[] loadIv() { + String base64Iv = getContext().getSharedPreferences(FINGERPRINT_IV_SHARED_PREFS, Context.MODE_PRIVATE) + .getString(IV_SHARED_PREFS_KEY, ""); + return Base64.decode(base64Iv, Base64.DEFAULT); + } + + private boolean isCreationStep() { + return fingerprintStep.getStateOrdinal() == PasscodeCreationStepLayout.State.CREATE.ordinal(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/FitnessStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/FitnessStepLayout.java new file mode 100644 index 000000000..617a21bc2 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/FitnessStepLayout.java @@ -0,0 +1,48 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.content.Context; +import android.util.AttributeSet; + +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.FitnessStep; + +/** + * Created by TheMDP on 2/16/17. + * + * This exists in iOS and the class is used to monitor certain special Recorders, + * in iOS these special recorders are HeartRate and Pedometer and they feed into HealthKit, + * But since there is not HealthKit equivalent on Android, I'm not sure this class should + * really exist + * + * TODO: potentially remove this class + */ + +public class FitnessStepLayout extends ActiveStepLayout { + + private FitnessStep fitnessStep; + + public FitnessStepLayout(Context context) { + super(context); + } + + public FitnessStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FitnessStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public FitnessStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void validateStep(Step step) { + if (!(step instanceof FitnessStep)) { + throw new IllegalStateException("FitnessStepLayout must have an FitnessStep"); + } + fitnessStep = (FitnessStep) step; + super.validateStep(step); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/FormStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/FormStepLayout.java new file mode 100644 index 000000000..735f66bf6 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/FormStepLayout.java @@ -0,0 +1,487 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Parcelable; +import androidx.annotation.IdRes; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.FormStep; +import org.researchstack.backbone.step.QuestionStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; +import org.researchstack.backbone.ui.step.body.BodyAnswer; +import org.researchstack.backbone.ui.step.body.StepBody; +import org.researchstack.backbone.ui.views.FixedSubmitBarLayout; +import org.researchstack.backbone.utils.LogExt; +import org.researchstack.backbone.utils.StepResultHelper; +import org.researchstack.backbone.utils.ViewUtils; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Created by TheMDP on 1/14/17. + */ + +public class FormStepLayout extends FixedSubmitBarLayout implements StepLayout { + private static final String TAG = FormStepLayout.class.getSimpleName(); + + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + // Data used to initializeLayout and return + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + protected FormStep formStep; + protected List subQuestionStepData; + protected StepResult stepResult; + + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + // Communicate w/ host + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + protected StepCallbacks callbacks; + + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + // Child Views + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + protected ViewGroup container; + protected LinearLayout stepBodyContainer; + protected TextView formTitleTextview; + protected TextView formSummaryTextview; + + public FormStepLayout(Context context) + { + super(context); + } + + public FormStepLayout(Context context, AttributeSet attrs) + { + super(context, attrs); + } + + public FormStepLayout(Context context, AttributeSet attrs, int defStyleAttr) + { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public FormStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void initialize(Step step) + { + initialize(step, null); + } + + @Override + public void initialize(Step step, StepResult result) + { + validateStepAndResult(step, result); // Also sets formStep member variable + + subQuestionStepData = new ArrayList<>(); + formStep = (FormStep) step; + + // Add all relevant questions steps + List questionSteps = new ArrayList<>(); + for (QuestionStep questionStep : formStep.getFormSteps()) { + questionSteps.add(questionStep); + } + + // Initialize the UI for title and summary, etc + initStepLayout(formStep); + // Fill up the step map + for (QuestionStep subStep : questionSteps) { + StepResult subStepResult = StepResultHelper.findStepResult(stepResult, subStep.getIdentifier()); + StepBody stepBody = SurveyStepLayout.createStepBody(subStep, subStepResult); + View surveyStepView = initStepBodyHolder(layoutInflater, stepBodyContainer, subStep, stepBody); + subQuestionStepData.add(new FormStepData(subStep, stepBody, surveyStepView)); + stepBodyContainer.addView(surveyStepView); + } + // refresh skip/next bar + refreshSubmitBar(); + setupEditTextImeOptions(); + } + + /** + * @param step to validate it's state + * @param stepResult step result to validate + */ + @SuppressWarnings("unchecked") // needed for StepResult cast + protected void validateStepAndResult(Step step, StepResult stepResult) { + if(step != null && step instanceof FormStep) + { + formStep = (FormStep)step; + + if (stepResult == null || stepResult.getResults() == null || stepResult.getResults().isEmpty()) { + this.stepResult = new StepResult<>(formStep); + } else { + for (Object resultObj : stepResult.getResults().values()) { + if (!(resultObj instanceof StepResult)) { + throw new RuntimeException("StepResult must be StepResult"); + } + } + this.stepResult = stepResult; + } + } + else + { + throw new RuntimeException("Step being used in FormStepLayout is not a FormStep or is null"); + } + } + + /** + * Assign the correct flow for next button to the next EditText + */ + protected void setupEditTextImeOptions() { + EditText firstEditText = null; + EditText nextEditText = null; + EditText previousEditText; + for (FormStepData stepData : subQuestionStepData) { + EditText editText = findEditText(stepData); + if (editText != null) { + if (firstEditText == null) { + firstEditText = editText; + } + previousEditText = nextEditText; + nextEditText = editText; + if (previousEditText != null) { + final EditText nextFocus = nextEditText; + previousEditText.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_NEXT) { + nextFocus.requestFocus(); + return true; + } + return false; + }); + } + } + } + // Assign the last EditText a Done button, which should automatically hide keyboard + if (nextEditText != null) { + nextEditText.setImeOptions(EditorInfo.IME_ACTION_DONE); + } + if (firstEditText != null && formStep.isAutoFocusFirstEditText()) { + focusKeyboard(firstEditText); + } + } + + protected void focusKeyboard(EditText onEditText) { + if (onEditText == null) { + return; + } + onEditText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); + } + } + + protected void hideKeyboard() { + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(getRootView().getWindowToken(), 0); + } + } + + /** + * @param stepData that contains a TextQuestionBody as the stepBody member variable + * @return EditText for this FormStepData if one exists and we can find it, null otherwise + */ + protected EditText findEditText(FormStepData stepData) { + if (stepData.view != null && stepData.view.get() != null) { + return ViewUtils.findFirstEditText(stepData.view.get()); + } + return null; + } + + @Override + public View getLayout() + { + return this; + } + + /** + * Method allowing a step to consume a back event. + * + * @return a boolean indication whether the back event is consumed + */ + @Override + public boolean isBackEventConsumed() { + if (formStep.isAutoFocusFirstEditText()) { + hideKeyboard(); + } + updateAllQuestionSteps(false); + callbacks.onSaveStep(StepCallbacks.ACTION_PREV, formStep, stepResult); + return false; + } + + @Override + public void setCallbacks(StepCallbacks callbacks) { + this.callbacks = callbacks; + } + + @Override + public int getContentResourceId() { + return R.layout.rsb_form_step_layout; + } + + public void refreshSubmitBar() { + if (submitBar == null) { + return; // custom layouts may not have a submit bar + } + + submitBar.setPositiveAction(v -> onNextClicked()); + + submitBar.setNegativeAction(v -> onSkipClicked()); + String skipTitle = skipButtonTitle(); + submitBar.setNegativeTitle(skipTitle); + submitBar.getNegativeActionView().setVisibility(skipTitle == null ? View.GONE : View.VISIBLE); + } + + /** + * @return null if skip should be hidden, a valid title otherwise + */ + protected String skipButtonTitle() { + boolean isSkipVisible = true; + // Check and see if any of the question steps are not optional + for (FormStepData stepData : subQuestionStepData) { + if (!stepData.step.isOptional()) { + isSkipVisible = false; + } + } + if (isSkipVisible) { + isSkipVisible = formStep.isOptional(); + } + if (!isSkipVisible) { + return null; + } + if (formStep.getSkipTitle() == null) { + return getString(R.string.rsb_step_skip); + } else { + return formStep.getSkipTitle(); + } + } + + /** + * @param step creates the root container for this form step layout + */ + protected void initStepLayout(FormStep step) + { + LogExt.i(getClass(), "initStepLayout()"); + + container = findViewById(R.id.rsb_form_step_content_container); + stepBodyContainer = findViewById(R.id.rsb_form_step_body_layout); + formTitleTextview = findViewById(getFormTitleId()); + formSummaryTextview = findViewById(getFormTextId()); + + SurveyStepLayout.setupTitleLayout(getContext(), step, formTitleTextview, formSummaryTextview); + } + + protected @IdRes int getFormTitleId() { + return R.id.rsb_form_step_title; + } + + protected @IdRes int getFormTextId() { + return R.id.rsb_form_step_summary; + } + + /** + * @param layoutInflater used to create StepLayout UI for QuestionStep + * @param stepBodyContainer container that will hold the returned View + * @param step the question step to use for the title and summary + * @param stepBody the step body to use for creating the step body view + * @return StepLayout View object container StepBody View and title, and text + */ + @NonNull + @MainThread + protected static View initStepBodyHolder(LayoutInflater layoutInflater, ViewGroup stepBodyContainer, QuestionStep step, StepBody stepBody) + { + LogExt.i(TAG, "initStepLayout()"); + + View surveyStepView = layoutInflater.inflate(R.layout.rsb_step_layout, stepBodyContainer, false); + + // Setup title and summary + TextView title = (TextView) surveyStepView.findViewById(R.id.rsb_survey_title); + TextView summary = (TextView) surveyStepView.findViewById(R.id.rsb_survey_text); + SurveyStepLayout.setupTitleLayout(layoutInflater.getContext(), step, title, summary); + + LinearLayout surveyStepContainer = (LinearLayout)surveyStepView.findViewById(R.id.rsb_survey_content_container); + View bodyView = stepBody.getBodyView(StepBody.VIEW_TYPE_DEFAULT, layoutInflater, surveyStepContainer); + SurveyStepLayout.replaceStepBodyView(surveyStepContainer, bodyView); + + return surveyStepView; + } + + @Override + public Parcelable onSaveInstanceState() + { + updateAllQuestionSteps(false); + callbacks.onSaveStep(StepCallbacks.ACTION_NONE, formStep, stepResult); + return super.onSaveInstanceState(); + } + + protected void updateAllQuestionSteps(boolean skipped) { + for (FormStepData stepData : subQuestionStepData) { + StepResult result = stepData.stepBody.getStepResult(skipped); + stepResult.getResults().put(stepData.step.getIdentifier(), result); + } + } + + protected void onNextClicked() + { + boolean isAnswerValid = isAnswerValid(true); + if (isAnswerValid) + { + if (formStep.isAutoFocusFirstEditText()) { + hideKeyboard(); + } + updateAllQuestionSteps(false); + onComplete(); + } + } + + protected void onComplete() { + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, formStep, stepResult); + } + + /** + * @param showErrorAlertOnInvalid if true, error toast is shown if return false, no toast otherwise + * @return true if subQuestionSteps steps are valid, false if one or more answers are invalid + */ + protected boolean isAnswerValid(boolean showErrorAlertOnInvalid) { + return isAnswerValid(subQuestionStepData, showErrorAlertOnInvalid, null); + } + + /** + * @param stepDataList the list of FormStepData to analyze if they are valid or not + * @param showErrorAlertOnInvalid if true, error toast is shown if return false, no toast otherwise + * @param identifierErrorMap key is the step identifier, value is the error message when it is not valid + * @return true if ALL question steps are valid, false if one or more answers are invalid + */ + protected boolean isAnswerValid(List stepDataList, boolean showErrorAlertOnInvalid, Map identifierErrorMap) { + boolean isAnswerValid = true; + List invalidReasons = new ArrayList<>(); + + for (FormStepData stepData : stepDataList) { + BodyAnswer bodyAnswer = stepData.stepBody.getBodyAnswerState(); + if (bodyAnswer == null || !bodyAnswer.isValid()) { + isAnswerValid = false; + + String invalidReason = null; + // This can override error messages to make it easier for the StepLayout + // to control the error message for the StepBody + if (identifierErrorMap != null) { + invalidReason = identifierErrorMap.get(stepData.step.getIdentifier()); + } + + // This is the main way to get an error message, it is based off of the StepBody + if (invalidReason == null) { + invalidReason = (bodyAnswer == null + ? BodyAnswer.INVALID.getString(getContext()) + : bodyAnswer.getString(getContext())); + } + + invalidReasons.add(invalidReason); + } + } + + if(!isAnswerValid && showErrorAlertOnInvalid) + { + String invalidReason = android.text.TextUtils.join(", ", invalidReasons) + "."; + Toast.makeText(getContext(), invalidReason, Toast.LENGTH_SHORT).show(); + } + return isAnswerValid; + } + + /** + * @param stepIdToFind finds Question step in subQuestionSteps with this String + * @return QuestionStep with stepIdToFind, or null if one does not exist + */ + protected FormStepData getFormStepData(String stepIdToFind) { + for (FormStepData stepData : subQuestionStepData) { + if (stepData.step.getIdentifier().equals(stepIdToFind)) { + return stepData; + } + } + return null; + } + + /** + * @param stepIdToFind uses step to match find step body + * @return null if stepIdToFind does not have a step with a corresponding StepBody in subQuestionsStep, + * the StepBody object matching stepIdToFind otherwise + */ + protected StepBody getStepBody(String stepIdToFind) { + for (FormStepData stepData : subQuestionStepData) { + if (stepData.step.getIdentifier().equals(stepIdToFind)) { + return stepData.stepBody; + } + } + return null; + } + + /** + * @param stepIdToFind finds Question step in subQuestionSteps with this String + * @return QuestionStep with stepIdToFind, or null if one does not exist + */ + protected QuestionStep getQuestionStep(String stepIdToFind) { + for (FormStepData stepData : subQuestionStepData) { + if (stepData.step.getIdentifier().equals(stepIdToFind)) { + return stepData.step; + } + } + return null; + } + + public void onSkipClicked() + { + if(callbacks != null) + { + updateAllQuestionSteps(true); + // empty step result when skipped + onComplete(); + } + } + + public FormStep getStep() { + return formStep; + } + + public String getString(@StringRes int stringResId) + { + return getResources().getString(stringResId); + } + + public class FormStepData { + protected QuestionStep step; + public QuestionStep getStep() { + return step; + } + protected StepBody stepBody; + public StepBody getStepBody() { + return stepBody; + } + public WeakReference view; + + FormStepData(QuestionStep step, StepBody stepBody, View view) { + this.step = step; + this.stepBody = stepBody; + this.view = new WeakReference<>(view); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/InstructionStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/InstructionStepLayout.java index 6e967df95..eb8f5419b 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/InstructionStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/InstructionStepLayout.java @@ -1,25 +1,50 @@ package org.researchstack.backbone.ui.step.layout; +import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; +import androidx.core.content.res.ResourcesCompat; import android.text.Html; import android.util.AttributeSet; +import android.util.Log; import android.view.View; +import android.widget.ImageView; import android.widget.TextView; import org.researchstack.backbone.R; import org.researchstack.backbone.ResourcePathManager; import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.InstructionStep; import org.researchstack.backbone.step.Step; import org.researchstack.backbone.ui.ViewWebDocumentActivity; import org.researchstack.backbone.ui.callbacks.StepCallbacks; import org.researchstack.backbone.ui.views.FixedSubmitBarLayout; -import org.researchstack.backbone.ui.views.SubmitBar; +import org.researchstack.backbone.utils.ResUtils; import org.researchstack.backbone.utils.TextUtils; public class InstructionStepLayout extends FixedSubmitBarLayout implements StepLayout { - private StepCallbacks callbacks; - private Step step; + + private static final String LOG_TAG = InstructionStepLayout.class.getCanonicalName(); + + protected StepCallbacks callbacks; + + protected InstructionStep instructionStep; + protected Step step; + + protected TextView titleTextView; + protected TextView textTextView; + protected ImageView imageView; + protected TextView moreDetailTextView; + + protected Handler mainHandler; + protected Runnable animationRepeatRunnbale; public InstructionStepLayout(Context context) { super(context); @@ -33,10 +58,28 @@ public InstructionStepLayout(Context context, AttributeSet attrs, int defStyleAt super(context, attrs, defStyleAttr); } + @TargetApi(21) + public InstructionStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + @Override public void initialize(Step step, StepResult result) { + validateAndSetStep(step); + connectStepUi( + R.id.rsb_instruction_title, + R.id.rsb_instruction_text, + R.id.rsb_image_view, + R.id.rsb_instruction_more_detail_text); + refreshStep(); + } + + protected void validateAndSetStep(Step step) { + if (!(step instanceof InstructionStep)) { + throw new IllegalStateException("InstructionStepLayout only works with InstructionStep"); + } + this.instructionStep = (InstructionStep)step; this.step = step; - initializeStep(); } @Override @@ -56,55 +99,198 @@ public void setCallbacks(StepCallbacks callbacks) { } @Override - public int getContentResourceId() { + public @LayoutRes int getContentResourceId() { return R.layout.rsb_step_layout_instruction; } - private void initializeStep() { + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mainHandler != null) { + mainHandler.removeCallbacksAndMessages(null); + } + } + + public void connectStepUi(@IdRes int titleRId, @IdRes int textRId, @IdRes int imageRId, @IdRes int detailRId) { + titleTextView = findViewById(titleRId); + textTextView = findViewById(textRId); + imageView = findViewById(imageRId); + moreDetailTextView = findViewById(detailRId); + } + + public void refreshStep() { if (step != null) { + String title = step.getTitle(); + String text = step.getText(); // Set Title - if (!TextUtils.isEmpty(step.getTitle())) { - TextView title = (TextView) findViewById(R.id.rsb_intruction_title); - title.setVisibility(View.VISIBLE); - title.setText(step.getTitle()); + if (! TextUtils.isEmpty(title)) { + titleTextView.setVisibility(View.VISIBLE); + titleTextView.setText(title); } // Set Summary - if (!TextUtils.isEmpty(step.getText())) { - TextView summary = (TextView) findViewById(R.id.rsb_intruction_text); - summary.setVisibility(View.VISIBLE); - summary.setText(Html.fromHtml(step.getText())); - summary.setMovementMethod(new TextViewLinkHandler() { - @Override - public void onLinkClick(String url) { - String path = ResourcePathManager.getInstance(). - generateAbsolutePath(ResourcePathManager.Resource.TYPE_HTML, url); - Intent intent = ViewWebDocumentActivity.newIntentForPath(getContext(), - step.getTitle(), - path); - getContext().startActivity(intent); - } - }); + if(! TextUtils.isEmpty(text)) { + textTextView.setVisibility(View.VISIBLE); + + // There is an odd bug where endlines do not show up with Html.fromHtml correctly, + // so we should use the old school text when we find one and assume it is not html, + // because html does not use "\n" it uses line breaks + if (text.contains("\n")) { + textTextView.setText(text); + } else { + textTextView.setText(Html.fromHtml(text)); + final String htmlDocTitle = title; + textTextView.setMovementMethod(new TextViewLinkHandler() { + @Override + public void onLinkClick(String url) { + String path = ResourcePathManager.getInstance(). + generateAbsolutePath(ResourcePathManager.Resource.TYPE_HTML, url); + Intent intent = ViewWebDocumentActivity.newIntentForPath( + getContext(), htmlDocTitle, path); + getContext().startActivity(intent); + } + }); + } } // Set Next / Skip - SubmitBar submitBar = (SubmitBar) findViewById(R.id.rsb_submit_bar); - submitBar.setPositiveTitle(R.string.rsb_next); - submitBar.setPositiveAction(v -> callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, - step, - null)); - - if (step.isOptional()) { - submitBar.setNegativeTitle(R.string.rsb_step_skip); - submitBar.setNegativeAction(v -> { - if (callbacks != null) { - callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, step, null); + if (submitBar != null) { + submitBar.setVisibility(View.VISIBLE); + submitBar.setPositiveTitle(R.string.rsb_next); + submitBar.setPositiveAction(v -> onComplete()); + + if (instructionStep.getSubmitBarNegativeActionSkipRule() != null) { + final InstructionStep.SubmitBarNegativeActionSkipRule rule = + instructionStep.getSubmitBarNegativeActionSkipRule(); + submitBar.setNegativeTitle(rule.getTitle()); + submitBar.setNegativeAction(v -> { + StepResult stepResult = new StepResult(step); + rule.onNegativeActionClicked(instructionStep, stepResult); + if (callbacks != null) { + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, step, stepResult); + } + }); + } else if (step.isOptional()) { + submitBar.setNegativeTitle(R.string.rsb_step_skip); + submitBar.setNegativeAction(v -> { + if (callbacks != null) { + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, step, null); + } + }); + } else { + submitBar.getNegativeActionView().setVisibility(View.GONE); + } + } + + refreshImage(instructionStep.getImage(), instructionStep.getIsImageAnimated()); + if (moreDetailTextView != null) { + refreshDetailText(instructionStep.getMoreDetailText(), moreDetailTextView.getCurrentTextColor()); + } + } + } + + protected void refreshImage(String imageName, boolean isAnimated) { + // Setup the Imageview, is compatible with normal, vector, and animated drawables + if (imageName != null) { + int drawableInt = ResUtils.getDrawableResourceId(getContext(), imageName); + if (drawableInt != 0) { + + // TODO: is there anyway to automatically check if an image is animatible + // TODO: other than setting a flag on the Step? + // TODO: catch exceptions maybe? + if (isAnimated) { + try { + // First, try and load the drawable as an animation-list + Drawable drawable = ResourcesCompat.getDrawable(getResources(), drawableInt, null); + if (drawable != null && drawable instanceof AnimationDrawable) { + AnimationDrawable animationDrawable = (AnimationDrawable)drawable; + imageView.setImageDrawable(animationDrawable); + animationDrawable.start(); + } else { + // This will trigger trying the animated vector compat code + throw new Resources.NotFoundException(); + } + } catch (Resources.NotFoundException notFoundException) { + // Animation was NOT an animation-list + // Try loading it as an animated vector drawable + try { + final AnimatedVectorDrawableCompat animatedVector = + AnimatedVectorDrawableCompat.create(getContext(), drawableInt); + imageView.setImageDrawable(animatedVector); + if (animatedVector != null) { + animatedVector.start(); + startAnimationRepeat(animatedVector); + } + } catch (ClassCastException castException) { + Log.e(LOG_TAG, "Could not parse animation drawable"); + } } - }); - } else { - submitBar.getNegativeActionView().setVisibility(View.GONE); + } else { + // TODO: check if above is needed, setImageResource may be sufficient + // https://developer.android.com/guide/topics/graphics/vector-drawable-resources.html + imageView.setImageResource(drawableInt); + } + + if (instructionStep.scaleType != null) { + imageView.setScaleType(instructionStep.scaleType); + } + + imageView.setVisibility(View.VISIBLE); } + } else { + imageView.setVisibility(View.GONE); } } + + protected void startAnimationRepeat(final AnimatedVectorDrawableCompat animatedVector) { + if (instructionStep.getAnimationRepeatDuration() > 0) { + if (mainHandler == null) { + mainHandler = new Handler(); + } + mainHandler.removeCallbacksAndMessages(null); + final long repeatDuration = instructionStep.getAnimationRepeatDuration(); + animationRepeatRunnbale = new Runnable() { + @Override + public void run() { + animatedVector.stop(); + animatedVector.start(); + mainHandler.postDelayed(animationRepeatRunnbale, repeatDuration); + } + }; + mainHandler.postDelayed(animationRepeatRunnbale, repeatDuration); + } + } + + protected void startAnimationRepeat(final AnimationDrawable animatedVector) { + if (instructionStep.getAnimationRepeatDuration() > 0) { + if (mainHandler == null) { + mainHandler = new Handler(); + } + mainHandler.removeCallbacksAndMessages(null); + final long repeatDuration = instructionStep.getAnimationRepeatDuration(); + animationRepeatRunnbale = new Runnable() { + @Override + public void run() { + animatedVector.stop(); + animatedVector.start(); + mainHandler.postDelayed(animationRepeatRunnbale, repeatDuration); + } + }; + mainHandler.postDelayed(animationRepeatRunnbale, repeatDuration); + } + } + + protected void refreshDetailText(String detailText, int detailTextColor) { + moreDetailTextView.setVisibility(detailText == null ? View.GONE : View.VISIBLE); + if (detailText != null) { + moreDetailTextView.setText(detailText); + } + moreDetailTextView.setTextColor(detailTextColor); + } + + protected void onComplete() { + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, step, null); + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/LoginStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/LoginStepLayout.java new file mode 100644 index 000000000..4af796612 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/LoginStepLayout.java @@ -0,0 +1,137 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.annotation.TargetApi; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.DataResponse; +import org.researchstack.backbone.R; +import org.researchstack.backbone.model.ProfileInfoOption; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.utils.ObservableUtils; +import org.researchstack.backbone.utils.StepLayoutHelper; + +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +import rx.Observable; + +/** + * Created by TheMDP on 1/14/17. + */ + +public class LoginStepLayout extends ProfileStepLayout { + public LoginStepLayout(Context context) { + super(context); + } + + public LoginStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LoginStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public LoginStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void initialize(Step step, StepResult result) + { + super.initialize(step, result); + + FormStepData emailStepData = getFormStepData(ProfileInfoOption.EMAIL.getIdentifier()); + if (emailStepData != null) { + // Add the Forgot Password UI below the login form + // Only add this if there is an Email step in the form. This might not be present if, + // for example, we are logging in using a method other than Email. + if (submitBar != null) { + submitBar.getNegativeActionView().setVisibility(View.VISIBLE); + submitBar.setNegativeTitle(R.string.rsb_forgot_password); + submitBar.setNegativeAction(v -> forgotPasswordClicked()); + } + } + } + + @Override + protected void onNextClicked() { + boolean isAnswerValid = isAnswerValid(subQuestionStepData, true); + if (isAnswerValid) { + showLoadingDialog(); + + final String email = getEmail(); + boolean hasEmail = email != null && !email.isEmpty(); + final String password = getPassword(); + boolean hasPassword = password != null && !password.isEmpty(); + final String externalId = getExternalId(); + boolean hasExternalId = externalId != null && !externalId.isEmpty(); + + Observable login; + if (hasEmail && hasPassword) { + // Login with email and password. + login = DataProvider.getInstance().signIn(getContext(), email, password); + } else if (hasEmail && !getProfileStep().getProfileInfoOptions().contains + (ProfileInfoOption.PASSWORD)) { + login = DataProvider.getInstance().requestSignInLink(email); + }else if (hasExternalId) { + // Login with external ID. + login = DataProvider.getInstance().signInWithExternalId(getContext(), externalId); + } else { + // This should never happen, but if it does, fail gracefully. + hideLoadingDialog(); + showOkAlertDialog("Unexpected error: No credentials provided."); + return; + } + + // Only gives a callback to response on success, the rest is handled by StepLayoutHelper + StepLayoutHelper.safePerform(login, this, new StepLayoutHelper.WebCallback() { + @Override + public void onSuccess(DataResponse response) { + hideLoadingDialog(); + LoginStepLayout.super.onNextClicked(); + } + + @Override + public void onFail(Throwable throwable) { + hideLoadingDialog(); + // TODO: use the status code instead of this string + if (throwable instanceof UnknownHostException) { + // This is likely a no internet connection error + LoginStepLayout.super.showOkAlertDialog(getString(R.string.rsb_error_no_internet)); + } else if (throwable.toString().contains("statusCode=412")) { + // Moving to the next step will trigger the re-consent flow + // Since the user is not consented, but signed in successfully + LoginStepLayout.super.onNextClicked(); + } else { + showOkAlertDialog(throwable.getMessage()); + } + } + }); + } + } + + protected void forgotPasswordClicked() { + // Forgot password button only needs a valid email + List validSteps = new ArrayList<>(); + validSteps.add(getFormStepData(ProfileInfoOption.EMAIL.getIdentifier())); + boolean isEmailValid = isAnswerValid(validSteps, true); + if (isEmailValid) { + + Observable forgotPassword = DataProvider.getInstance() + .forgotPassword(getContext(), getEmail()) + .compose(ObservableUtils.applyDefault()); + + // Only gives a callback to response on success, the rest is handled by StepLayoutHelper + StepLayoutHelper.safePerformWithAlerts(forgotPassword, this, response -> + showOkAlertDialog(response.getMessage()) + ); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/OnboardingCompletionStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/OnboardingCompletionStepLayout.java new file mode 100644 index 000000000..5b45cf86e --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/OnboardingCompletionStepLayout.java @@ -0,0 +1,35 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.annotation.TargetApi; +import android.content.Context; +import android.util.AttributeSet; + + +/** + * Created by TheMDP on 1/18/17. + */ + +public class OnboardingCompletionStepLayout extends InstructionStepLayout { + public OnboardingCompletionStepLayout(Context context) { + super(context); + } + + public OnboardingCompletionStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public OnboardingCompletionStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public OnboardingCompletionStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onComplete() { + // TODO: make a hook to control where the user goes after onboarding is complete + super.onComplete(); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/PasscodeCreationStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/PasscodeCreationStepLayout.java new file mode 100644 index 000000000..546357e7f --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/PasscodeCreationStepLayout.java @@ -0,0 +1,207 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Handler; +import androidx.core.content.ContextCompat; +import android.util.AttributeSet; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import com.jakewharton.rxbinding.widget.RxTextView; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.StorageAccess; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.PasscodeStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; +import org.researchstack.backbone.ui.views.PinCodeLayout; +import org.researchstack.backbone.utils.ThemeUtils; + +public class PasscodeCreationStepLayout extends PinCodeLayout implements StepLayout +{ + public static final String RESULT_OLD_PIN = "PassCodeCreationStep.oldPin"; + + protected StepCallbacks callbacks; + protected PasscodeStep step; + protected StepResult result; + + private CharSequence currentPin = null; + private State state = State.CREATE; + + public enum State + { + CHANGE, + CREATE, + CONFIRM, + RETRY + } + + public PasscodeCreationStepLayout(Context context) + { + super(context); + } + + public PasscodeCreationStepLayout(Context context, AttributeSet attrs) + { + super(context, attrs); + } + + public PasscodeCreationStepLayout(Context context, AttributeSet attrs, int defStyleAttr) + { + super(context, attrs, defStyleAttr); + } + + @Override + public void initialize(Step step, StepResult result) + { + this.step = (PasscodeStep) step; + this.result = result == null ? new StepResult<>(step) : result; + + if(this.step.getStateOrdinal() != - 1) + { + this.state = State.values()[this.step.getStateOrdinal()]; + } + + initializeLayout(); + } + + private void initializeLayout() + { + refreshState(); + + RxTextView.textChanges(editText) + .map(CharSequence:: toString) + .filter(pin -> pin.length() == config.getPinLength()) + .subscribe(pin -> { + if(state == State.CHANGE) + { + result.setResultForIdentifier(RESULT_OLD_PIN, pin); + + currentPin = pin; + editText.setText(""); + state = State.CREATE; + refreshState(); + } + else if(state == State.CREATE) + { + currentPin = pin; + editText.setText(""); + state = State.CONFIRM; + refreshState(); + } + else + { + if(pin.equals(currentPin)) + { + new Handler().postDelayed(() -> { + imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); + result.setResult(pin); + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, step, result); + + if (state == State.CONFIRM) { + StorageAccess.getInstance().createPinCode(getContext(), pin); + } + }, 300); + } + else + { + state = State.RETRY; + editText.setText(""); + refreshState(); + } + } + }); + + editText.post(() -> imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, + InputMethodManager.HIDE_IMPLICIT_ONLY)); + } + + @Override + public boolean isBackEventConsumed() + { + if(state == State.CREATE) + { + callbacks.onSaveStep(StepCallbacks.ACTION_PREV, step, null); + imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); + } + else + { + // pressed back while confirming, go back to creation state + currentPin = null; + editText.setText(""); + state = State.CREATE; + refreshState(); + } + return true; + } + + @Override + public View getLayout() + { + return this; + } + + @Override + public void setCallbacks(StepCallbacks callbacks) + { + this.callbacks = callbacks; + } + + private void refreshState() + { + String pinCodeTitle; + String pinCodeInstructions; + int summaryColor; + + Resources res = getResources(); + int pinLength = config.getPinLength(); + String characterType = res.getString(config.getPinType().getInputTypeStringId()); + switch(state) + { + case CONFIRM: + pinCodeTitle = res.getString(R.string.rsb_passcode_confirm_title); + pinCodeInstructions = res.getString(R.string.rsb_passcode_confirm_summary, + pinLength, + characterType); + summaryColor = ThemeUtils.getTextColorPrimary(getContext()); + break; + + case RETRY: + pinCodeTitle = res.getString(R.string.rsb_passcode_confirm_title); + pinCodeInstructions = res.getString(R.string.rsb_passcode_confirm_error, + pinLength, + characterType); + summaryColor = ContextCompat.getColor(getContext(), R.color.rsb_error); + break; + + case CREATE: + default: + pinCodeTitle = res.getString(R.string.rsb_passcode_create_title); + pinCodeInstructions = res.getString(R.string.rsb_passcode_create_summary, + pinLength, + characterType); + summaryColor = ThemeUtils.getTextColorPrimary(getContext()); + break; + + case CHANGE: + pinCodeTitle = res.getString(R.string.rsb_pincode_enter_title); + pinCodeInstructions = res.getString(R.string.rsb_pincode_enter_summary, + pinLength, + characterType); + summaryColor = ThemeUtils.getTextColorPrimary(getContext()); + break; + } + + updateText(pinCodeTitle, pinCodeInstructions, summaryColor); + } + + private void updateText(String titleString, String textString, int color) + { + title.setText(titleString); + summary.setText(textString); + summary.setTextColor(color); + } + +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/PermissionStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/PermissionStepLayout.java new file mode 100644 index 000000000..0bb647a81 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/PermissionStepLayout.java @@ -0,0 +1,216 @@ +package org.researchstack.backbone.ui.step.layout; +import android.content.Context; +import android.graphics.drawable.Drawable; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.jakewharton.rxbinding.view.RxView; + +import org.researchstack.backbone.PermissionRequestManager; +import org.researchstack.backbone.R; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.ui.callbacks.ActivityCallback; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; +import org.researchstack.backbone.ui.views.SubmitBar; +import org.researchstack.backbone.utils.ThemeUtils; + +import java.util.List; + +public class PermissionStepLayout extends LinearLayout implements StepLayout, StepPermissionRequest +{ + private Step step; + private StepResult result; + private StepCallbacks callbacks; + private ActivityCallback permissionCallback; + + private SubmitBar submitBar; + + public PermissionStepLayout(Context context) + { + this(context, null); + } + + public PermissionStepLayout(Context context, AttributeSet attrs) + { + this(context, attrs, 0); + } + + public PermissionStepLayout(Context context, AttributeSet attrs, int defStyleAttr) + { + super(context, attrs, defStyleAttr); + setOrientation(VERTICAL); + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + + if(getContext() instanceof ActivityCallback) + { + permissionCallback = (ActivityCallback) getContext(); + } + } + + @Override + protected void onDetachedFromWindow() + { + super.onDetachedFromWindow(); + + permissionCallback = null; + callbacks = null; + } + + @Override + public void initialize(Step step, StepResult result) + { + this.step = step; + this.result = result == null ? new StepResult<>(step) : result; + + initializeStep(); + } + + public void initializeStep() + { + LayoutInflater inflater = LayoutInflater.from(getContext()); + + // Inflate step UI + inflater.inflate(R.layout.rsb_layout_permission, this, true); + + // Add Sub-items to our ScrollView + LinearLayout permissionContainer = (LinearLayout) findViewById(R.id.rsb_container_permission_items); + + List items = PermissionRequestManager.getInstance() + .getPermissionRequests(); + + for(PermissionRequestManager.PermissionRequest item : items) + { + boolean isGranted = PermissionRequestManager.getInstance().hasPermission(getContext(), item.getId()); + + View child = inflater.inflate(R.layout.rsb_item_permission_content, + permissionContainer, + false); + + // Set tag to update action-state in the future + child.setTag(item.getId()); + + // Set Icon w/ accentTint + Drawable icon = ContextCompat.getDrawable(getContext(), item.getIcon()); + icon = DrawableCompat.wrap(icon); + DrawableCompat.setTint(icon, ThemeUtils.getAccentColor(getContext())); + ((ImageView) child.findViewById(R.id.rsb_permission_icon)).setImageDrawable(icon); + + // Set title + ((TextView) child.findViewById(R.id.rsb_permission_title)).setText(item.getTitle()); + + // Set details + ((TextView) child.findViewById(R.id.rsb_permission_details)).setText(item.getText()); + + // Text action + TextView action = (TextView) child.findViewById(R.id.rsb_permission_button); + action.setText(isGranted + ? R.string.rsb_granted + : item.isBlockingPermission() ? R.string.rsb_allow : R.string.rsb_optional); + RxView.clicks(action).subscribe(o -> { + permissionCallback.onRequestPermission(item.getId()); + }); + action.setEnabled(!isGranted); + + permissionContainer.addView(child); + } + + // Set submit bar behavior + submitBar = (SubmitBar) findViewById(R.id.submit_bar); + submitBar.setPositiveTitle(R.string.rsb_next); + submitBar.setPositiveAction(v -> { + if (isAnswerValid()) + { + onNext(true); + } + }); + submitBar.getNegativeActionView().setVisibility(GONE); + } + + @Override + public void onUpdateForPermissionResult() + { + updatePermissionItems(); + } + + private void updatePermissionItems() + { + List items = PermissionRequestManager.getInstance() + .getPermissionRequests(); + + for(PermissionRequestManager.PermissionRequest item : items) + { + boolean isGranted = PermissionRequestManager.getInstance().hasPermission(getContext(), item.getId()); + + View parent = findViewWithTag(item.getId()); + + TextView action = (TextView) parent.findViewById(R.id.rsb_permission_button); + action.setText(isGranted + ? R.string.rsb_granted + : item.isBlockingPermission() ? R.string.rsb_allow : R.string.rsb_optional); + action.setEnabled(!isGranted); + } + } + + private void onNext(boolean answerCorrect) + { + // Save the result and go to the next question + result.setResult(answerCorrect); + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, step, result); + } + + public boolean isAnswerValid() + { + List items = PermissionRequestManager.getInstance() + .getPermissionRequests(); + + for(PermissionRequestManager.PermissionRequest item : items) + { + boolean isGranted = PermissionRequestManager.getInstance().hasPermission(getContext(), item.getId()); + + if (!isGranted && item.isBlockingPermission()) + { + String permissionName = getResources().getString(item.getTitle()); + String formattedError = getResources().getString( + R.string.rsb_permission_continue_invalid, permissionName.toLowerCase()); + Toast.makeText(getContext(), formattedError, Toast.LENGTH_SHORT).show(); + return false; + } + } + + return true; + } + + @Override + public View getLayout() + { + return this; + } + + @Override + public boolean isBackEventConsumed() + { + callbacks.onSaveStep(StepCallbacks.ACTION_PREV, step, result); + return false; + } + + @Override + public void setCallbacks(StepCallbacks callbacks) + { + this.callbacks = callbacks; + } + +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ProfileStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ProfileStepLayout.java new file mode 100644 index 000000000..ec443acad --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ProfileStepLayout.java @@ -0,0 +1,258 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.content.Context; +import androidx.annotation.Nullable; +import android.util.AttributeSet; +import android.util.Log; + +import org.researchstack.backbone.DataProvider; +import org.researchstack.backbone.R; +import org.researchstack.backbone.model.ProfileInfoOption; +import org.researchstack.backbone.model.User; +import org.researchstack.backbone.model.survey.factory.SurveyFactory; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.ProfileStep; +import org.researchstack.backbone.step.QuestionStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.ui.step.body.StepBody; +import org.researchstack.backbone.utils.StepHelper; +import org.researchstack.backbone.utils.StepResultHelper; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by TheMDP on 1/14/17. + * + * ProfileStepLayout is used to display fields that relate to a user's profile + * and the QuestionSteps were created from + * see {@link org.researchstack.backbone.model.ProfileInfoOption} objects, + * which can be found in the @see {@link org.researchstack.backbone.step.ProfileStep} + */ + +public class ProfileStepLayout extends FormStepLayout { + + protected User user; + + /** Used to map steps identifiers to error messages */ + protected Map identifierErrorMap; + + public ProfileStepLayout(Context context) { + super(context); + } + + public ProfileStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ProfileStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public ProfileStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + protected ProfileStep getProfileStep() { + if (!(formStep instanceof ProfileStep)) { + throw new IllegalStateException("ProfileStepLayout must contain a ProfileStep"); + } + return (ProfileStep)formStep; + } + + @Override + public void initialize(Step step, StepResult result) { + validateStepAndResult(step, result); // also sets formStep variable + initializeErrorMap(); + // needed to have object passed in by reference in method below + if (result == null) { + result = new StepResult(step); + } + prePopulateUserProfileResults(stepResult); + super.initialize(step, result); + } + + protected void initializeErrorMap() { + identifierErrorMap = new HashMap<>(); + identifierErrorMap.put(ProfileInfoOption.NAME.getIdentifier(), getString(R.string.rsb_error_invalid_name)); + identifierErrorMap.put(ProfileInfoOption.EMAIL.getIdentifier(), getString(R.string.rsb_error_invalid_email)); + identifierErrorMap.put(ProfileInfoOption.PASSWORD.getIdentifier(), getString(R.string.rsb_error_invalid_password)); + // TODO: add the rest of the user profile error messages for options + } + + /** + * @param result to add pre-populated user profile results that are create from the User object + */ + protected void prePopulateUserProfileResults(StepResult result) { + user = DataProvider.getInstance().getUser(getContext()); + if (user == null) { + user = new User(); // first time controlling the user object + } + for (ProfileInfoOption option : getProfileStep().getProfileInfoOptions()) { + // Look to see if the step result for this profile option already exists + StepResult profileResult = StepResultHelper.findStepResult(result, option.getIdentifier()); + // If it doesn't exist, create one matching the profile info option type from User object + if (profileResult == null) { + Step profileStep = StepHelper.getStepWithIdentifier(formStep.getFormSteps(), option.getIdentifier()); + StepResult stepResult = null; + if (profileStep != null) { + switch (option) { + case NAME: + if (user.getName() != null) { + StepResult nameResult = new StepResult<>(profileStep); + nameResult.setResult(user.getName()); + stepResult = nameResult; + } + break; + case EMAIL: + if (user.getEmail() != null) { + StepResult nameResult = new StepResult<>(profileStep); + nameResult.setResult(user.getEmail()); + stepResult = nameResult; + } + break; + case BIRTHDATE: + if (user.getBirthDate() != null) { + StepResult nameResult = new StepResult<>(profileStep); + nameResult.setResult(user.getBirthDate().getTime()); + stepResult = nameResult; + } + break; + } + } + // Add the step result to our result object so that the profile step bodies will + // be pre-populated with the designated user's profile info + if (stepResult != null) { + result.setResultForIdentifier(stepResult.getIdentifier(), stepResult); + } + } + } + } + + @Override + protected void onNextClicked() + { + boolean isAnswerValid = isAnswerValid(true); + if (isAnswerValid) + { + // Profile will be updated + for (ProfileInfoOption option : getProfileStep().getProfileInfoOptions()) { + switch (option) { + case NAME: + user.setName(getName()); + break; + case EMAIL: + user.setEmail(getEmail()); + break; + case BIRTHDATE: + user.setBirthDate(getBirthdate()); + break; + } + } + try { + DataProvider.getInstance().setUser(getContext(), user); + } catch (NullPointerException nullException) { + Log.d(getClass().getCanonicalName(), "Encryption Data Provider is not initialized yet" + + "this means you are trying to save user data before the user has" + + "registered or logged in"); + } + super.onNextClicked(); + } + } + + protected boolean isAnswerValid(List stepDataList, boolean showErrorAlertOnInvalid) { + return super.isAnswerValid(stepDataList, showErrorAlertOnInvalid, identifierErrorMap); + } + + /** + * @return Name if this profile form step has it, null otherwise + */ + @Nullable + protected String getName() { + return getTextAnswer(ProfileInfoOption.NAME.getIdentifier()); + } + + /** + * @return Email if this profile form step has it, null otherwise + */ + @Nullable + protected String getEmail() { + return getTextAnswer(ProfileInfoOption.EMAIL.getIdentifier()); + } + + /** + * @return Email QuestionStep if this profile form step has it, null otherwise + */ + @Nullable + protected QuestionStep getEmailStep() { + return getQuestionStep(ProfileInfoOption.EMAIL.getIdentifier()); + } + + /** + * @return External ID if this profile form step has it, null otherwise + */ + @Nullable + protected String getExternalId() { + return getTextAnswer(ProfileInfoOption.EXTERNAL_ID.getIdentifier()); + } + + /** + * @return Password if this profile form step has it, null otherwise + */ + @Nullable + protected String getPassword() { + return getTextAnswer(ProfileInfoOption.PASSWORD.getIdentifier()); + } + + /** + * @return Confirm Password if this profile form step has it, null otherwise + */ + @Nullable + protected String getConfirmPassword() { + return getTextAnswer(SurveyFactory.PASSWORD_CONFIRMATION_IDENTIFIER); + } + + /** + * @return User's birthday if this profile form step has it, null otherwise + */ + @Nullable + protected Date getBirthdate() { + return getDateAnswer(ProfileInfoOption.BIRTHDATE.getIdentifier()); + } + + /** + * @param stepIdentifier the identifier for the step + * @return String answer of step body, null if one doesn't exist or it is not a String + */ + @Nullable + protected String getTextAnswer(String stepIdentifier) { + Object result = findStepResult(stepIdentifier); + if (result != null && result instanceof String) { + return (String)result; + } + return null; + } + + /** + * @param stepIdentifier the identifier for the step + * @return Date answer of step body, null if one doesn't exist or it is not a Date + */ + @Nullable + protected Date getDateAnswer(String stepIdentifier) { + Object result = findStepResult(stepIdentifier); + if (result != null && result instanceof Long) { + return new Date((Long)result); + } + return null; + } + + protected Object findStepResult(String stepIdentifier) { + StepBody matchingStepBody = getStepBody(stepIdentifier); + if (matchingStepBody != null) { + return matchingStepBody.getStepResult(false).getResult(); + } + return null; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/RegistrationStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/RegistrationStepLayout.java new file mode 100644 index 000000000..77e21aba4 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/RegistrationStepLayout.java @@ -0,0 +1,69 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.annotation.TargetApi; +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Toast; + +import org.researchstack.backbone.DataProvider; + +import org.researchstack.backbone.DataResponse; +import org.researchstack.backbone.R; +import org.researchstack.backbone.utils.ObservableUtils; +import org.researchstack.backbone.utils.StepLayoutHelper; + +import rx.Observable; + +/** + * Created by TheMDP on 1/15/17. + */ + +public class RegistrationStepLayout extends ProfileStepLayout { + public RegistrationStepLayout(Context context) { + super(context); + } + + public RegistrationStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RegistrationStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public RegistrationStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public boolean isBackEventConsumed() { + return true; // can't move backwards from this step layout + } + + @Override + protected void onNextClicked() { + boolean isAnswerValid = isAnswerValid(subQuestionStepData, true); + if (isAnswerValid) { + + final String email = getEmail(); + final String password = getPassword(); + final String confirmPassword = getConfirmPassword(); + + if (password != null && !password.equals(confirmPassword)) { + Toast.makeText(getContext(), getString(R.string.rsb_error_passwords_do_not_match), Toast.LENGTH_SHORT).show(); + return; + } + + Observable registration = DataProvider.getInstance() + // As of right now, username is unused in and email is only supported + .signUp(getContext(), email, email, password) + .compose(ObservableUtils.applyDefault()); + + // Only gives a callback to response on success, the rest is handled by StepLayoutHelper + StepLayoutHelper.safePerformWithAlerts(registration, this, response -> + super.onNextClicked() + ); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/RequireSystemFeatureStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/RequireSystemFeatureStepLayout.java new file mode 100644 index 000000000..4da8a8e14 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/RequireSystemFeatureStepLayout.java @@ -0,0 +1,91 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.content.Context; +import android.content.Intent; +import android.location.LocationManager; +import android.util.AttributeSet; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.RequireSystemFeatureStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; + +import rx.functions.Action1; + +/** + * Created by TheMDP on 2/21/17. + */ + +public class RequireSystemFeatureStepLayout extends InstructionStepLayout { + + protected boolean shouldGoNextOnCallbacksSet; + protected RequireSystemFeatureStep systemFeatureStep; + + public RequireSystemFeatureStepLayout(Context context) { + super(context); + } + + public RequireSystemFeatureStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RequireSystemFeatureStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public RequireSystemFeatureStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void initialize(Step step, StepResult result) { + super.initialize(step, result); + updateSystemFeatureStatus(); + } + + @Override + protected void validateAndSetStep(Step step) { + super.validateAndSetStep(step); + + if (!(step instanceof RequireSystemFeatureStep)) { + throw new IllegalStateException("RequireSystemFeatureStepLayout only works with RequireSystemFeatureStep"); + } + + systemFeatureStep = (RequireSystemFeatureStep)step; + } + + @Override + public void setCallbacks(StepCallbacks callbacks) { + super.setCallbacks(callbacks); + if (shouldGoNextOnCallbacksSet) { + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, systemFeatureStep, null); + } + } + + public void updateSystemFeatureStatus() { + if (systemFeatureStep.getSystemFeature() == RequireSystemFeatureStep.SystemFeature.GPS) { + LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE); + + if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + if (callbacks != null) { + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, systemFeatureStep, null); + } else { + shouldGoNextOnCallbacksSet = true; + } + } else { + submitBar.setPositiveTitle(R.string.rsb_enable); + submitBar.setPositiveAction(new Action1() { + @Override + public void call(Object o) { + Intent gpsOptionsIntent = new Intent( + android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); + getContext().startActivity(gpsOptionsIntent); + } + }); + } + } else { + throw new IllegalStateException("No other System feature supported besides GPS at the moment"); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ShareTheAppStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ShareTheAppStepLayout.java new file mode 100644 index 000000000..9350476d8 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ShareTheAppStepLayout.java @@ -0,0 +1,308 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.graphics.PorterDuff; +import android.net.Uri; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.ShareTheAppStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; +import org.researchstack.backbone.ui.views.FixedSubmitBarLayout; +import org.researchstack.backbone.utils.ResUtils; +import org.researchstack.backbone.utils.TextUtils; +import org.researchstack.backbone.utils.ThemeUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 1/26/17. + */ + +public class ShareTheAppStepLayout extends FixedSubmitBarLayout implements StepLayout { + + protected static final String FACEBOOK_SHARE_URL = "https://www.facebook.com/sharer/sharer.php?u="; + protected static final String TWITTER_SHARE_URL = "https://twitter.com/intent/tweet?source=webclient&text="; + protected static final String SMS_MIME_TYPE = "vnd.android-dir/mms-sms"; + protected static final String TEXT_MIME_TYPE = "text/plain"; + protected static final String SMS_BODY_KEY = "sms_body"; + protected static final String MAILTO_SCHEME = "mailto"; + + protected TextView titleTextView; + protected TextView textTextView; + protected RecyclerView recyclerView; + + protected ShareTheAppStep step; + protected StepCallbacks callbacks; + + public ShareTheAppStepLayout(Context context) { + super(context); + } + + public ShareTheAppStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ShareTheAppStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public ShareTheAppStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public int getContentResourceId() { + return R.layout.rsb_step_layout_share_the_app; + } + + @Override + public void initialize(Step step, StepResult result) { + validateStep(step); + + titleTextView = (TextView)contentContainer.findViewById(R.id.rsb_share_title_view); + titleTextView.setText(step.getTitle()); + textTextView = (TextView)contentContainer.findViewById(R.id.rsb_share_view_text); + textTextView.setText(step.getText()); + + ImageView logoView = (ImageView)contentContainer.findViewById(R.id.rsb_share_logo_view); + // look for a logo to show, otherwise hide it + int logoId = ResUtils.getDrawableResourceId(getContext(), ResUtils.LOGO_DISEASE); + if(logoId > 0) { + logoView.setImageResource(logoId); + logoView.setVisibility(View.VISIBLE); + } else { + logoView.setVisibility(View.GONE); + } + + recyclerView = (RecyclerView)contentContainer.findViewById(R.id.rsb_share_recycler_view); + recyclerView.setAdapter(new ShareAdapter(getContext(), loadItems())); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + + submitBar.setPositiveTitle(R.string.rsb_done); + submitBar.setPositiveAction(o -> { callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, step, null); }); + submitBar.getNegativeActionView().setVisibility(View.GONE); + } + + protected void validateStep(Step step) { + if (!(step instanceof ShareTheAppStep)) { + throw new IllegalStateException("ShareTheAppStepLayout only works with ShareTheAppStep"); + } + this.step = (ShareTheAppStep)step; + } + + @Override + public View getLayout() { + return this; + } + + @Override + public boolean isBackEventConsumed() { + callbacks.onSaveStep(StepCallbacks.ACTION_PREV, step, null); + return true; + } + + @Override + public void setCallbacks(StepCallbacks callbacks) { + this.callbacks = callbacks; + } + + protected List loadItems() { + List items = new ArrayList<>(); + + ShareItem twitter = new ShareItem(ResUtils.TWITTER_ICON, + getContext().getString(R.string.rsb_share_twitter), ShareTheAppStep.ShareType.TWITTER); + items.add(twitter); + + ShareItem facebook = new ShareItem(ResUtils.FACEBOOK_ICON, + getContext().getString(R.string.rsb_share_facebook), ShareTheAppStep.ShareType.FACEBOOK); + items.add(facebook); + + ShareItem sms = new ShareItem(ResUtils.SMS_ICON, + getContext().getString(R.string.rsb_share_sms), ShareTheAppStep.ShareType.SMS); + items.add(sms); + + ShareItem email = new ShareItem(ResUtils.EMAIL_ICON, + getContext().getString(R.string.rsb_share_email), ShareTheAppStep.ShareType.EMAIL); + items.add(email); + + return items; + } + + private class ShareItem { + public String icon; + public String text; + public ShareTheAppStep.ShareType type; + + private ShareItem(String i, String t, ShareTheAppStep.ShareType ty) { + icon = i; + text = t; + type = ty; + } + } + + public class ShareAdapter extends RecyclerView.Adapter { + + private Context context; + private List items; + private LayoutInflater inflater; + + private ShareAdapter(Context ctx, List itemList) { + super(); + context = ctx; + items = itemList; + this.inflater = LayoutInflater.from(context); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = inflater.inflate(R.layout.rsb_row_icon_text, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder hldr, int position) { + + ViewHolder holder = (ViewHolder) hldr; + ShareItem item = items.get(position); + + holder.title.setText(item.text); + + int resId = ResUtils.getDrawableResourceId(context, item.icon); + holder.icon.setImageResource(resId); + + // use accent color for the icons + int colorId = ThemeUtils.getAccentColor(context); + holder.icon.setColorFilter(colorId, PorterDuff.Mode.SRC_IN); + + holder.itemView.setOnClickListener(v -> { + Intent intent = null; + String message = context.getString(R.string.rsb_share_the_app_message); + switch(item.type) { + case TWITTER: + intent = getShareTwitterIntent(message); + break; + case FACEBOOK: + intent = getShareFacebookIntent(); + break; + case SMS: + intent = getShareSmsIntent(message); + break; + case EMAIL: + intent = getShareEmailIntent(message); + break; + } + + v.getContext().startActivity(intent); + + }); + + } + + @Override + public int getItemCount() { + return items.size(); + } + + private class ViewHolder extends RecyclerView.ViewHolder { + TextView title; + ImageView icon; + + private ViewHolder(View itemView) { + super(itemView); + title = (TextView) itemView.findViewById(R.id.rsb_share_item_title); + icon = (ImageView) itemView.findViewById(R.id.rsb_share_item_icon); + } + } + } + + /** + * Return an Intent for sharing by email. + * + * @param message The message to share. + * @return The share intent. + */ + protected Intent getShareEmailIntent(String message) { + Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts( + MAILTO_SCHEME, "", null)); + intent.putExtra(Intent.EXTRA_SUBJECT, getContext().getString(R.string.rsb_share_email_subject)); + intent.putExtra(Intent.EXTRA_TEXT, message); + return intent; + } + + /** + * Return an intent for sharing by SMS. + * + * @param message The message to share. + * @return The share intent. + */ + protected Intent getShareSmsIntent(String message) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setType(SMS_MIME_TYPE); + intent.putExtra(SMS_BODY_KEY,message); + return intent; + } + + /** + * Return an Intent for sharing by Twitter. + * + * @param message The message to share. + * @return The share intent. + */ + protected Intent getShareTwitterIntent(String message) { + String url = TWITTER_SHARE_URL + TextUtils.urlEncode(message); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + return intent; + } + + /** + * Return an intent for sharing by Facebook. + * Facebook API does not let you post a message to share, user must type it themselves + * @return The share intent. + */ + protected Intent getShareFacebookIntent() { + String urlToShare = getContext().getString(R.string.rsb_share_app_url); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(TEXT_MIME_TYPE); + intent.putExtra(Intent.EXTRA_TEXT, urlToShare); + + // See if official Facebook app is found + boolean facebookAppFound = false; + List matches = getContext().getPackageManager().queryIntentActivities(intent, 0); + for (ResolveInfo info : matches) + { + String facebookKey = getContext().getString(R.string.rsb_share_facebook_key); + if (info.activityInfo.packageName.toLowerCase().contains(facebookKey) || + info.activityInfo.name.toLowerCase().contains(facebookKey)) + { + intent.setPackage(info.activityInfo.packageName); + facebookAppFound = true; + break; + } + } + + // As fallback, launch sharer.php in a browser + if (!facebookAppFound) + { + String sharerUrl = FACEBOOK_SHARE_URL + urlToShare; + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(sharerUrl)); + } + + return intent; + } +} diff --git a/skin/src/main/java/org/researchstack/skin/ui/layout/SignUpPinCodeCreationStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/SignUpPinCodeCreationStepLayout.java similarity index 90% rename from skin/src/main/java/org/researchstack/skin/ui/layout/SignUpPinCodeCreationStepLayout.java rename to backbone/src/main/java/org/researchstack/backbone/ui/step/layout/SignUpPinCodeCreationStepLayout.java index 03900bdaf..3d79b91f7 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/layout/SignUpPinCodeCreationStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/SignUpPinCodeCreationStepLayout.java @@ -1,24 +1,24 @@ -package org.researchstack.skin.ui.layout; +package org.researchstack.backbone.ui.step.layout; import android.content.Context; import android.content.res.Resources; import android.os.Handler; -import android.support.v4.content.ContextCompat; +import androidx.core.content.ContextCompat; import android.util.AttributeSet; import android.view.View; import android.view.inputmethod.InputMethodManager; import com.jakewharton.rxbinding.widget.RxTextView; +import org.researchstack.backbone.R; import org.researchstack.backbone.result.StepResult; import org.researchstack.backbone.step.Step; import org.researchstack.backbone.ui.callbacks.StepCallbacks; -import org.researchstack.backbone.ui.step.layout.StepLayout; import org.researchstack.backbone.ui.views.PinCodeLayout; import org.researchstack.backbone.utils.ThemeUtils; -import org.researchstack.skin.R; -import org.researchstack.skin.step.PassCodeCreationStep; +import org.researchstack.backbone.step.PassCodeCreationStep; +@Deprecated // use PasscodeCreationStepLayout instead public class SignUpPinCodeCreationStepLayout extends PinCodeLayout implements StepLayout { public static final String RESULT_OLD_PIN = "PassCodeCreationStep.oldPin"; @@ -126,16 +126,16 @@ private void refreshState() { String characterType = res.getString(config.getPinType().getInputTypeStringId()); switch (state) { case CONFIRM: - pinCodeTitle = res.getString(R.string.rss_passcode_confirm_title); - pinCodeInstructions = res.getString(R.string.rss_passcode_confirm_summary, + pinCodeTitle = res.getString(R.string.rsb_passcode_confirm_title); + pinCodeInstructions = res.getString(R.string.rsb_passcode_confirm_summary, pinLength, characterType); summaryColor = ThemeUtils.getTextColorPrimary(getContext()); break; case RETRY: - pinCodeTitle = res.getString(R.string.rss_passcode_confirm_title); - pinCodeInstructions = res.getString(R.string.rss_passcode_confirm_error, + pinCodeTitle = res.getString(R.string.rsb_passcode_confirm_title); + pinCodeInstructions = res.getString(R.string.rsb_passcode_confirm_error, pinLength, characterType); summaryColor = ContextCompat.getColor(getContext(), R.color.rsb_error); @@ -143,8 +143,8 @@ private void refreshState() { case CREATE: default: - pinCodeTitle = res.getString(R.string.rss_passcode_create_title); - pinCodeInstructions = res.getString(R.string.rss_passcode_create_summary, + pinCodeTitle = res.getString(R.string.rsb_passcode_create_title); + pinCodeInstructions = res.getString(R.string.rsb_passcode_create_summary, pinLength, characterType); summaryColor = ThemeUtils.getTextColorPrimary(getContext()); diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/StepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/StepLayout.java index 49f87245a..aa62ac6dc 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/StepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/StepLayout.java @@ -1,5 +1,6 @@ package org.researchstack.backbone.ui.step.layout; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.view.View; import org.researchstack.backbone.result.StepResult; @@ -7,6 +8,10 @@ import org.researchstack.backbone.ui.callbacks.StepCallbacks; public interface StepLayout { + /** + * @param step Step to be related to this StepLayout + * @param result the StepResult for this step, if one already exists + */ void initialize(Step step, StepResult result); View getLayout(); @@ -14,7 +19,7 @@ public interface StepLayout { /** * Method allowing a step layout to consume a back event. * - * @return + * @return a boolean indicating whether the back event is consumed */ boolean isBackEventConsumed(); diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/StepPermissionRequest.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/StepPermissionRequest.java index 8035c56d1..21b5708ec 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/StepPermissionRequest.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/StepPermissionRequest.java @@ -1,5 +1,5 @@ package org.researchstack.backbone.ui.step.layout; public interface StepPermissionRequest { - public void onUpdateForPermissionResult(); + void onUpdateForPermissionResult(); } diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/SurveyStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/SurveyStepLayout.java index 4d27c1b4b..5c01505e3 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/SurveyStepLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/SurveyStepLayout.java @@ -1,13 +1,12 @@ package org.researchstack.backbone.ui.step.layout; - import android.content.Context; import android.content.Intent; import android.os.Parcelable; -import android.support.annotation.NonNull; -import android.support.annotation.StringRes; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; import android.text.Html; import android.util.AttributeSet; -import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; @@ -29,45 +28,52 @@ import java.lang.reflect.Constructor; -public class SurveyStepLayout extends FixedSubmitBarLayout implements StepLayout { +public class SurveyStepLayout extends FixedSubmitBarLayout implements StepLayout +{ public static final String TAG = SurveyStepLayout.class.getSimpleName(); //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Data used to initializeLayout and return //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= private QuestionStep questionStep; - private StepResult stepResult; + protected StepResult stepResult; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Communicate w/ host //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - private StepCallbacks callbacks; + protected StepCallbacks callbacks; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Child Views //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= private LinearLayout container; - private StepBody stepBody; + protected StepBody stepBody; - public SurveyStepLayout(Context context) { + public SurveyStepLayout(Context context) + { super(context); } - public SurveyStepLayout(Context context, AttributeSet attrs) { + public SurveyStepLayout(Context context, AttributeSet attrs) + { super(context, attrs); } - public SurveyStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + public SurveyStepLayout(Context context, AttributeSet attrs, int defStyleAttr) + { super(context, attrs, defStyleAttr); } - public void initialize(Step step) { + public void initialize(Step step) + { initialize(step, null); } @Override - public void initialize(Step step, StepResult result) { - if (!(step instanceof QuestionStep)) { + public void initialize(Step step, StepResult result) + { + if(! (step instanceof QuestionStep)) + { throw new RuntimeException("Step being used in SurveyStep is not a QuestionStep"); } @@ -78,37 +84,43 @@ public void initialize(Step step, StepResult result) { } @Override - public View getLayout() { + public View getLayout() + { return this; } /** * Method allowing a step to consume a back event. * - * @return + * @return a boolean indication whether the back event is consumed */ @Override - public boolean isBackEventConsumed() { + public boolean isBackEventConsumed() + { callbacks.onSaveStep(StepCallbacks.ACTION_PREV, getStep(), stepBody.getStepResult(false)); return false; } @Override - public void setCallbacks(StepCallbacks callbacks) { + public void setCallbacks(StepCallbacks callbacks) + { this.callbacks = callbacks; } @Override - public int getContentResourceId() { + public int getContentResourceId() + { return R.layout.rsb_step_layout; } - public void initializeStep() { + public void initializeStep() + { initStepLayout(); initStepBody(); } - public void initStepLayout() { + public void initStepLayout() + { LogExt.i(getClass(), "initStepLayout()"); container = (LinearLayout) findViewById(R.id.rsb_survey_content_container); @@ -117,45 +129,67 @@ public void initStepLayout() { SubmitBar submitBar = (SubmitBar) findViewById(R.id.rsb_submit_bar); submitBar.setPositiveAction(v -> onNextClicked()); - if (questionStep != null) { - if (!TextUtils.isEmpty(questionStep.getTitle())) { - title.setVisibility(View.VISIBLE); - title.setText(questionStep.getTitle()); - } + if(questionStep != null) + { + setupTitleLayout(getContext(), questionStep, title, summary); - if (!TextUtils.isEmpty(questionStep.getText())) { - summary.setVisibility(View.VISIBLE); - summary.setText(Html.fromHtml(questionStep.getText())); - summary.setMovementMethod(new TextViewLinkHandler() { - @Override - public void onLinkClick(String url) { - String path = ResourcePathManager.getInstance(). - generateAbsolutePath(ResourcePathManager.Resource.TYPE_HTML, url); - Intent intent = ViewWebDocumentActivity.newIntentForPath(getContext(), - questionStep.getTitle(), - path); - getContext().startActivity(intent); - } - }); - } - - if (questionStep.isOptional()) { + if(questionStep.isOptional()) + { submitBar.setNegativeTitle(R.string.rsb_step_skip); submitBar.setNegativeAction(v -> onSkipClicked()); - } else { + } + else + { submitBar.getNegativeActionView().setVisibility(View.GONE); } } } - public void initStepBody() { + @NonNull + @MainThread + // Protected and static so that FormStepLayout can access this method + protected static void setupTitleLayout(Context context, QuestionStep questionStep, TextView title, TextView summary) { + if(! TextUtils.isEmpty(questionStep.getTitle())) + { + title.setVisibility(View.VISIBLE); + title.setText(questionStep.getTitle()); + } + + if(! TextUtils.isEmpty(questionStep.getText())) + { + summary.setVisibility(View.VISIBLE); + summary.setText(Html.fromHtml(questionStep.getText())); + summary.setMovementMethod(new TextViewLinkHandler() + { + @Override + public void onLinkClick(String url) + { + String path = ResourcePathManager.getInstance(). + generateAbsolutePath(ResourcePathManager.Resource.TYPE_HTML, url); + Intent intent = ViewWebDocumentActivity.newIntentForPath(context, + questionStep.getTitle(), + path); + context.startActivity(intent); + } + }); + } + } + + public void initStepBody() + { LogExt.i(getClass(), "initStepBody()"); - LayoutInflater inflater = LayoutInflater.from(getContext()); stepBody = createStepBody(questionStep, stepResult); - View body = stepBody.getBodyView(StepBody.VIEW_TYPE_DEFAULT, inflater, this); + View body = stepBody.getBodyView(StepBody.VIEW_TYPE_DEFAULT, layoutInflater, this); + replaceStepBodyView(container, body); + } - if (body != null) { + @NonNull + @MainThread + // Protected and static so that FormStepLayout can access this method + protected static void replaceStepBodyView(LinearLayout container, View body) { + if(body != null) + { View oldView = container.findViewById(R.id.rsb_survey_step_body); int bodyIndex = container.indexOfChild(oldView); container.removeView(oldView); @@ -165,53 +199,71 @@ public void initStepBody() { } @NonNull - private StepBody createStepBody(QuestionStep questionStep, StepResult result) { - try { + @MainThread + // Protected and static so that FormStepLayout can access this method + protected static StepBody createStepBody(QuestionStep questionStep, StepResult result) + { + try + { Class cls = questionStep.getStepBodyClass(); Constructor constructor = cls.getConstructor(Step.class, StepResult.class); return (StepBody) constructor.newInstance(questionStep, result); - } catch (Exception e) { + } + catch(Exception e) + { throw new RuntimeException(e); } } @Override - public Parcelable onSaveInstanceState() { + public Parcelable onSaveInstanceState() + { callbacks.onSaveStep(StepCallbacks.ACTION_NONE, getStep(), stepBody.getStepResult(false)); return super.onSaveInstanceState(); } - protected void onNextClicked() { + protected void onNextClicked() + { BodyAnswer bodyAnswer = stepBody.getBodyAnswerState(); - if (bodyAnswer == null || !bodyAnswer.isValid()) { + if(bodyAnswer == null || ! bodyAnswer.isValid()) + { Toast.makeText(getContext(), - bodyAnswer == null - ? BodyAnswer.INVALID.getString(getContext()) - : bodyAnswer.getString(getContext()), - Toast.LENGTH_SHORT).show(); - } else { - callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, - getStep(), - stepBody.getStepResult(false)); + bodyAnswer == null + ? BodyAnswer.INVALID.getString(getContext()) + : bodyAnswer.getString(getContext()), + Toast.LENGTH_SHORT).show(); + } + else + { + onComplete(); } } - public void onSkipClicked() { - if (callbacks != null) { + protected void onComplete() { + stepResult = stepBody.getStepResult(false); + callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, getStep(), stepResult); + } + + public void onSkipClicked() + { + if(callbacks != null) + { // empty step result when skipped callbacks.onSaveStep(StepCallbacks.ACTION_NEXT, - getStep(), - stepBody.getStepResult(true)); + getStep(), + stepBody.getStepResult(true)); } } - public Step getStep() { + public Step getStep() + { return questionStep; } - public String getString(@StringRes int stringResId) { + public String getString(@StringRes int stringResId) + { return getResources().getString(stringResId); } -} +} \ No newline at end of file diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/TappingIntervalStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/TappingIntervalStepLayout.java new file mode 100644 index 000000000..54f415a63 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/TappingIntervalStepLayout.java @@ -0,0 +1,320 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.content.Context; +import android.graphics.Rect; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TappingIntervalResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.TappingIntervalStep; +import org.researchstack.backbone.utils.LogExt; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import static org.researchstack.backbone.result.TappingIntervalResult.TappingButtonIdentifier.TappedButtonLeft; +import static org.researchstack.backbone.result.TappingIntervalResult.TappingButtonIdentifier.TappedButtonNone; +import static org.researchstack.backbone.result.TappingIntervalResult.TappingButtonIdentifier.TappedButtonRight; + +/** + * Created by TheMDP on 2/23/17. + * + * The TappingIntervalStepLayout has two buttons at the bottom of the screen that the user + * is instructed to tap one and then the other repeatably. + * + * This goes on for as long as the active step desires, and collects data on the taps + * and packages them in a TappingIntervalResult + */ + +public class TappingIntervalStepLayout extends ActiveStepLayout { + + protected TappingIntervalStep tappingIntervalStep; + protected TappingIntervalResult tappingResult; + + protected long startTime; + protected int tapCount; + protected List sampleList; + + private static final int LEFT_BUTTON = 0; + private static final int RIGHT_BUTTON = 1; + private static final int NO_BUTTON = 2; + protected TappingIntervalResult.Sample[] buttonSamples = new TappingIntervalResult.Sample[NO_BUTTON + 1]; + + protected int[] lastPointerIdx = new int[NO_BUTTON + 1]; + private static final int INVALID_POINTER_IDX = -1; + + protected RelativeLayout tappingStepLayout; + protected TextView tapCountTextView; + protected FloatingActionButton leftTappingButton; + protected FloatingActionButton rightTappingButton; + + public TappingIntervalStepLayout(Context context) { + super(context); + } + + public TappingIntervalStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TappingIntervalStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public TappingIntervalStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void initialize(Step step, StepResult result) { + super.initialize(step, result); + } + + @Override + protected void validateStep(Step step) { + super.validateStep(step); + if (!(step instanceof TappingIntervalStep)) { + throw new IllegalStateException("TappingIntervalStepLayout must have an TappingIntervalStep"); + } + tappingIntervalStep = (TappingIntervalStep) step; + } + + @Override + public void setupSubmitBar() { + super.setupSubmitBar(); + submitBar.setVisibility(View.INVISIBLE); + } + + @Override + public void setupActiveViews() { + super.setupActiveViews(); + + remainingHeightOfContainer(new HeightCalculatedListener() { + @Override + public void heightCalculated(int height) { + tappingStepLayout = (RelativeLayout)layoutInflater.inflate(R.layout.rsb_step_layout_tapping_interval, activeStepLayout, false); + tapCountTextView = (TextView) tappingStepLayout.findViewById(R.id.rsb_total_taps_counter); + tapCountTextView.setText(String.format(Locale.getDefault(), "%2d", 0)); + leftTappingButton = (FloatingActionButton) tappingStepLayout.findViewById(R.id.rsb_tapping_interval_button_left); + rightTappingButton = (FloatingActionButton) tappingStepLayout.findViewById(R.id.rsb_tapping_interval_button_right); + + progressBarHorizontal.setProgress(0); + progressBarHorizontal.setMax(activeStep.getStepDuration()); + progressBarHorizontal.setVisibility(View.VISIBLE); + + activeStepLayout.addView(tappingStepLayout, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, height)); + + setupSampleResult(); + } + }); + } + + /** + * Should only be called after the UI has been laid out + */ + protected void setupSampleResult() { + sampleList = new ArrayList<>(); + tapCount = 0; + for (int i = 0; i <= NO_BUTTON; i++) { + lastPointerIdx[i] = INVALID_POINTER_IDX; + } + + tappingResult = new TappingIntervalResult(tappingIntervalStep.getIdentifier()); + + int[] activeStepLayoutXY = new int[2]; + activeStepLayout.getLocationOnScreen(activeStepLayoutXY); + { + View button = leftTappingButton; + + button.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + button.getViewTreeObserver().removeOnGlobalLayoutListener(this); + + int[] buttonXY = new int[2]; + button.getLocationOnScreen(buttonXY); + int buttonLeft = buttonXY[0] - activeStepLayoutXY[0]; + int buttonTop = buttonXY[1] - activeStepLayoutXY[1]; + int buttonRight = buttonLeft + button.getWidth(); + int buttonBottom = buttonRight + button.getHeight(); + Rect buttonRect = new Rect(buttonLeft, buttonTop, buttonRight, buttonBottom); + + setupTouchListener(button, LEFT_BUTTON, buttonRect, TappedButtonLeft, true); + tappingResult.setButtonRect1(buttonLeft, buttonTop, button.getWidth(), button.getHeight()); + } + }); + } + + { + View button = rightTappingButton; + + button.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + button.getViewTreeObserver().removeOnGlobalLayoutListener(this); + + int[] buttonXY = new int[2]; + button.getLocationOnScreen(buttonXY); + int buttonLeft = buttonXY[0] - activeStepLayoutXY[0]; + int buttonTop = buttonXY[1] - activeStepLayoutXY[1]; + int buttonRight = buttonLeft + button.getWidth(); + int buttonBottom = buttonRight + button.getHeight(); + Rect buttonRect = new Rect(buttonLeft, buttonTop, buttonRight, buttonBottom); + + setupTouchListener(button, RIGHT_BUTTON, buttonRect, TappedButtonRight, true); + tappingResult.setButtonRect2(buttonLeft, buttonTop, button.getWidth(), button.getHeight()); + } + }); + } + + { + View button = activeStepLayout; + + button.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + button.getViewTreeObserver().removeOnGlobalLayoutListener(this); + + int[] buttonXY = new int[2]; + button.getLocationOnScreen(buttonXY); + int buttonLeft = buttonXY[0] - activeStepLayoutXY[0]; + int buttonTop = buttonXY[1] - activeStepLayoutXY[1]; + int buttonRight = buttonLeft + button.getWidth(); + int buttonBottom = buttonRight + button.getHeight(); + Rect buttonRect = new Rect(buttonLeft, buttonTop, buttonRight, buttonBottom); + + setupTouchListener(button, NO_BUTTON, buttonRect, TappedButtonNone, false); + tappingResult.setStepViewSize(activeStepLayout.getWidth(), activeStepLayout.getHeight()); + } + }); + } + } + + @Override + public void doUIAnimationPerSecond() { + super.doUIAnimationPerSecond(); + progressBarHorizontal.setProgress(progressBarHorizontal.getProgress() + 1); + } + + @Override + public void start() { + super.start(); + + startTime = System.currentTimeMillis(); + tapCountTextView.setText(String.format(Locale.getDefault(), "%2d", tapCount)); + } + + protected void setupTouchListener( + final View view, + final int idx, + final Rect buttonRect, + TappingIntervalResult.TappingButtonIdentifier buttonId, + boolean countsAsATap) + { + view.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + + switch (motionEvent.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + // Make sure we aren't overriding another finger's down tap + if (lastPointerIdx[idx] == INVALID_POINTER_IDX) { + buttonSamples[idx] = new TappingIntervalResult.Sample(); + buttonSamples[idx].setTimestamp(motionEvent.getEventTime() - startTime); + buttonSamples[idx].setButtonIdentifier(buttonId); + buttonSamples[idx].setLocation( + (int)(motionEvent.getX() + buttonRect.left), + (int)(motionEvent.getY() + buttonRect.top)); + lastPointerIdx[idx] = motionEvent.getActionMasked(); + + LogExt.d(getClass(), "tap down with button idx " + idx); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_CANCEL: + + // We need to make sure the finger index matches up with the + // finger index that started the "down" motion event + boolean correctFingerForIdx = + motionEvent.getActionMasked() == MotionEvent.ACTION_CANCEL || + (motionEvent.getActionMasked() == MotionEvent.ACTION_UP && + lastPointerIdx[idx] == MotionEvent.ACTION_DOWN) || + (motionEvent.getActionMasked() == MotionEvent.ACTION_POINTER_UP && + lastPointerIdx[idx] == MotionEvent.ACTION_POINTER_DOWN); + + // Make sure we have the same finger's up tap + if (buttonSamples[idx] != null && correctFingerForIdx) { + buttonSamples[idx].setDuration(motionEvent.getDownTime()); + sampleList.add(buttonSamples[idx]); + buttonSamples[idx] = null; + lastPointerIdx[idx] = INVALID_POINTER_IDX; + if (countsAsATap) { + countATap(); + } + + LogExt.d(getClass(), "tap up with button idx " + idx); + } + + break; + } + + if (!countsAsATap) { + return true; + } else { + return view.onTouchEvent(motionEvent); + } + } + }); + } + + @Override + public void stop() { + super.stop(); + + // Complete any touches that have had a down but no up + for (int i = 0; i <= RIGHT_BUTTON; i++) { + if (buttonSamples[i] != null) { + buttonSamples[i].setDuration(System.currentTimeMillis() - buttonSamples[i].getTimestamp()); + sampleList.add(buttonSamples[i]); + buttonSamples[i] = null; + } + } + + if (sampleList == null || sampleList.isEmpty()) { + return; + } + + tappingResult.setStartDate(new Date(startTime)); + tappingResult.setEndDate(new Date()); + tappingResult.setSamples(sampleList); + + stepResult.getResults().put(tappingResult.getIdentifier(), tappingResult); + + leftTappingButton.setOnTouchListener(null); + rightTappingButton.setOnTouchListener(null); + activeStepLayout.setOnTouchListener(null); + } + + protected void countATap() { + // Start official data logging with first tap on a button + if (tapCount == 0) { + start(); + } + tapCount++; + tapCountTextView.setText(String.format(Locale.getDefault(), "%2d", tapCount)); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/TimedWalkStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/TimedWalkStepLayout.java new file mode 100644 index 000000000..7b1c6a18e --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/TimedWalkStepLayout.java @@ -0,0 +1,81 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.content.Context; +import android.text.format.DateUtils; +import android.util.AttributeSet; + +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TimedWalkResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.TimedWalkStep; + +/** + * Created by TheMDP on 2/22/17. + */ + +public class TimedWalkStepLayout extends ActiveStepLayout { + + private static final double TimedWalkMinimumDistanceInMeters = 1.0; + private static final double TimedWalkMaximumDistanceInMeters = 10000.0; + private static final double TimedWalkMinimumDuration = 1.0; + + private long startTime; + protected TimedWalkStep timedWalkStep; + + public TimedWalkStepLayout(Context context) { + super(context); + } + + public TimedWalkStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TimedWalkStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public TimedWalkStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void initialize(Step step, StepResult result) { + super.initialize(step, result); + + startTime = System.currentTimeMillis(); + } + + @Override + protected void stepResultFinished() { + super.stepResultFinished(); + + TimedWalkResult timedWalkResult = new TimedWalkResult(timedWalkStep.getIdentifier()); + timedWalkResult.setDistanceInMeters(timedWalkStep.getDistanceInMeters()); + int durationInSeconds = (int)((System.currentTimeMillis() - startTime) / DateUtils.SECOND_IN_MILLIS); + timedWalkResult.setDuration(durationInSeconds); + timedWalkResult.setTimeLimit(timedWalkStep.getStepDuration()); + stepResult.setResultForIdentifier(timedWalkResult.getIdentifier(), timedWalkResult); + } + + @Override + protected void validateStep(Step step) { + super.validateStep(step); + + if (!(step instanceof TimedWalkStep)) { + throw new IllegalStateException("TimedWalkStepLayout must have an TimedWalkStep"); + } + timedWalkStep = (TimedWalkStep) step; + + if (timedWalkStep.getDistanceInMeters() < TimedWalkMinimumDistanceInMeters || + timedWalkStep.getDistanceInMeters() > TimedWalkMaximumDistanceInMeters) + { + throw new IllegalStateException("timed walk distance must be greater than or equal to " + + TimedWalkMinimumDistanceInMeters + " meters and less than or equal to " + + TimedWalkMaximumDistanceInMeters + " meters"); + } + + if (timedWalkStep.getStepDuration() < TimedWalkMinimumDuration) { + throw new IllegalStateException("duration cannot be shorter than " + TimedWalkMinimumDuration + " seconds."); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ViewPagerSubstepListStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ViewPagerSubstepListStepLayout.java new file mode 100644 index 000000000..cf7ae501c --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/ViewPagerSubstepListStepLayout.java @@ -0,0 +1,305 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.content.Context; +import android.os.Parcelable; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.FrameLayout; + +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.SubstepListStep; +import org.researchstack.backbone.ui.callbacks.StepCallbacks; +import org.researchstack.backbone.ui.views.AlertFrameLayout; +import org.researchstack.backbone.utils.StepLayoutHelper; +import org.researchstack.backbone.utils.StepResultHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 1/16/17. + */ + +public class ViewPagerSubstepListStepLayout extends AlertFrameLayout implements StepLayout, StepCallbacks { + + /** Used to save and load the index of the view pager for when this view is destoryed/created */ + private static final String VIEW_PAGER_INDEX_KEY = "ViewPagerIndexKey"; + + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + // Communicate w/ host + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + protected StepCallbacks callbacks; + + protected SubstepListStep substepListStep; + + protected StepResult stepResult; + + protected SwipeDisabledViewPager viewPager; + protected ViewPagerSubstepStepLayoutAdapter viewPagerAdapter; + + public ViewPagerSubstepListStepLayout(Context context) { + super(context); + } + + public ViewPagerSubstepListStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * Called when the ViewPager tries to move past the total Views + * + * Override this in subclasses to perform something on completion + */ + protected void onComplete() { + removeViewPagerIndex(); + callbacks.onSaveStep(ACTION_NEXT, substepListStep, stepResult); + } + + @Override + public void initialize(Step step, StepResult result) { + validateStepAndResult(step, result); + + // Adds the view pager, and the view pager will create the substep step layouts + viewPager = new SwipeDisabledViewPager(getContext()); + viewPagerAdapter = new ViewPagerSubstepStepLayoutAdapter(); + viewPager.setAdapter(viewPagerAdapter); + addView(viewPager, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + + // If there is a cached view pager index, then send the user to that view + loadViewPagerIndex(); + } + + @Override + public Parcelable onSaveInstanceState() { + callbacks.onSaveStep(StepCallbacks.ACTION_NONE, substepListStep, stepResult); + return super.onSaveInstanceState(); + } + + @SuppressWarnings("unchecked") // StepResult cast + protected void validateStepAndResult(Step step, StepResult result) { + if (!(step instanceof SubstepListStep)) { + throw new IllegalStateException( + "ViewPagerStepLayout is only compatible with a SubtaskStep"); + } + + substepListStep = (SubstepListStep) step; + + if (result != null && !result.getResults().isEmpty()) { + for (Object valuObj : result.getResults().values()) { + if (valuObj != null && !(valuObj instanceof StepResult)) { + throw new IllegalStateException("StepResult only supports StepResult class"); + } + } + } else { + stepResult = null; + } + stepResult = result; + if (stepResult == null) { + stepResult = new StepResult<>(substepListStep); + } + } + + protected void hideKeyboard() { + InputMethodManager imm = (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(getWindowToken(), 0); + } + + @Override + public View getLayout() { + return this; + } + + /** + * Method allowing a step layout to consume a back event. + * @return a boolean indicating whether the back event is consumed + */ + @Override + public boolean isBackEventConsumed() { + return viewPagerAdapter.stepLayouts.get(viewPager.getCurrentItem()).isBackEventConsumed(); + } + + // This is used to monitor the StepLayout for the Subtask's steps + @Override + public void onSaveStep(int action, Step step, StepResult result) { + switch (action) { + case ACTION_NONE: + stepResult.getResults().put(step.getIdentifier(), result); + callbacks.onSaveStep(ACTION_NONE, substepListStep, stepResult); + break; + case ACTION_NEXT: + stepResult.getResults().put(step.getIdentifier(), result); + if (!viewPagerAdapter.moveNext()) { + onComplete(); + } else { + saveViewPagerIndex(); + callbacks.onSaveStep(ACTION_NONE, substepListStep, stepResult); + } + break; + case ACTION_PREV: + stepResult.getResults().remove(step.getIdentifier()); + if (!viewPagerAdapter.movePrevious()) { + removeViewPagerIndex(); + callbacks.onSaveStep(ACTION_PREV, substepListStep, stepResult); + } else { + saveViewPagerIndex(); + callbacks.onSaveStep(ACTION_NONE, substepListStep, stepResult); + } + break; + case ACTION_END: + removeViewPagerIndex(); + onCancelStep(); + break; + } + } + + protected Step getMockViewPagerStep() { + return new Step(substepListStep.getIdentifier() + "." + VIEW_PAGER_INDEX_KEY); + } + + protected void saveViewPagerIndex() { + Step mockStep = getMockViewPagerStep(); + StepResult mockStepResult = new StepResult<>(mockStep); + mockStepResult.setResult(viewPager.getCurrentItem()); + stepResult.getResults().put(mockStep.getIdentifier(), mockStepResult); + } + + protected void removeViewPagerIndex() { + stepResult.getResults().remove(getMockViewPagerStep().getIdentifier()); + } + + protected void loadViewPagerIndex() { + Step mockStep = getMockViewPagerStep(); + StepResult mockStepResult = stepResult.getResults().get(mockStep.getIdentifier()); + if (mockStepResult != null && mockStepResult.getResult() instanceof Integer) { + viewPager.setCurrentItem((Integer)mockStepResult.getResult()); + } + } + + @Override + public void onCancelStep() { + stepResult.getResults().clear(); + callbacks.onSaveStep(ACTION_END, substepListStep, stepResult); + } + + public void setCallbacks(StepCallbacks callbacks) { + this.callbacks = callbacks; + } + + /** + * Can only be called after the ViewPager is instantiated and the StepLayouts have been created + * If you need to access them when they are created, use onStepLayoutCreated method below + * + * @param index of StepLayout + * @return the StepLayout at index in the ViewPager + */ + protected StepLayout getStepLayout(int index) { + if (viewPagerAdapter == null || + viewPagerAdapter.stepLayouts == null || + viewPagerAdapter.stepLayouts.isEmpty() || + viewPagerAdapter.stepLayouts.size() <= index) + { + return null; + } + return viewPagerAdapter.stepLayouts.get(index); + } + + protected void onStepLayoutCreated(StepLayout stepLayout, int index) { + // can be implemented by sub-classes + loadViewPagerIndex(); + } + + protected class ViewPagerSubstepStepLayoutAdapter extends PagerAdapter { + + /** onSavedInstanceState wont be called unless view pager views have a valid id */ + private static final int BASE_VIEW_PAGER_ID = 10; + + List stepLayouts; + + ViewPagerSubstepStepLayoutAdapter() { + stepLayouts = new ArrayList<>(); + } + + @Override + public int getCount() { + return substepListStep.getStepList().size(); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public Object instantiateItem(ViewGroup collection, int position) { + Step step = substepListStep.getStepList().get(position); + + // Build ViewPager views based off of Step's StepLayouts, similar to what ViewTaskActivity does + StepLayout stepLayout = StepLayoutHelper.createLayoutFromStep(step, getContext()); + StepResult subStepResult = StepResultHelper.findStepResult(stepResult, step.getIdentifier()); + stepLayout.setCallbacks(ViewPagerSubstepListStepLayout.this); + stepLayout.initialize(step, subStepResult); + stepLayouts.add(stepLayout); + stepLayout.getLayout().setId(BASE_VIEW_PAGER_ID + position); + + collection.addView(stepLayout.getLayout(), 0); + + onStepLayoutCreated(stepLayout, position); + + return stepLayout.getLayout(); + } + + @Override + public void destroyItem(ViewGroup collection, int position, Object view) { + collection.removeView((View)view); + } + + boolean moveNext() { + hideKeyboard(); + int previousIndex = viewPager.getCurrentItem(); + viewPager.setCurrentItem(viewPager.getCurrentItem() + 1); + return previousIndex != viewPager.getCurrentItem(); + } + + boolean movePrevious() { + hideKeyboard(); + int previousIndex = viewPager.getCurrentItem(); + viewPager.setCurrentItem(viewPager.getCurrentItem() - 1); + return previousIndex != viewPager.getCurrentItem(); + } + } + + protected class SwipeDisabledViewPager extends ViewPager { + + public SwipeDisabledViewPager(Context context) { + super(context); + } + + public SwipeDisabledViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + // Never allow swiping to switch between pages + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Never allow swiping to switch between pages + return false; + } + + @Override + public Parcelable onSaveInstanceState() { + return super.onSaveInstanceState(); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/WalkingTaskStepLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/WalkingTaskStepLayout.java new file mode 100644 index 000000000..35f82f9fa --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/step/layout/WalkingTaskStepLayout.java @@ -0,0 +1,102 @@ +package org.researchstack.backbone.ui.step.layout; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import android.util.AttributeSet; +import android.view.View; + +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.active.recorder.AudioRecorder; +import org.researchstack.backbone.step.active.recorder.PedometerRecorder; +import org.researchstack.backbone.step.active.WalkingTaskStep; + +/** + * Created by TheMDP on 2/16/17. + * + * The WalkingTaskStepLayout is basically the same as the ActiveStepLayout, except that it + * limits the duration of the step based on the user's number of steps taken so far + * + * It also shows an indefinite progress dialog, since we don't know if it will end based on + * the stepDuration or the numberOfStepsPerLeg + */ + +public class WalkingTaskStepLayout extends ActiveStepLayout { + + private WalkingTaskStep walkingTaskStep; + private BroadcastReceiver pedometerReceiver; + + public WalkingTaskStepLayout(Context context) { + super(context); + } + + public WalkingTaskStepLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public WalkingTaskStepLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public WalkingTaskStepLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void initialize(Step step, StepResult result) { + super.initialize(step, result); + + timerTextview.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + } + + @Override + protected void validateStep(Step step) { + if (!(step instanceof WalkingTaskStep)) { + throw new IllegalStateException("WalkingTaskStepLayout must have an WalkingTaskStep"); + } + walkingTaskStep = (WalkingTaskStep) step; + super.validateStep(step); + } + + @Override + protected void registerRecorderBroadcastReceivers(Context appContext) { + super.registerRecorderBroadcastReceivers(appContext); + pedometerReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || intent.getAction() == null) { + return; + } + if (PedometerRecorder.BROADCAST_PEDOMETER_UPDATE_ACTION.equals(intent.getAction())) { + PedometerRecorder.PedometerUpdateHolder dataHolder = + PedometerRecorder.getPedometerUpdateHolder(intent); + if (dataHolder != null) { + if (walkingTaskStep.getNumberOfStepsPerLeg() > 0 && + (dataHolder.getStepCount() >= walkingTaskStep.getNumberOfStepsPerLeg())) + { + // TODO: mdephillips 1/13/18 + // we may want to move this functionality to the PedometerRecorder + // and having that signal to RecorderService to stop, + // since this StepLayout may be create/destroyed and miss this broadcast + WalkingTaskStepLayout.super.stop(); + } + } + } + } + }; + IntentFilter intentFilter = new IntentFilter(AudioRecorder.BROADCAST_SAMPLE_ACTION); + LocalBroadcastManager.getInstance(appContext) + .registerReceiver(pedometerReceiver, intentFilter); + } + + @Override + protected void unregisterRecorderBroadcastReceivers() { + super.unregisterRecorderBroadcastReceivers(); + Context appContext = getContext().getApplicationContext(); + LocalBroadcastManager.getInstance(appContext).unregisterReceiver(pedometerReceiver); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/AlertFrameLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/AlertFrameLayout.java new file mode 100644 index 000000000..d6ad7c2c8 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/AlertFrameLayout.java @@ -0,0 +1,107 @@ +package org.researchstack.backbone.ui.views; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import org.researchstack.backbone.R; + +/** + * Created by TheMDP on 1/16/17. + */ + +public class AlertFrameLayout extends FrameLayout { + protected AlertDialog alertDialog; + protected ProgressDialog progressDialog; + + public AlertFrameLayout(Context context) { + super(context); + } + + public AlertFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AlertFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public AlertFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /** + * Helper method for ProfileSteps that need to make calls to the web + * Uses default localization of "Loading..." + */ + public void showLoadingDialog() { + showLoadingDialog(getContext().getString(R.string.rsb_loading_ellipses)); + } + + /** + * Helper method for ProfileSteps that need to make calls to the web + * @param title title of the alert dialog + */ + public void showLoadingDialog(String title) { + if (getContext() == null) { + return; + } + hideLoadingDialog(); // just in case these are showing + hideAlertDialog(); + progressDialog = ProgressDialog.show(getContext(), "", title); + } + + /** + * Helper method for ProfileSteps that need to make calls to the web + */ + public void hideLoadingDialog() { + if (progressDialog != null) { + progressDialog.dismiss(); + } + } + + /** + * Helper method for showing an error alert + * @param message message that will be show with alert + */ + public void showOkAlertDialog(String message) { + showOkAlertDialog(message, null); + } + + /** + * Helper method for showing an error alert + * @param message message that will be show with alert + * @param listener on click listener + */ + public void showOkAlertDialog(String message, DialogInterface.OnClickListener listener) { + if (getContext() == null) { + return; + } + hideLoadingDialog(); // just in case these are showing + hideAlertDialog(); + alertDialog = new AlertDialog.Builder(getContext()) + .setMessage(message) + .setPositiveButton(getContext().getString(R.string.rsb_ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + dialogInterface.dismiss(); + if (listener != null) { + listener.onClick(dialogInterface, i); + } + } + }) + .create(); + alertDialog.show(); + } + + public void hideAlertDialog() { + if (alertDialog != null) { + alertDialog.dismiss(); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/AlertLinearLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/AlertLinearLayout.java new file mode 100644 index 000000000..762f892de --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/AlertLinearLayout.java @@ -0,0 +1,83 @@ +package org.researchstack.backbone.ui.views; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +import org.researchstack.backbone.R; + +/** + * Created by TheMDP on 1/16/17. + */ + +public class AlertLinearLayout extends LinearLayout { + protected AlertDialog alertDialog; + protected ProgressDialog progressDialog; + + public AlertLinearLayout(Context context) { + super(context); + } + + public AlertLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AlertLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public AlertLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /** + * Helper method for ProfileSteps that need to make calls to the web + * Uses default localization of "Loading..." + */ + public void showLoadingDialog() { + showLoadingDialog(getContext().getString(R.string.rsb_loading_ellipses)); + } + + /** + * Helper method for ProfileSteps that need to make calls to the web + * @param title title of the alert dialog + */ + public void showLoadingDialog(String title) { + hideLoadingDialog(); // just in case these are showing + hideAlertDialog(); + progressDialog = ProgressDialog.show(getContext(), "", title); + } + + /** + * Helper method for ProfileSteps that need to make calls to the web + */ + public void hideLoadingDialog() { + if (progressDialog != null) { + progressDialog.dismiss(); + } + } + + /** + * Helper method for showing an error alert + * @param message message that will be show with alert + */ + public void showOkAlertDialog(String message) { + hideLoadingDialog(); // just in case these are showing + hideAlertDialog(); + alertDialog = new AlertDialog.Builder(getContext()) + .setMessage(message) + .setPositiveButton(getContext().getString(R.string.rsb_ok), null) + .create(); + alertDialog.show(); + } + + public void hideAlertDialog() { + if (alertDialog != null) { + alertDialog.dismiss(); + } + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/ArcDrawable.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/ArcDrawable.java new file mode 100644 index 000000000..2f9f17fba --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/ArcDrawable.java @@ -0,0 +1,117 @@ +package org.researchstack.backbone.ui.views; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import androidx.annotation.NonNull; + +/** + * Created by mdephillips on 4/20/15 + * + * ArcDrawable can be set as the background of any view to show or animate an arc + */ + +public class ArcDrawable extends Drawable { + + private static final int DEFAULT_STROKE_COLOR = Color.GREEN; + private static final float DEFAULT_STROKE_WIDTH = 10.0f; // 10 px wide + public static final float FULL_SWEEPING_ANGLE = 360.0f; // full circle + private static final float DEFAULT_START_ANGLE = -90.0f; // 12 o'clock + + private final Paint mPaint; + private float mSweepingAngle; + public void setSweepAngle(float degrees) { + mSweepingAngle = degrees; + invalidateSelf(); + } + private float mStartAngle; + public void setStartAngle(float startAngle) { + mStartAngle = startAngle; + } + + private Path.Direction direction = Path.Direction.CCW; + public void setDirection(Path.Direction newDirection) { + direction = newDirection; + } + + private static final int DEFAULT_FULL_CIRCLE_COLOR = Color.GRAY; + private static final float DEFAULT_FULL_CIRCLE_STROKE_PERCENTAGE = 0.25f; + /** + * The full circle preview is a ring that shows behind the arc as an indication + * Of how and where the arc will follow + */ + private Paint mFullCirclePreviewPaint; + private boolean mIncludeFullCirclePreview; + public void setIncludeFullCirclePreview(boolean mIncludeFullCirclePreview) { + this.mIncludeFullCirclePreview = mIncludeFullCirclePreview; + } + private int mFullCirclePreviewColor = DEFAULT_FULL_CIRCLE_COLOR; + public void setFullCirclePreviewColor(int mFullCirclePreviewColor) { + this.mFullCirclePreviewColor = mFullCirclePreviewColor; + } + + public ArcDrawable() { + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setStrokeWidth(DEFAULT_STROKE_WIDTH); + mPaint.setColor(DEFAULT_STROKE_COLOR); + mPaint.setStyle(Paint.Style.STROKE); + mFullCirclePreviewPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mFullCirclePreviewPaint.setStyle(Paint.Style.STROKE); + mSweepingAngle = FULL_SWEEPING_ANGLE; + mStartAngle = DEFAULT_START_ANGLE; + } + + @Override + public void draw(@NonNull Canvas canvas) { + + float halfStrokeWidth = mPaint.getStrokeWidth() * 0.5f; + RectF rect = new RectF( + halfStrokeWidth, + halfStrokeWidth, + canvas.getWidth() - halfStrokeWidth, + canvas.getHeight() - halfStrokeWidth); + float angle = (direction == Path.Direction.CCW) ? -mSweepingAngle : mSweepingAngle; + + // Draw the preview first, if applicable, so it is under the main arc + if (mIncludeFullCirclePreview) { + mFullCirclePreviewPaint.setColor(mFullCirclePreviewColor); + float fullPreviewStroke = DEFAULT_FULL_CIRCLE_STROKE_PERCENTAGE * mPaint.getStrokeWidth(); + mFullCirclePreviewPaint.setStrokeWidth(fullPreviewStroke); + RectF fullCircleRect = new RectF(halfStrokeWidth, halfStrokeWidth, + canvas.getWidth() - halfStrokeWidth, + canvas.getHeight() - halfStrokeWidth); + canvas.drawArc(fullCircleRect, mStartAngle, FULL_SWEEPING_ANGLE, false, mFullCirclePreviewPaint); + } + + // Draw the arc over top the preview + canvas.drawArc(rect, mStartAngle, angle, false, mPaint); + } + + @Override + public void setAlpha(int alpha) { + mPaint.setAlpha(alpha); + } + + public void setColor(int color) { + mPaint.setColor(color); + } + + public void setArchWidth(float width) { + mPaint.setStrokeWidth(width); + } + + @Override + public void setColorFilter(ColorFilter cf) { + // not supported at this time, use setColor(int color) method + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } +} \ No newline at end of file diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/AssetVideoView.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/AssetVideoView.java index 65c5777a7..c7ad5d018 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/views/AssetVideoView.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/AssetVideoView.java @@ -39,14 +39,14 @@ import java.io.IOException; /** - * Displays a video file. The VideoView class can load images from various sources (such as + *

    Displays a video file. The VideoView class can load images from various sources (such as * resources or content providers), takes care of computing its measurement from the video so that * it can be used in any layout manager, and provides various display options such as scaling and - * tinting.

    - *

    + * tinting.

    + * * Note: VideoView does not retain its full state when going into the background. In * particular, it does not restore the current play state, play position, selected tracks, or any - * subtitle tracks added via {@link #addSubtitleSource addSubtitleSource()}. Applications should + * subtitle tracks added via {@link VideoView#addSubtitleSource addSubtitleSource()}. Applications should * save and restore these on their own in {@link android.app.Activity#onSaveInstanceState} and * {@link android.app.Activity#onRestoreInstanceState}.

    Also note that the audio session id (from * {@link #getAudioSessionId}) may change from its previously returned value when the VideoView is @@ -261,7 +261,8 @@ public AssetVideoView(Context context, AttributeSet attrs) { public AssetVideoView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); + super(context, attrs, defStyleAttr); + initVideoView(); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/AudioGraphView.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/AudioGraphView.java new file mode 100644 index 000000000..79c7990b3 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/AudioGraphView.java @@ -0,0 +1,214 @@ +package org.researchstack.backbone.ui.views; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import org.researchstack.backbone.step.active.recorder.AudioRecorder; +import org.researchstack.backbone.utils.ViewUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 2/27/17. + * + * The AudioGraphView is a vertically centered bar graph that begins drawing on the right side + * of a view and old samples are pushed to the left so as to appear the audio + * is moving across the graph as you continue to make calls to addSample() + */ + +public class AudioGraphView extends View { + + private static final int DEFAULT_SAMPLE_WIDTH = 24; + private int sampleWidthInPx; + + private static final int DEFAULT_MAX_SAMPLE_VALUE = AudioRecorder.MAX_VOLUME; + private int maxSampleValue; + + // Last-in-first-out for samples, which will be drawn on the graph from right to left + private List sampleList; + + /** + * The style to draw the dashes in between the value bars + */ + private Paint dashPaint; + + private static final int DEFAULT_GRAPH_COLOR = Color.BLACK; + private int graphColor; + + /** + * The style to draw the value bars in between the dashes + */ + private Paint barPaint; + + public AudioGraphView(Context context) { + super(context); + commonInit(); + } + + public AudioGraphView(Context context, AttributeSet attrs) { + super(context, attrs); + commonInit(); + } + + public AudioGraphView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + commonInit(); + } + + @TargetApi(21) + public AudioGraphView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + commonInit(); + } + + private void commonInit() { + sampleList = new ArrayList<>(); + + maxSampleValue = DEFAULT_MAX_SAMPLE_VALUE; + setSampleWidthInPx(DEFAULT_SAMPLE_WIDTH); + graphColor = DEFAULT_GRAPH_COLOR; + + refreshDashPaint(); + refreshBarPaint(); + } + + private void refreshDashPaint() { + dashPaint = new Paint(); + dashPaint.setColor(graphColor); + dashPaint.setStyle(Paint.Style.FILL_AND_STROKE); + + // Some devices' like Samsung disable dash path effect with hardware acceleration + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + + int pathDashWidths = getSampleWidthInPx(); + dashPaint.setStrokeWidth(pathDashWidths / 2); + dashPaint.setPathEffect(new DashPathEffect(new float[] {pathDashWidths, pathDashWidths}, 0)); + } + + private void setSampleWidthInPx(int sampleWidthInDp) { + int pathDashWidths = ViewUtils.dpToPx(getContext(), sampleWidthInDp); + if (pathDashWidths % 2 != 0) { + pathDashWidths++; + } + sampleWidthInPx = pathDashWidths; + } + + /** + * @return width of paths, will always be made divisible by 2 + */ + public int getSampleWidthInPx() { + return sampleWidthInPx; + } + + private void refreshBarPaint() { + barPaint = new Paint(); + barPaint.setColor(graphColor); + barPaint.setStyle(Paint.Style.FILL_AND_STROKE); + barPaint.setStrokeCap(Paint.Cap.ROUND); + + float strokeWidth = getSampleWidthInPx(); + barPaint.setStrokeWidth(strokeWidth); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int midY = canvas.getHeight() / 2; + int maxY = canvas.getHeight(); + int maxX = canvas.getWidth(); + + // Draw the dash line in the middle first + int pathDashWidth = getSampleWidthInPx(); + int remainderOfDash = maxY % pathDashWidth; + canvas.drawLine(maxX, midY, remainderOfDash, midY, dashPaint); + + // Draw the graph data next + int currentX = maxX - pathDashWidth; // start first sample on empty dash + for (int i = (sampleList.size() - 1); i >= 0; i--) { + if (currentX < remainderOfDash) { + i = -1; // exit loop + } else { + int sample = sampleList.get(i); + + float heightFactor = (float)sample / (float)maxSampleValue; + int lineHeight = (int)(canvas.getHeight() * heightFactor); + int yOffset = (canvas.getHeight() - lineHeight) / 2; + int halfWidth = pathDashWidth / 2; + int x = currentX - halfWidth; + + // Draw the vertical line + canvas.drawLine(x, yOffset, x, yOffset + lineHeight, barPaint); + + // Move to next sample line + currentX -= (pathDashWidth * 2); + } + } + } + + /** + * @param sampleWidthInDp the sample width that will be converted from dp to px + */ + public void setSampleWidthInDp(int sampleWidthInDp) { + setSampleWidthInPx(sampleWidthInDp); + refreshDashPaint(); + refreshBarPaint(); + invalidate(); + } + + public int getGraphColor() { + return graphColor; + } + + public void setGraphColor(int graphColor) { + this.graphColor = graphColor; + refreshDashPaint(); + refreshBarPaint(); + invalidate(); + } + + public int getMaxSampleValue() { + return maxSampleValue; + } + + /** + * @param maxSampleValue controls the scale for the graph, the value will be displayed at 100% height + */ + public void setMaxSampleValue(int maxSampleValue) { + this.maxSampleValue = maxSampleValue; + } + + /** + * Adds a sample value, and then redraws the graph to include the new value + * @param sampleValue to add to the graph + */ + public void addSample(int sampleValue) { + sampleList.add(sampleValue); + + // Limit max number of samples to be all that can fit in the view + int viewWidth = getWidth(); + if (viewWidth > 0) { + int maxSamples = viewWidth / (getSampleWidthInPx() * 2); + if (sampleList.size() > maxSamples) { + sampleList.remove(0); + } + } + + invalidate(); + } + + /** + * Clears out all old samples, and then redraws the graph as blank + */ + public void clearSamples() { + sampleList.clear(); + invalidate(); + } +} diff --git a/skin/src/main/java/org/researchstack/skin/ui/views/DividerItemDecoration.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/DividerItemDecoration.java similarity index 95% rename from skin/src/main/java/org/researchstack/skin/ui/views/DividerItemDecoration.java rename to backbone/src/main/java/org/researchstack/backbone/ui/views/DividerItemDecoration.java index 63afc476f..fe5c1beb2 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/views/DividerItemDecoration.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/DividerItemDecoration.java @@ -14,16 +14,16 @@ * limitations under the License. */ -package org.researchstack.skin.ui.views; +package org.researchstack.backbone.ui.views; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.view.View; public class DividerItemDecoration extends RecyclerView.ItemDecoration { diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/FixedSubmitBarLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/FixedSubmitBarLayout.java index bc5125378..56688d1ef 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/views/FixedSubmitBarLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/FixedSubmitBarLayout.java @@ -2,86 +2,158 @@ import android.annotation.TargetApi; import android.content.Context; -import android.support.v4.view.ViewCompat; +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.core.view.ViewCompat; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.widget.FrameLayout; import android.widget.ScrollView; import org.researchstack.backbone.R; import org.researchstack.backbone.ui.step.layout.StepLayout; -public abstract class FixedSubmitBarLayout extends FrameLayout implements StepLayout { - public FixedSubmitBarLayout(Context context) { +public abstract class FixedSubmitBarLayout extends AlertFrameLayout implements StepLayout +{ + protected LayoutInflater layoutInflater; + protected SubmitBar submitBar; + protected View submitBarGuide; + protected ViewGroup contentContainer; + protected ObservableScrollView scrollView; + + public FixedSubmitBarLayout(Context context) + { super(context); init(); } - public FixedSubmitBarLayout(Context context, AttributeSet attrs) { + public FixedSubmitBarLayout(Context context, AttributeSet attrs) + { super(context, attrs); init(); } - public FixedSubmitBarLayout(Context context, AttributeSet attrs, int defStyleAttr) { + public FixedSubmitBarLayout(Context context, AttributeSet attrs, int defStyleAttr) + { super(context, attrs, defStyleAttr); init(); } @TargetApi(21) - public FixedSubmitBarLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public FixedSubmitBarLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) + { super(context, attrs, defStyleAttr, defStyleRes); init(); } public abstract int getContentResourceId(); - private void init() { + private void init() + { // Init root - LayoutInflater inflater = LayoutInflater.from(getContext()); - inflater.inflate(R.layout.rsb_layout_fixed_submit_bar, this, true); + layoutInflater = LayoutInflater.from(getContext()); + layoutInflater.inflate(getFixedSubmitBarLayoutId(), this, true); // Add contentContainer to the layout - ViewGroup contentContainer = (ViewGroup) findViewById(R.id.rsb_content_container); - View content = inflater.inflate(getContentResourceId(), contentContainer, false); + contentContainer = findViewById(getContentContainerLayoutId()); + + View content = layoutInflater.inflate(getContentResourceId(), contentContainer, false); contentContainer.addView(content, 0); // Init scrollview and submit bar guide positioning - final View submitBarGuide = findViewById(R.id.rsb_submit_bar_guide); - final SubmitBar submitBar = (SubmitBar) findViewById(R.id.rsb_submit_bar); - ObservableScrollView scrollView = (ObservableScrollView) findViewById(R.id.rsb_content_container_scrollview); - scrollView.setScrollListener(scrollY -> onScrollChanged(scrollView, submitBarGuide, submitBar)); - scrollView.getViewTreeObserver() - .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - scrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - - // Set submitBarGuide the same height as submitBar - if (submitBarGuide.getHeight() != submitBar.getHeight()) { - submitBarGuide.getLayoutParams().height = submitBar.getHeight(); - submitBarGuide.requestLayout(); + submitBarGuide = findViewById(R.id.rsb_submit_bar_guide); + submitBar = (SubmitBar) findViewById(R.id.rsb_submit_bar); + + if (submitBarGuide == null) { + // This must be a custom layout + return; + } + + scrollView = findViewById(R.id.rsb_content_container_scrollview); + // Sub-classes can create layouts without scrollview for fullscreen behavior + if (scrollView != null) { + scrollView.setScrollListener(scrollY -> onScrollChanged(scrollView, submitBarGuide, submitBar)); + scrollView.getViewTreeObserver() + .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + scrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + + // Set submitBarGuide the same height as submitBar + if (submitBarGuide.getHeight() != submitBar.getHeight()) { + submitBarGuide.getLayoutParams().height = submitBar.getHeight(); + submitBarGuide.requestLayout(); + } + + onScrollChanged(scrollView, submitBarGuide, submitBar); } + }); + } + } - onScrollChanged(scrollView, submitBarGuide, submitBar); - } - }); + public @IdRes int getContentContainerLayoutId() { + return R.id.rsb_content_container; } - private void onScrollChanged(ScrollView scrollView, View submitBarGuide, View submitBar) { + public @LayoutRes int getFixedSubmitBarLayoutId() { + return R.layout.rsb_layout_fixed_submit_bar; + } + + private void onScrollChanged(ScrollView scrollView, View submitBarGuide, View submitBar) + { int scrollY = scrollView.getScrollY(); int guidePosition = submitBarGuide.getTop() - scrollY; int guideHeight = submitBarGuide.getHeight(); int yLimit = scrollView.getHeight() - guideHeight; - if (guidePosition <= yLimit) { + if(guidePosition <= yLimit) + { ViewCompat.setTranslationY(submitBar, 0); - } else { + } + else + { int translationY = guidePosition - yLimit; ViewCompat.setTranslationY(submitBar, translationY); } + } + + /** + * Calculated the remaining height left in the container, that if filled, + * would make the scrollview be "fillviewport" + * We avoid fillviewport however because it interferes with the submitbar + * @param heightCalculatedListener will be called once height is calculated + */ + protected void remainingHeightOfContainer(final HeightCalculatedListener heightCalculatedListener) { + float submitbarY = submitBar.getY(); + int containerHeight = contentContainer.getHeight(); + + // Views have not been laid out yet + if (containerHeight <= 0) { + submitBar.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() + { + @Override + public void onGlobalLayout() { + submitBar.getViewTreeObserver().removeOnGlobalLayoutListener(this); + + float submitbarY = submitBar.getY(); + int containerHeight = contentContainer.getHeight(); + + heightCalculatedListener.heightCalculated((int)submitbarY - containerHeight); + } + }); + } else { + heightCalculatedListener.heightCalculated((int)submitbarY - containerHeight); + } + } + + public SubmitBar getSubmitBar() { + return submitBar; + } + public interface HeightCalculatedListener { + void heightCalculated(int height); } -} +} \ No newline at end of file diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/IconTab.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/IconTab.java index 772edb7d0..c8b5d71ce 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/views/IconTab.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/IconTab.java @@ -3,9 +3,9 @@ import android.annotation.TargetApi; import android.content.Context; import android.graphics.drawable.Drawable; -import android.support.annotation.DrawableRes; -import android.support.annotation.StringRes; -import android.support.v4.graphics.drawable.DrawableCompat; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; +import androidx.core.graphics.drawable.DrawableCompat; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/IconTabLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/IconTabLayout.java index 65f598a09..4258530ad 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/views/IconTabLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/IconTabLayout.java @@ -4,8 +4,8 @@ import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Drawable; -import android.support.annotation.Nullable; -import android.support.design.widget.TabLayout; +import androidx.annotation.Nullable; +import com.google.android.material.tabs.TabLayout; import android.util.AttributeSet; import org.researchstack.backbone.R; diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/PinCodeLayout.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/PinCodeLayout.java index b6c4a6518..ea58a0bcb 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/views/PinCodeLayout.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/PinCodeLayout.java @@ -1,7 +1,8 @@ package org.researchstack.backbone.ui.views; import android.content.Context; -import android.support.annotation.CallSuper; +import androidx.annotation.CallSuper; +import androidx.appcompat.widget.AppCompatButton; import android.text.InputFilter; import android.util.AttributeSet; import android.view.LayoutInflater; @@ -26,6 +27,7 @@ public class PinCodeLayout extends RelativeLayout { protected TextView title; protected EditText editText; protected View progress; + protected AppCompatButton forgotPasscodeButton; public PinCodeLayout(Context context) { super(context); @@ -61,7 +63,7 @@ protected void init() { editText.setInputType(pinType.getInputType() | pinType.getVisibleVariationType(false)); char[] chars = new char[config.getPinLength()]; - Arrays.fill(chars, '◦'); + Arrays.fill(chars, (char) 9702); editText.setHint(new String(chars)); InputFilter[] filters = ViewUtils.addFilter(editText.getFilters(), @@ -70,6 +72,8 @@ protected void init() { editText.setFilters(filters); progress = findViewById(R.id.progress); + + forgotPasscodeButton = (AppCompatButton)findViewById(R.id.rsb_forgot_passcode_button); } public void resetSummaryText() { @@ -85,4 +89,7 @@ public void showProgress(boolean show) { progress.setVisibility(show ? VISIBLE : GONE); } + public AppCompatButton getForgotPasscodeButton() { + return forgotPasscodeButton; + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/SignatureView.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/SignatureView.java index c06faa3d1..d6f14dd6d 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/views/SignatureView.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/SignatureView.java @@ -14,7 +14,7 @@ import android.graphics.drawable.shapes.PathShape; import android.os.Parcel; import android.os.Parcelable; -import android.support.v4.view.ViewCompat; +import androidx.core.view.ViewCompat; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -251,6 +251,8 @@ public void setCallbacks(SignatureCallbacks callbacks) { * {@link android.graphics.PathMeasure} class and get minX and minY from that. *

    * 2. Scale bitmap down. Currently drawing at density of device. + * + * @return the signature bitmap */ public Bitmap createSignatureBitmap() { diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/StepSwitcher.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/StepSwitcher.java index b2b44daa8..635473344 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/views/StepSwitcher.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/StepSwitcher.java @@ -95,12 +95,26 @@ private void init() { * {@link StepSwitcher#SHIFT_LEFT} or {@link StepSwitcher#SHIFT_RIGHT} */ public void show(StepLayout stepLayout, int direction) { + show(stepLayout, direction, false); + } + + /** + * Adds a new step to the view hierarchy. If a step is currently showing, the direction + * parameter is used to indicate which direction(x-axis) that the views should animate to. + * + * @param stepLayout the step you want to switch to + * @param direction the direction of the animation in the x direction. This values can either be + * {@link StepSwitcher#SHIFT_LEFT} or {@link StepSwitcher#SHIFT_RIGHT} + * @param alwaysReplaceView if true, even if the view have the same step id, they will be replaced + * useful if you are trying to refresh a step view with different UI state + */ + public void show(StepLayout stepLayout, int direction, boolean alwaysReplaceView) { // if layouts originate from the same step, ignore show View currentStep = findViewById(R.id.rsb_current_step); if (currentStep != null) { String currentStepId = (String) currentStep.getTag(R.id.rsb_step_layout_id); String stepLayoutId = (String) stepLayout.getLayout().getTag(R.id.rsb_step_layout_id); - if (currentStepId.equals(stepLayoutId)) { + if (currentStepId.equals(stepLayoutId) && !alwaysReplaceView) { return; } } @@ -143,15 +157,7 @@ public void show(StepLayout stepLayout, int direction) { .setDuration(animationTime) .translationX(-1 * newTranslationX) .withEndAction(() -> { - InputMethodManager imm = (InputMethodManager) getContext() - .getSystemService(Activity.INPUT_METHOD_SERVICE); - - if (imm.isActive() && imm.isAcceptingText()) { - imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0); - } - removeView(currentStep); - }); } }); diff --git a/backbone/src/main/java/org/researchstack/backbone/ui/views/SubmitBar.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/SubmitBar.java index a927b53f9..f17cf8a94 100644 --- a/backbone/src/main/java/org/researchstack/backbone/ui/views/SubmitBar.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/SubmitBar.java @@ -2,8 +2,8 @@ import android.content.Context; import android.content.res.TypedArray; -import android.support.annotation.Nullable; -import android.support.v4.content.ContextCompat; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -18,8 +18,12 @@ import rx.functions.Action1; public class SubmitBar extends LinearLayout { + + private static final float DEFAULT_DISABLED_OPACITY = 0.4f; + private TextView positiveView; private TextView negativeView; + private float disabledOpacity = DEFAULT_DISABLED_OPACITY; public SubmitBar(Context context) { this(context, null); @@ -54,6 +58,10 @@ public SubmitBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr a.recycle(); } + public void setDisabledOpacity(float disabledOpacity) { + this.disabledOpacity = disabledOpacity; + } + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Positive Action Helper Methods //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -74,6 +82,11 @@ public View getPositiveActionView() { return positiveView; } + public void setPositiveActionViewEnabled(boolean enabled) { + positiveView.setEnabled(enabled); + positiveView.setAlpha(enabled ? 1.0f : disabledOpacity); + } + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Negative Action Helper Methods //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -95,4 +108,8 @@ public View getNegativeActionView() { return negativeView; } + public void setNegativeActionViewEnabled(boolean enabled) { + negativeView.setEnabled(enabled); + negativeView.setAlpha(enabled ? 1.0f : disabledOpacity); + } } diff --git a/skin/src/main/java/org/researchstack/skin/ui/adapter/TextWatcherAdapter.java b/backbone/src/main/java/org/researchstack/backbone/ui/views/TextWatcherAdapter.java similarity index 89% rename from skin/src/main/java/org/researchstack/skin/ui/adapter/TextWatcherAdapter.java rename to backbone/src/main/java/org/researchstack/backbone/ui/views/TextWatcherAdapter.java index 20e0b38af..dff8355ba 100644 --- a/skin/src/main/java/org/researchstack/skin/ui/adapter/TextWatcherAdapter.java +++ b/backbone/src/main/java/org/researchstack/backbone/ui/views/TextWatcherAdapter.java @@ -1,10 +1,9 @@ -package org.researchstack.skin.ui.adapter; +package org.researchstack.backbone.ui.views; import android.text.Editable; import android.text.TextWatcher; public class TextWatcherAdapter implements TextWatcher { - @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { diff --git a/skin/src/main/java/org/researchstack/skin/utils/ConsentFormUtils.java b/backbone/src/main/java/org/researchstack/backbone/utils/ConsentFormUtils.java similarity index 88% rename from skin/src/main/java/org/researchstack/skin/utils/ConsentFormUtils.java rename to backbone/src/main/java/org/researchstack/backbone/utils/ConsentFormUtils.java index b35056223..fac1a3f28 100644 --- a/skin/src/main/java/org/researchstack/skin/utils/ConsentFormUtils.java +++ b/backbone/src/main/java/org/researchstack/backbone/utils/ConsentFormUtils.java @@ -1,17 +1,14 @@ -package org.researchstack.skin.utils; +package org.researchstack.backbone.utils; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Environment; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import org.researchstack.backbone.ui.ViewWebDocumentActivity; -import org.researchstack.backbone.utils.LogExt; -import org.researchstack.backbone.utils.ObservableUtils; -import org.researchstack.backbone.utils.ResUtils; -import org.researchstack.skin.R; -import org.researchstack.skin.ResourceManager; +import org.researchstack.backbone.R; +import org.researchstack.backbone.ResourceManager; import java.io.File; import java.io.FileOutputStream; @@ -41,19 +38,20 @@ public static void shareConsentForm(Context context) { int stringId = context.getApplicationInfo().labelRes; String appName = context.getString(stringId); String emailSubject = context.getResources() - .getString(R.string.rss_study_overview_email_subject, appName); + .getString(R.string.rsb_study_overview_email_subject, appName); Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("message/rfc822"); intent.putExtra(Intent.EXTRA_SUBJECT, emailSubject); intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile((File) o)); - String title = context.getString(R.string.rss_send_email); + String title = context.getString(R.string.rsb_send_email); context.startActivity(Intent.createChooser(intent, title)); }); } /** + * @param context can be activity or application * @return Consent form pdf */ @NonNull diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/ConsentQuizQuestionUtils.java b/backbone/src/main/java/org/researchstack/backbone/utils/ConsentQuizQuestionUtils.java new file mode 100644 index 000000000..7f900dd97 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/utils/ConsentQuizQuestionUtils.java @@ -0,0 +1,65 @@ +package org.researchstack.backbone.utils; + +import android.content.Context; +import android.util.Log; + +import org.researchstack.backbone.R; +import org.researchstack.backbone.model.Choice; +import org.researchstack.backbone.model.ConsentQuizModel; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 12/15/16. + * + * This class the business logic of interacting with the ConsentQuizModel object + */ + +@Deprecated // We no long need this class now that we have deprecated ConsentQuizModel +public class ConsentQuizQuestionUtils { + + static final String LOG_TAG = ConsentQuizQuestionUtils.class.getCanonicalName(); + + /** + * @param ctx - Used to access String resources when building question Strings + * @param question - Question to create choices from + * @return A list of choices which can be fabricated in various ways + */ + public static List createChoices(Context ctx, ConsentQuizModel.QuizQuestion question) { + + if (question == null) { + Log.e(LOG_TAG, "Question was null, returning empty list"); + return new ArrayList<>(); + } + + switch (question.getType()) { + case BOOLEAN: + return createBooleanChoices(ctx, question); + case SINGLE_CHOICE_TEXT: + return createSingleTextChoices(question); + } + + return new ArrayList<>(); + } + + static List createSingleTextChoices(ConsentQuizModel.QuizQuestion question) { + if (question.getItems() != null && !question.getItems().isEmpty()) { + return new ArrayList<>(question.getItems()); + } else if (question.getTextChoices() != null && !question.getTextChoices().isEmpty()) { + List choices = new ArrayList(); + for (int i = 0; i < question.getTextChoices().size(); i++) { + choices.add(new Choice(question.getTextChoices().get(i), String.valueOf(i))); + } + return choices; + } + return new ArrayList<>(); + } + + static List createBooleanChoices(Context ctx, ConsentQuizModel.QuizQuestion question) { + List choices = new ArrayList(); + choices.add(new Choice<>(ctx.getString(R.string.rsb_btn_true), "true")); + choices.add(new Choice<>(ctx.getString(R.string.rsb_btn_false), "false")); + return choices; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/FileUtils.java b/backbone/src/main/java/org/researchstack/backbone/utils/FileUtils.java index e3d9954b3..8b498430b 100644 --- a/backbone/src/main/java/org/researchstack/backbone/utils/FileUtils.java +++ b/backbone/src/main/java/org/researchstack/backbone/utils/FileUtils.java @@ -1,6 +1,6 @@ package org.researchstack.backbone.utils; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import org.researchstack.backbone.storage.file.StorageAccessException; diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/FormatHelper.java b/backbone/src/main/java/org/researchstack/backbone/utils/FormatHelper.java index 721cdfbdc..f2cfba6bf 100644 --- a/backbone/src/main/java/org/researchstack/backbone/utils/FormatHelper.java +++ b/backbone/src/main/java/org/researchstack/backbone/utils/FormatHelper.java @@ -1,5 +1,8 @@ package org.researchstack.backbone.utils; +import android.content.Context; + +import org.researchstack.backbone.R; import org.researchstack.backbone.ui.step.layout.ConsentSignatureStepLayout; import java.text.DateFormat; @@ -8,6 +11,12 @@ public class FormatHelper { + private static final String COUNTRY_US = "US"; + private static final String COUNTRY_LIBERIA = "LR"; + private static final String COUNTRY_BURMA = "MM"; + + private static final double FEET_PER_METER = 3.28084; + public static final int NONE = -1; public static final String DATE_FORMAT_ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; public static final SimpleDateFormat DEFAULT_FORMAT = new SimpleDateFormat(FormatHelper.DATE_FORMAT_ISO_8601, @@ -16,6 +25,10 @@ public class FormatHelper { public static final SimpleDateFormat SIMPLE_FORMAT_DATE = new SimpleDateFormat( DATE_FORMAT_SIMPLE_DATE, Locale.getDefault()); + public static final String TIME_FORMAT_SIMPLE_DATE = "HH:mm:ss.sss"; + public static final SimpleDateFormat SIMPLE_FORMAT_TIME = new SimpleDateFormat( + TIME_FORMAT_SIMPLE_DATE, + Locale.getDefault()); private FormatHelper() { } @@ -62,4 +75,17 @@ public static boolean isStyle(int style) { return style >= DateFormat.FULL && style <= DateFormat.SHORT; } + public static String localizeDistance(Context context, double distanceInMeters, Locale currentLocale) { + String countryCode = currentLocale.getCountry(); + switch (countryCode) { + case COUNTRY_US: + case COUNTRY_LIBERIA: + case COUNTRY_BURMA: // in feet + return String.format(currentLocale, "%.01f %s", (distanceInMeters * FEET_PER_METER), + context.getString(R.string.rsb_distance_feet)); + default: // in meters + return String.format(currentLocale, "%.01f %s", distanceInMeters, + context.getString(R.string.rsb_distance_meters)); + } + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/ObjectUtils.java b/backbone/src/main/java/org/researchstack/backbone/utils/ObjectUtils.java new file mode 100644 index 000000000..7b740b046 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/utils/ObjectUtils.java @@ -0,0 +1,37 @@ +package org.researchstack.backbone.utils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +/** + * Created by TheMDP on 12/29/16. + */ + +public class ObjectUtils { + + /** + * @param copyObject the Object to copy, which must implement interface Serializable + * and all classes, subclasses, and member field classes must + * implement a default package-level constructor, or exception will be thrown + * @return deep object copy + */ + public static Object clone(Object copyObject) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(copyObject); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + Object deepCopy = ois.readObject(); + return deepCopy; + } catch (IOException e) { + e.printStackTrace(); + } catch(ClassNotFoundException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/OptionSetUtils.java b/backbone/src/main/java/org/researchstack/backbone/utils/OptionSetUtils.java new file mode 100644 index 000000000..b49f93003 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/utils/OptionSetUtils.java @@ -0,0 +1,27 @@ +package org.researchstack.backbone.utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 3/7/17. + */ + +public class OptionSetUtils { + + /** + * @param optionSetBitMask the the bit mask representing a list of enum options + * @param enumOptions this should always be the result of your Enum.values(); + * @param the type of the enum + * @return a list of enums that are represented by the optionSet + */ + public static List toEnumList(int optionSetBitMask, E[] enumOptions) { + List enumList = new ArrayList<>(); + for (int i = 0; i < enumOptions.length; i++) { + if ((optionSetBitMask & (0x1 << i)) != 0) { + enumList.add(enumOptions[i]); + } + } + return enumList; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/ResUtils.java b/backbone/src/main/java/org/researchstack/backbone/utils/ResUtils.java index 07010fae8..de38e36f0 100644 --- a/backbone/src/main/java/org/researchstack/backbone/utils/ResUtils.java +++ b/backbone/src/main/java/org/researchstack/backbone/utils/ResUtils.java @@ -1,11 +1,84 @@ package org.researchstack.backbone.utils; import android.content.Context; +import androidx.annotation.RequiresApi; import org.researchstack.backbone.StorageAccess; +import java.util.Locale; + public class ResUtils { + /** + * Since we cannot reference R.drawable.X integer references from JSON + * The Drawable resource must also be available by String lookup + */ + public static final String LOGO_DISEASE = "logo_disease"; + public static final String TWITTER_ICON = "rsb_ic_twitter_icon"; + public static final String FACEBOOK_ICON = "rsb_ic_facebook_icon"; + public static final String EMAIL_ICON = "rsb_ic_email_icon"; + public static final String SMS_ICON = "rsb_ic_sms_icon"; + public static final String ERROR_ICON = "rsb_error"; + public static final String IC_FINGERPRINT = "rsb_fingerprint"; + public static final String PHONE_IN_POCKET = "rsb_phone_in_pocket"; + public static final String TIMER = "rsb_timer"; + + public static class Audio { + public static final String PHONE_WAVES = "rsb_phonewaves"; + public static final String PHONE_SOUND_WAVES = "rsb_phonesoundwaves"; + } + + public static class Tapping { + public static final String PHONE_TAPPING_NO_TAP = "rsb_tapping_phone_notap_words"; + public static final String ANIMATED_TAPPING_RIGHT = "rsb_animated_tapping_right"; + public static final String ANIMATED_TAPPING_LEFT = "rsb_animated_tapping_left"; + } + + public static class TimedWalking { + public static final String TURNAROUND = "rsb_timed_walking_turnaround"; + public static final String MAN_RETURN = "rsb_timed_walking_man_return"; + public static final String MAN_OUTBOUND = "rsb_timed_walking_man_outbound"; + } + + public static class Tremor { + public static final String IN_HAND = "rsb_tremor_in_hand"; + public static final String IN_HAND_2 = "rsb_tremor_in_hand_2"; + public static final String HAND_IN_LAP = "rsb_tremor_hand_in_lap"; + public static final String HAND_OUT = "rsb_tremor_hand_out"; + public static final String ELBOW_BENT = "rsb_tremor_elbow_bent"; + public static final String HAND_TO_NOSE = "rsb_tremor_hand_to_nose"; + public static final String QUEEN_WAVE = "rsb_tremor_queen_wave"; + public static final String IN_HAND_FLIPPED = "rsb_tremor_in_hand_flipped"; + public static final String IN_HAND_2_FLIPPED = "rsb_tremor_in_hand_2_flipped"; + public static final String HAND_IN_LAP_FLIPPED = "rsb_tremor_hand_in_lap_flipped"; + public static final String HAND_OUT_FLIPPED = "rsb_tremor_hand_out_flipped"; + public static final String ELBOW_BENT_FLIPPED = "rsb_tremor_elbow_bent_flipped"; + public static final String HAND_TO_NOSE_FLIPPED = "rsb_tremor_hand_to_nose_flipped"; + public static final String QUEEN_WAVE_FLIPPED = "rsb_tremor_queen_wave_flipped"; + } + + public static class MoodSurvey { + public static final String OVERALL = "rsb_mood_survey_mood"; + public static final String CLARITY = "rsb_mood_survey_clarity"; + public static final String PAIN = "rsb_mood_survey_pain"; + public static final String SLEEP = "rsb_mood_survey_sleep"; + public static final String EXERCISE = "rsb_mood_survey_exercise"; + public static final String CUSTOM = "rsb_mood_survey_custom"; + + /** + * @param root One of the constants above, like, OVERALL, PAIN, etc. + * @param index the index of the image (1 through 5) + * @return The full drawable image res based on the root + */ + public static String normal(String root, int index) { + return String.format(Locale.getDefault(), "%s_%dg", root, index); + } + } + + // AnimatedVectorDrawable 's + public static final String ANIMATED_CHECK_MARK_DELAYED = "rsb_animated_check_delayed"; + public static final String ANIMATED_CHECK_MARK = "rsb_animated_check"; + public static final String ANIMATED_FINGERPRINT = "rsb_animated_fingerprint"; private ResUtils() { } @@ -40,10 +113,30 @@ public static String getRawFilePath(String docName, String postfix) { return "file:///android_res/raw/" + docName + "." + postfix; } + public static int getColorResourceId(Context context, String name) + { + return context.getResources().getIdentifier(name, "color", context.getPackageName()); + } + + public static int getStringResourceId(Context context, String name) + { + return context.getResources().getIdentifier(name, "string", context.getPackageName()); + } + public static int getDrawableResourceId(Context context, String name) { return getDrawableResourceId(context, name, 0); } + /** + * @param context can be app or activity + * @param name of the dimen resource + * @return the dimension resource to use in resources.getDimensionPixelOffset + */ + public static int getDimenResourceId(Context context, String name) + { + return context.getResources().getIdentifier(name, "dimen", context.getPackageName()); + } + public static int getDrawableResourceId(Context context, String name, int defaultResId) { if (name == null || name.length() == 0) { return defaultResId; diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/SerializableUtils.java b/backbone/src/main/java/org/researchstack/backbone/utils/SerializableUtils.java new file mode 100644 index 000000000..c26bcb185 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/utils/SerializableUtils.java @@ -0,0 +1,57 @@ +package org.researchstack.backbone.utils; + +import android.util.Base64; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * SerializableUtils can be used to save serializable objects to SharedPrefs + * It is better than using gson because it can properly handle sub-classes + */ + +public class SerializableUtils { + + /** + * Converts a serializable object to a Base64 string that can be stored in SharedPrefs + * @param object to convert to a base 64 string + * @return a base 64 string representing the object, null if something went wrong + */ + public static String toBase64String(Serializable object) { + String encoded = null; + try { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + objectOutputStream.writeObject(object); + objectOutputStream.close(); + encoded = Base64.encodeToString(byteArrayOutputStream.toByteArray(), 0); + } catch (IOException e) { + LogExt.e(SerializableUtils.class, e.getLocalizedMessage()); + } + return encoded; + } + + /** + * @param base64String to convert into a serializable object + * @return the serializable object, or null if something went wrong + */ + public static Serializable fromBase64String(String base64String) { + byte[] bytes = Base64.decode(base64String,0); + Serializable object = null; + try { + ObjectInputStream objectInputStream = new ObjectInputStream( new ByteArrayInputStream(bytes) ); + object = (Serializable)objectInputStream.readObject(); + } catch (IOException e) { + LogExt.e(SerializableUtils.class, e.getLocalizedMessage()); + } catch (ClassNotFoundException e) { + LogExt.e(SerializableUtils.class, e.getLocalizedMessage()); + } catch (ClassCastException e) { + LogExt.e(SerializableUtils.class, e.getLocalizedMessage()); + } + return object; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/StepHelper.java b/backbone/src/main/java/org/researchstack/backbone/utils/StepHelper.java new file mode 100644 index 000000000..06ab0f241 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/utils/StepHelper.java @@ -0,0 +1,157 @@ +package org.researchstack.backbone.utils; + +import android.util.Log; + +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; +import org.researchstack.backbone.step.FormStep; +import org.researchstack.backbone.step.NavigationExpectedAnswerQuestionStep; +import org.researchstack.backbone.step.QuestionStep; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.step.SubtaskStep; +import org.researchstack.backbone.task.OrderedTask; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by TheMDP on 1/15/17. + * + * TODO: unit tests + */ + +public class StepHelper { + + private static final String LOG_TAG = StepHelper.class.getCanonicalName(); + + /** + * /** + * This helper method is used by navigation steps to share the algorithm of + * the nextStepIdentifier method implementation + * @param skipToStepIdentifier the identifier to skip to if conditions pass + * @param skipIfPassed used to determine skip identifier if quiz is all passed or all failed + * @param formSteps sub steps to be compared to with expected answers + * @param result result to search for actual answers to compare to expected answers + * @param additionalTaskResults additional results + * @return identifier of next step if conditions are met, null if next step should be determined else where + */ + public static String navigationFormStepSkipIdentifier( + String skipToStepIdentifier, + boolean skipIfPassed, + List formSteps, + TaskResult result, + List additionalTaskResults) + { + if (skipToStepIdentifier == null) { + return null; + } + boolean allPassed = true; + for (QuestionStep step : formSteps) { + // Only perform search on navigation question steps that have expected answers + if (step instanceof NavigationExpectedAnswerQuestionStep) { + NavigationExpectedAnswerQuestionStep navStep = (NavigationExpectedAnswerQuestionStep)step; + boolean navStepPassed = containsMatchingAnswer( + navStep.getExpectedAnswer(), navStep.getIdentifier(), + result, additionalTaskResults); + if (!navStepPassed) { + allPassed = false; + } + } + } + if (allPassed && skipIfPassed || + !allPassed && !skipIfPassed) + { + return skipToStepIdentifier; + } + return null; + } + + /** + * @param formStep the form step containing quesiton steps + * @param result the result of the task so far + * @return true if form step was skipped optionally (all results are null), false otherwise + */ + public static boolean wasFormStepSkipped(FormStep formStep, TaskResult result) { + + List stepIdentifiersToCheck = new ArrayList<>(); + stepIdentifiersToCheck.add(formStep.getIdentifier()); + if (formStep.getFormSteps() != null) { + for (QuestionStep step: formStep.getFormSteps()) { + stepIdentifiersToCheck.add(step.getIdentifier()); + } + } + + for (String identifier: stepIdentifiersToCheck) { + StepResult stepResult = StepResultHelper.findStepResult(result, identifier); + if (stepResult != null && stepResult.getResult() != null) { + return false; + } + } + + return true; + } + + /** + * @param expectedAnswer expected answer of step + * @param stepIdentifier the step's identifier + * @param result task results + * @param additionalTaskResults additional results + * @return true if answer in result is our expected answer, false for all other cases + */ + private static boolean containsMatchingAnswer( + Object expectedAnswer, + String stepIdentifier, + TaskResult result, + List additionalTaskResults) + { + if (expectedAnswer == null) { + return false; + } + + + for (String stepId : result.getResults().keySet()) { + StepResult stepResult = StepResultHelper.findStepResult(result.getStepResult(stepId), stepIdentifier); + // We find an ID match + if (stepResult != null) { + if (!stepResult.getResults().isEmpty()) { + if (stepResult.getResults().size() > 1) { + Log.d(LOG_TAG, "This is currently only supported for " + + "StepResults with one result, looking at first result instead"); + } + Object answer = stepResult.getResults().values().toArray()[0]; + return expectedAnswer.equals(answer); + } + } + } + return false; + } + + /** + * @param stepList to be searched for step identifier + * @param stepId the search parameter + * @return the step in the list matching stepId, null if none found with stepId + */ + public static Step getStepWithIdentifier(List stepList, String stepId) { + if (stepList == null || stepId == null) { + return null; + } + for (Step step : stepList) { + if (stepId.equals(step.getIdentifier())) { + return step; + } + // A step can contain a task itself, so check for this as well + if (step instanceof SubtaskStep) { + SubtaskStep subtaskStep = (SubtaskStep)step; + if (subtaskStep.getSubtask() != null && + subtaskStep.getSubtask() instanceof OrderedTask) { + OrderedTask task = (OrderedTask)subtaskStep.getSubtask(); + Step foundSubtaskStep = getStepWithIdentifier(task.getSteps(), stepId); + if (foundSubtaskStep != null) { + return foundSubtaskStep; + } + } + } + } + return null; + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/StepLayoutHelper.java b/backbone/src/main/java/org/researchstack/backbone/utils/StepLayoutHelper.java new file mode 100644 index 000000000..bdc6c0e57 --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/utils/StepLayoutHelper.java @@ -0,0 +1,141 @@ +package org.researchstack.backbone.utils; + +import android.content.Context; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import android.view.View; + +import org.researchstack.backbone.DataResponse; +import org.researchstack.backbone.step.Step; +import org.researchstack.backbone.ui.step.layout.StepLayout; +import org.researchstack.backbone.ui.views.AlertFrameLayout; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; + +import rx.Observable; +import rx.android.schedulers.AndroidSchedulers; + +/** + * Created by TheMDP on 1/16/17. + */ + +public class StepLayoutHelper { + + @NonNull + @MainThread + public static StepLayout createLayoutFromStep(Step step, Context context) + { + try + { + Class cls = step.getStepLayoutClass(); + Constructor constructor = cls.getConstructor(Context.class); + return (StepLayout) constructor.newInstance(context); + } + catch(Exception e) + { + throw new RuntimeException(e); + } + } + + /** + * @param observable that is performing a web call, or asynchronous operation + * @param viewPerforming the view that is making the observable call + * @param callback will be invoked if the observable is invoked, and the view is in a valid state + */ + @MainThread + public static void safePerform( + Observable observable, + View viewPerforming, + final WebCallback callback) + { + final WeakReference weakView = new WeakReference<>(viewPerforming); + observable.observeOn(AndroidSchedulers.mainThread()).subscribe(dataResponse -> { + // Controls canceling an observable perform through weak reference to the view + if (weakView == null || weakView.get() == null || weakView.get().getContext() == null) { + return; // no callback + } + callback.onSuccess(dataResponse); + }, throwable -> { + // Controls canceling an observable perform through weak reference to the view + if (weakView == null || weakView.get() == null || weakView.get().getContext() == null) { + return; // no callback + } + callback.onFail(throwable); + }); + } + + /** + * This is the same as safePerform except all loading dialogs and error dialogs are + * shown automatically by this method + * + * @param observable that is performing a web call, or asynchronous operation + * @param viewPerforming the view that is making the observable call + * @param callback will be invoked if the observable is invoked, and the view is in a valid state + */ + @MainThread + public static void safePerformWithAlerts( + Observable observable, + AlertFrameLayout viewPerforming, + final WebSuccessCallback callback) + { + viewPerforming.showLoadingDialog(); + final WeakReference weakView = new WeakReference<>(viewPerforming); + safePerform(observable, viewPerforming, new WebCallback() { + @Override + public void onSuccess(DataResponse response) { + weakView.get().hideLoadingDialog(); + if (response.isSuccess()) { + callback.onSuccess(response); + } else { + weakView.get().showOkAlertDialog(response.getMessage()); + } + } + + @Override + public void onFail(Throwable throwable) { + weakView.get().hideLoadingDialog(); + weakView.get().showOkAlertDialog(throwable.getMessage()); + } + }); + } + + /** + * This is the same as safePerform except all loading dialogs are shown automatically + * + * @param observable that is performing a web call, or asynchronous operation + * @param viewPerforming the view that is making the observable call + * @param callback will be invoked if the observable is invoked, and the view is in a valid state + */ + @MainThread + public static void safePerformWithOnlyLoadingAlerts( + Observable observable, + AlertFrameLayout viewPerforming, + final WebCallback callback) + { + viewPerforming.showLoadingDialog(); + final WeakReference weakView = new WeakReference<>(viewPerforming); + safePerform(observable, viewPerforming, new WebCallback() { + @Override + public void onSuccess(DataResponse response) { + weakView.get().hideLoadingDialog(); + callback.onSuccess(response); + } + + @Override + public void onFail(Throwable throwable) { + weakView.get().hideLoadingDialog(); + callback.onFail(throwable); + } + }); + } + + public interface WebCallback { + void onSuccess(DataResponse response); + void onFail(Throwable throwable); + } + + public interface WebSuccessCallback { + void onSuccess(DataResponse response); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/StepResultHelper.java b/backbone/src/main/java/org/researchstack/backbone/utils/StepResultHelper.java new file mode 100644 index 000000000..dc9d8b19a --- /dev/null +++ b/backbone/src/main/java/org/researchstack/backbone/utils/StepResultHelper.java @@ -0,0 +1,319 @@ +package org.researchstack.backbone.utils; + +import org.researchstack.backbone.result.Result; +import org.researchstack.backbone.result.StepResult; +import org.researchstack.backbone.result.TaskResult; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Created by TheMDP on 1/16/17. + */ + +public class StepResultHelper { + + /** + * @param taskResult the TaskResult to search within + * @param stepResultKey the identifier of the step to find + * @return a StepResult object within taskResult that has map key stepResultKey, null otherwise + */ + public static StepResult findStepResult(TaskResult taskResult, String stepResultKey) { + if (taskResult == null || taskResult.getResults() == null || stepResultKey == null) { + return null; + } + return findStepResult(taskResult.getResults().values(), stepResultKey); + } + + /** + * @param stepResultList the stepResultList to search within + * @param stepResultKey the identifier of the step to find + * @return a StepResult object within the list of stepResultList that has map key stepResultKey, null otherwise + */ + public static StepResult findStepResult(Collection stepResultList, String stepResultKey) { + if (stepResultList == null || stepResultKey == null) { + return null; + } + for (StepResult stepResult : stepResultList) { + StepResult foundResult = findStepResult(stepResult, stepResultKey); + if (foundResult != null) { + return foundResult; + } + } + return null; + } + + /** + * @param stepResultList The list of StepResults to search within + * @param stepResultKey The identifier of the step to find + * @return the index within stepResultList that has map key stepResultKey, -1 otherwise + */ + public static int indexOfStepResultKey(List stepResultList, String stepResultKey) { + if (stepResultList == null || stepResultList.isEmpty() || stepResultKey == null) { + return -1; + } + for (int i = 0; i < stepResultList.size(); i++) { + StepResult stepResult = stepResultList.get(i); + StepResult foundResult = findStepResult(stepResult, stepResultKey); + if (foundResult != null) { + return i; + } + } + return -1; + } + + /** + * @param result A StepResult object that may or may not have other nested StepResults + * @param stepResultKey the map key to find + * @return a StepResult object within result that has map key stepResultKey, null otherwise + */ + public static StepResult findStepResult(StepResult result, String stepResultKey) { + if (result == null || stepResultKey == null) { + return null; + } + if (result.getIdentifier().equals(stepResultKey)) { + return result; + } + Map results = result.getResults(); + for (Object stepId : results.keySet()) { + Object stepResultObj = results.get(stepId); + if (stepResultObj instanceof StepResult) { + StepResult stepResult = (StepResult)stepResultObj; + if (stepResultKey.equals(stepId)) { + return stepResult; + } else { + StepResult recursiveStepResult = findStepResult(stepResult, stepResultKey); + if (recursiveStepResult != null) { + return recursiveStepResult; + } + } + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * @param taskResult the TaskResult to search within + * @param stepIdentifier for result + * @return String object if exists, empty string otherwise + */ + public static String findStringResult(TaskResult taskResult, String stepIdentifier) { + if (taskResult == null || taskResult.getResults() == null || stepIdentifier == null) { + return null; + } + for (StepResult stepResult : taskResult.getResults().values()) { + String stringResult = findStringResult(stepIdentifier, stepResult); + if (stringResult != null) { + return stringResult; + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * @param stepIdentifier for result + * @param stepResult the step result to try and find the String result in + * @return String object if exists, empty string otherwise + */ + public static String findStringResult(String stepIdentifier, StepResult stepResult) { + StepResult idStepResult = findStepResult(stepResult, stepIdentifier); + if (idStepResult != null) { + Object resultValue = idStepResult.getResult(); + if (resultValue instanceof String) { + return (String) resultValue; + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * @param stepIdentifier for result + * @param stepResult the step result to try and find the boolean result in + * @return String object if exists, empty string otherwise + */ + public static Boolean findBooleanResult(String stepIdentifier, StepResult stepResult) { + StepResult idStepResult = findStepResult(stepResult, stepIdentifier); + if (idStepResult != null) { + Object resultValue = idStepResult.getResult(); + if (resultValue instanceof Boolean) { + return (Boolean) resultValue; + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * @param stepIdentifier for result + * @param taskResult the task result to try and find the boolean result in + * @return String object if exists, empty string otherwise + */ + public static Boolean findBooleanResult(String stepIdentifier, TaskResult taskResult) { + if (taskResult == null || taskResult.getResults() == null || stepIdentifier == null) { + return null; + } + for (StepResult stepResult : taskResult.getResults().values()) { + Boolean stringResult = findBooleanResult(stepIdentifier, stepResult); + if (stringResult != null) { + return stringResult; + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * @param stepIdentifier for result + * @param stepResult the step result to try and find the Number result in + * @return Number object if exists, empty string otherwise + */ + public static Number findNumberResult(String stepIdentifier, StepResult stepResult) { + StepResult idStepResult = findStepResult(stepResult, stepIdentifier); + if (idStepResult != null) { + Object resultValue = idStepResult.getResult(); + if (resultValue instanceof Number) { + return (Number) resultValue; + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * @param stepIdentifier for result + * @param taskResult the task result to try and find the Number result in + * @return Number object if exists, empty string otherwise + */ + public static Number findNumberResult(String stepIdentifier, TaskResult taskResult) { + if (taskResult == null || taskResult.getResults() == null || stepIdentifier == null) { + return null; + } + for (StepResult stepResult : taskResult.getResults().values()) { + Number result = findNumberResult(stepIdentifier, stepResult); + if (result != null) { + return result; + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * @param stepIdentifier for result + * @param stepResult the step result to try and find the integer result in + * @return Integer object if exists, empty string otherwise + */ + public static Integer findIntegerResult(String stepIdentifier, StepResult stepResult) { + StepResult idStepResult = findStepResult(stepResult, stepIdentifier); + if (idStepResult != null) { + Object resultValue = idStepResult.getResult(); + if (resultValue instanceof Integer) { + return (Integer) resultValue; + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * @param stepIdentifier for result + * @param taskResult the task result to try and find the integer result in + * @return Integer object if exists, empty string otherwise + */ + public static Integer findIntegerResult(String stepIdentifier, TaskResult taskResult) { + if (taskResult == null || taskResult.getResults() == null || stepIdentifier == null) { + return null; + } + for (StepResult stepResult : taskResult.getResults().values()) { + Integer result = findIntegerResult(stepIdentifier, stepResult); + if (result != null) { + return result; + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * @param stepIdentifier for result + * @param stepResult the step result to try and find the date result in + * @return String object if exists, empty string otherwise + */ + public static Date findDateResult(String stepIdentifier, StepResult stepResult) { + StepResult idStepResult = findStepResult(stepResult, stepIdentifier); + if (idStepResult != null) { + Object resultValue = idStepResult.getResult(); + if (resultValue instanceof Long) { + return new Date((Long)resultValue); + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * @param stepIdentifier for result + * @param taskResult the task result to try and find the date result in + * @return Date object if exists, empty string otherwise + */ + public static Date findDateResult(String stepIdentifier, TaskResult taskResult) { + if (taskResult == null || taskResult.getResults() == null || stepIdentifier == null) { + return null; + } + for (StepResult stepResult : taskResult.getResults().values()) { + Date result = findDateResult(stepIdentifier, stepResult); + if (result != null) { + return result; + } + } + return null; + } + + /** + * Only works with the DEFAULT Result identifier keys + * Will find the first Result with a specific class type from within a StepResult + * @param stepResult the step result to search within + * @param comparator a class comparator that will be provided by the caller + * this is how you check if the type is the one provided + * @param a generic type of class that can be used to avoid casting of return type + * @return null if result object of type exists somewhere, the object otherwise + */ + @SuppressWarnings("unchecked") // needed for unchecked generic type casting + public static T findResultOfClass(StepResult stepResult, ResultClassComparator comparator) { + if (stepResult == null) { + return null; + } + Map results = stepResult.getResults(); + for (Object stepId : results.keySet()) { + Object value = results.get(stepId); + if (comparator.isTypeOfClass(value)) { + return (T)value; + } else if (value instanceof StepResult) { + StepResult substepResult = (StepResult)value; + T recursiveResult = findResultOfClass(substepResult, comparator); + if (recursiveResult != null) { + return recursiveResult; + } + } + } + return null; + } + + /** + * @param subtaskId the id of the parent subtask that contains the nested step result + * @param stepResultId the step result id + * @return the fully qualified identifier for use within StepResultHelper functions + */ + public static String subtaskIdentifier(String subtaskId, String stepResultId) { + return subtaskId + "." + stepResultId; + } + + public static abstract class ResultClassComparator { + public abstract boolean isTypeOfClass(Object object); + } +} diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/TextUtils.java b/backbone/src/main/java/org/researchstack/backbone/utils/TextUtils.java index c658096cf..fdc603345 100644 --- a/backbone/src/main/java/org/researchstack/backbone/utils/TextUtils.java +++ b/backbone/src/main/java/org/researchstack/backbone/utils/TextUtils.java @@ -1,11 +1,22 @@ package org.researchstack.backbone.utils; +import android.content.Context; +import android.content.res.Configuration; import android.text.InputFilter; import android.text.Spanned; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.regex.Pattern; +import static android.content.Context.WINDOW_SERVICE; + public class TextUtils { + private static final String LOG_TAG = TextUtils.class.getCanonicalName(); + public static final Pattern EMAIL_ADDRESS = Pattern.compile( "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + "\\@" + @@ -78,4 +89,31 @@ public CharSequence filter(CharSequence source, int start, int end, Spanned dest return null; } } + + public static String urlEncode(String input) { + String output = null; + try + { + output = URLEncoder.encode(input, "UTF-8"); + return output; + } + catch(UnsupportedEncodingException uee) + { + LogExt.i(TextUtils.class, "Failed to url encode: " + uee.getMessage()); + } + return input; + } + + public static void adjustFontScale(Configuration configuration, Context context, float maxFontScale) { + if (configuration.fontScale > maxFontScale) { + Log.w(LOG_TAG, "fontScale=" + configuration.fontScale); //Custom Log class, you can use Log.w + Log.w(LOG_TAG, "font too big. scale down..."); //Custom Log class, you can use Log.w + configuration.fontScale = (float) maxFontScale; + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + WindowManager wm = (WindowManager) context.getSystemService(WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + metrics.scaledDensity = configuration.fontScale * metrics.density; + context.getResources().updateConfiguration(configuration, metrics); + } + } } diff --git a/backbone/src/main/java/org/researchstack/backbone/utils/ViewUtils.java b/backbone/src/main/java/org/researchstack/backbone/utils/ViewUtils.java index 2eb753cd3..f64fada16 100644 --- a/backbone/src/main/java/org/researchstack/backbone/utils/ViewUtils.java +++ b/backbone/src/main/java/org/researchstack/backbone/utils/ViewUtils.java @@ -3,10 +3,13 @@ import android.app.Activity; import android.content.Context; import android.os.Build; -import android.support.v4.app.Fragment; +import androidx.fragment.app.Fragment; import android.text.InputFilter; import android.text.InputType; +import android.util.DisplayMetrics; +import android.util.TypedValue; import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; @@ -17,6 +20,27 @@ public class ViewUtils { private ViewUtils() { } + /** + * @param parent the view to be search within + * @return the first EditText that is the parent, or is within the parent + */ + public static EditText findFirstEditText(View parent) { + if (parent instanceof EditText) { + return (EditText)parent; + } + if (!(parent instanceof ViewGroup)) { + return null; + } + ViewGroup parentViewGroup = (ViewGroup)parent; + for (int i = 0; i < parentViewGroup.getChildCount(); i++) { + EditText editText = findFirstEditText(parentViewGroup.getChildAt(i)); + if (editText != null) { + return editText; + } + } + return null; + } + public static InputFilter[] addFilter(InputFilter[] filters, InputFilter filter) { if (filters == null || filters.length == 0) { return new InputFilter[]{filter}; @@ -88,4 +112,13 @@ public static Fragment createFragment(Class fragmentClass) { } } + /** + * @param context can be app or activity, used for Resources class + * @param dp the size in dp as the input + * @return the dp converted to px for this device based on its display metrics + */ + public static int dpToPx(Context context, int dp) { + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, dp, displayMetrics); + } } diff --git a/backbone/src/main/res/anim/rsb_check_animation.xml b/backbone/src/main/res/anim/rsb_check_animation.xml new file mode 100644 index 000000000..fd109be32 --- /dev/null +++ b/backbone/src/main/res/anim/rsb_check_animation.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/anim/rsb_check_animation_delayed.xml b/backbone/src/main/res/anim/rsb_check_animation_delayed.xml new file mode 100644 index 000000000..d3e3ee32e --- /dev/null +++ b/backbone/src/main/res/anim/rsb_check_animation_delayed.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/anim/rsb_fingerprint_animation.xml b/backbone/src/main/res/anim/rsb_fingerprint_animation.xml new file mode 100644 index 000000000..66cfaed17 --- /dev/null +++ b/backbone/src/main/res/anim/rsb_fingerprint_animation.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/anim/rsb_hide_after_medium_delay.xml b/backbone/src/main/res/anim/rsb_hide_after_medium_delay.xml new file mode 100644 index 000000000..bd49aff23 --- /dev/null +++ b/backbone/src/main/res/anim/rsb_hide_after_medium_delay.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/anim/rsb_show_after_medium_delay.xml b/backbone/src/main/res/anim/rsb_show_after_medium_delay.xml new file mode 100644 index 000000000..b6daedc53 --- /dev/null +++ b/backbone/src/main/res/anim/rsb_show_after_medium_delay.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skin/src/main/res/drawable-hdpi/rss_ic_action_learn.png b/backbone/src/main/res/drawable-hdpi/rsb_ic_action_learn.png similarity index 100% rename from skin/src/main/res/drawable-hdpi/rss_ic_action_learn.png rename to backbone/src/main/res/drawable-hdpi/rsb_ic_action_learn.png diff --git a/skin/src/main/res/drawable-hdpi/rss_ic_action_settings.png b/backbone/src/main/res/drawable-hdpi/rsb_ic_action_settings.png similarity index 100% rename from skin/src/main/res/drawable-hdpi/rss_ic_action_settings.png rename to backbone/src/main/res/drawable-hdpi/rsb_ic_action_settings.png diff --git a/skin/src/main/res/drawable-hdpi/rss_ic_location_24dp.png b/backbone/src/main/res/drawable-hdpi/rsb_ic_location_24dp.png similarity index 100% rename from skin/src/main/res/drawable-hdpi/rss_ic_location_24dp.png rename to backbone/src/main/res/drawable-hdpi/rsb_ic_location_24dp.png diff --git a/skin/src/main/res/drawable-hdpi/rss_ic_notification_24dp.png b/backbone/src/main/res/drawable-hdpi/rsb_ic_notification_24dp.png similarity index 100% rename from skin/src/main/res/drawable-hdpi/rss_ic_notification_24dp.png rename to backbone/src/main/res/drawable-hdpi/rsb_ic_notification_24dp.png diff --git a/skin/src/main/res/drawable-hdpi/rss_ic_quiz_retry.png b/backbone/src/main/res/drawable-hdpi/rsb_ic_quiz_retry.png similarity index 100% rename from skin/src/main/res/drawable-hdpi/rss_ic_quiz_retry.png rename to backbone/src/main/res/drawable-hdpi/rsb_ic_quiz_retry.png diff --git a/skin/src/main/res/drawable-hdpi/rss_ic_quiz_valid.png b/backbone/src/main/res/drawable-hdpi/rsb_ic_quiz_valid.png similarity index 100% rename from skin/src/main/res/drawable-hdpi/rss_ic_quiz_valid.png rename to backbone/src/main/res/drawable-hdpi/rsb_ic_quiz_valid.png diff --git a/skin/src/main/res/drawable-hdpi/rss_ic_tab_dashboard.png b/backbone/src/main/res/drawable-hdpi/rsb_ic_tab_dashboard.png similarity index 100% rename from skin/src/main/res/drawable-hdpi/rss_ic_tab_dashboard.png rename to backbone/src/main/res/drawable-hdpi/rsb_ic_tab_dashboard.png diff --git a/skin/src/main/res/drawable-hdpi/rss_ic_video.png b/backbone/src/main/res/drawable-hdpi/rsb_ic_video.png similarity index 100% rename from skin/src/main/res/drawable-hdpi/rss_ic_video.png rename to backbone/src/main/res/drawable-hdpi/rsb_ic_video.png diff --git a/skin/src/main/res/drawable-mdpi/rss_ic_action_learn.png b/backbone/src/main/res/drawable-mdpi/rsb_ic_action_learn.png similarity index 100% rename from skin/src/main/res/drawable-mdpi/rss_ic_action_learn.png rename to backbone/src/main/res/drawable-mdpi/rsb_ic_action_learn.png diff --git a/skin/src/main/res/drawable-mdpi/rss_ic_action_settings.png b/backbone/src/main/res/drawable-mdpi/rsb_ic_action_settings.png similarity index 100% rename from skin/src/main/res/drawable-mdpi/rss_ic_action_settings.png rename to backbone/src/main/res/drawable-mdpi/rsb_ic_action_settings.png diff --git a/skin/src/main/res/drawable-mdpi/rss_ic_location_24dp.png b/backbone/src/main/res/drawable-mdpi/rsb_ic_location_24dp.png similarity index 100% rename from skin/src/main/res/drawable-mdpi/rss_ic_location_24dp.png rename to backbone/src/main/res/drawable-mdpi/rsb_ic_location_24dp.png diff --git a/skin/src/main/res/drawable-mdpi/rss_ic_notification_24dp.png b/backbone/src/main/res/drawable-mdpi/rsb_ic_notification_24dp.png similarity index 100% rename from skin/src/main/res/drawable-mdpi/rss_ic_notification_24dp.png rename to backbone/src/main/res/drawable-mdpi/rsb_ic_notification_24dp.png diff --git a/skin/src/main/res/drawable-mdpi/rss_ic_quiz_retry.png b/backbone/src/main/res/drawable-mdpi/rsb_ic_quiz_retry.png similarity index 100% rename from skin/src/main/res/drawable-mdpi/rss_ic_quiz_retry.png rename to backbone/src/main/res/drawable-mdpi/rsb_ic_quiz_retry.png diff --git a/skin/src/main/res/drawable-mdpi/rss_ic_quiz_valid.png b/backbone/src/main/res/drawable-mdpi/rsb_ic_quiz_valid.png similarity index 100% rename from skin/src/main/res/drawable-mdpi/rss_ic_quiz_valid.png rename to backbone/src/main/res/drawable-mdpi/rsb_ic_quiz_valid.png diff --git a/skin/src/main/res/drawable-mdpi/rss_ic_tab_dashboard.png b/backbone/src/main/res/drawable-mdpi/rsb_ic_tab_dashboard.png similarity index 100% rename from skin/src/main/res/drawable-mdpi/rss_ic_tab_dashboard.png rename to backbone/src/main/res/drawable-mdpi/rsb_ic_tab_dashboard.png diff --git a/skin/src/main/res/drawable-mdpi/rss_ic_video.png b/backbone/src/main/res/drawable-mdpi/rsb_ic_video.png similarity index 100% rename from skin/src/main/res/drawable-mdpi/rss_ic_video.png rename to backbone/src/main/res/drawable-mdpi/rsb_ic_video.png diff --git a/skin/src/main/res/drawable-v21/rss_btn_state_onboarding_anim.xml b/backbone/src/main/res/drawable-v21/rsb_btn_state_onboarding_anim.xml similarity index 70% rename from skin/src/main/res/drawable-v21/rss_btn_state_onboarding_anim.xml rename to backbone/src/main/res/drawable-v21/rsb_btn_state_onboarding_anim.xml index 6e56790cf..6b6254226 100644 --- a/skin/src/main/res/drawable-v21/rss_btn_state_onboarding_anim.xml +++ b/backbone/src/main/res/drawable-v21/rsb_btn_state_onboarding_anim.xml @@ -5,17 +5,17 @@ - + - + - + - + @@ -23,17 +23,17 @@ - + - + - + - + diff --git a/skin/src/main/res/drawable-xhdpi/rss_ic_action_learn.png b/backbone/src/main/res/drawable-xhdpi/rsb_ic_action_learn.png similarity index 100% rename from skin/src/main/res/drawable-xhdpi/rss_ic_action_learn.png rename to backbone/src/main/res/drawable-xhdpi/rsb_ic_action_learn.png diff --git a/skin/src/main/res/drawable-xhdpi/rss_ic_action_settings.png b/backbone/src/main/res/drawable-xhdpi/rsb_ic_action_settings.png similarity index 100% rename from skin/src/main/res/drawable-xhdpi/rss_ic_action_settings.png rename to backbone/src/main/res/drawable-xhdpi/rsb_ic_action_settings.png diff --git a/skin/src/main/res/drawable-xhdpi/rss_ic_location_24dp.png b/backbone/src/main/res/drawable-xhdpi/rsb_ic_location_24dp.png similarity index 100% rename from skin/src/main/res/drawable-xhdpi/rss_ic_location_24dp.png rename to backbone/src/main/res/drawable-xhdpi/rsb_ic_location_24dp.png diff --git a/skin/src/main/res/drawable-xhdpi/rss_ic_notification_24dp.png b/backbone/src/main/res/drawable-xhdpi/rsb_ic_notification_24dp.png similarity index 100% rename from skin/src/main/res/drawable-xhdpi/rss_ic_notification_24dp.png rename to backbone/src/main/res/drawable-xhdpi/rsb_ic_notification_24dp.png diff --git a/skin/src/main/res/drawable-xhdpi/rss_ic_quiz_retry.png b/backbone/src/main/res/drawable-xhdpi/rsb_ic_quiz_retry.png similarity index 100% rename from skin/src/main/res/drawable-xhdpi/rss_ic_quiz_retry.png rename to backbone/src/main/res/drawable-xhdpi/rsb_ic_quiz_retry.png diff --git a/skin/src/main/res/drawable-xhdpi/rss_ic_quiz_valid.png b/backbone/src/main/res/drawable-xhdpi/rsb_ic_quiz_valid.png similarity index 100% rename from skin/src/main/res/drawable-xhdpi/rss_ic_quiz_valid.png rename to backbone/src/main/res/drawable-xhdpi/rsb_ic_quiz_valid.png diff --git a/skin/src/main/res/drawable-xhdpi/rss_ic_tab_activities.png b/backbone/src/main/res/drawable-xhdpi/rsb_ic_tab_activities.png similarity index 100% rename from skin/src/main/res/drawable-xhdpi/rss_ic_tab_activities.png rename to backbone/src/main/res/drawable-xhdpi/rsb_ic_tab_activities.png diff --git a/skin/src/main/res/drawable-xhdpi/rss_ic_tab_dashboard.png b/backbone/src/main/res/drawable-xhdpi/rsb_ic_tab_dashboard.png similarity index 100% rename from skin/src/main/res/drawable-xhdpi/rss_ic_tab_dashboard.png rename to backbone/src/main/res/drawable-xhdpi/rsb_ic_tab_dashboard.png diff --git a/skin/src/main/res/drawable-xhdpi/rss_ic_video.png b/backbone/src/main/res/drawable-xhdpi/rsb_ic_video.png similarity index 100% rename from skin/src/main/res/drawable-xhdpi/rss_ic_video.png rename to backbone/src/main/res/drawable-xhdpi/rsb_ic_video.png diff --git a/skin/src/main/res/drawable-xxhdpi/rss_ic_action_learn.png b/backbone/src/main/res/drawable-xxhdpi/rsb_ic_action_learn.png similarity index 100% rename from skin/src/main/res/drawable-xxhdpi/rss_ic_action_learn.png rename to backbone/src/main/res/drawable-xxhdpi/rsb_ic_action_learn.png diff --git a/skin/src/main/res/drawable-xxhdpi/rss_ic_action_settings.png b/backbone/src/main/res/drawable-xxhdpi/rsb_ic_action_settings.png similarity index 100% rename from skin/src/main/res/drawable-xxhdpi/rss_ic_action_settings.png rename to backbone/src/main/res/drawable-xxhdpi/rsb_ic_action_settings.png diff --git a/skin/src/main/res/drawable-xxhdpi/rss_ic_location_24dp.png b/backbone/src/main/res/drawable-xxhdpi/rsb_ic_location_24dp.png similarity index 100% rename from skin/src/main/res/drawable-xxhdpi/rss_ic_location_24dp.png rename to backbone/src/main/res/drawable-xxhdpi/rsb_ic_location_24dp.png diff --git a/skin/src/main/res/drawable-xxhdpi/rss_ic_notification_24dp.png b/backbone/src/main/res/drawable-xxhdpi/rsb_ic_notification_24dp.png similarity index 100% rename from skin/src/main/res/drawable-xxhdpi/rss_ic_notification_24dp.png rename to backbone/src/main/res/drawable-xxhdpi/rsb_ic_notification_24dp.png diff --git a/skin/src/main/res/drawable-xxhdpi/rss_ic_quiz_retry.png b/backbone/src/main/res/drawable-xxhdpi/rsb_ic_quiz_retry.png similarity index 100% rename from skin/src/main/res/drawable-xxhdpi/rss_ic_quiz_retry.png rename to backbone/src/main/res/drawable-xxhdpi/rsb_ic_quiz_retry.png diff --git a/skin/src/main/res/drawable-xxhdpi/rss_ic_quiz_valid.png b/backbone/src/main/res/drawable-xxhdpi/rsb_ic_quiz_valid.png similarity index 100% rename from skin/src/main/res/drawable-xxhdpi/rss_ic_quiz_valid.png rename to backbone/src/main/res/drawable-xxhdpi/rsb_ic_quiz_valid.png diff --git a/skin/src/main/res/drawable-xxhdpi/rss_ic_tab_activities.png b/backbone/src/main/res/drawable-xxhdpi/rsb_ic_tab_activities.png similarity index 100% rename from skin/src/main/res/drawable-xxhdpi/rss_ic_tab_activities.png rename to backbone/src/main/res/drawable-xxhdpi/rsb_ic_tab_activities.png diff --git a/skin/src/main/res/drawable-xxhdpi/rss_ic_tab_dashboard.png b/backbone/src/main/res/drawable-xxhdpi/rsb_ic_tab_dashboard.png similarity index 100% rename from skin/src/main/res/drawable-xxhdpi/rss_ic_tab_dashboard.png rename to backbone/src/main/res/drawable-xxhdpi/rsb_ic_tab_dashboard.png diff --git a/skin/src/main/res/drawable-xxhdpi/rss_ic_video.png b/backbone/src/main/res/drawable-xxhdpi/rsb_ic_video.png similarity index 100% rename from skin/src/main/res/drawable-xxhdpi/rss_ic_video.png rename to backbone/src/main/res/drawable-xxhdpi/rsb_ic_video.png diff --git a/skin/src/main/res/drawable-xxxhdpi/rss_ic_action_learn.png b/backbone/src/main/res/drawable-xxxhdpi/rsb_ic_action_learn.png similarity index 100% rename from skin/src/main/res/drawable-xxxhdpi/rss_ic_action_learn.png rename to backbone/src/main/res/drawable-xxxhdpi/rsb_ic_action_learn.png diff --git a/skin/src/main/res/drawable-xxxhdpi/rss_ic_action_settings.png b/backbone/src/main/res/drawable-xxxhdpi/rsb_ic_action_settings.png similarity index 100% rename from skin/src/main/res/drawable-xxxhdpi/rss_ic_action_settings.png rename to backbone/src/main/res/drawable-xxxhdpi/rsb_ic_action_settings.png diff --git a/skin/src/main/res/drawable-xxxhdpi/rss_ic_location_24dp.png b/backbone/src/main/res/drawable-xxxhdpi/rsb_ic_location_24dp.png similarity index 100% rename from skin/src/main/res/drawable-xxxhdpi/rss_ic_location_24dp.png rename to backbone/src/main/res/drawable-xxxhdpi/rsb_ic_location_24dp.png diff --git a/skin/src/main/res/drawable-xxxhdpi/rss_ic_notification_24dp.png b/backbone/src/main/res/drawable-xxxhdpi/rsb_ic_notification_24dp.png similarity index 100% rename from skin/src/main/res/drawable-xxxhdpi/rss_ic_notification_24dp.png rename to backbone/src/main/res/drawable-xxxhdpi/rsb_ic_notification_24dp.png diff --git a/skin/src/main/res/drawable-xxxhdpi/rss_ic_quiz_retry.png b/backbone/src/main/res/drawable-xxxhdpi/rsb_ic_quiz_retry.png similarity index 100% rename from skin/src/main/res/drawable-xxxhdpi/rss_ic_quiz_retry.png rename to backbone/src/main/res/drawable-xxxhdpi/rsb_ic_quiz_retry.png diff --git a/skin/src/main/res/drawable-xxxhdpi/rss_ic_quiz_valid.png b/backbone/src/main/res/drawable-xxxhdpi/rsb_ic_quiz_valid.png similarity index 100% rename from skin/src/main/res/drawable-xxxhdpi/rss_ic_quiz_valid.png rename to backbone/src/main/res/drawable-xxxhdpi/rsb_ic_quiz_valid.png diff --git a/skin/src/main/res/drawable-xxxhdpi/rss_ic_tab_activities.png b/backbone/src/main/res/drawable-xxxhdpi/rsb_ic_tab_activities.png similarity index 100% rename from skin/src/main/res/drawable-xxxhdpi/rss_ic_tab_activities.png rename to backbone/src/main/res/drawable-xxxhdpi/rsb_ic_tab_activities.png diff --git a/skin/src/main/res/drawable-xxxhdpi/rss_ic_tab_dashboard.png b/backbone/src/main/res/drawable-xxxhdpi/rsb_ic_tab_dashboard.png similarity index 100% rename from skin/src/main/res/drawable-xxxhdpi/rss_ic_tab_dashboard.png rename to backbone/src/main/res/drawable-xxxhdpi/rsb_ic_tab_dashboard.png diff --git a/skin/src/main/res/drawable-xxxhdpi/rss_ic_video.png b/backbone/src/main/res/drawable-xxxhdpi/rsb_ic_video.png similarity index 100% rename from skin/src/main/res/drawable-xxxhdpi/rss_ic_video.png rename to backbone/src/main/res/drawable-xxxhdpi/rsb_ic_video.png diff --git a/backbone/src/main/res/drawable/rsb_animated_check.xml b/backbone/src/main/res/drawable/rsb_animated_check.xml new file mode 100644 index 000000000..40b14960d --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_animated_check.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/drawable/rsb_animated_check_delayed.xml b/backbone/src/main/res/drawable/rsb_animated_check_delayed.xml new file mode 100644 index 000000000..a36b8867f --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_animated_check_delayed.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/drawable/rsb_animated_fingerprint.xml b/backbone/src/main/res/drawable/rsb_animated_fingerprint.xml new file mode 100644 index 000000000..baf5cae66 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_animated_fingerprint.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/drawable/rsb_animated_tapping_left.xml b/backbone/src/main/res/drawable/rsb_animated_tapping_left.xml new file mode 100644 index 000000000..794d895ed --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_animated_tapping_left.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/drawable/rsb_animated_tapping_right.xml b/backbone/src/main/res/drawable/rsb_animated_tapping_right.xml new file mode 100644 index 000000000..efa7f9861 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_animated_tapping_right.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/drawable/rsb_check_mark.xml b/backbone/src/main/res/drawable/rsb_check_mark.xml new file mode 100644 index 000000000..ab5e47684 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_check_mark.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/skin/src/main/res/drawable/rss_divider_1dp.xml b/backbone/src/main/res/drawable/rsb_divider_1dp.xml similarity index 100% rename from skin/src/main/res/drawable/rss_divider_1dp.xml rename to backbone/src/main/res/drawable/rsb_divider_1dp.xml diff --git a/backbone/src/main/res/drawable/rsb_error.xml b/backbone/src/main/res/drawable/rsb_error.xml new file mode 100644 index 000000000..7cf285730 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_error.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_fingerprint.xml b/backbone/src/main/res/drawable/rsb_fingerprint.xml new file mode 100644 index 000000000..01ad896fa --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_fingerprint.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/skin/src/main/res/drawable/rss_ic_circle_16dp.xml b/backbone/src/main/res/drawable/rsb_ic_circle_16dp.xml similarity index 100% rename from skin/src/main/res/drawable/rss_ic_circle_16dp.xml rename to backbone/src/main/res/drawable/rsb_ic_circle_16dp.xml diff --git a/backbone/src/main/res/drawable/rsb_ic_circle_empty.xml b/backbone/src/main/res/drawable/rsb_ic_circle_empty.xml new file mode 100644 index 000000000..c96370a24 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_ic_circle_empty.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/drawable/rsb_ic_clear.xml b/backbone/src/main/res/drawable/rsb_ic_clear.xml new file mode 100644 index 000000000..afce04f8b --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_ic_clear.xml @@ -0,0 +1,9 @@ + + + diff --git a/backbone/src/main/res/drawable/rsb_ic_email_icon.xml b/backbone/src/main/res/drawable/rsb_ic_email_icon.xml new file mode 100644 index 000000000..9d23ef7fa --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_ic_email_icon.xml @@ -0,0 +1,5 @@ + + + diff --git a/backbone/src/main/res/drawable/rsb_ic_facebook_icon.xml b/backbone/src/main/res/drawable/rsb_ic_facebook_icon.xml new file mode 100644 index 000000000..43567065c --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_ic_facebook_icon.xml @@ -0,0 +1,5 @@ + + + diff --git a/backbone/src/main/res/drawable/rsb_ic_location_24dp.xml b/backbone/src/main/res/drawable/rsb_ic_location_24dp.xml new file mode 100644 index 000000000..a5e833644 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_ic_location_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/skin/src/main/res/drawable/rss_ic_menu_24dp.xml b/backbone/src/main/res/drawable/rsb_ic_menu_24dp.xml similarity index 100% rename from skin/src/main/res/drawable/rss_ic_menu_24dp.xml rename to backbone/src/main/res/drawable/rsb_ic_menu_24dp.xml diff --git a/backbone/src/main/res/drawable/rsb_ic_microphone_24dp.xml b/backbone/src/main/res/drawable/rsb_ic_microphone_24dp.xml new file mode 100644 index 000000000..a6ce21a1a --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_ic_microphone_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/backbone/src/main/res/drawable/rsb_ic_recorder_notification.xml b/backbone/src/main/res/drawable/rsb_ic_recorder_notification.xml new file mode 100644 index 000000000..f64604ae9 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_ic_recorder_notification.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_ic_sms_icon.xml b/backbone/src/main/res/drawable/rsb_ic_sms_icon.xml new file mode 100644 index 000000000..97b4eb4b8 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_ic_sms_icon.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/backbone/src/main/res/drawable/rsb_ic_twitter_icon.xml b/backbone/src/main/res/drawable/rsb_ic_twitter_icon.xml new file mode 100644 index 000000000..baa0efa73 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_ic_twitter_icon.xml @@ -0,0 +1,5 @@ + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_clarity_1g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_clarity_1g.xml new file mode 100644 index 000000000..16e67d420 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_clarity_1g.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_clarity_2g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_clarity_2g.xml new file mode 100644 index 000000000..2393bcf40 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_clarity_2g.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_clarity_3g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_clarity_3g.xml new file mode 100644 index 000000000..98961ff7c --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_clarity_3g.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_clarity_4g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_clarity_4g.xml new file mode 100644 index 000000000..125060c3b --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_clarity_4g.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_clarity_5g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_clarity_5g.xml new file mode 100644 index 000000000..71431e149 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_clarity_5g.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_custom_1g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_custom_1g.xml new file mode 100644 index 000000000..3a9ce3d32 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_custom_1g.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_custom_2g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_custom_2g.xml new file mode 100644 index 000000000..a4971f60e --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_custom_2g.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_custom_3g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_custom_3g.xml new file mode 100644 index 000000000..2e06f76f5 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_custom_3g.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_custom_4g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_custom_4g.xml new file mode 100644 index 000000000..8e372f9fa --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_custom_4g.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_custom_5g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_custom_5g.xml new file mode 100644 index 000000000..84e00f64f --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_custom_5g.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_exercise_1g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_exercise_1g.xml new file mode 100644 index 000000000..9ed62b842 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_exercise_1g.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_exercise_2g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_exercise_2g.xml new file mode 100644 index 000000000..ecddada0d --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_exercise_2g.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_exercise_3g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_exercise_3g.xml new file mode 100644 index 000000000..051ed8ed4 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_exercise_3g.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_exercise_4g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_exercise_4g.xml new file mode 100644 index 000000000..883e9823c --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_exercise_4g.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_exercise_5g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_exercise_5g.xml new file mode 100644 index 000000000..42f60a96d --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_exercise_5g.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_mood_1g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_mood_1g.xml new file mode 100644 index 000000000..7b869470a --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_mood_1g.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_mood_2g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_mood_2g.xml new file mode 100644 index 000000000..efdee852d --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_mood_2g.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_mood_3g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_mood_3g.xml new file mode 100644 index 000000000..bf1b3ec0f --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_mood_3g.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_mood_4g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_mood_4g.xml new file mode 100644 index 000000000..90ef9db8e --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_mood_4g.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_mood_5g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_mood_5g.xml new file mode 100644 index 000000000..848486df9 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_mood_5g.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_pain_1g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_pain_1g.xml new file mode 100644 index 000000000..3975f4641 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_pain_1g.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_pain_2g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_pain_2g.xml new file mode 100644 index 000000000..a192c2c79 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_pain_2g.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_pain_3g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_pain_3g.xml new file mode 100644 index 000000000..d2c2bf01e --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_pain_3g.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_pain_4g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_pain_4g.xml new file mode 100644 index 000000000..d2739b955 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_pain_4g.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_pain_5g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_pain_5g.xml new file mode 100644 index 000000000..9cdf461d7 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_pain_5g.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_sleep_1g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_sleep_1g.xml new file mode 100644 index 000000000..1e6ba0fc4 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_sleep_1g.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_sleep_2g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_sleep_2g.xml new file mode 100644 index 000000000..cd98eec59 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_sleep_2g.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_sleep_3g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_sleep_3g.xml new file mode 100644 index 000000000..64530dd6d --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_sleep_3g.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_sleep_4g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_sleep_4g.xml new file mode 100644 index 000000000..b16c0f242 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_sleep_4g.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_mood_survey_sleep_5g.xml b/backbone/src/main/res/drawable/rsb_mood_survey_sleep_5g.xml new file mode 100644 index 000000000..9a58fe655 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_mood_survey_sleep_5g.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_phone_in_pocket.xml b/backbone/src/main/res/drawable/rsb_phone_in_pocket.xml new file mode 100644 index 000000000..762f85a84 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_phone_in_pocket.xml @@ -0,0 +1,15 @@ + + + + diff --git a/backbone/src/main/res/drawable/rsb_phonesoundwaves.xml b/backbone/src/main/res/drawable/rsb_phonesoundwaves.xml new file mode 100644 index 000000000..0447b947f --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_phonesoundwaves.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_phonewaves.xml b/backbone/src/main/res/drawable/rsb_phonewaves.xml new file mode 100644 index 000000000..b9f0fbb52 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_phonewaves.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tapping_left.xml b/backbone/src/main/res/drawable/rsb_tapping_left.xml new file mode 100644 index 000000000..2853be96f --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tapping_left.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tapping_phone_notap_words.xml b/backbone/src/main/res/drawable/rsb_tapping_phone_notap_words.xml new file mode 100644 index 000000000..f7d6c5cf2 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tapping_phone_notap_words.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tapping_right.xml b/backbone/src/main/res/drawable/rsb_tapping_right.xml new file mode 100644 index 000000000..291806051 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tapping_right.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skin/src/main/res/drawable/rss_text_color_accent.xml b/backbone/src/main/res/drawable/rsb_text_color_accent.xml similarity index 100% rename from skin/src/main/res/drawable/rss_text_color_accent.xml rename to backbone/src/main/res/drawable/rsb_text_color_accent.xml diff --git a/backbone/src/main/res/drawable/rsb_timed_walking_man_outbound.xml b/backbone/src/main/res/drawable/rsb_timed_walking_man_outbound.xml new file mode 100644 index 000000000..464ea036f --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_timed_walking_man_outbound.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_timed_walking_man_return.xml b/backbone/src/main/res/drawable/rsb_timed_walking_man_return.xml new file mode 100644 index 000000000..462fcd007 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_timed_walking_man_return.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_timed_walking_turnaround.xml b/backbone/src/main/res/drawable/rsb_timed_walking_turnaround.xml new file mode 100644 index 000000000..4ee65284d --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_timed_walking_turnaround.xml @@ -0,0 +1,15 @@ + + + /> + diff --git a/backbone/src/main/res/drawable/rsb_timer.xml b/backbone/src/main/res/drawable/rsb_timer.xml new file mode 100644 index 000000000..50df848b3 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_timer.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_elbow_bent.xml b/backbone/src/main/res/drawable/rsb_tremor_elbow_bent.xml new file mode 100644 index 000000000..5bd469161 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_elbow_bent.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_elbow_bent_flipped.xml b/backbone/src/main/res/drawable/rsb_tremor_elbow_bent_flipped.xml new file mode 100644 index 000000000..e84376959 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_elbow_bent_flipped.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_hand_in_lap.xml b/backbone/src/main/res/drawable/rsb_tremor_hand_in_lap.xml new file mode 100644 index 000000000..2f1c96111 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_hand_in_lap.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_hand_in_lap_flipped.xml b/backbone/src/main/res/drawable/rsb_tremor_hand_in_lap_flipped.xml new file mode 100644 index 000000000..638e7855b --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_hand_in_lap_flipped.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_hand_out.xml b/backbone/src/main/res/drawable/rsb_tremor_hand_out.xml new file mode 100644 index 000000000..b9bc78d9a --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_hand_out.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_hand_out_flipped.xml b/backbone/src/main/res/drawable/rsb_tremor_hand_out_flipped.xml new file mode 100644 index 000000000..520935fbe --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_hand_out_flipped.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_hand_to_nose.xml b/backbone/src/main/res/drawable/rsb_tremor_hand_to_nose.xml new file mode 100644 index 000000000..ad9443ea4 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_hand_to_nose.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_hand_to_nose_flipped.xml b/backbone/src/main/res/drawable/rsb_tremor_hand_to_nose_flipped.xml new file mode 100644 index 000000000..9661934a3 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_hand_to_nose_flipped.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_in_hand.xml b/backbone/src/main/res/drawable/rsb_tremor_in_hand.xml new file mode 100644 index 000000000..697e244a0 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_in_hand.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_in_hand_2.xml b/backbone/src/main/res/drawable/rsb_tremor_in_hand_2.xml new file mode 100644 index 000000000..dfe840f6b --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_in_hand_2.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_in_hand_2_flipped.xml b/backbone/src/main/res/drawable/rsb_tremor_in_hand_2_flipped.xml new file mode 100644 index 000000000..1b8b4e76d --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_in_hand_2_flipped.xml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_in_hand_flipped.xml b/backbone/src/main/res/drawable/rsb_tremor_in_hand_flipped.xml new file mode 100644 index 000000000..3ed137efc --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_in_hand_flipped.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_queen_wave.xml b/backbone/src/main/res/drawable/rsb_tremor_queen_wave.xml new file mode 100644 index 000000000..4f4c7a645 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_queen_wave.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/backbone/src/main/res/drawable/rsb_tremor_queen_wave_flipped.xml b/backbone/src/main/res/drawable/rsb_tremor_queen_wave_flipped.xml new file mode 100644 index 000000000..b0772a520 --- /dev/null +++ b/backbone/src/main/res/drawable/rsb_tremor_queen_wave_flipped.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/skin/src/main/res/layout-v21/rss_layout_toolbar_tabs.xml b/backbone/src/main/res/layout-v21/rsb_layout_toolbar_tabs.xml similarity index 94% rename from skin/src/main/res/layout-v21/rss_layout_toolbar_tabs.xml rename to backbone/src/main/res/layout-v21/rsb_layout_toolbar_tabs.xml index f19de74ed..2b1a935d9 100644 --- a/skin/src/main/res/layout-v21/rss_layout_toolbar_tabs.xml +++ b/backbone/src/main/res/layout-v21/rsb_layout_toolbar_tabs.xml @@ -7,7 +7,7 @@ android:orientation="vertical" android:outlineProvider="bounds"> - - + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto" +> - + + android:text="@string/rsb_confirm_title" + /> - + tools:text="@string/lorem_medium" + /> - + android:text="@string/rsb_confirm_diff" + /> + app:positiveActionTitle="@string/rsb_continue" + app:negativeActionTitle="@string/rsb_confirm_resend_email"/> + tools:visibility="gone" + layout="@layout/rsb_progress" + /> @@ -94,20 +100,20 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" - android:text="@string/rss_wrong_email"/> + android:text="@string/rsb_wrong_email"/> + android:text="@string/rsb_once_verified"/> + android:text="@string/rsb_continue"/> + android:text="@string/rsb_resend_email"/> diff --git a/skin/src/main/res/layout/rss_activity_fragment.xml b/backbone/src/main/res/layout/rsb_activity_fragment.xml similarity index 100% rename from skin/src/main/res/layout/rss_activity_fragment.xml rename to backbone/src/main/res/layout/rsb_activity_fragment.xml diff --git a/skin/src/main/res/layout/rss_activity_main.xml b/backbone/src/main/res/layout/rsb_activity_main.xml similarity index 80% rename from skin/src/main/res/layout/rss_activity_main.xml rename to backbone/src/main/res/layout/rsb_activity_main.xml index d109a89ed..6b4c882ad 100644 --- a/skin/src/main/res/layout/rss_activity_main.xml +++ b/backbone/src/main/res/layout/rsb_activity_main.xml @@ -4,9 +4,9 @@ android:layout_height="match_parent" android:orientation="vertical"> - + - - + tools:context=".ui.OverviewActivity" +> + android:background="@android:color/white" + > + android:orientation="vertical" + > + tools:src="@mipmap/ic_launcher" + /> + tools:text="@string/lorem_name" + /> + tools:text="@string/lorem_long" + /> @@ -60,17 +67,18 @@ android:layout_height="96dp" android:layout_alignParentBottom="true" android:layout_alignTop="@+id/intro_buttons" - android:background="@color/rsb_submit_bar_gray" /> + android:background="@color/rsb_submit_bar_gray" + /> - - + - + app:tabMode="scrollable" android:fillViewport="false"/> - - + app:elevation="16dp" + /> + - + - - - + - - + style="@style/Button.Backbone.Onboarding" + android:text="@string/rsb_join_study" + /> + - + android:textSize="12sp" + /> \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_button_study_overview.xml b/backbone/src/main/res/layout/rsb_button_study_overview.xml new file mode 100644 index 000000000..25d28ad94 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_button_study_overview.xml @@ -0,0 +1,5 @@ + + + diff --git a/backbone/src/main/res/layout/rsb_form_step_layout.xml b/backbone/src/main/res/layout/rsb_form_step_layout.xml new file mode 100644 index 000000000..220c6201f --- /dev/null +++ b/backbone/src/main/res/layout/rsb_form_step_layout.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_fragment_activities.xml b/backbone/src/main/res/layout/rsb_fragment_activities.xml new file mode 100644 index 000000000..b094fe209 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_fragment_activities.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/skin/src/main/res/layout/rss_fragment_dashboard.xml b/backbone/src/main/res/layout/rsb_fragment_dashboard.xml similarity index 100% rename from skin/src/main/res/layout/rss_fragment_dashboard.xml rename to backbone/src/main/res/layout/rsb_fragment_dashboard.xml diff --git a/backbone/src/main/res/layout/rsb_fragment_learn.xml b/backbone/src/main/res/layout/rsb_fragment_learn.xml new file mode 100644 index 000000000..f781d2db8 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_fragment_learn.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_fragment_share.xml b/backbone/src/main/res/layout/rsb_fragment_share.xml new file mode 100644 index 000000000..4bd0cf469 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_fragment_share.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_item_checkbox.xml b/backbone/src/main/res/layout/rsb_item_checkbox.xml index eab13322f..2f879d876 100644 --- a/backbone/src/main/res/layout/rsb_item_checkbox.xml +++ b/backbone/src/main/res/layout/rsb_item_checkbox.xml @@ -1,5 +1,5 @@ - - - + android:orientation="vertical" + android:layout_marginLeft="@dimen/rsb_margin_left" + android:layout_marginRight="@dimen/rsb_margin_right"> diff --git a/backbone/src/main/res/layout/rsb_item_image_button.xml b/backbone/src/main/res/layout/rsb_item_image_button.xml new file mode 100644 index 000000000..e39b5ca81 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_item_image_button.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_item_permission_card.xml b/backbone/src/main/res/layout/rsb_item_permission_card.xml new file mode 100644 index 000000000..ab9488b1a --- /dev/null +++ b/backbone/src/main/res/layout/rsb_item_permission_card.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/skin/src/main/res/layout/rss_item_permission_content.xml b/backbone/src/main/res/layout/rsb_item_permission_content.xml similarity index 57% rename from skin/src/main/res/layout/rss_item_permission_content.xml rename to backbone/src/main/res/layout/rsb_item_permission_content.xml index eed09e395..a65fa84ff 100644 --- a/skin/src/main/res/layout/rss_item_permission_content.xml +++ b/backbone/src/main/res/layout/rsb_item_permission_content.xml @@ -1,21 +1,23 @@ - - + tools:src="@drawable/rsb_ic_location_24dp" + /> - + tools:text="Permission Card Title" + /> - + tools:text="APPNAME needs permission to use your location to find accurate UV levels." + /> - + android:layout_alignEnd="@+id/rsb_permission_title" + android:layout_below="@+id/rsb_permission_details" + android:layout_alignRight="@+id/rsb_permission_title" + android:text="@string/rsb_allow" + /> diff --git a/backbone/src/main/res/layout/rsb_item_radio.xml b/backbone/src/main/res/layout/rsb_item_radio.xml index 6631b9b00..f9e2222f8 100644 --- a/backbone/src/main/res/layout/rsb_item_radio.xml +++ b/backbone/src/main/res/layout/rsb_item_radio.xml @@ -1,5 +1,5 @@ - - + + + + + + + + + diff --git a/backbone/src/main/res/layout/rsb_item_row_share.xml b/backbone/src/main/res/layout/rsb_item_row_share.xml new file mode 100644 index 000000000..21514d977 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_item_row_share.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_item_schedule.xml b/backbone/src/main/res/layout/rsb_item_schedule.xml new file mode 100644 index 000000000..eac2251a7 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_item_schedule.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_item_schedule_header.xml b/backbone/src/main/res/layout/rsb_item_schedule_header.xml new file mode 100644 index 000000000..01a41d4c0 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_item_schedule_header.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/skin/src/main/res/layout/rss_layout_consent_evaluation.xml b/backbone/src/main/res/layout/rsb_layout_consent_evaluation.xml similarity index 83% rename from skin/src/main/res/layout/rss_layout_consent_evaluation.xml rename to backbone/src/main/res/layout/rsb_layout_consent_evaluation.xml index f37baf4ba..ad6fdbb26 100644 --- a/skin/src/main/res/layout/rss_layout_consent_evaluation.xml +++ b/backbone/src/main/res/layout/rsb_layout_consent_evaluation.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent"> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center_horizontal" + android:orientation="vertical" +> + android:id="@+id/eligible_text" + /> + /> + android:layout_alignParentBottom="true" + /> \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_layout_email_verification.xml b/backbone/src/main/res/layout/rsb_layout_email_verification.xml new file mode 100644 index 000000000..091ddf81e --- /dev/null +++ b/backbone/src/main/res/layout/rsb_layout_email_verification.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/skin/src/main/res/layout/rss_layout_ineligible.xml b/backbone/src/main/res/layout/rsb_layout_ineligible.xml similarity index 55% rename from skin/src/main/res/layout/rss_layout_ineligible.xml rename to backbone/src/main/res/layout/rsb_layout_ineligible.xml index ea4877ea9..749c24927 100644 --- a/skin/src/main/res/layout/rss_layout_ineligible.xml +++ b/backbone/src/main/res/layout/rsb_layout_ineligible.xml @@ -6,14 +6,22 @@ style="@style/Backbone.Survey.Title" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/rss_ineligible" /> + android:text="@string/rsb_ineligible" /> + diff --git a/skin/src/main/res/layout/rss_layout_permission.xml b/backbone/src/main/res/layout/rsb_layout_permission.xml similarity index 63% rename from skin/src/main/res/layout/rss_layout_permission.xml rename to backbone/src/main/res/layout/rsb_layout_permission.xml index b1e105743..b5f2972a9 100644 --- a/skin/src/main/res/layout/rss_layout_permission.xml +++ b/backbone/src/main/res/layout/rsb_layout_permission.xml @@ -1,8 +1,11 @@ - + android:layout_height="match_parent" +> + android:text="@string/rsb_permission_disclaimer" + android:visibility="visible"/> + android:background="?android:attr/listDivider"/> + android:showDividers="middle|end" + android:divider="@drawable/rsb_divider_1dp" + android:orientation="vertical"/> @@ -39,6 +42,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:negativeActionTitle="@string/rsb_exit" - app:positiveActionTitle="@string/rsb_submit" /> + app:positiveActionTitle="@string/rsb_submit"/> \ No newline at end of file diff --git a/skin/src/main/res/layout/rss_layout_quiz_question.xml b/backbone/src/main/res/layout/rsb_layout_quiz_question.xml similarity index 98% rename from skin/src/main/res/layout/rss_layout_quiz_question.xml rename to backbone/src/main/res/layout/rsb_layout_quiz_question.xml index 777e6649f..0a64ac3ac 100644 --- a/skin/src/main/res/layout/rss_layout_quiz_question.xml +++ b/backbone/src/main/res/layout/rsb_layout_quiz_question.xml @@ -13,7 +13,7 @@ android:layout_height="wrap_content" android:layout_marginTop="0dp" android:paddingBottom="@dimen/rsb_padding_wedge" - android:text="@string/rss_quiz_question_disclamer" + android:text="@string/rsb_quiz_question_disclamer" android:visibility="visible" /> - + android:text="@string/rsb_username" /> - @@ -51,22 +51,22 @@ android:gravity="center_vertical" android:minHeight="@dimen/rsb_item_size_default"> - + android:text="@string/rsb_password" /> - @@ -82,7 +82,7 @@ app:negativeActionTitle="@string/rsb_exit" app:positiveActionTitle="@string/rsb_submit" /> - - - @@ -62,22 +62,22 @@ android:gravity="center_vertical" android:minHeight="@dimen/rsb_item_size_default"> - + android:text="@string/rsb_password" /> - diff --git a/skin/src/main/res/layout/rss_layout_study_html.xml b/backbone/src/main/res/layout/rsb_layout_study_html.xml similarity index 100% rename from skin/src/main/res/layout/rss_layout_study_html.xml rename to backbone/src/main/res/layout/rsb_layout_study_html.xml diff --git a/backbone/src/main/res/layout/rsb_layout_toolbar.xml b/backbone/src/main/res/layout/rsb_layout_toolbar.xml index 3493d8f39..68e769205 100644 --- a/backbone/src/main/res/layout/rsb_layout_toolbar.xml +++ b/backbone/src/main/res/layout/rsb_layout_toolbar.xml @@ -1,5 +1,5 @@ - - + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_step_body_image_choice.xml b/backbone/src/main/res/layout/rsb_step_body_image_choice.xml new file mode 100644 index 000000000..85c9e1701 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_step_body_image_choice.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_step_layout_active_step.xml b/backbone/src/main/res/layout/rsb_step_layout_active_step.xml new file mode 100644 index 000000000..a7591776a --- /dev/null +++ b/backbone/src/main/res/layout/rsb_step_layout_active_step.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_step_layout_audio.xml b/backbone/src/main/res/layout/rsb_step_layout_audio.xml new file mode 100644 index 000000000..8d3f381c7 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_step_layout_audio.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_step_layout_consent_signature.xml b/backbone/src/main/res/layout/rsb_step_layout_consent_signature.xml index 8cd2c3a5f..8fde5a81a 100644 --- a/backbone/src/main/res/layout/rsb_step_layout_consent_signature.xml +++ b/backbone/src/main/res/layout/rsb_step_layout_consent_signature.xml @@ -50,7 +50,7 @@ android:layout_alignParentBottom="true" /> - + + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_step_layout_instruction.xml b/backbone/src/main/res/layout/rsb_step_layout_instruction.xml index 0bd6e2e90..6087c6f04 100644 --- a/backbone/src/main/res/layout/rsb_step_layout_instruction.xml +++ b/backbone/src/main/res/layout/rsb_step_layout_instruction.xml @@ -1,12 +1,13 @@ + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_step_layout_pincode.xml b/backbone/src/main/res/layout/rsb_step_layout_pincode.xml index a5ec568a5..3e3ee81cb 100644 --- a/backbone/src/main/res/layout/rsb_step_layout_pincode.xml +++ b/backbone/src/main/res/layout/rsb_step_layout_pincode.xml @@ -22,7 +22,7 @@ android:text="@string/rsb_pincode_enter_summary" android:textColorLink="?attr/colorAccent" /> - + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_step_layout_share_the_app.xml b/backbone/src/main/res/layout/rsb_step_layout_share_the_app.xml new file mode 100644 index 000000000..08ec8c694 --- /dev/null +++ b/backbone/src/main/res/layout/rsb_step_layout_share_the_app.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_step_layout_tapping_interval.xml b/backbone/src/main/res/layout/rsb_step_layout_tapping_interval.xml new file mode 100644 index 000000000..931203d4a --- /dev/null +++ b/backbone/src/main/res/layout/rsb_step_layout_tapping_interval.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/layout/rsb_view_chart_bar.xml b/backbone/src/main/res/layout/rsb_view_chart_bar.xml index e4d54686f..b705cad4e 100644 --- a/backbone/src/main/res/layout/rsb_view_chart_bar.xml +++ b/backbone/src/main/res/layout/rsb_view_chart_bar.xml @@ -15,7 +15,7 @@ android:layout_height="wrap_content" tools:text="Amount of pie eaten" /> - - - - - - - + android:minHeight="@dimen/rsb_item_size_submit_bar"> - - +

    + + + + \ No newline at end of file diff --git a/backbone/src/main/res/values-ar/strings.xml b/backbone/src/main/res/values-ar/strings.xml new file mode 100644 index 000000000..883929b7a --- /dev/null +++ b/backbone/src/main/res/values-ar/strings.xml @@ -0,0 +1,396 @@ + + + + + موافقة + الاسم الأول + اسم العائلة + مطلوب + مراجعة + قم بمراجعة النموذج التالي واضغط على \"موافقة\" إذا كنت جاهزًا للمتابعة. + مراجعة + التوقيع + الرجاء التوقيع باستخدام إصبعك على السطر التالي. + التوقيع هنا + صفحة %1$ld من %2$ld + + مرحبًا + جمع البيانات + الخصوصية + استخدام البيانات + استطلاع الدراسة + مهام الدراسة + التزام الوقت + الانسحاب + معرفة المزيد + + معرفة المزيد عن كيفية جمع البيانات + معرفة المزيد عن كيفية استخدام البيانات + معرفة المزيد عن كيفية حماية خصوصيتك وهويتك + معرفة المزيد عن الدراسة أولاً + معرفة المزيد عن استطلاع الدراسة + معرفة المزيد عن أثر الدراسة على وقتك + معرفة المزيد عن المهام المتضمنة + معرفة المزيد عن الانسحاب + + خيارات المشاركة + مشاركة بياناتي مع %1$s والباحثين المؤهلين حول العالم + مشاركة بياناتي مع %1$s فقط + سيتلقى %1$s بيانات دراستك من مشاركتك في هذه الدراسة.\n\nمشاركة بيانات دراستك التي تم ترميزها بشكل أوسع (دون أن تتضمن معلومات مثل اسمك) قد تكون مفيدة لهذا البحث والأبحاث المستقبلية. + معرفة المزيد عن مشاركة البيانات + + اسم %1$s (مطبوعًا) + توقيع %1$s + التاريخ + + الخطوة %1$s من %2$s + + القيمة غير صالحة + ‏%1$s تتخطى القيمة القصوى المسموح بها (%2$s). + ‏%1$s أقل من القيمة الدنيا المسموح بها (%2$s). + ‏%1$s قيمة غير صالحة. + + عنوان البريد الإلكتروني غير صالح: %1$s + + أدخل عنوان + تعذر العثور على العنوان المحدد + غير قادر على حل موقعك الحالي. يرجى كتابة العنوان أو الانتقال إلى موقع فيه إشارة GPS أفضل إن وجد. + تم رفض الوصول إلى خدمات الموقع. يرجي منح هذا التطبيق الإذن لاستخدام خدمات الموقع في الإعدادات. + تعذر العثور على نتيجة للعنوان الذي تم إدخاله. يرجى التأكد من أن العنوان صالح. + إما أنك غير متصل بالإنترنت أو أنك تجاوزت الحد الأقصى من طلبات البحث عن العنوان. إذا لم تكن متصلًا بالإنترنت، يرجى تشغيل الـ Wi-Fi للإجابة على هذا السؤال، قم بتخطي هذا السؤال إذا كان زر التخطي متاحاً، أو العودة للاستبيان عندما تكون متصلًا بالإنترنت. خلاف ذلك، يرجى المحاولة مرة أخرى خلال بضع دقائق. + + المحتوى النصي يتجاوز الحد الأقصى للطول: %1$s + + الكاميرا غير متوفرة في العرض المقسم. + + البريد الإلكتروني + jappleseed@example.com + كلمة السر + أدخل كلمة السر + تأكيد + أدخل كلمة السر مرة أخرى + كلمتا السر غير متطابقتين. + معلومات إضافية + باسل + أسعد + الجنس + اختيار الجنس + تاريخ الميلاد + اختيار تاريخ + + التحقق + التحقق من البريد الإلكتروني + اضغط على الرابط أدناه إذا كنت لم تستلم بريد التحقق، وترغب بإرساله لك مرة أخرى. + إعادة إرسال بريد التحقق + + تسجيل الدخول + هل نسيت كلمة السر؟ + + أدخل رمز الدخول + تأكيد رمز الدخول + تم حفظ رمز الدخول + رمز الدخول غير مصدّق + أدخل رمز الدخول القديم + أدخل رمز الدخول الجديد + تأكيد رمز الدخول الجديد + رمز الدخول غير صحيح + رمزي الدخول غير متطابقين. حاول مجدداً. + الرجاء المصادقة باستخدام Touch ID + خطأ في Touch ID + فقط الأرقام مسموح بها. + مؤشر تقدم إدخال رمز الدخول + تم إدخال %1$s من %2$s من الأرقام + هل نسيت رمز الدخول؟ + + تعذر إضافة عنصر سلسلة المفاتيح. + تعذر تحديث عنصر سلسلة المفاتيح. + تعذر حذف عنصر سلسلة المفاتيح. + تعذر العثور على عنصر سلسلة المفاتيح. + + ‎A+ + ‎A- + ‎AB+ + ‎AB- + ‎B+ + ‎B- + ‎O+ + ‎O- + + أنثى + ذكر + غير ذلك + + لا + نعم + + سم + قدم + بوصة + + اضغط للإجابة + قم بتحديد إجابة + اضغط للتحديد + اضغط للكتابة + + موافقة + إلغاء + موافق + مسح + عدم الموافقة + تم + البدء + معرفة المزيد + التالي + تخطي + تخطي هذا السؤال + بدء تشغيل المؤقت + حفظ لما بعد + تجاهل النتائج + إنهاء المهمة + حفظ + مسح الإجابة + لا يمكن تعديل هذه الإجابة. + + بدء النشاط خلال + اكتمل النشاط + سيتم تحليل بياناتك وسيتم إعلامك عندما تصبح النتائج جاهزة. + متبقٍ %1$s من الثواني. + + التقاط صورة + إعادة التقاط الصورة + لم يتم العثور على كاميرا.\u0020\u0020لا يمكن إكمال هذه الخطوة. + لإكمال هذه الخطوة، اسمح لهذا التطبيق بالوصول إلى الكاميرا في الإعدادات. + لم يتم تحديد دليل إخراج للصور الملتقطة. + لا يمكن حفظ الصورة الملتقطة. + + بدء التسجيل + إيقاف التسجيل + إعادة التقاط الفيديو + + اللياقة + المسافة ‏(%1$s) + معدل نبض القلب (bpm) + خذ استراحة لمدة %1$s. + قم بالمشي بأقصى سرعة لديك لمدة %1$s. + يعمل هذا النشاط على مراقبة معدل نبض القلب لديك وقياس المسافة التي يمكنك أن تمشيها خلال %1$s. + قم بالمشي في الخارج بأقصى سرعة ممكنة لديك لمدة %1$s. عندما تنتهي، اجلس وخذ استراحة لمدة %2$s. للبدء، اضغط على البدء. + + سرعة المشي والتوازن + يقيس هذا النشاط سرعة مشيتك وتوازنك أثناء المشي والوقوف ثابتًا. لا تقم بالمتابعة إذا لم تكن قادرًا على المشي دون مساعدة. + اعثر على مكان يمكنك المشي فيه بصورة آمنة دون مساعدة لمسافة %1$d من الخطوات تقريبًا في خط مستقيم. + ضع هاتفك في جيبك أو في حقيبة واتبع التعليمات الصوتية. + الآن قف ثابتًا لمدة %1$s. + قف ثابتًا لمدة %1$s. + قم بالدوران والمشي رجوعًا إلى نقطة بدايتك. + قم بمشي حوالي %1$d خطوة في خط مستقيم. + + ابحث عن مكان يمكنك المشي فيه ذهابًا وإيابًا في خط مستقيم بشكل آمن. حاول أن تسير بشكل مستمر عن طريق الدوران في نهايات مسارك، كما لو كنت تسير حول مخروط.\n\nبعد ذلك، سيتم إرشادك لتقوم بالدوران في دائرة كاملة، ثم الوقوف ثابتًا مع وضع ذراعيك إلى جانبيك وقدميك متباعدتين بمقدار عرض الكتف تقريبًا. + اضغط على البدء عندما تكون مستعدًا للبدء.\nثم ضع هاتفك في جيبك أو في حقيبة واتبع التعليمات الصوتية. + قم بالمشي ذهابًا وإيابًا في خط مستقيم %1$s. قم بالمشي كما تفعل عادة. + استدر بمقدار دائرة كاملة ثم قف ثابتًا لمدة %1$s. + لقد أكملت النشاط. + + سرعة الضغط + اليد اليمنى + اليد اليسرى + يقيس هذا النشاط سرعتك في الضغط. + ضع هاتفك على سطح مستوٍ. + استخدم إصبعين من نفس اليد للضغط بالتبادل على الزرين على الشاشة. + استخدم إصبعين من يدك اليمنى للضغط بالتبادل على الزرين على الشاشة. + استخدم إصبعين من يدك اليسرى للضغط بالتبادل على الزرين على الشاشة. + الآن، كرر نفس الاختبار باستخدام يدك اليمنى. + الآن، كرر نفس الاختبار باستخدام يدك اليسرى. + اضغط بإصبع، ثم بالإصبع الآخر. حاول ضبط وقت ضغطاتك ليكون متساويًا قدر الإمكان. واستمر في الضغط لمدة %1$s. + اضغط على البدء لكي تبدأ. + اضغط على التالي للبدء. + الضغط + إجمالي الضغطات + اضغط على الزرين بتوافق قدر الإمكان باستخدام إصبعين. + اضغط على الزرين باستخدام يدك اليمنى. + اضغط على الزرين باستخدام يدك اليسرى. + تخطي هذه اليد + + الصوت + اضغط على البدء لكي تبدأ. + انطق \"آااااه\" في الميكروفون لأطول فترة ممكنة بالنسبة إليك. + خذ نفسًا عميقًا وانطق \"آااااه\" في الميكروفون لأطول فترة ممكنة بالنسبة إليك. حافظ على ثبات مستوى صوتك حتى تظل أشرطة الصوت باللون الأزرق. + يعمل هذا النشاط على تقييم صوتك من خلال تسجيله بالميكروفون الموجود في أسفل هاتفك. + عالٍ جدًا + غير قادر على تسجيل الصوت + الرجاء الانتظار حتى يتم التحقق من مستوى الضوضاء في الخلفية. + مستوى الضوضاء المحيطة عالٍ جدًا بحيث لا يمكن تسجيل صوتك. الرجاء الانتقال إلى مكان أكثر هدوءًا والمحاولة مرة أخرى. + اضغط على التالي عندما تكون مستعدًا. + + قياس سمع النغمة + يقيس هذا النشاط قدرتك على سماع الأصوات المختلفة. + قبل البدء، قم بتوصيل سماعات الرأس وارتدائها. + اضغط على البدء لكي تبدأ. + من المفترض أن تسمع نغمة الآن. قم بضبط مستوى الصوت باستخدام عناصر التحكم في جانب الجهاز.\n\nاضغط على الزر عندما تكون مستعدًا للبدء. + اضغط على الزر في كل مرة تبدأ في سماع صوت فيها. + ‏%1$s هرتز، يسار + ‏%1$s هرتز، يمين + + الذاكرة المكانية + يقيس هذا النشاط ذاكرتك المكانية قصيرة المدى من خلال مطالبتك بتكرار نفس ترتيب إضاءة %1$s. + زهور + زهور + ستضيء بعض %1$s كلٌ على حدة. اضغط على هذه الـ %2$s بنفس ترتيب إضاءتها. + ستضيء بعض %1$s كلٌ على حدة. اضغط على هذه الـ %2$s بعكس ترتيب إضاءتها. + للبدء، اضغط على البدء ثم قم بالمشاهدة عن قُرب. + %1$s + النتيجة + شاهد %1$s تتم إضاءتها + اضغط على %1$s لكي تتم إضاءتها + اضغط على %1$s بترتيب عكسي + اكتمل التتابع + للمتابعة اضغط على التالي. + المحاولة مرة أخرى + لم يحالفك التوفيق تلك المرة. اضغط على التالي للمتابعة. + انتهى الوقت + نفد الوقت المخصص لك.\nاضغط على التالي للمتابعة. + اكتملت اللعبة + تم الإيقاف مؤقتاً + للمتابعة اضغط على التالي. + + زمن رد الفعل + يعمل هذا النشاط على تقييم الوقت الذي تستغرقه في الاستجابة للتلميح البصري. + قم بهز الجهاز في أي اتجاه فور ظهور النقطة الزرقاء على الشاشة. سيُطلب منك فعل ذلك %D من المرات. + اضغط على البدء لكي تبدأ. + المحاولة %1$s من %2$s + قم بهز الجهاز سريعًا عندما تظهر الدائرة الزرقاء + + برج هانوي + يقيّم هذا النشاط مهاراتك في حل الألغاز. + انقل الكومة بالكامل إلى المنصة المميزة في أقل عدد ممكن من الحركات. + اضغط على البدء لكي تبدأ + حل اللغز + عدد الحركات: %1$s \n %2$s + لا أستطيع حل هذا اللغز + + المشي المحدد بوقت + يقيس هذا النشاط وظيفة الطرف السفلي لديك. + ابحث عن مكان، يفضل أن يكون بالخارج، يمكنك المشي فيه %1$s تقريبًا في خط مستقيم بأقصى سرعة ممكنة، لكن بأمان. لا تبطئ من سرعتك قبل أن تجتاز خط النهاية. + اضغط على التالي للبدء. + الجهاز المساعد + استخدم نفس الجهاز المساعد لكل اختبار. + هل ترتدي مقوام قدم وكاحل؟ + هل تستخدم جهازًا مساعدًا؟ + اضغط هنا لتحديد إجابة. + لا شيء + عصا أحادية + عكاز أحادي + عصا ثنائية + عكاز ثنائي + مشاية + قم بالمشي حتى %1$s في خط مستقيم. + قم بالدوران والمشي رجوعًا إلى نقطة بدايتك. + اضغط على تم عند الانتهاء. + + PASAT + PVSAT + PAVSAT + يقيس اختبار الجمع المتوالى السمعى المتواتر سرعة معالجة المعلومات السمعية والقدرة الحسابية. + يقيس اختبار الجمع المتوالى البصري المتواتر سرعة معالجة المعلومات المرئية والقدرة الحسابية. + يقيس اختبار الجمع المتوالى السمعى البصري المتواتر سرعة معالجة المعلومات السمعية والمرئية والقدرة الحسابية. + يتم تقديم الأعداد الفردية كل %1$s من الثواني.\nيجب أن تجمع كل عدد جديد على العدد الذي يسبقه مباشرةً.\nانتبه، لا ينبغي أن تحسب مجموع السلسلة المستمرة، لكن احسب فقط مجموع آخر رقمين. + اضغط على البدء لكي تبدأ. + تذكر هذا العدد الأول. + اجمع هذا العدد الجديد على العدد السابق. + - + + ‏%1$s-فحص العقدة والحفرة + هذا النشاط يقيس وظيفة الطرف العلوي عن طريق الطلب منك بأن تضع العقدة في حفرة. سوف يطلب منك أن تفعل هذا %1$s مرات. + سيتم فحص كلا اليدين اليسرى واليمنى.\nيجب عليك التقاط العقدة في أسرع وقت ممكن، ووضعها في الحفرة، وبعد إنهاء %1$s مرات، قم بإزالتها مرة أخرى %2$s مرات. + سيتم فحص كلا اليدين اليمنى واليسرى.\nيجب عليك التقاط العقدة في أسرع وقت ممكن، ووضعها في الحفرة، وبعد إنهاء %1$s مرات، قم بإزالتها مرة أخرى %2$s مرات. + اضغط على البدء لكي تبدأ. + قم بوضع العقدة في الحفرة باستخدام اليد اليسرى. + قم بوضع العقدة في الحفرة باستخدام اليد اليمنى. + قم بوضع العقدة خلف السطر باستخدام اليد اليسرى. + قم بوضع العقدة خلف السطر باستخدام اليد اليمنى. + رفع العقدة باستخدام اصبعين. + ارفع الأصابع لإسقاط العقدة. + + نشاط الرُعاش + يقيس هذا النشاط مقدار رُعاش يديك بمختلف الأوضاع. ابحث عن مكان يمكنك الجلوس فيه بشكل مريح أثناء مدة هذا النشاط. + أمسك الهاتف في يدك الأكثر تأثرًا كما يظهر في الصورة أدناه. + أمسك الهاتف في يدك اليمنى كما يظهر في الصورة أدناه. + أمسك الهاتف في يدك اليسرى كما يظهر في الصورة أدناه. + سيُطلب منك تنفيذ %1$s أثناء الجلوس مع الإمساك بالهاتف في يديك. + مهمة واحدة + مهمتان + ثلاث مهام + أربع مهام + خمس مهام + اضغط على التالي للمتابعة. + استعد لإمساك هاتفك في حِجرك. + استعد لإمساك هاتفك في حِجرك بيدك اليسرى. + استعد لإمساك هاتفك في حِجرك بيدك اليمنى. + استمر في إمساك هاتفك في حِجرك لمدة %1$d من الثواني. + الآن، أمسك هاتفك ويدك ممدودة في مستوى الكتف. + الآن، أمسك هاتفك ويدك اليسرى ممدودة في مستوى الكتف. + الآن، أمسك هاتفك ويدك اليمنى ممدودة في مستوى الكتف. + استمر في إمساك هاتفك ويدك ممدودة لمدة %1$d من الثواني. + الآن، أمسك هاتفك في مستوى الكتف مع ثني مرفقك. + الآن، أمسك هاتفك ويدك اليسرى في مستوى الكتف مع ثني مرفقك. + الآن، أمسك هاتفك ويدك اليمنى في مستوى الكتف مع ثني مرفقك. + استمر في إمساك هاتفك مع ثني مرفقك لمدة %1$d من الثواني + الآن، مع الاستمرار في ثني مرفقك، المس الهاتف بأنفك بشكل متكرر. + الآن، مع الاستمرار في ثني مرفقك والإمساك بالهاتف في يدك اليسرى، المس الهاتف بأنفك بشكل متكرر. + الآن، مع الاستمرار في ثني مرفقك والإمساك بالهاتف في يدك اليمنى، المس الهاتف بأنفك بشكل متكرر. + استمر في لمس الهاتف بأنفك لمدة %1$d من الثواني + استعد للتلويح كالملوك (التلويح عن طريق تدوير معصمك). + استعد للتلويح كالملوك مع إمساك هاتفك في يدك اليسرى (التلويح عن طريق تدوير معصمك). + استعد للتلويح كالملوك مع إمساك هاتفك في يدك اليمنى (التلويح عن طريق تدوير معصمك). + استمر في التلويح كالملوك لمدة %1$d من الثواني. + الآن، انقل الهاتف إلى يدك اليسرى وتابع إلى المهمة التالية. + الآن، انقل الهاتف إلى يدك اليمنى وتابع إلى المهمة التالية. + تابع إلى المهمة التالية. + اكتمل النشاط. + سيُطلب منك تنفيذ %1$s أثناء الجلوس مع الإمساك بالهاتف في إحدى يديك، ثم تكرار ذلك باليد الأخرى. + لا يمكنني القيام بهذا النشاط بيدي اليسرى. + لا يمكنني القيام بهذا النشاط بيدي اليمنى. + يمكنني القيام بهذا النشاط بكلتا يدي. + + تعذر إنشاء الملف + تعذر إزالة ملفات السجل الكافية للوصول إلى الحد + حدث خطأ في تعيين السمة + لم يتم تمييز الملف كمحذوف (لم يتم التمييز كـ \"تم التحميل\") + حدثت عدة أخطاء أثناء إزالة السجلات + لم يتم العثور على أي بيانات تم جمعها. + لم يتم تحديد دليل إخراج + + لا توجد بيانات + + السابق + توضيح %1$s + حقل التوقيع المخصص + قم بلمس الشاشة وتحريك إصبعك للتوقيع + موقّع + غير موقّع + محددة + غير محددة + شريط تمرير الاستجابة. النطاق من %1$s إلى %2$s + صورة بلا تسمية + بدء المهمة + نشط + صحيحة + غير صحيحة + هادئ + عنوان لعبة الذاكرة + معاينة الالتقاط + تم التقاط الصورة + معاينة التقاط الفيديو + الفيديو الملتقط + غير قادر على وضع القرص بحجم %1$s على القرص بحجم %2$s + الهدف + برج + اضغط مرتين لوضع القرص + اضغط مرتين لتحديد أعلى قرص + يحتوي على أقراص بأحجام %1$s + فارغ + يتراوح من %1$s إلى %2$s + مجموعة تتكون من + و + النقطة: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-ca/strings.xml b/backbone/src/main/res/values-ca/strings.xml new file mode 100644 index 000000000..e20da9092 --- /dev/null +++ b/backbone/src/main/res/values-ca/strings.xml @@ -0,0 +1,396 @@ + + + + + Consentiment + Nom + Cognoms + Obligatori + Revisió + Repassa el formulari de sota i prem Acceptar si estàs a punt per continuar. + Revisió + Signatura + Signa amb el dit a la línia de sota. + Signa aquí + Pàgina %1$ld de %2$ld + + Benvingut + Recopilació de dades + Privadesa + Ús de dades + Avaluació de l’estudi + Tasques d’estudi + Temps necessari + Abandonar + Més informació + + Més informació sobre com es recopilen les dades + Més informació sobre com s’utilitzen les dades + Més informació sobre com es protegeix la privadesa i la identitat + Més informació sobre el primer estudi + Més informació sobre l’avaluació de l’estudi + Més informació sobre el temps necessari per a l’estudi + Més informació sobre les tasques de l’estudi + Més informació sobre l’abandonament + + Opcions de compartir + Compartir les meves dades amb %1$s i investigadors qualificats de tot el món + Compartir només les meves dades amb %1$s + %1$s rebrà les teves dades de participació en aquest estudi.\n\nCompartir les dades codificades d’una manera més àmplia (sense informació com el nom) pot ser d’ajut en aquesta investigació i d’altres de futures. + Més informació sobre com compartir dades + + Nom de: %1$s (imprès) + Signatura de: %1$s + Data + + Pas %1$s de %2$s + + Valor no vàlid + %1$s supera el valor màxim permès (%2$s). + %1$s és menys que el valor mínim permès (%2$s). + %1$s no és un valor vàlid. + + Adreça electrònica no vàlida: %1$s + + Escriu una adreça + No s’ha trobat l‘adreça especificada + No s’ha pogut determinar la ubicació actual. Escriu una adreça o vés a un lloc que tingui un senyal de GPS més bo, si pot ser. + S’ha denegat l’accés als serveis de localització. Dóna permís perquè l’aplicació utilitzi els serveis de localització des de Configuració. + No s’ha trobat cap resultat per a l’adreça especificada. Comprova que sigui vàlida. + O no tens connexió a Internet o has superat el nombre màxim de peticions de cerca d’adreces. Si no tens connexió a Internet, activa la Wi-Fi per contestar aquesta pregunta, passa aquesta pregunta si hi ha disponible un botó d’omissió o recupera l’enquesta quan et puguis connectar a Internet. També pots tornar-ho a provar d’aquí a una mica. + + El contingut del text supera la llargada màxima: %1$s + + La càmera no està disponible amb la pantalla dividida. + + Correu electrònic + jmartorell@example.com + Contrasenya + Escriu la contrasenya + Confirmar + Torna a escriure la contrasenya + Les contrasenyes no coincideixen. + Més informació + Jordi + Martorell + Sexe + Selecciona un sexe + Data de naixement + Selecciona una data + + Verificació + Verifica el teu correu electrònic + Prem l’enllaç de sota si no has rebut cap correu de verificació i vols que se’t torni a enviar. + Reenviar correu de verificació + + Inici de sessió + Contrasenya oblidada? + + Escriu el codi + Confirma el codi + Codi desat + Codi autenticat + Escriu el codi antic + Escriu el codi nou + Confirma el nou codi + Codi incorrecte + Els codis no coincideixen. Torna-ho a provar. + Identifica’t amb el Touch ID + Error de Touch ID + Només es permeten caràcters numèrics. + Indicador de progrés d’ingrés del codi + %1$s de %2$s dígits introduïts + Has oblidat el codi? + + No s’ha pogut afegir l’ítem del clauer. + No s’ha pogut actualitzar l’ítem del clauer. + No s’ha pogut eliminar l’ítem del clauer. + No s’ha trobat l’ítem del clauer. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Dona + Home + Altres + + No + + + cm + ft + in + + Prémer per contestar + Seleccionar una resposta + Prem per seleccionar + Prem per escriure + + Acceptar + Cancel·lar + OK + Esborrar + No acceptar + Fet + Començar + Més informació + Següent + Ometre + Ometre aquesta pregunta + Iniciar el temporitzador + Desar per després + Descartar resultats + Finalitzar tasca + Desar + Esborrar la resposta + Aquesta resposta no es pot modificar. + + Iniciant l’activitat en + Activitat completada + S’analitzaran les dades i rebràs una notificació quan tinguis els resultats a punt. + Queden %1$s segons. + + Capturar la imatge + Tornar a capturar la imatge + No s’ha trobat cap càmera. Aquest pas no es pot completar. + Per completar aquest pas, permet que aquesta aplicació tingui accés a la càmera a Configuració. + No s’ha especificat cap directori de sortida per a les imatges capturades. + No s’ha pogut desar la imatge capturada. + + Iniciar la gravació + Aturar la gravació + Tornar a capturar el vídeo + + Activitat física + Distància (%1$s) + Ritme cardíac (bpm) + Seu còmodament %1$s. + Camina tan ràpid com puguis durant %1$s. + Aquesta activitat controla la freqüència cardíaca i mesura la distància que pots caminar en %1$s. + Camina a l’aire lliure al ritme més alt que puguis durant %1$s. Quan acabis, seu i descansa còmodament %2$s. Per començar, prem Començar. + + Marxa i equilibri + Aquesta activitat valora la marxa i l’equilibri quan camines i quan estàs quiet. No continuïs si no pots caminar sol de manera segura. + Busca un lloc on puguis caminar sense assistència de manera seguida uns %1$d passos en línia recta. + Posa’t el telèfon a la butxaca o a la bossa i segueix les instruccions d’àudio. + Ara estigues quiet %1$s. + Estigues quiet %1$s. + Fes mitja volta i torna allà on has començat. + Camina fins a %1$d passos en línia recta. + + Busca un lloc on puguis anar i tornar caminant en línia recta de forma segura. Camina sense parar i gira al final del camí com si estiguessis vorejant un con.\n\nA continuació, se’t demanarà que giris fent un cercle complet i que et quedis quiet amb els braços als costats i els peus separats més o menys per la mateixa distància que hi ha entre espatlla i espatlla. + Quan estiguis a punt per començar, prem Començar.\nLlavors, posa’t el mòbil a la butxaca o a la bossa i segueix les instruccions d’àudio. + Vés i torna caminant en línia recta durant %1$s. Camina naturalment. + Gira fent un cercle complet i llavors queda’t quiet durant %1$s. + Has completat l’activitat. + + Velocitat de premuda + Mà dreta + Mà esquerra + Aquesta activitat mesura la velocitat de premuda. + Deixa el mòbil sobre una superfície plana. + Prem els botons de la pantalla alternant dos dits de la mateixa mà. + Prem els botons de la pantalla alternant dos dits de la mà dreta. + Prem els botons de la pantalla alternant dos dits de la mà esquerra. + Ara repeteix la mateixa operació amb la mà dreta. + Ara repeteix la mateixa operació amb la mà esquerra. + Prem amb un dit, i després amb l’altre. Intenta que el ritme de premuda sigui constant. Prem durant %1$s. + Per iniciar, prem Començar. + Prem Següent per continuar. + Prem + Tocs totals + Prem els botons amb dos dits al ritme més constant que puguis. + Prem els botons amb la mà DRETA. + Prem els botons amb la mà ESQUERRA. + Ometre aquesta mà + + Veu + Per iniciar, prem Començar. + Fes un “Aaaaah” tan llarg com puguis al micròfon. + Inspira profundament i fes un “Aaaaah” tan llarg com puguis al micròfon. Mantingues un volum de veu estable fent que les barres d’àudio es mantinguin en blau. + Aquesta activitat avalua la teva veu tot gravant-la amb el micròfon de la part inferior del telèfon. + Massa alt + No s’ha pogut gravar l’àudio + Espera mentre es calcula el nivell de soroll de fons. + El soroll ambiental és massa fort per gravar la teva veu. Vés a un lloc més silenciós i torna‑ho a provar. + Prem Següent quan estiguis a punt. + + Audiometria tonal + Aquesta activitat mesura la capacitat de sentir diversos sons. + Abans de començar, connecta i posa’t els auriculars. + Per iniciar, prem Començar. + Ara hauries d’escoltar un to. Ajusta el volum amb els controls de la part lateral del dispositiu.\n\nPrem el botó quan estiguis a punt per començar. + Prem el botó cada vegada que comencis a escoltar un so. + %1$s Hz, esquerra + %1$s Hz, dreta + + Memòria espacial + Aquesta activitat mesura la memòria espacial a curt termini demanant-te que repeteixis l’ordre en què s’il·luminen les %1$s. + flors + flors + Algunes de les %1$s s’il·luminaran d’una en una. Prem les %2$s en qüestió en l’ordre en què s’hagin il·luminat. + Algunes de les %1$s s’il·luminaran d’una en una. Prem les %2$s en qüestió en l’ordre invers en què s’hagin il·luminat. + Per començar, prem Començar i mira atentament. + %1$s + Puntuació + Mira com s’il·luminen les imatges de %1$s + Prem les %1$s en l’ordre en què s’han il·luminat + Prem les %1$s en ordre invers + Seqüència completa + Per continuar, prem Següent + Reintentar + No ho has aconseguit aquesta vegada. Prem Següent per continuar. + És l’hora + T’has quedat sense temps.\nPrem Següent per continuar. + Joc completat + En pausa + Per continuar, prem Següent + + Temps de reacció + Aquesta activitat avalua el temps que trigues a respondre a un senyal visual. + Agita el dispositiu en qualsevol direcció de seguida que aparegui el punt blau a la pantalla. Se’t demanarà que facis això %D vegades. + Per iniciar, prem Començar. + Intent %1$s de %2$s + Agita ràpidament el dispositiu quan aparegui el cercle blau + + Torre de Hanoi + Aquesta activitat valora la teva capacitat de resoldre trencaclosques. + Trasllada tota la pila a la plataforma ressaltada en el mínim de moviments possibles. + Per iniciar, prem Començar + Resol el joc + Nombre de moviments: %1$s \n %2$s + No puc resoldre aquest joc + + Caminada cronometrada + Aquesta activitat mesura la funcionalitat de les teves extremitats inferiors. + Busca un lloc, preferiblement a l’aire lliure, on puguis caminar aproximadament %1$s en línia recta tan ràpid com puguis, amb total seguretat. No abaixis el ritme fins que no hagis passat la línia d’arribada. + Prem Següent per continuar. + Tipus d’assistència + Utilitza el mateix tipus d’assistència per a cada prova. + Portes ortesi al turmell? + Fas servir algun tipus d’assistència? + Prem aquí per respondre. + Cap + Un bastó + Una crossa + Dos bastons + Dues crosses + Caminador + Camina fins a %1$s en línia recta. + Fes mitja volta i torna allà on has començat. + Prem Fet quan hagis acabat. + + PASAT + PVSAT + PAVSAT + El PAST (test auditiu de suma en sèrie a ritme) mesura la teva velocitat de processament d’informació auditiva i la teva habilitat de càlcul. + El PAVT (test visual de suma en sèrie a ritme) mesura la teva velocitat de processament d’informació visual i la teva habilitat de càlcul. + El PAVST (test visual i auditiu de suma en sèrie a ritme) mesura la teva velocitat de processament d’informació auditiva i visual i la teva habilitat de càlcul. + Es presenten números solts cada %1$s segons.\nHas de sumar cada nou número al número immediatament anterior.\nAtenció: no has d’anar calculant la suma total, sinó només la suma dels dos últims números. + Per iniciar, prem Començar. + Recorda aquest primer dígit. + Suma aquest número a l’anterior. + - + + Test de la clavilla amb %1$s forats + Aquesta activitat mesura la funcionalitat de les teves extremitats superiors demanant-te que fiquis una clavilla dins d’un forat. Ho hauràs de fer %1$s cops. + S’avaluaran la mà esquerra i la dreta.\nHas d’agafar la clavilla tan ràpid com puguis, posar-la al forat i, un cop fet %1$s cops, tornar-la a treure %2$s cops. + S’avaluaran la mà dreta i l’esquerra.\nHas d’agafar la clavilla tan ràpid com puguis, posar-la al forat i, un cop fet %1$s cops, tornar-la a treure %2$s cops. + Per iniciar, prem Començar. + Posa la clavilla al forat amb la mà esquerra. + Posa la clavilla al forat amb la mà dreta. + Posa la clavilla darrere de la línia amb la mà esquerra. + Posa la clavilla darrere de la línia amb la mà dreta. + Agafa la clavilla fent servir dos dits. + Aixeca els dits per deixar caure la clavilla. + + Activitat de tremolor + Aquesta activitat mesura la tremolor de les mans en diferents posicions. Busca un lloc on puguis seure còmodament per fer aquesta activitat. + Agafa el mòbil amb la mà més afectada, tal com es mostra a la imatge de sota. + Agafa el mòbil amb la mà DRETA, tal com es mostra a la imatge de sota. + Agafa el mòbil amb la mà ESQUERRA, tal com es mostra a la imatge de sota. + Quan estiguis assegut i amb el mòbil a la mà, se’t demanarà que completis %1$s. + una tasca + dues tasques + tres tasques + quatre tasques + cinc tasques + Prem Següent per començar. + Prepara’t per agafar el mòbil i recolzar la mà sobre la falda. + Prepara’t per agafar el mòbil amb la mà ESQUERRA i recolzar aquesta mà sobre la falda. + Prepara’t per agafar el mòbil amb la mà DRETA i recolzar aquesta mà sobre la falda. + Agafa el mòbil i recolza la mà sobre la falda durant %1$d segons. + Ara agafa el mòbil i mantén el braç estirat cap enfora a l’altura de l’espatlla. + Ara agafa el mòbil amb la mà ESQUERRA i mantén el braç esquerre estirat cap enfora a l’altura de l’espatlla. + Ara agafa el mòbil amb la mà DRETA i mantén el braç dret estirat cap enfora a l’altura de l’espatlla. + Agafa el mòbil i mantén el braç estirat cap enfora durant %1$d segons. + Ara agafa el mòbil i mantén la mà a l’altura de l’espatlla amb el colze doblegat. + Ara agafa el mòbil i mantén la mà ESQUERRA a l’altura de l’espatlla amb el colze doblegat. + Ara agafa el mòbil i mantén la mà DRETA a l’altura de l’espatlla amb el colze doblegat. + Agafa el mòbil i mantén el colze doblegat durant %1$d segons. + Ara, amb el colze doblegat, toca el mòbil amb el nas repetidament. + Ara, amb el colze doblegat i el mòbil a la mà ESQUERRA, toca el mòbil amb el nas repetidament. + Ara, amb el colze doblegat i el mòbil a la mà DRETA, toca el mòbil amb el nas repetidament. + Toca el mòbil amb el nas durant %1$d segons. + Prepara’t per fer una salutació reial fent girar el canell amb el mòbil a la mà. + Prepara’t per fer una salutació reial fent girar el canell amb el mòbil a la mà ESQUERRA. + Prepara’t per fer una salutació reial fent girar el canell amb el mòbil a la mà DRETA. + Fes una salutació reial durant %1$d segons. + Ara agafa el mòbil amb la mà ESQUERRA i avança a la tasca següent. + Ara agafa el mòbil amb la mà DRETA i avança a la tasca següent. + Avança a la tasca següent. + Activitat completada. + Quan estiguis assegut i amb el mòbil a la mà, se’t demanarà que completis %1$s, primer amb una mà i després amb l’altra. + No puc completar l’activitat amb la mà ESQUERRA. + No puc completar l’activitat amb la mà DRETA. + Puc completar l’activitat amb les dues mans. + + No s’ha pogut crear l’arxiu + No s’han pogut eliminar prou arxius de registre per assolir el llindar + Error al configurar atribut + Arxiu no marcat eliminat (no marcat carregat) + Diversos errors a l’eliminar registres + No s’han trobat dades recollides. + No s’ha especificat cap directori de sortida + + Sense dades + + Enrere + Il·lustració de: %1$s + Camp designat de signatura + Toca la pantalla i mou el dit per signar + Signat + Sense signar + Seleccionat + No seleccionat + Regulador de resposta. Interval de %1$s a %2$s + Imatge sense etiquetar + Començar tasca + actiu + correcte + incorrecte + inactiu + Joc de memòria + Previsualització de la captura + Imatge capturada + Previsualització de la captura de vídeo + Vídeo capturat + No s’ha pogut situar el disc de mida %1$s sobre el disc de mida %2$s + Objectiu + Torre + Prem dos cops per situar el disc + Prem dos cops per seleccionar el disc superior + Té disc amb mides %1$s + Buidar + Interval de %1$s a %2$s + Pila composta per + i + Punt: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-cs/strings.xml b/backbone/src/main/res/values-cs/strings.xml new file mode 100644 index 000000000..a0238f516 --- /dev/null +++ b/backbone/src/main/res/values-cs/strings.xml @@ -0,0 +1,397 @@ + + + + + Souhlas + Jméno + Příjmení + Požadováno + Kontrola + Projděte si níže uvedený formulář a pokud budete chtít pokračovat, klepněte na Souhlasím. + Kontrola + Podpis + Na lince níže se prstem podepište. + Zde se podepište + Stránka %1$ld z %2$ld + + Vítejte + Shromažďování dat + Soukromí + Využití dat + Studijní průzkum + Studijní úkoly + Věnovaný čas + Odstoupení + Další informace + + Informace o shromažďování dat + Informace o způsobech využití dat + Informace o ochraně soukromí a identity + Úvodní informace o studii + Informace o realizaci průzkumu + Informace o dopadu studie na váš čas + Informace o úkolech obsažených ve studii + Informace o odstoupení od studie + + Volby sdílení + Sdílet data se společností %1$s a kvalifikovanými výzkumnými pracovišti na celém světě + Sdílet data pouze se společností %1$s + Data z vaší účasti ve studii budou předána společnosti %1$s.\n\nPro tento i budoucí výzkumy by mohlo být přínosné, kdyby byla zakódovaná data z vaší studie (bez údajů, jako je vaše jméno) sdílena ve větší šíři. + Další informace o sdílení dat + + %1$s - jméno + %1$s - podpis + Datum + + Krok %1$s z %2$s + + Neplatná hodnota + %1$s přesahuje maximální povolenou hodnotu (%2$s). + %1$s nedosahuje minimální povolené hodnoty (%2$s). + %1$s není platná hodnota. + + Neplatná e‑mailová adresa: %1$s + + Zadejte adresu + Zadanou adresu se nepodařilo najít + Vaši aktuální polohu nelze zjistit. Zadejte adresu nebo se přesuňte na místo s lepším GPS signálem, je‑li to možné. + Přístup k polohovým službám byl odepřen. Přejděte do Nastavení a udělte této aplikaci oprávnění k používání polohových služeb. + Pro zadanou adresu se nepodařilo najít výsledek. Ověřte správnost adresy. + Nejste připojeni k internetu nebo jste překročili maximální počet žádostí o vyhledání adresy. Pokud nejste připojeni k internetu a chcete tuto otázku zodpovědět, zapněte Wi-Fi. Je-li k dispozici tlačítko přeskočit, můžete otázku přeskočit nebo se můžete k průzkumu vrátit, až bude internet dostupný. Případně to můžete zkusit za několik minut znovu. + + Text překračuje maximální délku: %1$s + + Fotoaparát není na rozdělené obrazovce k dispozici. + + E‑mail + jnovak@example.com + Heslo + Zadejte heslo + Potvrzení + Zadejte heslo ještě jednou + Hesla se neshodují. + Doplňující informace + Jan + Novák + Pohlaví + Vyberte pohlaví + Datum narození + Vyberte datum + + Ověření + Ověřte svůj e-mail + Pokud jste neobdrželi ověřovací e‑mail a chcete, abychom vám ho zaslali znovu, klepněte na odkaz níže. + Znovu odeslat ověřovací e‑mail + + Přihlásit se + Zapomněli jste heslo? + + Zadejte kód + Potvrďte kód + Kód byl uložen + Kód byl ověřen + Zadejte starý kód + Zadejte nový kód + Potvrďte nový kód + Nesprávný kód + Kódy se neshodují. Zkuste to znovu. + Použijte ověření pomocí Touch ID + Chyba Touch ID + Jsou povoleny jen číselné znaky. + Indikátor průběhu zadávání kódu + Zadané číslice: %1$s z %2$s + Zapomněli jste kód? + + Nepodařilo se přidat položku do svazku klíčů. + Nepodařilo se aktualizovat položku ve svazku klíčů. + Nepodařilo se smazat položku ze svazku klíčů. + Nepodařilo se najít položku ve svazku klíčů. + + A+ + A- + AB+ + AB- + B+ + B- + 0+ + 0- + + Žena + Muž + Jiné + + Ne + Ano + + cm + ft + in + + Odpověď + Vyberte odpověď + Chcete‑li vybrat, klepněte + Chcete-li psát, klepněte + + Souhlasím + Zrušit + OK + Smazat + Nesouhlasím + Hotovo + Začít + Další informace + Další + Přeskočit + Tuto otázku přeskočit + Spustit odpočet + Nechat na později + Zahodit výsledky + Konec úlohy + Uložit + Smazat odpověď + Tuto odpověď nelze změnit. + + Aktivita se spustí za + Aktivita byla dokončena + Data budou analyzována a jakmile budou k dispozici výsledky, obdržíte oznámení. + Zbývá %1$s s. + + Pořídit obrázek + Pořídit obrázek znovu + Fotoaparát nebyl nalezen. Tento krok nelze dokončit. + Před dokončením tohoto kroku musíte v Nastavení povolit této aplikaci přístup k fotoaparátu. + Nebyl určen žádný výstupní adresář pro pořízené obrázky. + Pořízený obrázek se nepodařilo uložit. + + Začít nahrávat + Zastavit nahrávání + Znovu nahrát video + + Kondice + Vzdálenost (%1$s) + Srdeční tep (bpm) + Po %1$s pohodlně seďte. + Po %1$s co nejrychleji kráčejte. + Tato aktivita změří váš srdeční tep a vzdálenost, kterou ujdete za %1$s. + Po %1$s se procházejte venku nejvyšším možným tempem. Poté po %2$s v pohodlné pozici odpočívejte. Chcete-li začít, klepněte na Začít. + + Chůze a rovnováha + Tato aktivita změří váš krok a rovnováhu při chůzi a stání. Pokud nedokážete bezpečně chodit bez pomoci, v tomto testu nepokračujte. + Najděte si místo, kde můžete bezpečně bez asistence ujít přibližně %1$d kroků v přímém směru. + Uložte telefon do kapsy nebo do tašky či batohu a postupujte podle zvukových instrukcí. + Nyní po %1$s klidně stůjte. + Po %1$s klidně stůjte. + Obraťte se a jděte zpátky na začátek. + Ujděte až %1$d kroků v přímém směru. + + Najděte si místo, na kterém můžete bezpečně přecházet sem a tam po přímé linii. Zkuste chodit bez přestávek a na konci se vždy otočte, jako byste obcházeli kužel.\n\nNásledně dostanete pokyn abyste se otočili kolem své osy a zůstali stát s rukama podél těla a nohama rozkročenýma na šířku ramen. + Až budete připraveni, klepněte na Začít.\nPak uložte telefon do kapsy nebo tašky a postupujte podle zvukových pokynů. + Po %1$s se procházejte sem a tam po přímé linii. Jděte normálním krokem. + Otočte se kolem své osy a pak po %1$s klidně stůjte. + Dokončili jste aktivitu. + + Rychlost klepání + Pravá ruka + Levá ruka + Tato aktivita změří vaši rychlost klepání. + Položte telefon na rovný povrch. + Dvěma prsty jedné ruky střídavě klepejte na tlačítka na obrazovce. + Dvěma prsty pravé ruky střídavě klepejte na tlačítka na obrazovce. + Dvěma prsty levé ruky střídavě klepejte na tlačítka na obrazovce. + Nyní test zopakujte pravou rukou. + Nyní test zopakujte levou rukou. + Klepněte jedním prstem a potom druhým. Zkuste klepat co nejvíc rovnoměrně. Pokračujte v klepání po %1$s. + Začněte klepnutím na Začít. + Začněte klepnutím na Další. + Klepněte + Počet klepnutí + Dvěma prsty co nejrovnoměrněji klepejte na tlačítka. + Klepejte na tlačítka PRAVOU rukou. + Klepejte na tlačítka LEVOU rukou. + Tuto ruku vynechat + + Hlas + Začněte klepnutím na Začít. + Řekněte \"Ááááá\" do mikrofonu a vydržte co nejdéle. + Hluboce se nadechněte a při výdechu hlasitě a co nejdéle do mikrofonu vyslovujte \"Ááááá\". Udržujte konstantní úroveň hlasitosti, při které budou proužky zbarvené modře. + Tato aktivita vyhodnotí váš hlas, který bude zaznamenán pomocí mikrofonu u dolního okraje telefonu. + Příliš nahlas + Nelze zaznamenávat zvuk + Počkejte, než otestujeme úroveň okolního hluku. + Úroveň okolního hluku je pro nahrání vašeho hlasu příliš vysoká. Přesuňte se na klidnější místo a zkuste to znovu. + Až budete připraveni, klepněte na Další. + + Tónová audiometrie + Tato aktivita měří vaši schopnost slyšet různé zvuky. + Než začnete, zapněte sluchátka a nasaďte si je. + Začněte klepnutím na Začít. + Nyní byste měli slyšet tón. Nastavte hlasitost pomocí ovládacích prvků po straně zařízení.\n\nAž budete připraveni začít, klepněte na tlačítko. + Jakmile uslyšíte zvuk, klepněte na tlačítko. + %1$s Hz, vlevo + %1$s Hz, vpravo + + Prostorová paměť + Tato aktivita změří vaši krátkodobou prostorovou paměť. Budete požádáni o zopakování pořadí, ve kterém se budou rozsvěcovat %1$s. + květiny + květiny + Některé %1$s se postupně rozsvítí. Klepejte na %2$s v pořadí, ve kterém se rozsvěcovaly. + Některé %1$s se postupně rozsvítí. Klepejte na %2$s v obráceném pořadí rozsvěcování. + Chcete-li začít, klepněte na Začít a pozorně se dívejte. + %1$s + Skóre + Sledujte, jak se %1$s rozsvítí + Klepejte na %1$s v pořadí, ve kterém se budou rozsvěcovat + Klepejte na %1$s v obráceném pořadí + Sekvence byla dokončena + Pokračujte klepnutím na Další. + Zkusit znovu + Úplně se vám to tentokrát nepodařilo. Pokračujte klepnutím na Další. + Čas vypršel + Došel vám čas.\nPokračujte klepnutím na Další. + Hra byla dokončena + Pozastaveno + Pokračujte klepnutím na Další. + + Reakční doba + Tato aktivita zjišťuje, jak dlouho vám trvá, než zareagujete na vizuální podnět. + Jakmile se na displeji objeví modrá tečka, zatřeste zařízením v libovolném směru. Budete k tomu vyzváni celkem %Dx. + Začněte klepnutím na Začít. + Pokus %1$s z %2$s + Jakmile se objeví modrý kroužek, krátce zařízením zatřeste + + Hanojské věže + Tato aktivita vyhodnotí vaši schopnost řešit hlavolamy. + Přesuňte celou sadu na zvýrazněnou podložku s využitím co nejmenšího počtu kroků. + Začněte klepnutím na Začít + Vyřešte hlavolam + Počet kroků: %1$s \n %2$s + Tento hlavolam nedokážu vyřešit + + Chůze na čas + Tato aktivita změří funkci vašich dolních končetin. + Najděte si místo (nejlépe venku), kde můžete co nejrychleji a bezpečně ujít přibližně %1$s v přímém směru. Nezpomalujte, dokud nepřekročíte cílovou čáru. + Začněte klepnutím na Další. + Asistenční zařízení + Při všech testech používejte při chůzi tutéž oporu. + Používáte kotníkovou ortézu? + Používáte při chůzi oporu? + Klepněte a vyberte odpověď. + Ne + Jedna hůl + Jedna berla + Dvě hole + Dvě berle + Chodítko + Ujděte až %1$s v přímém směru. + Obraťte se a jděte zpátky na začátek. + Po dokončení klepněte na Hotovo. + + PASAT + PVSAT + PAVSAT + Paced Auditory a Serial Addition Test měří vaši rychlost a počtářské dovednosti při zpracovávání sluchových informací. + Paced Visual Serial Addition Test měří vaši rychlost a počtářské dovednosti při zpracovávání zrakových informací. + Paced Auditory a Visual Serial Addition Test měří vaši rychlost a počtářské dovednosti při zpracovávání sluchových a zrakových informací. + Vždy po %1$s sekundách se vám zobrazí jedno číslo.\nKaždé číslo musíte přičíst k bezprostředně předchozímu.\nPozor, nesčítejte všechna čísla, vždy jen dvě poslední. + Začněte klepnutím na Začít. + Zapamatujte si toto první číslo. + Přičtěte toto číslo k předchozímu. + - + + %1$skolíkový test + Tato aktivita otestuje motoriku vašich horních končetin. Požádáme vás o zasunutí kolíku do otvoru. Budete k tomu vyzváni celkem %1$s×. + Budeme testovat vaši levou i pravou ruku.\nCo nejrychleji uchopte kolík, %1$s× ho zasuňte do otvoru a poté ho %2$s× vytáhněte. + Budeme testovat vaši pravou i levou ruku.\nCo nejrychleji uchopte kolík, %1$s× ho zasuňte do otvoru a poté ho %2$s× vytáhněte. + Začněte klepnutím na Začít. + Zasuňte kolík do otvoru levou rukou. + Zasuňte kolík do otvoru pravou rukou. + Umístěte kolík za čáru levou rukou. + Umístěte kolík za čáru pravou rukou. + Uchopte kolík dvěma prsty. + Zvednutím prstů kolík pusťte. + + Třes rukou + Tato aktivita měří třas rukou v různých polohách. Najděte si místo, na kterém budete moct po celou dobu aktivity pohodlně sedět. + Podržte telefon ve více postižené ruce tak, jak vidíte na obrázku. + Podržte telefon v PRAVÉ ruce tak, jak vidíte na obrázku. + Podržte telefon v LEVÉ ruce tak, jak vidíte na obrázku. + Budete požádáni o %1$s při sezení s telefonem v ruce. + úkol + dva úkoly + tři úkoly + čtyři úkoly + pět úkolů + Pokračujte klepnutím na Další. + Připravte se držet telefon na klíně. + Připravte se držet telefon LEVOU rukou na klíně. + Připravte se držet telefon PRAVOU rukou na klíně. + Držte telefon na klíně po %1$d s. + Nyní držte telefon rukou nataženou ve výši ramen. + Nyní držte telefon LEVOU rukou nataženou ve výši ramen. + Nyní držte telefon PRAVOU rukou nataženou ve výši ramen. + S nataženou rukou držte telefon po %1$d s. + Nyní ohněte ruku v lokti a držte telefon ve výši ramen. + Nyní držte telefon ve výši ramen LEVOU rukou ohnutou v lokti. + Nyní držte telefon ve výši ramen PRAVOU rukou ohnutou v lokti. + S rukou ohnutou v lokti držte telefon po %1$d s. + Nyní se s rukou ohnutou v lokti opakovaně dotýkejte telefonem nosu. + Nyní držte telefon ve výši ramen LEVOU rukou ohnutou v lokti a dotýkejte se jím opakovaně nosu. + Nyní držte telefon ve výši ramen PRAVOU rukou ohnutou v lokti a dotýkejte se jím opakovaně nosu. + Dotýkejte se telefonem nosu po %1$d s. + Připravte se zamávat otáčením zápěstí. + Připravte se zamávat otáčením zápěstí s telefonem v LEVÉ ruce. + Připravte se zamávat otáčením zápěstí s telefonem v PRAVÉ ruce. + Mávejte otáčením zápěstí po %1$d s. + Nyní vezměte telefon do LEVÉ ruky a pokračujte dalším úkolem. + Nyní vezměte telefon do PRAVÉ ruky a pokračujte dalším úkolem. + Pokračujte další úlohou. + Aktivita byla dokončena. + Budete požádáni o %1$s při sezení s telefonem nejprve v jedné a pak v druhé ruce. + Tuto aktivitu nemůžu provádět LEVOU rukou. + Tuto aktivitu nemůžu provádět PRAVOU rukou. + Tuto aktivitu můžu provádět oběma rukama. + + Soubor nelze vytvořit + Nepodařilo se odstranit dostatečné množství protokolů pro dosažení prahové hodnoty + Chyba nastavení atributu + Soubor není označen jako smazaný (není označen jako odeslaný) + Při odstraňování protokolů došlo k více chybám + Nebyla nalezena žádná shromážděná data. + Nebyl určen výstupní adresář + + Žádná data + + Zpět + %1$s - ilustrace + Vyhrazené pole pro podpis + Chcete-li se podepsat, dotkněte se obrazovky a pohybujte prstem + Podepsáno + Nepodepsáno + Vybráno + Nevybráno + Jezdec odpovědi. Rozsah od %1$s do %2$s + Obrázek bez štítku + Spustit úlohu + aktivní + správně + nesprávně + klid + Kostka paměťové hry + Náhled + Pořízený obrázek + Náhled nahraného videa + Nahrané video + Disk o velikosti %1$s nelze umístit na disk o velikosti %2$s + Cíl + Věž + Poklepáním umístíte disk + Poklepáním vyberte vrchní disk + Obsahuje disky o velikosti %1$s + Prázdné + Rozmezí od %1$s do %2$s + Sada se skládá z + + Bod: %1$d + + + \ No newline at end of file diff --git a/backbone/src/main/res/values-da/strings.xml b/backbone/src/main/res/values-da/strings.xml new file mode 100644 index 000000000..6becc1b3b --- /dev/null +++ b/backbone/src/main/res/values-da/strings.xml @@ -0,0 +1,396 @@ + + + + + Tilladelse + Fornavn + Efternavn + Nødvendig + Gennemse + Gennemse nedenstående formular, og tryk på Enig, hvis du er klar til at fortsætte. + Gennemse + Underskrift + Underskriv på linjen nedenfor vha. din finger. + Skriv under her + Side %1$ld af %2$ld + + Velkommen + Dataindsamling + Anonymitet + Databrug + Undersøgelse + Opgaver + Tidsforbrug + Tilbagetrækning + Læs mere + + Læs mere om, hvordan data indsamles + Læs mere om, hvordan data bruges + Læs mere om, hvordan din anonymitet og identitet er beskyttet + Læs mere om undersøgelsen først + Læs mere om undersøgelsen + Læs mere om undersøgelsens indflydelse på din tid + Læs mere om de involverede opgaver + Læs mere om tilbagetrækning + + Valg til deling + Del mine data med %1$s og kvalificerede forskere i hele verden + Del kun mine data med %1$s + %1$s modtager dine data fra din deltagelse i undersøgelsen.\n\nHvis du deler dine kodede undersøgelsesdata mere bredt (uden oplysninger som dit navn) kan det gavne denne og fremtidige undersøgelser. + Læs mere om deling af data + + %1$ss navn (med bogstaver) + %1$ss underskrift + Dato + + Trin %1$s af %2$s + + Ugyldig værdi + %1$s overskrider den maks. tilladte værdi (%2$s). + %1$s er mindre end den min. tilladte værdi (%2$s). + %1$s er ikke en gyldig værdi. + + Ugyldig e-mailadresse: %1$s + + Indtast en adresse + Den angivne adresse blev ikke fundet + Din aktuelle lokalitet blev ikke fundet. Skriv en adresse, eller flyt til en lokalitet med bedre GPS-signal, hvis relevant. + Der er nægtet adgang til lokalitetstjenester. Giv denne app tilladelse til at bruge lokalitetstjenester vha. Indstillinger. + Den indtastede adresse blev ikke fundet. Kontroller, at adressen er gyldig. + Enten har du ikke oprettet forbindelse til internettet, eller også har du overskredet det maksimale antal anmodninger om adresseopslag. Hvis du ikke har oprettet forbindelse til internettet, skal du slå Wi-Fi til for at besvare dette spørgsmål, springe dette spørgsmål over, hvis knappen spring over er tilgængelig, eller vende tilbage til denne undersøgelse, når du har oprettet forbindelse til internettet. Ellers prøv igen om nogle minutter. + + Tekstindhold overskrider maks. længde: %1$s + + Kamera ikke tilgængeligt på delt skærm. + + E-mail + cfriis@example.com + Adgangskode + Skriv adgangskode + Bekræft + Skriv adgangskoden igen + Adgangskoder er forskellige. + Yderligere oplysninger + Clara + Friis + Køn + Vælg køn + Fødselsdato + Vælg en dato + + Bekræftelse + Bekræft din e-mail + Tryk på linket nedenfor, hvis du ikke modtog en e-mail med bekræftelse og gerne vil have den sendt igen. + Send e-mail med bekræftelse igen + + Log ind + Glemt adgangskode? + + Skriv adgangskode + Bekræft adgangskode + Adgangskode arkiveret + Adgangskode godkendt + Skriv den gamle adgangskode + Skriv den nye adgangskode + Bekræft den nye adgangskode + Forkert adgangskode + Adgangskoderne stemte ikke overens. Prøv igen. + Godkend med Touch ID + Touch ID-fejl + Kun numeriske tegn er tilladt. + Statusindikator for indtastning af adgangskode + %1$s af %2$s cifre indtastet + Glemt adgangskoden? + + Nøgleringsemne kunne ikke tilføjes. + Nøgleringsemne kunne ikke opdateres. + Nøgleringsemne kunne ikke slettes. + Nøgleringsemne blev ikke fundet. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Kvinde + Mand + Andet + + Ingen + Ja + + cm + fod + \" + + Tryk for at svare + Vælg et svar + Tryk for at vælge + Tryk for at skrive + + Enig + Annuller + OK + Ryd + Uenig + OK + Gå i gang + Læs mere + Næste + Spring over + Spring dette spørgsmål over + Start stopur + Arkiver til senere + Slet resultater + Slut opgave + Arkiver + Ryd svar + Dette svar kan ikke ændres. + + Starter aktivitet om + Aktivitet færdig + Dine data vil blive analyseret, og du får besked, når resultaterne er klar. + %1$s sekunder tilbage. + + Tag billede + Tag billede igen + Intet kamera fundet. Dette trin kan ikke gennemføres. + For at kunne gennemføre dette trin skal du give denne app adgang til kameraet i Indstillinger. + Der blev ikke anført noget resultatbibliotek til billederne. + Billedet kunne ikke arkiveres. + + Start optagelse + Stop optagelse + Optag video igen + + Fitness + Distance (%1$s) + Puls (spm) + Sid behageligt i %1$s. + Gå så hurtigt, du kan, i %1$s. + Denne aktivitet overvåger din puls og måler, hvor langt du kan gå på %1$s. + Gå så hurtigt, du kan, udendørs i %1$s. Når du er færdig, skal du sætte dig ned og hvile i %2$s. Tryk på Gå i gang for at starte. + + Gang og balance + Denne aktivitet måler din gang og balance, mens du går og står stille. Fortsæt ikke, hvis du ikke kan gå sikkert uden hjælp. + Find et sted, hvor du kan gå omkring %1$d trin sikkert uden hjælp i en lige linje. + Læg din telefon i en lomme eller taske, og følg lydinstruktionerne. + Stå nu stille i %1$s. + Stå stille i %1$s. + Vend om, og gå tilbage til det sted, hvor du startede. + Gå op til %1$d trin i en lige linje. + + Find et sted, hvor du kan gå sikkert frem og tilbage i en lige linje. Forsøg at gå uden stop ved at vende i slutningen af din rute, som om du går omkring en kegle.\n\nDu vil derefter blive bedt om at dreje 360 grader omkring og derefter stå stille med dine arme ned langs siderne og med en skulderbreddes afstand mellem fødderne. + Tryk på Gå i gang, når du er klar til at begynde.\nAnbring derefter telefonen i en lomme eller taske, og følg lydinstruktionerne. + Gå frem og tilbage i en lige linje i %1$s. Gå, som du plejer. + Drej 360 grader rundt, og stå derefter stille i %1$s. + Du har færdiggjort aktiviteten. + + Hastighed på tryk + Højre hånd + Venstre hånd + Denne aktivitet måler din trykhastighed. + Anbring din telefon på en plan overflade. + Tryk skiftevis på knapperne på skærmen med to fingre på den samme hånd. + Tryk skiftevis på knapperne på skærmen med to fingre på den højre hånd. + Tryk skiftevis på knapperne på skærmen med to fingre på den venstre hånd. + Gentag nu den samme test med din højre hånd. + Gentag nu den samme test med din venstre hånd. + Tryk med en finger og derefter med den anden. Forsøg at holde tiden mellem tryk så ensartet som muligt. Fortsæt med at trykke i %1$s. + Tryk på Gå i gang for at starte. + Tryk på Næste for at starte. + Tryk + Tryk i alt + Tryk på knapperne så ensartet, som du kan vha. to fingre. + Tryk på knapperne med din HØJRE hånd. + Tryk på knapperne med din VENSTRE hånd. + Spring denne hånd over + + Stemme + Tryk på Gå i gang for at starte. + Sig “Aaaaah” ind i mikrofonen i så lang tid, som du kan. + Tag en dyb indånding, og sig “Aaaaah” ind i mikrofonen i så lang tid, som du kan. Hold den samme styrke, så lydmålerne bliver ved med at være blå. + Denne aktivitet vurderer din stemme ved at optage den med mikrofonen nederst i telefonen. + For højt + Kan ikke optage lyd + Vent, mens støjniveauet i baggrunden kontrolles. + Det omgivende støjniveau er for højt til at optage din stemme. Gå et sted hen, hvor der er mere stille, og prøv igen. + Tryk på Næste, når du er klar. + + Toneaudiometri + Denne aktivitet måler din evne til at høre forskellige lyde. + Før du starter, skal du tilslutte dine hovedtelefoner og tage dem på. + Tryk på Gå i gang for at starte. + Du skulle nu høre en tone. Juster lydstyrken vha. betjeningsmulighederne på siden af enheden.\n\nTryk på knappen, når du er klar til at starte. + Tryk på knappen, hver gang du begynder at høre en lyd. + %1$s Hz, venstre + %1$s Hz, højre + + Rumlig hukommelse + Denne aktivitet måler din rumlige korttidshukommelse ved at bede dig om at gentage den rækkefølge, som de enkelte %1$s blinker i. + blomster + blomster + Nogle af %1$sne vil blinke en ad gangen. Tryk på %2$sne i den samme rækkefølge, som de blinker i. + Nogle af %1$sne vil blinke en ad gangen. Tryk på disse %2$s i modsat rækkefølge af, hvordan de blinkede. + Du starter ved at trykke på Gå i gang og følge nøje med. + %1$s + Point + Se, hvordan de enkelte %1$s blinker + Tryk på de enkelte %1$s i den rækkefølge, de tændes + Tryk på de enkelte %1$s i omvendt rækkefølge + Sekvens færdig + Tryk på Næste for at fortsætte + Prøv igen + Du klarede det ikke helt på den tildelte tid. Tryk på Næste for at fortsætte. + Tiden er gået + Tiden løb ud.\nTryk på Næste for at fortsætte. + Spil udført + På pause + Tryk på Næste for at fortsætte + + Reaktionstid + Denne aktivitet vurderer, hvor lang tid du er om at reagere på et visuelt tegn. + Ryst enheden i tilfældig retning, lige så snart den blå prik vises på skærmen. Du vil blive bedt om at gøre det %D gange. + Tryk på Gå i gang for at starte. + Forsøg %1$s af %2$s + Ryst hurtigt enheden, når den blå cirkel vises + + Tårnet i Hanoi + Denne aktivitet vurderer din evne til at løse opgaver. + Flyt hele stakken til den markerede platform med så få træk som muligt. + Tryk på Gå i gang for at starte + Løs opgaven + Antal træk: %1$s \n %2$s + Jeg kan ikke løse opgaven + + Gå på tid + Denne aktivitet måler funktionen i dine nedre ekstremiteter. + Find et sted, helst udenfor, hvor du trygt kan gå omkring i %1$s i en lige linje så hurtigt som muligt. Sænk ikke farten, før du har passeret mållinjen. + Tryk på Næste for at starte. + Hjælpemiddelenhed + Brug den samme hjælpemiddelenhed til hver test. + Bruger du en ankel-fodortose? + Bruger du hjælpemiddelenheder? + Tryk her for at vælge et svar. + Ingen + Enkeltsidig stok + Enkeltsidig krykke + Tosidig stok + Tosidig krykke + Gangstativ/rollator + Gå op til %1$s i en lige linje. + Vend om, og gå tilbage til det sted, hvor du startede. + Tryk på OK, når du er færdig. + + PASAT + PVSAT + PAVSAT + PASAT-testen måler, hvor hurtigt du behandler lydoplysninger og din evne til at foretage udregninger. + PVSAT-testen måler, hvor hurtigt du behandler synsoplysninger og din evne til at foretage udregninger. + PAVSAT-testen (Paced Auditory and Visual Serial Addition Test) måler, hvor hurtigt du behandler lyd- og synsoplysninger og din evne til at foretage udregninger. + Der vises enkelte tal hvert %1$s. sekund.\nDu skal lægge hvert nye tal sammen med tallet lige før det.\nBemærk: Du skal ikke lægge alle tallene sammen, kun de to sidste tal. + Tryk på Gå i gang for at starte. + Husk dette første tal. + Læg dette nye tal sammen med det forrige. + - + + %1$s-huls Pegtest + Denne aktivitet måler funktionen i dine øvre ekstremiteter ved at bede dig om at anbringe en pind i et hul. Du bliver bedt om at gøre dette %1$s gange. + Både din venstre og højre hånd testes.\nDu skal samle cirklen op så hurtigt som muligt, anbringe den i hullet, og når det er gjort %1$s gange, skal du fjerne den igen %2$s gange. + Både din højre og venstre hånd testes.\nDu skal samle cirklen op så hurtigt som muligt, anbringe den i hullet, og når det er gjort %1$s gange, skal du fjerne den igen %2$s gange. + Tryk på Gå i gang for at starte. + Anbring cirklen i hullet vha. din venstre hånd. + Anbring cirklen i hullet vha. din højre hånd. + Anbring cirklen bag ved stregen vha. din venstre hånd. + Anbring cirklen bag ved stregen vha. din højre hånd. + Saml cirklen op vha. to fingre. + Løft fingrene for at slippe cirklen. + + Aktivitet med rysten + Denne aktivitet måler dine hænders rysten i forskellige positioner. Find et sted, hvor du kan sidde behageligt under hele aktiviteten. + Hold telefonen i den hånd, der er mest påvirket, som vist på billedet nedenfor. + Hold telefonen i din HØJRE hånd som vist på billedet nedenfor. + Hold telefonen i din VENSTRE hånd som vist på billedet nedenfor. + Du vil blive bedt om at udføre %1$s, mens du sidder med telefonen i hånden. + en opgave + to opgaver + tre opgaver + fire opgaver + fem opgaver + Tryk på næste for at fortsætte. + Gør dig klar til at holde telefonen i skødet. + Gør dig klar til at holde telefonen i skødet med din VENSTRE hånd. + Gør dig klar til at holde telefonen i skødet med din HØJRE hånd. + Bliv ved med at holde din telefon i skødet i %1$d sekunder. + Hold nu din telefon i skulderhøjde med udstrakt arm. + Hold nu din telefon i skulderhøjde med VENSTRE arm udstrakt. + Hold nu din telefon i skulderhøjde med HØJRE arm udstrakt. + Bliv ved med at holde din telefon med udstrakt hånd i %1$d sekunder. + Hold nu din telefon i skulderhøjde med bøjet albue. + Hold nu din telefon med din VENSTRE hånd i skulderhøjde med bøjet albue. + Hold nu din telefon med din HØJRE hånd i skulderhøjde med bøjet albue. + Bliv ved med at holde din telefon med bøjet albue i %1$d sekunder + Hold nu din telefon med bøjet albue, mens du bliver ved med at røre din næse med telefonen. + Hold nu din telefon med bøjet albue i VENSTRE hånd, mens du bliver ved med at røre din næse med telefonen. + Hold nu din telefon med bøjet albue i HØJRE hånd, mens du bliver ved med at røre din næse med telefonen. + Bliv ved med at røre din næse med telefonen i %1$d sekunder. + Gør dig klar til at udføre et dronningevink (vink ved at dreje dit håndled). + Gør dig klar til at udføre et dronningevink (vink ved at dreje dit håndled) med telefonen i VENSTRE hånd. + Gør dig klar til at udføre et dronningevink (vink ved at dreje dit håndled) med telefonen i HØJRE hånd. + Bliv ved med at udføre et dronningevink i %1$d sekunder. + Flyt nu telefonen til din VENSTRE hånd, og fortsæt til næste opgave. + Flyt nu telefonen til din HØJRE hånd, og fortsæt til næste opgave. + Fortsæt til den næste opgave. + Aktivitet færdig. + Du vil blive bedt om at udføre %1$s, mens du sidder med telefonen først i den ene hånd og derefter igen med telefonen i den anden hånd. + Jeg kan ikke udføre denne aktivitet med min VENSTRE hånd. + Jeg kan ikke udføre denne aktivitet med min HØJRE hånd. + Jeg kan udføre denne aktivitet med begge hænder. + + Kunne ikke oprette arkiv + Kunne ikke fjerne tilstrækkeligt med logarkiver til at nå grænsen + Fejl under indstilling af egenskab + Arkiv ikke mærket som slettet (ikke mærket som overført) + Flere fejl under fjernelse af logarkiver + Der blev ikke fundet nogen indsamlede data. + Intet resultatbibliotek anført + + Ingen data + + Tilbage + Illustration af %1$s + Valgt felt til signatur + Rør ved skærmen, og bevæg din finger for at underskrive + Underskrevet + Ikke underskrevet + Valgt + Ikke valgt + Svarmærke. Udsnit fra %1$s til %2$s + Billede uden mærke + Start opgave + aktiv + rigtigt + forkert + passiv + Brik i memoryspil + Tag billede af eksempel + Taget billede + Eksempel på videooptagelse + Optaget video + Kan ikke anbringe ring i størrelse %1$s på ring i størrelse %2$s + Mål + Tårn + Tryk to gange for at anbringe ring + Tryk to gange for at vælge den øverste ring + Har ringe med størrelserne %1$s + Tom + Udsnit fra %1$s til %2$s + Stak består af + og + Punkt: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-de/strings.xml b/backbone/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..3f3488778 --- /dev/null +++ b/backbone/src/main/res/values-de/strings.xml @@ -0,0 +1,396 @@ + + + + + Zustimmung + Vorname + Nachname + Erforderlich + Überprüfen + Überprüfe das nachstehende Formular und tippe auf „Zustimmen“, wenn du bereit bist fortzufahren. + Überprüfen + Unterschrift + Unterschreibe unten auf der Linie mit deinem Finger. + Hier unterschreiben + Seite %1$ld von %2$ld + + Willkommen + Datenerfassung + Datenschutz + Datennutzung + Studienumfrage + Studienaufgaben + Zeitaufwand + Aussteigen + Weitere Infos + + Weitere Infos über die Erfassung der Daten + Weitere Infos über die Verwendung der Daten + Weitere Infos über den Schutz deiner Daten und Identität + Zunächst weitere Infos über diese Studie + Weitere Infos über die Studienumfrage + Weitere Infos über den für diese Studie benötigten Zeitaufwand + Weitere Infos über die Aufgaben in der Studie + Weitere Infos zum Aussteigen aus der Studie + + Freigabe-Optionen + Meine Daten für %1$s und qualifizierte Forscher auf der ganzen Welt freigeben + Meine Daten nur freigeben für „%1$s“ + Die Studiendaten von deiner Teilnahme an dieser Studie werden an %1$s weitergegeben.\n\nDie Freigabe deiner verschlüsselten Studiendaten (ohne Infos wie z. B. dein Name) können für diese und künftige Studienzwecke von Nutzen sein. + Weitere Infos zur Datenfreigabe + + Name von%1$s (Druckbuchstaben) + Unterschrift von %1$s + Datum + + Schritt %1$s von %2$s + + Ungültiger Wert + %1$s liegt über dem erlaubten Höchstwert (%2$s). + %1$s liegt unter dem erlaubten Mindestwert (%2$s). + %1$s ist kein gültiger Wert. + + Ungültige E-Mail-Adresse: %1$s + + Eine Adresse eingeben + Angegebene Adresse konnte nicht gefunden werden + Der aktuelle Standort konnte nicht ermittelt werden. Gib eine Adresse ein oder begib dich ggf. an einen Ort mit einem besseren GPS-Signal. + Diese App hat keinen Zugriff auf die Ortungsdienste. Erlaube ihr in den Einstellungen das Verwenden der Ortungsdienste. + Die eingegebene Adresse konnte nicht gefunden werden. Überprüfe, dass es sich um eine gültige Adresse handelt. + Es besteht entweder keine Internetverbindung oder du hast die höchstmögliche Anzahl an Adresssuchen erreicht. Wenn du nicht mit dem Internet verbunden bist, aktiviere WLAN, um diese Frage zu beantworten, oder überspringe sie, wenn die Taste „Überspringen“ angezeigt wird. Du kannst auch zur Umfrage zurückkehren, wenn eine Internetverbindung hergestellt wurde. Versuche es andernfalls zu einem späteren Zeitpunkt erneut. + + Textinhalt überschreitet die Maximallänge: %1$s + + Kamera in der geteilten Darstellung nicht verfügbar. + + E-Mail + max@example.com + Passwort + Passwort eingeben + Bestätigen + Erneut versuchen + Die Passwörter stimmen nicht überein. + Zusätzliche Infos + Max + Bauer + Geschlecht + Geschlecht wählen + Geburtsdatum + Datum wählen + + Überprüfung + E‑Mail überprüfen + Wenn du keine E-Mail zur Überprüfung erhalten hast, klicke auf den Link unten, um sie dir erneut senden zu lassen. + Bestätigungs-E‑Mail erneut senden + + Anmeldung + Passwort vergessen? + + Code eingeben + Code bestätigen + Code gesichert + Code authentifiziert + Alten Code eingeben + Neuen Code eingeben + Neuen Code bestätigen + Code nicht korrekt + Code stimmte nicht überein. Versuche es erneut. + Mit Touch ID authentifizieren + Touch ID-Fehler + Es sind nur numerische Zeichen zulässig. + Statusanzeige für Codeeingabe + %1$s von %2$s Stellen eingegeben + Code vergessen? + + Schlüsselbundobjekt konnte nicht hinzugefügt werden + Schlüsselbundobjekt konnte nicht aktualisiert werden + Schlüsselbundobjekt konnte nicht entfernt werden + Schlüsselbundobjekt konnte nicht gefunden werden + + A+ + A- + AB+ + AB- + B+ + B- + 0+ + 0- + + Weiblich + Männlich + Sonstiges + + Nein + Ja + + cm + ft + in + + Zum Antworten tippen + Antwort wählen + Zur Auswahl tippen + Zum Schreiben tippen + + Akzeptieren + Abbrechen + Ok + Löschen + Ablehnen + Fertig + Los geht’s + Weitere Infos + Weiter + Überspringen + Frage überspringen + Timer starten + Für später sichern + Ergebnisse löschen + Aufgabe beenden + Sichern + Antwort löschen + Diese Antwort kann nicht geändert werden. + + Aktivität beginnt in + Aktivität abgeschlossen + Deine Daten werden analysiert und du wirst benachrichtigt, wenn deine Ergebnisse bereitstehen. + Noch %1$s Sekunden. + + Bild aufnehmen + Bild erneut aufnehmen + Keine Kamera gefunden. Dieser Schritt kann nicht abgeschlossen werden. + Erlaube der App in den Einstellungen Zugriff auf die Kamera, um diesen Schritt abzuschließen. + Es wurde keine Ausgabeverzeichnis für aufgenommene Bilder festgelegt. + Das aufgenommene Bild konnte nicht gesichert werden. + + Aufnahme starten + Aufnahme stoppen + Video erneut aufnehmen + + Fitness + Strecke (%1$s) + Herzfrequenz (bpm) + Setze dich %1$s bequem hin. + Gehe %1$s lang so schnell wie möglich. + Mit dieser Aktivität wird deine Herzfrequenz kontrolliert und gemessen, wie weit du in %1$s laufen kannst. + Gehe draußen %1$s lang so schnell wie möglich. Setze dich anschließend %2$s bequem hin und ruhe dich aus. Tippe zum Starten auf „Los geht’s“. + + Gang und Gleichgewicht + Mit dieser Aktivität wird dein Gang und Gleichgewichtsinn beim Gehen und Stillstehen getestet. Fahre nur fort, wenn du ohne Hilfe sicher gehen kannst. + Finde eine Stelle, an der du sicher und ohne Hilfe etwa %1$d Schritte in einer geraden Linie gehen kannst. + Stecke dein iPhone in eine Tasche und folge den Audioanweisungen. + Stehe jetzt %1$s still. + Stehe %1$s still. + Drehe dich um und gehe zum Ausgangspunkt zurück. + Gehe bis zu %1$d Schritte in einer geraden Linie. + + Suche einen Ort an dem du sicher in einer geraden Linie vor und zurück gehen kannst. Versuche, ununterbrochen zu gehen, indem du am Ende des Wegs so wendest, als würdest du um ein Hütchen gehen.\n\nAnschließend wirst du aufgefordert, dich einmal im Kreis zu drehen und anschließend mit angelegten Armen etwa schulterbreit still zu stehen. + Tippe, wenn du bereit bist, anzufangen.\nStecke dann dein iPhone in eine Tasche und folge den Audioanweisungen. + Gehe in einer geraden Linie %1$s weit vor und zurück. Gehe so, wie du normal gehst. + Drehe dich einmal im Kreis und bewege dich dann %1$s lang nicht. + Du hast diese Aktivität abgeschlossen. + + Tipptempo + Rechte Hand + Linke Hand + Diese Aktivität misst dein Tipptempo. + Lege dein Telefon auf eine ebene Fläche. + Verwende zwei Finger derselben Hand, um abwechselnd auf die Tasten auf dem Bildschirm zu tippen. + Verwende zwei Finger deiner rechten Hand, um abwechselnd auf die Tasten auf dem Bildschirm zu tippen. + Verwende zwei Finger deiner linken Hand, um abwechselnd auf die Tasten auf dem Bildschirm zu tippen. + Wiederhole den Test nun mit deiner rechten Hand. + Wiederhole den Test nun mit deiner linken Hand. + Tippe erst mit einem Finger und dann mit dem anderen. Versuche in möglichst gleichmäßigen Abständen zu tippen. Tippe %1$s lang. + Tippe zum Starten auf „Los geht’s“. + Tippe auf „Weiter“, um anzufangen. + Tippen + Tippanzahl + Tippe mit zwei Fingern so gleichmäßig wie möglich auf die Tasten. + Tippe mit deiner RECHTEN Hand auf die Tasten. + Tippe mit deiner LINKEN Hand auf die Tasten. + Diese Hand überspringen + + Stimme + Tippe zum Starten auf „Los geht’s“. + Sage solange wie möglich „Aaaaah“ in das Mikrofon. + Hole tief Luft und sage solange wie möglich und mit gleichbleibender Lautstärke „Aaaaah“ in das Mikrofon, sodass die Audiobalken blau bleiben. + Mit dieser Aktivität wird deine Stimme mit dem Mikrofon im unteren Bereich des iPhone aufgenommen und dann bewertet. + Zu laut + Aufnahme von Audiomaterial nicht möglich + Bitte warte, während der Hintergrundgeräuschpegel überprüft wird. + Der Hintergrundgeräuschpegel ist zu hoch, um deine Stimme aufzunehmen. Bitte gehe an einen leiseren Ort und versuche es erneut. + Tippe auf „Weiter“, wenn du bereit bist. + + Tonaudiometrie + Diese Aktivität misst deine Fähigkeit, unterschiedliche Töne zu hören. + Bevor du beginnst, solltest du deine Kopfhörer anschließen und aufsetzen. + Tippe zum Starten auf „Los geht’s“. + Du solltest jetzt einen Ton hören. Passe die Lautstärke mithilfe der Steuerungen an der Seite des Geräts an.\n\nTippe auf die Taste, wenn du bereit bist. + Tippe jedes Mal, wenn du einen Ton hörst, auf die Taste. + %1$s Hz, Links + %1$s Hz, Rechts + + Räumliches Gedächtnis + Mit dieser Aktivität wird dein räumliches Kurzzeitgedächtnis gemessen, indem du aufgefordert wirst, die Reihenfolge, in der die %1$s aufleuchten, zu wiederholen. + Blumen + Blumen + Einige der %1$s werden der Reihe nach aufleuchten. Tippe in derselben Reihenfolge auf diese %2$s. + Einige der %1$s werden der Reihe nach aufleuchten. Tippe in umgekehrter Reihenfolge auf diese %2$s. + Tippe zum Starten auf „Los geht’s“ und schaue dann genau hin. + %1$s + Ergebnis + Auf das Aufleuchten der %1$s achten + Tippe auf die %1$s in der Reihenfolge, in der sie aufleuchten + Tippe in umgekehrter Reihenfolge auf die %1$s + Sequenz abgeschlossen + Zum Fortfahren tippst du auf „Weiter“ + Erneut versuchen + Du hast es diesmal nicht ganz geschafft. Zum Fortfahren tippst du auf „Weiter“. + Zeit ist abgelaufen + Die Zeit ist abgelaufen.\nZum Fortfahren tippst du auf „Weiter“. + Spiel abgeschlossen + Angehalten + Zum Fortfahren tippst du auf „Weiter“ + + Reaktionszeit + Mit dieser Aktivität wird bewertet, wie lange du brauchst, um auf optische Reize zu reagieren. + Schüttle das Gerät, sobald der blaue Punkt auf dem Bildschirm angezeigt wird. Du wirst %D mal darum gebeten. + Tippe zum Starten auf „Los geht’s“. + Versuch %1$s von %2$s + Wenn der blaue Kreis angezeigt wird, schüttle das Gerät schnell + + Türme von Hanoi + Diese Aktivität misst deine Fähigkeit, Rätsel zu lösen. + Bewege den gesamten Stapel in so wenigen Zügen wie möglich auf den markierten Turm. + Tippe zum Starten auf „Los geht’s“ + Löse das Rätsel + Anzahl Züge: %1$s \n %2$s + Ich kann dieses Rätsel nicht lösen + + Gehen mit Zeitmessung + Diese Aktivität misst die Funktion deiner unteren Extremitäten + Finde einen Ort, am besten draußen, an dem du sicher ca. %1$s so schnell wie möglich in einer geraden Linie gehen kannst. Werde erst langsamer, wenn du das Ziel erreicht hast. + Tippe auf „Weiter“, um anzufangen. + Gehhilfe + Verwende für jeden Test dieselbe Gehhilfe. + Verwendest du eine Unterschenkelorthese? + Verwendest du Gehhilfen? + Tippen, um eine Antwort auszuwählen. + Ohne + Gehstock, eine Seite + Krücke, eine Seite + Gehstock, beide Seiten + Krücken, beide Seiten + Rollator + Gehe bis zu %1$s in einer geraden Linie. + Drehe dich um und gehe zum Ausgangspunkt zurück. + Tippe danach auf „Fertig“. + + PASAT + PVSAT + PAVSAT + Der akustische Serienaddiertest mit Zeitlimit (PASAT, Paced Auditory Serial Addition Test) misst, wie schnell du akustische Informationen verarbeiten und rechnen kannst. + Der visuelle Serienaddiertest mit Zeitlimit (PASAT, Paced Visual Serial Addition Test) misst, wie schnell du optische Informationen verarbeiten und rechnen kannst. + Der audiovisuelle Serienaddiertest mit Zeitlimit (PAVSAT, Paced Auditory and Visual Serial Addition Test) misst, wie schnell du akustische und optische Informationen verarbeiten und rechnen kannst. + Alle %1$s Sekunden wird eine einstellige Zahl angezeigt.\nDu musst diese Zahl zu der addieren, die direkt zuvor angezeigt wurde.\nAchtung: Du sollst nicht die Gesamtsumme der Zahlen errechnen, sondern nur die jeweils beiden letzten Zahlen addieren. + Tippe zum Starten auf „Los geht’s“. + Merke dir die erste Zahl. + Addiere diese Zahl zur vorherigen. + - + + %1$s-Hole-Peg-Test + Mit dieser Aktivität wird die Reaktionsfähigkeit deiner oberen Extremitäten gemessen, indem du aufgefordert wirst, einen Kreis auf einem Loch zu platzieren. Das Ganze wird %1$s-mal wiederholt. + Die Reaktionsfähigkeit deiner linken und rechten Hand wird getestet.\nBewege den Kreis mit zwei Fingern so schnell wie möglich auf das Loch. Wenn du dies %1$s-mal getan hast, entferne ihn %2$s-mal. + Die Reaktionsfähigkeit deiner linken und rechten Hand wird getestet.\nBewege den Kreis mit zwei Fingern so schnell wie möglich auf das Loch. Wenn du dies %1$s-mal getan hast, entferne ihn %2$s-mal. + Tippe zum Starten auf „Los geht’s“. + Bewege den Kreis mit der linken Hand in das Loch. + Bewege den Kreis mit der rechten Hand in das Loch. + Bewege den Kreis mit der linken Hand hinter die Linie. + Bewege den Kreis mit der rechten Hand hinter die Linie. + Bewege den Kreis mit zwei Fingern. + Hebe den Finger, um den Kreis auf die Zielposition zu setzen. + + Zitteraktivität + Mit dieser Aktivität wird gemessen, wie sehr deine Hände in unterschiedlichen Haltungen zittern. Führe diese Aktivität an einem Ort durch, an dem du währenddessen bequem sitzen kannst. + Halte dein Telefon in der stärker betroffenen Hand, wie unten im Bild zu sehen. + Halte dein Telefon in der RECHTEN Hand, wie unten im Bild zu sehen. + Halte dein Telefon in der LINKEN Hand, wie unten im Bild zu sehen. + Du wirst aufgefordert %1$s durchzuführen, während du sitzend dein Telefon in der Hand hältst. + eine Aufgabe + zwei Aufgaben + drei Aufgaben + vier Aufgaben + fünf Aufgaben + Tippe auf „Weiter“, um fortzufahren. + Bereite dich darauf vor, dein Telefon im Schoß zu halten. + Bereite dich darauf vor, dein Telefon mit der LINKEN Hand im Schoß zu halten. + Bereite dich darauf vor, dein Telefon mit der RECHTEN Hand im Schoß zu halten. + Halte %1$d Sekunden lang dein Telefon in deinem Schoß. + Halte dein Telefon nun mit ausgestreckter Hand in Schulterhöhe. + Halte dein Telefon nun mit ausgestreckter LINKER Hand in Schulterhöhe. + Halte dein Telefon nun mit ausgestreckter RECHTER Hand in Schulterhöhe. + Halte mit ausgestreckter Hand %1$d Sekunden lang dein Telefon. + Halte dein Telefon nun mit angewinkelten Ellenbogen in Schulterhöhe. + Halte dein Telefon nun in deiner LINKEN Hand mit angewinkeltem Ellenbogen in Schulterhöhe. + Halte dein Telefon nun in deiner RECHTEN Hand mit angewinkeltem Ellenbogen in Schulterhöhe. + Halte mit angewinkelten Ellenbogen %1$d Sekunden lang dein Telefon + Winkle nun deinen Ellenbogen an und berühre wiederholt mit deinem Telefon deine Nase. + Winkle nun deinen Ellenbogen an, halte dein Telefon in der LINKEN Hand und berühre wiederholt mit deinem Telefon deine Nase. + Winkle nun deinen Ellenbogen an, halte dein Telefon in der RECHTEN Hand und berühre wiederholt mit deinem Telefon deine Nase. + Halte dein Telefon %1$d Sekunden lang an deine Nase + Bereite dich darauf vor, zu winken, indem du dein Handgelenk drehst. + Bereite dich darauf vor, mit dem Telefon in deiner LINKEN Hand zu winken, indem du dein Handgelenk drehst. + Bereite dich darauf vor, mit dem Telefon in deiner RECHTEN Hand zu winken, indem du dein Handgelenk drehst. + Winke %1$d Sekunden lang. + Wechsle nun zu deiner LINKEN Hand und fahre mit der nächsten Aufgabe fort. + Wechsle nun zu deiner RECHTEN Hand und fahre mit der nächsten Aufgabe fort. + Weiter zur nächsten Aufgabe. + Aktivität abgeschlossen. + Du wirst aufgefordert %1$s durchzuführen, während du sitzend dein Telefon zuerst in der einen, dann in der anderen Hand hältst. + Ich kann diese Aktivität mit meiner LINKEN Hand nicht durchführen. + Ich kann diese Aktivität mit meiner RECHTEN Hand nicht durchführen. + Ich kann diese Aktivität mit beiden Händen durchführen. + + Fehler beim Erstellen der Datei + Es konnten nicht genügend Protokolldateien gelöscht werden, um den Schwellenwert zu erreichen. + Fehler beim Einstellen des Attributs + Datei nicht als gelöscht markiert (nicht als geladen markiert) + Mehrere Fehler beim Entfernen von Protokollen + Es wurden keine erfassten Daten gefunden. + Kein Ausgabeverzeichnis angegeben + + Keine Daten + + Zurück + Abbildung von %1$s + Vorgesehenes Unterschriftsfeld + Bildschirm berühren und mit dem Finger unterschreiben + Unterschrieben + Keine Unterschrift + Ausgewählt + Nicht ausgewählt + Antwortregler. Bereich von %1$s bis %2$s + Nicht markiertes Bild + Aufgabe starten + aktiv + richtig + falsch + inaktiv + Gedächtnisspielkarten + Vorschau aufnehmen + Aufgenommenes Bild + Videoaufnahmenvorschau + Aufgenommenes Video + Die Scheibe der Größe %1$s kann nicht auf die Scheibe der Größe %2$s gelegt werden + Ziel + Turm + Zum Ablegen der Scheibe doppeltippen + Zum Auswählen der obersten Scheibe doppeltippen + Mit Scheiben in den Größen %1$s + Leer + Bereich von %1$s bis %2$s + Stapel besteht aus + und + Punkt: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-el/strings.xml b/backbone/src/main/res/values-el/strings.xml new file mode 100644 index 000000000..d1bc56b27 --- /dev/null +++ b/backbone/src/main/res/values-el/strings.xml @@ -0,0 +1,397 @@ + + + + + Συναίνεση + Όνομα + Επώνυμο + Απαιτείται + Έλεγχος + Διαβάστε την παρακάτω φόρμα και αγγίξτε «Συμφωνώ» όταν είστε έτοιμοι να συνεχίσετε. + Έλεγχος + Υπογραφή + Υπογράψτε στην παρακάτω γραμμή χρησιμοποιώντας το δάχτυλό σας. + Υπογράψτε εδώ + Σελίδα %1$ld από %2$ld + + Καλώς ορίσατε + Συλλογή δεδομένων + Απόρρητο + Χρήση δεδομένων + Έρευνα μελέτης + Εργασίες μελέτης + Απαιτούμενος χρόνος + Απόσυρση + Μάθετε περισσότερα + + Μάθετε περισσότερα για τη συλλογή δεδομένων + Μάθετε περισσότερα για τον τρόπο χρήσης των δεδομένων + Μάθετε περισσότερα για τον τρόπο προστασίας του απορρήτου και της ταυτότητάς σας + Μάθετε περισσότερα για τη μελέτη πρώτα + Μάθετε περισσότερα για την έρευνα μελέτης + Μάθετε περισσότερα για τον χρόνο που θα χρειαστείτε για να ολοκληρώσετε τη μελέτη + Μάθετε περισσότερα για τις σχετιζόμενες εργασίες + Μάθετε περισσότερα για την απόσυρση από την έρευνα + + Επιλογές κοινοποίησης + Κοινοποίηση των δεδομένων μου στο %1$s και πιστοποιημένους ερευνητές ανά τον κόσμο + Κοινοποίηση των δεδομένων μου μόνο σε: %1$s + Το %1$s θα λάβει τα δεδομένα που απορρέουν από τη συμμετοχή σας στην παρούσα μελέτη.\n\nΗ πιο ευρεία κοινοποίηση των κωδικοποιημένων δεδομένων μελέτης σας (χωρίς στοιχεία όπως το όνομά σας) μπορεί να συμβάλλει σε αυτήν την έρευνα, καθώς και σε μελλοντικές έρευνες. + Μάθετε περισσότερα για την κοινοποίηση δεδομένων + + %1$s: όνομα (με κεφαλαία) + %1$s: υπογραφή + Ημερομηνία + + Βήμα %1$s από %2$s + + Μη έγκυρη τιμή + Η τιμή «%1$s» υπερβαίνει τη μέγιστη επιτρεπόμενη τιμή (%2$s). + Η τιμή «%1$s» είναι χαμηλότερη από την ελάχιστη επιτρεπόμενη τιμή (%2$s). + Το «%1$s» δεν είναι έγκυρη τιμή. + + Μη έγκυρη διεύθυνση email: %1$s + + Εισαγάγετε διεύθυνση + Δεν βρέθηκε η καθορισμένη διεύθυνση + Δεν είναι δυνατή η επίλυση της τρέχουσας τοποθεσίας σας. Πληκτρολογήστε μια διεύθυνση ή μετακινηθείτε σε μια τοποθεσία με καλύτερο σήμα GPS, αν είναι δυνατόν. + Δεν επιτράπηκε η πρόσβαση στις υπηρεσίες τοποθεσίας. Παραχωρήστε άδεια σε αυτήν την εφαρμογή για χρήση των υπηρεσιών τοποθεσίας μέσω των Ρυθμίσεων. + Δεν είναι δυνατή η εύρεση αποτελέσματος για την εισηγμένη διεύθυνση. Βεβαιωθείτε ότι η διεύθυνση είναι έγκυρη. + Είτε δεν είστε συνδεδεμένοι στο Διαδίκτυο είτε έχετε υπερβεί το μέγιστο πλήθος αιτήσεων αναζήτησης διευθύνσεων. Αν δεν είστε συνδεδεμένοι στο Διαδίκτυο, ενεργοποιήστε το Wi-Fi για να απαντήσετε σε αυτήν την ερώτηση, παραλείψτε την ερώτηση αν είναι διαθέσιμο το κουμπί παράλειψης, ή επιστρέψτε στο ερωτηματολόγιο όταν συνδεθείτε στο Διαδίκτυο. Διαφορετικά, δοκιμάστε ξανά σε λίγα λεπτά. + + Το περιεχόμενο κειμένου υπερβαίνει το επιτρεπόμενο όριο χαρακτήρων: %1$s + + Η κάμερα δεν διατίθεται στην Προβολή διαίρεσης. + + Email + gmilosporos@example.com + Συνθηματικό + Εισαγάγετε συνθηματικό + Επιβεβαίωση + Εισαγάγετε πάλι το συνθηματικό + Τα συνθηματικά δεν ταιριάζουν. + Πρόσθετες πληροφορίες + Γιάννης + Μηλοσπόρος + Φύλο + Επιλέξτε φύλο + Ημερομηνία γέννησης + Επιλέξτε ημερομηνία + + Επαλήθευση + Επαληθεύστε το email σας + Αν δεν έχετε λάβει email επαλήθευσης, αγγίξτε τον παρακάτω σύνδεσμο για να σταλεί ξανά. + Νέα αποστολή email επιβεβαίωσης + + Είσοδος + Ξεχάσατε το συνθηματικό; + + Εισαγάγετε συνθηματικό + Επιβεβαίωση συνθηματικού + Το συνθηματικό αποθηκεύτηκε + Έγινε έλεγχος ταυτότητας συνθηματικού + Εισαγάγετε το παλιό συνθηματικό + Εισαγάγετε το νέο συνθηματικό + Επιβεβαίωση νέου συνθηματικού + Λανθασμένο συνθηματικό + Ασυμφωνία συνθηματικών. Δοκιμάστε ξανά. + Πραγματοποιήστε έλεγχο ταυτότητας με Touch ID + Σφάλμα Touch ID + Επιτρέπονται μόνο αριθμητικοί χαρακτήρες. + Ένδειξη προόδου εισαγωγής συνθηματικού + Εισάχθηκαν %1$s από %2$s ψηφία + Ξεχάσατε το συνθηματικό; + + Δεν ήταν δυνατή η προσθήκη στοιχείου της Κλειδοθήκης. + Δεν ήταν δυνατή η ενημέρωση στοιχείου της Κλειδοθήκης. + Δεν ήταν δυνατή η διαγραφή στοιχείου της Κλειδοθήκης. + Δεν ήταν δυνατή η εύρεση στοιχείου της Κλειδοθήκης. + + A+ + Α- + AB+ + AB- + B+ + B- + 0+ + 0- + + Γυναίκα + Άντρας + Άλλο + + Όχι + Ναι + + εκ. + πδ + ίντσ. + + Αγγίξτε για απάντηση + Επιλέξτε μια απάντηση + Αγγίξτε για επιλογή + Αγγίξτε για να γράψετε + + Συμφωνώ + Ακύρωση + OK + Εκκαθάριση + Διαφωνώ + Τέλος + Έναρξη + Μάθετε περισσότερα + Επόμενο + Παράλειψη + Παράλειψη αυτής της ερώτησης + Έναρξη χρονοδιακόπτη + Αποθήκευση για αργότερα + Απόρριψη αποτελεσμάτων + Τέλος εργασίας + Αποθήκευση + Εκκαθάριση απάντησης + Δεν είναι δυνατή η τροποποίηση αυτής της απάντησης. + + Έναρξη δραστηριότητας σε + Η δραστηριότητα ολοκληρώθηκε + Τα δεδομένα σας θα αναλυθούν και θα ειδοποιηθείτε όταν είναι έτοιμα τα αποτελέσματα. + Απομένουν %1$s δευτερόλεπτα. + + Καταγραφή εικόνας + Επανάληψη καταγραφής εικόνας + Δεν βρέθηκε κάμερα. Δεν είναι δυνατή η ολοκλήρωση αυτού του βήματος. + Για να ολοκληρώσετε αυτό το βήμα, επιτρέψτε σε αυτήν την εφαρμογή να προσπελάζει την κάμερα στις Ρυθμίσεις. + Δεν καθορίστηκε κατάλογος εξόδου για καταγραμμένες εικόνες. + Δεν ήταν δυνατή η αποθήκευση της καταγραμμένης εικόνας. + + Έναρξη εγγραφής + Διακοπή εγγραφής + Εκ νέου καταγραφή βίντεο + + Φυσική κατάσταση + Απόσταση (%1$s) + Καρδιακοί παλμοί (π.α.λ.) + Καθίστε κάπου άνετα για %1$s. + Περπατήστε όσο πιο γρήγορα μπορείτε για %1$s. + Αυτή η δραστηριότητα αξιολογεί τους καρδιακούς παλμούς σας και μετρά πόσο μακριά μπορείτε να περπατήσετε σε %1$s. + Περπατήστε σε εξωτερικό χώρο όσο πιο γρήγορα μπορείτε για %1$s. Όταν τελειώσετε, καθίστε κάπου άνετα και ξεκουραστείτε για %2$s. Για να αρχίσετε, αγγίξτε «Έναρξη». + + Βάδισμα και ισορροπία + Αυτή η δραστηριότητα αξιολογεί το βάδισμα και την ισορροπία σας καθώς περπατάτε και στέκεστε όρθιοι. Μην πραγματοποιήσετε τη δραστηριότητα αν δεν μπορείτε να περπατήσετε με ασφάλεια χωρίς βοήθεια. + Βρείτε έναν χώρο όπου μπορείτε να περπατήσετε με ασφάλεια χωρίς βοήθεια και κάντε περίπου %1$d βήματα σε ευθεία γραμμή. + Τοποθετήστε το τηλέφωνό σας στην τσέπη ή την τσάντα σας και μετά ακολουθήστε τις ηχητικές οδηγίες. + Τώρα μείνετε ακίνητοι για %1$s. + Μείνετε ακίνητοι για %1$s. + Γυρίστε πίσω και περπατήστε μέχρι το σημείο όπου αρχίσατε. + Περπατήστε έως %1$d βήματα σε ευθεία γραμμή. + + Βρείτε έναν χώρο όπου μπορείτε να περπατήσετε με ασφάλεια μπρος-πίσω σε ευθεία γραμμή. Προσπαθήστε να μη διακόψετε το βάδισμα, στρίβοντας στο τέλος της γραμμής και επιστρέφοντας πάλι εκεί που αρχίσατε και πάλι το ίδιο, σαν να περπατάτε γύρω από έναν κώνο.\n\nΚατόπιν, θα σας ζητηθεί να κάνετε μια πλήρη στροφή γύρω από τον εαυτό σας και μετά να σταθείτε ακίνητοι με τα χέρια ευθεία κάτω στο πλάι του σώματος και τα πόδια σας ανοιχτά σε απόσταση παρόμοια των ώμων σας. + Αγγίξτε «Έναρξη» όταν είστε έτοιμοι να ξεκινήσετε.\nΜετά, τοποθετήστε το τηλέφωνό σας στην τσέπη ή την τσάντα σας και ακολουθήστε τις ηχητικές οδηγίες. + Περπατήστε μπρος-πίσω σε ευθεία γραμμή για %1$s στον συνηθισμένο σας ρυθμό. + Κάντε μια πλήρη στροφή γύρω από τον εαυτό σας και μετά μείνετε ακίνητοι για %1$s. + Ολοκληρώσατε τη δραστηριότητα. + + Ταχύτητα αγγίγματος + Δεξιό χέρι + Αριστερό χέρι + Αυτή η δραστηριότητα αξιολογεί την ταχύτητα αγγιγμάτων σας. + Τοποθετήστε το τηλέφωνο σε επίπεδη επιφάνεια. + Χρησιμοποιήστε δύο δάχτυλα του ίδιου χεριού για εναλλάξ άγγιγμα των κουμπιών στην οθόνη. + Χρησιμοποιήστε δύο δάχτυλα του δεξιού χεριού για εναλλάξ άγγιγμα των κουμπιών στην οθόνη. + Χρησιμοποιήστε δύο δάχτυλα του αριστερού χεριού για εναλλάξ άγγιγμα των κουμπιών στην οθόνη. + Τώρα επαναλάβετε την ίδια δοκιμή με το δεξιό σας χέρι. + Τώρα επαναλάβετε την ίδια δοκιμή με το αριστερό σας χέρι. + Αγγίξτε με το ένα δάχτυλο και μετά με το άλλο. Προσπαθήστε ώστε ο χρόνος μεταξύ των αγγιγμάτων να είναι όσο το δυνατόν ίδιος. Συνεχίστε τα αγγίγματα για %1$s. + Αγγίξτε «Έναρξη» για να αρχίσετε. + Αγγίξτε «Επόμενο» για να ξεκινήσετε. + Αγγίξτε + Συνολικά αγγίγματα + Αγγίξτε τα κουμπιά χρησιμοποιώντας δύο δάχτυλα και φροντίστε ο χρόνος μεταξύ των αγγιγμάτων να είναι όσο το δυνατόν ίδιος. + Αγγίξτε τα κουμπιά χρησιμοποιώντας το ΔΕΞΙΟ σας χέρι. + Αγγίξτε τα κουμπιά χρησιμοποιώντας το ΑΡΙΣΤΕΡΟ σας χέρι. + Παράλειψη αυτού του χεριού + + Φωνή + Αγγίξτε «Έναρξη» για να αρχίσετε. + Πείτε «Ααααα» στο μικρόφωνο για όση περισσότερη ώρα μπορέσετε. + Πάρτε μια βαθιά ανάσα και πείτε «Ααααα» στο μικρόφωνο για όση περισσότερη ώρα μπορέσετε. Κρατήστε σταθερή την ένταση ήχου της φωνής σας για να παραμείνουν μπλε οι γραμμές ήχου. + Αυτή η δραστηριότητα αξιολογεί τη φωνή σας μέσω εγγραφής ενώ μιλάτε στο μικρόφωνο που βρίσκεται στο κάτω μέρος του τηλεφώνου. + Πολύ δυνατά + Δεν είναι δυνατή η εγγραφή ήχου + Περιμένετε μέχρι να ελεγχθεί το επίπεδο θορύβου υποβάθρου. + Το επίπεδο περιβαλλοντικού θορύβου είναι πολύ υψηλό για εγγραφή της φωνής σας. Πηγαίνετε σε έναν χώρο με λιγότερο θόρυβο και προσπαθήστε πάλι. + Αγγίξτε «Επόμενο» όταν είστε έτοιμοι. + + Τονική ακοομετρία + Η δραστηριότητα αυτή μετρά την ικανότητά σας να ακούτε διαφορετικούς ήχους. + Πριν ξεκινήσετε, συνδέστε τα ακουστικά και φορέστε τα. + Αγγίξτε «Έναρξη» για να αρχίσετε. + Τώρα θα πρέπει να ακούσετε έναν τόνο. Προσαρμόστε την ένταση ήχου χρησιμοποιώντας τα χειριστήρια στο πλάι της συσκευής σας.\n\nΌταν είστε έτοιμοι να ξεκινήσετε, αγγίξτε το κουμπί. + Αγγίξτε το κουμπί κάθε φορά που θα ακούτε έναν ήχο. + %1$s Hz, αριστερά + %1$s Hz, δεξιά + + Χωρική μνήμη + Αυτή η δραστηριότητα αξιολογεί τη χωρική μνήμη σας ζητώντας σας να επαναλάβετε τη σειρά με την οποία φωτίζονται οι %1$s. + εικόνες + εικόνες + Κάποιες από τις %1$s θα φωτίζονται μία-μία. Αγγίξτε αυτές τις %2$s με την ίδια σειρά με αυτήν που φωτίστηκαν. + Κάποιες από τις %1$s θα φωτίζονται μία-μία. Αγγίξτε αυτές τις %2$s με σειρά αντίστροφη από αυτήν που φωτίστηκαν. + Για να αρχίσετε, αγγίξτε «Έναρξη» και μετά παρακολουθήστε προσεκτικά. + %1$s + Βαθμολογία + Δείτε τις %1$s να φωτίζονται + Αγγίξτε τις %1$s με τη σειρά που επισημάνθηκαν + Αγγίξτε τις %1$s σε αντίστροφη σειρά + Η ακολουθία ολοκληρώθηκε + Για να συνεχίσετε, αγγίξτε «Επόμενο» + Νέα δοκιμή + Δεν τα καταφέρατε αυτήν τη φορά. Αγγίξτε «Επόμενο» για να συνεχίσετε. + Τέλος χρόνου + Ο χρόνος τελείωσε.\nΑγγίξτε «Επόμενο» για να συνεχίσετε. + Το παιχνίδι ολοκληρώθηκε + Σε παύση + Για να συνεχίσετε, αγγίξτε «Επόμενο» + + Χρόνος αντίδρασης + Η δραστηριότητα αυτή υπολογίζει τον χρόνο που χρειάζεστε για να αποκριθείτε σε μια οπτική ένδειξη. + Ανακινήστε τη συσκευή προς οποιαδήποτε κατεύθυνση αμέσως μόλις εμφανιστεί η μπλε κουκκίδα στην οθόνη. Θα σας ζητηθεί να το κάνετε αυτό %D φορές. + Αγγίξτε «Έναρξη» για να αρχίσετε. + Απόπειρα %1$s από %2$s + Ανακινήστε γρήγορα τη συσκευή όταν εμφανιστεί ο μπλε κύκλος + + Πύργος του Ανόι + Αυτή η δραστηριότητα αξιολογεί τις ικανότητές σας ως προς την επίλυση παζλ. + Μετακινήστε ολόκληρη τη στοίβα στην επισημασμένη πλατφόρμα με όσο το δυνατό λιγότερες κινήσεις. + Αγγίξτε «Έναρξη» για να αρχίσετε + Επιλύστε το παζλ + Αριθμός κινήσεων: %1$s \n %2$s + Δεν μπορώ να λύσω αυτό το παζλ + + Χρονισμένο περπάτημα + Αυτή η δραστηριότητα αξιολογεί τη λειτουργία των κάτω άκρων σας. + Βρείτε ένα μέρος, κατά προτίμηση σε εξωτερικό χώρο, όπου μπορείτε να περπατήσετε για περίπου %1$s σε ευθεία γραμμή όσο το δυνατόν πιο γρήγορα αλλά με ασφάλεια. Μη μειώσετε την ταχύτητά σας μέχρι να περάσετε τη γραμμή τερματισμού. + Αγγίξτε «Επόμενο» για να ξεκινήσετε. + Βοηθητική συσκευή + Χρησιμοποιήστε την ίδια βοηθητική συσκευή για κάθε τεστ. + Φοράτε νάρθηκα ποδοκνημικής; + Χρησιμοποιείτε βοηθητική συσκευή; + Αγγίξτε εδώ για επιλογή απάντησης + Όχι + Μονόπλευρο μπαστούνι + Μονόπλευρη πατερίτσα + Δίπλευρο μπαστούνι + Δίπλευρη πατερίτσα + Περιπατητήρας + Περπατήστε έως %1$s σε ευθεία γραμμή. + Γυρίστε πίσω και περπατήστε μέχρι το σημείο όπου αρχίσατε. + Αγγίξτε «Τέλος» όταν τελειώσετε. + + PASAT + PVSAT + PAVSAT + Η δοκιμασία PASAT (Βηματική ακουστική πρόσθεση αριθμών σε σειρά) αξιολογεί την ταχύτητα ακουστικής επεξεργασίας πληροφοριών και την ικανότητα υπολογισμών. + Η δοκιμασία PVSAT (Βηματική οπτική πρόσθεση αριθμών σε σειρά) αξιολογεί την ταχύτητα οπτικής επεξεργασίας πληροφοριών και την ικανότητα υπολογισμών. + Η δοκιμασία PAVSAT (Βηματική ακουστική και οπτική πρόσθεση αριθμών σε σειρά) αξιολογεί την ταχύτητα ακουστικής και οπτικής επεξεργασίας πληροφοριών και την ικανότητα υπολογισμών. + Τα μονά ψηφία εμφανίζονται κάθε %1$s δευτερόλεπτα.\nΠρέπει να προσθέτετε κάθε νέο ψηφίο στο αμέσως προηγούμενο ψηφίο.\nΠροσοχή: Δεν πρέπει να υπολογίζετε το τρέχον σύνολο, αλλά μόνο το άθροισμα των τελευταίων δύο αριθμών. + Αγγίξτε «Έναρξη» για να αρχίσετε. + Θυμηθείτε αυτό το πρώτο ψηφίο. + Προσθέστε αυτό το νέο ψηφίο στο προηγούμενο ψηφίο. + - + + Δοκιμή %1$s-HPT + Η δραστηριότητα αυτή μετρά την επιδεξιότητα των άνω άκρων σας ζητώντας σας να τοποθετήσετε έναν κύκλο σε μια τρύπα. Αυτό θα σας ζητηθεί να το κάνετε %1$s φορές. + Θα δοκιμαστούν και το αριστερό και το δεξί χέρι σας.\nΠρέπει να σηκώσετε τον κύκλο όσο το δυνατόν πιο γρήγορα, να τον τοποθετήσετε στην τρύπα και μόλις το κάνετε %1$s φορές, να τον αφαιρέσετε πάλι %2$s φορές. + Θα δοκιμαστούν και το δεξί και το αριστερό χέρι σας.\nΠρέπει να σηκώσετε τον κύκλο όσο το δυνατόν πιο γρήγορα, να τον τοποθετήσετε στην τρύπα και μόλις το κάνετε %1$s φορές, να τον αφαιρέσετε πάλι %2$s φορές. + Αγγίξτε «Έναρξη» για να αρχίσετε. + Τοποθετήστε τον κύκλο στην τρύπα με το αριστερό χέρι σας. + Τοποθετήστε τον κύκλο στην τρύπα με το δεξί χέρι σας. + Τοποθετήστε τον κύκλο πίσω από τη γραμμή με το αριστερό χέρι σας. + Τοποθετήστε τον κύκλο πίσω από τη γραμμή με το δεξί χέρι σας. + Σηκώστε τoν κύκλο με δύο δάχτυλα. + Σηκώστε τα δάχτυλα για να αφήσετε τον κύκλο. + + Δραστηριότητα τρέμουλου + Αυτή η δραστηριότητα μετρά το τρέμουλο των χεριών σας σε διάφορες θέσεις. Βρείτε ένα σημείο όπου μπορείτε να κάθεστε άνετα καθ\' όλη τη διάρκεια της δραστηριότητας. + Κρατήστε το τηλέφωνο στο πιο επηρεαζόμενο χέρι σας όπως φαίνεται στην παρακάτω εικόνα. + Κρατήστε το τηλέφωνο στο ΔΕΞΙΟ χέρι σας όπως φαίνεται στην παρακάτω εικόνα. + Κρατήστε το τηλέφωνο στο ΑΡΙΣΤΕΡΟ χέρι σας όπως φαίνεται στην παρακάτω εικόνα. + Θα σας ζητηθεί να εκτελέσετε %1$s σε θέση καθίσματος με το τηλέφωνο στο χέρι σας. + μια εργασία + δύο εργασίες + τρεις εργασίες + τέσσερις εργασίες + πέντε εργασίες + Αγγίξτε «Επόμενο» για να συνεχίσετε. + Προετοιμαστείτε να κρατήσετε το τηλέφωνο πάνω στα πόδια σας. + Προετοιμαστείτε να κρατήσετε το τηλέφωνο πάνω στα πόδια σας με το ΑΡΙΣΤΕΡΟ χέρι. + Προετοιμαστείτε να κρατήσετε το τηλέφωνο πάνω στα πόδια σας με το ΔΕΞΙΟ χέρι. + Συνεχίστε να κρατάτε το τηλέφωνο πάνω στα πόδια σας για %1$d δευτερόλεπτα. + Τώρα κρατήστε το τηλέφωνο στο χέρι σας τεντωμένο στο ύψος του ώμου. + Τώρα κρατήστε το τηλέφωνο στο ΑΡΙΣΤΕΡΟ χέρι τεντωμένο στο ύψος του ώμου. + Τώρα κρατήστε το τηλέφωνο στο ΔΕΞΙΟ χέρι τεντωμένο στο ύψος του ώμου. + Συνεχίστε να κρατάτε το τηλέφωνο με το τεντωμένο χέρι σας για %1$d δευτερόλεπτα. + Τώρα κρατήστε το τηλέφωνο στο ύψος του ώμου με τον αγκώνα λυγισμένο. + Τώρα κρατήστε το τηλέφωνο στο ΑΡΙΣΤΕΡΟ χέρι τεντωμένο στο ύψος του ώμου με τον αγκώνα λυγισμένο. + Τώρα κρατήστε το τηλέφωνο στο ΔΕΞΙΟ χέρι τεντωμένο στο ύψος του ώμου με τον αγκώνα λυγισμένο. + Συνεχίστε να κρατάτε το τηλέφωνο με λυγισμένο τον αγκώνα για %1$d δευτερόλεπτα. + Κρατώντας τον αγκώνα σας λυγισμένο, αγγίξτε τη μύτη σας με το τηλέφωνο επανειλημμένα. + Κρατώντας τον αγκώνα σας λυγισμένο με το τηλέφωνο στο ΑΡΙΣΤΕΡΟ χέρι σας, αγγίξτε τη μύτη σας με το τηλέφωνο επανειλημμένα. + Κρατώντας τον αγκώνα σας λυγισμένο με το τηλέφωνο στο ΔΕΞΙΟ χέρι σας, αγγίξτε τη μύτη σας με το τηλέφωνο επανειλημμένα. + Συνεχίστε να ακουμπάτε τη μύτη σας με το τηλέφωνο για %1$d δευτερόλεπτα. + Προετοιμαστείτε να κάνετε έναν βασιλικό χαιρετισμό (κουνώντας τον καρπό). + Προετοιμαστείτε να κάνετε έναν βασιλικό χαιρετισμό με το τηλέφωνο στο ΑΡΙΣΤΕΡΟ χέρι σας (χαιρετίστε κουνώντας τον καρπό). + Προετοιμαστείτε να κάνετε έναν βασιλικό χαιρετισμό με το τηλέφωνο στο ΔΕΞΙΟ χέρι σας (χαιρετίστε κουνώντας τον καρπό). + Συνεχίστε να χαιρετάτε με αυτόν τον τρόπο για %1$d δευτερόλεπτα. + Τώρα κρατήστε το τηλέφωνο στο ΑΡΙΣΤΕΡΟ χέρι σας και συνεχίστε στην επόμενη εργασία. + Τώρα κρατήστε το τηλέφωνο στο ΔΕΞΙΟ χέρι σας και συνεχίστε στην επόμενη εργασία. + Συνεχίστε στην επόμενη εργασία. + Η δραστηριότητα ολοκληρώθηκε. + Θα σας ζητηθεί να εκτελέσετε %1$s σε θέση καθίσματος με το τηλέφωνο πρώτα στο ένα χέρι και μετά στο άλλο. + Δεν μπορώ να εκτελέσω αυτήν τη δραστηριότητα με το ΑΡΙΣΤΕΡΟ χέρι μου. + Δεν μπορώ να εκτελέσω αυτήν τη δραστηριότητα με το ΔΕΞΙΟ χέρι μου. + Μπορώ να εκτελέσω αυτήν τη δραστηριότητα και με τα δύο χέρια. + + Δεν ήταν δυνατή η δημιουργία αρχείου + Δεν ήταν δυνατή η αφαίρεση επαρκών αρχείων καταγραφής για την επίτευξη ορίου + Σφάλμα κατά τον καθορισμό χαρακτηριστικού + Το αρχείο δεν έχει σημανθεί ως διαγραμμένο (δεν έχει σημανθεί ως απεσταλμένο) + Πολλαπλά σφάλματα κατά την αφαίρεση αρχείων καταγραφής + Δεν βρέθηκαν συλλεγμένα δεδομένα. + Δεν έχει καθοριστεί κατάλογος εξόδου + + Δεν υπάρχουν δεδομένα + + Πίσω + Απεικόνιση για %1$s + Καθορισμένο πεδίο υπογραφής + Αγγίξτε την οθόνη και μετακινήστε το δάχτυλό σας για υπογραφή + Υπογεγραμμένο + Ανυπόγραφο + Επιλεγμένο + Μη επιλεγμένο + Ρυθμιστικό απάντησης. Εύρος από %1$s έως %2$s + Εικόνα χωρίς ετικέτα + Έναρξη εργασίας + ενεργό + σωστό + λάθος + αδρανές + Πλακίδιο παιχνιδιού μνήμης + Προεπισκόπηση καταγραφής + Καταγραμμένη εικόνα + Προεπισκόπηση καταγραφής βίντεο + Καταγραμμένο βίντεο + Δεν είναι δυνατή η τοποθέτηση δίσκου μεγέθους %1$s πάνω σε δίσκο μεγέθους %2$s + Στόχος + Πύργος + Αγγίξτε δύο φορές για τοποθέτηση του δίσκου + Αγγίξτε δύο φορές για επιλογή του ανώτερου δίσκου + Έχει δίσκο με μεγέθη %1$s + Κενός + Εύρος από %1$s έως %2$s + Η στοίβα αποτελείται από + και + Σημείο: %1$d + + + \ No newline at end of file diff --git a/backbone/src/main/res/values-en-rAU/strings.xml b/backbone/src/main/res/values-en-rAU/strings.xml new file mode 100644 index 000000000..8d12ef4d8 --- /dev/null +++ b/backbone/src/main/res/values-en-rAU/strings.xml @@ -0,0 +1,397 @@ + + + + + Consent + First Name + Last Name + Required + Review + Review the form below and tap Agree if you\'re ready to continue. + Review + Signature + Please sign using your finger on the line below. + Sign Here + Page %1$ld of %2$ld + + Welcome + Data Gathering + Privacy + Data Use + Study Survey + Study Tasks + Time Commitment + Withdrawing + Learn More + + Learn more about how data is gathered + Learn more about how data is used + Learn more about how your privacy and identity are protected + Learn more about the study first + Learn more about the study survey + Learn more about the study\'s impact on your time + Learn more about the tasks involved + Learn more about withdrawing + + Sharing Options + Share my data with %1$s and qualified researchers worldwide + Only share my data with %1$s + %1$s will receive your study data from your participation in this study.\n\nSharing your coded study data more broadly (without information such as your name) may benefit this and future research. + Learn more about data sharing + + %1$s\'s Name (printed) + %1$s\'s Signature + Date + + Step %1$s of %2$s + + Invalid value + %1$s exceeds the maximum allowed value (%2$s). + %1$s is less than the minimum allowed value (%2$s). + %1$s is not a valid value. + + Invalid email address: %1$s + + Enter an address + Could not Find Specified Address + Unable to resolve your current location. Please type in an address or move to a location with better GPS signal if applicable. + Access to location services has been denied. Please grant this app permission to use location services through Settings. + Unable to find a result for the entered address. Please make sure the address is valid. + Either you are not connected to the Internet or you have exceeded the maximum amount of address lookup requests. If you are not connected to the Internet, please turn on your Wi-Fi to answer this question, skip this question if skip button is available, or come back to the survey when you are connected to the Internet. Otherwise, please try again in a few minutes. + + Text content exceeding maximum length: %1$s + + Camera not available in split screen. + + Email + jappleseed@example.com + Password + Enter password + Confirm + Enter password again + Passwords do not match. + Additional Information + John + Appleseed + Gender + Pick a gender + Birthdate + Pick a date + + Verification + Verify your Email + Tap on the link below if you did not receive a verification email and would like it sent again. + Resend Verification Email + + Login + Forgot password? + + Enter passcode + Confirm passcode + Passcode saved + Passcode authenticated + Enter your old passcode + Enter your new passcode + Confirm your new passcode + Passcode Incorrect + Passcodes did not match. Try again. + Please authenticate with Touch ID + Touch ID error + Only numeric characters allowed. + Passcode entry progress indicator + %1$s of %2$s digits entered + Forgot Passcode? + + Could not add Keychain item. + Could not update Keychain item. + Could not delete Keychain item. + Could not find Keychain item. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Female + Male + Other + + No + Yes + + cm + ft + in + + Tap to answer + Select an answer + Tap to select + Tap to write + + Agree + Cancel + OK + Clear + Disagree + Done + Get Started + Learn more + Next + Skip + Skip this question + Start Timer + Save for Later + Discard Results + End Task + Save + Clear answer + This answer cannot be modified. + + Starting activity in + Activity Complete + Your data will be analysed and you will be notified when your results are ready. + %1$s seconds remaining. + + Capture Image + Recapture Image + No camera found.\u0020\u0020This step cannot be completed. + In order to complete this step, allow this app access to the camera in Settings. + No output directory was specified for captured images. + The captured image could not be saved. + + Start Recording + Stop Recording + Recapture Video + + Fitness + Distance (%1$s) + Heart Rate (bpm) + Sit comfortably for %1$s. + Walk as fast as you can for %1$s. + This activity monitors your heart rate and measures how far you can walk in %1$s. + Walk outdoors at your fastest possible pace for %1$s. When you\'re done, sit and rest comfortably for %2$s. To begin, tap Get Started. + + Gait and Balance + This activity measures your gait and balance as you walk and stand still. Do not continue if you cannot safely walk unassisted. + Find a place where you can safely walk unassisted for about %1$d steps in a straight line. + Put your phone in a pocket or bag and follow the audio instructions. + Now stand still for %1$s. + Stand still for %1$s. + Turn around and walk back to where you started. + Walk up to %1$d steps in a straight line. + + Find a place where you can safely walk back and forth in a straight line. Try to walk continuously by turning at the ends of your path, as if you are walking around a cone.\n\nNext you will be instructed to turn around in a full circle, then stand still with your arms at your sides and your feet about shoulder-width apart. + Tap Get Started when you are ready to begin.\nThen place your phone in a pocket or bag and follow the audio instructions. + Walk back and forth in a straight line for %1$s. Walk as you would normally. + Turn in a full circle and then stand still for %1$s. + You have completed the activity. + + Tapping Speed + Right Hand + Left Hand + This activity measures your tapping speed. + Put your phone on a flat surface. + Use two fingers on the same hand to alternately tap the buttons on the screen. + Use two fingers on your right hand to alternately tap the buttons on the screen. + Use two fingers on your left hand to alternately tap the buttons on the screen. + Now repeat the same test using your right hand. + Now repeat the same test using your left hand. + Tap one finger, then the other. Try to time your taps to be as even as possible. Keep tapping for %1$s. + Tap Get Started to begin. + Tap Next to begin. + Tap + Total Taps + Tap the buttons as consistently as you can using two fingers. + Tap the buttons using your RIGHT hand. + Tap the buttons using your LEFT hand. + Skip this hand + + Voice + Tap Get Started to begin. + Say “Aaaaah” into the microphone for as long as you can. + Take a deep breath and say “Aaaaah” into the microphone for as long as you can. Keep a steady vocal volume so the audio bars remain blue. + This activity evaluates your voice by recording it with the microphone at the bottom of your phone. + Too Loud + Unable to record audio + Please wait while we check the background noise level. + The ambient noise level is too loud to record your voice. Please move somewhere quieter and try again. + Tap Next when ready. + + Tone Audiometry + This activity measures your ability to hear different sounds. + Before you begin, plug in and put your headphones on. + Tap Get Started to begin. + You should now hear a tone. Adjust the volume using the controls on the side of your device.\n\nTap the button when you are ready to begin. + Tap the button every time you start hearing a sound. + %1$s Hz, Left + %1$s Hz, Right + + Spatial Memory + This activity measures your short-term spatial memory by asking you to repeat the order in which %1$s light up. + flowers + flowers + Some of the %1$s will light up one at a time. Tap those %2$s in the same order they lit up. + Some of the %1$s will light up one at a time. Tap those %2$s in the reverse of the order they lit up. + To begin, tap Get Started, then watch closely. + %1$s + Score + Watch the %1$s light up + Tap the %1$s in the order they lit up + Tap the %1$s in reverse order + Sequence Complete + To continue, tap Next + Try Again + You didn\'t quite make it through that time. Tap Next to continue. + Time\'s Up + You ran out of time.\nTap Next to continue. + Game Complete + Paused + To continue, tap Next + + Reaction Time + This activity evaluates the time it takes for you to respond to a visual cue. + Shake the device in any direction as soon as the blue dot appears on screen. You will be asked to do this %D times. + Tap Get Started to begin. + Attempt %1$s of %2$s + Quickly shake the device when the blue circle appears + + Tower of Hanoi + This activity evaluates your puzzle solving skills. + Move the entire stack to the highlighted platform in as few moves as possible. + Tap Get Started to begin + Solve the Puzzle + Number of Moves: %1$s \n %2$s + I cannot solve this puzzle + + Timed Walk + This activity measures your lower extremity function. + Find a place, preferably outside, where you can walk for about %1$s in a straight line as quickly as possible, but safely. Do not slow down until after you\'ve passed the finish line. + Tap Next to begin. + Assistive device + Use the same assistive device for each test. + Do you wear an ankle foot orthosis? + Do you use assistive device? + Tap here to select an answer. + None + Unilateral Cane + Unilateral Crutch + Bilateral Cane + Bilateral Crutch + Walker/Rollator + Walk up to %1$s in a straight line. + Turn around. + Walk back to where you started. + Tap Done when complete. + + PASAT + PVSAT + PAVSAT + The Paced Auditory Serial Addition Test measures your auditory information processing speed and calculation ability. + The Paced Visual Serial Addition Test measures your visual information processing speed and calculation ability. + The Paced Auditory and Visual Serial Addition Test measures your auditory and visual information processing speed and calculation ability. + Single digits are presented every %1$s seconds.\nYou must add each new digit to the one immediately prior to it.\nAttention, you mustn\'t calculate a running total, but only the sum of the last two numbers. + Tap Get Started to begin. + Remember this first digit. + Add this new digit to the previous one. + - + + %1$s-Hole Peg Test + This activity measures your upper extremity function by asking you to place a peg in a hole. You will be asked to do this %1$s times. + Both your left and right hands will be tested.\nYou must pick up the peg as quickly as possible, put it in the hole and, once done %1$s times, remove it again %2$s times. + Both your right and left hands will be tested.\nYou must pick up the peg as quickly as possible, put it in the hole and, once done %1$s times, remove it again %2$s times. + Tap Get Started to begin. + Put the peg in the hole using your left hand. + Put the peg in the hole using your right hand. + Put the peg behind the line using your left hand. + Put the peg behind the line using your right hand. + Pick up the peg using two fingers. + Lift fingers to drop the peg. + + Tremor Activity + This activity measures the tremor of your hands in various positions. Find a place where you can sit comfortably for the duration of this activity. + Hold the phone in your more affected hand as shown in the image below. + Hold the phone in your RIGHT hand as shown in the image below. + Hold the phone in your LEFT hand as shown in the image below. + You will be asked to perform %1$s while sitting with the phone in your hand. + a task + two tasks + three tasks + four tasks + five tasks + Tap next to proceed. + Prepare to hold your phone in your lap. + Prepare to hold your phone in your lap with your LEFT hand. + Prepare to hold your phone in your lap with your RIGHT hand. + Keep holding your phone in your lap for %1$d seconds. + Now hold your phone with your hand extended out at shoulder height. + Now hold your phone with your LEFT hand extended out at shoulder height. + Now hold your phone with your RIGHT hand extended out at shoulder height. + Keep holding your phone with your hand extended for %1$d seconds. + Now hold your phone at shoulder height with your elbow bent. + Now hold your phone with your LEFT hand at shoulder height with your elbow bent. + Now hold your phone with your RIGHT hand at shoulder height with your elbow bent. + Keep holding your phone with your elbow bent for %1$d seconds + Now keeping your elbow bent, touch your phone to your nose repeatedly. + Now keeping your elbow bent with your phone in your LEFT hand, touch your phone to your nose repeatedly. + Now keeping your elbow bent with your phone in your RIGHT hand, touch your phone to your nose repeatedly. + Keep touching your phone to your nose for %1$d seconds + Prepare to do a queen wave (wave by turning your wrist). + Prepare to do a queen wave with your phone in your LEFT hand (wave by turning your wrist). + Prepare to do a queen wave with your phone in your RIGHT hand (wave by turning your wrist). + Keep performing a queen wave for %1$d seconds. + Now switch the phone to your LEFT hand and continue to the next task. + Now switch the phone to your RIGHT hand and continue to the next task. + Continue to the next task. + Activity completed. + You will be asked to perform %1$s while sitting with the phone first in one hand, then again with the other hand. + I cannot perform this activity with my LEFT hand. + I cannot perform this activity with my RIGHT hand. + I can perform this activity with both hands. + + Could not create file + Could not remove sufficient log files to reach threshold + Error setting attribute + File not marked deleted (not marked uploaded) + Multiple errors removing logs + No collected data was found. + No output directory specified + + No Data + + Back + Illustration of %1$s + Designated signature field + Touch the screen and move your finger to sign + Signed + Unsigned + Selected + Un-selected + Response slider. Range from %1$s to %2$s + Unlabelled image + Begin task + active + correct + incorrect + quiescent + Memory game tile + Capture preview + Captured image + Video capture preview + Captured video + Unable to place disk with size %1$s on disk with size %2$s + Target + Tower + Double-tap to place disk + Double-tap to select top-most disk + Has disk with sizes %1$s + Empty + Range from %1$s to %2$s + Stack composed of + and + Point: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-en-rGB/strings.xml b/backbone/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 000000000..cdc8e3763 --- /dev/null +++ b/backbone/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,397 @@ + + + + + Consent + First Name + Last Name + Required + Review + Review the form below, and tap Agree if you\'re ready to continue. + Review + Signature + Please sign using your finger on the line below. + Sign Here + Page %1$ld of %2$ld + + Welcome + Data Gathering + Privacy + Data Use + Study Survey + Study Tasks + Time Commitment + Withdrawing + Learn More + + Learn more about how data is gathered + Learn more about how data is used + Learn more about how your privacy and identity are protected + Learn more about the study first + Learn more about the study survey + Learn more about the study\'s impact on your time + Learn more about the tasks involved + Learn more about withdrawing + + Sharing Options + Share my data with %1$s and qualified researchers worldwide + Only share my data with %1$s + %1$s will receive your study data from your participation in this study.\n\nSharing your coded study data more broadly (without information such as your name) may benefit this and future research. + Learn more about data sharing + + %1$s\'s Name (printed) + %1$s\'s Signature + Date + + Step %1$s of %2$s + + Invalid value + %1$s exceeds the maximum allowed value (%2$s). + %1$s is less than the minimum allowed value (%2$s). + %1$s is not a valid value. + + Invalid email address: %1$s + + Enter an address + Could Not Find Specified Address + Unable to resolve your current location. Please type in an address or move to a location with better GPS signal if applicable. + Access to Location Services has been denied. Please grant this app permission to use Location Services through Settings. + Unable to find a result for the entered address. Please make sure the address is valid. + Either you are not connected to the Internet or you have exceeded the maximum amount of address lookup requests. If you are not connected to the Internet, please turn on your Wi-Fi to answer this question, skip this question if the skip button is available, or come back to the survey when you are connected to the Internet. Otherwise, please try again in a few minutes. + + Text content exceeding maximum length: %1$s + + Camera not available in split screen. + + Email + jappleseed@example.com + Password + Enter password + Confirm + Enter password again + Passwords do not match. + Additional Information + John + Appleseed + Gender + Pick a gender + Date of Birth + Pick a date + + Verification + Verify your Email + Tap the link below if you did not receive a verification email and would like it sent again. + Resend Verification Email + + Login + Forgot password? + + Enter passcode + Confirm passcode + Passcode saved + Passcode authenticated + Enter your old passcode + Enter your new passcode + Confirm your new passcode + Passcode Incorrect + Passcodes did not match. Try again. + Please authenticate with Touch ID + Touch ID error + Only numeric characters allowed. + Passcode entry progress indicator + %1$s of %2$s digits entered + Forgot Passcode? + + Could not add Keychain item. + Could not update Keychain item. + Could not delete Keychain item. + Could not find Keychain item. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Female + Male + Other + + No + Yes + + cm + ft + in + + Tap to answer + Select an answer + Tap to select + Tap to write + + Agree + Cancel + OK + Clear + Disagree + Done + Get Started + Learn more + Next + Skip + Skip this question + Start Timer + Save for Later + Discard Results + End Task + Save + Clear answer + This answer cannot be modified. + + Starting activity in + Activity Complete + Your data will be analysed and you will be notified when your results are ready. + %1$s seconds remaining. + + Capture Image + Recapture Image + No camera found.\u0020\u0020This step cannot be completed. + In order to complete this step, allow this app access to the camera in Settings. + No output directory was specified for captured images. + The captured image could not be saved. + + Start Recording + Stop Recording + Recapture Video + + Fitness + Distance (%1$s) + Heart Rate (bpm) + Sit comfortably for %1$s. + Walk as fast as you can for %1$s. + This activity monitors your heart rate and measures how far you can walk in %1$s. + Walk outdoors at your fastest possible pace for %1$s. When you\'re done, sit and rest comfortably for %2$s. To begin, tap Get Started. + + Gait and Balance + This activity measures your gait and balance as you walk and stand still. Do not continue if you cannot safely walk unassisted. + Find a place where you can safely walk unassisted for about %1$d steps in a straight line. + Put your phone in a pocket or bag and follow the audio instructions. + Now stand still for %1$s. + Stand still for %1$s. + Turn round and walk back to where you started. + Walk up to %1$d steps in a straight line. + + Find a place where you can safely walk back and forth in a straight line. Try to walk continuously by turning at the ends of your path, as if you are walking around a cone.\n\nNext you will be instructed to turn around in a full circle, then stand still with your arms at your sides and your feet about shoulder-width apart. + Tap Get Started when you are ready to begin.\nThen place your phone in a pocket or bag and follow the audio instructions. + Walk back and forth in a straight line for %1$s. Walk as you would normally. + Turn in a full circle and then stand still for %1$s. + You have completed the activity. + + Tapping Speed + Right Hand + Left Hand + This activity measures your tapping speed. + Put your phone on a flat surface. + Use two fingers on the same hand to alternately tap the buttons on the screen. + Use two fingers on your right hand to alternately tap the buttons on the screen. + Use two fingers on your left hand to alternately tap the buttons on the screen. + Now repeat the same test using your right hand. + Now repeat the same test using your left hand. + Tap one finger, then the other. Try to time your taps to be as even as possible. Keep tapping for %1$s. + Tap Get Started to begin. + Tap Next to begin. + Tap + Total Taps + Tap the buttons as consistently as you can using two fingers. + Tap the buttons using your RIGHT hand. + Tap the buttons using your LEFT hand. + Skip this hand + + Voice + Tap Get Started to begin. + Say “Aaaaah” into the microphone for as long as you can. + Take a deep breath and say “Aaaaah” into the microphone for as long as you can. Keep a steady vocal volume so the audio bars remain blue. + This activity evaluates your voice by recording it with the microphone at the bottom of your phone. + Too Loud + Unable to record audio + Please wait while we check the background noise level. + The ambient noise level is too loud to record your voice. Please move somewhere quieter and try again. + Tap Next when ready. + + Tone Audiometry + This activity measures your ability to hear different sounds. + Before you begin, plug in and put your headphones on. + Tap Get Started to begin. + You should now hear a tone. Adjust the volume using the controls on the side of your device.\n\nTap the button when you are ready to begin. + Tap the button every time you start hearing a sound. + %1$s Hz, Left + %1$s Hz, Right + + Spatial Memory + This activity measures your short-term spatial memory by asking you to repeat the order in which %1$s light up. + flowers + flowers + Some of the %1$s will light up one at a time. Tap those %2$s in the same order they lit up. + Some of the %1$s will light up one at a time. Tap those %2$s in the reverse of the order they lit up. + To begin, tap Get Started, then watch closely. + %1$s + Score + Watch the %1$s light up + Tap the %1$s in the order they lit up + Tap the %1$s in reverse order + Sequence Complete + To continue, tap Next + Try Again + You didn\'t quite make it through that time. Tap Next to continue. + Time\'s Up + You ran out of time.\nTap Next to continue. + Game Complete + Paused + To continue, tap Next + + Reaction Time + This activity evaluates the time it takes for you to respond to a visual cue. + Shake the device in any direction as soon as the blue dot appears on screen. You will be asked to do this %D times. + Tap Get Started to begin. + Attempt %1$s of %2$s + Quickly shake the device when the blue circle appears + + Tower of Hanoi + This activity evaluates your puzzle-solving skills. + Move the entire stack to the highlighted platform in as few moves as possible. + Tap Get Started to begin + Solve the Puzzle + Number of Moves: %1$s \n %2$s + I cannot solve this puzzle + + Timed Walk + This activity measures your lower extremity function. + Find a place, preferably outside, where you can walk for about %1$s in a straight line as quickly as possible, but safely. Do not slow down until after you\'ve passed the finish line. + Tap Next to begin. + Assistive device + Use the same assistive device for each test. + Do you wear an ankle-foot orthosis? + Do you use assistive device? + Tap here to select an answer. + None + Unilateral Cane + Unilateral Crutch + Bilateral Cane + Bilateral Crutch + Zimmer/Rollator + Walk up to %1$s in a straight line. + Turn round. + Walk back to where you started. + Tap Done when complete. + + PASAT + PVSAT + PAVSAT + The Paced Auditory Serial Addition Test measures your auditory information processing speed and calculation ability. + The Paced Visual Serial Addition Test measures your visual information processing speed and calculation ability. + The Paced Auditory and Visual Serial Addition Test measures your auditory and visual information processing speed and calculation ability. + Single digits are presented every %1$s seconds.\nYou must add each new digit to the one immediately before it.\nNote: you must not calculate a running total, only the sum of the last two numbers. + Tap Get Started to begin. + Remember this first digit. + Add this new digit to the previous one. + - + + %1$s-Hole Peg Test + This activity measures your upper extremity function by asking you to place a peg in a hole. You will be asked to do this %1$s times. + Both your left and right hands will be tested.\nYou must pick up the peg as quickly as possible, put it in the hole and, once done %1$s times, remove it again %2$s times. + Both your right and left hands will be tested.\nYou must pick up the peg as quickly as possible, put it in the hole and, once done %1$s times, remove it again %2$s times. + Tap Get Started to begin. + Put the peg in the hole using your left hand. + Put the peg in the hole using your right hand. + Put the peg behind the line using your left hand. + Put the peg behind the line using your right hand. + Pick up the peg using two fingers. + Lift fingers to drop the peg. + + Tremor Activity + This activity measures the tremor of your hands in various positions. Find a place where you can sit comfortably for the duration of this activity. + Hold the phone in your more affected hand as shown in the image below. + Hold the phone in your RIGHT hand as shown in the image below. + Hold the phone in your LEFT hand as shown in the image below. + You will be asked to perform %1$s while sitting with the phone in your hand. + a task + two tasks + three tasks + four tasks + five tasks + Tap next to proceed. + Prepare to hold your phone in your lap. + Prepare to hold your phone in your lap with your LEFT hand. + Prepare to hold your phone in your lap with your RIGHT hand. + Keep holding your phone in your lap for %1$d seconds. + Now hold your phone with your hand extended out at shoulder height. + Now hold your phone with your LEFT hand extended out at shoulder height. + Now hold your phone with your RIGHT hand extended out at shoulder height. + Keep holding your phone with your hand extended for %1$d seconds. + Now hold your phone at shoulder height with your elbow bent. + Now hold your phone with your LEFT hand at shoulder height with your elbow bent. + Now hold your phone with your RIGHT hand at shoulder height with your elbow bent. + Keep holding your phone with your elbow bent for %1$d seconds + Now keeping your elbow bent, touch your phone to your nose repeatedly. + Now keeping your elbow bent with your phone in your LEFT hand, touch your phone to your nose repeatedly. + Now keeping your elbow bent with your phone in your RIGHT hand, touch your phone to your nose repeatedly. + Keep touching your phone to your nose for %1$d seconds + Prepare to do a queen wave (wave by turning your wrist). + Prepare to do a queen wave with your phone in your LEFT hand (wave by turning your wrist). + Prepare to do a queen wave with your phone in your RIGHT hand (wave by turning your wrist). + Keep performing a queen wave for %1$d seconds. + Now switch the phone to your LEFT hand and continue to the next task. + Now switch the phone to your RIGHT hand and continue to the next task. + Continue to the next task. + Activity completed. + You will be asked to perform %1$s while sitting with the phone first in one hand, then again with the other hand. + I cannot perform this activity with my LEFT hand. + I cannot perform this activity with my RIGHT hand. + I can perform this activity with both hands. + + Could not create file + Could not remove sufficient log files to reach threshold + Error setting attribute + File not marked deleted (not marked uploaded) + Multiple errors removing logs + No collected data was found. + No output directory specified + + No Data + + Back + Illustration of %1$s + Designated signature field + Touch the screen and move your finger to sign + Signed + Unsigned + Selected + Unselected + Response slider. Range from %1$s to %2$s + Unlabelled image + Begin task + active + correct + incorrect + quiescent + Memory game tile + Capture preview + Captured image + Video capture preview + Captured video + Unable to place disk with size %1$s on disk with size %2$s + Target + Tower + Double-tap to place disk + Double-tap to select top-most disk + Has disk with sizes %1$s + Empty + Range from %1$s to %2$s + Stack composed of + and + Point: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-es-rMX/strings.xml b/backbone/src/main/res/values-es-rMX/strings.xml new file mode 100644 index 000000000..fd5306814 --- /dev/null +++ b/backbone/src/main/res/values-es-rMX/strings.xml @@ -0,0 +1,396 @@ + + + + + Consentimiento + Nombre + Apellidos + Campo obligatorio + Revisar + Revisa la siguiente forma y toca Acepto para continuar. + Revisar + Firma + Firma con tu dedo en la siguiente línea. + Firma aquí + Página %1$ld de %2$ld + + Bienvenido + Recopilación de datos + Privacidad + Uso de datos + Encuesta del estudio + Tareas del estudio + Compromiso de tiempo + Darse de baja + Más información + + Más información sobre cómo se recopilan los datos + Más información sobre cómo se usan los datos + Más información sobre cómo se protege tu privacidad e identidad + Obtén primero más información sobre el estudio + Más información sobre la encuesta del estudio + Más información sobre el impacto del estudio en tu tiempo + Más información sobre las tareas relacionadas + Más información + + Opciones al compartir + Compartir mis datos con %1$s y otros investigadores calificados a nivel mundial + Sólo compartir mis datos con %1$s + %1$s recibirá tus datos de participación en el estudio.\n\nCompartir más abiertamente los datos cifrados del estudio (sin que se divulgue información como tu nombre) podría beneficiar esta y futuras investigaciones. + Más información sobre compartir datos + + Nombre de %1$s (escrito) + Firma de %1$s + Fecha + + Paso %1$s de %2$s + + Valor no válido + %1$s excede el número máximo permitido (%2$s). + %1$s es menor al número mínimo permitido (%2$s). + %1$s no es un valor válido. + + Correo electrónico no válido: %1$s + + Ingresa una dirección + No se encontró la dirección especificada + No se pudo determinar tu ubicación actual. Ingresa una dirección o ve a una ubicación con mejor recepción de señal GPS, si aplica. + Se rechazó el acceso a los servicios de localización. Otorga a esta app permiso para usar los servicios de localización desde Configuración. + No fue posible encontrar coincidencias con la dirección ingresada. Asegúrate de que la dirección es válida. + No estás conectado a Internet o excediste la cantidad máxima de solicitudes de búsqueda de direcciones. Si no estás conectado a Internet, activa tu red Wi-Fi para contestar esta pregunta. También puedes omitirla (si el botón de omitir está disponible) o regresar a la encuesta cuando estés conectado a Internet. De lo contrario, vuelve a intentar en unos minutos. + + El texto sobrepasa la longitud máxima: %1$s + + No se puede usar la cámara con la pantalla dividida. + + Correo + juanlopez@example.com + Contraseña + Ingresar contraseña + Confirmar + Vuelve a ingresar la contraseña + Las contraseñas no coinciden. + Información adicional + Juan + López + Sexo + Elige un sexo + Fecha de nacimiento + Elige una fecha + + Verificación + Verifica tu correo electrónico + Toca el siguiente enlace si no recibiste un correo de verificación y quieres que se vuelva a enviar. + Reenviar correo de verificación + + Inicio de sesión + ¿Olvidaste tu contraseña? + + Ingresa el código + Confirmar código + Código guardado + Código autenticado + Ingresa el código anterior + Ingresa el nuevo código + Confirma el nuevo código + Código incorrecto + Los códigos no coinciden. Intenta de nuevo. + Autentica con Touch ID + Error de Touch ID + Solo se permiten caracteres numéricos. + Indicador del progreso de ingreso de código + %1$s de %2$s dígitos ingresados + ¿Olvidaste el código? + + No se pudo agregar el elemento de Llavero. + No se pudo actualizar el elemento de Llavero. + No se pudo eliminar el elemento de Llavero. + No se pudo encontrar el elemento de Llavero. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Mujer + Hombre + Otro + + No + + + cm + pie(s) + pulg. + + Toca para contestar + Seleccionar una respuesta + Toca para seleccionar + Toca para escribir + + Acepto + Cancelar + OK + Borrar + No acepto + Aceptar + Empezar + Más información + Siguiente + Omitir + Omitir esta pregunta + Activar temporizador + Guardar para después + Descartar resultados + Finalizar tarea + Guardar + Borrar respuesta + No se puede modificar esta respuesta. + + Comenzar la actividad en + Actividad completada + Se analizarán tus datos y se te avisará cuando tus resultados estén listos. + Quedan %1$s segundos. + + Capturar imagen + Recapturar imagen + No se encontraron cámaras. Este paso no se puede finalizar. + Para finalizar este paso, permite el acceso de la app a la cámara en Configuración. + No se especificó un directorio de salida para las imágenes capturadas. + No se pudo guardar la imagen capturada. + + Iniciar grabación + Detener grabación + Recapturar video + + Condición + Distancia (%1$s) + Frecuencia cardiaca (lpm) + Siéntate cómodamente durante %1$s. + Camina tan rápido como sea posible durante %1$s. + Esta actividad revisa tu frecuencia cardiaca y mide cuánto puedes caminar en el transcurso de %1$s. + Camina afuera tan rápido como puedas durante %1$s. Al terminar, siéntate y descansa cómodamente durante %2$s. Toca Empezar para comenzar. + + Pasos y equilibrio + Esta actividad mide tus pasos así como tu equilibrio al caminar y al permanecer de pie. No continúes si no puedes caminar sin ayuda. + Encuentra un lugar donde puedas dar %1$d pasos en línea recta sin ayuda. + Coloca el teléfono en un bolsillo o bolsa y sigue las instrucciones de audio. + Ahora, no te muevas durante %1$s. + No te muevas durante %1$s. + Da vuelta y regresa al punto de inicio. + Da hasta %1$d pasos en línea recta. + + Busca un lugar en donde puedas caminar de forma segura de un lado a otro en una línea recta. Intenta caminar sin detenerte al final, como si giraras alrededor de un cono.\n\nDespués se te pedirá que hagas una vuelta completa sobre tu eje y que te quedes quieto con los brazos a los lados y los pies separados y alineados con tus hombros. + Toca Empezar cuando estés listo.\nLuego coloca el teléfono en un bolsillo o bolsa y sigue las instrucciones de audio. + Camina de un lado a otro en una línea recta por %1$s. Camina como lo harías normalmente. + Da una vuelta completa sobre tu eje y quédate quieto por %1$s. + Completaste la actividad. + + Velocidad al tocar + Mano derecha + Mano izquierda + Esta actividad mide tu velocidad al tocar. + Coloca tu teléfono en una superficie plana. + Usa dos dedos de la misma mano para tocar alternativamente los botones de la pantalla. + Usa dos dedos de la mano derecha para tocar alternativamente los botones de la pantalla. + Usa dos dedos de la mano izquierda para tocar alternativamente los botones de la pantalla. + Ahora repite la prueba con la mano derecha. + Ahora repite la prueba con la mano izquierda. + Toca con un dedo y luego con el otro. Intenta calcular el tiempo entre cada toque para que sea lo más constante posible. Sigue tocando por %1$s. + Toca Empezar para comenzar. + Toca Siguiente para comenzar. + Tocar + Toques en total + Toca los botones lo más constante que puedas usando dos dedos. + Toca los botones usando tu mano derecha. + Toca los botones usando tu mano izquierda. + Omitir esta mano + + Voz + Toca Empezar para comenzar. + Di “Aaah” en el micrófono durante todo tiempo que puedas. + Inhala profundamente y di “Aaah” en el micrófono durante todo tiempo que puedas. Mantén la misma intensidad de volumen de manera que las barras de audio permanezcan azules. + Esta actividad evalúa tu voz grabándola con el micrófono en la parte inferior de tu teléfono. + Demasiado fuerte + No se pudo grabar audio + Espera mientras revisamos el nivel de ruido de fondo. + Hay mucho ruido ambiental como para grabar tu voz. Ve a un lugar más tranquilo y reintenta. + Toca Siguiente cuando estés listo. + + Audiometría tonal + Esta actividad mide tu capacidad para escuchar sonidos distintos. + Antes de comenzar, conecta y ponte tus audífonos. + Toca Empezar para comenzar. + Debes escuchar un tono ahora. Ajusta el volumen con los controles laterales de tu dispositivo.\n\nToca el botón cuando quieras comenzar. + Toca el botón en cuanto escuches un sonido. + %1$s Hz, izquierda + %1$s HZ, derecha + + Memoria espacial + Esta actividad mide tu memoria espacial a corto plazo al pedirte repetir el orden en que se encienden las imágenes de %1$s. + flores + flores + Algunas imágenes de %1$s se encenderán una a la vez. Toca las imágenes de %2$s en el mismo orden en que se encendieron. + Algunas imágenes de %1$s se encenderán una a la vez. Toca las imágenes de %2$s en orden inverso al que se encendieron. + Para comenzar, toca Empezar y luego observa cuidadosamente. + %1$s + Puntuación + Observa cuando se encienda las imágenes de %1$s + Toca las imágenes de %1$s según se encienden + Toca las imágenes de %1$s en orden contrario + Secuencia completada + Para continuar, toca Siguiente + Reintentar + No terminaste la ronda a tiempo. Toca Siguiente para continuar. + Se acabó el tiempo + Se acabó el tiempo.\nToca Siguiente para continuar. + Juego completado + En pausa + Para continuar, toca Siguiente + + Tiempo de reacción + Esta actividad evalúa el tiempo que te toma responder a una indicación visual. + Sacude el dispositivo en cualquier dirección en cuanto el punto azul se muestre en la pantalla. Se te pedirá esto %D veces. + Toca Empezar para comenzar. + Intento %1$s de %2$s + Sacude rápidamente el dispositivo cuando se muestre el círculo azul + + Torre de Hanói + Esta actividad evalúa tu capacidad para resolver acertijos. + Mueve toda la pila a la plataforma iluminada usando un número mínimo de movimientos. + Toca Empezar para comenzar + Resuelve el acertijo + Número de movimientos: %1$s \n %2$s + No puedo resolver este acertijo + + Caminata cronometrada + Esta actividad mide la función de tus extremidades inferiores. + Encuentra un lugar, de preferencia al aire libre, donde puedas caminar alrededor de %1$s en línea recta tan rápido como sea posible pero de forma segura. No te detengas sino hasta cruzar la meta. + Toca Siguiente para comenzar. + Dispositivo de ayuda + Usa el mismo dispositivo de ayuda para cada prueba. + ¿Usas una órtesis para pie y tobillo? + ¿Usas un dispositivo de ayuda? + Toca aquí para seleccionar una respuesta. + Ninguno + Bastón unilateral + Muleta unilateral + Bastón bilateral + Muleta bilateral + Andador (con o sin ruedas) + Camina hasta %1$s en línea recta. + Da vuelta y regresa al punto de inicio. + Toca OK al terminar. + + PASAT + PVSAT + PAVSAT + La Prueba de la adición auditiva consecutiva ritmada (PASAT) mide tu capacidad de cálculo y velocidad de procesamiento de información auditiva. + La Prueba de la adición visual consecutiva ritmada (PVSAT) mide tu capacidad de cálculo y velocidad de procesamiento de información visual. + La Prueba de la adición audiovisual consecutiva ritmada (PAVSAT) mide tu capacidad de cálculo y velocidad de procesamiento de información auditiva y visual. + Los dígitos únicos se presentan cada %1$s segundos.\nDebes sumar cada dígito nuevo al que lo precede inmediatamente.\nNota: No debes calcular un total acumulado; sólo la suma de los últimos dos números. + Toca Empezar para comenzar. + Recuerda este primer dígito. + Agregar este nuevo dígito al anterior. + - + + Prueba de las %1$s estacas + Esta actividad mide la funcionalidad de tus extremidades superiores pidiéndote que coloques una estaca en un hoyo. Se te pedirá que hagas esto %1$s veces. + Se probarán tanto tu mano izquierda como la derecha.\nDebes recoger la estaca tan rápido como te sea posible y ponerla en el hoyo; y una vez que hayas hecho eso %1$s veces, debes quitarla otras %2$s veces. + Se probarán tanto tu mano derecha como la izquierda.\nDebes recoger la estaca tan rápido como te sea posible y ponerla en el hoyo; y una vez que hayas hecho eso %1$s veces, debes quitarla otras %2$s veces. + Toca Empezar para comenzar. + Coloca la estaca en el hoyo usando tu mano izquierda. + Coloca la estaca en el hoyo usando tu mano derecha. + Coloca la estaca detrás de la línea usando tu mano izquierda. + Coloca la estaca detrás de la línea usando tu mano derecha. + Recoge la estaca con dos dedos. + Levanta los dedos para soltar la estaca. + + Actividad para temblores + Esta actividad mide cuánto tiemblan tus manos en varias posiciones. Busca un lugar en donde te puedas sentar cómodamente durante esta actividad. + Agarra el teléfono con tu mano más afectada como se muestra en la imagen. + Agarra el teléfono con tu mano derecha como se muestra en la imagen. + Agarra el teléfono con tu mano izquierda como se muestra en la imagen. + Se te pedirá que realices %1$s mientras estás sentado con el teléfono en la mano. + una tarea + dos tareas + tres tareas + cuatro tareas + cinco tareas + Toca Siguiente para continuar. + Prepárate para sostener el teléfono sobre tu pierna. + Prepárate para sostener el teléfono sobre tu pierna con tu mano izquierda. + Prepárate para sostener el teléfono sobre tu pierna con tu mano derecha. + Sigue sosteniendo el teléfono sobre tu pierna por %1$d segundos. + Ahora agarra el teléfono con la mano extendida a la altura de los hombros. + Ahora agarra el teléfono con tu mano izquierda extendida a la altura de los hombros. + Ahora agarra el teléfono con tu mano derecha extendida a la altura de los hombros. + Sigue sosteniendo el teléfono con tu mano extendida por %1$d segundos. + Ahora agarra el teléfono a la altura de los hombros con el codo doblado. + Ahora agarra el teléfono con tu mano izquierda a la altura de los hombros con el codo doblado. + Ahora agarra el teléfono con tu mano derecha a la altura de los hombros con el codo doblado. + Sigue sosteniendo el teléfono con tu codo doblado por %1$d segundos + Ahora, aún con el codo doblado, acerca y toca el teléfono con tu nariz varias veces. + Ahora, aún con el codo doblado y sosteniendo el teléfono con tu mano izquierda, acerca y toca el teléfono con tu nariz varias veces. + Ahora, aún con el codo doblado y sosteniendo el teléfono con tu mano derecha, acerca y toca el teléfono con tu nariz varias veces. + Sigue acercando y tocando el teléfono con tu nariz por %1$d segundos + Prepárate para hacer un gesto de saludo (girando la muñeca). + Prepárate para hacer un gesto de saludo (girando la muñeca) con tu teléfono en la mano izquierda. + Prepárate para hacer un gesto de saludo (girando la muñeca) con tu teléfono en la mano derecha. + Sigue haciendo un gesto de saludo por %1$d segundos. + Ahora cambia el teléfono a tu mano izquierda y pasa a la siguiente tarea. + Ahora cambia el teléfono a tu mano derecha y pasa a la siguiente tarea. + Pasa a la siguiente tarea. + Actividad completada. + Se te pedirá que realices %1$s mientras estás sentado, primero con el teléfono en una mano y luego en la otra. + No puedo realizar esta actividad con mi mano izquierda. + No puedo realizar esta actividad con mi mano derecha. + Puedo realizar esta actividad con las dos manos. + + No se pudo crear el archivo + No se pudo eliminar suficientes registros para alcanzar el umbral + Error al definir el atributo + El archivo no se marcó como eliminado (no se marcó como cargado) + Varios errores al eliminar registros + Datos recopilados no encontrados. + Directorio de salida no especificado + + Sin datos + + Atrás + Ilustración de %1$s + Campo para firma + Usa tu dedo para firmar la pantalla + Firmado + Sin firmar + Elemento seleccionado + No seleccionado + Regulador de respuesta. Rango de %1$s a %2$s + Imagen sin etiqueta + Comenzar tarea + activa + correcto + incorrecto + sin actividad + Cuadro del juego de memoria + Previsualización de captura + Imagen capturada + Previsualización de captura de video + Vídeo capturado + No se puede colocar un disco de tamaño %1$s sobre otro de tamaño %2$s + Destino + Torre + Toca dos veces para colocar el disco + Toca dos veces para seleccionar el disco de hasta arriba. + Tiene discos de tamaño %1$s + Vacía + Rango de %1$s a %2$s + Pila compuesta de + y + Punto: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-es/strings.xml b/backbone/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..7c5d9f857 --- /dev/null +++ b/backbone/src/main/res/values-es/strings.xml @@ -0,0 +1,396 @@ + + + + + Consentimiento + Nombre + Apellidos + Obligatorio + Revisar + Revisa el siguiente formulario y pulsa Acepto si estás listo para continuar. + Revisar + Firma + Firma con el dedo sobre la línea de abajo. + Firma aquí + Página %1$ld de %2$ld + + Bienvenido + Recopilación de datos + Privacidad + Utilización de datos + Evaluación del estudio + Tareas del estudio + Tiempo dedicado + Salida del estudio + Más información + + Más información sobre la recopilación de los datos + Más información sobre la utilización de los datos + Más información sobre la protección de la privacidad y la identidad + Más información sobre el estudio + Más información sobre la evaluación del estudio + Más información sobre el impacto del estudio en tu tiempo + Más información sobre las tareas del estudio + Más información sobre cómo salir del estudio + + Opciones para compartir + Compartir mis datos con %1$s y con investigadores cualificados de todo el mundo + Solo compartir mis datos con %1$s + %1$s recibirá los datos obtenidos de tu participación en este estudio.\n\nSi compartes los datos codificados del estudio de forma más amplia (sin incluir información personal como tu nombre), beneficiarás tanto esta investigación como las que se realicen en el futuro. + Más información sobre cómo se comparten los datos + + Nombre del %1$s (mayúsculas) + Firma del %1$s + Fecha + + Paso %1$s de %2$s + + Valor no válido + %1$s es superior al valor máximo permitido (%2$s). + %1$s es inferior al valor mínimo permitido (%2$s). + %1$s no es un valor válido. + + Dirección de correo electrónico no válida: %1$s + + Introduce una dirección + No se ha encontrado la dirección especificada + No se ha podido determinar tu ubicación actual. Escribe una dirección o dirígete a una ubicación donde la señal GPS sea mejor. + Se ha denegado el acceso a los servicios de localización. Ve a Ajustes para conceder a esta aplicación permiso para utilizar los servicios de localización. + No se ha encontrado ningún resultado para la dirección introducida. Asegúrate de que la dirección es válida. + O bien no estás conectado a Internet o bien has excedido el número máximo de búsquedas de direcciones. Si no estás conectado a Internet, activa la conexión Wi-Fi para responder a esta pregunta, omítela si el botón de omisión está disponible, o vuelve al estudio cuando dispongas de conexión a Internet. Si no es posible, inténtalo de nuevo dentro de unos minutos. + + El texto supera la longitud máxima: %1$s + + Cámara no disponible en pantalla dividida. + + Correo electrónico + juan@example.com + Contraseña + Introduce la contraseña + Confirmar + Introduce de nuevo la contraseña + Las contraseñas no coinciden. + Información adicional + Juan + López + Sexo + Selecciona tu sexo + Fecha de nacimiento + Selecciona una fecha + + Verificación + Verifica tu correo electrónico + Pulsa en el enlace siguiente si no has recibido un correo de verificación y quieres que se te vuelva a enviar. + Reenviar correo de verificación + + Iniciar sesión + ¿Has olvidado tu contraseña? + + Introduce el código + Confirmar código + Código guardado + Autenticado por código + Introduce el código antiguo + Introduce el código nuevo + Confirma el nuevo código + Código incorrecto + Los códigos no coinciden. Inténtalo de nuevo. + Autentícate con Touch ID + Error de Touch ID + Solo se admiten caracteres numéricos. + Indicador de progreso de introducción de código + %1$s de %2$s dígitos introducidos + ¿Has olvidado el código? + + No se ha podido añadir el ítem del llavero. + No se ha podido actualizar el ítem del llavero. + No se ha podido eliminar el ítem del llavero. + No se ha podido encontrar el ítem del llavero. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Mujer + Hombre + Otro + + No + + + cm + ft + in + + Pulsa para contestar + Selecciona una respuesta + Pulsa para seleccionar + Pulsa para escribir + + Acepto + Cancelar + Aceptar + Borrar + No acepto + OK + Empezar + Más información + Siguiente + Omitir + Omitir esta pregunta + Activar temporizador + Guardar para luego + Descartar resultados + Finalizar tarea + Guardar + Borrar respuesta + Esta respuesta no puede modificarse. + + Iniciando actividad en + Actividad completada + Se analizarán tus datos y recibirás los resultados cuando estén listos. + Quedan %1$s segundos. + + Capturar imagen + Volver a capturar imagen + No se ha encontrado ninguna cámara. Este paso no se puede completar. + Para completar este paso, ve a Ajustes para conceder a la aplicación acceso a la cámara. + No se ha especificado ningún directorio de salida para las imágenes capturadas. + No se ha podido guardar la imagen capturada. + + Iniciar grabación + Detener grabación + Volver a capturar vídeo + + Forma física + Distancia (%1$s) + Frecuencia cardíaca (ppm) + Siéntate cómodamente durante %1$s. + Camina lo más rápido que puedas durante %1$s. + Esta actividad controla tu frecuencia cardíaca y mide la distancia que puedes recorrer andando en %1$s. + Camina al aire libre lo más rápido que puedas durante %1$s. Transcurrido este tiempo, siéntate y descansa durante %2$s. Para comenzar, pulsa Empezar. + + Marcha y equilibrio + Esta actividad mide tu forma de andar, tu equilibrio al caminar y al permanecer quieto de pie. No sigas adelante si no puedes caminar de forma segura sin ayuda. + Busca un lugar en el que puedas dar unos %1$d pasos en línea recta de forma segura sin ayuda. + Guárdate el teléfono en un bolsillo o un bolso y sigue las instrucciones de audio. + Ahora permanece quieto de pie durante %1$s. + Permanece quieto de pie durante %1$s. + Da la vuelta y camina hasta el punto de partida. + Da unos %1$d pasos en línea recta. + + Busca un lugar en el que puedas caminar de un lado a otro en línea recta de forma segura. Camina sin parar y da la vuelta al final del recorrido, como si rodearas un cono.\n\nA continuación, se te indicará que gires sobre ti mismo dando una vuelta completa y que te quedes quieto con los brazos relajados a los lados del cuerpo y los pies separados de forma que estén alineados con los hombros. + Pulsa Empezar cuando estés preparado para comenzar.\nA continuación, guarda el teléfono en un bolsillo o en una bolsa y sigue las instrucciones de audio. + Camina de un lado a otro en línea recta durante %1$s. Camina como lo harías normalmente. + Gira sobre ti mismo dando una vuelta completa y quédate quieto durante %1$s. + Has acabado la actividad. + + Velocidad de pulsación + Mano derecha + Mano izquierda + Esta actividad mide tu velocidad de pulsación. + Coloca el teléfono sobre una superficie plana. + Utiliza dos dedos de la misma mano para pulsar de forma alternativa los botones de la pantalla. + Utiliza dos dedos de la mano derecha para pulsar de forma alternativa los botones de la pantalla. + Utiliza dos dedos de la mano izquierda para pulsar de forma alternativa los botones de la pantalla. + Ahora repite la prueba, pero con la mano derecha. + Ahora repite la prueba, pero con la mano izquierda. + Pulsa con un dedo y luego con el otro. Intenta calcular el tiempo entre pulsación y pulsación para que sea lo más regular posible. Sigue pulsando durante %1$s. + Pulsa Empezar para comenzar. + Pulsa Siguiente para empezar. + Pulsar + N.º total de pulsaciones + Pulsa los botones a un ritmo que sea lo más constante posible utilizando dos dedos. + Pulsa los botones con la mano derecha. + Pulsa los botones con la mano izquierda. + Omitir esta mano + + Voz + Pulsa Empezar para comenzar. + Di “aaaaaa” hacia el micrófono durante el máximo tiempo posible. + Inspira profundamente y di “aaaaaa” hacia el micrófono durante el máximo tiempo posible. Mantén un volumen constante para que las barras de audio permanezcan en color azul. + Esta actividad evalúa tu voz grabándola con el micrófono situado en la parte inferior del teléfono. + Demasiado alto + No se puede grabar el audio + Espera mientras se comprueba el nivel de ruido de fondo. + Hay demasiado ruido ambiental para grabar tu voz. Ve a un lugar más tranquilo y vuelve a intentarlo. + Pulsa Siguiente cuando estés preparado. + + Audiometría tonal + Esta actividad mide tu capacidad para oír diferentes sonidos. + Antes de empezar, conecta los auriculares y póntelos. + Pulsa Empezar para comenzar. + Ahora deberías oír un tono. Ajusta el volumen con los controles situados en el lateral del dispositivo.\n\nToca el botón cuando estés listo para empezar. + Toca el botón cada vez que oigas un sonido. + %1$s Hz, izquierdo + %1$s Hz, derecho + + Memoria espacial + Esta actividad mide tu memoria espacial a corto plazo haciendo que repitas el orden en que se iluminan las %1$s. + flores + flores + Las %1$s se irán iluminando de una en una. Pulsa las %2$s en el mismo orden en el que se han iluminado. + Las %1$s se irán iluminando de una en una. Pulsa las %2$s en orden inverso al que se han iluminado. + Para comenzar, pulsa Empezar y observa con atención. + %1$s + Puntuación + Observa las %1$s que se iluminan + Pulsa las %1$s en el orden en el que se han iluminado + Pulsa las %1$s en orden inverso + Secuencia completada + Para continuar, pulsa Siguiente + Reintentar + No te ha dado tiempo a hacerlo. Pulsa Siguiente para continuar. + Se acabó el tiempo + Se ha acabado el tiempo.\nPulsa Siguiente para continuar. + Juego completado + En pausa + Para continuar, pulsa Siguiente + + Tiempo de reacción + Esta actividad evalúa el tiempo que tardas en responder a un estímulo visual. + Agita el dispositivo en cualquier dirección en cuanto el punto azul aparezca en pantalla. Se te pedirá que lo hagas %D veces. + Pulsa Empezar para comenzar. + Intento %1$s de %2$s + Cuando aparezca el círculo azul, agita rápidamente el dispositivo + + Torre de Hanoi + Esta actividad evalúa tu habilidad para solucionar rompecabezas. + Mueve toda la pila a la plataforma resaltada en el mínimo número de movimientos posible. + Pulsa Empezar para comenzar + Soluciona el rompecabezas + Número de movimientos: %1$s \n %2$s + No sé resolver este rompecabezas + + Paseo cronometrado + Esta actividad mide la funcionalidad de tu extremidad inferior. + Busca un lugar, preferiblemente al aire libre, donde puedas caminar unos %1$s en línea recta lo más rápido posible, pero de forma segura. No disminuyas la velocidad hasta que hayas llegado al final del recorrido. + Pulsa Siguiente para empezar. + Dispositivo de ayuda + Utiliza el mismo dispositivo de ayuda en cada prueba. + ¿Utilizas una órtesis de pie y tobillo? + ¿Utilizas algún dispositivo de ayuda? + Pulsa aquí para responder. + Ninguno + Un bastón + Una muleta + Dos bastones + Dos muletas + Andador (con o sin ruedas) + Camina %1$s en línea recta. + Da la vuelta y camina hasta el punto de partida. + Pulsa OK cuando hayas acabado. + + PASAT + PVSAT + PAVSAT + La prueba de suma seriada auditiva por pasos mide tu velocidad de procesamiento de información auditiva y tu capacidad de cálculo. + La prueba de suma seriada visual por pasos mide tu velocidad de procesamiento de información visual y tu capacidad de cálculo. + La prueba de suma seriada auditiva y visual por pasos mide tu velocidad de procesamiento de información auditiva y visual y tu capacidad de cálculo. + Cada %1$s segundos se muestra un dígito.\nDebes sumar cada dígito nuevo al anterior.\nAtención: no debes calcular la suma total de todos los dígitos, sino solo la suma de los dos últimos números. + Pulsa Empezar para comenzar. + Recuerda este primer dígito. + Suma este otro dígito al anterior. + - + + Prueba de las %1$s clavijas + Esta actividad mide la capacidad funcional de tus extremidades superiores, para lo cual se te pedirá que introduzcas una clavija en un orificio %1$s veces. + La prueba debe realizarse tanto con la mano izquierda como con la derecha.\nHay que introducir la clavija lo más rápido posible en el orificio y, una vez que lo hayas hecho %1$s veces, volver a extraerla %2$s veces. + La prueba debe realizarse tanto con la mano derecha como con la izquierda.\nHay que introducir la clavija lo más rápido posible en el orificio y, una vez que lo hayas hecho %1$s veces, volver a extraerla %2$s veces. + Pulsa Empezar para comenzar. + Introduce la clavija en el orificio con la mano izquierda. + Introduce la clavija en el orificio con la mano derecha. + Coloca la clavija detrás de la línea con la mano izquierda. + Coloca la clavija detrás de la línea con la mano derecha. + Coge la clavija con dos dedos. + Levanta los dedos para soltar la clavija. + + Actividad para medir el temblor + Esta actividad mide el temblor de las manos en diferentes posiciones. Busca un lugar en el que puedas estar sentado cómodamente durante el tiempo que dure esta actividad. + Aguanta el teléfono en la mano más afectada como se muestra en la imagen siguiente. + Aguanta el teléfono en la mano derecha como se muestra en la imagen siguiente. + Aguanta el teléfono en la mano izquierda como se muestra en la imagen siguiente. + Se te solicitará que realices %1$s mientras estás sentado con el teléfono en la mano. + una tarea + dos tareas + tres tareas + cuatro tareas + cinco tareas + Pulsa Siguiente para continuar. + Prepárate para aguantar el teléfono en el regazo. + Prepárate para aguantar el teléfono en la mano izquierda apoyada en el regazo. + Prepárate para aguantar el teléfono en la mano derecha apoyada en el regazo. + Sigue aguantando el teléfono en el regazo durante %1$d segundos. + Ahora aguanta el teléfono con la mano extendida hacia arriba a la altura del hombro. + Ahora aguanta el teléfono con la mano izquierda extendida hacia arriba a la altura del hombro. + Ahora aguanta el teléfono con la mano derecha extendida hacia arriba a la altura del hombro. + Sigue aguantando el teléfono con la mano extendida durante %1$d segundos. + Ahora aguanta el teléfono a la altura del hombro con el codo doblado. + Ahora, aguanta el teléfono con la mano izquierda a la altura del hombro con el codo doblado. + Ahora, aguanta el teléfono con la mano derecha a la altura del hombro con el codo doblado. + Sigue aguantando el teléfono con el codo doblado durante %1$d segundos + Ahora, sin dejar de tener el codo doblado, toca el móvil con la nariz acercándotelo a la cara repetidamente. + Ahora, sin dejar de tener el codo doblado y con el teléfono en la mano izquierda, toca el móvil con la nariz acercándotelo a la cara repetidamente. + Ahora, sin dejar de tener el codo doblado y con el teléfono en la mano derecha, toca el móvil con la nariz acercándotelo a la cara repetidamente. + Sigue tocando el móvil con la nariz acercándotelo a la cara durante %1$d segundos + Prepárate para girar la muñeca a modo de saludo. + Prepárate para girar la muñeca a modo de saludo con el teléfono en la mano izquierda. + Prepárate para girar la muñeca a modo de saludo con el teléfono en la mano derecha. + Sigue girando la muñeca durante %1$d segundos a modo de saludo. + Ahora cambia el teléfono a la mano izquierda y continúa con la tarea siguiente. + Ahora cambia el teléfono a la mano derecha y continúa con la tarea siguiente. + Continúa con la tarea siguiente. + Actividad completada. + Se te solicitará que realices %1$s mientras estás sentado, primero con el teléfono en una mano y luego en la otra. + No puedo realizar esta actividad con la mano izquierda. + No puedo realizar esta actividad con la mano derecha. + Puedo realizar esta actividad con las dos manos. + + No se ha podido crear el archivo + No se han podido eliminar los archivos de registro suficientes para alcanzar el umbral + Error al establecer el atributo + Archivo no marcado como eliminado (no marcado como cargado) + Varios errores al eliminar los registros + No se han encontrado datos recopilados. + No se ha especificado ningún directorio de salida + + No hay datos + + Atrás + Ilustración de %1$s + Campo para la firma + Toca la pantalla y firma con el dedo + Firmado + Sin firmar + Seleccionado + No seleccionado + Regulador de respuesta que va del %1$s al %2$s. + Imagen sin etiqueta + Iniciar tarea + activo + correcto + incorrecto + inactivo + Juego de memoria + Previsualización de la captura + Imagen capturada + Previsualización de la captura de vídeo + Vídeo capturado + No se puede colocar un disco de tamaño %1$s en un disco de tamaño %2$s + Objetivo + Torre + Pulsa dos veces para colocar el disco + Pulsa dos veces para seleccionar el disco situado más arriba + Tiene discos de tamaño %1$s + Vacía + Rango de valores de %1$s a %2$s + Grupo compuesto por + y + Punto: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-fi/strings.xml b/backbone/src/main/res/values-fi/strings.xml new file mode 100644 index 000000000..2933fd387 --- /dev/null +++ b/backbone/src/main/res/values-fi/strings.xml @@ -0,0 +1,396 @@ + + + + + Suostumus + Etunimi + Sukunimi + Vaaditaan + Tarkasta + Tarkasta alla oleva lomake ja napauta ”Hyväksy”, jos olet valmis jatkamaan. + Tarkasta + Allekirjoitus + Allekirjoita sormellasi alla olevalle riville. + Allekirjoitus tähän + Sivu %1$ld / %2$ld + + Tervetuloa + Tietojen kerääminen + Tietosuoja + Tietojen käyttö + Tutkimuskysely + Tutkimustehtävät + Ajankäyttö + Peruuttaminen + Lisätietoja + + Lisätietoja tietojen keräämisestä + Lisätietoja tietojen käytöstä + Lisätietoja siitä, kuinka yksityisyyttäsi ja identiteettiäsi suojataan. + Lisätietoja tutkimuksesta + Lisätietoja tutkimuskyselystä + Lisätietoja tutkimuksen vaatimasta ajasta + Lisätietoja tutkimukseen liittyvistä tehtävistä + Lisätietoja peruuttamisesta + + Jakovalinnat + Jaa datani kohteen %1$s ja hyväksyttyjen tutkijoiden kanssa maailmanlaajuisesti + Jaa datani vain kohteen %1$s kanssa + %1$s vastaanottaa tutkimusdatasi tästä tutkimuksesta.\n\nKoodatun tutkimusdatan jakaminen laajemmalle käyttäjäkunnalle (ilman nimi ym. tietoja) voi edesauttaa tätä ja tulevia tutkimuksia. + Lisätietoja datajaosta + + %1$s – nimi tekstattuna + %1$s – allekirjoitus + Päivämäärä + + Vaihe %1$s / %2$s + + Virheellinen arvo + %1$s ylittää sallitun enimmäisarvon (%2$s). + %1$s on alle sallitun vähimmäisarvon (%2$s). + %1$s ei ole kelvollinen arvo. + + Virheellinen sähköpostiosoite: %1$s + + Syötä osoite + Määritettyä osoitetta ei löydy + Nykyistä sijaintiasi ei voida määrittää. Kirjoita osoite tai, jos mahdollista, siirry paikkaan, jossa GPS-signaali on parempi. + Sijaintipalveluiden käyttö on kielletty. Anna Asetuksissa tälle ohjelmalle oikeus käyttää Sijaintipalveluita. + Syötetyllä osoitteella ei löydy tuloksia. Varmista, että osoite on oikein. + Joko et ole yhteydessä internetiin tai olet ylittänyt osoitehakupyyntöjen enimmäismäärän. Jos et ole yhteydessä internetiin, laita Wi-Fi päälle vastataksesi tähän kysymykseen, ohita tämä kysymys (jos ohituspainike on käytettävissä), tai palaa kyselyyn, kun olet internet-yhteydessä. Muussa tapauksessa yritä muutaman minuutin päästä uudelleen. + + Tekstisisältö ylittää maksimipituuden: %1$s + + Kamera ei ole käytettävissä jaetulla näytöllä. + + Sähköposti + jappleseed@example.com + Salasana + Syötä salasana + Vahvista + Syötä salasana uudelleen + Salasanat eivät täsmää. + Lisätietoja + John + Appleseed + Sukupuoli + Valitse sukupuoli + Syntymäaika + Valitse päiväys + + Vahvistus + Vahvista sähköpostisi + Napauta alla olevaa linkkiä, jos et saanut vahvistussähköpostia ja haluat, että se lähetetään uudelleen. + Lähetä uusi vahvistussähköposti + + Kirjaudu sisään + Unohditko salasanan? + + Syötä pääsykoodi + Vahvista pääsykoodi + Pääsykoodi tallennettu + Pääsykoodi todennettu + Syötä vanha pääsykoodi + Syötä uusi pääsykoodi + Vahvista uusi pääsykoodi + Virheellinen pääsykoodi + Pääsykoodit eivät täsmää. Yritä uudelleen. + Tunnistaudu Touch ID:llä + Touch ID -virhe + Vain numerot ovat sallittuja. + Pääsykoodin syötön edistymisen osoitin + %1$s merkkiä yhteensä %2$s merkistä syötetty + Unohditko pääsykoodin? + + Avainnipun kohdetta ei voitu lisätä. + Avainnipun kohdetta ei voitu päivittää. + Avainnipun kohdetta ei voitu poistaa. + Avainnipun kohdetta ei löydy. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Nainen + Mies + Muu + + Ei + Kyllä + + cm + ft + in + + Vastaa napauttamalla + Valitse vastaus + Valitse napauttamalla + Kirjoita napauttamalla + + Hyväksy + Kumoa + OK + Tyhjennä + Hylkää + Valmis + Aloita + Lisätietoja + Seuraava + Ohita + Ohita tämä kysymys + Käynnistä ajastin + Jatka tästä myöhemmin + Hylkää tulokset + Lopeta tehtävä + Tallenna + Tyhjennä vastaus + Tätä vastausta ei voida muuttaa. + + Aktiviteetti alkaa + Aktiviteetti valmis + Datasi analysoidaan ja sinulle ilmoitetaan, kun tuloksesi ovat valmiina. + %1$s sekuntia jäljellä. + + Kaappaa kuva + Kaappaa kuva uudelleen + Kameraa ei löydy. Tätä vaihetta ei voida suorittaa. + Jotta tämä vaihe voidaan suorittaa, anna Asetuksissa tälle ohjelmalle käyttöoikeus kameraan. + Kaapatuille kuville ei ole määritetty kohdehakemistoa. + Kaapattua kuvaa ei voitu tallentaa. + + Aloita tallentaminen + Lopeta tallentaminen + Kaappaa video uudelleen + + Kuntoilu + Matka (%1$s) + Syke (bpm) + Istu %1$s. + Kävele niin nopeasti kuin voit %1$s. + Tämä aktiviteetti seuraa sykettäsi ja mittaa kuinka kauas voit kävellä, kun aikaa on %1$s. + Kävele ulkona kovinta mahdollista vauhtia %1$s. Kun olet valmis, istu ja lepää %2$s. Aloita napauttamalla Aloita. + + Askeleet ja tasapaino + Tämä aktiviteetti arvioi askellustasi ja tasapainoasi, kun kävelet tasaisesti ja seisot. Älä jatka, jos et pysty turvallisesti kävelemään avustamatta. + Etsi paikka, jossa voit turvallisesti kävellä avustamatta noin %1$d askelta suoraan. + Laita puhelin taskuun tai laukkuun ja seuraa ääniohjeita. + Seiso nyt paikoillaan %1$s. + Seiso paikoillaan %1$s. + Käänny ympäri ja kävele takaisin alkupisteeseen. + Kävele enintään %1$d askelta suoraan. + + Etsi paikka, jossa voit turvallisesti kävellä suoraa reittiä edestakaisin. Yritä kävellä jatkuvasti kääntyen reitin päissä ikään kuin kiertäisit liikenteenohjainta.\n\nSeuraavaksi sinua pyydetään kääntymään ympäri täysi kierros, ja sitten seisomaan paikallasi kädet sivuilla ja jalat noin hartianleveyden etäisyydellä toisistaan. + Kun olet valmis, napauta Aloita.\nLaita sitten puhelin taskuun tai laukkuun ja seuraa ääniohjeita. + Kävele suoraa reittiä edestakaisin %1$s. Kävele, kuten kävelisit tavallisesti. + Käänny kokonaan ympäri ja seiso paikallasi %1$s. + Aktiviteetti on suoritettu. + + Napautusnopeus + Oikea käsi + Vasen käsi + Tämä aktiviteetti mittaa napautusnopeuttasi. + Laita puhelin tasaiselle alustalle. + Käytä saman käden kahta sormea näytön painikkeiden napauttamiseen vuorotellen. + Käytä oikean käden kahta sormea näytön painikkeiden napauttamiseen vuorotellen. + Käytä vasemman käden kahta sormea näytön painikkeiden napauttamiseen vuorotellen. + Suorita nyt sama testi oikealla kädellä. + Suorita nyt sama testi vasemmalla kädellä. + Napauta yhtä sormea ja sitten toista. Yritä ajoittaa napautukset mahdollisimman tasaisesti. Napauttele %1$s. + Aloita napauttamalla Aloita-painiketta. + Aloita napauttamalla Seuraava. + Napauta + Napautuksia yhteensä + Napauta painikkeita niin tasaisesti kuin pystyt kahdella sormella. + Napauta painikkeita OIKEALLA kädellä. + Napauta painikkeita VASEMMALLA kädellä. + Ohita tämä käsi + + Ääni + Aloita napauttamalla Aloita-painiketta. + Sano ”aaa” mikrofoniin niin kauan kuin voit. + Hengitä syvään ja sano ”Aaaaa\" mikrofoniin niin pitkään kuin pystyt. Pidä äänenvoimakkuus tasaisena siten, että äänipalkit pysyvät sinisinä. + Tämä aktiviteetti arvioi ääntäsi tallentamalla sitä puhelimen alareunassa olevalla mikrofonilla. + Liian kovaa + Ei voida äänittää + Odota, taustamelun tasoa tarkistetaan. + Ympäristön melutaso on liian korkea, jotta ääntäsi voitaisiin tallentaa. Siirry hiljaisempaan paikkaan ja yritä uudelleen. + Napauta Seuraava, kun olet valmis. + + Audiometria + Tämä aktiviteetti mittaa kykyäsi kuulla erilaisia ääniä. + Ennen aloittamista liitä kuulokkeet ja laita ne korvillesi. + Aloita napauttamalla Aloita-painiketta. + Sinun pitäisi nyt kuulla ääni. Säädä voimakkuutta laitteen sivussa olevilla säätimillä.\n\nNapauta painiketta kun olet valmis aloittamaan. + Napauta painiketta heti, kun alat kuulla äänen. + %1$s Hz, vasen + %1$s Hz, oikea + + Avaruudellinen muisti + Tämä aktiiviteetti mittaa lyhytaikaista avaruudellista muistiasi. Toista järjestys, jossa %1$s syttyy. + kukkia + kukkia + Näet yksi kerrallaan syttyviä %1$s. Napauta %2$s samassa järjestyksessä kuin ne syttyivät. + Näet yksi kerrallaan syttyviä %1$s. Napauta %2$s käänteisessä syttymisjärjestyksessä. + Aloita napauttamalla Aloita ja katso tarkasti. + %1$s + Tulos + Katso %1$s, kun niihin syttyy valo. + Napauta %1$s siinä järjestyksessä, kun ne syttyivät + Napauta %1$s käänteisessä järjestyksessä + Jakso valmis + Jatka napauttamalla Seuraava + Yritä uudelleen + Et onnistunut tällä kertaa. Jatka napauttamalla Seuraava. + Aika loppui + Aika loppui.\nJatka napauttamalla Seuraava. + Peli päättyi + Keskeytetty + Jatka napauttamalla Seuraava + + Reaktioaika + Tämä aktiviteetti mittaa aikaa, joka sinulta kuluu vastata visuaaliseen tapahtumaan. + Ravista laitetta mihin tahansa suuntaan heti, kun sininen piste tulee näkyviin. Sinua pyydetään tekemään tämä %D kertaa. + Aloita napauttamalla Aloita-painiketta. + Yritys %1$s / %2$s + Kun sininen ympyrä tulee näkyviin, ravista nopeasti laitetta + + Hanoin torni + Tämä aktiviteetti arvioi ongelmanratkaisukykyä. + Siirrä koko pino korostetulle alustalle mahdollisimman vähillä siirroilla. + Aloita napauttamalla Aloita-painiketta. + Ratkaise tehtävä + Siirtojen määrä: %1$s \n %2$s + En osaa ratkaista tätä tehtävää + + Ajoitettu kävely + Tämä aktiviteetti mittaa alaraajojen toimintaa. + Etsi mieluiten ulkoa paikka, jossa voit turvallisesti kävellä suoraan noin %1$s niin nopeasti kuin mahdollista. Älä hidasta ennen kuin olet ylittänyt maaliviivan. + Aloita napauttamalla Seuraava. + Apuväline + Käytä samaa apuvälinettä kaikissa testeissä. + Käytätkö nilkkatukea? + Käytätkö apuvälinettä? + Valitse vastaus napauttamalla tähän. + Ei mitään + Yksi kävelykeppi + Yksi kainalosauva + Kaksi kävelykeppiä + Kaksi kainalosauvaa + Kävelytuki/rollaattori + Kävele %1$s suoraan. + Käänny ympäri ja kävele takaisin alkupisteeseen. + Kun se on tehty, napauta Valmis. + + PASAT + PVSAT + PAVSAT + PASAT-testi (Paced Auditory Serial Addition Test) mittaa kuullun informaation käsittelynopeuttasi ja laskutaitoasi. + PVSAT-testi (Paced Visual Serial Addition Test) mittaa nähdyn informaation käsittelynopeuttasi ja laskutaitoasi. + PAVSAT-testi (Paced Auditory and Visual Serial Addition Test) mittaa kuullun ja nähdyn informaation käsittelynopeuttasi ja laskutaitoasi. + Yksittäisiä numeroita näytetään %1$s sekunnin välein.\nKukin uusi numero on lisättävä sitä edeltäneeseen.\nHuomaa, että ei ole tarkoitus laskea jatkuvaa summaa, vaan vain kahden viimeisen numeron summa. + Aloita napauttamalla Aloita-painiketta. + Muista tämä ensimmäinen numero. + Lisää tämä uusi numero edelliseen. + - + + %1$s reiän palikkatesti + Tämä aktiviteetti mittaa yläraajojen toimintaa pyytämällä sinua asettamaan palikan reikään. Tämä toistetaan %1$s kertaa. + Sekä vasen että oikea käsi testataan.\nSinun on poimittava palikka mahdollisimman nopeasti, asetettava se reikään, ja tehtyäsi tämän %1$s kertaa, poistettava se jälleen %2$s kertaa. + Sekä oikea että vasen käsi testataan.\nSinun on poimittava palikka mahdollisimman nopeasti, asetettava se reikään, ja tehtyäsi tämän %1$s kertaa, poistettava se jälleen %2$s kertaa. + Aloita napauttamalla Aloita-painiketta. + Laita palikka reikään vasemmalla kädellä. + Laita palikka reikään oikealla kädellä. + Laita palikka viivan taakse vasemmalla kädellä. + Laita palikka viivan taakse oikealla kädellä. + Poimi palikka kahdella sormella. + Pudota palikka nostamalla sormia. + + Vapina-aktiviteetti + Tämä aktiviteetti mittaa käsiesi vapinaa eri asennoissa. Etsi paikka, jossa voit istua mukavasti tämän aktiviteetin keston ajan. + Pidä puhelinta alla olevan kuvan mukaisesti kädessä, jossa vaikutus on voimakkaampi. + Pidä puhelinta alla olevan kuvan mukaisesti OIKEASSA kädessä. + Pidä puhelinta alla olevan kuvan mukaisesti VASEMMASSA kädessä. + Sinua pyydetään suorittamaan %1$s puhelin kädessä istuen. + tehtävä + kaksi tehtävää + kolme tehtävää + neljä tehtävää + viisi tehtävää + Jatka napauttamalla Seuraava. + Valmistaudu pitämään puhelinta sylissäsi. + Valmistaudu pitämään puhelinta sylissäsi VASEMMALLA kädellä. + Valmistaudu pitämään puhelinta sylissäsi OIKEALLA kädellä. + Pidä puhelinta sylissäsi %1$d sekuntia. + Pidä puhelinta nyt käsi suorana olkapään korkeudella. + Pidä puhelinta nyt VASEN käsi suorana olkapään korkeudella. + Pidä puhelinta nyt OIKEA käsi suorana olkapään korkeudella. + Pidä puhelinta käsi suorana %1$d sekuntia. + Pidä puhelinta nyt olkapään korkeudella kyynärpää taivutettuna. + Pidä puhelinta nyt VASEMMALLA kädellä olkapään korkeudella kyynärpää taivutettuna. + Pidä puhelinta nyt OIKEALLA kädellä olkapään korkeudella kyynärpää taivutettuna. + Pidä puhelinta kyynärpää taivutettuna %1$d sekuntia + Kosketa nyt puhelimella nenääsi toistuvasti pitäen kyynärpäätä taivutettuna. + Kosketa nyt puhelimella nenääsi toistuvasti pitäen puhelinta VASEMMASSA kädessä ja kyynärpäätä taivutettuna. + Kosketa nyt puhelimella nenääsi toistuvasti pitäen puhelinta OIKEASSA kädessä ja kyynärpäätä taivutettuna. + Kosketa puhelimella nenääsi toistuvasti %1$d sekuntia + Valmistaudu vilkuttamaan kuninkaallisesti (vilkuttamaan rannetta pyörittämällä). + Valmistaudu puhelin VASEMMASSA kädessä vilkuttamaan kuninkaallisesti (vilkuttamaan rannetta pyörittämällä). + Valmistaudu puhelin OIKEASSA kädessä vilkuttamaan kuninkaallisesti (vilkuttamaan rannetta pyörittämällä). + Vilkuta kuninkaallisesti %1$d sekuntia. + Vaihda puhelin nyt VASEMPAAN käteen ja jatka seuraavaan tehtävään. + Vaihda puhelin nyt OIKEAAN käteen ja jatka seuraavaan tehtävään. + Jatka seuraavaan tehtävään. + Aktiviteetti valmis. + Sinua pyydetään suorittamaan %1$s istuen, ensin puhelin toisessa kädessä ja sitten toisessa. + En voi suorittaa tätä aktiviteettia VASEMMALLA kädellä. + En voi suorittaa tätä aktiviteettia OIKEALLA kädellä. + Voin suorittaa tämän aktiviteetin molemmilla käsillä. + + Tiedostoa ei voitu luoda + Ei voitu poistaa riittävästi lokitiedostoja raja-arvon saavuttamiseksi. + Virhe asetettaessa attribuuttia + Tiedostoa ei ole merkitty poistetuksi (ei merkitty lähetetyksi) + Useita virheitä poistettaessa lokeja + Kerättyjä tietoja ei löytynyt. + Ei määritettyä tulostehakemistoa + + Ei dataa + + Takaisin + Kuvassa %1$s + Määritetty allekirjoituskenttä + Allekirjoita koskettamalla näyttöä ja liikuttamalla sormea + Allekirjoitettu + Ei allekirjoitettu + Valittu + Valitsematon + Vastausliukusäädin. Asteikko %1$s–%2$s + Merkitsemätön kuva + Aloita tehtävä + aktiivinen + oikein + väärin + lepotilassa + Muistipelin ruutu + Kaappauksen esikatselu + Kaapattu kuva + Videokaappauksen esikatselu + Kaapattu video + Koon %1$s levyä ei voida laittaa koon %2$s levyn päälle + Kohde + Torni + Aseta levy kaksoisnapauttamalla + Valitse päällimmäinen levy kaksoisnapauttamalla + Sisältää levyjä, joiden koko on %1$s + Tyhjä + Alue välillä %1$s–%2$s + Pino, joka koostuu arvoista + ja + Piste: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-fr-rCA/strings.xml b/backbone/src/main/res/values-fr-rCA/strings.xml new file mode 100644 index 000000000..e05cfcc53 --- /dev/null +++ b/backbone/src/main/res/values-fr-rCA/strings.xml @@ -0,0 +1,396 @@ + + + + + Consentement + Prénom + Nom de famille + Requis + Révision + Consultez le formulaire ci-dessous et touchez Accepter si vous êtes prêt à continuer. + Révision + Signature + Signez avec votre doigt sur la ligne ci-dessous. + Signez ici + Page %1$ld sur %2$ld + + Bienvenue + Collecte de données + Confidentialité + Utilisation des données + Enquête de l’étude + Tâches de l’étude + Durée de l\'engagement + Abandonner + En savoir plus + + En savoir plus sur la façon dont les données sont recueillies + En savoir plus sur la façon dont les données sont utilisées + En savoir plus sur notre façon de protéger votre confidentialité et votre identité + Tout d’abord, en savoir plus sur l’étude + En savoir plus sur l’enquête de l’étude + En savoir plus sur l’impact de l’étude sur votre agenda + En savoir plus sur les tâches concernées + En savoir plus sur l’abandon + + Options de partage + Partager mes données avec %1$s et les chercheurs qualifiés de par le monde + Ne partager mes données qu’avec %1$s + %1$s recevra les données de votre participation à cette étude .\n\nLe partage de vos données codées relatives à l’étude (hormis les informations, telles que votre nom) pourrait permettre de faire avancer cette recherche et les recherches futures. + Plus d’info sur le partage des données + + Nom de %1$s (en imprimé) + Signature de %1$s + Date + + Étape %1$s sur %2$s + + Valeur non valide + %1$s dépasse la valeur maximale autorisée (%2$s). + %1$s est inférieur à la valeur minimale autorisée (%2$s). + %1$s n’est pas une valeur valide. + + Adresse courriel non valide : %1$s + + Entrer une adresse + Adresse indiquée introuvable + Impossible de déterminer votre position actuelle. Tapez une adresse ou déplacez-vous vers un endroit où le signal GPS est meilleur, si possible. + L’accès au service de localisation a été refusé. Veuillez autoriser cette app à utiliser le service de localisation dans Réglages. + Impossible de trouver un résultat pour l’adresse indiquée. Assurez-vous que celle-ci est valide. + Soit vous n’êtes pas connecté à Internet ou vous avez atteint la limite de demandes de recherche d’adresses. Si vous n’êtes pas connecté à Internet, activez le Wi-Fi pour répondre à cette question, ignorez cette question si le bouton ignorer est disponible, ou revenez au questionnaire lorsque vous êtes connecté à Internet. Sinon, réessayez dans quelques minutes. + + La longueur du texte dépasse le maximum autorisé : %1$s + + Caméra indisponible en écran scindé. + + Courriel + gallain@example.com + Mot de passe + Entrer le mot de passe + Confirmer + Entrer à nouveau le mot de passe + Les mots de passe sont différents. + Informations supplémentaires + Gilles + Allain + Sexe + Choisir un sexe + Date de naissance + Choisir une date + + Vérification + Vérifiez votre courriel + Touchez le lien ci-dessous si vous n’avez pas reçu de courriel de confirmation et que vous souhaitez qu’il vous soit envoyé à nouveau. + Renvoyer le courriel de vérification + + Connexion + Mot de passe oublié? + + Entrez le code. + Confirmez le code. + Code enregistré + Code authentifié + Entrez votre ancien code. + Entrez votre nouveau code. + Confirmez votre nouveau code. + Code incorrect + Les codes sont différents. Réessayez. + Authentifiez-vous avec Touch ID + Erreur de Touch ID + Seuls les caractères numériques sont autorisés. + Indicateur de progression de saisie du code + %1$s sur %2$s chiffres entrés + Code oublié? + + Impossible d’ajouter l’élément du trousseau. + Impossible de mettre à jour l’élément du trousseau. + Impossible de supprimer l’élément du trousseau. + Impossible de trouver l’élément du trousseau. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Femme + Homme + Autre + + Non + Oui + + cm + pi + po + + Toucher pour répondre + Sélectionner une réponse + Toucher pour sélectionner + Toucher pour écrire + + Accepter + Annuler + OK + Effacer + Refuser + OK + Démarrer + En savoir plus + Suivant + Ignorer + Ignorer cette question + Démarrer le minuteur + Enregistrer pour plus tard + Supprimer les résultats + Terminer la tâche + Enregistrer + Effacer la réponse + Impossible de modifier cette réponse. + + L’activité commence dans + Activité terminée + Les données vont être analysées, et vous recevrez une notification lorsque les résultats seront disponibles. + %1$s secondes restantes. + + Capturer l’image + Recapturer l’image + Caméra introuvable. Cette étape n’a pas pu être achevée. + Afin de conclure cette étape, autorisez cette app à accéder à la caméra dans Réglages. + Aucun répertoire de sortie n’a été spécifié pour les images capturées. + L’image capturée n’a pas pu être enregistrée. + + Lancer l’enregistrement + Arrêter l’enregistrement + Recapturer la vidéo + + Forme + Distance (%1$s) + Rythme cardiaque (bpm) + Asseyez-vous confortablement pendant %1$s. + Marchez aussi vite que possible pendant %1$s. + Cette activité mesure votre rythme cardiaque, ainsi que la distance que vous pouvez parcourir à pied en %1$s. + Marchez aussi vite que possible pendant %1$s en plein air. Ensuite, asseyez-vous et reposez-vous confortablement pendant %2$s. Pour commencer, touchez Démarrer. + + Démarche et équilibre + Cette activité mesure votre démarche et votre équilibre lorsque vous marchez et lorsque vous êtes immobile. Ne poursuivez pas cette activité si vous ne pouvez pas marcher sans assistance. + Trouvez un endroit adéquat pour parcourir %1$d pas en ligne droite, en sécurité et sans assistance. + Placez votre téléphone dans une poche ou un sac, et suivez les instructions audio. + Ne bougez plus pendant %1$s. + Ne bougez pas pendant %1$s. + Retournez-vous, puis marchez vers votre point de départ. + Parcourez %1$d pas en ligne droite. + + Identifiez un endroit où vous pouvez faire des allers-retours en ligne droite de façon sécuritaire. Essayez de marcher sans vous arrêter, en faisant demi-tour en fin de ligne droite, comme si vous contourniez un cône.\n\nIl vous sera ensuite demandé de vous retourner en effectuant un cercle complet, puis de rester immobile, les bras le long du corps et les pieds dans l’alignement des épaules. + Touchez Démarrer lorsque vous êtes prêt.\nPlacez ensuite votre téléphone dans une poche ou un sac, puis suivez les instructions audio. + Faites des allers-retours en ligne droite pendant %1$s. Marchez de façon habituelle. + Effectuez un cercle complet, puis restez immobile pendant %1$s. + Vous avez terminé l’activité. + + Vitesse de frappe + Main droite + Main gauche + Cette activité mesure votre vitesse de frappe. + Placez votre téléphone sur une surface plane. + Utilisez deux doigts d’une même main pour toucher tour à tour les boutons affichés à l’écran. + Utilisez deux doigts de la main droite pour toucher tour à tour les boutons affichés à l’écran. + Utilisez deux doigts de la main gauche pour toucher tour à tour les boutons affichés à l’écran. + Maintenant, répétez le test avec la main droite. + Maintenant, répétez le test avec la main gauche. + Touchez l’écran avec un doigt, puis avec l’autre. Faites en sorte que les contacts soient aussi réguliers que possible. Continuez pendant %1$s. + Touchez Démarrer pour commencer. + Touchez Suivant pour commencer. + Toucher + Total de contacts + Touchez les boutons aussi régulièrement que possible avec deux doigts. + Touchez les boutons avec la main DROITE. + Touchez les boutons avec la main GAUCHE. + Ignorer cette main + + Voix + Touchez Démarrer pour commencer. + Dites « Aaaaa » dans le micro le plus longtemps possible. + Respirez profondément et dites « Aaaaa » dans le micro le plus longtemps possible. Produisez un volume vocal stable de façon à ce que les barres audio restent bleues. + Cette activité évalue votre voix en l’enregistrant grâce au micro situé au bas de votre téléphone. + Trop fort + Impossible d’enregistrer le son + Veuillez patienter pendant que nous vérifions le niveau de bruit de fond. + Le niveau de bruit ambiant est trop élevé pour enregistrer votre voix. Choisissez un endroit plus silencieux, puis réessayez. + Touchez Suivant lorsque vous êtes prêt. + + Audiométrie tonale + Cette activité mesure votre aptitude à entendre différents sons. + Avant de commencer, branchez et placez vos écouteurs. + Touchez Démarrer pour commencer. + Vous devriez maintenant entendre un son. Réglez le volume en utilisant les boutons latéraux de votre appareil.\n\nTouchez le bouton lorsque vous êtes prêt. + Touchez le bouton à chaque fois que vous entendez un son. + %1$s Hz, gauche + %1$s Hz, droite + + Mémoire spatiale + Cette activité mesure votre mémoire spatiale à court terme en vous demandant de reproduire l’ordre dans lequel les %1$s se sont allumées. + fleurs + fleurs + Certaines %1$s s’allumeront une par une. Touchez ces %2$s dans l’ordre duquel elles se sont allumées. + Certaines %1$s s’allumeront une par une. Touchez ces %2$s dans l’ordre inverse duquel elles se sont allumées. + Pour commencer, touchez Démarrer et soyez attentif. + %1$s + Score + Observez les %1$s s’allumer + Touchez les %1$s dans l’ordre d’affichage. + Touchez les %1$s dans l’ordre inverse. + Séquence terminée + Pour continuer, touchez Suivant + Réessayer + Vous n’y êtes pas arrivé cette fois-ci. Touchez Suivant pour continuer. + Temps écoulé + Le temps s’est écoulé.\nTouchez Suivant pour continuer. + Jeu terminé + En pause + Pour continuer, touchez Suivant + + Temps de réaction + Cette activité évalue votre de temps de réaction pour répondre à un signal visuel. + Secouez l’appareil dans n’importe quel sens dès que le point bleu apparaît à l’écran. Vous serez invité à effectuer cette action %D fois. + Touchez Démarrer pour commencer. + Tentative %1$s sur %2$s + Secouez rapidement l’appareil lorsque le cercle bleu apparaît. + + Tours de Hanoï + Cette activité évalue votre capacité à résoudre un casse-tête. + Déplacez toute la pile vers la plateforme surlignée en effectuant un minimum de mouvements. + Touchez Démarrer pour commencer. + Résoudre le casse-tête + Nombre de mouvements : %1$s \n %2$s + Je n’arrive pas à résoudre ce casse-tête + + Marche chronométrée + Cette activité évalue la fonction des membres inférieurs. + Identifiez un endroit, de préférence à l’extérieur, où vous pouvez marcher sur %1$s en ligne droite, le plus vite possible, mais en toute sécurité. Ne ralentissez pas tant que vous n’avez pas franchi la ligne d’arrivée. + Touchez Suivant pour commencer. + Appareil d’assistance + Utilisez le même appareil d’assistance pour chaque test. + Portez-vous une orthèse de cheville et de pied? + Utilisez-vous un appareil d’assistance? + Sélectionnez une réponse. + Aucune + Une canne + Une béquille + Deux cannes + Deux béquilles + Cadre de marche / ambulateur + Parcourez %1$s en ligne droite. + Retournez-vous, puis marchez vers votre point de départ. + Touchez OK lorsque vous avez terminé. + + PASAT + PVSAT + PAVSAT + Le test rythmé d’addition en série auditive (PASAT) mesure la vitesse de traitement d’informations auditives, ainsi que la capacité de calcul. + Le test rythmé d’addition en série visuelle (PAVAT) mesure la vitesse de traitement d’informations visuelles, ainsi que la capacité de calcul. + Le test rythmé d’addition en série auditive et visuelle (PAVSAT) mesure la vitesse de traitement d’informations auditives et visuelles, ainsi que la capacité de calcul. + Un chiffre s’affiche toutes les %1$s secondes.\nVous devez ajouter chaque nouveau chiffre au chiffre précédent.\nAttention, vous ne devez pas calculer la somme totale, mais bien la somme des deux derniers chiffres. + Touchez Démarrer pour commencer. + Mémorisez ce premier chiffre. + Ajoutez ce nouveau chiffre au précédent. + - + + Test des %1$s trous + Cette activité mesure la dextérité de vos membres supérieurs en vous demandant de placer un cercle dans un trou. On vous demandera de le faire %1$s fois. + Votre main gauche et votre main droite seront testées.\nVous devez saisir le cercle aussi rapidement que possible, le placer dans le trou et, après avoir fait cela %1$s fois, le retirer à nouveau %2$s fois. + Votre main droite et votre main gauche seront testées.\nVous devez saisir le cercle aussi rapidement que possible, le placer dans le trou et, après avoir fait cela %1$s fois, le retirer à nouveau %2$s fois. + Touchez Démarrer pour commencer. + Placez le cercle dans le trou en utilisant votre main gauche. + Placez le cercle dans le trou en utilisant votre main droite. + Placez le cercle derrière la ligne en utilisant votre main gauche. + Placez le cercle derrière la ligne en utilisant votre main droite. + Saisissez le cercle avec deux doigts. + Levez vos doigts pour relâcher le cercle. + + Tremblements + Cette activité mesure les tremblements de vos mains dans plusieurs positions. Choisissez un endroit où vous pouvez vous assoir confortablement pendant la durée de cette activité. + Tenez le téléphone dans la main la plus affectée comme indiqué dans l’image ci-dessous. + Tenez le téléphone dans la main DROITE comme indiqué dans l’image ci-dessous. + Tenez le téléphone dans la main GAUCHE comme indiqué dans l’image ci-dessous. + Il vous sera demandé d’effectuer %1$s tout en étant assis avec le téléphone dans la main. + une tâche + deux tâches + trois tâches + quatre tâches + cinq tâches + Touchez Suivant pour continuer. + Préparez-vous à tenir votre téléphone sur les genoux. + Préparez-vous à tenir votre téléphone sur les genoux avec votre main GAUCHE. + Préparez-vous à tenir votre téléphone sur les genoux avec votre main DROITE. + Tenez votre téléphone sur les genoux pendant %1$d secondes. + Maintenant, tenez votre téléphone en tendant votre bras à la hauteur de votre épaule. + Maintenant, tenez votre téléphone dans la main GAUCHE en tendant votre bras à la hauteur de votre épaule. + Maintenant, tenez votre téléphone dans la main DROITE en tendant votre bras à la hauteur de votre épaule. + Tenez votre téléphone avec la main tendue pendant %1$d secondes. + Maintenant, tenez votre téléphone à la hauteur de votre épaule tout en pliant le coude. + Maintenant, tenez votre téléphone dans la main GAUCHE à la hauteur de votre épaule tout en pliant le coude. + Maintenant, tenez votre téléphone dans la main DROITE à la hauteur de votre épaule tout en pliant le coude. + Tenez votre téléphone tout en pliant le coude pendant %1$d secondes. + Maintenant, pliez le coude et touchez votre nez avec le téléphone de façon répétée. + Maintenant, pliez le coude tout en tenant votre téléphone dans la main GAUCHE et touchez votre nez avec le téléphone de façon répétée. + Maintenant, pliez le coude tout en tenant votre téléphone dans la main DROITE et touchez votre nez avec le téléphone de façon répétée. + Touchez votre nez avec le téléphone à plusieurs reprises pendant %1$d secondes. + Préparez-vous à saluer en faisant pivoter le poignet. + Préparez-vous à saluer de la main GAUCHE en tenant le téléphone. + Préparez-vous à saluer de la main DROITE en tenant le téléphone. + Saluez de la main pendant %1$d secondes. + Prenez le téléphone dans la main GAUCHE, puis passez à la tâche suivante. + Prenez le téléphone dans la main DROITE, puis passez à la tâche suivante. + Passez à la tâche suivante. + Activité terminée. + Il vous sera demandé d’effectuer %1$s tout en étant assis avec le téléphone dans une main, puis dans l’autre. + Je ne peux pas effectuer cette activité avec la main GAUCHE. + Je ne peux pas effectuer cette activité avec la main DROITE. + Je peux effectuer cette activité avec les deux mains. + + Impossible de créer le fichier + Impossible de supprimer suffisamment de fichiers d’historique pour atteindre le seuil + Erreur de réglage d’attribut + Fichier non marqué comme supprimé (non marqué comme téléchargé) + Plusieurs erreurs se sont produites lors de la suppression des historiques + Aucune donnée recueillie n’a été trouvée. + Aucun répertoire de sortie n’est spécifié + + Aucune donnée + + Retour + Illustration de %1$s + Champ de signature désigné + Toucher l’écran et signer du doigt + Signé + Non signé + Sélectionné + Désélectionné + Curseur de réponse. De %1$s à %2$s + Image sans étiquette + Démarrer la tâche + active + correct + incorrect + calme + Fiche du jeu de mémoire + Aperçu de l’image capturée + Image capturée + Aperçu de la vidéo capturée + Vidéo capturée + Impossible de placer un disque de taille %1$s sur un disque de taille %2$s + Cible + Tour + Touchez deux fois pour placer le disque. + Touchez deux fois pour sélectionner le disque le plus haut. + Comporte des disques de tailles %1$s + Vide + De %1$s à %2$s + Pile composée de + et + Point : %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-fr/strings.xml b/backbone/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..e8dc311d4 --- /dev/null +++ b/backbone/src/main/res/values-fr/strings.xml @@ -0,0 +1,397 @@ + + + + + Consentement + Prénom + Nom + Obligatoire + Vérification + Vérifiez le formulaire ci-dessous et touchez Accepter si vous êtes prêt à continuer. + Vérification + Signature + Signez avec votre doigt sur la ligne ci-dessous. + Signer ici + Page %1$ld sur %2$ld + + Bienvenue + Collecte des données + Confidentialité + Utilisation des données + Étude + Tâches de l’étude + Temps consacré à l’étude + Retrait de l’étude + En savoir plus + + En savoir plus sur la collecte des données + En savoir plus sur l’utilisation des données + En savoir plus sur la protection de la confidentialité et de votre identité + Commencer par en savoir plus sur l’étude + En savoir plus sur l’étude + En savoir plus sur l’impact de l’étude sur votre emploi du temps + En savoir plus sur les tâches impliquées + En savoir plus sur le retrait de l’étude + + Options de partage + Partager mes données avec %1$s et des chercheurs du monde entier dans ce domaine + Ne partager mes données qu’avec %1$s + %1$s recevra les données recueillies dans le cadre de votre participation à cette étude.\n\nLe partage de ces données, codées, avec une plus large audience (en omettant votre nom, entre autres) peut être bénéfique à cette étude et à des études futures. + En savoir plus sur le partage de données + + Nom de %1$s (en majuscules) + Signature de %1$s + Date + + Étape %1$s sur %2$s + + Valeur non valide + %1$s dépasse la valeur maximale autorisée (%2$s). + %1$s est inférieur à la valeur minimale autorisée (%2$s). + %1$s n’est pas une valeur valide. + + Adresse e-mail non valide : %1$s + + Saisir une adresse + Impossible de localiser l’adresse + Impossible de vous localiser. Saisissez une adresse ou rendez-vous si possible dans un endroit avec un meilleur signal GPS. + Accès au service de localisation refusé. Permettez à cette app d’utiliser le service de localisation dans Réglages. + Impossible de trouver un résultat pour l’adresse saisie. Assurez-vous qu’elle est valide. + Vous n’êtes pas connecté à Internet ou vous avez atteint la limite de requêtes d’adresse. Si vous n’êtes pas connecté à Internet, activez votre Wi-Fi pour répondre à cette question, ignorez cette question si le bouton est disponible, ou reprenez l’enquête lorsque vous serez connecté à Internet. Sinon, veuillez attendre quelques minutes. + + La longueur du texte dépasse la limite : %1$s + + Appareil photo non disponible en plein écran. + + Adresse e-mail + gallain@example.com + Mot de passe + Saisir le mot de passe + Confirmer + Saisir à nouveau le mot de passe + Les mots de passe sont différents. + Informations supplémentaires + Gilles + Allain + Sexe + Choisir + Date de naissance + Définir la date + + Vérification + Vérifier votre adresse e-mail + Touchez le lien ci-dessous si vous n’avez pas reçu d’e-mail de vérification et aimeriez le renvoyer. + Renvoyer l’e-mail de vérification + + Connexion + Mot de passe oublié ? + + Saisir le code + Confirmer le code + Code enregistré + Authentification par code + Saisir votre ancien code + Saisir votre nouveau code + Confirmer votre nouveau code + Code incorrect + Les codes sont différents. Réessayez. + Authentification avec Touch ID requise + Erreur Touch ID + Caractères numériques uniquement. + Indicateur de progression de la saisie du code + %1$s sur %2$s chiffres saisis + Code oublié ? + + Impossible d’ajouter l’élément du trousseau. + Impossible de mettre à jour l’élément du trousseau. + Impossible de supprimer l’élément du trousseau. + Impossible de localiser l’élément du trousseau. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Femme + Homme + Autre + + Non + Oui + + cm + pi + po + + Toucher pour répondre + Sélectionner une réponse + Toucher pour sélectionner + Toucher pour écrire + + Accepter + Annuler + OK + Effacer + Refuser + Terminé + Démarrer + En savoir plus + Suivant + Ignorer + Ignorer cette question + Lancer le minuteur + Enregistrer pour plus tard + Effacer les résultats + Terminer la tâche + Enregistrer + Effacer la réponse + Cette réponse ne peut pas être modifiée. + + Début de l’activité dans + Activité terminée + Vos données vont être analysées. Vous serez averti lorsque les résultats seront prêts. + %1$s secondes restantes. + + Capturer l’image + Recapturer l’image + Aucun appareil photo n’a été trouvé. Cette étape ne peut être effectuée. + Afin d’effectuer cette étape, autorisez cette app à accéder à l’appareil photo dans Réglages. + Aucun répertoire de sortie n’a été spécifié pour les images capturées. + L’image capturée n’a pas pu être enregistrée. + + Lancer l’enregistrement + Arrêter l’enregistrement + Recapturer la vidéo + + Fitness + Distance (%1$s) + Rythme cardiaque (bpm) + Asseyez-vous confortablement pendant %1$s. + Marchez le plus rapidement possible pendant %1$s. + Cette activité mesure votre rythme cardiaque et la distance maximale que vous pouvez parcourir à pied en %1$s. + Marchez à l’extérieur à votre rythme le plus rapide pendant %1$s. Asseyez-vous ensuite confortablement pendant %2$s. Touchez Démarrer pour commencer. + + Marche et équilibre + Cette activité observe votre allure et votre équilibre pendant que vous marchez et que vous vous tenez debout. Ne continuez pas si vous ne pouvez pas marcher en sécurité sans assistance. + Trouvez un endroit où vous pouvez effectuer environ %1$d pas sur une ligne droite en sécurité et sans assistance. + Placez votre téléphone dans votre poche ou dans un sac et suivez les instructions audio. + Maintenant, ne bougez plus pendant %1$s. + Ne bougez pas pendant %1$s. + Faites demi-tour et revenez à votre point de départ. + Faites jusqu’à %1$d pas sur une ligne droite. + + Trouvez un endroit où vous pouvez faire des allers-retours en ligne droite en sécurité. Essayez de marcher de façon continue en effectuant des demi-tours au bout de chaque aller, comme si vous effectuiez un virage autour d’un cône.\n\nVous devrez ensuite effectuer un cercle complet puis vous tenir immobile, les bras le long du corps et les pieds écartés environ de la largeur des épaules. + Touchez Démarrer lorsque vous êtes prêt à commencer.\nPlacez votre téléphone dans votre poche ou dans un sac et suivez les instructions audio. + Faites des allers-retours en ligne droite pendant %1$s. Marchez normalement. + Effectuez un cercle complet en marchant, puis restez immobile pendant %1$s. + Vous avez terminé cette activité. + + Vitesse de saisie + Main droite + Main gauche + Cette activité mesure votre vitesse de saisie. + Placez votre téléphone sur une surface plane. + Avec deux doigts de la même main et en les alternant, touchez les boutons à l’écran. + Avec deux doigts de la main droite et en les alternant, touchez les boutons à l’écran. + Avec deux doigts de la main gauche et en les alternant, touchez les boutons à l’écran. + Répétez à présent ce test avec la main droite. + Répétez à présent ce test avec la main gauche. + Touchez l’écran avec un doigt, puis avec l’autre, pendant %1$s. Essayez de respecter le même intervalle de temps entre chaque toucher pour être aussi régulier que possible. + Touchez Démarrer pour commencer. + Touchez Suivant pour commencer. + Toucher + Total de touchers + Touchez les boutons de façon aussi régulière que possible avec deux doigts. + Touchez les boutons de la main DROITE. + Touchez les boutons de la main GAUCHE. + Ne pas faire avec cette main + + Voix + Touchez Démarrer pour commencer. + Dites « Aaaaah » dans le micro le plus longtemps possible. + Inspirez profondément et dites « Aaaaah » dans le micro le plus longtemps possible. Les barres audio doivent rester bleues : gardez le même volume. + Cette activité évalue votre voix en l’enregistrant dans le micro au bas de votre téléphone. + Trop fort + Enregistrement audio impossible + Veuillez patienter pendant que nous étudions le volume du fond sonore. + Le volume ambiant est trop fort pour enregistrer votre voix. Rendez-vous dans un endroit plus calme et réessayez. + Touchez Suivant lorsque vous êtes prêt. + + Audiométrie tonale + Cette activité mesure votre capacité à entendre différents sons. + Avant de commencer, branchez vos écouteurs et portez-les. + Touchez Démarrer pour commencer. + Vous devriez maintenant entendre une tonalité. Réglez le volume à l’aide des touches situées sur le côté de votre appareil.\n\nTouchez le bouton lorsque vous êtes prêt à commencer. + Touchez le bouton chaque fois que vous commencez à entendre un son. + %1$s Hz, à gauche + %1$s Hz, à droite + + Mémoire spatiale + Cette activité évalue votre mémoire spatiale à court terme en vous demandant de répéter l’ordre dans lequel les images de %1$s s’éclairent. + fleurs + fleurs + Certaines des images de %1$s s’allumeront une par une. Touchez ces images de %2$s dans l’ordre dans lequel elles s’allument. + Certaines des images de %1$s s’allumeront une par une. Touchez ces images de %2$s dans l’ordre inverse dans lequel elles s’allument. + Pour commencer, touchez Démarrer puis regardez avec attention. + %1$s + Résultat + Regarder les %1$s s’éclairer + Toucher les images de %1$s dans l’ordre d’éclairage + Toucher les images de %1$s dans l’ordre inverse + Séquence terminée + Pour continuer, touchez Suivant. + Réessayer + Vous n’avez pas totalement réussi cette fois-ci. Touchez Suivant pour continuer. + Temps écoulé + Vous avez manqué de temps.\nTouchez Suivant pour continuer. + Jeu terminé + En pause + Pour continuer, touchez Suivant. + + Temps de réaction + Cette activité évalue le temps qu’il vous faut pour réagir à un signal visuel. + Secouez l’appareil dans n’importe quelle direction dès que le point bleu apparaît à l’écran. Il vous sera demandé de le faire %D fois. + Touchez Démarrer pour commencer. + Tentative %1$s sur %2$s + Secouer rapidement l’appareil lorsque le cercle bleu apparaît + + Tour de Hanoï + Cette activité évalue votre capacité à terminer un jeu de réflexion. + Déplacez toute la pile de disques sur la plateforme en surbrillance en un minimum de déplacements. + Toucher Démarrer pour commencer + Jeu de réflexion + Nombre de déplacements : %1$s\n %2$s + Je ne parviens pas à faire ce jeu + + Marche chronométrée + Cette activité évalue le fonctionnement de vos extrémités inférieures. + Trouvez un endroit, de préférence en extérieur, où vous pouvez marcher environ %1$s en ligne droite aussi vite que possible, mais sans vous mettre en danger. Ne ralentissez pas avant d’avoir passé la ligne d’arrivée. + Touchez Suivant pour commencer. + Appareil d’assistance + Utilisez le même appareil d’assistance pour chaque test. + Portez-vous un releveur de pied ? + Utilisez-vous un appareil d’assistance ? + Touchez ici pour répondre. + Non + Une canne + Une béquille + Deux cannes + Deux béquilles + Un déambulateur/rollator + Marchez %1$s en ligne droite. + Faites demi-tour. + Revenez à votre point de départ. + Touchez Terminé une fois terminé. + + PASAT + PVSAT + PAVSAT + Le test PASAT (Paced Auditory Serial Addition Test) mesure votre vitesse de traitement des informations audio et votre capacité à calculer. + Le test PVSAT (Paced Visual Serial Addition Test) mesure votre vitesse de traitement des informations visuelles et votre capacité à calculer. + Le test PAVSAT (Paced Auditory and Visual Serial Addition Test) mesure votre vitesse de traitement des informations audio et visuelles et votre capacité à calculer. + Un chiffre apparaît toutes les %1$s secondes.\nAjoutez chaque nouveau chiffre à celui qui le précède.\nAttention, vous ne devez pas calculer la somme de tous les chiffres ajoutés mais seulement la somme du nouveau chiffre et du précédent. + Touchez Démarrer pour commencer. + Souvenez-vous de ce chiffre. + Ajoutez ce chiffre au précédent. + - + + Test des %1$s chevilles + Cette activité évalue le fonctionnement de vos extrémités supérieures. Vous devez placer un rond dans un trou et répéter cette opération %1$s fois. + Vos deux mains seront testées.\nRamassez le rond aussi vite que possible, placez-le dans le trou et, après avoir fait cela %1$s fois, retirez le rond %2$s fois. + Vos deux mains seront testées.\nRamassez le rond aussi vite que possible, placez-le dans le trou et, après avoir fait cela %1$s fois, retirez le rond %2$s fois. + Touchez Démarrer pour commencer. + Placez le rond dans le trou avec votre main gauche. + Placez le rond dans le trou avec votre main droite. + Placez le rond derrière la ligne avec votre main gauche. + Placez le rond derrière la ligne avec votre main droite. + Ramassez le rond avec deux doigts. + Levez les doigts pour faire tomber le rond. + + Tremblements + Cette activité mesure les tremblements de vos mains dans différentes positions. Trouvez une position assise confortable, que vous garderez pendant toute la durée de ce test. + Tenez votre téléphone dans la main la plus affectée, comme indiqué dans l’image ci-dessous. + Tenez votre téléphone dans la main DROITE, comme indiqué dans l’image ci-dessous. + Tenez votre téléphone dans la main GAUCHE, comme indiqué dans l’image ci-dessous. + Vous serez invité à réaliser %1$s en positon assise, le téléphone dans votre main. + une tâche + deux tâches + trois tâches + quatre tâches + cinq tâches + Touchez Suivant pour continuer. + Préparez-vous à tenir votre téléphone sur vos genoux. + Préparez-vous à tenir votre téléphone sur vos genoux avec votre main GAUCHE. + Préparez-vous à tenir votre téléphone sur vos genoux avec votre main DROITE. + Tenez votre téléphone sur vos genoux pendant %1$d secondes. + Maintenant, tenez votre téléphone à hauteur d’épaule, la main tendue. + Maintenant, tenez votre téléphone avec votre main GAUCHE, à hauteur d’épaule, la main tendue. + Maintenant, tenez votre téléphone avec votre main DROITE, à hauteur d’épaule, la main tendue. + Tenez votre téléphone, la main tendue, pendant %1$d secondes. + Maintenant, tenez votre téléphone à hauteur d’épaule, le coude plié. + Maintenant, tenez votre téléphone avec votre main GAUCHE, à hauteur d’épaule, le coude plié. + Maintenant, tenez votre téléphone avec votre main DROITE, à hauteur d’épaule, le coude plié. + Tenez votre téléphone, le coude plié, pendant %1$d secondes + Maintenant, avec le coude plié, portez votre téléphone à votre nez, plusieurs fois. + Maintenant, avec le coude plié et le téléphone dans votre main GAUCHE, portez votre téléphone à votre nez, plusieurs fois. + Maintenant, avec le coude plié et le téléphone dans votre main DROITE, portez votre téléphone à votre nez, plusieurs fois. + Portez votre téléphone à votre nez, plusieurs fois, pendant %1$d secondes + Préparez-vous à saluer de la main, les doigts serrés les uns contre les autres. + Préparez-vous à saluer de la main GAUCHE, le téléphone en main, les doigts serrés les uns contre les autres. + Préparez-vous à saluer de la main DROITE, le téléphone en main, les doigts serrés les uns contre les autres. + Saluez de la main, les doigts serrés les uns contre les autres, pendant %1$d secondes. + Placez maintenant le téléphone dans votre main GAUCHE et passez à la tâche suivante. + Placez maintenant le téléphone dans votre main DROITE et passez à la tâche suivante. + Passez à la tâche suivante. + Activité terminée. + Vous serez invité à réaliser %1$s en positon assise, le téléphone dans une main, puis dans l’autre. + Je ne peux pas effectuer cette activité avec ma main GAUCHE. + Je ne peux pas effectuer cette activité avec ma main DROITE. + Je peux effectuer cette activité des deux mains. + + Création du fichier impossible + Impossible de supprimer assez de fichiers journaux pour atteindre le seuil + Erreur de définition de l’attribut + Fichier non marqué comme supprimé (non marqué comme chargé) + Plusieurs erreurs lors de la suppression des journaux + Aucune donnée collectée n’a été détectée. + Aucun répertoire de sortie spécifié + + Aucune donnée + + Retour + Illustration de %1$s + Champ de signature indiqué + Signer avec votre doigt sur l’écran + Signé + Non signé + Sélectionné + Désélectionné + Curseur de réponses. De %1$s à %2$s + Image sans étiquette + Commencer la tâche + actif + correct + incorrect + latent + Jeu de mémoire + Aperçu de la capture + Image capturée + Aperçu de la capture vidéo + Vidéo capturée + Impossible de placer un disque de taille %1$s sur un disque de taille %2$s + Cible + Tour + Toucher deux fois pour placer le disque + Toucher deux fois pour sélectionner le disque supérieur + Possède un disque de tailles %1$s + Vide + De %1$s à %2$s + Pile composée de + et + Point : %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-hi/strings.xml b/backbone/src/main/res/values-hi/strings.xml new file mode 100644 index 000000000..ee45c21ed --- /dev/null +++ b/backbone/src/main/res/values-hi/strings.xml @@ -0,0 +1,397 @@ + + + + + सहमति + प्रथम नाम + उपनाम + आवश्यक + समीक्षा + नीचे दिए गए फ़ॉर्म को देखें और जारी रखने के लिए “सहमत” पर टैप करें। + समीक्षा + हस्ताक्षर + कृपया अपनी उँगली का उपयोग करके नीचे दी गई रेखा पर हस्ताक्षर करें। + यहाँ हस्ताक्षर करें + पृष्ठ %1$ld/%2$ld + + स्वागत है + डेटा एकत्रीकरण + गोपनीयता + डेटा उपयोग + सर्वेक्षण अध्ययन + अध्ययन कार्य + समय प्रतिबद्धता + सहमति वापस लेना + और अधिक जानें + + डेटा एकत्रीकरण की प्रक्रिया के बारे में और अधिक जानें + डेटा उपयोग करने की प्रक्रिया के बारे में और अधिक जानें + आपकी गोपनीयता और पहचान की सुरक्षा के बारे में और अधिक जानें + अध्ययन से पहले उसके बारे में और अधिक जानें + सर्वेक्षण अध्ययन के बारे में और अधिक जानें + अपने समय पर अध्ययन के प्रभाव के बारे में और अधिक जानें + सम्मिलित कार्यों के बारे में और अधिक जानें + सहमति वापस लेने के बारे में और अधिक जानें + + साझाकरण विकल्प + मेरे डेटा को %1$s और दुनिया भर के योग्य शोधकर्ताओं के साथ साझा करें + मेरा डेटा केवल %1$s के साथ साझा करें + इस अध्ययन में आपकी सहभागिता से %1$s को आपका अध्ययन डेटा प्राप्त होगा।\n\nकोड किए हुए अध्ययन डेटा का विस्तृत रूप से (आपका नाम दिए बिना) साझाकरण, इस शोध और भविष्य में होने वाले शोधों के लिए उपयोगी हो सकता है। + डेटा साझाकरण के बारे में अधिक जानें + + %1$s का नाम (प्रिंट किया हुआ) + %1$s का हस्ताक्षर + तिथि + + चरण %1$s/%2$s + + अमान्य मान + %1$s अधिकतम अनुमत मान (%2$s) से अधिक है। + %1$s न्यूनतम अनुमत मान (%2$s) से कम है। + %1$s मान्य मान नहीं है। + + अमान्य ईमेल पता : %1$s + + पता दर्ज करें + निर्दिष्ट पता ढूँढा नहीं जा सका + आपके वर्तमान स्थान का पता लगाने में असमर्थ। कृपया कोई पता टाइप करें या लागू होने पर किसी ऐसे स्थान पर जाएँ जहाँ बेहतर GPS सिग्नल हों। + “स्थान सेवाएँ” तक पहुँच को अस्वीकृत किया गया। कृपया सेटिंग्ज़ द्वारा इस ऐप को “स्थान सेवाएँ” का उपयोग करने की अनुमति दें। + दर्ज किए गए पते के लिए कोई परिणाम नहीं मिले। कृपया सुनिश्चित करें कि यह पता मान्य है। + या तो आप इंटरनेट से कनेक्टेड नहीं हैं या आपने पता देखने हेतु अधिकतम अनुमत अनुरोध कर लिए हैं। यदि आप इंटरनेट से कनेक्टेड नहीं हैं, तो इस प्रश्न का जवाब देने के लिए अपने वाई-फ़ाई को चालू करें, यदि स्किप बटन उपलब्ध हो, तो इस प्रश्न को स्किप करें या फिर इंटरनेट से कनेक्ट होने पर इस सर्वेक्षण पर वापस आएँ। + + टेक्स्ट कॉन्टेंट अधिकतम अनुमत सीमा से अधिक है : %1$s + + स्प्लिट स्क्रीन में कैमरा उपलब्ध नहीं है। + + ईमेल + jappleseed@example.com + पासवर्ड + पासवर्ड दर्ज करें + पुष्टि करें + पासवर्ड पुनः दर्ज करें + पासवर्ड मेल नहीं खा रहे हैं। + अतिरिक्त जानकारी + साहिल + मलिक + लिंग + लिंग चुनें + जन्मतिथि + तिथि चुनें + + सत्यापन + अपना ईमेल सत्यापित करें + यदि आपको सत्यापन ईमेल प्राप्त नहीं हुआ है और आप चाहते हैं कि उसे पुनः प्रेषित किया जाए, तो नीचे दिए गए लिंक पर टैप करें। + सत्यापन ईमेल फिर से भेजें + + लॉगइन करें + पासवर्ड भूल गए? + + पासकोड दर्ज करें + पासकोड की पुष्टि करें + पासकोड सहेजा गया + पासकोड प्रमाणित किया गया + अपना पुराना पासकोड दर्ज करें + अपना नया पासकोड दर्ज करें + अपने नए पासकोड की पुष्टि करें + ग़लत पासकोड + पासकोड एक समान नहीं हैं। पुनः प्रयास करें। + कृपया Touch ID से प्रमाणित करें। + Touch ID त्रुटि + केवल न्यूमैरिक वर्णों की अनुमति है। + पासकोड दर्ज करने की प्रक्रिया का संकेतक + %1$s/%2$s मान दर्ज किए गए + पासकोड भूल गए? + + कीचेन आइटम को जोड़ा नहीं जा सका। + कीचेन आइटम को अपडेट नहीं किया जा सका। + कीचेन आइटम को डिलीट नहीं किया जा सका। + कीचेन आइटम को खोजा नहीं जा सका। + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + महिला + पुरूष + अन्य + + नहीं + हाँ + + सें॰मी॰ + फ़ीट + इंच + + उत्तर देने के लिए टैप करें + उत्तर का चयन करें + चयन करने के लिए टैप करें + लिखने के लिए टैप करें + + सहमत + रद्द करें + ठीक + साफ़ करें + असहमत + पूर्ण + आरंभ करें + और अधिक जानें + अगला + छोड़ें + यह प्रश्न छोड़ें + टाइमर प्रारंभ करें + बाद के लिए सहेजें + परिणाम ख़ारिज करें + कार्य समाप्त करें + सहेजें + उत्तर साफ़ करें + इस उत्तर को संशोधित नहीं किया जा सकता है। + + गतिविधि शुरू होने में शेष समय + गतिविधि पूर्ण + आपके डेटा का विश्लेषण किया जाएगा और परिणाम के तैयार होते ही आपको सूचित किया जाएगा। + %1$s सेकंड शेष। + + छवि कैप्चर करें + छवि पुनः कैप्चर करें + कोई कैमरा नहीं मिला। यह चरण पूरा नहीं किया जा सकता है। + इस चरण को पूरा करने के लिए “सेटिंग्ज़” में इस ऐप को कैमरे तक पहुँच प्रदान करें। + कैप्चर की गई छवियों के लिए कोई आउटपुट निर्देशिका निर्दिष्ट नहीं की गई थी। + कैप्चर की गई छवि सहेजी नहीं जा सकी। + + रिकॉर्डिंग शुरू करें + रिकॉर्डिंग रोकें + वीडियो पुनः कैप्चर करें + + तंदुरुस्ती + दूरी (%1$s) + हृदय गति (bpm) + %1$s तक आराम से बैठें। + %1$s तक जितना तेज़ हो सके, चलें। + इस गतिविधि द्वारा आपकी हृदय गति का निरीक्षण किया जाता है और यह मापित किया जाता है कि %1$s में आप कितनी दूर पैदल चल सकते हैं। + बाहर %1$s तक जितनी तेज़ गति से संभव हो, चलें। उसके बाद बैठ जाएँ और आराम से %2$s तक सुस्ताएँ। आरंभ करने के लिए “आरंभ करें” पर टैप करें। + + चाल और संतुलन + इस गतिविधि द्वारा आपके पैदल चलने या खड़े रहने पर आपकी चाल और संतुलन का मापन किया जाता है। यदि आप बिना किसी सहायता के सुरक्षित पैदल नहीं चल सकते हैं, तो इसे जारी न रखें। + ऐसी जगह ढूँढें जहाँ आप बिना किसी सहायता के लगभग %1$d क़दम सीधे चल सकें। + अपने फ़ोन को जेब या बैग में रखें और ऑडियो निर्देशों का अनुसरण करें। + अब %1$s तक स्थिर खड़े रहें। + %1$s तक स्थिर खड़े रहें। + पीछे मुड़ें और जहाँ से आपने शुरूआत की थी, वहाँ वापस चलकर जाएँ। + लगभग %1$d कदम सीधे चलें। + + ऐसी जगह ढूँढें, जहाँ आप एक ओर व दूसरी ओर सुरक्षित रूप से सीधे चल सकें। मार्ग की समाप्ति पर मुड़ें और लगातार चलते रहने का प्रयास करें, जैसे कि आप किसी शंकु के इर्द-गिर्द चल रहे हों।\n\nइसके बाद आपको पूर्ण वृत्त में मुड़ने के लिए, फिर अपनी भुजाओं को फैलाकर और अपने पैरों को लगभग कंधे की चौड़ाई तक फैलाकर स्थिर खड़े रहने के लिए कहा जाएगा। + जब आप शुरू करने के लिए तैयार हों, “आरंभ करें” पर टैप करें।\nफिर अपने फ़ोन को जेब या बैग में रखें और ऑडियो निर्देशों का पालन करें। + %1$s तक पीछे और आगे सीधे चलें। अपनी सामान्य चाल से चलें। + पूर्ण वृत्त में मुड़ें फिर %1$s तक स्थिर खड़े रहें। + आपने गतिविधि को पूर्ण कर लिया है। + + टैप करने की गति + दायॉं हाथ + बायाँ हाथ + इस गतिविधि द्वारा आपकी टैप करने की गति का मापन किया जाता है। + अपना फ़ोन समतल सतह पर रखें। + स्क्रीन पर बटनों पर एक के बाद एक टैप करने के लिए एक ही हाथ की दो उँगलियों का उपयोग करें। + स्क्रीन पर बटनों पर एक के बाद एक टैप करने के लिए दाएँ हाथ की दो उँगलियों का उपयोग करें। + स्क्रीन पर बटनों पर एक के बाद एक टैप करने के लिए बाएँ हाथ की दो उँगलियों का उपयोग करें। + अब अपने दाएँ हाथ का उपयोग करके यही परीक्षण दोहराएँ। + अब अपने बाएँ हाथ का उपयोग करके यही परीक्षण दोहराएँ। + एक उँगली से फिर दूसरी उँगली से टैप करें। अपने टैप का समय जितना संभव हो, एक समान रखने का प्रयास करें। %1$s तक टैप करते रहें। + आरंभ करने के लिए “आरंभ करें” पर टैप करें। + आरंभ करने के लिए “अगला” पर टैप करें। + टैप करें + कुल टैप + बटनों को दो उँगलियों से जितना हो सके, उतने समान समयांतराल पर टैप करें। + अपने दाएँ हाथ से बटनों पर टैप करें। + अपने बाएँ हाथ से बटनों पर टैप करें। + इस हाथ को स्किप करें + + वॉइस + आरंभ करने के लिए “आरंभ करें” पर टैप करें। + जितनी देर तक बोल सकें, माइक्रोफ़ोन में “आह” बोलिए। + गहरी साँस लें और जितनी देर तक बोल सकें, माइक्रोफ़ोन में “आह” बोलें। अपनी बोलने की गति और आवाज़ स्थिर रखें ताकि ऑडियो बारों का रंग नीला बना रहे। + इस गतिविधि द्वारा आपके फ़ोन के नीचे लगे माइक्रोफ़ोन से आपकी आवाज़ को रिकॉर्ड करके उसका मूल्यांकन किया जाता है। + बहुत तेज़ + ऑडियो रिकॉर्ड करने में असमर्थ + पृष्ठभूमि नॉइज़ स्तर की जाँच होने तक कृपया प्रतीक्षा करें। + आस-पास नॉइज़ का स्तर बहुत अधिक होने के कारण आपकी आवाज़ रिकॉर्ड नहीं की जा सकती। कृपया किसी शांत जगह पर जाएँ और पुनः प्रयास करें। + तैयार होने पर “अगला” पर टैप करें। + + टोन ऑडियोमेट्री + यह गतिविधि अलग-अलग प्रकार की ध्वनियाँ सुनने की आपकी क्षमता का आकलन करती है। + आरंभ करने से पहले अपने हेडफ़ोन को प्लग इन करें और कानों पर लगाएं। + आरंभ करने के लिए “आरंभ करें” पर टैप करें। + अब आपको एक टोन सुनाई देनी चाहिए। अपने उपकरण के किनारे दिए गए नियंत्रणों का उपयोग करके वॉल्यूम समायोजित करें।\n\nजब आप आरंभ करने के लिए तैयार हो जाएँ, तो बटन को टैप करें। + आपको जितनी बार भी आवाज़ सुनाई दे, उतनी बार बटन को टैप करें। + %1$s Hz, बाएँ + %1$s Hz, दाएँ + + स्थानिक याददाश्त + %1$s के चमकने के क्रम को दोहराने के लिए कहकर इस गतिविधि द्वारा आपकी अल्पावधि स्थानिक याददाश्त का मापन किया जाता है। + फूलों + फूल + %1$s में से कुछ एक-एक करके चमकेंगे। उन %2$s पर उनके चमकने के क्रम में टैप करें। + %1$s में से कुछ एक-एक करके चमकेंगे। उन %2$s पर उनके चमकने के विपरीत क्रम में टैप करें। + आरंभ करने के लिए “आरंभ करें” पर टैप करें और फिर ध्यान से देखें। + %1$s + स्कोर + %1$s को चमकते हुए देखें + %1$s को उनके चमकने के क्रम में टैप करें + %1$s को विपरीत क्रम में टैप करें + अनुक्रम पूर्ण + जारी रखने के लिए “अगला” पर टैप करें + पुनः प्रयास करें + आप निर्धारित समय में इसे पूरा नहीं कर पाए। जारी रखने के लिए “अगला” पर टैप करें। + समय-समाप्त + आप निर्धारित समय में इसे पूरा नहीं कर पाए।\nजारी रखने के लिए “अगला” पर टैप करें। + गेम पूर्ण हुआ + ठहरा हुआ + जारी रखने के लिए “अगला” पर टैप करें + + प्रतिक्रिया समय + यह गतिविधि किसी दृश्यात्मक संकेत की प्रतिक्रिया देने में आपके द्वारा लिए जाने वाले समय का आकलन करती है। + स्क्रीन पर नीले रंग का बिंदु दिखाई देते ही उपकरण को किसी भी दिशा में हिलाएँ। आपसे %D बार ऐसा करने का अनुरोध किया जाएगा। + आरंभ करने के लिए “आरंभ करें” पर टैप करें। + प्रयास %1$s/%2$s + नीले रंग का वृत्त दिखाई देने पर शीघ्रता से उपकरण को हिलाएँ + + हनोई का टॉवर + इस गतिविधि के द्वारा पहेली को हल करने की आपकी क्षमता का मूल्यांकन किया जाएगा। + पूरे स्टैक को चिह्नांकित प्लेटफ़ॉर्म पर कम से कम प्रयासों में स्थानांतरित करें। + आरंभ करने के लिए “आरंभ करें” पर टैप करें + पहेली हल करें + प्रयासों की संख्या : %1$s \n %2$s + यह पहेली मेरे द्वारा हल नहीं की जा सकती है + + समयबद्ध चाल + इस गतिविधि के द्वारा शरीर के निचले हिस्से के कार्य करने की क्षमता को मापा जाता है। + ऐसी जगह ढूँढें, यदि संभव हो तो घर के बाहर, जहाँ पर आप लगभग %1$s तक जितना संभव हो, उतनी तेज़ी से लेकिन सुरक्षित रूप से सीधे चल सकें। समाप्ति रेखा तक पहुँचने तक गति कम न करें। + आरंभ करने के लिए “अगला” पर टैप करें। + सहायक उपकरण + प्रत्येक परीक्षण हेतु समान सहायक उपकरण का उपयोग करें। + क्या आप टखनों और पैरों को सहारा देने वाला उपकरण पहनते हैं? + क्या आप सहायक उपकरण का उपयोग करते हैं? + उत्तर का चयन करने के लिए यहाँ टैप करें। + कुछ नहीं + एक हाथ के लिए बेंत + एक हाथ के लिए बैसाखी + दोनों हाथों के लिए बेंत + दोनों हाथों के लिए बैसाखी + वॉकर/रोलेटर + सीधी रेखा में लगभग %1$s चलें। + पीछे मुड़ें और जहाँ से आपने शुरूआत की थी, वहाँ वापस चलकर जाएँ। + पूरा हो जाने पर “पूर्ण” पर टैप करें। + + PASAT + PVSAT + PAVSAT + पेस्ड ऑडिटरी सीरियल ऐडीशन टेस्ट के द्वारा सुनकर जानकारी संसाधित करने की आपकी गति और परिकलन क्षमता को मापा जाता है। + पेस्ड विजुअल सीरियल ऐडीशन टेस्ट के द्वारा देखकर जानकारी संसाधित करने की आपकी गति और परिकलन क्षमता को मापा जाता है। + पेस्ड ऑडिटरी एंड विजुअल सीरियल ऐडीशन टेस्ट के द्वारा देख और सुनकर जानकारी संसाधित करने की आपकी गति और परिकलन क्षमता को मापा जाता है। + एकल अंक प्रति %1$s सेकंड में प्रस्तुत किए जाते हैं।\nआपको प्रत्येक नए अंक का उससे ठीक पहले वाले अंक से योग करना है।\nध्यान रखें, आपको कुल योग परिकलित नहीं करना है बल्कि केवल अंतिम दो अंकों का योग परिकलित करना है। + आरंभ करने के लिए “आरंभ करें” पर टैप करें। + इस पहले अंक को याद रखें। + इस नए अंक का पिछले अंक में योग करें। + - + + %1$s-होल पेग परीक्षण + छिद्र में पेग को डालने के लिए कहकर इस गतिविधि द्वारा आपके अपर एक्सट्रेमिटी फ़ंक्शन का मापन किया जाता है। आपसे %1$s बार ऐसा करने का अनुरोध किया जाएगा। + आपके बाएँ और दाएँ, दोनों हाथों का परीक्षण किया जाएगा।\nआपको जितनी जल्दी हो सके, पेग को लेकर छिद्र में डालना होगा और %1$s बार करने के बाद %2$s बार पुनः उसे वहाँ से हटाना होगा। + आपके दाएँ और बाएँ, दोनों हाथों का परीक्षण किया जाएगा।\nआपको जितनी जल्दी हो सके, पेग को लेकर छिद्र में डालना होगा और %1$s बार करने के बाद %2$s बार पुनः उसे वहाँ से हटाना होगा। + आरंभ करने के लिए “आरंभ करें” पर टैप करें। + अपने बाएँ हाथ से पेग को छिद्र में डालें। + अपने दाएँ हाथ से पेग को छिद्र में डालें। + अपने बाएँ हाथ से पेग को रेखा के पीछे करें। + अपने दाएँ हाथ से पेग को रेखा के पीछे करें। + दो उँगलियों से पेग को उठाएँ। + पेग को डालने के लिए उँगलियों को उठाएँ। + + कंपन गतिविधि + इस गतिविधि से विभिन्न स्थितियों में आपके हाथों के कंपन को मापा जाता है। ऐसी जगह ढूँढें जहाँ आप इस गतिविधि की अवधि के दौरान आराम से बैठ सकें। + नीचे दी गई छवि में दिखाए अनुसार फ़ोन को अपने अधिक प्रभावित हाथ में पकड़ें। + नीचे दी गई छवि में दिखाए अनुसार फ़ोन को अपने दाएँ हाथ में पकड़ें। + नीचे दी गई छवि में दिखाए अनुसार फ़ोन को अपने बाएँ हाथ में पकड़ें। + बैठे रहकर फ़ोन को अपने हाथ में पकड़कर आपसे %1$s करने के लिए कहा जाएगा। + कार्य + दो कार्य + तीन कार्य + चार कार्य + पाँच कार्य + अागे बढ़ने के लिए “अगला” पर टैप करें। + अपने फ़ोन को अपनी गोद में पकड़ें। + बाएँ हाथ से अपने फ़ोन को अपनी गोद में पकड़ें। + दाएँ हाथ से अपने फ़ोन को अपनी गोद में पकड़ें। + अपने फ़ोन को अपनी गोद में %1$d सेकंड तक पकड़े रखें। + अब अपने हाथ को कंधे की ऊँचाई तक फैलाकर अपने फ़ोन को पकड़ें। + अब अपने बाएँ हाथ को कंधे की ऊँचाई तक फैलाकर अपने फ़ोन को पकड़ें। + अब अपने दाएँ हाथ को कंधे की ऊँचाई तक फैलाकर अपने फ़ोन को पकड़ें। + हाथ फैलाकर अपने फ़ोन को %1$d सेकंड तक पकड़े रखें। + अब अपनी कोहनी मोड़कर अपने फ़ोन को कंधे की ऊंचाई पर पकड़ें। + अब अपने बाएँ हाथ की कोहनी मोड़कर कंधे की ऊँचाई पर अपने फ़ोन को पकड़ें। + अब अपने दाएँ हाथ की कोहनी मोड़कर कंधे की ऊँचाई पर अपने फ़ोन को पकड़ें। + अपनी कोहनी मोड़कर अपने फ़ोन को %1$d सेकंड तक पकड़े रखें + अब अपनी कोहनी मुड़ी हुई रखकर अपने फ़ोन को बार-बार अपनी नाक से स्पर्श करें। + अब अपने बाएँ हाथ में फ़ोन पकड़कर और कोहनी मुड़ी हुई रखकर अपने फ़ोन को बार-बार अपनी नाक से स्पर्श करें। + अब अपने दाएँ हाथ में फ़ोन पकड़कर और कोहनी मुड़ी हुई रखकर अपने फ़ोन को बार-बार अपनी नाक से स्पर्श करें। + अपने फ़ोन को अपनी नाक से %1$d सेकंड तक स्पर्श करते रहें + रानी की तरह हाथ हिलाने (अपनी कलाई को मोड़कर हाथ हिलाना) की तैयारी करें। + अपने फ़ोन को अपने बाएँ हाथ में रखकर रानी की तरह हाथ हिलाने (अपनी कलाई को मोड़कर हाथ हिलाना) की तैयारी करें। + अपने फ़ोन को अपने दाएँ हाथ में रखकर रानी की तरह हाथ हिलाने (अपनी कलाई को मोड़कर हाथ हिलाना) की तैयारी करें। + %1$d सेकंड तक रानी की तरह हाथ हिलाते रहें। + अब अपने फ़ोन को अपने बाएँ हाथ में लें और अगला कार्य जारी रखें। + अब अपने फ़ोन को अपने दाएँ हाथ में लें और अगला कार्य जारी रखें। + अगला कार्य जारी रखें। + गतिविधि पूर्ण। + बैठे रहकर फ़ोन को पहले एक हाथ में फिर दूसरे हाथ में पकड़कर आपसे %1$s करने के लिए कहा जाएगा। + इस गतिविधि को मेरे बाएँ हाथ से नहीं किया जा सकता है। + इस गतिविधि को मेरे दाएँ हाथ से नहीं किया जा सकता है। + इस गतिविधि को मेरे दोनों हाथों से किया जा सकता है। + + फ़ाइल नहीं बन सकी + अधिकतम संग्रहण सीमा तक पहुँचने के लिए आवश्यक लॉग फ़ाइलें नहीं हटाई जा सकीं + विशेषता सेट करने में त्रुटि + फ़ाइल डिलीट की गई (अपलोड की गई) के रूप में चिन्हित नहीं है + लॉग हटाने में कई त्रुटियाँ + कोई संग्रहित डेटा नहीं मिला। + कोई आउटपुट निर्देशिका निर्दिष्ट नहीं + + कोई डेटा नहीं + + वापस + %1$s का चित्रांकन + निर्दिष्ट हस्ताक्षर फ़ील्ड + हस्ताक्षर करने के लिए स्क्रीन को स्पर्श करें और अपनी उँगली हिलाएँ + हस्ताक्षरित + अहस्ताक्षरित + चयनित + अचयनित + प्रतिक्रिया स्लाइडर। %1$s से %2$s तक सीमा + बिना लेबल की छवि + कार्य आरंभ करें + सक्रिय + सही + ग़लत + निष्क्रिय + मेमोरी गेम टाइटल + पूर्वावलोकन को कैप्चर करें + कैप्चर की हुई छवि + वीडियो कैप्चर पूर्वावलोकन + कैप्चर किया गया वीडियो + %2$s आकार के डिस्क पर %1$s आकार के डिस्क को रखने में असमर्थ + लक्ष्य + टॉवर + डिस्क रखने के लिए डबल टैप करें + सबसे ऊपर के डिस्क का चयन करने के लिए डबल टैप करें + इसमें %1$s आकारों के डिस्क हैं + ख़ाली + %1$s से %2$s तक श्रेणी + निम्नलिखित का संग्रह + और + पॉइंट : %1$d + + + \ No newline at end of file diff --git a/backbone/src/main/res/values-hr/strings.xml b/backbone/src/main/res/values-hr/strings.xml new file mode 100644 index 000000000..31da9104b --- /dev/null +++ b/backbone/src/main/res/values-hr/strings.xml @@ -0,0 +1,396 @@ + + + + + Pristanak + Ime + Prezime + Potrebno + Pregled informacija + Pregledajte obrazac ispod i dodirnite \"Slažem se\" ako ste spremni za nastavak. + Pregled informacija + Potpis + Molimo, potpišite se prstom na crtu ispod. + Potpišite se ovdje + Stranica %1$ld od %2$ld + + Dobrodošli + Prikupljanje podataka + Privatnost + Uporaba podataka + Istraživačka anketa + Zadaci istraživanja + Potrebno vrijeme + Povlačenje pristanka + Saznajte više + + Saznajte više o načinu prikupljanja podataka + Saznajte više o načinu uporabe podataka + Saznajte više o načinu zaštite vaše privatnosti i identiteta + Saznajte više o istraživanju + Saznajte više o istraživačkoj anketi + Saznajte kako će istraživanje utjecati na vaše vrijeme + Saznajte više o zadacima + Saznajte više o povlačenju pristanka + + Opcije dijeljenja + Dijeli moje podatke sa sveučilištem %1$s i kvalificiranim istraživačima diljem svijeta + Dijeli moje podatke samo sa sveučilištem %1$s + %1$s će primiti vaše istraživačke podatke od sudjelovanja u ovom istraživanju.\n\nMožete pomoći ovom i budućim istraživanjima tako da podijelite vaše kodirane istraživačke podatke na širi način (bez informacija kao što je vaše ime). + Saznajte više o dijeljenju podataka + + %1$s: ime (tiskano) + %1$s: potpis + Datum + + Korak %1$s od %2$s + + Nevažeća vrijednost + %1$s prekoračuje maksimalnu dozvoljenu vrijednost (%2$s). + %1$s je manje od minimalne dozvoljene vrijednosti (%2$s). + %1$s nije važeća vrijednost. + + Nevažeća e-mail adresa: %1$s + + Unesite adresu + Navedena adresa nije pronađena + Nije moguće utvrditi vašu trenutnu lokaciju. Molimo unesite adresu ili se premjestite na lokaciju s boljim GPS signalom, ako je primjenjivo. + Odbijen je pristup lokacijskim uslugama. Molimo dajte ovoj aplikaciji dopuštenje da koristi lokacijske usluge putem Postavki. + Nije moguće naći rezultat za unesenu adresu. Molimo provjerite je li adresa valjana. + Niste spojeni na internet ili ste prekoračili maksimalan broj zahtjeva za traženje adresa. Ako niste spojeni na internet, uključite Wi-Fi kako biste odgovorili na ovo pitanje, preskočite pitanje ako je dostupna tipka za preskakanje ili se vratite na anketu kad ste spojeni na internet. U suprotnom, probajte ponovno za nekoliko minuta. + + Tekstualni sadržaj prekoračuje maksimalnu duljinu: %1$s + + Kamera nije dostupna u razdvojenom zaslonu. + + Pošaljite e-mail + ihorvat@example.com + Lozinka + Unesite lozinku + Potvrdi + Ponovno unesite lozinku + Lozinke nisu iste. + Dodatne informacije + Ivan + Horvat + Spol + Izaberite rod + Datum rođenja + Izaberite datum + + Potvrda + Potvrdite vaš e-mail + Ako niste primili e-mail za potvrdu i želite da bude ponovno poslan, dodirnite link ispod. + Ponovno pošalji e-mail za provjeru + + Prijava + Zaboravili ste lozinku? + + Unesite šifru + Potvrdite šifru + Šifra je spremljena + Šifra je autorizirana + Unesite staru šifru + Unesite novu šifru + Potvrdite vašu novu šifru + Netočna šifra + Šifre nisu iste. Pokušajte ponovno. + Molimo autorizirajte pomoću Touch ID-a + Greška Touch ID-a + Dopušteni su samo numerički znakovi. + Indikator napretka unošenja šifre + %1$s od %2$s unesenih znamenki + Zaboravili ste šifru? + + Nije moguće dodati stavku privjeska ključeva. + Nije moguće ažurirati stavku privjeska ključeva. + Nije moguće obrisati stavku privjeska ključeva. + Nije moguće pronaći stavku privjeska ključeva. + + A+ + A- + AB+ + AB- + B+ + B- + 0+ + 0- + + Žensko + Muško + Ostalo + + Ne + Da + + cm + ft + u + + Dodirnite za odgovaranje + Odaberite odgovor + Dodirnite za odabir + Dodirnite za pisanje + + Slažem se + Poništi + OK + Očisti + Ne slažem se + OK + Pokreni + Saznajte više + Sljedeće + Preskoči + Preskoči ovo pitanje + Pokreni brojač + Spremi za kasnije + Odbaci rezultate + Završi zadatak + Spremi + Očisti odgovor + Ovaj odgovor se ne može promijeniti. + + Započinjanje aktivnosti za + Aktivnost je dovršena + Vaši podaci će se analizirati i bit ćete obaviješteni kad rezultati budu spremni. + %1$s s preostalo. + + Snimi sliku + Ponovno snimi sliku + Kamera nije pronađena.\u0020\u0020Korak se ne može dovršiti. + Za dovršetak ovog koraka, u Postavkama dopustite aplikaciji pristup kameri. + Nije naveden izlazni direktorij za snimljene slike. + Snimljena slika ne može se spremiti. + + Započni snimati + Zaustavi snimanje + Ponovno snimi video + + Kondicija + Udaljenost (%1$s) + Puls u minuti + Sjedite udobno tijekom %1$s. + Hodajte što brže možete tijekom %1$s. + Ova aktivnost prati vašu brzinu otkucaja srca i mjeri koliko možete prehodati unutar %1$s. + Hodajte na otvorenom najbrže što možete tijekom %1$s. Kad završite, sjednite i odmarajte tijekom %2$s. Za početak, dodirnite Pokreni. + + Položaj tijela i ravnoteža + Ova aktivnost mjeri vaš položaj tijela i ravnotežu dok hodate i stojite mirno. Nemojte nastavljati ako ne možete sigurno hodati bez pomoći. + Pronađite mjesto na kojem možete sigurno hodati približno %1$d koraka u ravnoj liniji. + Stavite vaš telefon u džep ili torbu i slijedite audio upute. + Sada stojite mirno %1$s. + Stojite mirno %1$s. + Okrenite se i hodajte natrag do mjesta od kojeg ste krenuli. + Hodajte najviše %1$d koraka u ravnoj liniji. + + Pronađite mjesto na kojem možete sigurno hodati naprijed-natrag u ravnoj liniji. Pokušajte kontinuirano hodati tako da se okrenete na kraju putanje, kao da hodate oko čunja.\n\nZatim će se od vas zatražiti da hodate punim krugom, zatim da stojite nepomično s rukama položenim uz tijelo i stopalima razmaknutima u širini ramena. + Dodirnite Pokreni kad budete spremni započeti.\nZatim stavite vaš telefon u džep ili torbu i slijedite audio upute. + Hodajte naprijed-natrag u ravnoj liniji tijekom %1$s. Hodajte kao i inače. + Okrenite se punim krugom i zatim budite mirni %1$s. + Dovršili ste aktivnost. + + Brzina dodirivanja + Desna ruka + Lijeva ruka + Ova aktivnost mjeri vašu brzinu dodirivanja. + Stavite telefon na ravnu podlogu. + Koristeći dva prsta iste ruke, naizmjence dodirujte tipke na zaslonu. + Koristeći dva prsta desne ruke, naizmjence dodirujte tipke na zaslonu. + Koristeći dva prsta lijeve ruke, naizmjence dodirujte tipke na zaslonu. + Sad ponovite isti test desnom rukom. + Sad ponovite isti test lijevom rukom. + Dodirnite jednim prstom, zatim drugim. Pokušajte tempirati dodire kako bi bili ujednačeni. Nastavite dodirivati tijekom %1$s. + Dodirnite Pokreni za početak. + Dodirnite Sljedeće za početak. + Dodirnite + Ukupan broj dodira + Dodirujte tipke što ujednačenije možete koristeći dva prsta. + Dodirujte tipke koristeći DESNU ruku. + Dodirujte tipke koristeći LIJEVU ruku. + Preskoči ovu ruku + + Glas + Dodirnite Pokreni za početak. + Izgovarajte \"Aaaaaa\" u mikrofon što dulje možete. + Duboko udahnite i izgovorite \"Aaaaaa\" u mikrofon što dulje možete. Održavajte ujednačenu glasnoću glasa tako da audio stupci ostanu plavi. + Ova aktivnost evaluira vaš glas tako što ga snima pomoću mikrofona na dnu vašeg telefona. + Preglasno + Zvuk se ne može snimati + Molimo, pričekajte da provjerimo razinu pozadinske buke. + Razina buke u okruženju je previsoka za snimanje vašeg glasa. Molimo, premjestite se negdje gdje je tiše i pokušajte ponovno. + Dodirnite Sljedeće kad budete spremni. + + Tonalna audiometrija + Ovom se aktivnošću mjeri vaša sposobnost da čujete različite zvukove. + Prije početka priključite i stavite slušalice. + Dodirnite Pokreni za početak. + Sada biste trebali čuti ton. Podesite glasnoću uz pomoć kontrola s bočne strane uređaja.\n\nDodirnite tipku kada ste spremni za početak. + Dodirnite tipku svaki put kada začujete zvuk. + %1$s Hz, lijevo + %1$s Hz, desno + + Prostorna memorija + Ova aktivnost mjeri vašu kratkotrajnu prostornu memoriju tako što traži da dodirnete %1$s redoslijedom kojim su zasvijetlili. + cvjetove + cvjetovi + Gledajte %1$s kako će zasvijetliti jedan za drugim. Dodirnite te %2$s istim redoslijedom kojim su zasvijetlili. + Gledajte %1$s kako će zasvijetliti jedan za drugim. Dodirnite te %2$s obrnutim redoslijedom od onog kojim su zasvijetlili. + Za početak, dodirnite Pokreni, zatim gledajte pažljivo. + %1$s + Rezultat + Gledajte %1$s kako svijetle + Dodirnite %1$s redoslijedom kojim su zasvijetlili + Dodirnite %1$s obrnutim redoslijedom + Niz je dovršen + Za nastavak, dodirnite Sljedeće + Pokušaj ponovno + Ovaj put niste uspjeli. Dodirnite Sljedeće za nastavak. + Vrijeme je isteklo + Ponestalo vam je vremena.\nDodirnite Sljedeće za nastavak. + Igra je dovršena + Pauzirano + Za nastavak, dodirnite Sljedeće + + Vrijeme reakcije + Ovom se aktivnošću procjenjuje koliko vam je vremena potrebno za reakciju na vizualni znak. + Protresite uređaj u bilo kojem smjeru čim se na zaslonu pojavi plava točka. Od vas će se zatražiti da to učinite %D puta. + Dodirnite Pokreni za početak. + Pokušaj %1$s od %2$s + Kada se pojavi plavi krug, brzo protresite uređaj + + Hanojski toranj + Ova aktivnost procjenjuje vaše vještine rješavanja slagalica. + Premjestite cijeli stog na označenu platformu u što je moguće manje poteza. + Dodirnite Pokreni za početak + Riješite slagalicu + Broj poteza: %1$s \n %2$s + Ne mogu riješiti ovu slagalicu + + Tempirana šetnja + Ova aktivnost mjeri funkcionalnost vaših donjih ekstremiteta. + Pronađite mjesto, po mogućnosti vani, gdje možete hodati otprilike %1$s u ravnoj liniji što brže možete, ali sigurno. Nemojte usporavati sve dok ne prijeđete ciljnu liniju. + Dodirnite Sljedeće za početak. + Pomagalo + Koristite isto pomagalo za svaki test. + Nosite li ortozu za gležanj i stopalo? + Koristite li pomagalo? + Dodirnite ovdje za odabir odgovora. + Nijedno + Jedan štap za hodanje + Jedna štaka + Dva štapa za hodanje + Dvije štake + Hodalica/Guralica + Hodajte najviše %1$s u ravnoj liniji. + Okrenite se i hodajte natrag do mjesta od kojeg ste krenuli. + Dodirnite OK kad završite. + + PASAT + PVSAT + PAVSAT + Auditivni test tempiranog serijskog zbrajanja (PASAT) mjeri brzinu vaše obrade auditivnih podataka te sposobnost računanja. + Vizualni test tempiranog serijskog zbrajanja mjeri (PVSAT) brzinu vaše obrade vizualnih podataka te sposobnost računanja. + Auditivni i vizualni test tempiranog serijskog zbrajanja (PAVSAT) mjeri brzinu vaše obrade auditivnih i vizualnih podataka te sposobnost računanja. + Jednoznamenkasti brojevi prikazuju se svakih %1$s s.\nSvaku novu znamenku morate zbrojiti s onom koja je neposredno prije nje.\nPažnja, ne smijete izračunavati ukupni iznos, nego samo zbrojiti zadnja dva broja. + Dodirnite Pokreni za početak. + Zapamtite prvu znamenku. + Zbrojite ovu novu znamenku s prethodnom. + - + + Test s %1$s rupa i klinovima + Ova aktivnost mjeri funkciju vaših gornjih ekstremiteta tako što traži da stavite klin u rupu. Od vas će se zatražiti da to učinite %1$s puta. + Testirat će vam se i lijeva i desna ruka.\nMorate uhvatiti klin što brže možete, staviti ga u rupu i kad to učinite %1$s puta, ponovno ga ukloniti %2$s puta. + Testirat će vam se i desna i lijeva ruka.\nMorate uhvatiti klin što brže možete, staviti ga u rupu i kad to učinite %1$s puta, ponovno ga ukloniti %2$s puta. + Dodirnite Pokreni za početak. + Stavite klin u rupu koristeći se lijevom rukom. + Stavite klin u rupu koristeći se desnom rukom. + Stavite klin iza crte koristeći se lijevom rukom. + Stavite klin iza crte koristeći se desnom rukom. + Podignite klin s dva prsta. + Podignite prste da biste ispustili klin. + + Aktivnost podrhtavanja + Ova aktivnost mjeri podrhtavanje vaših ruku u različitim položajima. Pronađite mjesto na kojem možete udobno sjediti tijekom trajanja ove aktivnosti. + Držite telefon u ruci koja vam je više zahvaćena, kako je prikazano na slici ispod. + Držite telefon u DESNOJ ruci kako je prikazano na slici ispod. + Držite telefon u LIJEVOJ ruci kako je prikazano na slici ispod. + Od vas će se zatražiti da napravite %1$s dok sjedite s telefonom u ruci. + zadatak + dva zadatka + tri zadatka + četiri zadatka + pet zadataka + Dodirnite Sljedeće za nastavak. + Pripremite se na držanje telefona u krilu. + Pripremite se na držanje telefona u krilu LIJEVOM rukom. + Pripremite se na držanje telefona u krilu DESNOM rukom. + Nastavite držati telefon u krilu tijekom %1$d s. + Sad držite telefon s ispruženom rukom u visini ramena. + Sad držite telefon u LIJEVOJ šaci s rukom ispruženom u visini ramena. + Sad držite telefon u DESNOJ šaci s rukom ispruženom u visini ramena. + Nastavite držati telefon s ispruženom rukom tijekom %1$d s. + Sad držite telefon u visini ramena sa savijenim laktom. + Sad držite telefon LIJEVOM rukom u visini ramena sa savijenim laktom. + Sad držite telefon DESNOM rukom u visini ramena sa savijenim laktom. + Nastavite držati telefon sa savijenim laktom tijekom %1$d s + Sad držite lakat savijen i neprekidno dodirujte nos telefonom. + Sad držite lakat savijen s telefonom u LIJEVOJ ruci i neprekidno dodirujte nos telefonom. + Sad držite lakat savijen s telefonom u DESNOJ ruci i neprekidno dodirujte nos telefonom. + Nastavite dodirivati nos telefonom tijekom %1$d s + Pripremite se za mahanje rotiranjem šake u zapešću (kao što maše engleska kraljica). + Pripremite se za mahanje rotiranjem šake, s telefonom u LIJEVOJ ruci (kao što maše engleska kraljica). + Pripremite se za mahanje rotiranjem šake, s telefonom u DESNOJ ruci (kao što maše engleska kraljica). + Nastavite mahati rotiranjem šake tijekom %1$d s. + Sad uzmite telefon u LIJEVU ruku i nastavite na sljedeći zadatak. + Sad uzmite telefon u DESNU ruku i nastavite na sljedeći zadatak. + Nastavite na sljedeći zadatak. + Aktivnost je dovršena. + Od vas će se zatražiti da napravite %1$s dok sjedite s telefonom prvo u jednoj ruci, zatim ponovno u drugoj ruci. + Ne mogu obaviti ovaj zadatak LIJEVOM rukom. + Ne mogu obaviti ovaj zadatak DESNOM rukom. + Mogu obaviti ovaj zadatak s obje ruke. + + Datoteka se ne može izraditi + Nije se moglo ukloniti dovoljno log datoteka za dosezanje praga + Greška prilikom podešavanja atributa + Datoteka nije označena kao obrisana (nije označena kao postavljena) + Više grešaka prilikom uklanjanja log zapisa + Nisu pronađeni prikupljeni podaci. + Izlazni direktorij nije naveden + + Bez podataka + + Natrag + Slika: %1$s + Polje za potpis + Dodirnite zaslon i pomičite prst za potpisivanje + Potpisano + Nepotpisano + Odabrano + Nije odabrano + Kliznik odgovora. Raspon od %1$s do %2$s + Neoznačena slika + Započni zadatak + aktivno + točno + netočno + neaktivno + Igra memorije s pločicama + Snimi pregled + Snimljena slika + Pregled snimanja videa + Snimljeni video + Pločica veličine %1$s ne može se staviti na pločicu veličine %2$s + Cilj + Toranj + Dodirnite dvaput za stavljanje pločice + Dodirnite dvaput za odabir najviše pločice + Ima pločicu veličine %1$s + Isprazni + Raspon od %1$s do %2$s. + Stog sastavljen od + i + Točka: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-hu/strings.xml b/backbone/src/main/res/values-hu/strings.xml new file mode 100644 index 000000000..57896437a --- /dev/null +++ b/backbone/src/main/res/values-hu/strings.xml @@ -0,0 +1,396 @@ + + + + + Beleegyezés + Utónév + Családi név + Szükséges + Áttekintés + Tekintse át az alábbi űrlapot, majd koppintson az Elfogadom gombra, ha készen áll a folytatásra. + Áttekintés + Aláírás + Kérjük, írja alá az ujjával az alábbi vonalon. + Itt írja alá + %2$ld/%1$ld oldal + + Üdvözöljük + Adatgyűjtés + Adatvédelem + Adathasználat + Kérdőív + A vizsgálat feladatai + Szükséges idő + Visszavonás + További infók + + További információk az adatok gyűjtésének módjáról + További információk az adatok felhasználásának módjáról + További információk az adatok és személyes információk védelmének módjáról + További információk a vizsgálatról + További információk a kérdőívről + További információk arról, mennyi időt vesz igénybe a felmérés + További információk a bennfoglalt feladatokról + További információk a visszavonásról + + Megosztási beállítások + Az adataim megosztása a következővel és világszerte szakképzett kutatókkal: %1$s + Az adataim megosztása csak a következővel: %1$s + A(z) %1$s meg fogja kapni az Ön vizsgálati adatait az Ön vizsgálatban való részvételéről.\n\nA kódolt vizsgálati adatok szélesebb körű megosztása (az olyan információk nélkül, mint az Ön neve) segítheti ezt és a jövőbeli kutatásokat. + További infó az adatok megosztásáról + + %1$s neve (kinyomtatva) + %1$s aláírása + Dátum + + %1$s/%2$s. lépés + + Érvénytelen érték + A(z) %1$s túllépi a maximálisan engedélyezett értéket (%2$s). + A(z) %1$s kisebb a minimálisan engedélyezett értéknél (%2$s). + A(z) %1$s nem érvényes érték. + + Érvénytelen e-mail cím: %1$s + + Adjon meg egy címet + A megadott cím nem található + Az aktuális hely feloldása nem sikerült. Írjon be egy címet, vagy menjen olyan helyre, ahol a GPS-jelerősség kedvezőbb. + A helymeghatározáshoz való hozzáférés le van tiltva. Engedélyezze az alkalmazás számára a helymeghatározás funkció használatát a Beállításokban. + A megadott címhez nem található eredmény. Győződjön meg arról, hogy a cím érvényes. + Ön nem csatlakozik az internethez, vagy túllépte a címkeresési kérelmek maximálisan engedélyezett számát. Ha nem csatlakozik az internethez, akkor kapcsolja be a Wi-Fi-t és válaszoljon erre a kérdésre, ha a kihagyás gomb megjelenik, akkor hagyja ki ezt a kérdést, vagy térjen vissza a kérdőívre, amikor már csatlakozik az internethez. Ellenkező esetben próbálkozzon újra néhány perc múlva. + + A szöveg túllépi a maximális hosszt: %1$s + + A kamera nem érhető el osztott képernyős nézetben. + + E-mail + nagyede@example.com + Jelszó + Jelszó megadása + Megerősítés + Jelszó ismételt megadása + A jelszavak nem egyeznek meg. + További információk + Ede + Nagy + Nem + Nem kiválasztása + Születési dátum + Dátum kiválasztása + + Ellenőrzés + E-mail ellenőrzése + Koppintson az alábbi linkre, ha nem kapott ellenőrző e-mailt, és azt szeretné, ha újra elküldenénk. + Ellenőrző e-mail újraküldése + + Bejelentkezés + Elfelejtette a jelszót? + + Adja meg a jelkódot + Erősítse meg a jelkódját + A jelkód mentve + A jelkód hitelesítve van + Adja meg a régi jelkódját + Adja meg az új jelkódját + Erősítse meg az új jelkódját + A jelkód helytelen + A jelkódok nem egyeznek meg. Próbálja újra. + Kérjük, végezzen hitelesítést a Touch ID-val + Touch ID-val kapcsolatos hiba + Kizárólag számkarakterek engedélyezettek. + Jelkóddal történő belépés folyamatjelzője + %1$s/%2$s számjegy megadva + Elfelejtette a jelkódot? + + Nem lehetett hozzáadni a Kulcskarika-elemet. + Nem lehetett frissíteni a Kulcskarika-elemet. + Nem lehetett törölni a Kulcskarika-elemet. + A Kulcskarika-elem nem található. + + A+ + A- + AB+ + AB- + B+ + B- + 0+ + 0- + + + Férfi + Egyéb + + Nem + Igen + + cm + ft + in + + Koppintson a válaszhoz + Válasz kiválasztása + Koppintson a kijelöléshez + Koppintson az íráshoz. + + Elfogadom + Mégsem + OK + Törlés + Nem fogadom el + Kész + Kezdés + További infók + Következő + Kihagyás + A kérdés kihagyása + Időzítő indítása + Mentés későbbre + Eredmények elvetése + Feladat befejezése + Mentés + Válasz törlése + Ez a válasz nem módosítható. + + Tevékenység indítása: + Tevékenység befejezve + Az adatok ki lesznek elemezve, és értesítést fog kapni, ha az eredmény elkészült. + %1$s másodperc van hátra. + + Kép készítése + Kép készítése újra + Nem található kamera. Ezt a lépést nem lehet végrehajtani. + A lépés végrehajtásához a Beállításokban engedélyezze az alkalmazás számára, hogy hozzáférjen a kamerához. + Az elkészített képekhez nem lett megadva kimeneti könyvtár. + Az elkészített képet nem sikerült menteni. + + Felvétel indítása + Felvétel leállítása + Videó ismételt rögzítése + + Fitnesz + Távolság (%1$s) + Pulzusszám (bpm) + Üljön kényelmesen %1$s időtartamig. + Gyalogoljon olayn gyorsan, amilyen gyorsan tud %1$s időtartam alatt. + Ez a tevékenység a pulzusát figyeli, és megméri, milyen messzire tud gyalogolni %1$s időtartam alatt. + Kültéren gyalogoljon olyan gyorsan, ahogy csak tud %1$s időtartamig. Amikor befejezte, üljön le, és pihenjen kényelmesen %2$s időtartamig. A kezdéshez koppintson a Kezdés elemre. + + Testtartás és egyensúly + Ez a tevékenység a testtartását és egyensúlyát méri járás és állás közben. Ne folytassa, ha segítség nélkül nem tud biztonságosan gyalogolni. + Keressen olyan helyet, ahol segítség nélkül gyalogolhat egyenesen körülbelül %1$d lépést. + Tegye a telefont a zsebébe vagy a táskájába, és kövesse a hangutasításokat. + Most álljon mozdulatlanul %1$s időtartamig. + Álljon mozdulatlanul %1$s időtartamig. + Forduljon meg, és menjen vissza a kiindulási ponthoz. + Tegyen meg legalább %1$d lépést egyenesen. + + Keressen egy olyan helyet, ahol egyenes vonalban tud oda-vissza gyalogolni. Próbáljon meg folyamatosan gyalogolni, és a végeken úgy forduljon meg, mintha egy útjelző bóját kerülne meg.\n\nEzután utasítást fog kapni, hogy fogduljon meg egy teljes kört megtéve, majd álljon meg, engedje le a törzse mellett karjait, és álljon vállszélességű terpeszben. + Koppintson a Kezdés elemre, amikor készen áll.\nEzután tegye a telefont a zsebébe vagy a táskájába, és kövesse a hangutasításokat. + Gyalogoljon oda-vissza egy egyenes vonal mentén ennyi ideig: %1$s. A szokásos tempójában gyalogoljon. + Forduljon meg egy teljes kör mentén, és álljon mozdulatlanul ennyi ideig: %1$s. + Befejezte a tevékenységet. + + Koppintási sebesség + Jobb kéz + Bal kéz + Ez a tevékenység megméri a koppintási sebességét. + Tegye a telefont egy vízszintes felületre. + Ugyanannak a kezének két ujjával felváltva koppintson a gombokra a kijelzőn. + A jobb kezének két ujjával felváltva koppintson a gombokra a kijelzőn. + A bal kezének két ujjával felváltva koppintson a gombokra a kijelzőn. + Most ismételje meg ugyanezt a tesztet jobb kézzel. + Most ismételje meg ugyanezt a tesztet bal kézzel. + Koppintson az egyik, majd a másik ujjával. Próbáljon meg egyenletes időközönként koppintani. Folytassa a koppintásokat ennyi ideig: %1$s. + A kezdéshez koppintson a Kezdés elemre. + Koppintson\na Következő elemre\na kezdéshez. + Koppintás + Összes koppintás + Két ujjal koppintson a gombokra olyan egyenletesen, ahogy csak tud. + Koppintson a gombokra JOBB kézzel. + Koppintson a gombokra BAL kézzel. + Kéz kihagyása + + Hang + A kezdéshez koppintson a Kezdés elemre. + Amilyen hosszan csak tudja, mondja, hogy „Áááááá” a mikrofonba. + Vegyen mély levegőt, és amilyen hosszan csak tudja, mondja a mikrofonba, hogy „Áááááá”. Próbáljon egyenletes hangerőt tartani, hogy a hangsávok kékek maradjanak. + Ez a tevékenység kiértékeli a hangját, rögzítve, ahogy a telefon alján lévő mikrofonba beszél. + Túl hangos + Nem sikerült hangot felvenni + Kérjük, várjon, amíg ellenőrizzük a háttérzaj szintjét. + A környezeti zaj szinte túl magas a hangja felvételéhez. Kérjük, menjen csendesebb helyre, és próbálkozzon újra. + Koppintson a Következő elemre, amikor készen áll. + + Hangaudiometria + Ez a tevékenység azt méri fel, hogyan hallja Ön a különböző hangokat. + Először is csatlakoztassa és tegye fel a fejhallgatót. + A kezdéshez koppintson a Kezdés elemre. + Ön most valószínűleg hall egy hangot. Állítsa be a hangerőt a készülék oldalán található vezérlőkkel.\n\nKoppintson a gombra, amikor készen áll a kezdésre. + Koppintson a gombra mindig, amikor egy hangot hall. + %1$s Hz, bal + %1$s Hz, jobb + + Térbeli memória + Ez a tevékenység a rövidtávú térbeli memóriát vizsgálja. Ehhez ismételje meg a felvillanó %1$s képeinek sorrendjét. + virágok + virágok + A(z) %1$s egymás után fel fognak villanni. Koppintson a(z) %2$s képeire a megjelenésükkel azonos sorrendben. + A(z) %1$s egymás után fel fognak villanni. Koppintson a(z) %2$s képeire a megjelenésük fordított sorrendjében. + A kezdéshez koppintson a Kezdés elemre, majd figyeljen. + %1$s + Eredmény + Figyelje, ahogy a(z) %1$s felvillannak + Koppintson a(z) %1$s képekre olyan sorrendben, ahogy felvillantak + Koppintson a(z) %1$s képekre fordított sorrendben + A sorozat befejeződött + A folytatáshoz koppintson a Következő gombra + Újrapróbálkozás + Ez alkalommal nem sikerült. A folytatáshoz koppintson a Következő gombra. + Letelt az idő + Elfogyott az idő.\nA folytatáshoz koppintson a Következő gombra. + Játék befejezve + Szüneteltetve + A folytatáshoz koppintson a Következő gombra + + Reakcióidő + Ez a tevékenység felméri, hogy mennyi ideig tart Önnek válaszolni egy vizuális jelzésre. + Rázza meg a készüléket tetszőleges irányban, amint megjelenik a képernyőn a kék pont. A készülék %D alkalommal fogja megkérni erre. + A kezdéshez koppintson a Kezdés elemre. + %1$s/%2$s. kísérlet + Gyorsan rázza meg a készüléket, amikor megjelenik a kék kör + + Hanoi tornyai + Ez a tevékenység kiértékeli az Ön kirakómegoldási képességeit. + Helyezze át a teljes köteget a kijelölt oszlopra a lehető legkevesebb lépésben. + A kezdéshez koppintson a Kezdés elemre + A kirakós megoldása + Lépések száma: %1$s \n %2$s + Nem tudom megoldani ezt a kirakót + + Időre mért gyaloglás + Ez a tevékenység az alsó végtagok működését méri. + Keressen olyan helyet, lehetőleg kültéren, ahol %1$s ideig egyenesen tud gyalogolni olyan gyorsan, ahogy csak tud, de biztonságosan. Ne lassítson le, amíg el nem hagyta a célvonalat. + Koppintson\na Következő elemre\na kezdéshez. + Segítőeszköz + Ugyanazt a segítőeszközt használja minden teszthez. + Használ boka-járáskönnyítőt? + Használ segítőeszközt? + Válasz kijelöléséhez koppintson ide. + Nincs + Egyoldali bot + Egyoldali mankó + Kétoldali bot + Kétoldali mankó + Járókeret/rollátor + Gyalogoljon legalább %1$s távolságra egyenesen. + Forduljon meg, és menjen vissza a kiindulási ponthoz. + Koppintson a Kész lehetőségre, ha elkészült. + + PASAT + PVSAT + PAVSAT + Az Ütemes hallási sorozatösszeadási teszt a hallási információfeldolgozási sebességet és számítási képességet méri. + Az Ütemes vizuális sorozatösszeadási teszt a vizuális információfeldolgozási sebességet és számítási képességet méri. + Az Ütemes hallási és vizuális sorozatösszeadási teszt a hallási és vizuális információfeldolgozási sebességet és számítási képességet méri. + %1$s másodpercenként számjegyek jelennek meg.\nMinden új számjegyet adjon hozzá az előtte lévőhöz.\nFigyelem: Ne számítsa ki a teljes sorozat összegét, csak az utolsó két számjegy összegét. + A kezdéshez koppintson a Kezdés elemre. + Jegyezze meg ezt az első számjegyet. + Adja hozzá ezt az új számjegyet az előzőhöz. + - + + %1$s lyukú körillesztő teszt + Ez a tevékenység a felső végtagok mozgását vizsgálja, amelynek során a rendszer megkéri Önt, hogy illesszen bele egy kört egy lyukba. Ezt %1$s alkalommal kell megtennie. + A bal és a jobb keze egyaránt tesztelve lesz.\nMinél gyorsabban vegye fel a kört, és illessze be a lyukba, és miután ezt %1$s alkalommal megtette, távolítsa el %2$s alkalommal. + A jobb és a bal keze egyaránt tesztelve lesz.\nMinél gyorsabban vegye fel a kört, és illessze be a lyukba, és miután ezt %1$s alkalommal megtette, távolítsa el %2$s alkalommal. + A kezdéshez koppintson a Kezdés elemre. + Tegye a kört a lyukba a bal kezével. + Tegye a kört a lyukba a jobb kezével. + Tegye a kört a vonal mögé a bal kezével. + Tegye a kört a vonal mögé a jobb kezével. + Fogja meg a kört két ujjal. + Emelje fel az ujjait a kör elhelyezéséhez. + + Remegési tevékenység + Ez a tevékenység a keze remegését méri különböző helyzetekben. Keressen egy olyan helyet, ahol kényelmesen le tud ülni a tevékenység idejére. + Tartsa a telefont a gyakrabban használt kezében, a képen látható módon. + Tartsa a telefont a JOBB kezében, a képen látható módon. + Tartsa a telefont a BAL kezében, a képen látható módon. + A rendszer fel fogja kérni a(z) %1$s végrehajtására, miközben ül a telefonnal a kezében. + egy feladat + két feladat + három feladat + négy feladat + öt feladat + Koppintson a Következő elemre a folytatáshoz. + Készüljön fel arra, hogy a telefont az ölében tartsa. + Készüljön fel arra, hogy a telefont a BAL kezével az ölében tartsa. + Készüljön fel arra, hogy a telefont a JOBB kezével az ölében tartsa. + Tartsa az ölében a telefont %1$d másodpercig. + Most tartsa a telefont kinyújtott karral vállmagasságban. + Most tartsa a telefont kinyújtott karral, BAL kézzel vállmagasságban. + Most tartsa a telefont kinyújtott karral, JOBB kézzel vállmagasságban. + Tartsa a telefont kinyújtott karral %1$d másodpercig. + Most tartsa a telefont behajlított könyökkel vállmagasságban. + Most tartsa a telefont behajlított könyökkel, BAL kézzel vállmagasságban. + Most tartsa a telefont behajlított könyökkel, JOBB kézzel vállmagasságban. + Tartsa a telefont behajlított könyökkel %1$d másodpercig. + Most a könyökét behajlítva érintse többször az orrához a telefont. + Most tartsa a telefont BAL kézzel, a könyökét behajlítva, és érintse többször az orrához a telefont. + Most tartsa a telefont JOBB kézzel, a könyökét behajlítva, és érintse többször az orrához a telefont. + Érintse a telefont az orrához %1$d másodpercig. + Forgassa a felemelt csuklóját (mintha integetne). + Forgassa a felemelt BAL csuklóját a telefonnal a kezében (mintha integetne). + Forgassa a felemelt JOBB csuklóját a telefonnal a kezében (mintha integetne). + Forgassa a felemelt csuklóját %1$d másodpercig. + Most tegye át a telefont a BAL kezébe, és folytassa a következő feladattal. + Most tegye át a telefont a JOBB kezébe, és folytassa a következő feladattal. + Folytatás a következő feladattal. + Tevékenység befejezve. + A rendszer fel fogja kérni a(z) %1$s végrehajtására, miközben ül a telefonnal az egyik kezében, majd a másik kezében. + A BAL kezemmel nem tudom elvégezni ezt a feladatot. + A JOBB kezemmel nem tudom elvégezni ezt a feladatot. + Mindkét kézzel el tudom végezni ezt a feladatot. + + A fájl nem hozható létre + Nem sikerült elég naplófájlt eltávolítani a küszöb eléréséhez + Hiba történt az attribútum beállításakor + A fájl nincs töröltként megjelölve (nincs feltöltöttként megjelölve) + Több hiba történt a naplók eltávolításakor + Nem található összegyűjtött adat. + Nincs megadva kimeneti könyvtár + + Nincs adat + + Vissza + %1$s illusztrációja + Külön aláírásmező + Érintse meg a képernyőt, és mozgassa az ujját az aláíráshoz + Aláírva + Aláírás nélküli + Kiválasztva + Nincs kijelölve + Válaszcsúszka. Tartomány: %1$s - %2$s + Címke nélküli kép + Feladat megkezdése + aktív + helyes + helytelen + nyugodt + Memóriajáték címe + Előnézet rögzítése + Készített kép + Videorögzítés előnézete + Rögzített videó + Nem helyezhető %1$s méretű korong %2$s méretű korongra + Cél + Torony + Koppintson duplán a korong elhelyezéséhez + Koppintson duplán a legfelső korong kijelöléséhez + A következő méretű korongokat tárolja: %1$s + Üres + Tartomány ettől: %1$s eddig: %2$s + Halom a következőkből: + és + Pont: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-in/strings.xml b/backbone/src/main/res/values-in/strings.xml new file mode 100644 index 000000000..ebe38de67 --- /dev/null +++ b/backbone/src/main/res/values-in/strings.xml @@ -0,0 +1,396 @@ + + + + + Persetujuan + Nama Depan + Nama Belakang + Diperlukan + Tinjauan + Tinjau formulir di bawah, dan ketuk Setuju jika Anda siap melanjutkan. + Tinjauan + Tanda Tangan + Silakan tanda tangan menggunakan jari Anda pada baris di bawah. + Tanda Tangan di Sini + Halaman %1$ld dari %2$ld + + Selamat Datang + Pengumpulan Data + Privasi + Penggunaan Data + Survei Studi + Tugas Studi + Komitmen Waktu + Penarikan Diri + Lebih Lanjut + + Lebih lanjut mengenai cara data dikumpulkan + Lebih lanjut mengenai cara data digunakan + Lebih lanjut mengenai cara privasi dan identitas Anda dilindungi + Pelajari lebih lanjut mengenai studi terlebih dahulu + Lebih lanjut mengenai survei studi + Lebih lanjut mengenai dampak studi terhadap waktu Anda + Lebih lanjut mengenai tugas yang bersangkutan + Lebih lanjut mengenai penarikan diri + + Pilihan Berbagi + Bagikan data saya dengan %1$s dan peneliti berkualifikasi di seluruh dunia + Hanya bagikan data saya dengan %1$s + %1$s akan menerima data penelitian dari partisipasi Anda dalam studi ini.\n\nBerbagi data studi yang dikodekan dengan lebih banyak pihak (tanpa informasi seperti nama Anda) dapat bermanfaat bagi penelitian ini dan yang akan datang. + Pelajari berbagi data lebih lanjut + + Nama %1$s (dicetak) + Tanda Tangan %1$s + Tanggal + + Langkah %1$s dari %2$s + + Nilai tidak sah + %1$s melebihi nilai maksimum yang diizinkan (%2$s). + %1$s kurang dari nilai minimum yang diizinkan (%2$s). + %1$s bukan nilai yang sah. + + Alamat email tidak sah: %1$s + + Masukkan alamat + Tidak Dapat Menemukan Alamat yang Ditetapkan + Tidak dapat menemukan lokasi Anda saat ini. Ketikkan alamat atau pindah ke lokasi dengan sinyal GPS yang lebih baik jika memungkinkan. + Akses ke layanan lokasi telah ditolak. Izinkan app ini untuk menggunakan layanan lokasi melalui Pengaturan. + Tidak dapat menemukan hasil untuk alamat yang dimasukkan. Pastikan bahwa alamat sah. + Anda tidak terhubung ke internet atau telah melampaui jumlah maksimum permintaan pencarian alamat. Jika Anda tidak terhubung ke internet, nyalakan Wi-Fi untuk menjawab pertanyaan ini, lewati pertanyaan ini jika tombol lewati tersedia, atau kembali ke survey setelah Anda terhubung ke internet. Jika tidak, coba lagi dalam beberapa menit. + + Konten teks melebihi panjang maksimum: %1$s + + Kamera tidak tersedia dalam layar terpisah. + + Email + jappleseed@example.com + Kata Sandi + Masukkan kata sandi + Konfirmasi + Masukkan kata sandi lagi + Kata sandi tidak cocok. + Informasi Tambahan + John + Appleseed + Jenis Kelamin + Pilih jenis kelamin + Tanggal Lahir + Pilih tanggal + + Verifikasi + Verifikasi Email Anda + Ketuk tautan di bawah jika Anda tidak menerima email verifikasi dan ingin email dikirimkan lagi. + Kirim Ulang Email Verifikasi + + Masuk + Lupa kata sandi? + + Masukkan kode sandi + Konfirmasi kode sandi + Kode sandi disimpan + Kode sandi disahkan + Masukkan kode sandi Anda yang lama + Masukkan kode sandi Anda yang baru + Konfirmasi kode sandi baru Anda + Kode Sandi Salah + Kode sandi tidak cocok. Coba lagi. + Sahkan dengan Touch ID + Kesalahan Touch ID + Hanya karakter numerik yang diizinkan. + Indikator kemajuan entri kode sandi + %1$s dari %2$s digit dimasukkan + Lupa Kode Sandi? + + Tidak dapat menambah item Rantai Kunci. + Tidak dapat memperbarui item Rantai Kunci. + Tidak dapat menghapus item Rantai Kunci. + Tidak dapat menemukan item Rantai Kunci. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Wanita + Pria + Lainnya + + Tidak + Ya + + cm + kaki + inci + + Ketuk untuk menjawab + Pilih jawaban + Ketuk untuk memilih + Ketuk untuk menulis + + Setuju + Batalkan + OKE + Bersihkan + Tidak Setuju + Selesai + Mulai + Lebih lanjut + Berikutnya + Lewati + Lewati pertanyaan ini + Mulai Timer + Simpan untuk Nanti + Hapus Hasil + Akhiri Tugas + Simpan + Bersihkan jawaban + Jawaban ini tidak dapat dimodifikasi. + + Memulai aktivitas dalam + Aktivitas Selesai + Data Anda akan dianalisis dan Anda akan diberi tahu setelah hasil Anda siap. + Tersisa %1$s detik. + + Ambil Gambar + Ambil Ulang Gambar + Kamera tidak ditemukan.\u0020\u0020Langkah ini tidak dapat diselesaikan. + Untuk menyelesaikan langkah ini, izinkan app ini untuk mengakses kamera di Pengaturan. + Tidak ada direktori output yang ditentukan untuk gambar yang diambil. + Gambar yang diambil tidak dapat disimpan. + + Mulai Merekam + Berhenti Merekam + Ambil Ulang Video + + Kebugaran + Jarak (%1$s) + Detak Jantung (bpm) + Duduk dengan nyaman selama %1$s. + Jalan secepat mungkin selama %1$s. + Aktivitas ini memonitor detak jantung Anda dan mengukur seberapa jauh Anda dapat berjalan dalam %1$s. + Jalan di luar ruangan dengan laju secepat mungkin selama %1$s. Saat Anda selesai, duduk dan istirahat dengan nyaman selama %2$s. Untuk memulai, ketuk Mulai. + + Gaya dan Keseimbangan + Aktivitas ini mengukur gaya dan keseimbangan Anda saat Anda berjalan dan berdiri. Jangan lanjutkan jika Anda tidak dapat berjalan dengan aman tanpa bantuan. + Temukan tempat yang memungkinkan Anda untuk berjalan kaki dengan aman tanpa bantuan sejauh sekitar %1$d langkah dalam garis lurus. + Masukkan telepon Anda ke saku atau kantong dan ikuti instruksi audio. + Sekarang berdiri selama %1$s. + Berdiri selama %1$s. + Putar balik, dan kembali ke tempat Anda memulai. + Berjalan kaki hingga %1$d langkah dalam garis lurus. + + Cari tempat untuk dapat berjalan bolak-balik di jalur lurus dengan aman. Coba terus berjalan dengan berbelok di akhir jalur Anda, seolah-olah berjalan mengitari kerucut.\n\nSelanjutnya Anda akan diinstruksikan untuk berputar dalam lingkaran penuh, lalu tetap berdiri dengan lengan berada di samping dan kaki terbuka selebar bahu. + Ketuk Mulai saat Anda siap untuk memulai.\nLalu letakkan telepon Anda di saku atau tas dan ikuti instruksi audio. + Jalan bolak-balik di jalur lurus selama %1$s. Jalan dengan normal. + Berputar dalam lingkaran penuh lalu tetap berdiri selama %1$s. + Anda telah menyelesaikan aktivitas. + + Kecepatan Mengetuk + Tangan Kanan + Tangan Kiri + Aktivitas ini mengukur kecepatan mengetuk Anda. + Letakkan telepon Anda di permukaan datar. + Gunakan dua jari pada tangan yang sama untuk mengetuk tombol di layar secara bergantian. + Gunakan dua jari pada tangan kanan Anda untuk mengetuk tombol di layar secara bergantian. + Gunakan dua jari pada tangan kiri Anda untuk mengetuk tombol di layar secara bergantian. + Sekarang ulangi tes yang sama menggunakan tangan kanan. + Sekarang ulangi tes yang sama menggunakan tangan kiri. + Ketuk dengan satu jari, lalu jari lainnya. Sebisa mungkin, coba atur ketukan Anda agar seimbang. Terus mengetuk selama %1$s. + Ketuk Mulai untuk memulai. + Ketuk Berikutnya untuk memulai. + Ketuk + Total Ketukan + Ketuk tombol sekonsisten mungkin menggunakan dua jari. + Ketuk tombol menggunakan tangan KANAN Anda. + Ketuk tombol menggunakan tangan KIRI Anda. + Lewati tangan Ini + + Suara + Ketuk Mulai untuk memulai. + Katakan “Aaaaah” di mikrofon selama mungkin. + Tarik nafas dalam-dalam dan katakan “Aaaaah” ke mikrofon selama Anda bisa. Pertahankan volume suara sehingga bar audio tetap berwarna biru. + Tes ini mengevaluasi suara Anda dengan merekamnya saat Anda berbicara melalui mikrofon di bagian bawah telepon Anda. + Terlalu Keras + Tidak dapat merekam audio + Harap tunggu saat kami memeriksa level kebisingan latar belakang. + Level kebisingan di sekitar terlalu kencang untuk merekam suara Anda. Pindah ke tempat lain yang lebih hening dan coba lagi. + Ketuk Berikutnya saat sudah siap. + + Audiometri Nada + Aktivitas ini mengukur kemampuan Anda untuk mendengar bunyi yang berbeda. + Sebelum memulai, sambungkan dan pakai headphone Anda. + Ketuk Mulai untuk memulai. + Seharusnya Anda mendengar nada sekarang. Sesuaikan volume menggunakan kontrol di bagian samping perangkat Anda.\n\nKetuk tombol jika Anda siap untuk memulai. + Ketuk tombol setiap kali Anda mulai mendengar bunyi. + %1$s Hz, Kiri + %1$s Hz, Kanan + + Memori Spasial + Aktivitas ini mengukur memori spasial jangka pendek dengan meminta Anda untuk mengulangi urutan menyalanya %1$s. + bunga + bunga + Beberapa %1$s akan menyala satu per satu. Ketuk %2$s tersebut sesuai urutan menyalanya. + Beberapa %1$s akan menyala satu per satu. Ketuk %2$s tersebut berkebalikan dengan urutan menyalanya. + Untuk memulai, ketuk Mulai, lalu perhatikan dengan saksama. + %1$s + Skor + Lihat %1$s menyala + Ketuk %1$s sesuai urutan menyalanya + Ketuk %1$s dalam urutan terbalik + Urutan Selesai + Untuk melanjutkan, ketuk Berikutnya + Coba Lagi + Anda tidak berhasil menyelesaikannya. Ketuk Berikutnya untuk melanjutkan. + Waktu Habis + Anda kehabisan waktu.\nKetuk Berikutnya untuk melanjutkan. + Permainan Selesai + Dijeda + Untuk melanjutkan, ketuk Berikutnya + + Waktu Reaksi + Aktivitas ini mengevaluasi waktu yang Anda butuhkan untuk merespons isyarat visual. + Goyang perangkat ke arah mana pun segera setelah titik biru muncul di layar. Anda akan diminta untuk melakukannya sebanyak %D kali. + Ketuk Mulai untuk memulai. + Percobaan %1$s dari %2$s + Goyang perangkat dengan cepat saat lingkaran biru muncul + + Menara Hanoi + Aktivitas ini mengevaluasi kemampuan Anda dalam menyelesaikan teka-teki. + Pindahkan seluruh tumpukan ke platform yang disorot, semakin sedikit gerakan maka semakin baik. + Ketuk Mulai untuk memulai + Selesaikan Teka-Teki + Jumlah Gerakan: %1$s \n %2$s + Saya tidak dapat menyelesaikan teka-teki ini + + Berjalan Kaki dibatasi Waktu + Aktivitas ini mengukur fungsi ekstrem rendah Anda. + Carilah tempat, lebih baik di luar ruangan, di mana Anda dapat berjalan sekitar %1$s dengan lurus secepat mungkin, namun tetap aman. Jangan kurangi kecepatan hingga Anda tiba di garis akhir. + Ketuk Berikutnya untuk memulai. + Perangkat bantuan + Gunakan perangkat bantuan yang sama untuk tiap pengujian. + Apakah Anda mengenakan ortosis gelang kaki? + Apakah Anda menggunakan perangkat bantuan? + Ketuk di sini untuk memilih jawaban. + Tiada + Tongkat Unilateral + Kruk Unilateral + Tongkat Bilateral + Kruk Bilateral + Walker/Rollator + Berjalan kaki hingga %1$s dalam garis lurus. + Putar balik, dan kembali ke tempat Anda memulai. + Ketuk Selesai setelah selesai. + + PASAT + PVSAT + PAVSAT + Pengujian Paced Auditory Serial Addition mengukur kecepatan pemrosesan informasi dan kemampuan menghitung auditori Anda. + Pengujian Paced Visual Serial Addition mengukur kecepatan pemrosesan informasi dan kemampuan menghitung visual Anda. + Pengujian Paced Auditory dan Visual Serial Addition mengukur kecepatan pemrosesan informasi dan kemampuan menghitung auditori dan visual Anda. + Satu digit ditampilkan setiap %1$s detik.\nAnda harus segera menambahkan tiap digit baru ke digit sebelumnya.\nPerhatian, jangan hitung total keseluruhan, tetapi hanya jumlah dua angka terakhir. + Ketuk Mulai untuk memulai. + Ingat digit pertama ini. + Tambahkan digit baru ini ke digit sebelumnya. + - + + Uji Sumbat %1$s Lubang + Aktivitas ini mengukur fungsi bagian teratas dengan meminta Anda untuk meletakkan sumbat di lubang. Anda akan diminta untuk melakukan ini %1$s kali. + Tangan kiri dan kanan Anda akan diuji.\nAnda harus mengambil sumbat secepat mungkin, memasukkannya ke lubang, dan setelah selesai %1$s kali, pindahkan lagi %2$s kali. + Tangan kanan dan kiri Anda akan diuji.\nAnda harus mengambil sumbat secepat mungkin, memasukkannya ke lubang, dan setelah selesai %1$s kali, pindahkan lagi %2$s kali. + Ketuk Mulai untuk memulai. + Letakkan sumbat di lubang menggunakan tangan kiri Anda. + Letakkan sumbat di lubang menggunakan tangan kanan Anda. + Letakkan sumbat di belakang garis menggunakan tangan kiri Anda. + Letakkan sumbat di belakang garis menggunakan tangan kanan Anda. + Ambil sumbat dengan dua jari. + Angkat jari untuk menjatuhkan sumbat. + + Aktivitas Tremor + Aktivitas ini mengukur tremor tangan Anda dalam berbagai posisi. Cari tempat yang dapat Anda gunakan untuk duduk dengan nyaman selama durasi aktivitas ini. + Genggam telepon di tangan yang terkena dampak lebih besar seperti yang ditampilkan dalam gambar di bawah. + Genggam telepon Anda di tangan KANAN seperti yang ditampilkan dalam gambar di bawah. + Genggam telepon Anda di tangan KIRI seperti yang ditampilkan dalam gambar di bawah. + Anda akan diminta untuk melakukan %1$s saat duduk dengan telepon di tangan Anda. + sebuah tugas + dua tugas + tiga tugas + empat tugas + lima tugas + Ketuk berikutnya untuk melanjutkan. + Bersiap untuk menggenggam telepon di pangkuan. + Bersiap untuk menggenggam telepon di pangkuan dengan tangan KIRI. + Bersiap untuk menggenggam telepon di pangkuan dengan tangan KANAN. + Terus genggam telepon Anda di pangkuan selama %1$d detik. + Sekarang genggam telepon Anda dengan tangan terentang setinggi bahu. + Sekarang genggam telepon Anda dengan tangan KIRI terentang setinggi bahu. + Sekarang genggam telepon Anda dengan tangan KANAN terentang setinggi bahu. + Terus genggam telepon Anda dengan tangan terentang selama %1$d detik. + Sekarang genggam telepon Anda setinggi bahu dengan siku ditekuk. + Sekarang genggam telepon Anda dengan tangan KIRI setinggi bahu dengan siku ditekuk. + Sekarang genggam telepon Anda dengan tangan KANAN setinggi bahu dengan siku ditekuk. + Terus genggam telepon dengan siku ditekuk selama %1$d detik + Sekarang siku tetap ditekuk, sentuhkan telepon ke hidung Anda berulang kali. + Sekarang siku tetap ditekuk dengan telepon di tangan KIRI, sentuhkan telepon ke hidung Anda berulang kali. + Sekarang siku tetap ditekuk dengan telepon di tangan KANAN, sentuhkan telepon ke hidung Anda berulang kali. + Terus sentuhkan telepon ke hidung Anda selama %1$d detik + Bersiap untuk melakukan lambaian tangan ratu (melambai dengan memutar pergelangan tangan). + Bersiap untuk melakukan lambaian tangan ratu dengan telepon di tangan KIRI Anda (melambai dengan memutar pergelangan tangan). + Bersiap untuk melakukan lambaian tangan ratu dengan telepon di tangan KANAN Anda (melambai dengan memutar pergelangan tangan). + Terus lakukan lambaian tangan ratu selama %1$d detik. + Sekarang pindahkan telepon ke tangan KIRI dan lanjutkan ke tugas berikutnya. + Sekarang pindahkan telepon ke tangan KANAN dan lanjutkan ke tugas berikutnya. + Lanjutkan ke tugas berikutnya. + Aktivitas selesai. + Anda akan diminta untuk melakukan %1$s saat duduk dengan telepon di satu tangan lebih dahulu, lalu melakukan lagi dengan tangan lainnya. + Saya tidak dapat melakukan aktivitas ini dengan tangan KIRI. + Saya tidak dapat melakukan aktivitas ini dengan tangan KANAN. + Saya dapat melakukan aktivitas ini dengan kedua tangan. + + Tidak dapat membuat file + Tidak dapat menghapus cukup file log untuk mencapai ambang + Kesalahan saat mengatur atribut + File tidak ditandai sebagai dihapus (tidak ditandai sebagai diunggah) + Beberapa kesalahan saat menghapus log + Tidak ada data terkumpul yang ditemukan. + Tidak ada direktori output yang ditentukan + + Tidak Ada Data + + Kembali + Ilustrasi %1$s + Bidang tanda tangan yang ditetapkan + Sentuh layar dan gerakkan jari Anda untuk menandatangani + Ditandatangani + Tidak ditandatangani + Dipilih + Tidak dipilih + Penggeser respons. Berkisar dari %1$s hingga %2$s + Gambar tidak dilabeli + Mulai tugas + aktif + benar + salah + diam + Ubin permainan memori + Pratinjau gambar + Gambar terambil + Pratinjau pengambilan video + Video terambil + Tidak dapat meletakkan disk dengan ukuran %1$s pada disk dengan ukuran %2$s + Target + Menara + Ketuk dua kali untuk meletakkan disk + Ketuk dua kali untuk memilih disk teratas + Memiliki disk dengan ukuran %1$s + Kosong + Berkisar dari %1$s hingga %2$s + Tumpukan terdiri dari + dan + Poin: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-it/strings.xml b/backbone/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..177544e46 --- /dev/null +++ b/backbone/src/main/res/values-it/strings.xml @@ -0,0 +1,396 @@ + + + + + Consenso + Nome + Cognome + Obbligatorio + Verifica + Verifica il modulo sottostante e scegli Accetto per continuare. + Verifica + Firma + Firma con il dito sulla linea sottostante. + Firma qui + Pagina %1$ld di %2$ld + + Benvenuto + Raccolta dati + Privacy + Utilizzo dati + Indagine conoscitiva + Attività indagine conoscitiva + Tempo richiesto + Ritiro + Scopri di più + + Scopri di più sulla raccolta di dati + Scopri di più sull\'utilizzo dei dati + Scopri di più sulla tutela dei dati relativi alla privacy e alla tua identità + Prima di iniziare, scopri di più sull\'indagine conoscitiva + Scopri di più sull\'indagine conoscitiva + Scopri di più sull\'impatto che l\'indagine conoscitiva avrà sul tuo tempo + Scopri di più sulle attività previste + Scopri di più sul ritiro + + Opzioni di condivisione + Condividi i miei dati con %1$s e con ricercatori qualificati a livello mondiale + Condividi i miei dati solo con %1$s + %1$s riceverà i dati raccolti grazie alla tua partecipazione all\'indagine conoscitiva.\n\nUna condivisione più ampia di questi dati codificati (senza rivelare informazioni sensibili come il tuo nome) può aiutare lo studio ed eventuali ricerche future. + Scopri di più sulla condivisione dei dati + + Nome di %1$s (stampatello) + Firma di %1$s + Data + + Passaggio %1$s di %2$s + + Valore non valido + %1$s supera il valore massimo consentito (%2$s). + %1$s è inferiore al valore minimo consentito (%2$s). + %1$s non è un valore valido. + + Indirizzo e-mail non valido: %1$s + + Inserisci un indirizzo + Impossibile trovare l\'indirizzo specificato + Impossibile individuare la tua posizione attuale. Inserisci l\'indirizzo o spostati in una zona con una migliore copertura GPS. + L\'accesso ai servizi di localizzazione è stato negato. Vai in Impostazioni e autorizza l\'applicazione a utilizzare i servizi di localizzazione. + Impossibile trovare risultati per l\'indirizzo inserito, assicurati che sia valido. + Non sei connesso a Internet o hai superato il numero massimo di tentativi di ricerca dell\'indirizzo. Se non sei connesso a Internet, attiva la rete Wi-Fi per rispondere a questa domanda, ignora il passaggio se è disponibile il tasto Ignora o torna al sondaggio una volta connesso. In alternativa, riprova tra qualche minuto. + + Il testo supera la lunghezza massima: %1$s + + Fotocamera non disponibile in modalità “Vista suddivisa”. + + E-mail + gmela@example.com + Password + Inserisci password + Conferma + Inserisci di nuovo la password + Le password non coincidono. + Ulteriori informazioni + Giovanni + Mela + Sesso + Scegli il sesso + Data di nascita + Scegli una data + + Verifica + Verifica il tuo indirizzo e-mail + Seleziona il link se non hai ricevuto l\'e-mail di conferma e desideri che venga inviata nuovamente. + Invia di nuovo e-mail di verifica + + Accedi + Hai dimenticato la password? + + Inserisci codice + Conferma codice + Codice salvato + Codice autenticato + Inserisci il vecchio codice + Inserisci il nuovo codice + Conferma il nuovo codice + Codice non corretto + I codici non corrispondono, riprova. + Effettua l\'autenticazione con Touch ID + Errore Touch ID + Sono consentiti solo caratteri numerici. + Indicatore progresso inserimento codice + %1$s di %2$s cifre inserite + Hai dimenticato il codice? + + Impossibile aggiungere l\'elemento del portachiavi. + Impossibile aggiornare l\'elemento del portachiavi. + Impossibile eliminare l\'elemento del portachiavi. + Impossibile trovare l\'elemento del portachiavi. + + A+ + A- + AB+ + AB- + B+ + B- + 0+ + 0- + + Donna + Uomo + Altro + + No + + + cm + piedi + pollici + + Tocca per rispondere + Seleziona una risposta + Tocca per selezionare + Tocca per scrivere + + Accetto + Annulla + OK + Cancella + Rifiuto + Fine + Inizia + Scopri di più + Avanti + Ignora + Ignora la domanda + Avvia il timer + Salva e continua in seguito + Ignora i risultati + Fine attività + Salva + Cancella risposta + La risposta non può essere modificata. + + Inizio attività tra + Attività completata + I dati verranno analizzati, riceverai una notifica quando i risultati saranno pronti. + %1$s secondi rimanenti. + + Scatta foto + Scatta altra foto + Fotocamera non trovata. Non è possibile completare l\'operazione. + Per completare l\'operazione, consenti a questa app di accedere alla fotocamera in Impostazioni. + Non è stata specificata nessuna directory di output per le immagini acquisite. + L\'immagine acquisita non può essere salvata. + + Avvia registrazione + Interrompi registrazione + Riacquisisci video + + Forma fisica + Distanza (%1$s) + Battito cardiaco (bpm) + Siediti comodamente per %1$s. + Cammina il più velocemente possibile per %1$s. + L\'attività monitora la tua frequenza cardiaca e valuta la distanza massima che puoi raggiungere in %1$s. + Cammina all\'aperto all\'andatura massima che riesci a sostenere per %1$s. Al termine, siediti e riposati per %2$s. Per cominciare, tocca Inizia. + + Deambulazione ed equilibrio + L\'attività valuta la capacità di deambulazione e di equilibrio mentre cammini e rimani fermo. Non continuare se non sei in grado di spostarti in modo sicuro e autonomo. + Trova un luogo in cui puoi fare autonomamente circa %1$d passi in linea retta in modo sicuro. + Metti iPhone in tasca o in borsa e segui le istruzioni audio. + Rimani in piedi per %1$s. + Rimani in piedi per %1$s. + Girati e torna verso il punto in cui hai cominciato a camminare. + Fai fino a %1$d passi in linea retta. + + Trova un luogo dove puoi camminare in sicurezza avanti e indietro in linea retta. Cerca di camminare senza interruzione girandoti alla fine del percorso come se camminassi attorno a un cono.\n\nSuccessivamente ti verrà chiesto di girare in un cerchio completo, quindi di fermarti con le braccia lungo il corpo e i piedi circa alla larghezza delle spalle. + Tocca Inizia quando sei pronto a cominciare.\nQuindi posiziona il telefono in tasca o in borsa e segui le istruzioni audio. + Cammina avanti e indietro in linea retta per %1$s. Cammina come faresti normalmente. + Gira in un cerchio completo, quindi fermati per %1$s. + Hai completato l\'attività. + + Velocità tocco + Mano destra + Mano sinistra + L\'attività misura la velocità dei tocchi che esegui. + Posiziona il telefono su una superficie piana. + Utilizza due dita della stessa mano per toccare alternativamente i pulsanti sullo schermo. + Utilizza due dita della mano destra per toccare alternativamente i pulsanti sullo schermo. + Utilizza due dita della mano sinistra per toccare alternativamente i pulsanti sullo schermo. + Ora ripeti lo stesso test con la mano destra. + Ora ripeti lo stesso test con la mano sinistra. + Tocca con un dito, quindi con l\'altro. Cerca di effettuare i tocchi con un ritmo più regolare possibile. Continua a toccare per %1$s. + Tocca Inizia per cominciare. + Tocca Avanti per iniziare. + Tocca + Totale tocchi + Tocca i pulsanti il più regolarmente possibile usando due dita. + Tocca i pulsanti usando la mano DESTRA. + Tocca i pulsanti usando la mano SINISTRA. + Salta la mano + + Voce + Tocca Inizia per cominciare. + Di\' “Aaaaa” nel microfono il più a lungo possibile. + Fai un respiro profondo e di\' “Aaaaa” nel microfono il più a lungo possibile. Mantieni un volume uniforme in modo tale che le barre dell\'audio rimangano di colore blu. + L\'attività valuta la tua voce registrandola tramite il microfono di iPhone. + Volume troppo alto + Impossibile registrare l\'audio + Attendi la verifica del livello di rumore di sottofondo. + Il livello di rumore ambientale è troppo alto per registrare la tua voce. Spostati in un luogo più silenzioso e riprova. + Tocca Avanti quando sei pronto. + + Audiometria tonale + L\'attività valuta la tua capacità di sentire suoni differenti. + Prima di cominciare collega gli auricolari e indossali. + Tocca Inizia per cominciare. + Ora sentirai un tono, regola il volume mediante i tasti laterali del tuo dispositivo.\n\nTocca il pulsante quando sei pronto per iniziare. + Tocca il pulsante ogni volta che senti un suono. + %1$s Hz, a sinistra + %1$s Hz, a destra + + Memoria spaziale + L\'attività valuta la tua memoria spaziale a breve termine. Ripeti l\'ordine di accensione dei simboli a forma di %1$s. + fiore + fiore + I simboli a forma di %1$s si accenderanno uno alla volta. Toccali seguendo l\'ordine di accensione. + I simboli a forma di %1$s si accenderanno uno alla volta. Toccali in ordine inverso rispetto alla sequenza iniziale. + Per cominciare, tocca Inizia e osserva con attenzione. + %1$s + Punteggio + Osserva quando il simbolo a forma di %1$s si accende + Tocca i simboli a forma di %1$s seguendo l\'ordine con cui si sono accesi + Tocca i simboli a forma di %1$s in ordine inverso + Sequenza completa + Per continuare, tocca Avanti + Riprova + Non sei riuscito a completare l\'attività richiesta nel tempo desiderato. Tocca Avanti per continuare. + Tempo scaduto + Tempo scaduto.\nTocca Avanti per continuare. + Fine + In pausa + Per continuare, tocca Avanti + + Tempo di reazione + L\'attività valuta il tuo tempo di risposta quando ricevi un\'indicazione visiva. + Quando vedi comparire il cerchio di colore blu sullo schermo, agita il dispositivo in qualsiasi direzione. Dovrai ripetere l\'operazione %D volte. + Tocca Inizia per cominciare. + Tentativo %1$s di %2$s + Agita velocemente il dispositivo quando compare il cerchio di colore blu + + Torre di Hanoi + Questa attività valuta le tue capacità di risolvere rompicapi. + Sposta l\'intera pila sul piano evidenziato con il minor numero di mosse possibili. + Tocca Inizia per cominciare + Risolvi il rompicapo + Numero di mosse: %1$s \n %2$s + Non riesco a risolvere il rompicapo + + Camminata a tempo + Questa attività misura la funzionalità dei tuoi arti inferiori. + Trova un posto, preferibilmente all\'aperto, dove puoi camminare per %1$s in linea retta più velocemente possibile, ma in sicurezza. Non rallentare finché non hai superato il traguardo. + Tocca Avanti per iniziare. + Ausilio ortopedico + Utilizza lo stesso ausilio ortopedico per ciascun test. + Indossi un\'ortesi piede-caviglia? + Utilizzi ausili ortopedici? + Tocca per selezionare una risposta. + Nessuno + Bastone unilaterale + Stampella unilaterale + Bastoni bilaterali + Stampelle bilaterali + Deambulatore/Rollatore + Cammina per %1$s in linea retta. + Girati e torna verso il punto in cui hai cominciato a camminare. + Al termine, tocca Fine. + + PASAT + PVSAT + PAVSAT + Il Paced Auditory Serial Addition Test (PASAT) misura la velocità di elaborazione delle informazioni uditive e la capacità di calcolo. + Il Paced Visual Serial Addition Test (PVSAT) misura la velocità di elaborazione delle informazioni visive e la capacità di calcolo. + Il Paced Auditory and Visual Serial Addition Test (PAVSAT) misura la velocità di elaborazione delle informazioni uditive e visive e la capacità di calcolo. + Le singole cifre vengono mostrate ogni %1$s secondi.\nDevi aggiungere ciascuna cifra a quella immediatamente precedente.\nAttenzione, non devi calcolare un totale collettivo, ma solo la somma degli ultimi due numeri. + Tocca Inizia per cominciare. + Ricorda questa prima cifra. + Aggiungi questa nuova cifra alla precedente. + - + + Test %1$s-hole peg (NHPT) + Questo test valuta la funzionalità delle tue estremità superiori. Dovrai posizionare l\'icona azzurra nella sagoma vuota %1$s volte. + Verrà esaminata la funzionalità di entrambe le mani.\nÈ necessario selezionare l\'icona colorata il più velocemente possibile e inserirla nell\'apposita sagoma vuota. Una volta completata l\'operazione %1$s volte, dovrai rimuovere l\'icona altre %2$s volte. + Verrà esaminata la funzionalità di entrambe le mani.\nÈ necessario selezionare l\'icona colorata il più velocemente possibile e inserirla nell\'apposita sagoma vuota. Una volta completata l\'operazione %1$s volte, dovrai rimuovere l\'icona altre %2$s volte. + Tocca Inizia per cominciare. + Sposta l\'icona colorata nella sagoma vuota usando la mano sinistra. + Sposta l\'icona colorata nella sagoma vuota usando la mano destra. + Sposta l\'icona colorata dietro la linea usando la mano sinistra. + Sposta l\'icona colorata dietro la linea usando la mano destra. + Seleziona l\'icona colorata utilizzando due dita. + Solleva le dita per rilasciare l\'icona colorata. + + Attività tremore + L\'attività misura il tremore delle mani in varie posizioni. Trova un luogo dove puoi sederti comodamente per la durata dell\'attività. + Tieni il telefono nella mano maggiormente interessata come mostrato nell\'immagine sotto. + Tieni il telefono nella mano DESTRA come mostrato nell\'immagine sotto. + Tieni il telefono nella mano SINISTRA come mostrato nell\'immagine sotto. + Ti verrà richiesto di eseguire %1$s mentre stai seduto con il telefono in mano. + un\'attività + due attività + tre attività + quattro attività + cinque attività + Tocca Avanti per procedere. + Preparati a tenere il telefono in grembo. + Preparati a tenere il telefono in grembo con la mano SINISTRA. + Preparati a tenere il telefono in grembo con la mano DESTRA. + Continua a tenere il telefono in grembo per %1$d secondi. + Ora tieni il telefono con la mano distesa all\'altezza della spalla. + Ora tieni il telefono con la mano SINISTRA distesa all\'altezza della spalla. + Ora tieni il telefono con la mano DESTRA distesa all\'altezza della spalla. + Continua a tenere il telefono con la mano distesa per %1$d secondi. + Ora tieni il telefono all\'altezza della spalla con il gomito piegato. + Ora tieni il telefono con la mano SINISTRA all\'altezza della spalla con il gomito piegato. + Ora tieni il telefono con la mano DESTRA all\'altezza della spalla con il gomito piegato. + Continua a tenere il telefono con il gomito piegato per %1$d secondi + Ora, tenendo il gomito piegato, tocca ripetutamente il telefono sul naso. + Ora, tenendo il gomito piegato con il telefono nella mano SINISTRA, tocca ripetutamente il telefono sul naso. + Ora, tenendo il gomito piegato con il telefono nella mano DESTRA, tocca ripetutamente il telefono sul naso. + Continua a toccare il telefono sul naso per %1$d secondi + Preparati a ruotare ripetutamente il polso a destra e a sinistra. + Preparati a ruotare ripetutamente il polso a destra e a sinistra con la mano SINISTRA. + Preparati a ruotare ripetutamente il polso a destra e a sinistra con la mano DESTRA. + Continua a ruotare il polso a destra e a sinistra per %1$d secondi. + Ora passa il telefono alla mano SINISTRA e continua con l\'attività successiva. + Ora passa il telefono alla mano DESTRA e continua con l\'attività successiva. + Continua con l\'attività successiva. + Attività completata. + Ti verrà richiesto di eseguire %1$s mentre stai seduto con il telefono prima in una mano, poi di nuovo nell\'altra. + Non posso eseguire l\'attività con la mano SINISTRA. + Non posso eseguire l\'attività con la mano DESTRA. + Posso eseguire l\'attività con entrambe le mani. + + Impossibile creare file + Impossibile rimuovere file di log sufficienti per raggiungere la soglia + Errore di impostazione dell\'attributo + File non contrassegnati eliminati (non contrassegnati caricati) + Errori multipli durante la rimozione dei log + Non ci sono dati disponibili. + Non è stata specificata nessuna directory di output + + Nessun dato + + Indietro + Immagine di %1$s + Campo firma + Tocca lo schermo e sposta il dito per firmare + Firmato + Senza firma + Selezionato + Non selezionato + Slider risposta. Da %1$s a %2$s + Immagine senza etichetta + Inizia + attivo + corretto + non corretto + inattivo + Tassello gioco mnemonico + Anteprima immagine + Immagine scattata + Anteprima acquisizione video + Video acquisito + Impossibile posizionare il disco di dimensione %1$s sul disco di dimensione %2$s + Destinazione + Torre + Tocca due volte per posizionare il disco + Tocca due volte per selezionare il disco superiore + Dimensioni dischi: %1$s + Vuota + Intervallo da %1$s a %2$s + Pila composta da + e + Punto: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-iw/strings.xml b/backbone/src/main/res/values-iw/strings.xml new file mode 100644 index 000000000..e602140fb --- /dev/null +++ b/backbone/src/main/res/values-iw/strings.xml @@ -0,0 +1,396 @@ + + + + + הסכמה + שם פרטי + שם משפחה + חובה + סקור/י + סקור/י את הטופס הבא, והקש/י על ״אני מסכים/ה״ אם הינך מוכן/ה להמשיך. + סקור/י + חתימה + חתום/י עם האצבע על הקו שלמטה. + חתום/י כאן + עמוד %1$ld מתוך %2$ld + + ברוך/ה הבא/ה + איסוף נתונים + פרטיות + שימוש בנתונים + סקר מחקר + משימות מחקר + התחייבות זמן + ביטול השתתפות + פרטים נוספים + + קבל/י מידע נוסף על איסוף הנתונים + קבל/י מידע נוסף על השימוש בנתונים + קבל/י מידע נוסף בנושא ההגנה על הפרטיות והזהות שלך + קבל/י מידע נוסף תחילה בנושא המחקר + קבל/י מידע נוסף בנושא סקר המחקר + קבל/י מידע נוסף בנושא השפעת המחקר על זמנך + קבל/י מידע נוסף בנושא המשימות הכרוכות + קבל/י מידע נוסף בנושא ביטול ההשתתפות + + אפשרויות שיתוף + שתף את הנתונים שלי עם %1$s ועם חוקרים מוסמכים מכל העולם + שתף את הנתונים שלי רק עם %1$s + %1$s תקבל את נתוני המחקר שבו השתתפת.\n\nשיתוף נרחב יותר של נתוני המחקר המקודדים שלך (שאינם מכילים מידע כגון שמך) עשוי לתרום למחקר זה ולמחקרים עתידיים. + קבל/י מידע נוסף בנושא שיתוף נתונים + + השם של %1$s (מודפס) + החתימה של %1$s + תאריך + + שלב %1$s מתוך %2$s + + ערך שאינו תקין + %1$s חורג מהערך המרבי המותר (%2$s). + %1$s נמוך מהערך המינימלי המותר (%2$s). + %1$s הוא לא ערך תקין. + + כתובת דוא״ל לא תקינה: %1$s + + הקש/י כתובת + לא ניתן למצוא את הכתובת שצוינה + לא ניתן לזהות את מיקומך הנוכחי. הקלד/י כתובת או עבור/י אל מיקום שבו קליטת ה-GPS טובה יותר, אם אפשר. + הגישה אל שירותי המיקום נדחתה. הענק/י ליישום זה הרשאה להשתמש בשירותי המיקום דרך ״הגדרות״. + לא ניתן למצוא תוצאה עבור הכתובת שהוקשה. ודא/י שהכתובת תקינה. + יתכן שאינך מחובר/ת לאינטרנט, או שחרגת מהמספר המרבי המותר של בקשות בדיקת כתובת. אם אינך מחובר/ת לאינטרנט, הפעל/י את הרשת האלחוטית בכדי לענות על שאלה זו, דלג/י על השאלה אם כפתור הדילוג זמין, או חזור/י ובצע/י שוב את הסקר כשתהיה/י מחובר/ת לאינטרנט. אחרת, נסה/י שוב בעוד כמה דקות. + + תוכן המלל חורג מהאורך המרבי: %1$s + + המצלמה אינה זמינה במסך מפוצל. + + שלח בדוא״ל + israelisraeli@example.com + סיסמה + הקש/י סיסמה + אשר/י + הקש/י את הסיסמה שוב + הסיסמאות לא היו זהות. + מידע נוסף + ישראל + ישראלי + מין + בחר/י מין + תאריך לידה + בחר/י תאריך + + אימות + אימות הדוא״ל שלך + הקש/י על הקישור שלמטה אם לא קיבלת דוא״ל אימות וברצונך שהוא יישלח שוב. + שלח הודעת דוא״ל לאימות מחדש + + התחבר + שכחת את הסיסמה? + + הקש/י את קוד הגישה + אשר/י קוד גישה + קוד הגישה נשמר + קוד הגישה מאומת + הקש/י את קוד הגישה הישן + הקש/י את קוד הגישה החדש + אשר/י את קוד הגישה החדש + קוד הגישה שגוי + קודי הגישה לא היו זהים. נסה/י שוב. + בצע/י אימות באמצעות Touch ID + שגיאת Touch ID + ניתן להשתמש בתווי ספרות בלבד. + מד התקדמות של הקשת קוד גישה + הוקשו %1$s מתוך %2$s ספרות + שכחת את קוד הגישה? + + לא ניתן היה להוסיף את פריט צרור המפתחות. + לא ניתן היה לעדכן את פריט צרור המפתחות. + לא ניתן היה למחוק את פריט צרור המפתחות. + לא ניתן היה למצוא את פריט צרור המפתחות. + + ‎A+‎ + ‎A-‎ + ‎AB+‎ + ‎AB-‎ + ‎B+‎ + ‎B-‎ + ‎O+‎ + ‎O-‎ + + נקבה + זכר + אחר + + לא + כן + + ס״מ + רגל + אינץ׳ + + הקש/י בכדי לענות + בחר/י תשובה + הקש/י לבחירה + הקש/י לכתיבה + + אני מסכים/ה + ביטול + אישור + נקה + איני מסכים/ה + סיום + התחל + פרטים נוספים + הבא + דלג + דלג על שאלה זו + הפעל את שעון העצר + שמור למועד מאוחר יותר + מחק את התוצאות + סיים את המשימה + שמור + נקה את התשובה + לא ניתן לשנות את התשובה הזאת. + + מתחיל פעילות בעוד + הפעילות הושלמה + הנתונים שלך ינותחו וכאשר התוצאות יהיו מוכנות תקבל/י הודעה. + נותרו %1$s שניות. + + צלם תמונה + צלם תמונה שוב + לא נמצאה מצלמה. לא ניתן להשלים שלב זה. + על-מנת לסיים שלב זה, אפשר/י ליישום זה לגשת למצלמה שלך ב״הגדרות״. + לא צוינה ספריית פלט עבור התמונות שצולמו. + לא ניתן היה לשמור את התמונה שצולמה. + + התחל הקלטה + הפסק הקלטה + צלם/י את הסרט מחדש + + כושר + מרחק (%1$s) + קצב לב (פעימות לדקה) + שב/י בצורה נוחה למשך %1$s. + לך/י הכי מהר שתוכל/י למשך %1$s. + פעילות זו בודקת את קצב הלב שלך ומודדת כמה רחוק תוכל/י ללכת תוך %1$s. + לך/י בחוץ בקצב המהיר ביותר שלך למשך %1$s. לסיום, התיישב/י והרגע/י למשך %2$s. בכדי להתחיל, הקש/י על ״התחל״. + + הליכה ואיזון + פעילות זו מודדת את ההליכה והאיזון שלך תוך כדי הליכה ועמידה במקום. אל תמשיך/י אם אינך יכול/ה ללכת בבטחה ללא סיוע. + מצא/י מקום שבו תוכל/י ללכת בבטחה וללא סיוע למרחק של %1$d צעדים בקו ישר. + הנח/י את הטלפון בכיס או בתיק ופעל/י על-פי הנחיות השמע. + כעת עמוד/י ללא תזוזה למשך %1$s. + עמוד/י ללא תזוזה למשך %1$s. + הסתובב/י והתהלך/י חזרה למקום שבו התחלת. + לך/י %1$d צעדים לכל היותר בקו ישר. + + מצא/י מקום שבו ניתן ללכת בקו ישר הלוך וחזור בבטחה. נסה/י לצעוד באופן רציף ולבצע פניות בקצוות המסלול, כאילו תוך הקפת קונוס דמיוני המסמן את הקצה.\n\nלאחר מכן, תתבקש/י ללכת במסלול מעגלי שלם ואז לעצור ולעמוד כשהידיים לצידי הגוף והרגליים בפיסוק קל ברוחב הכתפיים. + הקש/י על ״התחל״ כשתהיה/י מוכן/ה.\nלאחר מכן הנח/י את הטלפון בכיס או בתיק ובצע/י את ההוראות שיושמעו לך. + לך/י הלוך ושוב בקו ישר במשך %1$s, באופן ההליכה הרגיל שלך. + בצע/י סיבוב שלם ולאחר מכן עמוד/י למשך %1$s. + השלמת את הפעילות. + + מהירות הקשה + יד ימין + יד שמאל + פעילות זו מודדת את מהירות ההקשה שלך. + הנח/י את הטלפון על משטח ישר. + הקש/י על הכפתורים שמופיעים על המסך עם שתי אצבעות של אותה היד המתחלפות ביניהן לסירוגין. + הקש/י על הכפתורים שמופיעים על המסך בשתי אצבעות יד ימין המתחלפות ביניהן לסירוגין. + הקש/י על הכפתורים שמופיעים על המסך בשתי אצבעות יד שמאל המתחלפות ביניהן לסירוגין. + כעת חזור/י על אותה פעולה ביד ימין. + כעת חזור/י על אותה פעולה ביד שמאל. + הקש/י באצבע אחת, ולאחר מכן בשניה. נסה/י להקיש בקצב קבוע ככל שתוכל/י. המשך/י להקיש למשך %1$s. + הקש/י על ״התחל״ בכדי להתחיל. + הקש/י על ״הבא״ בכדי להתחיל. + הקש/י + סה״כ הקשות + הקש/י על הכפתורים בשתי אצבעות באופן עקבי ככל שניתן. + הקש/י על הכפתורים ביד ימין. + הקש/י על הכפתורים ביד שמאל. + דלג/י על יד זו + + קול + הקש/י על ״התחל״ בכדי להתחיל. + אמור/י ״אהההה״ לתוך המיקרופון למשך זמן ארוך ככל שתוכל/י. + נשום/י נשימה עמוקה ואמור/י ״אהההה״ לתוך המיקרופון למשך זמן ארוך ככל שתוכל/י. נסה/י לשמור על עוצמת קול קבועה כך שפסי השמע יישארו כחולים. + פעילות זו מעריכה את קולך על-ידי הקלטתו דרך המיקרופון שבתחתית הטלפון. + חזק מדי + לא ניתן להקליט שמע + המתן/י עד לסיום מדידת רעשי הרקע. + רעש הרקע חזק מדי ולא ניתן להקליט את קולך. עבור/י למקום שקט יותר ונסה/י שוב. + כשתהיה/י מוכן, הקש/י על ״הבא״. + + מדידת היכולת לשמוע צלילים + פעילות זו מודדת את יכולתך לשמוע צלילים שונים. + לפני שתתחיל/י, חבר/י את האזניות והרכב/י אותן באזניים. + הקש/י על ״התחל״ בכדי להתחיל. + כעת הינך אמור/ה לשמוע צליל. כוונן/י את עוצמת הקול בעזרת פקדי הבקרה בצד המכשיר.\n\nהקש/י על הכפתור בכל שלב בכדי להתחיל. + הקש/י על הכפתור בכל פעם שתתחיל/י לשמוע צליל. + ‏%1$s הרץ, שמאל + ‏%1$s הרץ, ימין + + זכרון מרחבי + פעילות זו מודדת את הזכרון המרחבי שלך לטווח קצר על-ידי הנחיה לחזור על הסדר שבו נדלקו ה%1$s. + פרחים + פרחים + כמה מה%1$s יידלקו זה אחר זה. הקש/י על ה%2$s בסדר זהה לזה שבו הם נדלקו. + כמה מה%1$s יידלקו זה אחר זה. הקש/י על ה%2$s בסדר הפוך מזה שבו הם נדלקו. + בכדי להתחיל, הקש/י על ״התחל״ וצפה/י בתשומת לב. + %1$s + תוצאה + צפה/י ב%1$s נדלקים + הקש/י על ה%1$s לפי הסדר שבו הם נדלקים + הקש/י על ה%1$s בסדר הפוך + הרצף הושלם + להמשך, הקש/י על ״הבא״. + נסה/י שוב + לא השלמת את המשימה בזמן. הקש/י על ״הבא״ להמשך. + נגמר הזמן + אזל הזמן.\nהקש/י על ״הבא״ להמשך. + המשחק הושלם + מושהה + להמשך, הקש/י על ״הבא״. + + זמן תגובה + פעילות זו מעריכה את משך הזמן שלוקח לך להגיב לרמז חזותי. + נער/י את המכשיר בכיוון כלשהו ברגע שהנקודה הכחולה מופיעה על המסך. תתבקש/י לעשות זאת %D פעמים. + הקש/י על ״התחל״ בכדי להתחיל. + נסיון %1$s מתוך %2$s + נער/י את המכשיר במהירות כשמופיע העיגול הכחול + + מגדל האנוי + פעילות זו מעריכה את מיומנויות פתרון הפאזלים שלך. + הזז/י את המערום כולו אל הפלטפורמה המסומנת בכמה שפחות מהלכים. + הקש/י על ״התחל״ בכדי להתחיל + פתור/י את הפאזל + מספר מהלכים: %1$s \n %2$s + איני מצליח/ה לפתור את הפאזל + + הליכה מתוזמנת + פעילות זו מודדת את תפקוד פלג הגוף התחתון שלך. + מצא/י מקום, עדיף בחוץ, שבו תוכל/י ללכת במשך %1$s בקו ישר במהירות המרבית שניתן, אך בבטחה. אל תאט/י עד שתעבור/י את קו הסיום. + הקש/י על ״הבא״ בכדי להתחיל. + אמצעי עזר + השתמש/י באותו אמצעי עזר בכל בדיקה. + האם הינך חובש/ת תומך לקרסול? + האם הינך משתמש/ת באמצעי עזר? + הקש/י כאן לבחירת תשובה. + ללא + מקל הליכה ליד אחת + קב ליד אחת + מקל הליכה לשתי הידיים + קביים לשתי הידיים + הליכון + לך/י עד %1$s בקו ישר. + הסתובב/י והתהלך/י חזרה למקום שבו התחלת. + הקש/י על ״סיום״ לאחר שתסיים/י. + + PASAT + PVSAT + PAVSAT + הבדיקה ״חיבור סדרתי מתוזמן של שמע״ מודדת את מהירות עיבוד המידע ויכולת החישוב שלך בשמיעה. + הבדיקה ״חיבור סדרתי מתוזמן של ראיה״ מודדת את מהירות עיבוד המידע ויכולת החישוב שלך בראיה. + הבדיקה ״חיבור סדרתי מתוזמן של שמע וראיה״ מודדת את מהירות עיבוד המידע ויכולת החישוב שלך, הן בשמיעה והן בראייה. + ספרות בודדות מוצגות כל %1$s שניות.\nעליך להוסיף ספרה חדשה אל זו שנמצאת ממש לפניה.\nלתשומת לבך, אין לחשב את הסה״כ המצטבר, אלא רק את סכום שני המספרים האחרונים. + הקש/י על ״התחל״ בכדי להתחיל. + זכור/י ספרה ראשונה זו. + הוסף/י ספרה חדשה זו לספרה הקודמת. + - + + מבחן דיסקית של %1$s חורים + פעילות זו מודדת את תפקוד פלג הגוף העליון בכך שהיא מבקשת ממך להניח דיסקית עגולה בתוך חור. את הפעולה הזו תתבקש/י לבצע %1$s פעמים. + כעת תתבצע בדיקה של היד השמאלית והימנית שלך.\nעליך להרים את הדיסקית במהירות המרבית ולהכניס אותה לחור. לאחר שתבצע/י זאת %1$s פעמים, עליך להוציא אותה שוב %2$s פעמים. + כעת תתבצע בדיקה של היד הימנית והשמאלית שלך.\nעליך להרים את הדיסקית במהירות המרבית ולהכניס אותה לחור. לאחר שתבצע/י זאת %1$s פעמים, עליך להוציא אותה שוב %2$s פעמים. + הקש/י על ״התחל״ בכדי להתחיל. + מקם/י את הדיסקית בתוך החור באמצעות ידך השמאלית. + מקם/י את הדיסקית בתוך החור באמצעות ידך הימנית. + מקם/י את הדיסקית מאחורי הקו באמצעות ידך השמאלית. + מקם/י את הדיסקית מאחורי הקו באמצעות ידך הימנית. + הרם/י את הדיסקית בשתי אצבעות. + הרם/י את אצבעותיך כדי להפיל את הדיסקית. + + פעילות למדידת הרעד + פעילות זו מודדת את הרעד בידיים שלך בתנוחות שונות. מצא/י מקום שבו תוכל/י לשבת בנוחות במשך כל הפעילות. + החזק/י את הטלפון ביד שבה הבעיה מורגשת יותר, כמתואר בתמונה למטה. + החזק/י את הטלפון ביד ימין, כמתואר בתמונה למטה. + החזק/י את הטלפון ביד שמאל, כמתואר בתמונה למטה. + בהמשך תתבקש/י לבצע %1$s בישיבה עם הטלפון ביד. + משימה אחת + שתי משימות + שלוש משימות + ארבע משימות + חמש משימות + הקש/י על ״הבא״ בכדי להמשיך. + התכונן/י להחזיק את הטלפון על הירכיים. + התכונן/י להחזיק את הטלפון ביד שמאל כשזאת מונחת על הירכיים. + התכונן/י להחזיק את הטלפון ביד ימין כשזאת מונחת על הירכיים. + המשך/י להחזיק את הטלפון על הירכיים למשך %1$d שניות. + כעת החזק/י את הטלפון בגובה הכתפיים תוך יישור המרפק. + כעת החזק/י את הטלפון ביד שמאל בגובה הכתפיים תוך יישור המרפק. + כעת החזק/י את הטלפון ביד ימין בגובה הכתפיים תוך יישור המרפק. + המשך/י להחזיק את הטלפון תוך יישור המרפק למשך %1$d שניות. + כעת החזק/י את הטלפון בגובה הכתפיים תוך כיפוף המרפק. + כעת החזק/י את הטלפון ביד שמאל בגובה הכתפיים תוך כיפוף המרפק. + כעת החזק/י את הטלפון ביד ימין בגובה הכתפיים תוך כיפוף המרפק. + המשך/י להחזיק את הטלפון תוך כיפוף המרפק למשך %1$d שניות + כעת, תוך שמירה על מרפק מכופף, גע/י עם הטלפון באף מספר פעמים ברצף. + כעת, עם הטלפון ביד שמאל ותוך שמירה על מרפק מכופף, גע/י עם הטלפון באף מספר פעמים ברצף. + כעת, עם הטלפון ביד ימין ותוך שמירה על מרפק מכופף, גע/י עם הטלפון באף מספר פעמים ברצף. + המשך/י לגעת עם הטלפון באף למשך %1$d שניות + התכונן/י לבצע ״נפנוף מלכותי״ (סיבוב מפרק כף היד על צירו). + התכונן/י לביצוע ״נפנוף מלכותי״ תוך אחיזת הטלפון ביד שמאל (סיבוב מפרק כף היד על צירו). + התכונן/י לביצוע ״נפנוף מלכותי״ תוך אחיזת הטלפון ביד ימין (סיבוב מפרק כף היד על צירו). + המשך/י לבצע ״נפנוף מלכותי״ למשך %1$d שניות. + כעת העבר/י את הטלפון ליד שמאל והמשך/י למשימה הבאה. + כעת העבר/י את הטלפון ליד ימין והמשך/י למשימה הבאה. + המשך/י אל המשימה הבאה. + הפעילות הושלמה. + בהמשך תתבקש/י לבצע %1$s בישיבה כשהטלפון נמצא תחילה ביד אחת, ולאחר מכן ביד השניה. + אני לא יכול/ה לבצע את הפעולה הזו ביד שמאל. + אני לא יכול/ה לבצע את הפעולה הזו ביד ימין. + אני יכול/ה לבצע את הפעולה הזו בשתי הידיים. + + לא ניתן ליצור את הקובץ + לא ניתן היה להסיר מספיק קובצי רישום בכדי להגיע לסף + ארעה שגיאה בהגדרת המאפיין + הקובץ אינו מסומן כנמחק (אינו מסומן כקובץ שהועלה) + ארעו שגיאות מרובות במהלך הסרת קובצי הרישום + לא נמצאו נתונים שנאספו. + לא צוינה ספריית פלט + + אין נתונים + + הקודם + איור של %1$s + שדה חתימה ייעודי + גע/י במסך והזז/י את האצבע בכדי לחתום + חתום + לא חתום + נבחרו + לא נבחר + מחוון תגובה. טווח שנע בין %1$s ל-%2$s + תמונה ללא תיוג + התחל משימה + פעיל + נכון + לא נכון + ללא תנועה + אריח משחק זכרון + צלם תצוגה מקדימה + התמונה צולמה + תצוגה מקדימה של הסרט שצולם + הסרט שצולם + לא ניתן למקם דיסקית בגודל %1$s על-גבי דיסקית בגודל %2$s + יעד + מגדל + הקש/י פעמיים למיקום הדיסקית + הקש/י פעמיים לבחירת הדיסקית הכי עליונה + כולל דיסק בגדלים %1$s + ריק + נע בין %1$s ל-%2$s + הערימה מכילה + וגם + נקודה: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-ja/strings.xml b/backbone/src/main/res/values-ja/strings.xml new file mode 100644 index 000000000..2cf1fe904 --- /dev/null +++ b/backbone/src/main/res/values-ja/strings.xml @@ -0,0 +1,396 @@ + + + + + 同意 + + + 必須 + 確認 + 下記のフォームを確認して、続ける場合は“同意する”をタップしてください。 + 確認 + 署名 + 下の線の上に指で署名してください。 + ここに署名 + ページ%1$ld/%2$ld + + ようこそ + データの収集 + プライバシー + データの使用方法 + 調査アンケート + 調査タスク + 所要時間 + 同意の撤回 + 詳しい情報 + + データの収集方法についての詳しい情報を表示します + データの使用方法についての詳しい情報を表示します + プライバシーおよび個人を特定する情報の保護方法についての詳しい情報を表示します + まず調査についての詳しい情報を表示します + 調査アンケートについての詳しい情報を表示します + 調査にかかる時間についての詳しい情報を表示します + 必要なタスクについての詳しい情報を表示します + 同意の撤回についての詳しい情報を表示します + + 共有オプション + 自分のデータを%1$sおよび資格を持つ世界中の研究者と共有します + 自分のデータを%1$sとのみ共有します + %1$sは、この調査に参加したあなたの調査データを受信します。\n\n名前などの情報を含まない、大まかにコード化された調査データを共有すると、今回と将来の調査に役立てることができます。 + データ共有についての詳しい情報を表示します + + %1$sさんの名前(活字体) + %1$sさんの署名 + 日付 + + ステップ%1$s/%2$s + + 値が無効です + %1$sは許容される最大値(%2$s)を超えています。 + %1$sは許容される最小値(%2$s)より小さいです。 + %1$sは無効な値です。 + + メールアドレスが無効です: %1$s + + 住所を入力 + 指定した住所が見つかりませんでした + 現在地を確認できません。住所を入力するか、GPS信号の受信状況が良好な場所に移動してください。 + 位置情報サービスへのアクセスが拒否されました。“設定”でこのAppに位置情報サービスの使用を許可してください。 + 入力した住所の結果が見つかりません。住所が正しいことを確認してください。 + インターネットに接続されていないか、住所検索要求の最大数に達しました。インターネットに接続されていない場合は、Wi-Fiをオンにしてこの質問に回答するか、この質問をスキップするか(スキップボタンが表示されている場合)、またはインターネットに接続されているときにアンケートをやり直してください。インターネットに接続されている場合は、数分待ってからやり直してください。 + + 文字数が最大許容数を超えています: %1$s + + 分割画面ではカメラを使用できません。 + + メール + hiro_sato@example.com + パスワード + パスワードを入力 + 確認 + パスワードを再入力 + パスワードが一致しません。 + 追加情報 + John + Appleseed + 性別 + 性別を選択 + 生年月日 + 日付を選択 + + 確認 + メールを確認 + 確認メールが届いておらず、送信し直したい場合は、下のリンクをタップしてください。 + 確認メールを再送信 + + ログイン + パスワードをお忘れですか? + + パスコードを入力してください + パスコードを確認してください + パスコードが保存されました + パスコードが認証されました + 古いパスコードを入力してください + 新しいパスコードを入力してください + 新しいパスコードを確認してください + パスコードが正しくありません + パスコードが一致しません。もう一度入力してください。 + Touch IDで認証してください + Touch IDエラー + 入力できるのは数字のみです。 + パスコード入力の進行状況インジケータ + %1$s/%2$s桁を入力しました + パスコードをお忘れですか? + + キーチェーン項目を追加できませんでした。 + キーチェーン項目をアップデートできませんでした。 + キーチェーン項目を削除できませんでした。 + キーチェーン項目が見つかりませんでした。 + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + 女性 + 男性 + その他 + + いいえ + はい + + cm + ft + in + + タップで回答 + 答えを選択してください + タップで選択 + タップで書き込む + + 同意する + キャンセル + OK + 消去 + 同意しない + 完了 + 開始 + 詳しい情報 + 次へ + スキップ + この質問をスキップ + タイマーを開始 + 後で使用するために保存 + 結果を破棄 + タスクを終了 + 保存 + 答えを消去 + この答えは変更できません。 + + アクティビティ開始まで + アクティビティ完了 + データは分析され、結果の準備ができると通知されます。 + 残り%1$s秒です。 + + イメージを取り込む + 再度イメージを取り込む + カメラが見つからないため、この手順を完了できません。 + この手順を完了するには、“設定”でこのAppがカメラにアクセスすることを許可してください。 + 取り込んだイメージの出力ディレクトリが指定されていません。 + 取り込んだイメージを保存できませんでした。 + + 録画を開始 + 録画を停止 + 再度ビデオを取り込む + + 体力 + 距離(%1$s) + 心拍数(bpm) + %1$s間楽な姿勢で座ってください。 + できるだけ速く%1$s間歩いてください。 + このアクティビティでは、心拍数をモニタし、%1$sで歩ける距離を測定します。 + 屋外でできるだけ速いペースで%1$s間歩いてください。終わったら座って、%2$s間楽にしてください。始めるには、“開始”をタップしてください。 + + 歩行/バランス + このアクティビティでは、歩行中とじっと立っているときの歩行/バランスを測定します。介助なしで安全に歩行できない場合は、続行しないでください。 + 介助なしで安全に、まっすぐに%1$d歩程度歩ける場所を見つけてください。 + 電話をポケットまたはバッグに入れて、音声による指示に従ってください。 + ここで%1$s間じっと立っていてください。 + %1$s間じっと立っていてください。 + 後ろを向いて、元の場所まで歩いて戻ってください。 + まっすぐに%1$d歩まで歩いてください。 + + 安全にまっすぐ行ったり来たり歩ける場所を見つけてください。進路の終わりまで来たら、コーンを回るように折り返して歩き続けます。\n\n次に、完全な円を描いて回るように指示されます。その後、両腕を脇につけ、足を肩幅くらいに開いてじっと立ってください。 + 開始の準備ができたら、“開始”をタップしてください。\n次に、電話をポケットまたはバッグに入れて、音声による指示に従ってください。 + まっすぐ行ったり来たりして%1$s間歩きます。いつもと同じように歩いてください。 + 完全な円を描いて歩いてから、%1$s間じっと立ってください。 + アクティビティが完了しました。 + + タップの速度 + 右手 + 左手 + このアクティビティでは、タップの速度を測定します。 + 電話を平らな面に置いてください。 + 同じ手の2本の指を使って、画面上のボタンを交互にタップします。 + 右手の2本の指を使って、画面上のボタンを交互にタップしてください。 + 左手の2本の指を使って、画面上のボタンを交互にタップしてください。 + 次に、右手で同じテストを繰り返します。 + 次に、左手で同じテストを繰り返します。 + 一方の指でタップしてから、他方の指でタップします。できるだけ一定の時間間隔でタップしてください。%1$s間タップし続けます。 + 始めるには、“開始”をタップしてください。 + 始めるには、“次へ”をタップしてください。 + タップ + 合計タップ回数 + 2本の指を使って、できるだけ一定の速度でボタンをタップしてください。 + 右手を使ってボタンをタップしてください。 + 左手を使ってボタンをタップしてください。 + この手をスキップ + + + 始めるには、“開始”をタップしてください。 + マイクに向かってできるだけ長く「あーーーー」と声を出してください。 + 大きく息を吸ってから、マイクに向かってできるだけ長く「あーーーー」と声を出してください。声の大きさを一定に保って、オーディオバーを青色のままにしてください。 + このアクティビティでは、電話の下部にあるマイクで録音することで、あなたの声を評価します。 + 大きすぎる + 声を録音できません + 背景ノイズのレベルを確認中です。しばらくお待ちください。 + 周囲のノイズが大きすぎるため、声を録音できません。静かな場所に移動してやり直してください。 + 準備ができたら“次へ”をタップしてください。 + + 聴力検査 + このアクティビティでは、さまざまな音がどのように聞こえるかが測定されます。 + 始める前にヘッドフォンを接続し、装着してください。 + 始めるには、“開始”をタップしてください。 + すると音が聞こえてくるはずです。デバイスの横にあるコントロールで音量を調整してください。\n\n始める準備ができたらボタンをタップしてください。 + 音が聞こえるたびにボタンをタップしてください。 + %1$s Hz、左 + %1$s Hz、右 + + 空間記憶 + このアクティビティでは、%1$sが明るく表示される順番を再現することで、短期空間記憶を測定します。 + + + %1$sのいくつかが1つずつ明るく表示されます。それらの%2$sを明るく表示された順番と同じ順番でタップしてください。 + %1$sのいくつかが1つずつ明るく表示されます。それらの%2$sを明るく表示された順番と逆の順番でタップしてください。 + 始めるには、“開始”をタップしてから、じっと見てください。 + %1$s + スコア + 明るく表示される%1$sに注意してください + 明るく表示された順番で%1$sをタップしてください + 逆の順番で%1$sをタップしてください + シーケンス完了 + 続けるには、“次へ”をタップしてください + やり直す + うまくできませんでした。\n続けるには、“次へ”をタップしてください。 + タイムアップ + 時間がなくなりました。\n続けるには、“次へ”をタップしてください。 + ゲーム完了 + 一時停止 + 続けるには、“次へ”をタップしてください + + 反応時間 + このアクティビティでは、視覚的な合図に反応するまでの時間が測定されます。 + 画面に青い点が表示されたらすぐにデバイスを振ってください。測定は%D回行います。 + 始めるには、“開始”をタップしてください。 + %1$s/%2$s回 + 青い円が表示されたら、すぐにデバイスを振ってください + + ハノイの塔 + このアクティビティでは、パズルの解決力を評価します。 + 塔全体を色の付いた台にできるだけ少ない回数で移動してください。 + 始めるには、“開始”をタップしてください + パズルを解く + 移動回数: %1$s \n %2$s + 降参 + + 歩行時間 + このアクティビティでは、下肢の機能を測定します。 + 約%1$sを安全にまっすぐ歩ける場所を見つけてください。できれば屋外が良いでしょう。このアクティビティでは安全にできるだけ速く歩いていただきます。ゴールを過ぎるまで速度を落とさないでください。 + 始めるには、“次へ”をタップしてください。 + 補助器具 + すべてのテストで同じ補助器具を使用してください。 + 短下肢装具をお付けになりますか? + 補助器具をお使いになりますか? + ここをタップして答えを選択。 + なし + 片側ケイン + 片側クラッチ + 両側ケイン + 両側クラッチ + 歩行器 + まっすぐ%1$s歩いてください。 + 後ろを向いて、元の場所まで歩いて戻ってください。 + 完了したら、“完了”をタップしてください。 + + PASAT + PVSAT + PAVSAT + PASAT(定速聴覚的連続加算)では、聴覚による情報処理速度と計算能力を測定します。 + PVSAT(定速視覚的連続加算)では、視覚による情報処理速度と計算能力を測定します。 + PAVSAT(定速聴覚および視覚的連続加算)では、聴覚および視覚による情報処理速度と計算能力を測定します。 + %1$s秒ごとに1つの数字が表示されます。\n表示された数字を、直前に表示された数字と足してください。\nすべての数字を足すのではありません。常に、表示された数字とその直前の数字の2つだけを足してください。 + 始めるには、“開始”をタップしてください。 + この最初の数字を覚えておいてください。 + この数字を直前の数字と足してください。 + - + + %1$sホールペグテスト + このアクティビティでは、円を穴に入れる動作で上肢機能を測定します。測定は%1$s回行います。 + 左手と右手の両方をテストします。\n円をできるだけすばやくつかんでください。穴に入れる動作を%1$s回繰り返したら、今度は穴から円を取り出す動作を%2$s回繰り返します。 + 右手と左手の両方をテストします。\n円をできるだけすばやくつかんでください。穴に入れる動作を%1$s回繰り返したら、今度は穴から円を取り出す動作を%2$s回繰り返します。 + 始めるには、“開始”をタップしてください。 + 左手で円を穴に置いてください。 + 右手で円を穴に置いてください。 + 左手で円を線の向こう側に置いてください。 + 右手で円を線の向こう側に置いてください。 + 2本の指で円をつまんでください。 + 指で円を持ち上げて移動させ落としてください。 + + 震え測定アクティビティ + このアクティビティでは、さまざまな位置で手の震えを測定します。このアクティビティが終わるまで楽な姿勢で座っていられる場所を見つけてください。 + 下の図のように、震えの大きい方の手で電話を持ってください。 + 下の図のように、右手で電話を持ってください。 + 下の図のように、左手で電話を持ってください。 + 電話を手に持って座ったまま、%1$sを行うように求められます。 + 1つのタスク + 2つのタスク + 3つのタスク + 4つのタスク + 5つのタスク + 続けるには、“次へ”をタップします。 + 電話を膝の上で持つ準備をしてください。 + 左手を使って電話を膝の上で持つ準備をしてください。 + 右手を使って電話を膝の上で持つ準備をしてください。 + 電話を膝の上で%1$d秒間持ち続けてください。 + 次に、手を伸ばして電話を肩の高さに持ってください。 + 次に、左手を伸ばして電話を肩の高さに持ってください。 + 次に、右手を伸ばして電話を肩の高さに持ってください。 + 手を伸ばして、%1$d秒間電話を持ち続けてください。 + 肘を曲げて、電話を肩の高さに持ってください。 + 肘を曲げて、電話を左手で肩の高さに持ってください。 + 肘を曲げて、電話を右手で肩の高さに持ってください。 + 肘を曲げて%1$d秒間電話を持ち続けてください。 + 次に、肘を曲げて電話で繰り返し鼻に触れてください。 + 次に、電話を左手に持って肘を曲げたまま、電話で繰り返し鼻に触れてください。 + 次に、電話を右手に持って肘を曲げたまま、電話で繰り返し鼻に触れてください。 + 電話で%1$d秒間鼻に触れ続けてください。 + 手首を回すように手を振る準備をしてください。 + 電話を左手に持って手首を回すように手を振る準備をしてください。 + 電話を右手に持って手首を回すように手を振る準備をしてください。 + %1$d秒間、手を振り続けてください。 + 電話を左手に持ち替えて、次のタスクに進んでください。 + 電話を右手に持ち替えて、次のタスクに進んでください。 + 次のタスクに進んでください。 + アクティビティが完了しました。 + 最初に一方の手、次に他方の手で電話を持って座ったまま、%1$sを行うように求められます。 + 左手ではこのアクティビティができません。 + 右手ではこのアクティビティができません。 + どちらの手でもこのアクティビティができます。 + + ファイルを作成できませんでした + しきい値に達するのに十分なログファイルを削除できませんでした + 属性の設定中にエラーが起きました + ファイルが削除済みとマークされていません(アップロード済みとマークされていません) + ログ削除時に複数のエラー + 収集したデータが見つかりませんでした。 + 出力ディレクトリが指定されていません + + データなし + + 戻る + %1$sのイラスト + 指定された署名フィールド + 画面をタッチしながら指を動かして署名します + 署名済み + 未署名 + 選択 + 選択解除 + 応答スライダ。範囲は%1$sから%2$sです + ラベルなしイメージ + タスクを開始します + 動いている + 正解 + 不正解 + 止まっている + 記憶ゲームタイル + プレビューを取り込む + 取り込んだイメージ + ビデオ取り込みのプレビュー + 取り込んだビデオ + サイズ%1$sの円盤をサイズ%2$sの円盤の上に置くことはできません + ターゲット + + ダブルタップして円盤を置く + ダブルタップして一番上の円盤を選択 + %1$sのサイズの円盤があります + + 範囲%1$s〜%2$s + 次の内容のスタック: + + ポイント: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-ko/strings.xml b/backbone/src/main/res/values-ko/strings.xml new file mode 100644 index 000000000..d3c317aae --- /dev/null +++ b/backbone/src/main/res/values-ko/strings.xml @@ -0,0 +1,397 @@ + + + + + 동의 + 이름 + + 필요함 + 검토 + 아래 양식을 검토하고 계속할 준비가 되었으면 동의를 탭하십시오. + 검토 + 서명 + 손가락을 사용하여 아래에 표시된 선 위로 서명하십시오. + 여기에 서명하기 + %1$ld/%2$ld페이지 + + 환영합니다. + 데이터 수집 + 개인 정보 보호 + 데이터 사용 + 연구 설문 + 연구 과제 + 소요 시간 + 철회하기 + 더 알아보기 + + 데이터 수집 방법에 관하여 더 알아보기 + 데이터 사용 방법에 관하여 더 알아보기 + 사용자의 개인 정보 및 신원 보호 방법에 관하여 더 알아보기 + 연구에 관하여 더 알아보기 + 연구 설문에 관하여 더 알아보기 + 이 연구가 사용자의 시간에 미치는 영향에 관하여 더 알아보기 + 관련된 과제에 관하여 더 알아보기 + 철회하기에 관하여 더 알아보기 + + 공유 옵션 + 나의 데이터를 %1$s 및 전세계의 인증된 연구원에게 공유 + 나의 데이터를 %1$s에게만 공유 + 사용자가 참여한 이 연구 조사의 데이터가 %1$s(으)로 보내집니다.\n\n코드화된 사용자의 연구 데이터를 더 광범위하게 공유할수록(사용자 이름 등의 정보는 제외) 현재 및 향후 실시될 연구에 도움이 됩니다. + 데이터 공유에 관하여 더 알아보기 + + %1$s의 이름 (인쇄됨) + %1$s의 서명 + 날짜 + + %1$s/%2$s단계 + + 유효하지 않은 값 + %1$s은(는) 최대 허용 값을 초과합니다(%2$s). + %1$s은(는) 최소 허용 값보다 작습니다(%2$s). + %1$s은(는) 유효한 값이 아닙니다. + + 이메일 주소가 유효하지 않음: %1$s + + 주소 입력 + 지정된 주소를 찾을 수 없음 + 사용자의 현재 위치를 확인할 수 없습니다. 주소를 입력하거나, 가능한 경우 GPS 신호가 좋은 곳으로 위치를 이동하십시오. + 위치 서비스에 대한 접근이 거부되었습니다. 설정에서 위치 서비스를 사용할 수 있는 권한을 이 App에 허용하십시오. + 입력한 주소에 대한 결과를 찾을 수 없습니다. 주소가 유효한지 확인하십시오. + 인터넷에 연결되어 있지 않거나 주소 검색을 요청할 수 있는 최대치를 초과하였습니다. 인터넷에 연결되어 있지 않으면 Wi-Fi를 켜고 질문에 답하십시오. 건너뛰기 버튼을 사용할 수 있는 경우 질문을 건너뛸 수 있습니다. 그렇지 않으면 인터넷에 연결되어 있을 때 다시 설문 조사를 재개하거나 몇 분 후에 다시 시도해 보십시오. + + 텍스트 내용이 최대 길이를 초과함: %1$s + + 분할된 화면에서는 카메라를 사용할 수 없습니다. + + 이메일 + jappleseed@example.com + 암호 + 암호 입력 + 확인 + 암호 다시 입력 + 암호가 일치하지 않습니다. + 추가 정보 + 길동 + + 성별 + 성별 선택 + 생년월일 + 날짜 선택 + + 확인 + 이메일 확인 + 확인 이메일을 받지 못했다면 아래 링크를 탭하여 다시 보낼 수 있습니다. + 확인 이메일 다시 보내기 + + 로그인 + 암호를 잊어버렸습니까? + + 암호 입력 + 암호 확인 + 암호가 저장됨 + 암호가 인증됨 + 이전 암호 입력 + 새로운 암호 입력 + 새로운 암호 확인 + 올바르지 않은 암호 + 암호가 일치하지 않습니다. 다시 시도하십시오. + Touch ID를 사용하여 인증하십시오. + Touch ID 오류 + 숫자만 허용됩니다. + 암호 입력 진행 과정 표시기 + %2$s개 중 %1$s개의 숫자가 입력됨 + 암호를 잊어버렸습니까? + + 키체인 항목을 추가할 수 없습니다. + 키체인 항목을 업데이트할 수 없습니다. + 키체인 항목을 삭제할 수 없습니다. + 키체인 항목을 찾을 수 없습니다. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + 여성 + 남성 + 기타 + + 아니요 + + + cm + ft + in + + 탭하여 대답하기 + 대답 선택하기 + 탭하여 선택하기 + 탭하여 쓰기 + + 동의 + 취소 + 확인 + 지우기 + 동의 안 함 + 완료 + 시작하기 + 더 알아보기 + 다음 + 건너뛰기 + 이 질문 건너뛰기 + 타이머 시작 + 나중을 위해 저장 + 결과 삭제 + 과제 종료 + 저장 + 대답 지우기 + 이 대답을 수정할 수 없습니다. + + 실험 시작까지 + 실험 완료 + 데이터를 분석하여 결과가 준비되면 통보합니다. + %1$s초 남았습니다. + + 이미지 캡처 + 이미지 다시 캡처 + 카메라를 찾을 수 없습니다.\u0020\u0020이 단계를 완료할 수 없습니다. + 이 단계를 완료하려면 설정에서 이 App이 카메라에 접근할 수 있도록 허용하십시오. + 캡처한 이미지를 위한 출력 디렉토리가 지정되지 않았습니다. + 캡처한 이미지를 저장할 수 없습니다. + + 녹화 시작 + 녹화 중단 + 비디오 다시 캡처 + + 피트니스 + 거리(%1$s) + 심박수(bpm) + %1$s 동안 편하게 앉아 있으십시오. + %1$s 동안 최대한 빨리 걸으십시오. + 이 실험은 사용자의 심박수를 모니터하고 %1$s 동안 얼마나 멀리 걸을 수 있는지를 측정합니다. + 실외에서 최대한 빠른 페이스로 %1$s 동안 걸으십시오. 걷기가 끝나면 편하게 앉아서 %2$s 동안 휴식하십시오. 시작하려면 시작하기를 탭하십시오. + + 보행 및 균형 + 이 실험은 사용자가 걸을 때와 가만히 서 있을 때의 보행 및 균형 능력을 측정합니다. 도움 없이 안전하게 걸을 수 있는 상황이 아닌 경우 이 실험을 중단하십시오. + 다른 도움 없이 일직선으로 안전하게 약 %1$d보를 걸을 수 있는 장소를 찾으십시오. + 전화를 주머니 또는 가방에 넣고 오디오 지침을 따르십시오. + 이제 %1$s 동안 가만히 서 있으십시오. + %1$s 동안 가만히 서 있으십시오. + 뒤로 돌아서 시작한 지점으로 되돌아가십시오. + 일직선으로 최대 %1$d보를 걸으십시오. + + 일직선으로 안전하게 왔다 갔다 할 수 있는 공간을 찾으십시오. 계속 걷다가 반환점에서 마치 원뿔형 도로 표지가 있는 것처럼 주변을 돌아서 되돌아갑니다.\n\n그런 다음, 원을 그리며 돌아서서 양팔을 옆구리에 대고 발은 어깨너비만큼 벌린 채 서 있으십시오. + 시작할 준비가 되면 시작하기를 탭하십시오.\n그런 다음 전화기를 주머니나 가방에 넣고 오디오 지침을 따르십시오. + %1$s 동안 일직선으로 왔다 갔다 걸으십시오. 평상시 걷는 것처럼 걸으면 됩니다. + 원을 그리며 돌아선 다음 %1$s 동안 가만히 서 있으십시오. + 이 실험을 완료했습니다. + + 탭하기 속도 + 오른손 + 왼손 + 이 실험은 사용자의 탭하기 속도를 측정합니다. + 전화기를 평평한 표면에 두십시오. + 동일한 손의 두 손가락으로 화면의 버튼을 번갈아 탭하십시오. + 오른손의 두 손가락으로 화면의 버튼을 번갈아 탭하십시오. + 왼손의 두 손가락으로 화면의 버튼을 번갈아 탭하십시오. + 이제 오른손으로 동일한 테스트를 반복하십시오. + 이제 왼손으로 동일한 테스트를 반복하십시오. + 한 손가락을 탭하고 다른 손가락을 탭하십시오. 탭 간격이 거의 없도록 연속으로 탭하십시오. %1$s를 계속 탭하십시오. + 시작하려면 시작하기를 탭하십시오. + 시작하려면 다음을 탭하십시오. + + 탭한 총 횟수 + 두 손가락으로 버튼을 빠르게 연속으로 탭하십시오. + 오른손으로 버튼을 탭하십시오. + 왼손으로 버튼을 탭하십시오. + 이 손 건너뛰기 + + 음성 + 시작하려면 시작하기를 탭하십시오. + 마이크에 대고 최대한 길게 ‘아’하고 말하십시오. + 숨을 크게 들이쉰 다음 마이크에 대고 최대한 길게 ‘아’하고 말하십시오. 오디오 막대가 파란색으로 지속되도록 발성 음량을 꾸준하게 유지하십시오. + 이 실험은 전화 하단에 있는 마이크로 목소리를 녹음하여 사용자의 음성을 평가합니다. + 소리가 너무 큼 + 오디오를 녹음할 수 없습니다. + 주변 소음 단계를 측정하는 동안 기다리십시오. + 음성을 녹음하기에 주변 소음이 너무 큽니다. 좀 더 조용한 장소로 이동한 다음 다시 시도하십시오. + 준비되면 다음을 탭하십시오. + + 순음 청력 검사 + 이 실험은 다른 사운드를 들을 수 있는 사용자의 청력을 측정합니다. + 시작하기 전에 헤드폰을 연결하고 착용하십시오. + 시작하려면 시작하기를 탭하십시오. + 이제 소리가 들립니다. 기기 측면에 있는 제어기를 사용하여 음량을 조절하십시오.\n\n시작할 준비가 되었으면 버튼을 탭하십시오. + 사운드가 들리기 시작할 때마다 버튼을 탭하십시오. + %1$sHz, 왼쪽 + %1$sHz, 오른쪽 + + 공간 기억 + 이 실험은 %1$s에 불이 들어오는 순서를 반복하도록 하여 사용자의 단기 공간 기억 능력을 측정합니다. + + + 일부 %1$s에 한 번에 하나씩 불이 들어옵니다. 이 %2$s을(를) 불이 들어온 순서와 같은 순서로 탭하십시오. + 일부 %1$s에 한 번에 하나씩 불이 들어옵니다. 이 %2$s을(를) 불이 들어온 순서와 반대로 탭하십시오. + 시작하려면 시작하기를 탭한 다음 주의깊게 보십시오. + %1$s + 점수 + %1$s에 불이 들어오는 것을 잘 보십시오. + %1$s을(를) 불이 들어온 순서대로 탭하십시오 + %1$s을(를) 반대 순서대로 탭하십시오. + 순서 완료 + 계속하려면 다음을 탭하십시오. + 다시 시도 + 이번에는 성공적으로 완료하지 못했습니다. 계속하려면 다음을 탭하십시오. + 제한 시간 종료 + 시간이 초과되었습니다.\n계속하려면 다음을 탭하십시오. + 게임 종료 + 일시 정지됨 + 계속하려면 다음을 탭하십시오. + + 반응 시간 + 이 실험은 시각 신호에 대한 사용자의 반응 시간을 평가합니다. + 파란 점이 화면에 나타나면 방향에 상관없이 즉시 기기를 흔드십시오. 이 동작을 %D번 해야 합니다. + 시작하려면 시작하기를 탭하십시오. + %1$s/%2$s 시도 + 파란 원이 나타나면 재빨리 기기를 흔드십시오. + + 하노이의 탑 + 이 실험은 사용자의 퍼즐 푸는 능력을 평가합니다. + 아래 스택 전체를 최대한 적게 이동하여 하이라이트된 플랫폼으로 옮기십시오. + 시작하려면 시작하기를 탭하십시오. + 퍼즐을 푸십시오. + 이동 횟수: %1$s \n %2$s + 이 퍼즐을 풀 수 없습니다. + + 보행 테스트 + 이 실험은 사용자의 하지 기능을 측정합니다. + 되도록 실외로 장소를 골라서 안전하면서도 최대한 빨리 일직선으로 약 %1$s를 걸으십시오. 결승선을 지나기 전에는 속도를 늦추지 마십시오. + 시작하려면 다음을 탭하십시오. + 보조 기구 + 매 테스트에 동일한 보조 기구를 사용하십시오. + 단하지 보조기를 착용하십니까? + 보조 기구를 사용하십니까? + 여기를 탭하여 대답을 선택하십시오. + 없음 + 편측 지팡이 + 편측 목발 + 양측 지팡이 + 양측 목발 + 워커/롤레이터 + 일직선으로 %1$s까지 걸으십시오. + 뒤로 돌아서 시작한 지점으로 되돌아가십시오. + 완료하면 완료를 탭하십시오. + + PASAT + PVSAT + PAVSAT + PASAT 테스트(Paced Auditory Serial Addition Test)는 사용자의 청각 정보 처리 속도 및 계산 능력을 측정합니다. + PVSAT 테스트(Paced Visual Serial Addition Test)는 사용자의 시각 정보 처리 속도 및 계산 능력을 측정합니다. + PAVSAT 테스트(Paced Auditory and Visual Serial Addition Test)는 사용자의 청각 및 시각 정보 처리 속도 및 계산 능력을 측정합니다. + 한 자릿수의 숫자가 %1$s초마다 제시됩니다.\n새로운 숫자가 제시될 때마다 직전에 제시된 숫자에 더하십시오.\n총 누계를 계산하지 않도록 주의하십시오. 직전에 제시된 두 개의 숫자만 더하십시오. + 시작하려면 시작하기를 탭하십시오. + 아래 첫 번째 숫자를 기억하십시오. + 아래 새로운 숫자를 이전 숫자에 더하십시오. + - + + %1$s-홀 페그 테스트 + 이 실험은 페그를 구멍에 넣는 동작을 통해 사용자의 상지 기능을 측정합니다. 이 동작을 %1$s번 해야 합니다. + 왼손과 오른손을 모두 테스트합니다.\n페그를 최대한 빨리 집어서 구멍에 넣으십시오. 이 동작을 %1$s번 완료하면, 페그를 제거하는 동작을 다시 %2$s번 하십시오. + 오른손과 왼손을 모두 테스트합니다.\n페그를 최대한 빨리 집어서 구멍에 넣으십시오. 이 동작을 %1$s번 완료하면, 페그를 제거하는 동작을 다시 %2$s번 하십시오. + 시작하려면 시작하기를 탭하십시오. + 왼손을 사용하여 페그를 구멍에 넣으십시오. + 오른손을 사용하여 페그를 구멍에 넣으십시오. + 왼손을 사용하여 페그를 선 뒤에 놓으십시오. + 오른손을 사용하여 페그를 선 뒤에 놓으십시오. + 두 손가락을 사용하여 페그를 집으십시오. + 손가락을 떼어서 페그를 내려놓으십시오. + + 손떨림 실험 + 이 실험은 여러 가지 상황에서 손떨림 정도를 측정합니다. 이 실험을 위해 편안하게 앉아있을 수 있는 장소로 이동하십시오. + 아래 그림처럼 전화기를 더 자주 사용하는 손에 들고 있으십시오. + 아래 그림처럼 전화기를 오른손에 들고 있으십시오. + 아래 그림처럼 전화기를 왼손에 들고 있으십시오. + 전화를 손에 든 채로 앉아있는 동안 %1$s를 수행해야 합니다. + 한 건의 과제 + 두 건의 과제 + 세 건의 과제 + 네 건의 과제 + 다섯 건의 과제 + 다음을 탭하여 계속합니다. + 전화기를 무릎에 두는 동작을 준비하십시오. + 왼손으로 전화기를 쥐고 무릎에 두는 동작을 준비하십시오. + 오른손으로 전화기를 쥐고 무릎에 두는 동작을 준비하십시오. + 전화기를 %1$d초 동안 무릎에 두십시오. + 이제 전화기를 어깨 높이로 들고 있으십시오. + 이제 전화기를 왼손에 어깨 높이로 들고 있으십시오. + 이제 전화기를 오른손에 어깨 높이로 들고 있으십시오. + %1$d초 동안 손을 내민 채로 전화기를 들고 있으십시오. + 이제 팔꿈치를 굽히고 전화기를 어깨 높이에 들고 있으십시오. + 이제 왼쪽 팔꿈치를 굽히고 왼손으로 전화기를 어깨 높이에 들고 있으십시오. + 이제 오른쪽 팔꿈치를 굽히고 오른손으로 전화기를 어깨 높이에 들고 있으십시오. + %1$d초 동안 팔꿈치를 굽힌 채로 전화기를 들고 있으십시오. + 이제 팔꿈치를 굽히고 전화기를 코에 반복적으로 대십시오. + 이제 왼쪽 팔꿈치를 굽히고 왼손으로 전화기를 든 다음 전화기를 코에 반복적으로 대십시오. + 이제 오른쪽 팔꿈치를 굽히고 오른손으로 전화기를 든 다음 전화기를 코에 반복적으로 대십시오. + 전화기를 %1$d초 동안 코에 대고 있으십시오. + 반짝반짝 율동을 취할 준비를 하십시오. + 왼손으로 전화기를 들고 반짝반짝 율동을 취할 준비를 하십시오. + 오른손으로 전화기를 들고 반짝반짝 율동을 취할 준비를 하십시오. + %1$d초 동안 반짝반짝 율동을 수행하십시오. + 이제 전화기를 왼손으로 바꿔 들고 다음 과제를 진행하십시오. + 이제 전화기를 오른손으로 바꿔 들고 다음 과제를 진행하십시오. + 다음 과제를 진행합니다. + 실험이 완료되었습니다. + 전화를 한 손에 먼저 들었다가 다른 손으로 들면서 %1$s를 수행해야 합니다. + 저는 이 과제를 왼손으로 수행할 수 없습니다. + 저는 이 과제를 오른손으로 수행할 수 없습니다. + 저는 이 과제를 양손으로 수행할 수 있습니다. + + 파일을 생성할 수 없습니다. + 임계값에 이르기 위한 충분한 수의 로그 파일을 제거하는 데 실패했습니다. + 속성 설정 오류 + 삭제로 표시되지 않은 파일(업로드됨으로 표시되지 않음) + 로그 제거 중에 여러 개의 오류 발생 + 수집된 데이터를 찾을 수 없습니다. + 지정된 출력 디렉토리 없음 + + 데이터 없음 + + 뒤로 + %1$s의 그림 + 지정된 서명 필드 + 화면을 터치하고 손가락을 움직여서 서명하십시오. + 서명됨 + 서명 안 됨 + 선택됨 + 선택 안 됨 + 응답 슬라이더. %1$s에서 %2$s까지의 범위 + 꼬리표 없는 이미지 + 과제 시작 + 활동 + 맞음 + 틀림 + 대기 + 메모리 게임 타일 + 캡처 미리보기 + 캡처된 이미지 + 비디오 캡처 미리보기 + 캡처된 비디오 + 크기가 %1$s인 디스크를 크기가 %2$s인 디스크 위에 놓을 수 없습니다. + 대상 + + 디스크를 놓으려면 이중 탭하십시오. + 가장 위에 있는 디스크를 선택하려면 이중 탭하십시오. + %1$s 크기의 디스크가 있음 + 비어 있음 + %1$s에서 %2$s까지의 범위 + 다음으로 구성된 스택 + + 포인트: %1$d + + + \ No newline at end of file diff --git a/backbone/src/main/res/values-ms/strings.xml b/backbone/src/main/res/values-ms/strings.xml new file mode 100644 index 000000000..3d9840dfa --- /dev/null +++ b/backbone/src/main/res/values-ms/strings.xml @@ -0,0 +1,396 @@ + + + + + Keizinan + Nama Pertama + Nama Akhir + Diperlukan + Semak + Semak borang di bawah dan ketik Setuju jika anda bersedia untuk meneruskan. + Semak + Tandatangan + Sila tandatangan menggunakan jari anda pada garis di bawah. + Tandatangan Di Sini + Halaman %1$ld daripada %2$ld + + Selamat Datang + Pengumpulan Data + Privasi + Penggunaan Data + Tinjauan Kajian + Tugas Kajian + Komitmen Masa + Menarik Diri + Ketahui Lebih Lanjut + + Ketahui lebih lanjut tentang cara data dikumpulkan + Ketahui lebih lanjut tentang cara data digunakan + Ketahui lebih lanjut tentang cara privasi dan identiti anda dilindungi + Ketahui lebih lanjut tentang kajian terlebih dahulu + Ketahui lebih lanjut tentang tinjauan kajian + Ketahui lebih lanjut tentang impak kajian pada masa anda + Ketahui lebih lanjut tentang tugas yang terlibat + Ketahui lebih lanjut tentang menarik diri + + Pilihan Perkongsian + Kongsi data saya dengan %1$s dan penyelidik yang layak di seluruh dunia + Kongsi data saya dengan %1$s sahaja + %1$s akan menerima data kajian anda daripada penyertaan anda dalam kajian ini.\n\nBerkongsi data kajian berkod anda dengan lebih luas (tanpa maklumat seperti nama anda) mungkin memberi manfaat kepada penyelidikan ini dan penyelidikan masa hadapan. + Ketahui lebih lanjut tentang perkongsian data + + Nama %1$s (bertulis) + Tandatangan %1$s + Tarikh + + Langkah %1$s daripada %2$s + + Nilai tidak sah + %1$s melebihi nilai maksimum yang dibenarkan (%2$s). + %1$s kurang daripada nilai minimum yang dibenarkan (%2$s). + %1$s bukan nilai yang sah. + + Alamat e-mel tidak sah: %1$s + + Masukkan alamat + Tidak Menemui Alamat yang Ditentukan + Gagal menyelesaikan lokasi semasa anda. Sila taipkan alamat atau pergi ke lokasi dengan isyarat GPS yang lebih baik jika berkenaan. + Akses kepada perkhidmatan lokasi telah ditolak. Sila berikan aplikasi ini kebenaran untuk menggunakan perkhidmatan lokasi menerusi Seting. + Gagal mencari hasil untuk alamat yang dimasukkan. Sila pastikan alamat adalah sah. + Sama ada anda tidak disambungkan ke internet atau anda telah melebihi bilangan maksimum permintaan carian alamat. Jika anda tidak disambungkan ke internet, sila aktifkan Wi-Fi anda untuk menjawab soalan ini, langkau soalan ini jika butang langkau tersedia, atau kembali ke tinjauan apabila anda disambungkan ke internet. Sebaliknya, sila cuba lagi dalam masa beberapa minit. + + Kandungan teks melebihi panjang maksimum: %1$s + + Kamera tidak tersedia dalam skrin terpisah. + + E-mel + jappleseed@example.com + Kata Laluan + Masukkan kata laluan + Sahkan + Masukkan kata laluan sekali lagi + Kata laluan tidak sepadan. + Maklumat Tambahan + John + Appleseed + Jantina + Pilih jantina + Tarikh Lahir + Pilih tarikh + + Pengesahan + Sahkan E-mel anda + Ketik pautan di bawah jika anda tidak menerima e-mel pengesahan dan mahu e-mel tersebut dihantar sekali lagi. + Hantar Semula E-mel Pengesahan + + Log Masuk + Terlupa kata laluan? + + Masukkan kod laluan + Sahkan kod laluan + Kod laluan disimpan + Kod laluan disahkan + Masukkan kod laluan lama anda + Masukkan kod laluan baru anda + Sahkan kata laluan baru anda + Kod Laluan Salah + Kod laluan tidak sepadan. Cuba lagi. + Sila sahkan menggunakan Touch ID + Ralat Touch ID + Hanya aksara angka dibenarkan. + Penunjuk kemajuan entri kod laluan + %1$s daripada %2$s digit dimasukkan + Terlupa Kod Laluan? + + Tidak dapat menambah item Rantai Kunci. + Tidak dapat mengemas kini item Rantai Kunci. + Tidak dapat memadamkan item Rantai Kunci. + Tidak dapat mencari item Rantai Kunci. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Perempuan + Lelaki + Lain + + Tidak + Ya + + cm + ka + inci + + Ketik untuk jawab + Pilih jawapan + Ketik untuk pilih + Ketik untuk tulis + + Setuju + Batal + OK + Kosongkan + Tidak Setuju + Selesai + Mulakan + Ketahui lebih lanjut + Seterusnya + Langkau + Langkau soalan ini + Mulakan Pemasa + Simpan untuk Kemudian + Buang Hasil + Tamatkan Tugas + Simpan + Kosongkan jawapan + Jawapan ini tidak boleh diubah suai. + + Memulakan aktiviti dalam + Aktiviti Selesai + Data anda akan dianalisis dan anda akan dimaklumkan apabila keputusan anda tersedia. + Tinggal %1$s saat. + + Tangkap Imej + Tangkap Semula Imej + Tiada kamera ditemui.\u0020\u0020Langkah ini tidak boleh dilengkapkan. + Untuk melengkapkan langkah ini, benarkan aplikasi ini mengakses kamera dalam Seting. + Tiada direktori output ditentukan untuk imej yang ditangkap. + Imej yang ditangkap tidak dapat disimpan. + + Mula Merakam + Henti Merakam + Rakam Semula Video + + Kecergasan + Jarak (%1$s) + Kadar Denyut Jantung (bpm) + Duduk dengan selesa selama %1$s. + Jalan selaju yang anda boleh selama %1$s. + Aktiviti ini memantau kadar denyut jantung anda dan mengukur jarak anda boleh berjalan dalam %1$s. + Jalan di luar bangunan secepat yang anda boleh selama %1$s. Apabila anda selesai, duduk dan berehat dengan selesa selama %2$s. Untuk memulakan, ketik Mulakan. + + Gaya Jalan dan Imbangan + Aktiviti ini mengukur gaya jalan dan keseimbangan anda semasa anda berjalan dan berdiri pegun. Jangan teruskan jika anda tidak boleh berjalan dengan selamat tanpa bantuan. + Cari tempat yang boleh anda berjalan dengan selamat tanpa bantuan sejauh %1$d langkah dalam garis lurus. + Masukkan telefon anda ke dalam saku atau beg dan ikuti arahan audio. + Sekarang berdiri pegun selama %1$s. + Berdiri pegun selama %1$s. + Pusing dan berjalan balik ke tempat anda bermula. + Jalan sehingga %1$d langkah dalam garis lurus. + + Cari tempat di mana anda boleh berjalan mundar mandir dalam garis lurus. Cuba berjalan berterusan dengan berpusing di kedua penghujung laluan anda, seperti anda berjalan mengelilingi kon.\n\nSeterusnya anda akan diarahkan untuk berpusing dalam bulatan penuh, kemudian berdiri tegak dengan tangan anda di sisi anda dan kaki anda dibuka selebar bahu. + Ketik Mulakan apabila anda bersedia untuk bermula.\nKemudian letakkan telefon anda di dalam saku atau beg dan ikuti arahan audio. + Berjalan mundar mandir dalam garis lurus selama %1$s. Jalan seperti biasa anda lakukan. + Buat pusingan penuh dan kemudian berdiri tegak selama %1$s. + Anda telah melengkapkan aktiviti. + + Kelajuan Ketikan + Tangan Kanan + Tangan Kiri + Aktiviti ini mengukur kelajuan mengetik anda. + Letakkan telefon anda di atas permukaan rata. + Gunakan dua jari pada tangan yang sama untuk mengetik butang pada skrin secara berselang-seli. + Gunakan dua jari pada tangan kanan anda untuk mengetik butang pada skrin secara berselang-seli. + Gunakan dua jari pada tangan kiri anda untuk mengetik butang pada skrin secara berselang-seli. + Sekarang ulang ujian yang sama menggunakan tangan kanan anda. + Sekarang ulang ujian yang sama menggunakan tangan kiri anda. + Ketik satu jari, kemudian ketik yang lain. Cuba mengetik dengan kadar yang sekata mungkin. Teruskan mengetik selama %1$s. + Ketik Mulakan untuk memulakan. + Ketik Seterusnya untuk bermula. + Ketik + Jumlah Ketikan + Ketik butang sekonsisten yang anda boleh menggunakan dua jari. + Ketik butang menggunakan tangan KANAN anda. + Ketik butang menggunakan tangan KIRI anda. + Langkau tangan ini + + Suara + Ketik Mulakan untuk memulakan. + Sebut “Aaaaah” ke dalam mikrofon selama yang anda boleh. + Tarik nafas panjang dan sebut “Aaaaah” ke dalam mikrofon selama yang anda boleh. Kekalkan kelantangan suara yang tetap agar bar audio kekal biru. + Aktiviti ini menilai suara anda dengan merakamnya menggunakan mikrofon di bahagian bawah telefon anda. + Terlalu Kuat + Gagal merakam audio + Sila tunggu sementara kami memeriksa aras hingar latar belakang. + Aras hingar ambien terlalu bising untuk merakamkan suara anda. Sila pergi ke tempat yang lebih senyap dan cuba lagi. + Ketik Seterusnya apabila bersedia. + + Audiometri Nada + Aktiviti ini mengukur keupayaan anda untuk mendengar bunyi berlainan. + Sebelum anda bermula, pasang dan pakai fon kepala anda. + Ketik Mulakan untuk memulakan. + Anda sepatutnya mendengar nada sekarang. Laraskan kelantangan menggunakan kawalan di sisi peranti anda.\n\nKetik butang apabila anda sedia untuk bermula. + Ketik butang setiap kali anda mula mendengar bunyi. + %1$s Hz, Kiri + %1$s Hz, Kanan + + Memori Ruang + Aktiviti ini mengukur memori ruang jangka pendek anda dengan meminta anda mengulangi tertib yang %1$s bernyala. + bunga + bunga + Sesetengah %1$s akan bernyala satu demi satu. Ketik %2$s tersebut dalam urutan yang sama ia menyala. + Sesetengah %1$s akan bernyala satu demi satu. Ketik %2$s tersebut dalam urutan terbalik ia menyala. + Untuk memulakan, ketik Mulakan, kemudian lihat dengan teliti. + %1$s + Skor + Lihat %1$s bernyala + Ketik %1$s dalam tertib ia bernyala + Ketik %1$s dalam tertib terbalik + Jujukan Selesai + Untuk meneruskan, ketik Seterusnya + Cuba Lagi + Anda tidak berjaya dalam pusingan tersebut. Ketik Seterusnya untuk meneruskan. + Habis Masa + Anda kehabisan masa.\nKetik Seterusnya untuk meneruskan. + Permainan Selesai + Dijedakan + Untuk meneruskan, ketik Seterusnya + + Masa Reaksi + Aktiviti ini menilai masa yang diambil untuk anda membalas kepada isyarat visual. + Goncang peranti dalam sebarang arah sebaik sahaja titik biru muncul pada skrin. Anda akan diminta untuk melakukan ini %D kali. + Ketik Mulakan untuk memulakan. + Percubaan %1$s daripada %2$s + Goncang peranti dengan cepat apabila bulatan biru muncul + + Menara Hanoi + Aktiviti ini menilai keupayaan penyelesaian teka-teki anda. + Alihkan keseluruhan tindanan ke platform yang diserlahkan dalam paling kurang pergerakan yang mungkin. + Ketik Mulakan untuk bermula + Selesaikan Teka-Teki + Bilangan Pergerakan: %1$s \n %2$s + Saya tidak boleh menyelesaikan teka-teki ini + + Jalan Bermasa + Aktiviti ini mengukur fungsi anggota badan bawah anda. + Cari sesuatu tempat, lebih baik jika di luar bangunan, yang boleh anda berjalan selama lebih kurang %1$s dalam garis lurus secepat mungkin, namun dengan selamat. Jangan perlahankan diri sehingga anda melepasi garisan penamat. + Ketik Seterusnya untuk bermula. + Peranti bantuan + Gunakan peranti bantuan yang sama untuk setiap ujian. + Adakah anda memakai ortosis buku lali kaki? + Adakah anda menggunakan peranti bantuan? + Ketik di sini untuk memilih jawapan. + Tiada + Tongkat Satu Tangan + Topang Satu Tangan + Tongkat Dua Tangan + Topang Dua Tangan + Pejalan/Rollator + Jalan sehingga %1$s dalam garis lurus. + Pusing dan berjalan balik ke tempat anda bermula. + Ketik Selesai apabila selesai. + + PASAT + PVSAT + PAVSAT + Ujian Penambahan Bersiri Pendengaran Berkadar mengukur kelajuan pemprosesan maklumat pendengaran dan keupayaan pengiraan anda. + Ujian Penambahan Bersiri Visual Berkadar mengukur kelajuan pemprosesan maklumat visual dan keupayaan pengiraan anda. + Ujian Penambahan Bersiri Pendengaran dan Visual Berkadar mengukur kelajuan pemprosesan maklumat pendengaran dan visual serta keupayaan pengiraan anda. + Digit tunggal dipaparkan setiap %1$s saat.\nAnda mesti menambahkan setiap digit baru dengan digit yang betul-betul sebelumnya.\nHarap maklum, anda tidak boleh mengira jumlah keseluruhan, namun jumlah dua nombor terakhir sahaja. + Ketik Mulakan untuk memulakan. + Ingati digit pertama ini. + Tambah digit baru ini pada yang sebelumnya. + - + + Ujian Pancang %1$s Lubang + Aktiviti ini mengukur fungsi anggota badan atas anda dengan meminta anda meletakkan pancang ke dalam lubang. Anda akan diminta melakukan ini %1$s kali. + Tangan kiri dan kanan anda akan diuji.\nAnda mesti mengutip pancang secepat mungkin, memasukkannya ke dalam lubang dan setelah dilakukan %1$s kali, keluarkannya sekali lagi %2$s kali. + Tangan kanan dan kiri anda akan diuji.\nAnda mesti mengutip pancang secepat mungkin, memasukkannya ke dalam lubang dan setelah dilakukan %1$s kali, keluarkannya sekali lagi %2$s kali. + Ketik Mulakan untuk memulakan. + Letakkan pancang ke dalam lubang menggunakan tangan kiri anda. + Letakkan pancang ke dalam lubang menggunakan tangan kanan anda. + Letakkan pancang di belakang garis menggunakan tangan kiri anda. + Letakkan pancang di belakang garis menggunakan tangan kanan anda. + Kutip pancang menggunakan dua jari. + Angkat jari untuk menjatuhkan pancang. + + Aktiviti Getaran + Aktiviti ini mengukur getaran tangan anda dalam pelbagai posisi. Cari tempat di mana anda boleh duduk dengan selesa sepanjang tempoh aktiviti ini. + Pegang telefon di tangan anda yang lebih terjejas seperti yang ditunjukkan dalam imej di bawah. + Pegang telefon di tangan KANAN anda seperti yang ditunjukkan dalam imej di bawah. + Pegang telefon di tangan KIRI anda seperti yang ditunjukkan dalam imej di bawah. + Anda akan diminta untuk melakukan %1$s sementara duduk dengan telefon di tangan anda. + satu tugas + dua tugas + tiga tugas + empat tugas + lima tugas + Ketik seterusnya untuk teruskan. + Bersedia untuk memegang telefon anda di riba anda. + Bersedia untuk memegang telefon anda di riba anda dengan tangan KIRI anda. + Bersedia untuk memegang telefon anda di riba anda dengan tangan KANAN anda. + Teruskan memegang telefon anda di riba anda selama %1$d saat. + Sekarang pegang telefon anda dengan tangan anda diluruskan pada ketinggian bahu. + Sekarang pegang telefon anda dengan tangan KIRI anda diluruskan pada ketinggian bahu. + Sekarang pegang telefon anda dengan tangan KANAN anda diluruskan pada ketinggian bahu. + Teruskan memegang telefon anda dengan tangan anda diluruskan selama %1$d saat. + Sekarang pegang telefon anda pada ketinggian bahu dengan siku anda dibengkokkan. + Sekarang pegang telefon anda dengan tangan KIRI anda pada ketinggian bahu dengan siku anda dibengkokkan. + Sekarang pegang telefon anda dengan tangan KANAN anda pada ketinggian bahu dengan siku anda dibengkokkan. + Teruskan memegang telefon anda dengan siku anda dibengkokkan selama %1$d saat + Terus bengkokkan siku anda, sentuh telefon anda ke hidung anda berulang kali. + Terus bengkokkan siku anda dengan telefon anda di tangan KIRI anda, sentuh telefon anda ke hidung anda berulang kali. + Terus bengkokkan siku anda dengan telefon anda di tangan KANAN anda, sentuh telefon anda ke hidung anda berulang kali. + Teruskan menyentuh telefon anda ke hidung anda selama %1$d saat + Bersedia untuk melakukan lambaian ratu (lambai dengan memusingkan pergelangan tangan anda). + Bersedia untuk melakukan lambaian ratu dengan telefon anda di tangan KIRI anda (lambai dengan memusingkan pergelangan tangan anda). + Bersedia untuk melakukan lambaian ratu dengan telefon anda di tangan KANAN anda (lambai dengan memusingkan pergelangan tangan anda). + Terus melakukan lambaian ratu untuk %1$d saat. + Sekarang tukar telefon ke tangan KIRI anda dan teruskan ke tugas seterusnya. + Sekarang tukar telefon ke tangan KANAN anda dan teruskan ke tugas seterusnya. + Teruskan ke tugas seterusnya. + Aktiviti selesai. + Anda akan diminta untuk melakukan %1$s sementara duduk dengan telefon di sebelah tangan dahulu, kemudian sekali lagi dengan tangan yang lain. + Saya tidak boleh laksanakan aktiviti ini menggunakan tangan KIRI saya. + Saya tidak boleh laksanakan aktiviti ini menggunakan tangan KANAN saya. + Saya boleh laksanakan aktiviti ini menggunakan kedua belah tangan. + + Tidak dapat mencipta fail + Tidak dapat mengeluarkan fail log yang mencukupi untuk mencapai nilai ambang + Ralat mengeset atribut + Fail tidak ditandakan sebagai dipadamkan (tidak ditandakan sebagai dimuat naik) + Berbilang ralat semasa mengeluarkan log + Tiada data yang dikumpulkan ditemui. + Tiada direktori output ditentukan + + Tiada Data + + Balik + Ilustrasi %1$s + Medan tandatangan yang ditetapkan + Sentuh skrin dan gerakkan jari anda untuk menandatangan + Ditandatangani + Tidak Ditandatangani + Dipilih + Dinyahpilih + Gelangsar respons. Julat dari %1$s hingga %2$s + Imej tanpa label + Mulakan tugas + aktif + betul + salah + kuisen + Jubin permainan memori + Pratonton tangkapan + Imej ditangkap + Pratonton rakaman video + Video dirakam + Gagal menempatkan cakera bersaiz %1$s pada cakera bersaiz %2$s + Sasaran + Menara + Dwiketik untuk menempatkan cakera + Dwiketik untuk memilih cakera teratas + Mempunyai cakera bersaiz %1$s + Kosong + Julat daripada %1$s hingga %2$s + Tindanan terdiri daripada + dan + Titik: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-nb/strings.xml b/backbone/src/main/res/values-nb/strings.xml new file mode 100644 index 000000000..00e384cec --- /dev/null +++ b/backbone/src/main/res/values-nb/strings.xml @@ -0,0 +1,396 @@ + + + + + Samtykke + Fornavn + Etternavn + Kreves + Se gjennom + Se gjennom skjemaet nedenfor, og trykk på Enig hvis du er klar til å fortsette. + Se gjennom + Signatur + Signer med fingeren på linjen nedenfor. + Signer her + Side %1$ld av %2$ld + + Velkommen + Datainnsamling + Personvern + Databruk + Undersøkelse + Oppgaver + Tidsbruk + Avslutte studien + Finn ut mer + + Finn ut mer om hvordan data samles inn + Finn ut mer om hvordan dataene brukes + Finn ut mer om hvordan personvernet og identiteten din beskyttes + Finn ut mer om studien + Finn ut mer om undersøkelsen + Finn ut mer om hvor mye tid studien krever + Finn ut mer om oppgavene i studien + Finn ut mer om å avslutte studien + + Delingsvalg + Del dataene dine med %1$s og erfarne forskere over hele verden + Del data kun med %1$s + %1$s vil motta data fra undersøkelsen du deltar i.\n\nEn videre deling av dine kodede opplysninger (navn og lignende fjernes) kan komme til nytte både i denne og framtidige undersøkelser. + Finn ut mer om datadeling + + Navnet til %1$s (store bokstaver) + Signaturen til %1$s + Dato + + Trinn %1$s av %2$s + + Ugyldig verdi + %1$s overstiger høyest tillatte verdi (%2$s). + %1$s er mindre enn lavest tillatte verdi (%2$s). + %1$s er ikke en gyldig verdi. + + Ugyldig e-postadresse: %1$s + + Skriv inn en adresse + Fant ikke angitt adresse + Finner ikke din nåværende posisjon. Skriv inn adressen, eller gå om mulig til et sted med bedre GPS-signal. + Du har ikke gitt appen tilgang til stedstjenester. Du kan endre tilgangen til stedstjenester i Innstillinger. + Fant ingen treff for angitt adresse. Kontroller at adressen er riktig og prøv igjen. + Du er enten ikke koblet til Internett, eller du har oversteget grensen for antall adressesøk. Hvis du ikke er koblet til Internett, må du slå på Wi-Fi for å svare på spørsmålet. Du kan hoppe over spørsmålet hvis hopp over-knappen er tilgjengelig, eller komme tilbake til undersøkelsen når du er koblet til Internett. Prøv ellers igjen om noen minutter. + + Teksten overstiger makslengden: %1$s + + Kameraet er ikke tilgjengelig på delt skjerm. + + E-post + jappleseed@example.com + Passord + Oppgi passord + Bekreft + Oppgi passordet igjen + Passordene er ikke like. + Tilleggsinformasjon + John + Appleseed + Kjønn + Angi kjønn + Fødselsdato + Angi dato + + Verifisering + Verifiser e-postadressen + Trykk på koblingen nedenfor hvis du ikke mottok verifiseringsmeldingen på e-post og vil sende den på nytt. + Send verifiseringsmelding på nytt + + Logg på + Glemt passordet? + + Oppgi kode + Bekreft kode + Kode arkivert + Kode godkjent + Oppgi den gamle koden + Angi den nye koden + Bekreft den nye koden + Koden er feil + Kodene er ikke like. Prøv igjen. + Autentiser med Touch ID + Touch ID-feil + Kun numeriske tegn er tillatt. + Framdriftsindikator for inntasting av kode + %1$s av %2$s sifre oppgitt + Glemt koden? + + Kunne ikke legge til nøkkelringobjekt. + Kunne ikke oppdatere nøkkelringobjekt. + Kunne ikke slette nøkkelringobjekt. + Fant ikke nøkkelringobjekt. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Kvinne + Mann + Annet + + Nei + Ja + + cm + fot + to + + Trykk for å svare + Velg et svar + Trykk for å velge + Trykk for å skrive + + Enig + Avbryt + OK + Fjern + Uenig + Ferdig + Start + Finn ut mer + Neste + Hopp over + Hopp over spørsmålet + Start tidtaker + Spar til senere + Forkast resultater + Avslutt oppgave + Arkiver + Fjern svar + Dette svaret kan ikke endres. + + Starter aktivitet om + Aktivitet fullført + Dataene vil bli analysert, og du blir varslet når resultatene er klare. + %1$s sekunder gjenstår. + + Ta bilde + Ta nytt bilde + Finner ikke kamera. Trinnet kunne ikke fullføres. + For å fullføre trinnet må du først gi appen tilgang til kameraet i Innstillinger. + Du har ikke angitt en katalog for bildene du tar. + Bildet kunne ikke arkiveres. + + Start opptak + Stopp opptak + Spill inn video på nytt + + Kondisjon + Strekning (%1$s) + Puls (slag/min) + Sitt komfortabelt i %1$s. + Gå så fort du kan i %1$s. + Denne aktiviteten leser av pulsen din og måler hvor langt du kan gå på %1$s. + Gå utendørs så fort du kan i %1$s. Når du er ferdig, setter du deg og hviler komfortabelt i %2$s.Trykk på Start for å begynne. + + Balanse og gange + Denne aktiviteten måler balansen og gangen din mens du går og står i ro. Ikke fortsett hvis du ikke kan gå trygt uten hjelp. + Finn et sted der du trygt og uten hjelp kan gå omtrent %1$d skritt i rett linje. + Legg telefonen i lommen eller en veske, og følg lydinstruksjonene. + Stå stille i %1$s. + Stå stille i %1$s. + Snu og gå tilbake der du startet. + Gå opptil %1$d skritt i rett linje. + + Finn et sted hvor du trygt kan gå fram og tilbake i en rett linje. Prøv å gå kontinuerlig når du skal snu, som om du skal gå rundt en kjegle.\n\nDu vil bli deretter bedt om å snu deg helt rundt og stå helt stille med armene ned langs siden og føttene med en skulderbreddes avstand. + Trykk på Kom i gang når du er klar til å starte.\nPlasser deretter telefonen i en lomme eller veske, og følg lydinstruksjonene. + Gå fram og tilbake i en rett linje i %1$s. Gå så normalt så mulig. + Snu deg helt rundt og stå stille i %1$s. + Du har fullført aktiviteten. + + Trykkhastighet + Høyre hånd + Venstre hånd + Denne aktiviteten måler trykkhastigheten din. + Plasser telefonen på en flat overflate. + Bruk to fingre på samme hånd til å trykke vekselvis på knappene på skjermen. + Bruk to fingre på høyre hånd til å trykke vekselvis på knappene på skjermen. + Bruk to fingre på venstre hånd til å trykke vekselvis på knappene på skjermen. + Gjør den samme testen med høyre hånd. + Gjør den samme testen med venstre hånd. + Trykk med én finger, og deretter den andre. Veksle mellom knappene i jevne intervaller. Fortsett å trykke i %1$s. + Trykk på Start for å begynne. + Trykk på Neste for å begynne. + Trykk + Trykk totalt + Trykk på knappene så jevnt som du klarer, med to fingre. + Trykk på knappene med din HØYRE hånd. + Trykk på knappene med din VENSTRE hånd. + Utelat denne hånden + + Stemme + Trykk på Start for å begynne. + Si «aaa» inn i mikrofonen så lenge du klarer. + Trekk pusten dypt inn og si «aaa» inn i mikrofonen så lenge du klarer. Hold et jevnt volum slik at lydstolpene forblir blå. + Aktiviteten vil evaluere stemmen din ved å ta den opp i mikrofonen i nedre kant av telefonen. + For høyt + Kan ikke ta opp lyd + Vent mens vi kontrollerer støynivået i bakgrunnen. + Støynivået fra omgivelsene er for høyt til å gjøre opptak av stemmen din. Gå til et sted mindre støy, og prøv igjen. + Trykk på Neste når du er klar. + + Toneaudiometri + Aktiviteten måler hvor godt du hører ulike lyder. + Koble til og ta på deg hodetelefonene før du begynner. + Trykk på Start for å begynne. + Du bør nå høre en tone. Juster volumet med kontrollene på siden av enheten.\n\nTrykk på knappen når du er klar til å begynne. + Trykk på knappen hver gang du hører en lyd. + %1$s Hz, venstre + %1$s Hz, høyre + + Visuell hukommelse + Denne aktiviteten måler den visuelle korttidshukommelsen din ved å be deg gjenta i hvilken rekkefølge %1$s lyser. + blomstrene + blomstrene + Noen av %1$s vil lyse én om gangen. Trykk på disse %2$s i samme rekkefølge som de lyser opp. + Noen av %1$s vil lyse én om gangen. Trykk på disse %2$s i omvendt rekkefølge av hvordan de lyser opp. + Begynn ved å trykke på Start, og følg så nøye med. + %1$s + Poeng + Se %1$s lyse opp + Trykk på %1$s i samme rekkefølge som de lyste + Trykk på %1$s i omvendt rekkefølge + Mønster fullført + Trykk på Neste for å fortsette + Prøv igjen + Testbesvarelsen var ikke helt riktig. Trykk på Neste for å fortsette. + Tiden er ute + Du gikk over tiden.\nTrykk på Neste for å fortsette. + Spill ferdig + Pause + Trykk på Neste for å fortsette + + Reaksjonstid + Aktiviteten måler hvor lang tid det tar før du reagerer på visuelle signaler. + Rist enheten så snart du ser den blå prikken vises på skjermen. Du vil bli bedt om å gjøre dette %D ganger. + Trykk på Start for å begynne. + Forsøk %1$s av %2$s + Rist enheten fort når den blå sirkelen vises + + Tårnet i Hanoi + Denne aktiviteten vurderer evnen din til å løse oppgaver. + Flytt hele stabelen til den markerte plattformen med så få trekk som mulig. + Trykk på Start for å begynne + Løs oppgaven + Antall trekk: %1$s \n %2$s + Jeg klarer ikke å løse oppgaven + + Gå på tid + Denne aktiviteten måler funksjonen i nedre kroppshalvdel. + Finn et sted, helst utendørs, der du trygt kan gå omtrent %1$s i rett linje, så raskt som mulig. Ikke senk farten til etter at du har passert mållinjen. + Trykk på Neste for å begynne. + Hjelpemiddel + Bruk samme hjelpemiddel i hver test. + Bruker du en ankel- eller fotortose? + Bruker du et hjelpemiddel? + Trykk her for å velge et svar. + Ingen + Én stokk + Én krykke + To stokker + To krykker + Gåstol/rollator + Gå opptil %1$s i rett linje. + Snu og gå tilbake der du startet. + Trykk på Ferdig når du har fullført oppgaven. + + PASAT + PVSAT + PAVSAT + PASAT-testen (Paced Auditory Serial Addition Test) måler hvor raskt du behandler lydinformasjon og evnen din til å gjøre utregninger. + PVSAT-testen (Paced Visual Serial Addition Test) måler hvor raskt du behandler synsinformasjon og evnen din til å gjøre utregninger. + PAVSAT-testen (Paced Auditory and Visual Serial Addition Test) måler hvor raskt du behandler lyd- og synsinformasjon og evnen din til å gjøre utregninger. + Enkeltsifre vises hvert %1$s. sekund.\nDu må legge det nye sifferet sammen med det som ble vist rett før.\nMerk at du ikke skal summere alle tallene, bare de to som ble vist sist. + Trykk på Start for å begynne. + Husk dette første sifferet. + Legg dette nye sifferet til det forrige. + - + + %1$s-hulls pluggtest + I denne aktiviteten skal du plassere en plugg i et hull for å måle funksjonen i armene dine. Du vil bli bedt om å gjøre det %1$s ganger. + Både venstre og høyre hånd vil bli testet.\nDu må plukke opp pluggen så fort som mulig, og legge den i hullet. Gjør dette %1$s ganger, og fjern den så igjen %2$s ganger. + Både høyre og venstre hånd vil bli testet.\nDu må plukke opp pluggen så fort som mulig, og legge den i hullet. Gjør dette %1$s ganger, og fjern den så igjen %2$s ganger. + Trykk på Start for å begynne. + Plasser pluggen i hullet med venstre hånd. + Plasser pluggen i hullet med høyre hånd. + Plasser pluggen bak streken med venstre hånd. + Plasser pluggen bak streken med høyre hånd. + Bruk to fingre til å løfte opp pluggen. + Løft fingrene for å slippe pluggen. + + Skjelving – aktivitet + Denne aktiviteten måler skjelving i hendene dine i forskjellige posisjoner. Finn et sted hvor du kan sitte i ro i løpet av denne aktiviteten. + Hold telefonen i hånden som er hardest rammet, som vist i bildet nedenfor. + Hold telefonen i din HØYRE hånd som vist i bildet nedenfor. + Hold telefonen i din VENSTRE hånd som vist i bildet nedenfor. + Du vil bli bedt om å utføre %1$s mens du sitter med telefonen i den ene hånden. + en oppgave + to oppgaver + tre oppgaver + fire oppgaver + fem oppgaver + Trykk på Neste for å gå videre. + Forbered deg på å holde telefonen i fanget. + Forbered deg på å holde telefonen i fanget med VENSTRE hånd. + Forbered deg på å holde telefonen i fanget med HØYRE hånd. + Hold telefonen i fanget i %1$d sekunder. + Hold telefonen med utstrakt hånd i skulderhøyde. + Hold telefonen med VENSTRE hånd utstrakt i skulderhøyde. + Hold telefonen med HØYRE hånd utstrakt i skulderhøyde. + Hold telefonen med utstrakt hånd i %1$d sekunder. + Hold telefonen i skulderhøyde med albuen bøyd. + Hold telefonen med VENSTRE hånd i skulderhøyde med albuen bøyd. + Hold telefonen med HØYRE hånd i skulderhøyde med albuen bøyd. + Hold telefonen med albuen bøyd i %1$d sekunder + Hold albuen bøyd, og trykk telefonen mot nesen gjentatte ganger. + Hold albuen bøyd med telefonen i VENSTRE hånd, og trykk telefonen mot nesen gjentatte ganger. + Hold albuen bøyd med telefonen i HØYRE hånd, og trykk telefonen mot nesen gjentatte ganger. + Hold telefonen mot nesen i %1$d sekunder + Forbered deg på å vinke som en dronning (vink ved å vri på håndleddet). + Forbered deg på å vinke som en dronning med telefonen i VENSTRE hånd (vink ved å vri på håndleddet). + Forbered deg på å vinke som en dronning med telefonen i HØYRE hånd (vink ved å vri på håndleddet). + Vink som en dronning i %1$d sekunder. + Flytt telefonen til VENSTRE hånd, og fortsett oppgaven. + Flytt telefonen til HØYRE hånd, og fortsett oppgaven. + Fortsett til neste oppgave. + Aktivitet fullført. + Du vil bli bedt om å utføre %1$s mens du sitter med telefonen i den ene hånden, og deretter igjen med den andre hånden. + Jeg kan ikke utføre denne aktiviteten med min VENSTRE hånd. + Jeg kan ikke utføre denne aktiviteten med min HØYRE hånd. + Jeg kan utføre denne aktiviteten med begge hender. + + Kunne ikke opprette fil + Kunne ikke fjerne nok loggfiler til å nå grensen + Feil ved angivelse av attributt + Fil er ikke merket som slettet (ikke merket som opplastet) + Flere feil ved fjerning av logger + Fant ingen innsamlede data. + Ingen katalog angitt + + Ingen data + + Tilbake + Illustrasjon av %1$s + Eget signaturfelt + Berør skjermen og flytt fingeren for å signere + Signert + Ikke signert + Markert + Ikke markert + Svarskyveknapp fra %1$s til %2$s + Bilde uten etikett + Start oppgave + aktiv + riktig + feil + inaktiv + Brikke i bildelottospill + Forhåndsvisning av bilde + Tatt bilde + Forhåndsvisning av videoopptak + Innspilt video + Kan ikke plassere skive med størrelse %1$s på skive med størrelse %2$s + Mål + Tårn + Dobbelttrykk for å plassere skiven + Dobbelttrykk for å velge øverste skive + Har skive med størrelse %1$s + Tom + Område fra %1$s til %2$s + Stabel bestående av + og + Punkt: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-nl/strings.xml b/backbone/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..5c4225dc6 --- /dev/null +++ b/backbone/src/main/res/values-nl/strings.xml @@ -0,0 +1,396 @@ + + + + + Toestemming + Voornaam + Achternaam + Vereist + Bekijk + Lees het formulier hieronder door en tik op \'Akkoord\' als je gereed bent om verder te gaan. + Bekijk + Handtekening + Onderteken met je vinger op de onderstaande lijn. + Zet hier je handtekening + Pagina %1$ld van %2$ld + + Welkom + Gegevens verzamelen + Privacy + Gegevensgebruik + Onderzoeksenquête + Onderzoekstaken + Benodigde tijd + Afzien van deelname + Meer informatie + + Meer informatie over het verzamelen van gegevens + Meer informatie over het gebruik van gegevens + Meer informatie over de bescherming van je privacy en identiteit + Meer informatie over het onderzoek + Meer informatie over de onderzoeksenquête + Meer informatie over hoe lang het onderzoek duurt + Meer informatie over wat je moet doen + Meer informatie over afzien van deelname + + Opties voor delen + Deel mijn gegevens met %1$s en gekwalificeerde onderzoekers wereldwijd + Deel mijn gegevens alleen met %1$s + De resultaten van je deelname aan dit onderzoek worden naar %1$s gestuurd.\n\nDoor de gecodeerde onderzoeksgegevens met anderen te delen (zonder persoonlijke gegevens zoals je naam) kun je helpen om dit en toekomstig onderzoek te verbeteren. + Meer informatie over het delen van gegevens + + Naam van %1$s (voluit) + Handtekening van %1$s + Datum + + Stap %1$s van %2$s + + Ongeldige waarde + %1$s is hoger dan de maximaal toegestane waarde (%2$s). + %1$s is lager dan de minimaal toegestane waarde (%2$s). + %1$s is geen geldige waarde. + + Ongeldig e‑mailadres: %1$s + + Voer een adres in + Opgegeven adres niet gevonden + De huidige locatie kan niet worden bepaald. Typ een adres of ga indien nodig naar een plek met een beter GPS-signaal. + Toegang tot \'Locatievoorzieningen\' is geweigerd. Ga naar Instellingen en geef deze app toestemming om \'Locatievoorzieningen\' te gebruiken. + Het ingevoerde adres is niet gevonden. Zorg dat je een geldig adres invoert. + Je bent niet verbonden met het internet of je hebt te vaak naar een adres gezocht. Zet om deze vraag te beantwoorden Wi‑Fi aan als je niet verbonden bent met het internet. Je kunt deze vraag ook overslaan als er een knop \'Sla over\' is of verdergaan met deze vragenlijst als de internetverbinding actief is. Of je kunt het over een paar minuten opnieuw proberen. + + Tekst overschrijdt maximale lengte: %1$s + + Camera niet beschikbaar in gedeeld scherm. + + E‑mail + jappleseed@example.com + Wachtwoord + Voer wachtwoord in + Herhaal + Herhaal wachtwoord + De wachtwoorden komen niet overeen. + Aanvullende informatie + John + Appleseed + Geslacht + Kies een geslacht + Geboortedatum + Kies een datum + + Verificatie + Verifieer je e-mailadres + Tik op de koppeling hieronder als je geen verificatiemail hebt ontvangen en je deze opnieuw wilt laten versturen. + Stuur controlemail opnieuw + + Inloggen + Wachtwoord vergeten? + + Voer toegangscode in + Herhaal toegangscode + Toegangscode bewaard + Toegangscode geverifieerd + Voer je oude toegangscode in + Voer je nieuwe toegangscode in + Herhaal je nieuwe wachtwoord + Toegangscode onjuist + De codes komen niet overeen. Probeer het opnieuw. + Log in met Touch ID + Touch ID-fout + Alleen cijfertekens toegestaan. + Voortgangsindicator toegangscode-invoer + %1$s van %2$s cijfers ingevoerd + Toegangscode vergeten? + + Sleutelhangeronderdeel kan niet worden toegevoegd. + Sleutelhangeronderdeel kan niet worden bijgewerkt. + Sleutelhangeronderdeel kan niet worden verwijderd. + Sleutelhangeronderdeel is onvindbaar. + + A+ + A- + AB+ + AB- + B+ + B- + 0+ + 0- + + Vrouw + Man + Anders + + Nee + Ja + + cm + vt + inch + + Tik om te antwoorden + Kies een antwoord + Tik om te selecteren + Tik om te schrijven + + Akkoord + Annuleer + OK + Wis + Niet akkoord + Gereed + Start + Meer informatie + Volgende + Sla over + Sla deze vraag over + Start timer + Bewaar voor later + Wis resultaten + Stop met taak + Bewaar + Wis antwoord + Dit antwoord kan niet worden gewijzigd. + + Activiteit begint over + Activiteit voltooid + Je gegevens worden geanalyseerd. Je ontvangt bericht als de resultaten gereed zijn. + Nog %1$s seconden. + + Leg afbeelding vast + Leg afbeelding opnieuw vast + Geen camera gevonden. Deze stap kan niet worden voltooid. + Om deze stap te voltooien, moet je in Instellingen deze app toegang tot de camera geven. + Er is geen uitvoermap voor vastgelegde afbeeldingen opgegeven. + De vastgelegde afbeelding kan niet worden bewaard. + + Start opname + Stop opname + Leg video opnieuw vast + + Conditie + Afstand (%1$s) + Hartslag (spm) + Ga %1$s gemakkelijk zitten. + Loop %1$s zo snel als je kunt. + Tijdens deze activiteit wordt je hartslag gemeten en wordt gekeken hoe ver je in %1$s kunt lopen. + Loop buiten %1$s zo snel als je kunt. Ga daarna gemakkelijk zitten en rust %2$s uit. Tik op \'Start\' om te beginnen. + + Tred en balans + Tijdens deze activiteit worden je tred en balans gemeten terwijl je loopt en stilstaat. Ga niet verder als je niet veilig zelfstandig kunt lopen. + Zoek een plek waar je veilig en zelfstandig ongeveer %1$d stappen in een rechte lijn kunt zetten. + Stop je iPhone in je zak of tas en volg de audio-instructies. + Sta nu %1$s lang stil. + Sta %1$s stil. + Draai om en loop terug naar het beginpunt. + Zet maximaal %1$d stappen in een rechte lijn. + + Zoek een plek waar je veilig in een rechte lijn heen en weer kunt lopen. Maak aan de einden van de looproute een bocht (alsof je om een pylon heenloopt) zodat je niet hoeft te stoppen.\n\nDaarna wordt je gevraagd om één keer om je as te draaien en vervolgens om stil te staan met de armen langs je lichaam en je voeten uit elkaar op schouderbreedte. + Tik op \'Start\' als je klaar bent om te beginnen.\nStop de telefoon vervolgens in je zak of tas en volg de audio-instructies. + Loop %1$s in een rechte lijn heen en weer. Loop zoals je altijd loopt. + Draai één keer om je as en sta vervolgens %1$s stil. + Je hebt de activiteit voltooid. + + Tiksnelheid + Rechterhand + Linkerhand + Tijdens deze activiteit wordt de snelheid waarmee je tikt gemeten. + Plaats de telefoon op een egaal oppervlak. + Tik met twee vingers van dezelfde hand beurtelings op de knoppen op het scherm. + Tik met twee vingers van je rechterhand beurtelings op de knoppen op het scherm. + Tik met twee vingers van je linkerhand beurtelings op de knoppen op het scherm. + Voer nu dezelfde test uit met je rechterhand. + Voer nu dezelfde test uit met je linkerhand. + Tik eerst met de ene vinger en dan met de andere. Probeer zo gelijkmatig mogelijk te tikken. Ga %1$s door met tikken. + Tik op \'Start\' om te beginnen. + Tik op \'Volgende\' om te beginnen. + Tik + Totaal aantal tikken + Tik met twee vingers zo gelijkmatig mogelijk op de knoppen. + Tik op de knoppen met de vingers van je RECHTERHAND. + Tik op de knoppen met de vingers van je LINKERHAND. + Sla deze hand over + + Stem + Tik op \'Start\' om te beginnen. + Zeg zo lang mogelijk \'aaaaah\' in de microfoon. + Haal diep adem en zeg zo lang mogelijk \'aaaaah\' in de microfoon. Houd daarbij dezelfde geluidssterkte aan zodat de audiobalken blauw blijven. + Tijdens deze activiteit wordt je stemgeluid gemeten door dit op te nemen met de microfoon aan de onderkant van je telefoon. + Te hard + Geluid opnemen niet mogelijk + Even geduld. Het achtergrondgeluid wordt gecontroleerd. + Er is te veel omgevingsgeluid om je stem op te nemen. Ga naar een stillere plek en probeer het opnieuw. + Tik op \'Volgende\' als je klaar bent. + + Toonaudiometrie + Met deze activiteit wordt gemeten of je verschillende geluiden kunt horen. + Sluit om te beginnen je koptelefoon aan en zet deze op. + Tik op \'Start\' om te beginnen. + Je moet nu een toon horen. Pas het volume aan met de knoppen aan de zijkant van je apparaat.\n\nTik op de knop wanneer je klaar bent om te beginnen. + Tik op de knop telkens wanneer je een geluid begint te horen. + %1$s Hz, links + %1$s Hz, rechts + + Ruimtelijk geheugen + Tijdens deze activiteit wordt je ruimtelijk kortetermijngeheugen gemeten door je de volgorde waarin %1$s oplichten te laten herhalen. + bloemen + bloemen + Een aantal van de %1$s licht een voor een op. Tik op die %2$s in dezelfde volgorde als waarin ze oplichtten. + Een aantal van de %1$s licht een voor een op. Tik op die %2$s in de omgekeerde volgorde als waarin ze oplichtten. + Tik op \'Start\' om te beginnen en kijk goed. + %1$s + Score + Kijk hoe de %1$s oplichten + Tik op de %1$s in de volgorde waarin ze oplichtten + Tik in omgekeerde volgorde op de %1$s + Volgorde voltooid + Tik op \'Ga door\' om verder te gaan + Probeer het opnieuw + Het is niet helemaal gelukt dit keer. Tik op \'Volgende\' om verder te gaan. + De tijd is om + Je hebt het niet binnen de tijd gehaald.\nTik op \'Volgende\' om verder te gaan. + Spel voltooid + Pauze + Tik op \'Ga door\' om verder te gaan + + Reactietijd + Met deze activiteit wordt gekeken hoe snel je op een visuele aanwijzing reageert. + Schud het apparaat in een willekeurige richting zodra de blauwe stip op het scherm verschijnt. Je wordt gevraagd om dit %D keer te doen. + Tik op \'Start\' om te beginnen. + Poging %1$s van %2$s + Schud het apparaat snel heen en weer als de blauwe cirkel verschijnt + + Torens van Hanoi + Met deze activiteit wordt gekeken hoe goed je puzzels kunt oplossen. + Verplaats de hele stapel in zo min mogelijk stappen naar het gemarkeerde platform. + Tik op \'Start\' om te beginnen + Los de puzzel op + Aantal stappen: %1$s \n %2$s + Ik kan deze puzzel niet oplossen + + Lopen met tijdmeting + Met deze activiteit wordt het functioneren van je onderlichaam gemeten. + Zoek een plek, bij voorkeur buiten, waar je veilig zo snel mogelijk ongeveer %1$s in een rechte lijn kunt lopen. Vertraag je tempo niet totdat je voorbij de eindstreep bent. + Tik op \'Volgende\' om te beginnen. + Loophulpmiddel + Gebruik hetzelfde hulpmiddel voor elke test. + Draag je een enkel-voetorthese? + Gebruik je een loophulpmiddel? + Tik hier om een antwoord te kiezen. + Geen + Eén wandelstok + Eén kruk + Twee wandelstokken + Twee krukken + Looprek/Rollator + Loop max. %1$s in een rechte lijn. + Draai om en loop terug naar het beginpunt. + Tik op \'Gereed\' als je klaar bent. + + PASAT + PVSAT + PAVSAT + Met de PASAT (een stapsgewijze auditieve seriële opteltest) wordt gemeten hoe snel je hoorbare informatie verwerkt en hoe goed je kunt rekenen. + Met de PVSAT (een stapsgewijze visuele seriële opteltest) wordt gemeten hoe snel je zichtbare informatie verwerkt en hoe goed je kunt rekenen. + Met de PAVSAT (een stapsgewijze auditieve en visuele seriële opteltest) wordt gemeten hoe snel je hoorbare en zichtbare informatie verwerkt en hoe goed je kunt rekenen. + Elke %1$s seconden ziet en/of hoor je een cijfer.\nJe moet elk nieuw cijfer optellen bij het vorige cijfer.\nLet op: je moet niet het totaal van alle cijfers berekenen, maar alleen de som van de laatste twee cijfers. + Tik op \'Start\' om te beginnen. + Onthoud dit eerste cijfer. + Tel dit nieuwe cijfer op bij het vorige. + - + + %1$s-schijventest + Tijdens deze activiteit mee je het functioneren van je bovenste ledematen door een schijfje in een cirkel te plaatsen. Dit moet je %1$s keer doen. + Zowel je linker- als rechterhand wordt getest.\nJe moet het schijfje zo snel mogelijk oppakken en in de cirkel plaatsen. Nadat je dit %1$s keer hebt gedaan, moet je het schijfje %2$s keer weer weghalen. + Zowel je rechter- als linkerhand wordt getest.\nJe moet het schijfje zo snel mogelijk oppakken en in de cirkel plaatsen. Nadat je dit %1$s keer hebt gedaan, moet je het schijfje %2$s keer weer weghalen. + Tik op \'Start\' om te beginnen. + Plaats met je linkerhand het schijfje in de cirkel. + Plaats met je rechterhand het schijfje in de cirkel. + Plaats met je linkerhand het schijfje achter de lijn. + Plaats met je rechterhand het schijfje achter de lijn. + Pak het schijfje op met twee vingers. + Til je vingers op om het schijfje los te laten. + + Trillingsactiviteit + Met deze activiteit wordt gemeten hoe sterk je handen trillen in verschillende posities. Ga naar een plek waar je comfortabel kunt zitten gedurende deze activiteit. + Houd de telefoon in de hand waarvan je de meeste last heeft, zoals hieronder afgebeeld. + Houd de telefoon in je RECHTERHAND, zoals hieronder afgebeeld. + Houd de telefoon in je LINKERHAND, zoals hieronder afgebeeld. + Je wordt gevraagd om %1$s zittend uit te voeren met de telefoon in je hand. + een taak + twee taken + drie taken + vier taken + vijf taken + Tik op \'Volgende\' om door te gaan. + Bereid je voor om de telefoon in je schoot te houden. + Bereid je voor om de telefoon in je schoot te houden met je LINKERHAND. + Bereid je voor om de telefoon in je schoot te houden met je RECHTERHAND. + Houd de telefoon %1$d seconden vast in je schoot. + Houd nu de telefoon op schouderhoogte in je uitgestoken hand. + Houd nu de telefoon op schouderhoogte in je uitgestoken LINKERHAND. + Houd nu de telefoon op schouderhoogte in je uitgestoken RECHTERHAND. + Houd de telefoon %1$d seconden vast in je uitgestoken hand. + Houd nu de telefoon op schouderhoogte terwijl je je elleboog gebogen houdt. + Houd nu de telefoon op schouderhoogte in je LINKERHAND terwijl je je elleboog gebogen houdt. + Houd nu de telefoon op schouderhoogte in je RECHTERHAND terwijl je je elleboog gebogen houdt. + Houd de telefoon %1$d seconden vast terwijl je je elleboog gebogen houdt + Raak nu je neus herhaaldelijk aan met de telefoon terwijl je je elleboog gebogen houdt. + Houd nu de telefoon in je LINKERHAND en raak je neus herhaaldelijk aan met de telefoon terwijl je je elleboog gebogen houdt. + Houd nu de telefoon in je RECHTERHAND en raak je neus herhaaldelijk aan met de telefoon terwijl je je elleboog gebogen houdt. + Raak %1$d seconden je neus aan met de telefoon + Bereid je voor om te wuiven vanuit de pols. + Bereid je voor om te wuiven vanuit de pols met de telefoon in je LINKERHAND. + Bereid je voor om te wuiven vanuit de pols met de telefoon in je RECHTERHAND. + Wuif %1$d seconden vanuit de pols. + Neem de telefoon nu in je LINKERHAND en ga door met de volgende taak. + Neem de telefoon nu in je RECHTERHAND en ga door met de volgende taak. + Ga door naar de volgende taak. + Activiteit voltooid. + Je wordt gevraagd om %1$s zittend uit te voeren met de telefoon in de ene hand en daarna opnieuw met de telefoon in de andere hand. + Ik kan deze activiteit niet met mijn LINKERHAND uitvoeren. + Ik kan deze activiteit niet met mijn RECHTERHAND uitvoeren. + Ik kan deze activiteit met beide handen uitvoeren. + + Bestand aanmaken mislukt + Er kunnen niet genoeg logbestanden worden verwijderd om de drempel te halen + Fout bij kenmerk instellen + Bestand niet gemarkeerd als verwijderd (niet gemarkeerd als geüpload) + Meerdere fouten bij logbestanden verwijderen + Geen verzamelde gegevens gevonden. + Geen uitvoerdirectory opgegeven + + Geen gegevens + + Terug + Afbeelding van %1$s + Veld voor handtekening + Beweeg je vinger over het scherm om je handtekening te zetten + Ondertekend + Niet ondertekend + Geselecteerd + Selectie opgeheven + Antwoordschuifknop. Bereik van %1$s tot %2$s + Naamloze afbeelding + Begin met taak + actief + goed + fout + inactief + Tegel in geheugenspel + Voorvertoning opname + Vastgelegde afbeelding + Voorvertoning video-opname + Vastgelegde video + De schijf met grootte %1$s kan niet op de schijf met grootte %2$s worden geplaatst. + Doel + Toren + Tik dubbel om de schijf te plaatsen + Tik dubbel om de bovenste schijf te selecteren + Heeft schijf met groottes %1$s + Leeg + Bereik van %1$s tot %2$s + Stack bestaat uit + en + Punt: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-pl/strings.xml b/backbone/src/main/res/values-pl/strings.xml new file mode 100644 index 000000000..e7a67751d --- /dev/null +++ b/backbone/src/main/res/values-pl/strings.xml @@ -0,0 +1,396 @@ + + + + + Zgoda + Imię + Nazwisko + Wymagane + Przejrzyj + Przejrzyj poniższy formularz i stuknij w Akceptuję, jeśli chcesz kontynuować. + Przejrzyj + Podpis + Złóż podpis na linii poniżej, używając palca. + Podpisz tutaj + Strona %1$ld z %2$ld + + Witamy + Gromadzenie danych + Prywatność + Użycie danych + Ankieta badawcza + Zadania badawcze + Zobowiązanie czasowe + Wycofywanie zgody + Więcej informacji + + Więcej informacji o gromadzeniu danych + Więcej informacji o wykorzystywaniu danych + Więcej informacji o ochronie prywatności i tożsamości + Na początek: więcej informacji o badaniu + Więcej informacji o ankiecie + Więcej informacji o wymogach czasowych badania + Więcej informacji o zadaniach + Więcej informacji o wycofywaniu się z badania + + Opcje udostępniania + %1$s i wykwalifikowani badacze z całego świata mogą uzyskać moje dane + Tylko %1$s może uzyskać moje dane + %1$s otrzyma dane zebrane podczas Twojego udziału w tym badaniu.\n\nSzersze udostępnianie zakodowanych danych (bez informacji takich jak Twoje imię i nazwisko) jest korzystne dla tego i przyszłych badań. + Więcej informacji o udostępnianiu danych + + %1$s – imię i nazwisko (drukowane) + %1$s – podpis + Data + + Krok %1$s z %2$s + + Nieprawidłowa wartość + %1$s przekracza maksymalną dozwoloną wartość (%2$s). + %1$s nie przekracza minimalnej dozwolonej wartości (%2$s). + %1$s nie jest poprawną wartością. + + Nieprawidłowy adres email: %1$s + + Podaj adres + Nie można było znaleźć określonego adresu + Nie można określić Twojego bieżącego położenia. Podaj adres lub przenieś się w miejsce z lepszym sygnałem GPS. + Nastąpiła odmowa dostępu do usług lokalizacji. Przejdź do Ustawień i pozwól temu programowi używać usług lokalizacji. + Nie można znaleźć wyniku dla podanego adresu. Upewnij się, że adres jest prawidłowy. + Nie masz połączenia z Internetem lub przekroczona została maksymalna liczba żądań wyszukiwania adresu. Jeśli nie masz połączenia z Internetem, włącz sieć Wi-Fi, aby odpowiedzieć na to pytanie, pomiń pytanie (jeśli dostępny jest przycisk pomijania) lub powróć do ankiety po nawiązaniu połączenia z Internetem. W przeciwnym przypadku spróbuj ponownie za kilka minut. + + Przekroczony limit długości tekstu: %1$s + + Kamera niedostępna na ekranie podzielonym. + + Email + jjablonka@example.com + Hasło + Podaj hasło + Potwierdź + Powtórz hasło + Hasła są niezgodne. + Informacje dodatkowe + Jan + Jabłonka + Płeć + Wybierz płeć + Data urodzenia + Wybierz datę + + Weryfikacja + Zweryfikuj adres email + Jeśli email weryfikacji nie dotarł na Twoje konto, stuknij w łącze poniżej, aby wysłać go ponownie. + Wyślij ponownie email z weryfikacją + + Logowanie + Nie pamiętasz hasła? + + Podaj kod + Potwierdź kod + Kod został zachowany + Kod został uwierzytelniony + Podaj stary kod + Podaj nowy kod + Potwierdź nowy kod + Nieprawidłowy kod + Niezgodne kody. Spróbuj ponownie. + Uwierzytelnij przy użyciu Touch ID + Błąd Touch ID + Dozwolone są tylko znaki numeryczne. + Wskaźnik postępu podawania kodu + Liczba wprowadzonych cyfr: %1$s z %2$s + Nie pamiętasz kodu? + + Nie można było dodać rzeczy pęku kluczy. + Nie można było uaktualnić rzeczy pęku kluczy. + Nie można było usunąć rzeczy pęku kluczy. + Nie można było znaleźć rzeczy pęku kluczy. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Kobieta + Mężczyzna + Inna + + Nie + Tak + + cm + ft + cale + + Stuknij, aby odpowiedzieć + Wybierz odpowiedź + Stuknij, aby wybrać + Stuknij, aby wpisać + + Akceptuję + Anuluj + OK + Wymaż + Nie akceptuję + Gotowe + Start + Więcej informacji + Dalej + Pomiń + Pomiń to pytanie + Włącz stoper + Zachowaj na później + Odrzuć wyniki + Zakończ zadanie + Zachowaj + Wymaż odpowiedź + Nie można modyfikować tej odpowiedzi. + + Początek ćwiczenia za + Ćwiczenie zakończone + Twoje dane zostaną przeanalizowane i otrzymasz powiadomienie o wynikach. + Pozostało: %1$s s. + + Zarejestruj obraz + Zarejestruj obraz ponownie + Nie znaleziono aparatu. Nie można wykonać kroku. + Aby wykonać ten krok, przejdź do Ustawień i daj temu programowi dostęp do aparatu. + Nie wskazano katalogu wyjściowego dla zarejestrowanych obrazów. + Nie można było zachować zarejestrowanego obrazu. + + Rozpocznij nagrywanie + Zatrzymaj nagrywanie + Zarejestruj wideo ponownie + + Sprawność + Dystans (%1$s) + Tętno (ud./min) + Siedź wygodnie przez %1$s. + Idź najszybciej jak potrafisz przez %1$s. + To ćwiczenie monitoruje Twoje tętno i mierzy dystans, jaki potrafisz przejść przez %1$s. + Wyjdź na zewnątrz i idź najszybciej jak potrafisz przez %1$s. Gdy skończysz, usiądź wygodnie i odpoczywaj przez %2$s. Aby rozpocząć, stuknij w Start. + + Chód i równowaga + To ćwiczenie mierzy Twój chód i równowagę w ruchu i bezruchu. Jeśli nie możesz bezpiecznie chodzić bez pomocy innych, nie wykonuj go. + Znajdź miejsce, w którym możesz bezpiecznie i bez pomocy innych iść prosto i zatrzymać się po około %1$d krokach. + Włóż telefon do kieszeni lub torby i postępuj zgodnie z odczytywanymi instrukcjami. + Teraz stój w bezruchu przez %1$s. + Stój w bezruchu przez %1$s. + Odwróć się i przejdź do miejsca startu. + Idź prosto i zatrzymaj się po maksymalnie %1$d krokach. + + Znajdź miejsce, w którym możesz bezpiecznie chodzić w tę i z powrotem w linii prostej. Próbuj chodzić w sposób ciągły, zawracając na końcach tak, jak robi się to wokół słupka.\n\nNastępnie wykonasz pełny obrót i staniesz z rękoma ułożonymi wzdłuż ciała i stopami rozstawionymi do szerokości ramion. + Aby rozpocząć, stuknij w Start.\nNastępnie umieść telefon w kieszeni lub torbie i postępuj zgodnie z instrukcjami dźwiękowymi. + Chodź zwykłym krokiem w tę i z powrotem w linii prostej przez %1$s. + Wykonaj pełny obrót i stój bez ruchu przez %1$s. + Ćwiczenie zostało ukończone. + + Szybkość stukania + Prawa ręka + Lewa ręka + To ćwiczenie ocenia Twoją szybkość stukania. + Połóż telefon na płaskiej powierzchni. + Naprzemiennie stukaj w przyciski na ekranie dwoma palcami tej samej ręki. + Naprzemiennie stukaj w przyciski na ekranie dwoma palcami prawej ręki. + Naprzemiennie stukaj w przyciski na ekranie dwoma palcami lewej ręki. + Teraz wykonaj to ćwiczenie prawą ręką. + Teraz wykonaj to ćwiczenie lewą ręką. + Stukaj na zmianę jednym i drugim palcem. Próbuj stukać możliwie miarowo. Stukaj przez %1$s. + Aby rozpocząć, stuknij w Start. + Aby rozpocząć, stuknij w Dalej. + Stuknij + Liczba stuknięć + Stukaj w przyciski dwoma palcami możliwie miarowo. + Stukaj w przyciski PRAWĄ ręką. + Stukaj w przyciski LEWĄ ręką. + Pomiń tę rękę + + Głos + Aby rozpocząć, stuknij w Start. + Mów „Aaaaa” do mikrofonu najdłużej jak potrafisz. + Weź głęboki oddech i mów „Aaaaa” do mikrofonu najdłużej jak potrafisz. Utrzymuj stały poziom głośności, aby paski dźwięku były przez cały czas niebieskie. + To ćwiczenie ocenia Twój głos, nagrywając go przy użyciu mikrofonu znajdującego się u dołu telefonu. + Zbyt głośno + Nie można nagrać dźwięku + Czekaj, sprawdzamy poziom szumu tła. + Szum otoczenia jest zbyt głośny, aby nagrać Twój głos. Przenieś się w cichsze miejsce i spróbuj ponownie. + Aby rozpocząć, stuknij w Dalej. + + Audiometria tonalna + To ćwiczenie ocenia Twoją zdolność słyszenia różnych dźwięków. + Zanim zaczniesz, podłącz i załóż słuchawki. + Aby rozpocząć, stuknij w Start. + Dźwięk powinien być teraz słyszalny. Zmień głośność, używając przycisków z boku urządzenia.\n\nAby rozpocząć, stuknij w przycisk. + Stuknij w przycisk za każdym razem, gdy usłyszysz dźwięk. + %1$s Hz, lewy + %1$s Hz, prawy + + Pamięć przestrzenna + To ćwiczenie ocenia Twoją krótkotrwałą pamięć przestrzenną: podświetla %1$s w określonej sekwencji i prosi o jej powtórzenie. + kwiaty + kwiaty + Widoczne na ekranie %1$s będą kolejno podświetlane. Stuknij w te %2$s w takie samej kolejności. + Widoczne na ekranie %1$s będą kolejno podświetlane. Stuknij w te %2$s w odwrotnej kolejności. + Aby rozpocząć, stuknij w Start, a następnie obserwuj ekran. + %1$s + Wynik + Obserwuj podświetlane %1$s + Stuknij w %1$s w kolejności, w jakiej były podświetlane + Stuknij w %1$s w odwrotnej kolejności + Koniec sekwencji + Aby kontynuować, stuknij w Dalej + Spróbuj ponownie + Tym razem nie udało Ci się. Stuknij w Dalej, aby kontynuować. + Czas upłynął + Czas minął.\nAby kontynuować, stuknij w Dalej. + Gra zakończona + Wstrzymana + Aby kontynuować, stuknij w Dalej + + Czas reakcji + To ćwiczenie ocenia Twój czas odpowiedzi na bodziec wzrokowy. + Potrząśnij urządzeniem w dowolnym kierunku, gdy tylko zobaczysz na ekranie niebieską kropkę. Test zostanie powtórzony %D razy. + Aby rozpocząć, stuknij w Start. + Próba %1$s z %2$s + Gdy pojawi się niebieskie kółko, szybko potrząśnij urządzeniem + + Wieże Hanoi + To ćwiczenie ocenia Twoją zdolność rozwiązywania łamigłówek. + Przenieś cały zestaw krążków na wyróżnioną podstawę, wykonując jak najmniej ruchów. + Aby rozpocząć, stuknij w Start + Rozwiąż łamigłówkę + Liczba ruchów: %1$s \n %2$s + Nie potrafię rozwiązać tej łamigłówki + + Chód na czas + To ćwiczenie mierzy sprawność Twoich kończyn dolnych. + Znajdź miejsce, najlepiej na wolnym powietrzu, gdzie możesz iść w linii prostej przez ok. %1$s możliwie najszybciej, ale bezpiecznie. Nie zwalniaj, dopóki nie przekroczysz linii końcowej. + Aby rozpocząć, stuknij w Dalej. + Urządzenie wspomagające + Użyj tego samego urządzenia wspomagającego w każdym teście. + Czy nosisz ortezę stawu skokowego? + Czy używasz urządzenia wspomagającego? + Stuknij tutaj, aby odpowiedzieć. + Brak + Jedna laska + Jedna kula + Dwie laski + Dwie kule + Chodzik/balkonik + Idź maks. %1$s w linii prostej. + Odwróć się i przejdź do miejsca startu. + Gdy skończysz, stuknij w Gotowe. + + PASAT + PVSAT + PAVSAT + Test PASAT mierzy szybkość przetwarzania danych audialnych i zdolność obliczeniową. + Test PVSAT mierzy szybkość przetwarzania danych wizualnych i zdolność obliczeniową. + Test PAVSAT mierzy szybkość przetwarzania danych audiowizualnych i zdolność obliczeniową. + Co %1$s s prezentowana jest cyfra.\nTwoje zadanie polega na dodaniu jej do poprzedniej.\nUwaga: Nie obliczasz sumy bieżącej wszystkich cyfr, a jedynie ostatnich dwóch. + Aby rozpocząć, stuknij w Start. + Zapamiętaj tę pierwszą cyfrę. + Dodaj tę nową cyfrę do poprzedniej. + - + + Test %1$s-HPT (otwory i kołki) + To ćwiczenie mierzy sprawność kończyn górnych: umieszczasz „kołek” (pełne kółko) w „otworze” (puste kółko). (Do wykonania %1$s razy). + Test obejmuje zarówno lewą, jak i prawą rękę.\nMusisz jak najszybciej podnieść „kołek” i umieścić go w „otworze”. Gdy wykonasz to %1$s razy, wyjmij go również %2$s razy. + Test obejmuje zarówno prawą, jak i lewą rękę.\nMusisz jak najszybciej podnieść „kołek” i umieścić go w „otworze”. Gdy wykonasz to %1$s razy, wyjmij go również %2$s razy. + Aby rozpocząć, stuknij w Start. + Umieść „kołek” w „otworze”, używając lewej ręki. + Umieść „kołek” w „otworze”, używając prawej ręki. + Umieść „kołek” za linią, używając lewej ręki. + Umieść „kołek” za linią, używając prawej ręki. + Podnieś „kołek” dwoma palcami. + Unieś palce, aby upuścić „kołek”. + + Ćwiczenie oceniające drżenie rąk + To ćwiczenie ocenia drżenie rąk w różnych pozycjach. Na czas tego ćwiczenia znajdź miejsce, w którym możesz wygodnie siedzieć. + Trzymaj telefon w bardziej drżącej ręce, tak jak na obrazku poniżej. + Trzymaj telefon w PRAWEJ ręce, tak jak na obrazku poniżej. + Trzymaj telefon w LEWEJ ręce, tak jak na obrazku poniżej. + Wykonasz %1$s w pozycji siedzącej, trzymając telefon w ręce. + zadanie + dwa zadania + trzy zadania + cztery zadania + pięć zadań + Aby kontynuować, stuknij w Dalej. + Przygotuj się do trzymania telefonu na kolanach. + Przygotuj się do trzymania telefonu na kolanach LEWĄ ręką. + Przygotuj się do trzymania telefonu na kolanach PRAWĄ ręką. + Trzymaj telefon na kolanach przez %1$d s. + Teraz trzymaj telefon w wyciągniętej ręce na wysokości barku. + Teraz trzymaj telefon w wyciągniętej LEWEJ ręce na wysokości barku. + Teraz trzymaj telefon w wyciągniętej PRAWEJ ręce na wysokości barku. + Trzymaj telefon w wyciągniętej ręce przez %1$d s. + Teraz trzymaj telefon w ręce ugiętej w łokciu na wysokości barku. + Teraz trzymaj telefon w LEWEJ ręce ugiętej w łokciu na wysokości barku. + Teraz trzymaj telefon w PRAWEJ ręce ugiętej w łokciu na wysokości barku. + Trzymaj telefon w ręce zgiętej w łokciu przez %1$d s + Teraz, trzymając telefon w ręce zgiętej w łokciu, dotknij nim nosa i powtarzaj tę czynność. + Teraz, trzymając telefon w LEWEJ ręce zgiętej w łokciu, dotknij nim nosa i powtarzaj tę czynność. + Teraz, trzymając telefon w PRAWEJ ręce zgiętej w łokciu, dotknij nim nosa i powtarzaj tę czynność. + Dotykaj telefonem nosa przez %1$d s + Przygotuj się do machania dłonią, zginając rękę w nadgarstku. + Przygotuj się do machania LEWĄ dłonią, zginając rękę w nadgarstku. + Przygotuj się do machania PRAWĄ dłonią, zginając rękę w nadgarstku. + Unieś rękę i machaj dłonią, zginając rękę w nadgarstku, przez %1$d s. + Teraz przełóż telefon do LEWEJ ręki i przejdź do następnego zadania. + Teraz przełóż telefon do PRAWEJ ręki i przejdź do następnego zadania. + Przejdź do następnego zadania. + Ćwiczenie ukończone. + Wykonasz %1$s w pozycji siedzącej z telefonem w jednej ręce, a następnie w drugiej. + Nie mogę wykonywać tego ćwiczenia LEWĄ ręką. + Nie mogę wykonywać tego ćwiczenia PRAWĄ ręką. + Mogę wykonywać to ćwiczenie obiema rękami. + + Nie można było utworzyć pliku + Nie można było usunąć liczby plików dzienników pozwalającej na osiągnięcie progu + Błąd ustawiania atrybutu + Plik nie oznaczony jako usunięty (nie oznaczony jako wysłany) + Wiele błędów usuwania dzienników + Nie znaleziono zgromadzonych danych. + Nie wskazano katalogu wyjściowego + + Brak danych + + Wróć + Obrazek: %1$s + Wyznaczone pole podpisu + Dotknij ekranu i przesuwaj palec, aby podpisać + Podpisane + Niepodpisane + Zaznaczone + Nie zaznaczone + Suwak odpowiedzi. Zakres od %1$s do %2$s + Obrazek bez etykiety + Rozpocznij zadanie + aktywna + prawidłowa + nieprawidłowa + nieaktywna + Płytka gry pamięciowej + Zarejestruj podgląd + Zarejestrowany obraz + Podgląd rejestracji wideo + Zarejestrowane wideo + Nie można umieścić dysku o rozmiarze %1$s na dysku o rozmiarze %2$s + Krążek docelowy + Wieża + Stuknij dwukrotnie, aby umieścić krążek + Stuknij dwukrotnie, aby wybrać krążek znajdujący się na górze + Zawiera krążki o rozmiarach %1$s + Pusta + Zakres %1$s–%2$s + Stos złożony z + + Punkt: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-pt-rPT/strings.xml b/backbone/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 000000000..dd57e9151 --- /dev/null +++ b/backbone/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,396 @@ + + + + + Autorização + Nome + Apelido + necessário + Leia com atenção + Leia com atenção o texto abaixo e toque em Concordo se pretende continuar. + Leia com atenção + Assinatura + Assine com o dedo na linha abaixo. + Assine aqui + Página %1$ld de %2$ld + + Damos‑lhe as boas‑vindas! + Recolha de dados + Privacidade + Utilização de dados + Inquérito + Tarefas + Tempo dedicado + Desistência + Saiba mais + + Saiba mais acerca de como os dados são recolhidos. + Saiba mais acerca de como os dados são utilizados. + Saiba mais acerca de como a sua privacidade e identidade são protegidas. + Saiba mais acerca deste inquérito antes de começar. + Saiba mais acerca deste inquérito. + Saiba mais acerca da duração do inquérito. + Saiba mais acerca das tarefas incluídas. + Saiba mais acerca de como desistir do inquérito. + + Opções de partilha + Partilhar dados com %1$s e investigadores qualificados em todo o mundo + Partilhar dados só com %1$s + %1$s receberá os dados relativos à sua participação neste estudo.\n\nPartilhar os seus dados codificados com mais entidades (excluindo informação como o seu nome, por exemplo) beneficiará estudos futuros. + Saiba mais acerca da partilha de dados + + Nome de: %1$s (maiúsculas) + Assinatura de: %1$s + Data + + Passo %1$s de %2$s + + Valor inválido + %1$s ultrapassa o valor máximo permitido (%2$s). + %1$s é inferior ao valor mínimo permitido (%2$s). + %1$s não é um valor válido. + + Endereço de e‑mail inválido: %1$s + + Digite um endereço + Não foi possível encontrar o endereço indicado. + Não foi possível encontrar a localização atual. Digite um endereço ou desloque‑se para um local com sinal GPS mais forte. + O acesso aos serviços de localização foi recusado. Permita a esta aplicação aceder a estes serviços nas Definições. + Não foi encontrado nenhum resultado. Certifique‑se de que o endereço indicado é válido. + Ou não dispõe de ligação à Internet ou excedeu o número limite de consultas. Caso não tenha ligação estabelecida à Internet, ative a rede Wi‑Fi para responder a esta pergunta ou para a ignorar (se esta opção estiver disponível). Também pode optar por voltar ao questionário quando tiver estabelecido ligação à Internet ou por voltar a tentar dentro de alguns minutos. + + Conteúdo de texto que excede o comprimento máximo: %1$s + + A câmara não está disponível em ecrã dividido. + + E‑mail + mmacieira@example.com + Palavra‑passe + Digite a palavra‑passe + Confirmar + Volte a digitar a palavra‑passe + As palavras‑passe não coincidem. + Informação adicional + Manuel + Macieira + Sexo + Escolha o sexo + Data de nascimento + Escolha uma data + + Confirmação + Confirme o e‑mail + Caso não tenha recebido um e‑mail de confirmação, toque na hiperligação abaixo; o e‑mail voltará a ser enviado. + Reenviar e‑mail de confirmação + + Iniciar sessão + Não se lembra da palavra‑passe? + + Digite o código + Confirmar código + Código guardado + Código autenticado + Digite o código antigo + Digite o novo código + Confirme o novo código + Código incorreto + Introduziu códigos diferentes. Volte a tentar. + Efetue autenticação com o Touch ID + Erro do Touch ID + Só são permitidos caracteres numéricos. + Indicador de progresso de introdução do código + %1$s de %2$s dígitos introduzidos + Não se lembra do código? + + Não foi possível adicionar elemento do Porta‑chaves. + Não foi possível atualizar elemento do Porta‑chaves. + Não foi possível apagar elemento do Porta‑chaves. + Não foi possível encontrar elemento do Porta‑chaves. + + A+ + A- + AB+ + AB- + B+ + B- + 0+ + 0- + + Feminino + Masculino + Outro + + Não + Sim + + cm + pés + in + + Toque para responder + Selecione uma resposta + Toque para selecionar + Toque para escrever + + Concordo + Cancelar + OK + Limpar + Não concordo + OK + Começar + Saiba mais + Seguinte + Ignorar + Passar à pergunta seguinte + Iniciar relógio + Guardar para depois + Não usar os resultados + Terminar tarefa + Guardar + Limpar resposta + Esta resposta não pode ser alterada. + + Iniciar exercício daqui a + Exercício concluído + Os seus dados serão analisados e receberá um aviso quando os resultados estiverem disponíveis. + Faltam %1$s segundos. + + Capturar imagem + Voltar a capturar imagem + Nenhuma câmara encontrada. This step cannot be completed. + Para completar este passo, permita que esta aplicação aceda à câmara nas Definições. + Não foi especificado nenhum diretório de saída para as imagens capturadas. + Não foi possível guardar a imagem capturada. + + Iniciar gravação + Parar gravação + Voltar a filmar + + Fitness + Distância (%1$s) + Ritmo cardíaco (bpm) + Sente‑se confortavelmente durante %1$s. + Ande o mais depressa que puder durante %1$s. + Este exercício mede o seu ritmo cardíaco e a distância que consegue percorrer a pé em %1$s. + Durante %1$s, ande na rua o mais depressa que puder, sem correr. No fim, sente‑se confortavelmente e descanse durante %2$s. Para dar inicio ao teste, toque em Começar. + + Equilíbrio e modo de andar + Este exercício mede o seu equilíbrio e modo de andar. Se não tem condições para andar em segurança sem ajuda, não continue. + Encontre um sítio onde possa andar em linha reta, em segurança e sem ajuda, durante aproximadamente %1$d passos. + Ponha o telefone no bolso ou dentro da mala e siga as instruções pelos auriculares. + Agora, pare e mantenha-se de pé durante %1$s. + Pare e mantenha-se de pé durante %1$s. + Dê meia‑volta e ande de volta ao ponto de partida. + Dê até %1$d passos em linha reta. + + Escolha um sítio onde possa andar em segurança, para trás e para frente em linha reta. Tente andar sem parar, dando a volta no final do percurso como se estivesse a andar à volta de um cone.\n\nDepois, ser‑lhe‑à pedido que se vire, dando uma volta completa, e que permaneça imóvel, com os braços ao logo do corpo e os pés afastados à largura dos ombros. + Quando estiver pronto(a), toque em Começar.\nDepois, coloque o telefone no bolso ou na carteira e siga as instruções. + Ande em linha reta, para trás e para frente, durante %1$s. Ande normalmente. + Rode dando uma volta completa e, depois, permaneça imóvel durante %1$s. + Concluiu este exercício. + + Velocidade de toque + Mão direita + Mão esquerda + Este exercício mede a sua velocidade de toque. + Coloque o telefone numa superfície plana. + Com dois dedos da mesma mão, toque alternadamente nos botões no ecrã. + Com dois dedos da mão direita, toque alternadamente nos botões no ecrã. + Com dois dedos da mão esquerda, toque alternadamente nos botões no ecrã. + Agora repita o mesmo teste com a mão direita. + Agora repita o mesmo teste com a mão esquerda. + Toque com um dedo e depois com o outro. Tente fazer com que os toques sejam o mais regulares possível. Continue a tocar durante %1$s. + Para iniciar, toque em Começar. + Toque em Seguinte para começar. + Toque + Total de toques + Com dois dedos, toque nos botões o mais regularmente que puder. + Com a mão DIREITA, toque nos botões. + Com a mão ESQUERDA, toque nos botões. + Não testar esta mão + + Voz + Para iniciar, toque em Começar. + Diga “Aaaaah” ao microfone durante o máximo de tempo possível. + Respire fundo e diga “Aaaaah” ao microfone durante o máximo de tempo possível. Mantenha um nível vocal estável - as barras de áudio devem permanecer azuis. + Este exercício avalia a sua voz, gravando‑a através do microfone na parte inferior do telefone. + Demasiado alto + Não é possível gravar áudio + Aguarde enquanto verificamos o nível de ruído de fundo. + O nível de ruído ambiente é demasiado elevado para gravar a sua voz. Desloque‑se para um local menos ruidoso e volte a tentar. + Quando estiver pronto(a), toque em Seguinte. + + Audiometria de tom + Este exercício mede a sua capacidade de ouvir sons diferentes. + Antes de começar, ligue os auscultadores e coloque-os na cabeça. + Para iniciar, toque em Começar. + Deve ouvir um som agora. Ajuste o volume usando os controlos na parte lateral do dispositivo.\n\nToque no botão quando estiver pronto para começar. + Toque no botão sempre que começar a ouvir um som. + %1$s Hz, esquerda + %1$s Hz, direita + + Memória espacial + Este exercício mede a sua memória espacial de curto termo, pedindo‑lhe que repita a ordem pela qual os elementos (%1$s) se tornam mais destacados. + flores + flores + Um de cada vez, alguns dos elementos (%1$s) vão ficar mais destacados que os outros. Toque nos elementos (%2$s) na ordem pela qual se destacam. + Um de cada vez, alguns dos elementos (%1$s) vão ficar mais destacados que os outros. Toque nos elementos (%2$s) na ordem inversa à qual se destacam. + Para dar início ao teste, toque em Começar e observe com atenção. + %1$s + Resultado + Note como os elementos (%1$s) ficam mais destacados. + Toque nos elementos (%1$s) na ordem em que se destacam. + Toque nos elementos (%1$s) em ordem contrária. + Sequência completa + Para continuar, toque em Seguinte + Voltar a tentar + Desta vez, não correu muito bem. Toque em Seguinte para continuar. + Acabou o tempo + Acabou o tempo.\nToque em Seguinte para continuar. + Jogo terminado + Em pausa + Para continuar, toque em Seguinte + + Tempo de reação + Este exercício avalia o seu tempo de resposta a uma pista visual. + Agite o dispositivo em qualquer direção assim que o ponto azul aparecer no ecrã. Ser-lhe-á pedido para fazer isto %D vezes. + Para iniciar, toque em Começar. + Tentativa %1$s de %2$s + Agite rapidamente o dispositivo quando o círculo azul aparecer + + Torre de Hanoi + Este exercício avalia a sua capacidade de resolver puzzles. + Mova a pilha inteira para a plataforma destacada no menor número de movimentos possível. + Para iniciar, toque em Começar. + Resolver o puzzle + Número de movimentos: %1$s \n %2$s + Não consigo resolver este puzzle + + Caminhada temporizada + Este exercício mede a função das suas extremidades inferiores. + Encontre um lugar, preferencialmente no exterior, onde possa caminhar cerca de %1$s em linha reta o mais rapidamente possível, mas em segurança. Não abrande até ultrapassar a meta. + Toque em Seguinte para começar. + Dispositivo auxiliar + Use o mesmo dispositivo auxiliar para cada teste. + Utiliza uma órtotese tornozelo-pé? + Utiliza um dispositivo auxiliar? + Toque aqui para responder. + Nenhum + Bengala unilateral + Muleta unilateral + Bengala bilateral + Muleta bilateral + Andarilho + Caminhe até %1$s em linha reta. + Dê meia‑volta e ande de volta ao ponto de partida. + Toque em OK quando terminar. + + PASAT + PVSAT + PAVSAT + O teste PASAT (Paced Auditory Serial Addition Test) mede a velocidade de processamento de informação auditiva e a capacidade de cálculo. + O teste PVSAT (Paced Auditory Serial Addition Test) mede a velocidade de processamento de informação visual e a capacidade de cálculo. + O teste PAVSAT (Paced Auditory and Visual Serial Addition Test) mede a velocidade de processamento de informação auditiva e visual e a capacidade de cálculo. + Os dígitos únicos são apresentados a cada %1$s segundos.\nTem de adicionar cada novo dígito ao dígito imediatamente anterior.\nAtenção, não deve calcular o valor total, mas apenas a soma dos últimos dois números. + Para iniciar, toque em Começar. + Lembre-se deste primeiro dígito. + Adicione este novo dígito ao anterior. + - + + Teste dos %1$s buracos e pinos + Esta exercício mede as funções das extremidades superiores pedindo‑lhe que coloque um círculo dentro de um buraco. Ser-lhe-à pedido que repita %1$s vezes. + As duas mãos serão testadas.\nTem de pegar no círculo o mais depressa possível e arrastá‑lo para o buraco. Depois de o fazer %1$s vezes, faça o movimento contrário e remova o círculo do buraco %2$s vezes. + As duas mãos serão testadas.\nTem de pegar no círculo o mais depressa possível e arrastá‑lo para o buraco. Depois de o fazer %1$s vezes, faça o movimento contrário e remova o círculo do buraco %2$s vezes. + Para iniciar, toque em Começar. + Com a mão esquerda, ponha o círculo dentro do buraco. + Com a mão direita, ponha o círculo dentro do buraco. + Com a mão esquerda, ponha o círculo do outro lado da linha. + Com a mão direita, ponha o círculo do outro lado da linha. + Apanhe o círculo com dois dedos. + Levante os dedos para largar o círculo. + + Tremor + Este exercício mede o tremor das mãos em várias posições. Durante todo o exercício, é importante manter‑se confortavelmente sentado(a). + Segure no telefone (como mostra a imagem abaixo) com a mão mais afetada. + Segure no telefone (como mostra a imagem abaixo) com a mão DIREITA. + Segure no telefone (como mostra a imagem abaixo) com a mão ESQUERDA. + Ser‑lhe‑à pedido que efetue %1$s, na posição sentada, com o telefone na mão. + uma tarefa + duas tarefas + três tarefas + quatro tarefas + cinco tarefas + Toque em Seguinte para continuar. + Prepare‑se para segurar no telefone no colo. + Prepare‑se para segurar no telefone no colo, com a mão ESQUERDA. + Prepare‑se para segurar no telefone no colo, com a mão DIREITA. + Continue com o telefone no colo durante %1$d segundos. + Agora segure no telefone com o braço estendido à altura do ombro. + Agora segure no telefone com a mão ESQUERDA, com o braço estendido à altura do ombro. + Agora segure no telefone com a mão DIREITA, com o braço estendido à altura do ombro. + Continue a segurar no telefone com o braço estendido durante %1$d segundos. + Agora segure no telefone à altura do ombro, com o cotovelo dobrado. + Agora, segure no telefone com a mão ESQUERDA e, com o cotovelo dobrado, levante o braço à altura do ombro. + Agora, segure no telefone com a mão DIREITA e, com o cotovelo dobrado, levante o braço à altura do ombro. + Continue a segurar no telefone com o cotovelo dobrado durante %1$d segundos. + Agora, mantendo o cotovelo dobrado, toque repetidamente no nariz com o telefone. + Agora, com a mão ESQUERDA e mantendo o cotovelo dobrado, toque repetidamente no nariz com o telefone. + Agora, com a mão DIREITA e mantendo o cotovelo dobrado, toque repetidamente no nariz com o telefone. + Continue a tocar com o telefone no nariz durante %1$d segundos. + Prepare‑se para acenar (rodando o pulso com a mão levantada). + Prepare‑se para acenar com o telefone na mão ESQUERDA (rodando o pulso com a mão levantada). + Prepare‑se para acenar com o telefone na mão DIREITA (rodando o pulso com a mão levantada). + Continue a acenar com a mão durante %1$d segundos. + Agora mude o telefone para a mão ESQUERDA e passe à tarefa seguinte. + Agora mude o telefone para a mão DIREITA e passe à tarefa seguinte. + Passar à tarefa seguinte. + Exercício concluído. + Ser‑lhe‑à pedido que efetue %1$s, na posição sentada, primeiro com o telefone numa mão e, depois, na outra. + Não consigo fazer este exercício com a mão ESQUERDA. + Não consigo fazer este exercício com a mão DIREITA. + Consigo fazer este exercício com ambas as mãos. + + Não foi possível criar ficheiro + Não foi possível remover ficheiros de registo suficientes para atingir o limite. + Erro ao definir atributo. + Ficheiro não marcado como pagado (não marcado como enviado). + Vários erros ao remover registos. + Os dados recolhidos não foram encontrados. + Não foi especificado nenhum diretório de saída. + + Sem dados + + Voltar + Ilustração de %1$s + Campo de assinatura + Para assinar, toque no ecrã e escreva com o dedo. + Assinado + Por assinar + Selecionado + Não selecionado + Nivelador de resposta. De %1$s a %2$s + Imagem sem etiqueta + Iniciar tarefa + ativa + correto + incorreto + quiescente + Mosaico do jogo de memória + Pré‑visualização da captura + Imagem capturada + Pré‑visualização de vídeo + vídeo filmado + Impossível colocar o disco com tamanho %1$s no disco com tamanho %2$s + Destino + Torre + Dê dois toques para colocar o disco + Dê dois toques para selecionar o primeiro disco + Tem disco com tamanhos %1$s + Vazio + Intervalo de %1$s a %2$s + Pilha composta por + e + Ponto: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-pt/strings.xml b/backbone/src/main/res/values-pt/strings.xml new file mode 100644 index 000000000..c816089aa --- /dev/null +++ b/backbone/src/main/res/values-pt/strings.xml @@ -0,0 +1,396 @@ + + + + + Consentimento + Nome + Sobrenome + Obrigatório + Revisão + Analise o formulário abaixo e toque em Aceitar se estiver pronto para continuar. + Revisão + Assinatura + Assine com o seu dedo na linha abaixo. + Assine Aqui + Página %1$ld de %2$ld + + Bem-vindo + Coleta de Dados + Privacidade + Uso dos Dados + Opinião do Estudo + Tarefas do Estudo + Compromisso de Tempo + Desistência + Saiba Mais + + Saiba mais sobre como os dados são coletados + Saiba mais sobre como os dados são usados + Saiba mais sobre como sua privacidade e sua identidade são protegidas + Antes, saiba mais sobre o estudo + Saiba mais sobre a pesquisa de opinião + Saiba mais sobre o impacto do estudo sobre o seu tempo + Saiba mais sobre as tarefas envolvidas + Saiba mais sobre a desistência + + Opções de Compartilhamento + Compartilhar meus dados com %1$s e pesquisadores qualificados mundialmente + Compartilhar meus dados somente com %1$s + %1$s receberá os seus dados de estudo conforme a sua participação neste estudo.\n\nO compartilhamento dos seus dados de estudo codificados de forma mais ampla (sem conter informações como o seu nome) poderá beneficiar esta pesquisa e pesquisas futuras. + Saiba mais sobre o compartilhamento de dados + + Nome do(a) %1$s (impresso) + Assinatura do(a) %1$s + Data + + Passo %1$s de %2$s + + Valor inválido + %1$s excede o valor máximo permitido (%2$s). + %1$s é menor que o valor mínimo permitido (%2$s). + %1$s não é um valor válido. + + Endereço de e-mail inválido: %1$s + + Digite um endereço + Não Foi Possível Encontrar o Endereço Especificado + Não foi possível determinar sua localização atual. Digite um endereço ou vá para um local com melhor sinal GPS, se aplicável. + O acesso aos Serviços de Localização foi negado. Permita que este aplicativo use os Serviços de Localização nos Ajustes. + Não foi possível encontrar um resultado para o endereço inserido. Certifique-se de que o endereço é válido. + Você não esta conectado à Internet ou o número máximo de solicitações de busca de endereço foi atingido. Se você não está conectado à Internet, ative a conexão Wi-Fi para responder a esta pergunta, pule esta pergunta se houver um botão que permita esta ação ou volte para a busca quando estiver conectado à Internet. Caso contrário, tente novamente em alguns minutos. + + Conteúdo de texto excedendo duração máxima: %1$s + + A câmera não está disponível na tela dividia. + + E-mail + jaime@example.com + Senha + Digite a senha + Confirmar + Digite a senha novamente + As senhas não coincidem. + Informações Adicionais + Jaime + Silveira + Sexo + Escolha um sexo + Data de Nascimento + Escolha uma data + + Verificação + Verifique seu E-mail + Caso não tenha recebido um e-mail de verificação e gostaria que ele fosse enviado novamente, toque no link abaixo. + Reenviar e-mail de verificação + + Início de Sessão + Esqueceu a senha? + + Digite o código + Confirmar código + Código salvo + Código autenticado + Digite o seu código anterior + Digite seu novo código + Confirme o seu novo código + Código Incorreto + Os códigos não coincidem. Tente de novo. + Realize a autenticação com Touch ID + Erro do Touch ID + Apenas caracteres numéricos são permitidos. + Indicador de progresso de inserção de código + Você digitou %1$s de %2$s dígitos + Esqueceu o Código? + + Não foi possível adicionar a chave. + Não foi possível atualizar a chave. + Não foi possível apagar a chave. + Não foi possível encontrar a chave. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Feminino + Masculino + Outro + + Não + Sim + + cm + pés + pol + + Toque para responder + Selecione uma resposta + Toque para selecionar + Toque para escrever + + Concordar + Cancelar + OK + Limpar + Discordar + OK + Iniciar + Saiba mais + Seguinte + Ignorar + Ignorar esta pergunta + Iniciar Timer + Salvar para Mais Tarde + Descartar Resultados + Finalizar Tarefa + Salvar + Limpar resposta + Esta resposta não pode ser modificada. + + Iniciando atividade em + Atividade Concluída + Seus dados serão analisados e você será notificado quando os resultados estiverem prontos. + %1$s segundos restantes. + + Capturar Imagem + Recapturar Imagem + Não foi encontrada uma câmera. Não foi possível concluir esta etapa. + Para concluir esta etapa, permita que este aplicativo acesse a câmera, em Ajustes. + Não foi especificado um diretório de saída para imagens capturadas. + A imagem capturada não pôde ser salva. + + Iniciar Gravação + Parar Gravação + Recapturar Vídeo + + Condicionamento Físico + Distância (%1$s) + Frequência Cardíaca (bpm) + Sente confortavelmente por %1$s. + Ande o mais rápido que conseguir por %1$s. + Esta atividade monitora a sua pulsação e mede o quão longe você consegue andar em %1$s. + Ande ao ar livre o mais rápido que conseguir por %1$s. Ao terminar, sente e descanse confortavelmente por %2$s. Para começar, toque em Iniciar. + + Caminhada e Equilíbrio + Esta atividade mede a sua caminhada e o seu equilíbrio ao andar e ficar de pé. Não prossiga caso não possa andar seguramente sem ajuda. + Encontre um local onde você possa andar seguramente sem ajuda para dar %1$d passos em linha reta. + Coloque o telefone em um bolso ou bolsa e siga as instruções de áudio. + Agora pare e fique de pé por %1$s. + Pare e fique de pé por %1$s. + Vire-se e volte para o local onde você começou. + Dê até %1$d passos em linha reta. + + Encontre um local onde você possa andar para a frente e para trás em linha reta com segurança. Tente andar continuamente, virando ao final do caminho, como se estivesse dando a volta em um cone.\n\nA seguir, você será instruído a virar em um círculo completo e ficar parado com os braços ao lado do corpo e os pés separados pela distância entre os ombros. + Toque em Iniciar quando estiver pronto.\nColoque o telefone em um bolso ou bolsa e siga as instruções de áudio. + Ande para a frente e para trás em linha reta por %1$s. Ande normalmente. + Vire em um círculo completo e fique parado por %1$s. + Você concluiu a atividade. + + Velocidade de Toque + Mão Direita + Mão Esquerda + Esta atividade mede a sua velocidade de toque. + Coloque o telefone em uma superfície plana. + Use dois dedos da mesma mão para tocar nos botões da tela alternadamente. + Use dois dedos da mão direita para tocar nos botões da tela alternadamente. + Use dois dedos da mão esquerda para tocar nos botões da tela alternadamente. + Agora repita o mesmo teste com a mão direita. + Agora repita o mesmo teste com a mão esquerda. + Toque um dedo e depois o outro. Tente manter o ritmo dos toques o mais constante possível. Continue tocando por %1$s. + Toque em Iniciar para começar. + Toque em Seguinte para começar. + Toque + Total de Toques + Toque nos botões com o máximo de consistência que puder usando dois dedos. + Toque nos botões usando a mão DIREITA. + Toque nos botões usando a mão ESQUERDA. + Ignorar esta mão + + Voz + Toque em Iniciar para começar. + Diga “Aaaaah” no microfone por quanto tempo conseguir. + Tome fôlego e diga “Aaaaah” no microfone por quanto tempo conseguir. Mantenha o volume vocal estável para que as barras de áudio permaneçam azuis. + Esta atividade avalia a sua voz gravando-a com o microfone na parte inferior do telefone. + Alto Demais + Não foi possível gravar áudio + Aguarde a verificação do nível de ruído de fundo. + O nível de ruído ambiente está muito alto para gravar a sua voz. Vá até um local mais silencioso e tente novamente. + Toque em Seguinte quando pronto. + + Audiometria Tonal + Esta atividade mede a sua capacidade de ouvir sons diversos. + Antes de começar, conecte e coloque seus fones de ouvido. + Toque em Iniciar para começar. + Agora você deverá ouvir um tom. Ajuste o volume usando os controles na lateral do seu dispositivo.\n\nToque no botão quando você estiver pronto para começar. + Toque no botão toda vez que começar a ouvir um som. + %1$s Hz, Esquerdo + %1$s Hz, Direito + + Memória Espacial + Esta atividade mede a sua memória espacial de curto prazo ao pedir que você repita a ordem em que as %1$s se acendem. + flores + flores + Algumas das %1$s serão acesas uma por vez. Toque nas %2$s na mesma ordem em que se acenderam. + Algumas das %1$s serão acesas uma por vez. Toque nas %2$s na ordem reversa em que se acenderam. + Para começar, toque em Iniciar e observe com atenção. + %1$s + Pontuação + Observe as imagens de %1$s se acenderem + Toque nas imagens de %1$s na ordem em que se acenderem + Toque nas imagens de %1$s em ordem reversa + Sequência Concluída + Para continuar, toque em Seguinte + Tentar Novamente + Não foi desta vez. Toque em Seguinte para continuar. + O Tempo Acabou + O tempo acabou.\nToque em Seguinte para continuar. + Jogo Concluído + Em pausa + Para continuar, toque em Seguinte + + Tempo de Reação + Esta atividade avalia quanto tempo você leva para responder a um sinal visual. + Agite o dispositivo em qualquer direção logo que o ponto azul aparecer na tela. Você será solicitado a fazer isso %D vezes. + Toque em Iniciar para começar. + Tentativa %1$s de %2$s + Agite o dispositivo rapidamente quando o círculo azul aparecer + + Torre de Hanói + Esta atividade avalia sua capacidade para resolver puzzles. + Mova toda a pilha para a plataforma em destaque usando o mínimo de movimentos possíveis. + Para iniciar, toque em Começar + Solucione o Puzzle + Número de Movimentos: %1$s \n %2$s + Não consigo solucionar este puzzle + + Caminhada Cronometrada + Esta atividade mede o funcionamento de suas extremidades inferiores. + Encontre um local, de preferência ao ar livre, onde você possa andar por %1$s em linha reta o mais rápido que puder com segurança. Não desacelere até ultrapassar a linha de chegada. + Toque em Seguinte para começar. + Dispositivo assistivo + Use o mesmo dispositivo assistivo em cada teste. + Você usa uma órtese no tornozelo? + Você usa um dispositivo assistivo? + Toque para selecionar resposta. + Nenhum + Bengala Unilateral + Muleta Unilateral + Bengala Bilateral + Muleta Bilateral + Andador/Rodador + Ande por até %1$s em linha reta. + Vire-se e volte para o local onde você começou. + Toque em OK ao concluir. + + TASAM + TASVM + TASAV + O Teste de Adição Sequencial de Audição Medida processa a velocidade e capacidade de calcular para medir suas informações auditivas. + O Teste de Adição Sequencial de Visão Medida processa a velocidade e capacidade de calcular para medir suas informações visuais. + O Teste de Adição Sequencial de Audição e Visão mede suas informações auditivas e visuais processando a velocidade e a capacidade de calcular. + Dígitos únicos são apresentados a cada %1$s segundos.\nVocê deve adicionar cada novo dígito ao dígito anterior.\nAtenção, não calcule um total cumulativo, apenas a soma dos últimos dois números. + Toque em Iniciar para começar. + Memorize este primeiro dígito. + Adicione esse novo dígito ao anterior. + - + + Teste dos %1$s Pinos nos Buracos + Esta atividade determinará a capacidade das suas extremidades superiores solicitando que você coloque um círculo em um buraco. Você precisará fazer isso %1$s vezes. + Tanto a sua mão direita como a esquerda serão testadas.\nVocê deve pegar o círculo o mais rapidamente possível, colocá-lo no buraco e, após repetir %1$s vezes, removê-lo novamente %2$s vezes. + Tanto a sua mão direita como a esquerda serão testadas.\nVocê deve pegar o círculo o mais rapidamente possível, colocá-lo no buraco e, após repetir %1$s vezes, removê-lo novamente %2$s vezes. + Toque em Iniciar para começar. + Coloque o círculo no buraco com sua mão esquerda. + Coloque o círculo no buraco com sua mão direita. + Coloque o círculo atrás da linha com sua mão esquerda. + Coloque o círculo atrás da linha com sua mão direita. + Pegue o círculo usando dois dedos. + Tire o dedo da tela para largar o círculo. + + Atividade de Tremor + Esta atividade mede o tremor das mãos em várias posições. Encontre um local onde você possa se sentar confortavelmente durante esta atividade. + Segure o telefone na mão mais afetada, conforme mostrado na imagem abaixo. + Segure o telefone na mão DIREITA, conforme mostrado na imagem abaixo. + Segure o telefone na mão ESQUERDA, conforme mostrado na imagem abaixo. + Você será solicitado a realizar %1$s sentado com o telefone na mão. + uma tarefa + duas tarefas + três tarefas + quatro tarefas + cinco tarefas + Toque em Seguinte para prosseguir. + Prepare-se para segurar o telefone na perna. + Prepare-se para segurar o telefone na perna com a mão ESQUERDA. + Prepare-se para segurar o telefone na perna com a mão DIREITA. + Continue segurando o telefone na perna por %1$d segundos. + Agora segure o telefone com a mão estendida na altura do ombro. + Agora segure o telefone com a mão ESQUERDA estendida na altura do ombro. + Agora segure o telefone com a mão DIREITA estendida na altura do ombro. + Continue segurando o telefone com a mão estendida por %1$d segundos. + Agora segure o telefone na altura do ombro com o cotovelo dobrado. + Agora segure o telefone com a mão ESQUERDA na altura do ombro com o cotovelo dobrado. + Agora segure o telefone com a mão DIREITA na altura do ombro com o cotovelo dobrado. + Continue segurando o telefone com o cotovelo dobrado por %1$d segundos + Agora, mantendo o cotovelo dobrado, toque o telefone no nariz repetidamente. + Agora, mantendo o cotovelo dobrado e com o telefone na mão ESQUERDA, toque o telefone no nariz repetidamente. + Agora, mantendo o cotovelo dobrado e com o telefone na mão DIREITA, toque o telefone no nariz repetidamente. + Continue tocando o telefone no nariz por %1$d segundos + Prepare-se para fazer um aceno como da rainha (acene virando o pulso). + Prepare-se para fazer um aceno como da rainha com o telefone na mão ESQUERDA (acene virando o pulso). + Prepare-se para fazer um aceno como da rainha com o telefone na mão DIREITA (acene virando o pulso). + Continue fazendo um aceno como da rainha por %1$d segundos. + Agora passe o telefone para a mão ESQUERDA e continue para a tarefa seguinte. + Agora passe o telefone para a mão DIREITA e continue para a tarefa seguinte. + Continue para a tarefa seguinte. + Atividade concluída. + Você será solicitado a realizar %1$s sentado com o telefone em uma das mãos e, depois, na outra. + Não posso realizar esta atividade com a mão ESQUERDA. + Não posso realizar esta atividade com a mão DIREITA. + Posso realizar esta atividade com ambas as mãos. + + Não foi possível criar o arquivo + Não foi possível remover arquivos de registro suficientes para alcançar o limite + Erro ao definir atributo + Arquivo não marcado como apagado (não marcado como enviado) + Múltiplos erros ao remover os registros + Nenhum dado coletado foi encontrado. + Um diretório de saída não foi especificado + + Nenhum Dado + + Voltar + Ilustração de %1$s + Campo de assinatura designada + Toque na tela e mova o dedo para assinar + Assinado + Não assinado + Selecionado + Não selecionado + Controle de resposta. Os valores vão de %1$s a %2$s + Imagem sem rótulo + Iniciar tarefa + ativo + correto + incorreto + em repouso + Peça do jogo de memória + Pré-visualização da captura + Imagem capturada + Pré-visualização da captura de vídeo + Vídeo capturado + Não pôde colocar o disco com %1$s de tamanho no disco com %2$s de tamanho + Destino + Torre + Toque duas vezes para posicionar o disco + Toque duas vezes para selecionar o disco mais acima + Tem disco com tamanhos %1$s + Esvaziar + Varia entre %1$s e %2$s + Pilha composta de + e + Ponto: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-ro/strings.xml b/backbone/src/main/res/values-ro/strings.xml new file mode 100644 index 000000000..4e5e3fec5 --- /dev/null +++ b/backbone/src/main/res/values-ro/strings.xml @@ -0,0 +1,396 @@ + + + + + Consimțământ + Prenume + Nume de familie + Obligatoriu + Recapitulare + Recapitulați formularul de mai jos și apăsați “De acord” dacă sunteți gata să continuați. + Recapitulare + Semnătura + Semnați-vă folosind un deget pe linia de mai jos + Semnați aici + Pagina %1$ld din %2$ld + + Bun venit + Colectarea datelor + Intimitate + Utilizarea datelor + Chestionar studiu + Sarcini studiu + Alocarea timpului + Retragere + Aflați mai multe + + Aflați mai multe despre colectarea datelor + Aflați mai multe despre utilizarea datelor + Aflați mai multe despre protejarea intimității și identității dvs. + Mai întâi, aflați mai multe despre studiu + Aflați mai multe despre chestionarul studiului + Aflați mai multe despre impactul studiului asupra timpului dvs. + Aflați mai multe despre sarcinile implicate + Aflați mai multe despre retragere + + Opțiuni de partajare + Partajați datele dvs. cu %1$s și cercetători calificați din întreaga lume + Partajați datele dvs. doar cu %1$s + Datele rezultate în urma participării dvs. la acest studiu vor fi trimise la %1$s .\n\nPartajarea pe o scară mai largă a datelor dvs. codificate (fără a include informații precum numele dvs.) poate fi utilă pentru acest studiu de cercetare și pentru altele viitoare. + Mai multe despre partajarea datelor + + Nume %1$s (tipărit) + Semnătură %1$s + Data + + Pasul %1$s din %2$s + + Valoare nevalidă + Valoarea “%1$s” depășește maximul permis (%2$s). + Valoarea “%1$s” este mai mică decât minimul permis (%2$s). + %1$s nu este o valoare validă. + + Adresă de e-mail nevalidă: %1$s + + Introduceți o adresă + Adresa specificată nu a putut fi găsită + Imposibil de rezolvat localizarea dvs. actuală. Introduceți o adresă sau mergeți într-un loc cu semnal GPS mai bun dacă este cazul. + Accesul la serviciile de localizare a fost refuzat. Acordați acestei aplicații permisiunea de a utiliza serviciile de localizare din Configurări. + Imposibil de găsit un rezultat pentru adresa introdusă. Asigurați-vă că adresa este validă. + Nu dispuneți de o conexiune la Internet sau ați depășit numărul maxim al solicitărilor de identificare a adreselor. Dacă nu dispuneți de conexiune la Internet, activați Wi-Fi pentru a răspunde la această întrebare, omiteți întrebarea dacă butonul de omitere este disponibil sau reveniți la chestionar după ce vă conectați la Internet. În caz contrar, reîncercați peste câteva minute. + + Conținutul de text depășește lungimea maximă: %1$s + + Camera nu este disponibilă în ecranul divizat. + + E-mail + ioanapopa@example.com + Parolă + Introduceți parola + Confirmați + Introduceți din nou parola + Parolele nu coincid. + Informații suplimentare + Ioana + Popescu + Sex + Alegeți o opțiune + Data nașterii + Alegeți o dată + + Verificare + Verificați-vă e‑mailul + Apăsați pe linkul de mai jos dacă nu ați primit un e‑mail de verificare și ați dori retrimiterea acestuia. + Retrimiteți e‑mailul de verificare + + Login + Ați uitat parola? + + Introduceți codul + Confirmare cod + Cod salvat + Cod autentificat + Introduceți codul vechi + Introduceți codul nou + Confirmați noul cod + Cod incorect + Codurile nu coincid. Reîncercați. + Autentificați-vă cu Touch ID + Eroare Touch ID + Sunt permise doar caractere numerice. + Indicator de progres pentru introducerea codului + %1$s din %2$s cifre introduse + Ați uitat codul de acces? + + Imposibil de adăugat articolul din portchei. + Imposibil de actualizat articolul din portchei. + Imposibil de șters articolul din portchei. + Imposibil de găsit articolul din portchei. + + A+ + A- + AB+ + AB- + B+ + B- + 0+ + 0- + + Feminin + Masculin + Altul + + Nu + Da + + cm + ft + in + + Apăsați pentru a răspunde + Selectați un răspuns + Apăsați pentru a selecta + Apăsați pentru a scrie + + De acord + Anulați + OK + Degajați + Nu sunt de acord + OK + Start + Aflați mai multe + Înainte + Omiteți + Omiteți această întrebare + Porniți temporizatorul + Salvați pentru mai târziu + Abandonați rezultatele + Încheiați sarcina + Salvați + Degajați răspunsul + Acest răspuns nu poate fi modificat. + + Activitatea începe peste + Activitate finalizată + Datele dvs. vor fi analizate și veți primi o notificare atunci când rezultatele vor fi disponibile. + timp rămas: %1$s secunde. + + Capturați imaginea + Recapturați imaginea + Nicio cameră găsită. Această etapă nu poate fi finalizată. + Pentru a finaliza această etapă, acordați acestei aplicații acces la cameră în Configurări. + Nu a fost specificat un director de ieșire pentru imaginile capturate. + Imaginea capturată nu a putut fi salvată. + + Porniți înregistrarea + Opriți înregistrarea + Recapturați clipul video + + Fitness + Distanță (%1$s) + Ritm cardiac (bpm) + Așezați-vă confortabil timp de %1$s. + Mergeți cât de repede puteți timp de %1$s. + Această activitate vă monitorizează ritmul cardiac și măsoară cât de mult puteți merge în %1$s. + Mergeți în aer liber cu viteza maximă posibilă timp de %1$s. Când sunteți gata, așezați-vă și stați confortabil timp de %2$s. Pentru a începe, apăsați “Start”. + + Mers și echilibru + Această activitate vă măsoară mersul și echilibrul în timp ce mergeți și stați în repaus. Nu continuați dacă nu puteți merge în siguranță, fără a necesita asistență. + Găsiți un loc unde să puteți merge în siguranță, fără a necesita asistență, aproximativ %1$d pași în linie dreaptă. + Puneți-vă telefonul într-un buzunar sau într-o geantă și urmați instrucțiunile audio. + Acum nu vă mișcați timp de %1$s. + Nu vă mișcați timp de %1$s. + Întorceți-vă și mergeți înapoi la locul de pornire. + Mergeți până la %1$d pași în linie dreaptă. + + Găsiți un loc unde să puteți merge în siguranță pe un traseu, înainte și înapoi, în linie dreaptă. Încercați să mergeți continuu, întorcându‑vă la capătul traseului ca și cum ați ocoli un jalon.\n\nÎn continuare, vi se va solicita să vă întoarceți descriind un cerc complet, apoi să stați în repaus cu brațele întinse și picioarele depărtate la o distanță aproximativ egală cu lățimea umerilor. + Apăsați “Start” când sunteți gata să începeți.\nPuneți apoi telefonul în buzunar sau în geantă și urmați instrucțiunile audio. + Mergeți înainte și înapoi în linie dreaptă timp de %1$s. Mergeți așa cum o ați face‑o în mod normal. + Întoarceți‑vă descriind un cerc complet, apoi stați în repaus timp de %1$s. + Ați finalizat activitatea. + + Viteză de apăsare + Mâna dreaptă + Mâna stângă + Această activitate vă măsoară viteza de apăsare cu degetele. + Puneți‑vă telefonul pe o suprafață plată. + Folosiți două degete de la aceeași mână pentru a apăsa alternativ butoanele de pe ecran. + Folosiți două degete de la mâna dreaptă pentru a apăsa alternativ butoanele de pe ecran. + Folosiți două degete de la mâna stângă pentru a apăsa alternativ butoanele de pe ecran. + Acum, repetați același test folosind mâna dreaptă. + Acum, repetați același test folosind mâna stângă. + Apăsați cu un deget, apoi cu celălalt. Încercați ca apăsările să se succeadă cât regulat cu putință. Continuați să apăsați timp de %1$s. + Apăsați “Start” pentru a începe. + Apăsați “Înainte” pentru a începe. + Apăsați + Total apăsări + Apăsați butoanele cât de consecvent puteți folosind două degete. + Apăsați butoanele folosind mâna DREAPTĂ. + Apăsați butoanele folosind mâna STÂNGĂ. + Omiteți această mână + + Voce + Apăsați “Start” pentru a începe. + Spuneți “Aaaaah” în microfon cât de mult timp puteți. + Inspirați adânc și spuneți “Aaaaah” în microfon cât de mult timp puteți. Mențineți un volum vocal constant, astfel încât barele audio să rămână albastre. + Această activitate vă evaluează vocea prin înregistrarea acesteia cu microfonul din partea de jos a telefonului dvs. + Prea tare + Imposibil de înregistrat audio + Așteptați să se verifice nivel zgomotului de fundal. + Nivelul zgomotului ambiental este prea puternic pentru a vă înregistra vocea. Mutați‑vă într‑un loc mai liniștit și reîncercați. + Apăsați “Înainte” când sunteți gata. + + Audiometrie tonală + Această activitate vă măsoară capacitatea de a auzi diferite sunete. + Înainte de a începe, conectați și puneți‑vă căștile. + Apăsați “Start” pentru a începe. + Ar trebui să auziți un ton acum. Reglați volumul folosind comenzile de pe partea laterală a dispozitivului.\n\nApăsați butonul când sunteți gata să începeți. + Apăsați butonul de fiecare dată când începeți să auziți un sunet. + %1$s Hz, stânga + %1$s Hz, dreapta + + Memorie spațială + Această activitate vă măsoară memoria spațială pe termen scurt, solicitându-vă să repetați ordinea în care se aprind simbolurile reprezentând %1$s. + flori + flori + Unele dintre simbolurile de %1$s se vor aprinde pe rând. Apăsați pe %2$s în ordinea în care s‑au aprins. + Unele dintre simbolurile de %1$s se vor aprinde pe rând. Apăsați pe %2$s în ordinea inversă aprinderii lor. + Pentru a începe, apăsați “Start”, apoi priviți cu atenție. + %1$s + Scor + Priviți cum se aprind simbolurile de %1$s + Apăsați pe %1$s în ordinea în care s‑au aprins + Apăsați pe %1$s în ordine inversă + Secvență finalizată + Pentru a continua, apăsați “Înainte” + Reîncercați + Nu ați reușit să vă încadrați în timpul alocat. Apăsați “Înainte” pentru a repeta. + Timpul a expirat + Ați epuizat timpul.\nApăsați “Înainte” pentru a continua. + Joc finalizat + Suspendat + Pentru a continua, apăsați “Înainte” + + Timp de reacție + Această activitate evaluează timpul de care aveți nevoie pentru a răspunde la un stimul vizual. + Agitați dispozitivul în orice direcție imediat ce apare punctul albastru pe ecran. Va trebui să repetați acțiunea de %D ori. + Apăsați “Start” pentru a începe. + Încercarea %1$s din %2$s + Agitați rapid dispozitivul atunci când apare cercul albastru + + Turnul din Hanoi + Această activitate evaluează aptitudinile dvs. de rezolvare a puzzle‑urilor + Mutați toată stiva pe platforma evidențiată din cât mai puține mișcări posibil. + Apăsați “Start” pentru a începe + Rezolvați puzzle‑ul + Număr de mutări: %1$s \n %2$s + Nu pot rezolva acest puzzle + + Mers cronometrat + Această activitate măsoară funcționarea extremităților dvs. inferioare. + Găsiți un loc, preferabil afară, unde să puteți merge aproximativ %1$s în linie dreaptă cât de repede posibil, dar în siguranță. Nu încetiniți decât după ce treceți linia de sosire. + Apăsați “Înainte” pentru a începe. + Dispozitiv asistiv + Utilizați același dispozitiv asistiv pentru fiecare test. + Purtați o orteză de gleznă? + Utilizați un dispozitiv asistiv? + Apăsați aici pentru un răspuns. + Nu + Baston unilateral + Cârjă unilaterală + Bastoane bilaterale + Cârje bilaterale + Cadru de mers/cu rotile + Mergeți până la %1$s în linie dreaptă. + Întorceți-vă și mergeți înapoi la locul de pornire. + Apăsați OK când terminați. + + PASAT + PVSAT + PAVSAT + Testul PAVSAT (testul auditiv pe etape de adunare în serie) măsoară aptitudinile dvs. de calcul și viteza de procesare a informațiilor auditive. + Testul PAVSAT (testul vizual pe etape de adunare în serie) măsoară aptitudinile dvs. de calcul și viteza de procesare a informațiilor vizuale. + Testul PAVSAT (testul auditiv și vizual pe etape de adunare în serie) măsoară aptitudinile dvs. de calcul și viteza de procesare a informațiilor auditive și vizuale. + Pe ecran va fi afișată câte o cifră la fiecare %1$s secunde.\nTrebuie să adunați fiecare cifră nouă cu cea imediat precedentă acesteia.\nAtenție, nu trebuie să calculați un total cumulat, ci doar suma ultimelor două numere. + Apăsați “Start” pentru a începe. + Rețineți această primă cifră. + Adăugați această nouă cifră la cea anterioară. + - + + Testul cu %1$s cercuri + Această activitate vă măsoară funcționalitatea extremităților superioare, solicitându‑vă să plasați un cerc într‑un orificiu. Va trebui să repetați acțiunea de %1$s ori. + Vor fi testate atât mâna dvs. stângă, cât și cea dreaptă.\nTrebuie să ridicați cercul cât mai repede posibil, să îl puneți în orificiu și, după ce repetați acțiunea de %1$s ori, să îl eliminați din nou de %2$s ori. + Vor fi testate atât mâna dvs. dreaptă, cât și cea stângă.\nTrebuie să ridicați cercul cât mai repede posibil, să îl puneți în orificiu și, după ce repetați acțiunea de %1$s ori, să îl eliminați din nou de %2$s ori. + Apăsați “Start” pentru a începe. + Puneți cercul în orificiu folosind mâna stângă. + Puneți cercul în orificiu folosind mâna dreaptă. + Puneți cercul după linie folosind mâna stângă. + Puneți cercul după linie folosind mâna dreaptă. + Ridicați cercul folosind două degete. + Ridicați degetele pentru a elibera cercul. + + Activitate referitoare la tremor + Această activitate vă măsoară tremorul mâinilor în diverse poziții. Găsiți un loc unde să puteți sta confortabil pe durata activității. + Țineți telefonul în mâna mai afectată, ca în imaginea de mai jos. + Țineți telefonul în mâna DREAPTĂ, ca în imaginea de mai jos. + Țineți telefonul în mâna STÂNGĂ, ca în imaginea de mai jos. + Vi se va solicita să efectuați %1$s stând cu telefonul în mână. + o sarcină + două sarcini + trei sarcini + patru sarcini + cinci sarcini + Apăsați “Înainte” pentru a continua. + Pregătiți‑vă să țineți telefonul pe genunchi. + Pregătiți‑vă să țineți telefonul pe genunchi cu mâna STÂNGĂ. + Pregătiți‑vă să țineți telefonul pe genunchi cu mâna DREAPTĂ. + Continuați să țineți telefonul pe genunchi timp de %1$d secunde. + Acum, țineți telefonul cu mâna întinsă la înălțimea umărului. + Acum, țineți telefonul cu mâna STÂNGĂ întinsă la înălțimea umărului. + Acum, țineți telefonul cu mâna DREAPTĂ întinsă la înălțimea umărului. + Continuați să țineți telefonul cu mâna întinsă timp de %1$d secunde. + Acum, țineți telefonul la înălțimea umărului, cu cotul îndoit. + Acum, țineți telefonul cu mâna STÂNGĂ la înălțimea umărului, cu cotul îndoit. + Acum, țineți telefonul cu mâna DREAPTĂ la înălțimea umărului, cu cotul îndoit. + Continuați să țineți telefonul cu cotul îndoit timp de %1$d secunde. + Acum, ținându‑vă cotul îndoit, atingeți‑vă nasul cu telefonul în mod repetat. + Acum, ținându‑vă telefonul în mâna STÂNGĂ cu cotul îndoit, atingeți‑vă nasul cu telefonul în mod repetat. + Acum, ținându‑vă telefonul în mâna DREAPTĂ cu cotul îndoit, atingeți‑vă nasul cu telefonul în mod repetat. + Continuați să vă atingeți nasul cu telefonul timp de %1$d secunde. + Pregătiți‑vă să fluturați mâna, răsucind din încheietură. + Pregătiți‑vă să fluturați mâna STÂNGĂ, ținând telefonul în aceasta și răsucind din încheietură. + Pregătiți‑vă să fluturați mâna DREAPTĂ, ținând telefonul în aceasta și răsucind din încheietură. + Continuați să fluturați mâna timp de %1$d secunde. + Acum, treceți telefonul în mâna STÂNGĂ și continuați cu sarcina următoare. + Acum, treceți telefonul în mâna DREAPTĂ și continuați cu sarcina următoare. + Continuați cu sarcina următoare. + Activitate finalizată. + Vi se va solicita să efectuați %1$s stând mai întâi cu telefonul într‑o mână, apoi în cealaltă. + Nu pot efectua această activitate cu mâna STÂNGĂ. + Nu pot efectua această activitate cu mâna DREAPTĂ. + Pot efectua această activitate cu ambele mâini. + + Fișierul nu a putut fi creat + Nu au putut fi eliminate suficiente fișiere jurnal pentru atingerea pragului admis + Eroare la definirea atributului + Fișierul nu a fost marcat drept șters (nu a fost marcat drept încărcat) + Mai multe erori la eliminarea jurnalelor + Nu au fost găsite date colectate. + Nu a fost specificat un director de ieșire + + Nu există date + + Înapoi + Ilustrație reprezentând %1$s + Câmp de semnătură desemnat + Atingeți ecranul și deplasați-vă degetul pentru a semna + Semnat + Nesemnat + Selectat + Neselectat + Glisor de răspuns. Variază de la %1$s la %2$s + Imagine neetichetată + Începeți sarcina + activ + corect + incorect + pasiv + Piesă joc memorie + Previzualizare captură + Imagine capturată + Previzualizare captură video + Clip video capturat + Nu se poate plasa un disc de mărimea %1$s pe un disc de mărimea %2$s + Țintă + Turn + Apăsați dublu pentru a plasa discul + Apăsați dublu pentru a selecta discul cel mai de sus + Are disc de mărime %1$s + Gol + Interval de la %1$s la %2$s + Stiva compusă din + și + Punctul: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-ru/strings.xml b/backbone/src/main/res/values-ru/strings.xml new file mode 100644 index 000000000..e69e8f8f9 --- /dev/null +++ b/backbone/src/main/res/values-ru/strings.xml @@ -0,0 +1,396 @@ + + + + + Согласие + Имя + Фамилия + Обязательно + Проверка + Проверьте приведенную ниже форму и коснитесь «Принимаю», если Вы готовы продолжить. + Проверка + Подпись + Распишитесь пальцем над приведенной ниже линией. + Место для подписи + Стр. %1$ld из %2$ld + + Добро пожаловать! + Сбор данных + Личные данные + Использование данных + Опрос + Задачи + Соглашение о времени + Отказ от участия + Подробнее + + Подробнее о сборе данных + Подробнее об использовании данных + Подробнее о конфиденциальности и защите личной информации + Подробнее об исследовании + Подробнее об опросе + Подробнее о том, сколько времени занимает исследование + Подробнее о задачах, включенных в исследование + Подробнее об отказе от участия + + Параметры предоставления данных + Предоставлять мои данные %1$s и мировому научному сообществу. + Предоставлять мои данные только %1$s + %1$s получат данные о Вашем участии в этом исследовании.\n\nПредоставление этих данных (без Вашей личной информации) научному сообществу может помочь усовершенствовать это и будущие исследования. + Подробнее о предоставлении данных + + %1$s (расшифровка подписи) + %1$s (подпись) + Дата + + Шаг %1$s из %2$s + + Недопустимое значение + %1$s превышает максимально допустимое значение (%2$s). + %1$s меньше минимально допустимого значения (%2$s). + Недопустимое значение: %1$s. + + Недействительный адрес e-mail: %1$s + + Введите адрес + Указанный адрес не найден + Не удается устранить проблему с текущей геопозицией. Введите адрес или переместитесь в геопозицию с лучшим GPS‑сигналом, если возможно. + В доступе к службам геолокации отказано. Разрешите этой программе использовать службы геолокации в Настройках. + Не удается найти результат по введенному адресу. Убедитесь, что адрес действителен. + Вы не подключены к Интернету или превысили максимальное количество запросов на поиск адресов. Если Вы не подключены к Интернету, включите Wi‑Fi, чтобы ответить на это вопрос; пропустите этот вопрос, если кнопка «Пропустить» доступна; или вернитесь к опросу после подключения к Интернету. В противном случае, повторите попытку через несколько минут. + + Превышены максимальная длина текста: %1$s + + Камера недоступна в разделенном экране. + + Е‑mail + jappleseed@example.com + Пароль + Введите пароль + Подтверждение + Введите пароль снова + Пароли не совпадают. + Дополнительная информация + Иван + Арсентьев + Пол + Выберите пол + Дата рождения + Выберите дату + + Проверка + Проверка e‑mail + Нажмите ссылку ниже, если Вы не получили e‑mail с подтверждением и хотите, чтобы он был отправлен повторно. + Повторно отправить e‑mail + + Вход + Забыли пароль? + + Введите код‑пароль + Подтвердите код‑пароль + Код‑пароль сохранен + Код‑пароль аутентифицирован + Введите старый код‑пароль + Введите новый код‑пароль + Подтвердите новый код-пароль + Неверный код‑пароль + Код‑пароли не совпали. Повторите попытку. + Выполните аутентификацию с Touch ID + Ошибка Touch ID + Можно использовать только цифры. + Индикатор хода ввода код‑пароля + Введено цифр: %1$s из %2$s + Забыли код‑пароль? + + Не удалось добавить объект связки ключей. + Не удалось обновить объект связки ключей. + Не удалось удалить объект связки ключей. + Не удалось найти объект связки ключей. + + A(II)Rh+ + A(II)Rh‑ + AB(IV)Rh+ + AB(IV)Rh‑ + B(III)Rh+ + B(III)Rh‑ + 0(I)Rh+ + 0(I)Rh‑ + + Женский + Мужской + Другой + + Нет + Да + + см + фт + дюйм. + + Коснитесь, чтобы ответить + Выберите ответ + Коснитесь, чтобы выбрать + Коснитесь, чтобы написать + + Принимаю + Отменить + ОК + Очистить + Не принимаю + Готово + Начать + Подробнее + Далее + Пропустить + Пропустить этот вопрос + Запустить таймер + Сохранить на потом + Сбросить результаты + Завершить + Сохранить + Очистить ответ + Этот ответ нельзя изменить. + + Задача начинается через + Задача выполнена + Данные будут проанализированы. Вы получите уведомление, когда результаты будут готовы. + Осталось %1$s с. + + Захват изображения + Повторный захват изображения + Камера не найдена. Невозможно завершить этот шаг. + Для выполнения этого шага необходимо в Настройках разрешить этой программе доступ к камере. + Не указана папка для сохранения полученных изображений. + Не удается сохранить полученное изображение. + + Начать запись + Остановить запись + Снять видео еще раз + + Выносливость + Расстояние (%1$s) + Пульс (уд/мин) + Сядьте поудобнее и сидите: %1$s. + Идите с максимальной скоростью: %1$s. + Во время выполнения этой задачи программа следит за частотой Вашего пульса и измеряет расстояние, которое Вы пройдете за %1$s. + Идите по улице с максимальной скоростью: %1$s. После этого удобно сядьте и отдыхайте: %2$s. Коснитесь «Начать», чтобы приступить. + + Координация движений + В этой задаче проверяется координация движений при ходьбе и в положении стоя. Не выполняйте эту задачу, если Вы не можете передвигаться без посторонней помощи. + Найдите место, где Вы можете безопасно пройти %1$d шагов по прямой линии без посторонней помощи. + Поместите телефон в карман или сумку и следуйте звуковым указаниям. + Теперь стойте спокойно: %1$s. + Стойте спокойно: %1$s. + Развернитесь и пройдите до исходного местоположения. + Пройдите прямо %1$d шагов. + + Найдите место, где бы Вы могли ходить по прямой линии в обоих направлениях. Постарайтесь идти непрерывно, разворачиваясь на концах линии так, как будто обходите острый угол.\n\nЗатем Вы получите указание развернуться на 360 градусов и стоять спокойно, опустив руки вдоль тела и выставив ноги на ширине плеч. + Коснитесь «Начать», когда будете готовы приступить к выполнению задания.\nЗатем положите телефон в карман или сумку и следуйте звуковым указаниям. + Походите по прямой вперед и назад в течение %1$s. Идите нормальной походкой. + Развернитесь на 360 градусов и стойте спокойно %1$s. + Вы выполнили задачу. + + Скорость касания + Правая рука + Левая рука + В этой задаче оценивается скорость касания пальцами. + Положите телефон на ровную поверхность. + Двумя пальцами одной руки попеременно касайтесь появляющихся кнопок на экране. + Двумя пальцами правой руки попеременно касайтесь появляющихся кнопок на экране. + Двумя пальцами левой руки попеременно касайтесь появляющихся кнопок на экране. + Теперь повторите это упражнение правой рукой. + Теперь повторите это упражнение левой рукой. + Коснитесь одним пальцем, затем другим. Старайтесь, чтобы касания были максимально регулярными. Продолжайте в течение %1$s. + Чтобы приступить, коснитесь «Начать». + Чтобы начать, коснитесь «Далее». + Коснитесь + Всего касаний + Касайтесь кнопок двумя пальцами с одинаковой скоростью. + Касайтесь кнопок пальцами ПРАВОЙ руки. + Касайтесь кнопок пальцами ЛЕВОЙ руки. + Пропустить эту руку + + Голос + Чтобы приступить, коснитесь «Начать». + Произносите «Ааааа» в микрофон как можно дольше. + Сделайте глубокий вдох и произнесите «Ааааа» в микрофон как можно дольше. Сохраняйте такую громкость голоса, чтобы полоски звука на диаграмме оставались синими. + Во время выполнения этой задачи программа записывает Вашу речь через микрофон телефона и затем оценивает характеристики Вашего голоса. + Слишком громко + Не удается записать аудио. + Подождите, пока устройство измерит уровень фонового шума. + Уровень фонового шума слишком большой и не позволяет записать Ваш голос. Найдите более тихое место и повторите попытку. + Коснитесь «Далее», когда будете готовы. + + Аудиометрия + В этой задаче оценивается Ваша способность слышать различные звуки. + Перед началом подключите наушники и наденьте их. + Чтобы приступить, коснитесь «Начать». + Сейчас Вы услышите звук. Настройте громкость с помощью регулятора на боковой панели устройства.\n\nКоснитесь кнопки, когда будете готовы. + Касайтесь кнопки каждый раз, когда будете слышать звук. + %1$s Гц, левый + %1$s Гц, правый + + Пространственная память + Во время выполнения этой задачи оцениваются возможности Вашей кратковременной пространственной памяти. Программа попросит Вас повторить последовательность, в которой подсвечиваются %1$s. + цветы + цветы + Некоторые %1$s будут подсвечиваться поочередно в определенном порядке. Коснитесь этих %2$s в том же порядке. + Некоторые %1$s будут подсвечиваться поочередно в определенном порядке. Коснитесь этих %2$s в обратном порядке. + Чтобы приступить, коснитесь «Начать» и внимательно смотрите на экран. + %1$s + Счет + Следите за тем, как подсвечиваются %1$s + Коснитесь изображений (%1$s) в том порядке, в котором они подсвечивались + Коснитесь изображений (%1$s) в обратном порядке + Последовательность выполнена + Нажмите «Далее», чтобы продолжить + Повторить + В этот раз Вы не совсем справились с заданием. Коснитесь «Далее», чтобы продолжить. + Время истекло + Вам не хватило времени.\nНажмите «Далее», чтобы продолжить. + Игра завершена + Пауза + Нажмите «Далее», чтобы продолжить + + Время реакции + В этой задаче измеряется время реакции на визуальный сигнал. + Как только на экране появится синяя точка, встряхните устройство в любом направлении. Количество повторений этого задания: %D. + Чтобы приступить, коснитесь «Начать». + Попытка %1$s из %2$s + При появлении синего круга быстро встряхните устройство + + Ханойская башня + В этой задаче проверяется способность решать сложные вопросы. + Переместите всю стопку на отмеченное цветом место за наименьшее число ходов. + Чтобы приступить, коснитесь «Начать» + Решить задачу + Число ходов: %1$s \n %2$s + Я не могу решить эту задачу + + Ходьба на время + В этой задаче проверяется работоспособность нижних конечностей. + Найдите место, предпочтительно снаружи, где Вы смогли бы безопасно идти %1$s по прямой линии с максимальной скоростью. Не замедляйте шаг, пока не дойдете до финиша. + Чтобы начать, коснитесь «Далее». + Вспомогательные приспособления + Используйте одно и тоже вспомогательное приспособление для всех упражнений. + Вы носите голеностопный ортез? + Вы пользуетесь вспомогательными приспособлениями? + Коснитесь, чтобы выбрать ответ. + Нет + Одна трость + Один костыль + Трости + Костыли + Ходунки/ролятор + Пройдите прямо %1$s. + Развернитесь и пройдите до исходного местоположения. + Коснитесь «Готово» после завершения. + + PASAT + PVSAT + PAVSAT + В слуховом тесте на сложение с заданным темпом измеряются способность обрабатывать аудиальную информацию, а также способность к вычислениям. + В визуальном тесте на сложение с заданным темпом измеряются способность обрабатывать визуальную информацию, а также способность к вычислениям. + В слуховом и визуальном тестах на сложение с заданным темпом измеряются способности обрабатывать аудиальную и визуальную информацию, а также способность к вычислениям. + Каждые %1$s с будет показано однозначное число.\nВам необходимо прибавить каждое новое число к предыдущему.\nВнимание! Общую сумму вычислять не нужно, только сумму последних двух чисел. + Чтобы приступить, коснитесь «Начать». + Запомните эту первую цифру. + Прибавьте это новое число к предыдущему. + - + + Тест «Вставляем %1$s колышков» + В этой задаче проверяется работоспособность верхних конечностей. Вам предложат поместить колышек в лунку %1$s раз(а). + Проверены будут обе руки: левая и правая.\nВам потребуется поднять колышек как можно быстрее, поместить его в лунку, а затем, повторив упражнение %1$s раз(а), удалить его снова %2$s раз(а). + Проверены будут обе руки: правая и левая.\nВам потребуется поднять колышек как можно быстрее, поместить его в лунку, а затем, повторив упражнение %1$s раз(а), удалить его снова %2$s раз(а). + Чтобы приступить, коснитесь «Начать». + Поместите колышек в лунку левой рукой. + Поместите колышек в лунку правой рукой. + Поместите колышек за линию левой рукой. + Поместите колышек за линию правой рукой. + Поднимите колышек двумя пальцами. + Приподнимите пальцы, чтобы бросить колышек. + + Измерение дрожания рук + В этой задаче измеряется величина дрожания рук в различных положения. Найдите место, где бы Вы могли сидеть спокойно во время выполнения задачи. + Держите телефон рукой с более выраженными симптомами, как показано на изображении ниже. + Держите телефон ПРАВОЙ рукой, как показано на изображении ниже. + Держите телефон ЛЕВОЙ рукой, как показано на изображении ниже. + Вам нужно будет выполнить %1$s в сидячем положении, держа телефон в руке. + одно задание + два задания + три задания + четыре задания + пять заданий + Коснитесь «Далее», чтобы продолжить. + Приготовьтесь держать телефон на коленях. + Приготовьтесь держать телефон на коленях в ЛЕВОЙ руке. + Приготовьтесь держать телефон на коленях в ПРАВОЙ руке. + Держите телефон на коленях в течение %1$d с. + Теперь держите телефон в вытянутой руке на уровне плеч. + Теперь держите телефон в вытянутой ЛЕВОЙ руке на уровне плеч. + Теперь держите телефон в вытянутой ПРАВОЙ руке на уровне плеч. + Удерживайте телефон в вытянутой руке в течение %1$d с. + Теперь держите телефон в согнутой в локте руке на уровне плеч. + Теперь держите телефон в согнутой в локте ЛЕВОЙ руке на уровне плеч. + Теперь держите телефон в согнутой в локте ПРАВОЙ руке на уровне плеч. + Удерживайте телефон в руке, согнутой в локте, в течение %1$d с. + Теперь держите телефон в согнутой в локте руке и несколько раз коснитесь им своего носа. + Теперь держите телефон в согнутой в локте ЛЕВОЙ руке и несколько раз коснитесь им своего носа. + Теперь держите телефон в согнутой в локте ПРАВОЙ руке и несколько раз коснитесь им своего носа. + Касайтесь телефоном своего носа в течение %1$d с. + Подготовьтесь выполнить махание рукой (сгибая руку в запястье). + Подготовьтесь выполнить махание ЛЕВОЙ рукой, держа в ней телефон (сгибая руку в запястье). + Подготовьтесь выполнить махание ПРАВОЙ рукой, держа в ней телефон (сгибая руку в запястье). + Продолжайте махать рукой в течение %1$d с. + Теперь возьмите телефон ЛЕВОЙ рукой и перейдите к следующему заданию. + Теперь возьмите телефон ПРАВОЙ рукой и перейдите к следующему заданию. + Перейдите к следующему заданию. + Задача выполнена. + Вам нужно будет выполнить %1$s в сидячем положении, держа телефон сначала в одной руке, затем в другой. + Я не могу выполнить эту задачу ЛЕВОЙ рукой. + Я не могу выполнить эту задачу ПРАВОЙ рукой. + Я могу выполнить эту задачу обеими руками. + + Не удалось создать файл + Не удалось удалить файлы журналов, чтобы освободить достаточно места + Ошибка при установке атрибута + Файл не помечен как удаленный (выгруженный) + Несколько ошибок при удалении журналов + Собранные данные не найдены. + Не указан каталог для выходных данных + + Нет данных + + Назад + Изображение (%1$s) + Место для подписи + Прикоснитесь к экрану и проведите по нему пальцем, чтобы оставить подпись + Подписано + Неподписано + Выбрано + Не выбрано + Бегунок ответа. Диапазон от %1$s до %2$s + Изображение без подписи + Приступить + активно + верно + неверно + неподвижно + Игра на проверку памяти + Просмотр изображения + Отснятое изображение + Просмотр записанного видео + Снятое видео + Нельзя положить кольцо размера %1$s на кольцо размера %2$s + Цель + Башня + Коснитесь дважды, чтобы положить кольцо + Коснитесь дважды, чтобы выбрать самое верхнее кольцо + Содержит кольцо размеров: %1$s + Пусто + Диапазон от %1$s до %2$s + Стопка из + и + Точка: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-sk/strings.xml b/backbone/src/main/res/values-sk/strings.xml new file mode 100644 index 000000000..27b4a8556 --- /dev/null +++ b/backbone/src/main/res/values-sk/strings.xml @@ -0,0 +1,396 @@ + + + + + Súhlas + Meno + Priezvisko + Povinné + Skontrolovať + Skontrolujte formulár nižšie, a ak ste pripravení pokračovať, klepnite na Súhlasím. + Skontrolovať + Podpis + Pomocou prsta sa podpíšte na riadok nižšie. + Sem sa podpíšte + Strana %1$ld z %2$ld + + Vitajte + Zber dát + Súkromie + Využitie dát + Študijný prieskum + Študijné úlohy + Časová náročnosť + Odstúpenie + Viac informácií + + Viac informácií o zbere dát + Viac informácií o využívaní dát + Viac informácií o ochrane súkromia a identity + Viac informácií o štúdii + Viac informácií o prieskume štúdie + Viac informácií o vplyvu štúdie na váš čas + Viac informácií o úlohách + Viac informácií o odstúpení + + Možnosti zdieľania + Zdieľať dáta s inštitúciou %1$s a ďalšími kvalifikovanými výskumníkmi. + Zdieľať dáta len s inštitúciou %1$s + Vaše dáta z účasti v tejto štúdii budú doručené inštitúcii %1$s.\n\nŠiršie zdieľanie zakódovaných dát štúdie (bez informácií, ako je vaše meno) môže byť užitočné pre tento a budúci výskum. + Viac informácií o zdieľaní dát + + Meno osoby %1$s (vytlačené) + Podpis osoby %1$s + Dátum + + Krok %1$s z %2$s + + Neplatná hodnota + %1$s prekračuje maximálnu povolenú hodnotu (%2$s). + %1$s nedosahuje minimálnu povolenú hodnotu (%2$s). + %1$s nie je platná hodnota. + + Neplatná emailová adresa: %1$s + + Zadajte adresu + Špecifikovaná adresa sa nenašla + Nepodarilo sa identifikovať aktuálnu polohu. Zadajte adresu alebo prejdite na miesto s lepším signálom GPS. + Prístup k lokalizačným službám bol zamietnutý. Udeľte tejto aplikácii povolenie na používanie lokalizačných služieb v Nastaveniach. + Nenašiel sa žiadny výsledok zodpovedajúci zadanej adrese. Skontrolujte, či je adresa platná. + Buď nemáte pripojenie na internet, alebo ste prekročili maximálny počet žiadostí o vyhľadanie adresy. Ak nemáte pripojenie na internet a chcete odpovedať na túto otázku, zapnite Wi-Fi. Ak je dostupné tlačidlo Preskočiť, môžete otázku preskočiť, prípadne sa vrátiť späť po pripojení na internet. Ináč to skúste znovu o niekoľko minút. + + Textový obsah prekračuje maximálnu dĺžku: %1$s + + Kamera nie je dostupná v režime rozdelenej obrazovky. + + Email + jankohrasko@example.com + Heslo + Zadajte heslo + Potvrdiť + Znovu zadajte heslo + Heslá sa nezhodujú. + Ďalšie informácie + Janko + Hraško + Pohlavie + Vyberte pohlavie + Dátum narodenia + Vyberte dátum + + Overenie + Overte svoj email + Ak vám nebol doručený overovací email a chcete ho znovu odoslať, klepnite na odkaz nižšie. + Znovu odoslať overovací email + + Prihlásenie + Zabudli ste heslo? + + Zadajte kód + Potvrďte kód + Kód uložený + Kód autentifikovaný + Zadajte starý kód + Zadajte nový kód + Potvrďte nový kód + Nesprávny kód + Kódy sa nezhodujú. Skúste to znovu. + Autentifikujte sa pomocou Touch ID + Chyba Touch ID + Povolené sú len číselné znaky. + Indikátor priebehu zadávania kódu + Zadané číslice: %1$s z %2$s + Zabudli ste kód? + + Nepodarilo sa pridať položku kľúčenky. + Nepodarilo sa aktualizovať položku kľúčenky. + Nepodarilo sa vymazať položku kľúčenky. + Položka kľúčenky sa nenašla. + + A+ + A- + AB+ + AB- + B+ + B- + 0+ + 0- + + Žena + Muž + Iné + + Nie + Áno + + cm + ft + in + + Klepnutím odpovedajte + Vyberte odpoveď + Klepnutím vyberte + Klepnutím píšte + + Súhlasím + Zrušiť + Potvrdiť + Vyčistiť + Nesúhlasím + Hotovo + Začať + Viac informácií + Ďalej + Preskočiť + Preskočiť túto otázku + Spustiť časovač + Uložiť na neskôr + Zahodiť výsledky + Ukončiť úlohu + Uložiť + Vyčistiť odpoveď + Túto odpoveď nie je možné upraviť. + + Aktivita sa spustí o + Aktivita dokončená + Vaše dáta budú analyzované a na výsledky budete upozornení po ich vyhodnotení. + Zostáva %1$s s. + + Zachytiť obrázok + Znovu zachytiť obrázok + Nenašla sa žiadna kamera. Tento krok nebude možné dokončiť. + Ak chcete dokončiť tento krok, v Nastaveniach povoľte tejto aplikácii prístup ku kamere. + Pre zachytené obrázky nebol uvedený žiadny cieľový priečinok. + Zachytený obrázok sa nepodarilo uložiť. + + Spustiť nahrávanie + Zastaviť nahrávanie + Znovu zachytiť video + + Kondícia + Vzdialenosť (%1$s) + Srdcová frekvencia (bpm) + Pohodlne sa posaďte na %1$s. + Choďte čo najrýchlejšie po dobu %1$s. + Táto aktivita monitoruje vašu srdcovú frekvenciu a zmeria vzdialenosť, ktorú prejdete za %1$s. + Po dobu %1$s sa prechádzajte vonku čo najrýchlejšou chôdzou. Po skončení sa pohodlne posaďte na %2$s. Začnite klepnutím na Začať. + + Držanie tela a rovnováha + Táto aktivita zmeria vaše držanie tela a rovnováhu počas chôdze a státia. Ak nemôžete bezpečne chodiť bez cudzej pomoci, nepokračujte. + Nájdite si miesto, kde môžete bez cudzej pomoci bezpečne prejsť rovno %1$d krokov. + Vložte si telefón do vrecka alebo tašky a nasledujte zvukové pokyny. + Teraz nehybne stojte po dobu %1$s. + Nehybne stojte po dobu %1$s. + Otočte sa a choďte späť na miesto, kde ste začali. + Prejdite rovno %1$d krokov + + Nájdite si miesto, kde môžete bezpečne chodiť po priamke tam a naspäť. Skúste kráčať plynule a na konci sa otočiť, ako keby ste obchádzali kužeľ.\n\nV ďalšom kroku budete požiadaní o otočenie na mieste o 360 stupňov a následné státie v pokoji s pripaženými rukami a chodidlami na šírku ramien. + Keď ste pripravení, klepnite na Začať.\nPotom si vložte telefón do vrecka alebo tašky a nasledujte zvukové pokyny. + Kráčajte tam a naspäť po priamke po dobu %1$s. Chôdza by mala byť prirodzená. + Otočte sa o 360 stupňov a ostaňte stáť po dobu %1$s. + Dokončili ste aktivitu. + + Rýchlosť klepania + Pravá ruka + Ľavá ruka + Táto aktivita zmeria rýchlosť klepania. + Položte telefón na vodorovnú plochu. + Pomocou dvoch prstov rovnakej ruky striedavo klepte na tlačidlá na obrazovke. + Pomocou dvoch prstov pravej ruky striedavo klepte na tlačidlá na obrazovke. + Pomocou dvoch prstov ľavej ruky striedavo klepte na tlačidlá na obrazovke. + Zopakujte tento test pomocou pravej ruky. + Zopakujte tento test pomocou ľavej ruky. + Klepnite najprv jedným prstom a potom druhým prstom. Skúste klepnutia načasovať tak, aby boli čo najviac pravidelné. Klepte po dobu %1$s. + Začnite klepnutím na Začať. + Pre spustenie klepnite na Ďalej. + Klepnite + Celkom klepnutí + Pravidelne klepte na tlačidlá dvomi prstami. + Klepte na tlačidlá pomocou PRAVEJ ruky. + Klepte na tlačidlá pomocou ĽAVEJ ruky. + Preskočiť túto ruku + + Hlas + Začnite klepnutím na Začať. + Po čo najdlhšiu dobu vravte do mikrofónu „Aaaaaaaaaa“. + Zhlboka sa nadýchnite a po čo najdlhšiu dobu vravte do mikrofónu „Aaaaaaaaaa“. Snažte sa zachovať rovnakú hlasitosť - paličky audia by mali byť modré. + Táto aktivita vyhodnotí váš hlas, ktorý nahráte pomocou mikrofónu na spodku telefónu. + Príliš nahlas + Nie je možné nahrať zvuk + Počkajte na dokončenie merania úrovne hluku pozadia. + Úroveň hluku okolitého prostredia je príliš vysoká na nahrávanie hlasu. Presuňte sa na tichšie miesto a skúste to znovu. + Keď ste pripravení, klepnite na Ďalej. + + Tónová audiometria + Táto aktivita meria vašu schopnosť počuť rôzne zvuky. + Skôr ako začnete, pripojte a založte si slúchadlá. + Začnite klepnutím na Začať. + Teraz by ste mali počuť tón. Nastavte hlasitosť pomocou ovládacích prvkov na boku zariadenia.\n\nKeď budete pripravení začať, klepnite na tlačidlo. + Klepnite na tlačidlo vždy, keď zaznie zvuk. + %1$s Hz, naľavo + %1$s Hz, napravo + + Priestorová pamäť + Táto aktivita zmeria vašu krátkodobú pamäť zobrazením sekvencie obrázkov (%1$s), ktorú budete musieť zopakovať. + kvety + kvety + Obrázky (%1$s) sa budú postupne rozsvecovať. Klepnite na obrázky (%2$s) v rovnakom poradí, v akom sa rozsvietili. + Obrázky (%1$s) sa budú postupne rozsvecovať. Klepnite na obrázky (%2$s) v opačnom poradí, v akom sa rozsvietili. + Klepnutím na Začať spustite test a pozorne sa dívajte. + %1$s + Skóre + Pozorujte rozsvecovanie obrázkov (%1$s) + Klepnite na obrázky (%1$s) v poradí, v akom sa rozsvietili + Klepnite na obrázky (%1$s) v opačnom poradí + Sekvencia dokončená + Pre pokračovanie klepnite na Ďalej. + Skúste to znovu + Tentokrát sa vám to nepodarilo. Pokračujte klepnutím na Ďalej. + Čas vypršal + Vypršal vám čas.\nPokračujte klepnutím na Ďalej. + Hra dokončená + Pozastavené + Pre pokračovanie klepnite na Ďalej. + + Čas reakcie + Táto aktivita hodnotí, aký dlhý čas vám trvá reakcia na vizuálny podnet. + Hneď ako sa na obrazovke objaví modrý bod, potraste zariadením v ľubovoľnom smere. Budete vyzvaní, aby ste to urobili %D-krát. + Začnite klepnutím na Začať. + Pokus %1$s z %2$s + Keď sa zobrazí modrý kruh, rýchlo potraste zariadením. + + Hanojská veža + Táto aktivita vyhodnotí vašu schopnosť riešiť rébusy. + Presuňte celú pyramídu na zvýraznenú podložku čo najmenším počtom ťahov. + Začnite klepnutím na Začať + Vyriešte rébus + Počet ťahov: %1$s \n %2$s + Tento rébus nedokážem vyriešiť + + Chôdza na čas + Táto aktivita meria funkčnosť vašich dolných končatín. + Nájdite si miesto, najlepšie vonku, kde môžete čo najrýchlejšie bezpečne prejsť priamo približne %1$s. Nespomaľujte, kým neprejdete cieľom. + Pre spustenie klepnite na Ďalej. + Asistenčné zariadenie + Pre každý test použite rovnaké asistenčné zariadenie. + Nosíte ortézu na členku? + Používate asistenčné zariadenie? + Klepnutím sem vyberte odpoveď. + Nie + Jedna palica + Jedna barla + Dve palice + Dve barly + Chodítko + Prejdite do %1$s v priamom smere. + Otočte sa a choďte späť na miesto, kde ste začali. + Po skončení klepnite na Hotovo. + + PASAT + PVSAT + PAVSAT + Paced Auditory Serial Addition Test meria rýchlosť, akou dokážete spracovať a spočítať informácie prijaté prostredníctvom sluchu. + Paced Visual Serial Addition Test meria rýchlosť, akou dokážete spracovať a spočítať informácie prijaté prostredníctvom zraku. + Paced Auditory a Visual Serial Addition Test meria rýchlosť, akou dokážete spracovať a spočítať informácie prijaté prostredníctvom sluchu a zraku. + Každé %1$s s sa zobrazí nové číslo.\nKaždé nové číslo musíte pripočítať k číslu zobrazenému pred ním.\nPozor, nesmiete počítať celkový súčet týchto čísiel, ale len sumu dvoch posledných čísiel. + Začnite klepnutím na Začať. + Zapamätajte si toto prvé číslo. + Pripočítajte nové číslo k predošlému číslu. + - + + %1$s-kolíkový test + Táto aktivita slúži na otestovanie motoriky horných končatín. Počas testu budete umiestňovať kolíky do otvorov. Test budete opakovať %1$s-krát. + Otestovaná bude vaša ľavá aj pravá ruka.\nČo najrýchlejšie vezmite kolík, umiestnite do otvoru %1$s-krát a potom ho vytiahnite taktiež %2$s-krát. + Otestovaná bude vaša pravá aj ľavá ruka.\nČo najrýchlejšie vezmite kolík, umiestnite do otvoru %1$s-krát a potom ho vytiahnite taktiež %2$s-krát. + Začnite klepnutím na Začať. + Umiestnite kolík do otvoru pomocou ľavej ruky. + Umiestnite kolík do otvoru pomocou pravej ruky. + Položte kolík za čiaru pomocou ľavej ruky. + Položte kolík za čiaru pomocou pravej ruky. + Zdvihnite kolík dvomi prstami. + Nadvihnutím prstov pustite kolík. + + Trasenie rúk + Táto aktivita zmeria trasenie rúk v rôznych polohách. Po celý čas by ste mali pohodlne sedieť. + Držte telefón v ruke, ktorá je viac postihnutá, ako na obrázku nižšie. + Držte telefón v PRAVEJ ruke, ako na obrázku nižšie. + Držte telefón v ĽAVEJ ruke, ako na obrázku nižšie. + Budete požiadaní o vykonanie %1$s. Po celý čas budete sedieť s telefónom v ruke. + jednej úlohy + dvoch úloh + troch úloh + štyroch úloh + piatich úloh + Ak chcete pokračovať, klepnite na Ďalej. + Pripravte sa na držanie telefónu v ruke. + Pripravte sa na držanie telefónu v ĽAVEJ ruke. + Pripravte sa na držanie telefónu v PRAVEJ ruke. + Držte telefón v ruke po dobu %1$d s. + Držte telefón vo vystretej ruke vo výške ramien. + Držte telefón vo vystretej ĽAVEJ ruke vo výške ramien. + Držte telefón vo vystretej PRAVEJ ruke vo výške ramien. + Držte telefón vo vystretej ruke po dobu %1$d s. + Držte telefón v ruke s pokrčeným lakťom vo výške ramien. + Držte telefón v ĽAVEJ ruke s pokrčeným lakťom vo výške ramien. + Držte telefón v PRAVEJ ruke s pokrčeným lakťom vo výške ramien. + Držte telefón v ruke s pokrčeným lakťom po dobu %1$d s. + S pokrčeným lakťom sa opakovane dotýkajte nosa telefónom. + S pokrčeným lakťom a telefónom v ĽAVEJ ruke sa opakovane dotýkajte nosa telefónom. + S pokrčeným lakťom a telefónom v PRAVEJ ruke sa opakovane dotýkajte nosa telefónom. + Dotýkajte sa nosa telefónom po dobu %1$d s. + Pripravte sa na mávanie, ako keď máva kráľovná (mávajte otáčaním zápästia). + Pripravte sa na mávanie, ako keď máva kráľovná, s telefónom v ĽAVEJ ruke (mávajte otáčaním zápästia). + Pripravte sa na mávanie, ako keď máva kráľovná, s telefónom v PRAVEJ ruke (mávajte otáčaním zápästia). + Mávajte ako kráľovná po dobu %1$d s. + Uchopte telefón do ĽAVEJ ruky a pokračujte na ďalšiu úlohu. + Uchopte telefón do PRAVEJ ruky a pokračujte na ďalšiu úlohu. + Pokračujte na ďalšiu úlohu. + Aktivita dokončená. + Budete požiadaní o vykonanie %1$s. Po celý čas budete sedieť s telefónom najprv v jednej a potom v druhej ruke. + Túto aktivitu nedokážem vykonať pomocou ĽAVEJ ruky. + Túto aktivitu nedokážem vykonať pomocou PRAVEJ ruky. + Túto aktivitu dokážem vykonať pomocou oboch rúk. + + Nepodarilo sa vytvoriť súbor + Nepodarilo odstrániť dostatok súborov záznamov pre dosiahnutie prahovej hodnoty + Chyba počas nastavovania vlastnosti + Súbor neoznačený ako vymazaný (neoznačený ako odoslaný) + Viacero chýb pri odstraňovaní záznamov + Nenašli sa žiadne zhromaždené dáta. + Nie je špecifikovaný výstupný adresár + + Žiadne dáta + + Späť + Obrázok predmetu %1$s + Pole pre podpis + Pre podpísanie sa dotknite obrazovky a ťahajte prst + Podpísané + Nepodpísané + Označené + Neoznačené + Posuvník pre odpoveď. Rozsah je od %1$s do %2$s + Obrázok bez popisu + Spustiť úlohu + aktívne + správne + nesprávne + nehybné + Dlaždica pamäťovej hry + Náhľad + Zachytený obrázok + Náhľad zachytávaného videa + Zachytené video + Kotúč veľkosti %1$s nie je možné umiestniť na kotúč veľkosti %2$s + Cieľ + Veža + Klepnutím dvakrát umiestnite kotúč + Klepnutím dvakrát vyberte najvrchnejší kotúč + Obsahuje kotúče o veľkostiach %1$s + Prázdne + Rozsah od %1$s do %2$s + Pyramída pozostáva z + + Bod: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-sv/strings.xml b/backbone/src/main/res/values-sv/strings.xml new file mode 100644 index 000000000..4e2dc57db --- /dev/null +++ b/backbone/src/main/res/values-sv/strings.xml @@ -0,0 +1,396 @@ + + + + + Godkännande + Förnamn + Efternamn + Krävs + Granska + Granska formuläret nedan och tryck på Godkänn om du är redo att fortsätta. + Granska + Signatur + Signera med hjälp av fingret på raden nedan. + Signera här + Sidan %1$ld av %2$ld + + Välkommen + Datainsamling + Integritet + Dataanvändning + Studieenkät + Studieåtgärder + Tidsåtgång + Tillbakadragning + Läs mer + + Läs mer om hur data samlas in + Läs mer om hur data används + Läs mer om hur din integritet och identitet skyddas + Läs mer om studien först + Läs mer om studieenkäten + Läs mer om studiens påverkan på din tid + Läs mer om de berörda åtgärderna + Läs mer om tillbakadragning + + Delningsalternativ + Dela mina data med %1$s och kvalificerade forskare över hela världen + Dela mina data endast med %1$s + %1$s kommer att få dina studiedata från deltagandet i den här studien.\n\nGenom att dela dina kodade studiedata mer brett (utan information som ditt namn) kan vi gagna både denna och framtida forskning. + Läs mer om datadelning + + %1$ss namn (tryckt) + %1$ss signatur + Datum + + Steg %1$s av %2$s + + Ogiltigt värde + %1$s överskrider det högsta tillåtna värdet (%2$s). + %1$s är mindre än det minsta tillåtna värdet (%2$s). + %1$s är inte ett giltigt värde. + + Ogiltig e-postadress: %1$s + + Ange en adress + Kunde inte hitta den angivna adressen + Kunde inte bestämma din aktuella plats. Ange en adress eller flytta dig till en plats med bättre GPS-signal om det är tillämpligt. + Åtkomst till platstjänster har nekats. Tillåt att den här appen använder platstjänster via Inställningar. + Det gick inte att hitta något resultat för den angivna adressen. Kontrollera att adressen är giltig. + Antingen är du inte ansluten till internet eller så har du överskridit det maximala antalet addressökbegäranden. Om du inte är ansluten till internet aktiverar du Wi-Fi så att du kan svara på frågan, hoppa över frågan om en sådan knapp är tillgänglig eller gå tillbaka till enkäten när du är ansluten till internet. I annat fall försöker du igen om några minuter. + + Textinnehållet överskrider maximal längd: %1$s + + Kamera inte tillgänglig i delad skärm. + + E-post + jappleseed@example.com + Lösenord + Ange lösenord + Bekräfta + Ange lösenordet igen + Lösenorden stämmer inte överens. + Ytterligare information + John + Appleseed + Kön + Välj ett kön + Födelsedatum + Välj ett datum + + Verifiering + Bekräfta din e-postadress + Tryck på länken nedan om du inte fick något bekräftelsebrev och vill att det skickas på nytt. + Skicka bekräftelsebrev igen + + Inloggning + Har du glömt lösenordet? + + Ange lösenkod + Bekräfta lösenkod + Lösenkoden sparad + Autentiserad med lösenkod + Ange din gamla lösenkod + Ange din nya lösenkod + Bekräfta den nya lösenkoden + Felaktig lösenkod + Lösenkoderna stämde inte överens. Försök igen. + Autentisera med Touch ID + Touch ID-fel + Endast numeriska tecken tillåts. + Förloppsindikator för angivande av lösenkod + %1$s av %2$s siffror angivna + Glömt lösenkoden? + + Kunde inte lägga till nyckelringsobjekt. + Kunde inte uppdatera nyckelringsobjekt. + Kunde inte radera nyckelringsobjekt. + Kunde inte hitta nyckelringsobjekt. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Kvinna + Man + Annat + + Nej + Ja + + cm + fot + tum + + Tryck för att svara + Välj ett svar + Tryck för att markera + Tryck för att skriva + + Godkänn + Avbryt + OK + Rensa + Avböj + Klar + Kom igång + Läs mer + Nästa + Hoppa över + Hoppa över denna fråga + Starta timern + Spara till senare + Bortse från träffar + Avsluta åtgärd + Spara + Rensa svar + Det här svaret kan inte ändras. + + Startar aktivitet om + Aktiviteten är klar + Dina data kommer att analyseras, och du blir meddelad när resultaten är klara. + %1$s sekunder återstår. + + Spara bild + Spara bild igen + Ingen kamera hittades. Steget kan inte slutföras. + För att slutföra det här steget måste du ge denna app tillgång till kameran i Inställningar. + Ingen utmatningskatalog har angetts för de sparade bilderna. + Bilden kunde inte sparas. + + Starta inspelning + Stoppa inspelning + Spela in video igen + + Motion + Distans (%1$s) + Puls – slag/minut + Sitt bekvämt under %1$s. + Gå så fort du kan under %1$s. + Den här aktiviteten registrerar din puls och mäter hur långt du kan gå på %1$s. + Gå utomhus i snabbast möjliga takt under %1$s. När du är klar sitter du och vilar bekvämt under %2$s. Tryck på Kom igång för att börja. + + Gång och balans + Den här aktiviteten mäter din gångstil och balans medan du går och står still. Fortsätt endast om du säkert kan gå utan hjälp. + Hitta en plats där du säkert kan gå ungefär %1$d steg i en rak linje utan hjälp. + Placera telefonen i en ficka eller väska och följ de talade anvisningarna. + Stå nu stilla under %1$s. + Stå stilla under %1$s. + Vänd om och gå tillbaka till utgångsplatsen. + Gå upp till %1$d steg i en rak linje. + + Hitta en plats där du tryggt kan gå fram och tillbaka i en rät linje. Försök att gå kontinuerligt genom att vända dig om vid slutet av linjen, som om du gick runt en kon.\nDärefter blir du ombedd att vända dig ett helt varv och sedan stå stilla med armarna längs sidorna och fötterna i ungefär axelbredd. + Tryck på Kom igång när du är redo att börja.\nPlacera sedan telefonen i en ficka eller väska och följ de talade anvisningarna. + Gå fram och tillbaka i en rät linje under %1$s. Gå som du vanligtvis gör. + Vänd dig ett helt varv och stå sedan stilla under %1$s. + Du har slutfört aktiviteten. + + Tryckhastighet + Höger hand + Vänster hand + Den här aktiviteten mäter din tryckhastighet. + Lägg telefonen på en slät yta. + Använd två fingrar på samma hand till att omväxlande trycka på knapparna på skärmen. + Använd två fingrar på högra handen till att omväxlande trycka på knapparna på skärmen. + Använd två fingrar på vänstra handen till att omväxlande trycka på knapparna på skärmen. + Nu upprepar du samma test med högra handen. + Nu upprepar du samma test med vänstra handen. + Tryck med ett finger och sedan det andra. Försök att trycka i en så jämn takt som möjligt. Fortsätt trycka under %1$s. + Tryck på Kom igång för att börja. + Tryck på Nästa för att börja. + Tryck + Totalt antal tryckningar + Tryck på knapparna så konsekvent du kan med två fingrar. + Tryck på knapparna med HÖGRA handen. + Tryck på knapparna med VÄNSTRA handen. + Hoppa över den här handen + + Röst + Tryck på Kom igång för att börja. + Säg ”aaah” i mikrofonen så länge du kan. + Ta ett djupt andetag och säg ”aaah” i mikrofonen så länge du kan. Håll röststyrkan stadig så att ljudstaplarna förblir blå. + Den här aktiviteten utvärderar din röst genom att spela in den med mikrofonen i nederkanten av telefonen. + För högt + Kan inte spela in ljud + Vänta medan vi kontrollerar ljudnivån på bakgrundsbullret. + Bakgrundsbullret är för högt för att spela in din röst. Gå någonstans tystare och försök igen. + Tryck på Nästa när du är redo. + + Tonaudiometri + Den här aktiviteten mäter din förmåga att höra olika ljud. + Anslut och ta på dig hörlurarna innan du börjar. + Tryck på Kom igång för att börja. + Nu bör du höra en ton. Justera volymen med reglagen på sidan av enheten.\n\nTryck på knappen när du är redo att börja. + Tryck på knappen varje gång du börjar höra ett ljud. + %1$s Hz, vänster + %1$s Hz, höger + + Spatialt minne + Den här aktiviteten mäter ditt spatiala korttidsminne genom att be dig upprepa den ordning i vilken %1$s lyser upp. + blommor + blommor + En del av %1$sna kommer att lysa upp en i taget. Tryck på de %2$sna i samma ordning som de lyser upp. + En del av %1$sna kommer att lysa upp en i taget. Tryck på de %2$sna i omvänd ordning jämfört med hur de lyser upp. + Tryck på Kom igång för att börja och titta noggrant. + %1$s + Poäng + Se när %1$s lyser upp + Tryck på %1$s i den ordning de lyser upp + Tryck på %1$s i omvänd ordning + Sekvensen är klar + Tryck på Nästa när du vill fortsätta + Försök igen + Du klarade dig inte riktigt igenom den gången. Tryck på Nästa för att fortsätta. + Tiden är slut + Tiden tog slut.\nTryck på Nästa för att fortsätta. + Spelet är klart + Pausat + Tryck på Nästa när du vill fortsätta + + Reaktionstid + Den här aktiviteten utvärderar den tid det tar för dig att reagera på visuella stimuli. + Skaka enheten i valfri riktning så fort den blå punkten visas på skärmen. Du blir ombedd att göra detta %D gånger. + Tryck på Kom igång för att börja. + Försök %1$s av %2$s + Skaka snabbt enheten när den blå cirkeln visas + + Tornen i Hanoi + Den här aktiviteten utvärderar din förmåga att lösa pussel. + Flytta hela traven till den markerade plattformen med så få drag som möjligt. + Tryck på Kom igång för att börja + Lös pusslet + Antal drag: %1$s \n %2$s + Jag kan inte lösa pusslet + + Gång med tidtagning + Den här aktiviteten mäter benfunktionen. + Hitta en plats, helst utomhus, där du kan gå ungefär %1$s i en rät linje så fort som möjligt (men fortfarande säkert). Sakta inte ned förrän du har passerat mållinjen. + Tryck på Nästa för att börja. + Hjälpmedel + Använd samma hjälpmedel för alla test. + Bär du en ankel- och fotortos? + Använder du ett hjälpmedel? + Tryck här för att välja ett svar. + Inget + Ensidig käpp + Ensidig krycka + Dubbelsidig käpp + Dubbelsidig krycka + Gåstativ/rollator + Gå upp till %1$s i en rät linje. + Vänd om och gå tillbaka till utgångsplatsen. + Tryck på Klar när du är färdig. + + PASAT + PVSAT + PAVSAT + PASAT (Paced Auditory Serial Addition Test) mäter din auditiva bearbetningshastighet och beräkningsförmåga. + PVSAT (Paced Visual Serial Addition Test) mäter din visuella bearbetningshastighet och beräkningsförmåga. + PAVSAT (Paced Auditory and Visual Serial Addition Test) mäter din auditiva och visuella bearbetningshastighet och beräkningsförmåga. + Ensiffriga tal visas med intervall på %1$s sekunder.\nDu måste addera varje nytt tal till talet som visades omedelbart före det.\nObs! Du ska inte beräkna en löpande totalsumma, utan bara summan av de senaste två talen. + Tryck på Kom igång för att börja. + Kom ihåg den första siffran. + Addera det nya talet till det föregående. + + + %1$s-håls pegtest + Den här aktiviteten mäter funktionen för dina övre extremiteter genom att be dig placera en pinne i ett hål. Du uppmanas att göra detta %1$s gånger. + Både din vänstra och högra hand kommer att testas.\nDu måste plocka upp pinnen så snabbt som möjligt och stoppa in den i hålet. När du har gjort det %1$s gånger tar du bort den igen %2$s gånger. + Både din högra och vänstra hand kommer att testas.\nDu måste plocka upp pinnen så snabbt som möjligt och stoppa in den i hålet. När du har gjort det %1$s gånger tar du bort den igen %2$s gånger. + Tryck på Kom igång för att börja. + Placera pinnen i hålet med hjälp av vänster hand. + Placera pinnen i hålet med hjälp av höger hand. + Placera pinnen bakom strecket med hjälp av vänster hand. + Placera pinnen bakom strecket med hjälp av höger hand. + Lyft upp pinnen med två fingrar. + Släpp pinnen genom att lyfta på fingrarna. + + Tremoraktivitet + Den här aktiviteten mäter darrningarna i dina händer i olika positioner. Hitta en plats där du kan sitta bekvämt under hela den här aktiviteten. + Håll telefonen i den mer påverkade handen enligt bilden nedan. + Håll telefonen i den HÖGRA handen enligt bilden nedan. + Håll telefonen i den VÄNSTRA handen enligt bilden nedan. + Du blir ombedd att utföra %1$s medan du sitter med telefonen i handen. + en åtgärd + två åtgärder + tre åtgärder + fyra åtgärder + fem åtgärder + Tryck på nästa för att fortsätta. + Förbered dig på att hålla telefonen i knät. + Förbered dig på att hålla telefonen i knät med den VÄNSTRA handen. + Förbered dig på att hålla telefonen i knät med den HÖGRA handen. + Fortsätt hålla telefonen i knät under %1$d sekunder. + Håll nu telefonen med handen utsträckt i axelhöjd. + Håll nu telefonen med den VÄNSTRA handen utsträckt i axelhöjd. + Håll nu telefonen med den HÖGRA handen utsträckt i axelhöjd. + Fortsätt hålla telefonen med handen utsträckt under %1$d sekunder. + Håll nu telefonen i axelhöjd med armbågen böjd. + Håll nu telefonen med den VÄNSTRA handen i axelhöjd med armbågen böjd. + Håll nu telefonen med den HÖGRA handen i axelhöjd med armbågen böjd. + Fortsätt hålla telefonen med armbågen böjd under %1$d sekunder + Nu ska du hålla armbågen böjd och röra vid näsan med telefonen flera gånger. + Nu ska du hålla armbågen böjd med telefonen i den VÄNSTRA handen och röra vid näsan med telefonen flera gånger. + Nu ska du hålla armbågen böjd med telefonen i den HÖGRA handen och röra vid näsan med telefonen flera gånger. + Fortsätt röra vid näsan med telefonen under %1$d sekunder + Förbered dig på att vinka kungligt (vinka genom att vrida på handleden). + Förbered dig på att vinka kungligt med telefonen i den VÄNSTRA handen (vinka genom att vrida på handleden). + Förbered dig på att vinka kungligt med telefonen i den HÖGRA handen (vinka genom att vrida på handleden). + Fortsätt utföra en kunglig vinkning under %1$d sekunder. + Flytta nu telefonen till den VÄNSTRA handen och fortsätt med nästa åtgärd. + Flytta nu telefonen till den HÖGRA handen och fortsätt med nästa åtgärd. + Fortsätt till nästa åtgärd. + Aktiviteten är klar. + Du blir ombedd att utföra %1$s medan du sitter med telefonen i ena handen, och sedan en gång till med den andra handen. + Jag kan inte utföra den här aktiviteten med VÄNSTRA handen. + Jag kan inte utföra den här aktiviteten med HÖGRA handen. + Jag kan utföra den här aktiviteten med båda händerna. + + Kunde inte skapa fil + Kunde inte ta bort tillräckligt många loggfiler för att nå tröskelvärdet + Fel vid angivning av attribut + Filen är inte markerad som raderad (ej markerad som öveförd) + Flera fel vid borttagning av loggar + Inga insamlade data hittades. + Ingen utmatningskatalog har angetts + + Inga data + + Tillbaka + Illustration av %1$s + Tilldelat signaturfält + Rör vid skärmen och signera genom att röra fingret + Signerat + Osignerat + Markerat + Avmarkerat + Svarsreglage. Intervall från %1$s till %2$s + Omärkt bild + Påbörja åtgärd + aktiv + rätt + fel + inaktiv + Minnesspelbricka + Spara förhandsvisning + Sparad bild + Förhandsvisning av videoinspelning + Inspelad video + Ka inte placera skivan med storleken %1$s på skivan med storleken %2$s + Mål + Torn + Tryck snabbt två gånger för att placera skivan + Tryck snabbt två gånger för att markera den översta skivan + Har skivor med storlekarna %1$s + Tomt + Intervall från %1$s till %2$s + Trave som består av + och + Punkt: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-th/strings.xml b/backbone/src/main/res/values-th/strings.xml new file mode 100644 index 000000000..6237b539e --- /dev/null +++ b/backbone/src/main/res/values-th/strings.xml @@ -0,0 +1,396 @@ + + + + + ความยินยอม + ชื่อ + นามสกุล + บังคับ + ตรวจทาน + ตรวจทานแบบฟอร์มด้านล่างแล้วแตะยอมรับหากคุณพร้อมดำเนินการต่อแล้ว + ตรวจทาน + ลายเซ็น + โปรดเซ็นชื่อที่บรรทัดด้านล่างโดยใช้นิ้วของคุณ + เซ็นชื่อที่นี่ + หน้า %1$ld จาก %2$ld + + ยินดีต้อนรับ + การรวบรวมข้อมูล + ความเป็นส่วนตัว + การใช้ข้อมูล + การสำรวจงานศึกษา + งานศึกษา + เวลาดำเนินการ + การถอนตัว + เรียนรู้เพิ่มเติม + + เรียนรู้เพิ่มเติมเกี่ยวกับวิธีรวบรวมข้อมูล + เรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้ข้อมูล + เรียนรู้เพิ่มเติมเกี่ยวกับวิธีการปกป้องความเป็นส่วนตัวและข้อมูลประจำตัวของคุณ + เรียนรู้เพิ่มเติมเกี่ยวกับงานศึกษาก่อน + เรียนรู้เพิ่มเติมเกี่ยวกับการสำรวจงานศึกษา + เรียนรู้เพิ่มเติมเกี่ยวกับผลกระทบของงานศึกษาที่มีต่อเวลาของคุณ + เรียนรู้เพิ่มเติมเกี่ยวกับงานที่เกี่ยวข้อง + เรียนรู้เพิ่มเติมเกี่ยวกับการถอนตัว + + ตัวเลือกการแชร์ + แบ่งปันข้อมูลกับ %1$s และนักวิจัยที่ได้รับการรับรองทั่วโลก + แชร์ข้อมูลของฉันกับ %1$s เท่านั้น + %1$s จะได้รับข้อมูลการศึกษาของคุณจากการเข้าร่วมของคุณในการศึกษานี้\n\nการแชร์ข้อมูลการศึกษาของคุณกับผู้อื่น (โดยไม่แชร์ข้อมูลอย่างชื่อของคุณ) อาจช่วยพัฒนาการวิจัยชิ้นนี้และชิ้นอื่นๆ ได้ + เรียนรู้เพิ่มเติมเกี่ยวกับการแชร์ข้อมูล + + ชื่อของ %1$s (ตัวพิมพ์) + ลายเซ็นของ %1$s + วันที่ + + ขั้นที่ %1$s จาก %2$s + + ค่าไม่ถูกต้อง + %1$s เกินจำนวนค่าสูงสุดที่อนุญาต (%2$s) + %1$s น้อยกว่าค่าต่ำสุดที่อนุญาต (%2$s) + %1$s ไม่ใช่ค่าที่ถูกต้อง + + ที่อยู่อีเมลที่ไม่ถูกต้อง: %1$s + + ป้อนที่อยู่ + ไม่สามารถหาที่อยู่ที่ระบุได้ + ไม่สามารถหาตำแหน่งที่ตั้งปัจจุบันของคุณ กรุณาป้อนที่อยู่หรือไปอยู่ในที่ที่มีสัญญาณ GPS ที่ดีกว่านี้หากเป็นไปได้ + การเข้าถึงบริการหาตำแหน่งที่ตั้งถูกปฏิเสธ กรุณาอนุญาตให้แอพนี้ใช้บริการหาตำแหน่งที่ตั้งได้ผ่านทางการตั้งค่า + ไม่มีผลการค้นหาสำหรับที่อยู่ที่ป้อน กรุณาตรวจสอบให้แน่ใจว่าที่อยู่ถูกต้อง + คุณไม่ได้เชื่อมต่อกับอินเทอร์เน็ตหรือจำนวนที่อยู่เต็มแล้ว ถ้าคุณไม่ได้เชื่อมต่อกับอินเทอร์เน็ต กรุณาเปิดใช้งาน Wi-Fi ของคุณเพื่อตอบคำถามนี้ หากมีปุ่มข้ามคุณจะสามารถข้ามคำถามนี้ได้ หรือกลับมาที่แบบสำรวจนี้เมื่อคุณเชื่อมต่อกับอินเทอร์เน็ต หรือกรุณารอสักครู่หนึ่งแล้วลองอีกครั้ง + + เนื้อหาข้อความที่เกินจำนวนสูงสุด: %1$s + + ไม่สามารถใช้งานกล้องในหน้าจอแบบแยกได้ + + อีเมล + jappleseed@example.com + รหัสผ่าน + ป้อนรหัสผ่าน + ยืนยัน + ป้อนรหัสผ่านอีกครั้ง + รหัสผ่านไม่ตรงกัน + ข้อมูลเพิ่มเติม + John + Appleseed + เพศ + เลือกเพศ + วันเกิด + เลือกวันที่ + + การยืนยัน + ยืนยันอีเมลของคุณ + แตะที่ลิงก์ข้างล่างหากคุณไม่ได้รับอีเมลสำหรับการยืนยันและต้องการที่จะให้ส่งอีเมลอีกครั้ง + ส่งอีเมลการยืนยันอีกครั้ง + + เข้าสู่ระบบ + ลืมรหัสผ่านหรือไม่ + + ป้อนรหัสผ่านตัวเลข + ยืนยันรหัสผ่านตัวเลข + ได้บันทึกรหัสผ่านตัวเลขแล้ว + รหัสผ่านตัวเลขผ่านการตรวจสอบแล้ว + ป้อนรหัสผ่านตัวเลขเก่าของคุณ + ป้อนรหัสผ่านตัวเลขใหม่ของคุณ + ยืนยันรหัสผ่านตัวเลขใหม่ของคุณ + รหัสผ่านไม่ถูกต้อง + รหัสผ่านตัวเลขไม่ตรงกัน ลองอีกครั้ง + กรุณาใช้ Touch ID ตรวจสอบรหัสผ่านตัวเลข + Touch ID ผิดพลาด + เฉพาะตัวเลขเท่านั้น + สัญลักษณ์แสดงว่ากำลังป้อนรหัสผ่านตัวเลขอยู่ + ตัวเลขที่ป้อนหลักที่ %1$s จาก %2$s หลัก + ลืมรหัสหรือไม่ + + ไม่สามารถเพิ่มรายการในพวงกุญแจ + ไม่สามารถอัพเดทรายการในพวงกุญแจ + ไม่สามารถลบรายการในพวงกุญแจ + ไม่สามารถค้นหารายการในพวงกุญแจ + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + หญิง + ชาย + อื่นๆ + + ไม่ + ใช่ + + ซม. + ฟุต + นิ้ว + + แตะเพื่อตอบ + เลือกคำตอบ + แตะเพื่อเลือก + แตะเพื่อเขียน + + ยอมรับ + ยกเลิก + ตกลง + ล้าง + ไม่ยอมรับ + เสร็จ + เริ่มต้นใช้งาน + เรียนรู้เพิ่มเติม + ถัดไป + ข้าม + ข้ามคำถามนี้ + เริ่มจับเวลา + บันทึกไว้ใช้ภายหลัง + ละทิ้งผลลัพธ์ + สิ้นสุดงาน + บันทึก + ล้างคำตอบ + ไม่สามารถแก้ไขคำตอบนี้ได้ + + กำลังจะเริ่มกิจกรรมใน + กิจกรรมเสร็จสมบูรณ์ + ข้อมูลของคุณจะได้รับการวินิจฉัยและคุณจะได้รับแจ้งเมื่อได้ผลลัพธ์ของคุณแล้ว + เหลืออีก %1$s วินาที + + จับภาพ + จับภาพอีกครั้ง + ไม่พบกล้อง\u0020\u0020ไม่สามารถทำให้ขั้นตอนนี้เสร็จสมบูรณ์ + อนุญาตให้แอพนี้เข้าถึงกล้องได้ในการตั้งค่าเพื่อทำขั้นตอนนี้ให้เสร็จสมบูรณ์ + ไม่ได้ระบุสารบบผลลัพธ์ไว้สำหรับรูปภาพที่จับภาพ + รูปภาพที่จับภาพไว้บันทึกไม่ได้ + + เริ่มอัด + หยุดอัด + ถ่ายวิดีโออีกครั้ง + + ฟิตเนส + ระยะทาง (%1$s) + อัตราการเต้นของหัวใจ (bpm) + นั่งสบายๆ นาน %1$s + เดินให้เร็วที่สุดเท่าที่คุณทำได้นาน %1$s + กิจกรรมนี้จะตรวจสอบอัตราการเต้นของหัวใจของคุณและวัดระยะทางที่คุณสามารถเดินได้ภายใน %1$s + เดินกลางแจ้งด้วยอัตราการเดินที่เร็วที่สุดที่คุณทำได้นาน %1$s เมื่อคุณเดินเสร็จแล้ว ให้นั่งพักสบายๆ นาน %2$s ในการเริ่มต้น ให้แตะเริ่มต้นใช้งาน + + ท่าเดินและความสมดุล + กิจกรรมนี้จะวัดท่าเดินและความสมดุลของคุณในขณะที่คุณเดินและยืนนิ่งๆ อย่าดำเนินการต่อหากคุณไม่สามารถเดินได้อย่างปลอดภัยโดยไม่มีตัวช่วย + ค้นหาที่ที่คุณสามารถเดินเป็นเส้นตรงได้ไกลประมาณ %1$d ก้าวได้อย่างปลอดภัยโดยไม่ต้องมีสิ่งช่วยเหลือ + ใส่โทรศัพท์ไว้ในกระเป๋ากางเกงหรือกระเป๋าแล้วทำตามคำแนะนำด้วยเสียง + ตอนนี้ให้ยืนนิ่งๆ นาน %1$s + ยืนนิ่งๆ นาน %1$s + หันหลัง แล้วเดินกลับไปจุดเริ่มต้น + เดินตรงขึ้นไป %1$d ก้าว + + หาสถานที่ที่คุณสามารถเดินไปมาเป็นเส้นตรงได้อย่างปลอดภัย ให้ลองเดินไปเรื่อยๆ จนสุดทางแล้วเดินกลับ เสมือนคุณเดินวนรอบกรวย\n\nจากนั้นระบบจะแนะนำให้คุณกลับหลังหัน แล้วยืนนิ่งๆ โดยแนบแขนไว้ข้างลำตัวและวางเท้าให้ห่างกันเท่ากับความกว้างของหัวไหล่ + แตะเริ่มต้น เมื่อคุณพร้อมที่จะเริ่มใช้งาน\nจากนั้นใส่โทรศัพท์ของคุณไว้ในกระเป๋ากางเกงหรือกระเป๋าถือ แล้วทำตามคำแนะนำด้วยเสียง + เดินไปมาเป็นเส้นตรงนาน %1$s โดยให้คุณเดินตามปกติ + กลับหลังหันแล้วยืนนิ่งๆ นาน %1$s + คุณทำกิจกรรมเสร็จสมบูรณ์แล้ว + + ความเร็วของการแตะ + มือขวา + มือซ้าย + กิจกรรมนี้จะวัดความเร็วในการแตะของคุณ + วางโทรศัพท์ของคุณไว้บนพื้นผิวที่ราบเรียบ + ใช้นิ้วมือข้างเดียวกันสองนิ้วแตะสลับกันที่ปุ่มบนหน้าจอ + ใช้นิ้วมือข้างขวาสองนิ้วแตะสลับกันที่ปุ่มบนหน้าจอ + ใช้นิ้วมือข้างซ้ายสองนิ้วแตะสลับกันที่ปุ่มบนหน้าจอ + ตอนนี้ให้ทดสอบแบบเดียวกันซ้ำอีกครั้งโดยใช้มือขวา + ตอนนี้ให้ทดสอบแบบเดียวกันซ้ำอีกครั้งโดยใช้มือซ้าย + แตะหนึ่งนิ้ว แล้วตามด้วยอีกนิ้ว ลองจับเวลาการแตะของคุณโดยให้มีจังหวะเท่ากันมากที่สุด แตะต่อไปเรื่อยๆ เป็นเวลา %1$s + แตะเริ่มต้นใช้งานเพื่อเริ่ม + แตะถัดไปเพื่อเริ่ม + แตะ + การแตะทั้งหมด + ใช้สองนิ้วแตะปุ่มโดยลงน้ำหนักให้สม่ำเสมอที่สุดเท่าที่จะทำได้ + ใช้มือขวาแตะปุ่ม + ใช้มือซ้ายแตะปุ่ม + ข้ามมือข้างนี้ + + เสียง + แตะเริ่มต้นใช้งานเพื่อเริ่ม + พูด “อาาาาา” ใส่ไมโครโฟนให้นานที่สุดเท่าที่คุณจะทำได้ + สูดลมหายใจเข้าลึกๆ แล้วพูด “อาาาาา” ใส่ไมโครโฟนให้นานที่สุดเท่าที่คุณจะทำได้ ให้ออกเสียงด้วยความดังที่คงที่เพื่อให้แถบเสียงคงสีฟ้าไว้ + กิจกรรมนี้จะประเมินเสียงของคุณโดยใช้ไมโครโฟนที่อยู่ด้านล่างสุดของโทรศัพท์ของคุณบันทึกเสียงคุณไว้ + ดังเกินไป + ไม่สามารถบันทึกเสียง + โปรดรอในระหว่างที่เราตรวจสอบระดับเสียงรบกวนรอบข้าง + ระดับเสียงแวดล้อมดังเกินกว่าที่จะอัดเสียงของคุณได้ โปรดย้ายไปอยู่บริเวณที่มีเสียงเบากว่านี้แล้วลองอีกครั้ง + แตะถัดไปเมื่อพร้อม + + การตรวจวัดการได้ยินเสียง + กิจกรรมนี้จะวัดความสามารถของคุณในการฟังเสียงต่างๆ + ก่อนที่คุณจะเริ่ม ให้เสียบสายแล้วสวมหูฟังของคุณ + แตะเริ่มต้นใช้งานเพื่อเริ่ม + ตอนนี้คุณจะได้ยินเสียงเตือน ให้ปรับความดังโดยใช้ตัวควบคุมที่อยู่ด้านข้างอุปกรณ์ของคุณ\n\nแตะปุ่มเมื่อคุณพร้อมที่จะเริ่ม + แตะที่ปุ่มทุกครั้งที่คุณเริ่มได้ยินเสียง + %1$s Hz ซ้าย + %1$s Hz ขวา + + ความจำเชิงพื้นที่ + กิจกรรมนี้จะวัดความจำเชิงพื้นที่ระยะสั้นของคุณโดยจะขอให้คุณทวนลำดับการสว่างขึ้นของ%1$s + ดอกไม้ + ดอกไม้ + %1$sบางตัวจะสว่างขึ้นครั้งละหนึ่งตัว ให้แตะ%2$sเหล่านั้นแบบตามลำดับการสว่าง + %1$sบางตัวจะสว่างขึ้นครั้งละหนึ่งตัว ให้แตะ%2$sเหล่านั้นแบบย้อนลำดับการสว่าง + ในการเริ่มต้น ให้แตะเริ่มต้นใช้งาน แล้วจับตาดูอย่างใกล้ชิด + %1$s + คะแนน + ดู%1$sสว่างขึ้น + แตะ%1$sตามลำดับการสว่างขึ้น + แตะ%1$sแบบย้อนกลับลำดับ + ลำดับเสร็จสมบูรณ์ + ในการดำเนินการต่อ ให้แตะถัดไป + ลองอีกครั้ง + คุณทำได้ไม่ค่อยดีนักในครั้งที่ผ่านมา ให้แตะถัดไปเพื่อดำเนินการต่อ + หมดเวลา + หมดเวลาแล้ว \nให้แตะถัดไปเพื่อดำเนินการต่อ + เกมเสร็จสมบูรณ์ + พักอยู่ + ในการดำเนินการต่อ ให้แตะถัดไป + + เวลาตอบสนอง + กิจกรรมนี้จะประเมินเวลาที่คุณใช้ในการตอบสนองต่อการมองเห็น + เขย่าอุปกรณ์ในทิศทางใดก็ได้ทันทีที่มีจุดสีน้ำเงินปรากฏขึ้นมาบนหน้าจอ ระบบจะขอให้คุณทำสิ่งนี้ %D ครั้ง + แตะเริ่มต้นใช้งานเพื่อเริ่ม + ความพยายาม %1$s จาก %2$s + เขย่าอุปกรณ์อย่างรวดเร็วเมื่อมีวงกลมสีนำ้เงินปรากฏขึ้น + + หอคอยแห่งฮานอย + กิจกรรมนี้จะประเมินทักษะในการไขปริศนาของคุณ + ย้ายทั้งกองไปที่แท่นวางที่ไฮไลท์ไว้โดยเคลื่อนย้ายให้น้อยที่สุดเท่าที่จะทำได้ + แตะเริ่มต้นใช้งานเพื่อเริ่ม + ไขปริศนา + จำนวนการเคลื่อนย้าย: %1$s \n %2$s + ฉันไขปริศนานี้ไม่ได้ + + การเดินแบบจับเวลา + กิจกรรมนี้จะประเมินการทำหน้าที่ของรยางค์ล่างของคุณ + ค้นหาสถานที่ โดยเฉพาะบริเวณกลางแจ้ง ซึ่งคุณสามารถเดินเป็นเส้นตรงในระยะทางประมาณ %1$s ได้เร็วที่สุดเท่าที่จะทำได้อย่างปลอดภัย อย่าลดความเร็วลงจนกว่าคุณจะเดินผ่านจุดสิ้นสุด + แตะถัดไปเพื่อเริ่ม + อุปกรณ์ช่วยเหลือ + ใช้อุปกรณ์ช่วยเหลืออันเดียวกันสำหรับการทดสอบแต่ละครั้ง + คุณสวมกายอุปกรณ์เสริมสำหรับเท้าและข้อเท้าหรือไม่ + คุณใช้อุปกรณ์ช่วยเหลือหรือไม่ + แตะที่นี่เพื่อเลือกคำตอบ + ไม่มี + ไม้เท้าแบบข้างเดียว + ไม้ค้ำยันแบบข้างเดียว + ไม้เท้าแบบสองข้าง + ไม้ค้ำยันแบบสองข้าง + วอล์กเกอร์/รถเข็นหัดเดิน + เดินเป็นเส้นตรงในระยะทางไม่เกิน %1$s + หันหลัง แล้วเดินกลับไปจุดเริ่มต้น + แตะเสร็จสิ้นเมื่อทำเสร็จ + + PASAT + PVSAT + PAVSAT + การทดสอบความสามารถผ่านการบวกเลขตามความเร็วที่ได้ยินจะประเมินข้อมูลด้านการได้ยินของคุณโดยการประมวลผลความเร็วและความสามารถด้านการคิดคำนวณ + การทดสอบความสามารถผ่านการบวกเลขตามความเร็วที่มองเห็นจะประเมินข้อมูลด้านการมองเห็นของคุณโดยการประมวลผลความเร็วและความสามารถด้านการคิดคำนวณ + การทดสอบความสามารถผ่านการบวกเลขตามความเร็วที่ได้ยินและมองเห็นจะประเมินข้อมูลด้านการได้ยินและการมองเห็นของคุณโดยการประมวลผลความเร็วและความสามารถด้านการคิดคำนวณ + ตัวเลขหนึ่งหลักจะปรากฏขึ้นทุก %1$s วินาที\nคุณต้องเพิ่มตัวเลขใหม่แต่ละตัวไปที่ตัวเลขก่อนหน้านั้นทันที\nโปรดทราบว่าคุณต้องไม่คำนวณผลรวมสะสม แต่ให้คำนวณเฉพาะผลรวมของตัวเลขสองตัวสุดท้ายเท่านั้น + แตะเริ่มต้นใช้งานเพื่อเริ่ม + จดจำตัวเลขตัวแรกนี้ + เพิ่มตัวเลขตัวใหม่นี้ไปที่ตัวเลขก่อนหน้า + - + + การทดสอบวางหมุด %1$s ครั้ง + การกระทำนี้จะวัดความสามารถของการใช้นิ้วมือของคุณโดยที่คุณจะต้องวางหมุดลงในช่อง คุณจะต้องทำการวางหมุด %1$s ครั้ง + ทั้งมือซ้ายและมือขวาของคุณจะถูกทดสอบ\nคุณจะต้องหยิบหมุดให้ไวที่สุดเท่าที่จะทำได้ แล้ววางหมุดลงในช่อง ทำซ้ำ %1$s ครั้ง จากนั้นให้หยิบหมุดออกจากช่อง %2$s ครั้ง + ทั้งมือขวาและมือซ้ายของคุณจะถูกทดสอบ\nคุณต้องหยิบหมุดให้ไวที่สุดเท่าที่จะทำได้ แล้ววางหมุดลงในช่อง ทำซ้ำ %1$s ครั้ง จากนั้นให้หยิบหมุดออกจากช่อง %2$s ครั้ง + แตะเริ่มต้นใช้งานเพื่อเริ่ม + ใช้มือซ้ายหยิบหมุดวางลงในช่อง + ใช้มือขวาหยิบหมุดวางลงในช่อง + ใช้มือซ้ายหยิบหมุดวางไว้ข้างหลังเส้น + ใช้มือขวาหยิบหมุดวางไว้ข้างหลังเส้น + ใช้นิ้วสองนิ้วหยิบหมุด + ปล่อยนิ้วเพื่อปล่อยหมุด + + กิจกรรมวัดอาการสั่น + กิจกรรมนี้เป็นตัววัดอาการสั่นของมือคุณในอิริยาบทต่างๆ โปรดหาสถานที่ที่คุณสามารถนั่งได้อย่างสบายๆ ในระหว่างที่ทำกิจกรรมนี้ + ถือโทรศัพท์ไว้ในมือข้างที่มีอาการสั่นมากกว่าตามที่แสดงในรูปภาพด้านล่าง + ถือโทรศัพท์ไว้ในมือขวาตามที่แสดงในรูปภาพด้านล่าง + ถือโทรศัพท์ไว้ในมือซ้ายตามที่แสดงในรูปภาพด้านล่าง + ระบบจะขอให้คุณทำ%1$sในระหว่างที่นั่งถือโทรศัพท์ไว้ในมือ + หนึ่งบททดสอบ + สองบททดสอบ + สามบททดสอบ + สี่บททดสอบ + ห้าบททดสอบ + แตะถัดไปเพื่อทำต่อ + เตรียมถือโทรศัพท์ของคุณไว้บนตัก + เตรียมถือโทรศัพท์ของคุณไว้บนตักโดยใช้มือซ้าย + เตรียมถือโทรศัพท์ของคุณไว้บนตักโดยใช้มือขวา + ถือโทรศัพท์ของคุณไว้นิ่งๆ บนตักนาน %1$d วินาที + ตอนนี้ให้วางโทรศัพท์ของคุณบนฝ่ามือแล้วถือไว้นิ่งๆ ที่ความสูงระดับไหล่ + ตอนนี้ให้วางโทรศัพท์ของคุณบนฝ่ามือข้างซ้ายแล้วถือไว้นิ่งๆ ที่ความสูงระดับไหล่ + ตอนนี้ให้วางโทรศัพท์ของคุณบนฝ่ามือข้างขวาแล้วถือไว้นิ่งๆ ที่ความสูงระดับไหล่ + วางโทรศัพท์ของคุณบนฝ่ามือแล้วถือไว้นิ่งๆ นาน %1$d วินาที + ตอนนี้ให้ถือโทรศัพท์ที่ความสูงระดับไหล่โดยงอข้อศอกของคุณไว้ + ตอนนี้ให้ใช้มือซ้ายถือโทรศัพท์ที่ความสูงระดับไหล่โดยงอข้อศอกของคุณไว้ + ตอนนี้ให้ใช้มือขวาถือโทรศัพท์ที่ความสูงระดับไหล่โดยงอข้อศอกของคุณไว้ + ถือโทรศัพท์ของคุณไว้นิ่งๆ โดยงอข้อศอกนาน %1$d วินาที + ตอนนี้ให้งอข้อศอกของคุณไว้นิ่งๆ แล้วเอาโทรศัพท์มาแตะที่จมูกแบบสลับไปมาซ้ำๆ + ตอนนี้ให้ใช้มือซ้ายถือโทรศัพท์และงอข้อศอกของคุณไว้นิ่งๆ แล้วเอาโทรศัพท์มาแตะที่จมูกแบบสลับไปมาซ้ำๆ + ตอนนี้ให้ใช้มือขวาถือโทรศัพท์และงอข้อศอกของคุณไว้นิ่งๆ แล้วเอาโทรศัพท์มาแตะที่จมูกแบบสลับไปมาซ้ำๆ + เอาโทรศัพท์ของคุณมาแตะที่จมูกแบบสลับไปมานาน %1$d วินาที + เตรียมทำท่าโบกมือเบาๆ (โบกมือโดยขยับแค่ข้อมือ) + เตรียมทำท่าโบกมือเบาๆ โดยใช้มือซ้ายที่ถือโทรศัพท์ไว้ (โบกมือโดยขยับแค่ข้อมือ) + เตรียมทำท่าโบกมือเบาๆ โดยใช้มือขวาที่ถือโทรศัพท์ไว้ (โบกมือโดยขยับแค่ข้อมือ) + ทำท่าโบกมือเบาๆ นาน %1$d วินาที + ตอนนี้ให้สลับไปใช้มือซ้ายถือโทรศัพท์แล้วทำบททดสอบถัดไปต่อ + ตอนนี้ให้สลับไปใช้มือขวาถือโทรศัพท์แล้วทำบททดสอบถัดไปต่อ + ทำบททดสอบถัดไปต่อ + กิจกรรมเสร็จสมบูรณ์แล้ว + ระบบจะขอให้คุณทำ%1$sในระหว่างที่นั่งถือโทรศัพท์ไว้ในมือข้างหนึ่ง แล้วเปลี่ยนไปถือในมืออีกข้างหนึ่ง + ฉันไม่สามารถทำกิจกรรมนี้ได้โดยใช้มือซ้าย + ฉันไม่สามารถทำกิจกรรมนี้ได้โดยใช้มือขวา + ฉันสามารถทำกิจกรรมนี้ได้โดยใช้มือทั้งสองข้าง + + สร้างไฟล์ไม่ได้ + ไม่สามารถเอาไฟล์บันทึกออกให้เพียงพอกับค่าที่กำหนด + ข้อผิดพลาดในการตั้งค่าคุณลักษณะ + ลบไฟล์ที่ไม่ได้ทำเครื่องหมายว่าอัปโหลดแล้ว + ข้อผิดพลาดหลายส่วนเกี่ยวกับการเอาบันทึกออก + ไม่พบข้อมูลที่เก็บรวบรวม + ไม่ได้ระบุสารบบผลลัพธ์ไว้ + + ไม่มีข้อมูล + + ย้อนกลับ + ภาพประกอบของ %1$s + ช่องสำหรับลายเซ็นที่กำหนด + แตะหน้าจอแล้วเลื่อนนิ้วของคุณเพื่อเซ็นชื่อ + เซ็นชื่อแล้ว + ไม่ได้เซ็นชื่อ + เลือกอยู่ + ไม่ได้เลือก + แถบเลื่อนการตอบสนอง ช่วงตั้งแต่ %1$s ถึง %2$s + รูปภาพที่ไม่มีคำอธิบาย + เริ่มงาน + ใช้งานอยู่ + ถูกต้อง + ไม่ถูกต้อง + ไม่ได้ใช้งาน + เกมเปิดป้ายจับคู่ + แสดงตัวอย่างการเก็บภาพ + รูปภาพที่จับภาพ + แสดงตัวอย่างภาพถ่ายวิดีโอ + วิดีโอที่ถ่ายไว้ + ไม่สามารถวางจานที่มีขนาด %1$s บนจานที่มีขนาด %2$s + เป้าหมาย + หอคอย + แตะสองครั้งเพื่อวางจาน + แตะสองครั้งเพื่อเลือกจานที่อยู่ด้านบนสุด + มีจานที่มีขนาด %1$s + ว่างเปล่า + ช่วงตั้งแต่ %1$s ถึง %2$s + สแต็คประกอบด้วย + และ + จุด: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-tr/strings.xml b/backbone/src/main/res/values-tr/strings.xml new file mode 100644 index 000000000..bc34680dc --- /dev/null +++ b/backbone/src/main/res/values-tr/strings.xml @@ -0,0 +1,396 @@ + + + + + İzin + Ad + Soyad + Gerekli + Gözden Geçir + Aşağıdaki formu gözden geçirin ve sürdürmeye hazırsanız Kabul Ediyorum’a dokunun. + Gözden Geçir + İmza + Lütfen parmağınızı kullanarak aşağıdaki çizgiye imza atın. + Burayı İmzalayın + Sayfa %1$ld / %2$ld + + Hoş Geldiniz + Veri Toplama + Gizlilik + Veri Kullanımı + Çalışma Anketi + Çalışma Görevleri + Zaman Ayırma + Sözünü Geri Alma + Daha Fazla Bilgi + + Verilerin toplanması hakkında daha fazla bilgi + Verilerin kullanılması hakkında daha fazla bilgi + Gizliliğinizin ve kimliğinizin korunması hakkında daha fazla bilgi + Çalışma hakkında daha fazla bilgi + Çalışma anketi hakkında daha fazla bilgi + Çalışmanın zamanınıza etkisi hakkında daha fazla bilgi + İlişkili görevler hakkında daha fazla bilgi + Sözünü geri alma hakkında daha fazla bilgi + + Paylaşma Seçenekleri + Verilerimi %1$s ve dünya çapında nitelikli araştırmacılarla paylaş + Verilerimi yalnızca %1$s ile paylaş + %1$s, bu çalışmaya katılımınız sonucundaki çalışma verilerinizi alacaktır.\n\nKodlanmış çalışma verilerinizin daha kapsamlı bir biçimde paylaşılması (adınız gibi bilgiler olmadan) bu çalışmaya ve ilerideki çalışmalara faydalı olabilir. + Veri paylaşımı hakkında daha fazla bilgi + + %1$s Adı (yazılı) + %1$s İmzası + Tarih + + Adım %1$s / %2$s + + Geçersiz değer + %1$s, izin verilen maksimum değeri (%2$s) aşıyor. + %1$s, izin verilen minimum değerden (%2$s) küçük. + %1$s geçerli bir değer değil. + + Geçersiz e-posta adresi: %1$s + + Adres girin + Belirtilen Adres Bulunamadı + Şu anki konumunuz çözülemedi. Lütfen bir adres girin veya uygunsa daha iyi GPS sinyalı olan bir konuma gidin. + Konum servislerine erişim reddedildi. Lütfen konum servislerini kullanmak için Ayarlar\'da bu uygulamaya izin verin. + Girilen adres için bir sonuç bulunmadı. Lütfen adresin geçerli olduğundan emin olun. + İnternet\'e bağlı değilsiniz veya en fazla adres arama isteği miktarını aştınız. İnternet\'e bağlı değilseniz lütfen bu soruya yanıt vermek için Wi-Fi\'yi açın, atlama düğmesi varsa bu soruyu atlayın ya da İnternet\'e bağlı olduğunuzda anket için yeniden sayfayı ziyaret edin. Aksi takdirde, birkaç dakika sonra yeniden deneyin. + + Metin içeriği maksimum uzunluğu aşıyor: %1$s + + Bölünmüş ekranda Kamera kullanılamıyor. + + E-posta + ad@example.com + Parola + Parola girin + Doğrula + Parolayı yeniden girin + Parolalar eşleşmiyor. + Ek Bilgi + Ali + Utku + Cinsiyet + Bir cinsiyet seçin + Doğum Tarihi + Bir tarih seçin + + Doğrulama + E-postanızı doğrulayın + Doğrulama e-postası almadıysanız ve yeniden gönderilmesini istiyorsanız, lütfen aşağıdaki bağlantıya dokunun. + Doğrulamayı Yeniden E-postala + + Oturum Aç + Parolayı mı unuttunuz? + + Parola girin + Parolayı doğrulayın + Parola kaydedildi + Parola doğrulandı + Eski parolanızı girin + Yeni parolanızı girin + Yeni parolanızı doğrulayın + Parola Doğru Değil + Parolalar eşleşmedi. Yeniden deneyin. + Lütfen Touch ID ile kimlik doğrulayın + Touch ID hatası + Yalnızca sayısal karakterlere izin veriliyor. + Parola giriş ilerleme göstergesi + %1$s / %2$s basamak girildi + Parolayı mı Unuttunuz? + + Anahtar zinciri öğesi eklenemedi. + Anahtar zinciri öğesi güncellenemedi. + Anahtar zinciri öğesi silinemedi. + Anahtar zinciri öğesi bulunamadı. + + A+ + A- + AB+ + AB- + B+ + B- + 0+ + 0- + + Kadın + Erkek + Diğer + + Hayır + Evet + + cm + fit + inç + + Yanıtlamak için dokunun + Bir yanıt seçin + Seçmek için dokunun + Yazmak için dokunun + + Kabul Ediyorum + Vazgeç + Tamam + Sil + Kabul Etmiyorum + Bitti + Başla + Daha fazla bilgi + İleri + Atla + Bu soruyu atla + Sayacı Başlat + Sonrası İçin Kaydet + Sonuçları At + Görevi Bitir + Kaydet + Yanıtı sil + Bu yanıt değiştirilemiyor. + + Aktivite başlatılıyor + Aktivite Tamamlandı + Verileriniz incelenecek ve sonuçlarınız hazır olduğunuzda size haber verilecektir. + %1$s saniye kaldı. + + Görüntü Yakala + Görüntüyü Yeniden Yakala + Kamera bulunamadı. Bu adım tamamlanamıyor. + Bu adımı tamamlamak için Ayarlar\'da bu uygulamanın kameraya erişimine izin verin. + Yakalanan görüntülerin kaydedileceği bir dizin belirtilmemiş. + Yakalanan görüntü kaydedilemedi. + + Kaydı Başlat + Kaydı Durdur + Videoyu Yeniden Yakala + + Fitness + Mesafe (%1$s) + Kalp Atış Hızı (vuruş/dk) + Rahat bir şekilde %1$s oturun. + Yürüyebildiğiniz kadar hızlı %1$s yürüyün. + Bu aktivite, kalp atış hızınızı izler ve %1$s içinde ne kadar mesafe yürüyebileceğinizi ölçer. + Açık havada yürüyebildiğiniz en yüksek hızda %1$s yürüyün. Bitirdiğinizde, oturun ve %2$s rahat bir şekilde dinlenin. Başlamak için Başla’ya dokunun. + + Yürüyüş Tarzı ve Denge + Bu aktivite, yürürken ve ayakta dururken yürüyüş tarzınızı ve dengenizi ölçer. Yardımsız, güvenli bir şekilde yürüyemiyorsanız devam etmeyin. + Düz bir çizgide yaklaşık %1$d adım yardımsız, güvenli bir şekilde yürüyebileceğiniz bir yer bulun. + Telefonunuzu cebinize veya çantanıza koyun ve sesli yönergeleri izleyin. + Şimdi %1$s süresince hareketsiz durun. + Hareketsiz bir şekilde %1$s durun. + Geriye dönün ve başladığınız yere yürüyün. + Düz bir çizgide en fazla %1$d adım yürüyün. + + Düz bir çizgide ileri geri güvenli bir şekilde yürüyebileceğiniz bir yer bulun. Yolunuzun sonunda geri dönüp devam ederek durmadan yürümeye çalışın.\n\nSonra tam daire etrafında dönmeniz, ardından da kollarınız yanlarda ve ayaklarınız omuz genişliğinde açık olarak hareketsiz durmanız istenecektir. + Başlamaya hazır olduğunuzda Başla’ya dokunun.\nSonra telefonunuzu cebinize veya çantanıza koyun ve sesli yönergeleri izleyin. + Düz bir çizgide ileri geri %1$s yürüyün. Normal bir şekilde yürüyün. + Tam daire şeklinde dönün ve sonra %1$s süresince hareketsiz durun. + Bu aktiviteyi tamamladınız. + + Dokunma Hızı + Sağ El + Sol El + Bu aktivite dokunma hızınızı ölçer. + Telefonunuzu düz bir yüzeye koyun. + Ekrandaki düğmelere dönüşümlü olarak dokunmak için aynı elinizin iki parmağını kullanın. + Ekrandaki düğmelere dönüşümlü olarak dokunmak için sağ elinizin iki parmağını kullanın. + Ekrandaki düğmelere dönüşümlü olarak dokunmak için sol elinizin iki parmağını kullanın. + Şimdi aynı testi sağ elinizi kullanarak tekrarlayın. + Şimdi aynı testi sol elinizi kullanarak tekrarlayın. + Tek parmağınızla, sonra diğeriyle dokunun. Mümkün olduğunca eşit zaman aralıklarıyla dokunmaya çalışın. %1$s süresince dokunmaya devam edin. + Başlamak için Başla’ya dokunun. + Başlamak için İleri’ye dokunun. + Dokun + Toplam Dokunma + İki parmağınızı kullanarak düğmelere dokunabildiğiniz kadar tutarlı dokunun. + Düğmelere SAĞ elinizi kullanarak dokunun. + Düğmelere SOL elinizi kullanarak dokunun. + Bu eli atla + + Ses + Başlamak için Başla’ya dokunun. + Mikrofona, söyleyebildiğiniz kadar uzun “Aaaaa” deyin. + Derin bir nefes alın ve mikrofona, söyleyebildiğiniz kadar uzun “Aaaaa” deyin. Ses çubukları mavi kalacak şekilde ses yüksekliğini sabit tutmaya çalışın. + Bu aktivite, telefonunuzun alt tarafındaki mikrofonla kayıt yaparak sesinizi değerlendirir. + Çok Yüksek + Ses kaydı yapılamıyor + Arkaplan gürültü düzeyi denetlenirken lütfen bekleyin. + Ortam gürültü düzeyi sesinizi kaydedemeyecek kadar yüksek. Lütfen daha sessiz bir yere gidip yeniden deneyin. + Hazır olduğunuzda İleri’ye dokunun. + + Ses Tonu Odyometrisi + Bu aktivite, farklı sesleri duyabilme becerinizi ölçer. + Başlamadan önce kulaklığınızı bağlayın ve takın. + Başlamak için Başla’ya dokunun. + Bir ses duyacaksınız. Aygıtınızın yan tarafında bulunan denetimleri kullanarak ses seviyesini ayarlayın.\n\nBaşlamaya hazır olduğunuzda düğmeye dokunun. + Her ses duymaya başladığınızda düğmeye dokunun. + %1$s Hz, Sol + %1$s Hz, Sağ + + Konumsal Bellek + Bu aktivite, %1$s görüntülerinin sırasını yinelemenizi isteyerek kısa süreli konumsal belleğinizi ölçer. + çiçek + çiçek + Bu %1$s görüntülerinin bazıları teker teker yanacaktır. Bu %2$s görüntülerine yandıkları sırada dokunun. + Bu %1$s görüntülerinin bazıları teker teker yanacaktır. Bu %2$s görüntülerine yandıkları sıranın tersinde dokunun. + Başlamak için Başla\'ya dokunun, sonra dikkatlice izleyin. + %1$s + Puan + Bu %1$s görüntülerinin yanmasını izleyin + Bu %1$s görüntülerine yandıkları sırada dokunun + Bu %1$s görüntülerine ters sırada dokunun + Dizi Tamamlandı + Devam etmek için İleri’ye dokunun + Yeniden Deneyin + Bu sefer pek iyi yapamadınız. Devam etmek için İleri’ye dokunun. + Süre Doldu + Süreniz kalmadı.\nDevam etmek için İleri’ye dokunun. + Oyun Tamamlandı + Duraklatıldı + Devam etmek için İleri’ye dokunun + + Tepki Süresi + Bu aktivite, görünen bir ipucuna yanıt verebildiğiniz süreyi hesaplar. + Ekranda mavi nokta görünür görünmez aygıtı herhangi bir yönde hızlıca sallayın. Bunu %D kez yapmanız istenecek. + Başlamak için Başla’ya dokunun. + %1$s / %2$s Girişim + Mavi çember göründüğünde aygıtı hızlıca sallayın + + Hanoi Kulesi + Bu aktivite bulmaca çözme becerilerinizi ölçer. + Tüm yığını, vurgulanan platforma mümkün olan en az hamlede taşıyın. + Başlamak için Başla’ya dokunun + Bulmacayı Çöz + Hamle Sayısı: %1$s \n %2$s + Bu bulmacayı çözemedim + + Süreli Yürüme + Bu aktivite, alt ekstremite fonksiyonlarınızı ölçer. + Düz bir çizgide mümkün olduğunca hızlı ama güvenli bir şekilde %1$s yürüyebileceğiniz bir yer (tercihen dışarıda) bulun. Bitirme çizgisini geçene dek yavaşlamayın. + Başlamak için İleri’ye dokunun. + Yardımcı aygıt + Her test için aynı yardımcı aygıtı kullanın. + Ayak bileği ortezi takıyor musunuz? + Yardımcı aygıt kullanıyor musunuz? + Bir yanıt seçmek için dokunun. + Hiçbiri + Tek Taraflı Baston + Tek Taraflı Koltuk Değneği + Çift Taraflı Baston + Çift Taraflı Koltuk Değneği + Yürüteç/Yürüme Desteği + Düz bir çizgide en fazla %1$s yürüyün. + Geriye dönün ve başladığınız yere yürüyün. + Tamamladığınızda Bitti’ye dokunun. + + PASAT + PVSAT + PAVSAT + Tempolu İşitsel Toplama Testi, işitsel bilgi işleme hızınızı ve hesaplama becerinizi ölçer. + Tempolu Görsel Toplama Testi, görsel bilgi işleme hızınızı ve hesaplama becerinizi ölçer. + Tempolu İşitsel-Görsel Toplama Testi, işitsel ve görsel bilgi işleme hızınızı ve hesaplama becerinizi ölçer. + Her %1$s saniyede bir tek tek rakamlar sunulur.\nHer yeni rakamı bir önceki rakama eklemeniz gerekir.\nDikkat edin, değişen toplamı değil yalnızca son iki rakamın toplamını hesaplamanız gerekir. + Başlamak için Başla’ya dokunun. + Bu ilk rakamı aklınızda tutun. + Bu yeni rakamı bir öncekine ekleyin. + - + + %1$s Delikli Daire Testi + Bu aktivite, bir daireyi deliğe yerleştirmenizi isteyerek üst uzuvlarınızın işlevini ölçer. Bunu %1$s kez yapmanız istenecektir. + Hem sol hem de sağ eliniz test edilecektir.\nDaireyi mümkün olduğu kadar hızlı bir şekilde almalı, onu deliğe koymalı ve bunu %1$s kez yaptıktan sonra yeniden %2$s kez kaldırmalısınız. + Hem sağ hem de sol eliniz test edilecektir.\nDaireyi mümkün olduğu kadar hızlı bir şekilde almalı, onu deliğe koymalı ve bunu %1$s kez yaptıktan sonra yeniden %2$s kez kaldırmalısınız. + Başlamak için Başla’ya dokunun. + Sol elinizi kullanarak daireyi deliğin içine koyun. + Sağ elinizi kullanarak daireyi deliğin içine koyun. + Sol elinizi kullanarak daireyi çizginin arkasına koyun. + Sağ elinizi kullanarak daireyi çizginin arkasına koyun. + İki parmağınızla daireyi alın. + Daireyi bırakmak için parmakları kaldırın. + + Tremor Aktivitesi + Bu aktivite, çeşitli pozisyonlarda ellerinizdeki tremor düzeyini ölçer. Bu aktivite süresince rahat bir şekilde oturabileceğiniz bir yer bulun. + Telefonu aşağıdaki görüntüde gösterildiği gibi daha çok etkilenen elinizde tutun. + Telefonu aşağıdaki görüntüde gösterildiği gibi SAĞ elinizde tutun. + Telefonu aşağıdaki görüntüde gösterildiği gibi SOL elinizde tutun. + Telefonunuz elinizde otururken %1$s gerçekleştirmeniz istenecektir. + bir görev + iki görev + üç görev + dört görev + beş görev + Devam etmek için İleri’ye dokunun. + Telefonunuzu kucağınızda tutmaya hazırlanın. + Telefonunuzu SOL elinizle kucağınızda tutmaya hazırlanın. + Telefonunuzu SAĞ elinizle kucağınızda tutmaya hazırlanın. + Telefonunuzu %1$d saniye kucağınızda tutmaya devam edin. + Şimdi telefonunuzu eliniz uzatılmış olarak omuz yüksekliğinde tutun. + Şimdi telefonunuzu SOL eliniz uzatılmış olarak omuz yüksekliğinde tutun. + Şimdi telefonunuzu SAĞ eliniz uzatılmış olarak omuz yüksekliğinde tutun. + Telefonunuzu eliniz uzatılmış olarak %1$d saniye tutmaya devam edin. + Şimdi telefonunuzu dirseğiniz kıvrık olarak omuz yüksekliğinde tutun. + Şimdi telefonunuzu dirseğiniz kıvrık olarak omuz yüksekliğinde SOL elinizle tutun. + Şimdi telefonunuzu dirseğiniz kıvrık olarak omuz yüksekliğinde SAĞ elinizle tutun. + Telefonunuzu dirseğiniz kıvrık olarak %1$d saniye tutmaya devam edin + Şimdi dirseğinizi kıvrık tutarak telefonunuzu burnunuza tekrar tekrar dokundurun. + Şimdi telefonunuz SOL elinizde olacak şekilde dirseğinizi kıvrık tutarak telefonunuzu burnunuza tekrar tekrar dokundurun. + Şimdi telefonunuz SAĞ elinizde olacak şekilde dirseğinizi kıvrık tutarak telefonunuzu burnunuza tekrar tekrar dokundurun. + Telefonunuzu burnunuza değdirmeye %1$d saniye devam edin + Bileğinizi çevirerek el sallamaya hazırlanın. + Telefonunuz SOL elinizde olacak şekilde bileğinizi çevirerek el sallamaya hazırlanın. + Telefonunuz SAĞ elinizde olacak şekilde bileğinizi çevirerek el sallamaya hazırlanın. + Bileğinizi çevirerek el sallamaya %1$d saniye devam edin. + Şimdi telefonunuzu SOL elinize alıp bir sonraki göreve geçin. + Şimdi telefonunuzu SAĞ elinize alıp bir sonraki göreve geçin. + Bir sonraki göreve geçin. + Aktivite tamamlandı. + Telefonunuz önce bir elinizde, sonra diğer elinizde otururken %1$s gerçekleştirmeniz istenecektir. + Bu aktiviteyi SOL elimle gerçekleştiremem. + Bu aktiviteyi SAĞ elimle gerçekleştiremem. + Bu aktiviteyi her iki elimle gerçekleştirebilirim. + + Dosya yaratılamadı + Eşiğe ulaşmaya yetecek kadar günlük dosyası silinemedi + Özelliği ayarlama hatası + Dosya silinmiş (karşıya yüklenmiş) olarak işaretlenemedi + Günlükleri silmeyle ilgili birden fazla hata + Toplanmış bir veri bulunamadı. + Çıkış dizini belirtilmemiş + + Veri Yok + + Geri + %1$s resmi + Belirtilen imza alanı + İmzalamak için ekrana dokunup parmağınızı hareket ettirin + İmzalanmış + İmzalanmamış + Seçili + Seçili değil + Yanıt sürgüsü. %1$s ile %2$s aralığında + Etiketsiz görüntü + Göreve başla + etkin + doğru + yanlış + hareketsiz + Bellek oyunu taşı + Yakalanan görüntü önizlemesi + Yakalanmış görüntü + Video yakalama önizlemesi + Yakalanmış video + %1$s büyüklüğündeki disk, %2$s büyüklüğündeki diskin üzerine yerleştirilemez + Hedef + Kule + Diski yerleştirmek için çift dokunun + En üstteki diski seçmek için çift dokunun + %1$s büyüklüklerinde disk içeriyor + Boş + %1$s - %2$s aralığında + Yığın içeriği: + ve + Nokta: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-uk/strings.xml b/backbone/src/main/res/values-uk/strings.xml new file mode 100644 index 000000000..48c74b025 --- /dev/null +++ b/backbone/src/main/res/values-uk/strings.xml @@ -0,0 +1,396 @@ + + + + + Згода + Ім’я + Прізвище + Обов’язково + Огляд + Перегляньте наведене нижче і торкніть «Погоджуюсь», якщо ви готові продовжити. + Огляд + Підпис + Будь ласка, розпишіться пальцем на лінії нижче. + Розпишіться тут + Сторінка %1$ld із %2$ld + + Ласкаво просимо + Збір даних + Приватність + Використання даних + Дослідне опитування + Дослідні завдання + Часове навантаження + Відкликання + Докладніше + + Докладніше про збір даних + Докладніше про використання даних + Докладніше про захист вашої приватності і особистих даних + Докладніше про дослідження + Докладніше про дослідне опитування + Докладніше про вплив дослідження на ваш час + Докладніше про залучені завдання + Докладніше про відкликання + + Опції доступу + Оприлюднити мої дані для %1$s і кваліфікованих вчених в усьому світі + Оприлюднювати тільки для %1$s + %1$s отримають дані дослідження після вашої участі в цьому дослідженні.\n\nОприлюднення шифрованих даних дослідження для ширшої аудиторії (без таких даних, як ваше ім’я) може бути корисним для цього та майбутніх досліджень. + Докладніше про оприлюднення даних + + Ім’я %1$s (друкованими) + Підпис %1$s + Дата + + Крок %1$s із %2$s + + Неправильне значення + %1$s перевищує максимально дозволене значення (%2$s). + %1$s є меншим мінімально дозволеного значення (%2$s). + %1$s не є правильним значенням. + + Хибна адреса е-пошти: %1$s + + Введіть адресу + Не вдалося знайти вказану адресу + Не вдається відстежити ваше поточне місце. Введіть адресу або перейдіть у місце з кращим сигналом GPS, якщо це можливо. + У доступі до Служби локації відмовлено. Надайте цій програмі дозвіл на використання Служби локації у Параметрах. + Не вдається знайти збіги для введеної адреси. Перевірте правильність адреси. + Ви або не підключені до Інтернету, або перевищили ліміт запитів пошуку адреси. Якщо ви не підключені до Інтернету, увімкніть Wi-Fi, щоб відповісти на це питання, пропустити це питання, якщо кнопка пропуску доступна, або повертайтесь до опитування, коли підключитесь до Інтернету. У іншому випадку повторіть спробу через кілька хвилин. + + Текстовий вміст перевищує ліміт: %1$s + + На розділеному екрані Камера недоступна. + + Е-пошта + syabluchko@example.com + Пароль + Введіть пароль + Підтвердити + Введіть пароль ще раз + Паролі не збігаються. + Додаткова інформація + Степан + Яблучко + Стать + Виберіть стать + Дата народження + Виберіть дату + + Перевірка + Перевірка е-пошти + Торкніть наведене нижче посилання, якщо ви не отримали лист перевірки і хочете, щоб його надіслали повторно. + Надіслати лист перевірки ще раз + + Ім’я + Забули пароль? + + Введіть код допуску + Підтвердьте код + Код допуску збережено + Код допуску прийнято + Введіть старий код допуску + Введіть новий код допуску + Підтвердьте свій новий код + Неправильний код допуску + Незбіг кодів. Спробуйте ще раз. + Будь ласка, засвідчіть своїм Touch ID + Помилка Touch ID + Дозволено тільки цифри. + Індикатор перебігу вводу коду допуску + Введено: %1$s із %2$s + Забули код допуску? + + Не вдалося додати елемент до В\'язки. + Не вдалося оновити елемент В\'язки. + Не вдалося видалити елемент із В\'язки. + Не вдалося знайти елемент В\'язки. + + A(II)+ + A(II)- + AB(IV)+ + AB(IV)- + B(III)+ + B(III)- + O(I)+ + O(I)- + + Жіноча + Чоловіча + Інше + + Ні + Так + + см + фт + дм + + Торкніть, щоб відповісти + Вибрати відповідь + Торкніть, щоб вибрати + Торкніть, щоб написати + + Погоджуюсь + Скасувати + OK + Очистити + Не погоджуюсь + Готово + Розпочати + Докладніше + Далі + Пропустити + Пропустити питання + Запустити таймер + Пізніше + Відхилити результати + Завершити завдання + Зберегти + Стерти відповідь + Цю відповідь неможливо змінити. + + Початок тесту через + Тест завершено + Ваші дані буде проаналізовано, і ви отримаєте сповіщення, коли будуть готові результати. + Лишлось %1$s секунд. + + Зробити знімок + Зробити знімок ще раз + Камери не знайдено.\u0020\u0020Неможливо виконати цей крок. + Щоб завершити цей крок, надайте цій програмі доступ до камери в Параметрах. + Для зроблених знімків не вказано вихідного каталогу. + Не вдається зберегти зроблений знімок. + + Почати запис + Зупинити запис + Зняти відео ще раз + + Фітнес + Відстань (%1$s) + Пульс (уд./хв) + Сидіть зручно протягом %1$s. + Ідіть якомога швидше протягом %1$s. + Цей тест оцінює ваш серцевий ритм і обчислює, скільки ви можете пройти за %1$s. + Ідіть надворі своєю найшвидшою ходою протягом %1$s. Завершивши, зручно сядьте і відпочивайте протягом %2$s. Щоб почати, торкніть «Розпочати». + + Хода і рівновага + Цей тест оцінює вашу ходу і рівновагу під час ходіння і стояння. Не продовжуйте, якщо ви не можете безпечно ходити без допомоги. + Знайдіть місце, де ви можете безпечно зробити без допомоги інших %1$d кроків по прямій. + Покладіть свій телефон у кишеню або сумку, і виконуйте аудіоінструкції. + Тепер не рухайтесь %1$s. + Не рухайтесь протягом %1$s. + Розверніться і йдіть туди, де ви почали. + Зробіть до %1$d кроків по прямій. + + Знайдіть місце, де ви можете безпечно пройти по прямій уперед і назад. Спробуйте не зупинятися, коли розвертаєтесь у кінці прямої, немов обходячи уявний конус.\n\nДалі вам буде запропоновано пройти повну відстань в обидва напрямки, зупинитися і не рухатися, тримаючи руки по боках і ноги на ширині плечей. + Торкніть «Розпочати», коли ви готові.\nПотім покладіть телефон у кишеню або сумку, і виконуйте аудіоінструкції. + Ідіть по прямій уперед і назад протягом %1$s. Ідіть, як ви ходите зазвичай. + Пройдіть повну відстань по колу, потім зупиніться і не рухайтесь протягом %1$s. + Ви завершили виконання тесту. + + Темп торкання + Права рука + Ліва рука + Цей тест оцінює вашу швидкість торкання. + Покладіть телефон на рівну поверхню. + Двома пальцями однієї руки по черзі торкайте кнопки на екрані. + Двома пальцями правої руки по черзі торкайте кнопки на екрані. + Двома пальцями лівої руки по черзі торкайте кнопки на екрані. + Тепер повторіть цей самий тест для правої руки. + Тепер повторіть цей самий тест для лівої руки. + Торкніть одним пальцем, потім іншим. Намагайтеся торкатися з якомога рівнішим інтервалом. Продовжуйте торкати протягом %1$s. + Торкніть «Розпочати», щоб почати. + Торкніть «Далі», щоб почати. + Торкнути + Всього дотиків + Торкайте кнопки двома пальцями якомога послідовніше. + Торкайте кнопки ПРАВОЮ рукою. + Торкайте кнопки ЛІВОЮ рукою. + Минути цю руку + + Голос + Торкніть «Розпочати», щоб почати. + Вимовляйте «А-а-а-а» у мікрофон якомога довше. + Зробіть глибокий подих і вимовляйте «А-а-а-а» у мікрофон якомога довше. Тримайте однакову гучність голосу, щоб смуги звуку залишалися синіми. + Цей тест оцінює ваш голос, записуючи його через мікрофон внизу вашого телефону. + Надто гучно + Не вдається записати аудіо + Будь ласка, зачекайте, поки ми перевіряємо рівень фонового шуму. + Рівень навколишнього шуму зависокий для записування голосу. Будь ласка, повторіть спробу у тихішому місці. + Торкніть «Далі», коли ви готові. + + Аудіометрія + Цей тест вимірює вашу здатність чути різні звуки. + Перед початком приєднайте і надіньте навушники. + Торкніть «Розпочати», щоб почати. + Зараз ви почуєте звук. Регулюйте гучність кнопками на боці вашого пристрою.\n\nТоркніть кнопку, коли ви готові починати. + Торкайте кнопку щоразу, коли почуєте звук. + %1$s Гц, зліва + %1$s Гц, справа + + Просторова пам’ять + Цей тест оцінює вашу короткострокову пам’ять, коли вам потрібно повторити порядок загоряння %1$s. + квітки + квітки + Деякі %1$s будуть загорятися по черзі. Торкайте ці %2$s у такому самому порядку загоряння. + Деякі %1$s будуть загорятися по черзі. Торкайте %2$s у зворотному порядку їх загоряння. + Щоб почати, торкніть «Розпочати», потім уважно слідкуйте. + %1$s + Рахунок + Слідкуйте, як загоряються %1$s + Торкайте %1$s у порядку, у якому вони загоряються + Торкайте %1$s у зворотному порядку + Послідовність завершено + Щоб продовжити, торкніть «Далі» + Повторити спробу + У вас не вийшло вкластися у цей час. Торкніть «Далі», щоб продовжити. + Час вийшов + У вас вийшов час.\nТоркніть «Далі», щоб продовжити. + Гру завершено + Припинено + Щоб продовжити, торкніть «Далі» + + Час реакції + Цей тест оцінює час, потрібний вам, щоб зреагувати на візуальну команду. + Трясніть пристроєм в будь-який бік, щойно на екрані з’явиться синя точка. Вас попросять зробити це кілька разів: %D. + Торкніть «Розпочати», щоб почати. + Спроба %1$s із %2$s + Швидко трусніть пристроєм, коли з’явиться синє коло + + Ханойська вежа + Цей тест оцінює вашу здібність до розв’язання головоломок. + Перенесіть весь стос на виділену платформу мінімальною кількістю ходів. + Торкніть «Розпочати», щоб почати + Розв’яжіть головоломку + Кількість ходів: %1$s \n %2$s + Я не можу розв’язати цю головоломку + + Ходіння за часом + Цей тест оцінює роботу ваших нижніх кінцівок. + Знайдіть місце, краще надворі, де ви можете якомога швидко і безпечно пройти близько %1$s по прямій. Не зменшуйте швидкість, доки не перетнете фінішну лінію. + Торкніть «Далі», щоб почати. + Допоміжний пристрій + Вживайте однаковий допоміжний засіб для всіх тестів. + Ви носите гомілкостопний біопротез? + Ви користуєтесь допоміжними засобами? + Торкніть тут і виберіть відповідь. + Немає + Однобічна тростина + Однобічна милиця + Двобічна тростина + Двобічна милиця + Ходунки/ролятор + Пройдіть до %1$s по прямій. + Розверніться і йдіть туди, де ви почали. + Завершивши, торкніть «Готово». + + СТСЗТ + ВТСЗТ + ВСТСЗТ + Слуховий тест на складання у заданому темпі вимірює швидкість обробки вами слухової інформації та вашу здібність до обчислення. + Візуальний тест на складання у заданому темпі вимірює швидкість обробки вами візуальної інформації та вашу здібність до обчислення. + Слуховий і візуальний тест на складання у заданому темпі вимірює швидкість обробки вами слухової і візуальної інформації та вашу здібність до обчислення. + Кожні %1$s с будуть відображатися однозначні числа.\nВам потрібно додавати кожне нове число до попереднього.\nЗауважте, що потрібно обчислювати суму лише двох останніх чисел, а не загальну суму всіх чисел. + Торкніть «Розпочати», щоб почати. + Запам’ятайте цю першу цифру. + Додайте це нове число до попереднього. + - + + Кілковий тест на %1$s отворів + Цей тест оцінює функцію ваших верхніх кінцівок, коли вам потрібно покласти кілок в отвір. Вас попросять зробити це %1$s разів. + Буде перевірено вашу ліву і праву руку.\nВам потрібно взяти кілок якомога швидше, покласти його в отвір, і зробивши це %1$s разів, заберіть його %2$s разів. + Буде перевірено вашу праву і ліву руку.\nВам потрібно взяти кілок якомога швидше, покласти його в отвір, і зробивши це %1$s разів, заберіть його %2$s разів. + Торкніть «Розпочати», щоб почати. + Покладіть кілок в отвір лівою рукою. + Покладіть кілок в отвір правою рукою. + Покладіть кілок за лінію лівою рукою. + Покладіть кілок за лінію правою рукою. + Візьміть кілок двома пальцями. + Підніміть пальці, щоб опустити кілок. + + Тест на тремтіння + Цей тест вимірює рівень тремтіння рук у різних положеннях. Знайдіть місце, де можна зручно сидіти протягом виконання цього тесту. + Тримайте телефон у найбільш ураженій руці, як це показано на зображенні нижче. + Тримайте телефон у ПРАВІЙ руці, як це показано на зображенні нижче. + Тримайте телефон у ЛІВІЙ руці, як це показано на зображенні нижче. + Вам буде запропоновано виконати %1$s, сидячи з телефоном у руці. + завдання + два завдання + три завдання + чотири завдання + пʼять завдань + Торкніть «Далі», щоб продовжити. + Готуйтеся тримати телефон на колінах. + Готуйтеся тримати телефон на колінах ЛІВОЮ рукою. + Готуйтеся тримати телефон на колінах ПРАВОЮ рукою. + Продовжуйте тримати телефон на колінах протягом %1$d секунд. + Тепер тримайте телефон рукою, протягнутою на рівні плечей. + Тепер тримайте телефон у ЛІВІЙ руці із зігнутим ліктем, протягнутою на рівні плечей. + Тепер тримайте телефон у ПРАВІЙ руці із зігнутим ліктем, протягнутою на рівні плечей. + Продовжуйте тримати телефон простягнутою рукою протягом %1$d секунд. + Тепер тримайте телефон на висоті плечей у руці із зігнутим ліктем. + Тепер тримайте телефон у ЛІВІЙ руці із зігнутим ліктем на рівні плечей. + Тепер тримайте телефон у ПРАВІЙ руці із зігнутим ліктем на рівні плечей. + Продовжуйте тримати телефон у руці із зігнутим ліктем протягом %1$d секунд + Тепер, не розгинаючи ліктя, торкайтеся неодноразово телефоном свого носа. + Тепер, тримаючи телефон у ЛІВІЙ руці із зігнутим ліктем, торкайтеся неодноразово телефоном свого носа. + Тепер, тримаючи телефон у ПРАВІЙ руці із зігнутим ліктем, торкайтеся неодноразово телефоном свого носа. + Продовжуйте торкатися телефоном свого носа протягом %1$d секунд + Готуйтеся махати, як це робить королева (вертячи запʼястком). + Готуйтеся махати телефоном у ЛІВІЙ руці, як це робить королева (вертячи запʼястком). + Готуйтеся махати телефоном у ПРАВІЙ руці, як це робить королева (вертячи запʼястком). + Продовжуйте виконувати махання королеви протягом %1$d секунд. + Тепер перекладіть телефон у ЛІВУ руку, і перейдіть до наступного завдання. + Тепер перекладіть телефон у ПРАВУ руку, і перейдіть до наступного завдання. + Перейдіть до наступного завдання. + Тест виконано. + Вам буде запропоновано виконати %1$s, сидячи з телефоном спочатку в одній руці, потім ще раз в іншій руці. + Я не можу виконати цю дію ЛІВОЮ рукою. + Я не можу виконати цю дію ПРАВОЮ рукою. + Я можу виконати цю дію обома руками. + + Не вдалося створити файл + Не вдалося вилучити достатньо файлів журналів, щоб досягти порогу + Помилка задання атрибуту + Файл не позначено як видалений (не позначено як відвантажений) + Кілька помилок під час вилучення журналів + Не знайдено жодних зібраних даних. + Не задано вихідний каталог + + Немає даних + + Назад + Ілюстрація %1$s + Спеціальне поле для підпису + Торкніть екран і рухайте пальцем, щоб поставити підпис + Підписано + Без підпису + Вибрано + Вибір скасовано + Повзунок відповіді. Діапазон від %1$s до %2$s + Зображення без мітки + Почати завдання + активна + правильно + неправильно + неактивний + Клітинка тесту на пам’ять + Перегляд знімку + Зроблений знімок + Перегляд знятого відео + Зняте відео + Неможливо розмістити диск розміру %1$s на диск розміру %2$s + Ціль + Вежа + Торкніть двічі, щоб розмістити диск + Торкніть двічі, щоб вибрати верхній диск + Має диски з розмірами %1$s + Пусто + Діапазон від %1$s до %2$s + Стос, створений із + і + Точка: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-v21/styles.xml b/backbone/src/main/res/values-v21/styles.xml new file mode 100644 index 000000000..4a9d176db --- /dev/null +++ b/backbone/src/main/res/values-v21/styles.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/backbone/src/main/res/values-vi/strings.xml b/backbone/src/main/res/values-vi/strings.xml new file mode 100644 index 000000000..1b1904e0f --- /dev/null +++ b/backbone/src/main/res/values-vi/strings.xml @@ -0,0 +1,396 @@ + + + + + Đồng ý + Tên + Họ + Bắt buộc + Xem lại + Hãy xem xét biểu mẫu bên dưới và chạm vào Đồng ý nếu bạn sẵn sàng tiếp tục. + Xem lại + Chữ ký + Vui lòng ký tên bằng ngón tay của bạn trên dòng bên dưới. + Ký vào đây + Trang %1$ld / %2$ld + + Chào mừng + Thu thập Dữ liệu + Quyền riêng tư + Sử dụng Dữ liệu + Khảo sát Nghiên cứu + Nhiệm vụ Nghiên cứu + Cam kết Thời gian + Rút lui + Tìm hiểu Thêm + + Tìm hiểu thêm về cách thu thập dữ liệu + Tìm hiểu thêm về cách sử dụng dữ liệu + Tìm hiểu thêm về cách bảo vệ quyền riêng tư và nhận dạng của bạn + Trước tiên, tìm hiểu thêm về nghiên cứu + Tìm hiểu thêm về khảo sát nghiên cứu + Tìm hiểu thêm về tác động của nghiên cứu đối với thời gian của bạn + Tìm hiểu thêm về các nhiệm vụ liên quan + Tìm hiểu thêm về việc rút lui + + Tùy chọn Chia sẻ + Chia sẻ dữ liệu của tôi với %1$s và các nhà nghiên cứu đủ chuyên môn trên thế giới + Chỉ chia sẻ dữ liệu của tôi với %1$s + %1$s sẽ nhận được dữ liệu nghiên cứu của bạn từ quá trình bạn tham gia nghiên cứu này.\n\nViệc chia sẻ rộng rãi hơn dữ liệu nghiên cứu được mã hóa của bạn (không có thông tin như tên bạn) có thể giúp ích cho nghiên cứu này và các công trình trong tương lai. + Tìm hiểu thêm về chia sẻ dữ liệu + + Tên của %1$s (chữ in hoa) + Chữ ký của %1$s + Ngày + + Bước %1$s / %2$s + + Giá trị không hợp lệ + %1$s vượt quá giá trị tối đa được phép (%2$s). + %1$s nhỏ hơn giá trị tối thiểu được phép (%2$s). + %1$s không phải giá trị hợp lệ. + + Địa chỉ email không hợp lệ: %1$s + + Nhập một địa chỉ + Không thể Tìm thấy Địa chỉ Được chỉ định + Không thể xử lý vị trí hiện tại của bạn. Vui lòng nhập địa chỉ hoặc di chuyển đến một vị trí có tín hiệu GPS tốt hơn nếu có thể. + Truy cập vào dịch vụ định vị này đã bị từ chối. Vui lòng cấp quyền cho ứng dụng này sử dụng dịch vụ định vị qua phần Cài đặt. + Không thể tìm thấy kết quả cho địa chỉ đã nhập. Vui lòng đảm bảo địa chỉ này hợp lệ. + Bạn không được kết nối vào internet hoặc bạn đã vượt quá số lượng yêu cầu tra cứu địa chỉ tối đa. Nếu bạn không được kết nối vào internet, vui lòng bật Wi-Fi để trả lời câu hỏi này, bỏ qua câu hỏi này nếu có nút bỏ qua hoặc quay trở lại bản khảo sát khi bạn được kết nối vào internet. Nếu không, vui lòng thử lại sau một vài phút nữa. + + Nội dung văn bản vượt quá độ dài tối đa: %1$s + + Camera không có sẵn trong chế độ màn hình tách rời. + + Email + jappleseed@example.com + Mật khẩu + Nhập mật khẩu + Xác nhận + Nhập lại mật khẩu + Các mật khẩu không khớp. + Thông tin Bổ sung + John + Appleseed + Giới tính + Chọn giới tính + Ngày sinh + Chọn một ngày + + Xác minh + Xác minh Email của bạn + Chạm vào liên kết bên dưới nếu bạn không nhận được email xác minh và muốn được gửi lại. + Gửi lại Email Xác minh + + Đăng nhập + Quên mật khẩu? + + Nhập mã khóa + Xác nhận mã khóa + Đã lưu mã khóa + Đã xác thực mã khóa + Nhập mã khóa cũ của bạn + Nhập mã khóa mới của bạn + Xác nhận mã khóa mới của bạn + Mã khóa Không đúng + Các mã khóa không khớp. Thử lại. + Vui lòng xác thực bằng Touch ID + Lỗi Touch ID + Chỉ cho phép các ký tự số. + Chỉ báo tiến trình nhập mã khóa + Đã nhập %1$s / %2$s số + Bạn quên mật mã? + + Không thể thêm mục Chuỗi khóa. + Không thể cập nhật mục Chuỗi khóa. + Không thể xóa mục Chuỗi khóa. + Không thể tìm mục Chuỗi khóa. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Nữ + Nam + Khác + + Không + + + cm + ft + in + + Chạm để trả lời + Chọn một câu trả lời + Chạm để chọn + Chạm để viết + + Đồng ý + Hủy + OK + Xóa + Không đồng ý + Xong + Bắt đầu + Tìm hiểu thêm + Tiếp + Bỏ qua + Bỏ qua câu hỏi này + Bắt đầu Hẹn giờ + Lưu để Sau này + Hủy bỏ Kết quả + Kết thúc Nhiệm vụ + Lưu + Xóa câu trả lời + Không thể sửa đổi câu trả lời này. + + Bắt đầu hoạt động sau + Hoạt động Hoàn thành + Dữ liệu của bạn sẽ được phân tích và bạn sẽ được thông báo khi có kết quả. + Còn lại %1$s giây. + + Chụp Ảnh + Chụp lại Ảnh + Không tìm thấy camera nào. Không thể hoàn thành bước này. + Để hoàn thành bước này, hãy cho phép ứng dụng này truy cập camera trong Cài đặt. + Không có thư mục đầu ra nào được chỉ định cho hình ảnh được chụp. + Không thể lưu hình ảnh đã chụp. + + Bắt đầu Ghi + Dừng Ghi + Quay lại Video + + Thể dục + Quãng đường (%1$s) + Nhịp tim (nhịp/phút) + Ngồi thoải mái trong %1$s. + Đi bộ với tốc độ nhanh nhất có thể trong %1$s. + Hoạt động này sẽ theo dõi nhịp tim của bạn và tính quãng đường bạn có thể đi bộ trong %1$s. + Đi bộ ngoài trời với tốc độ cao nhất có thể của bạn trong %1$s. Khi bạn hoàn tất, hãy ngồi và nghỉ ngơi thoải mái trong %2$s. Để bắt đầu, chạm Bắt đầu. + + Dáng đi và Thăng bằng + Hoạt động này sẽ đo khả năng thăng bằng và dáng đi của bạn khi bạn đi bộ và đứng yên. Không tiếp tục hoạt động nếu bạn không thể đi bộ an toàn mà không có sự hỗ trợ. + Tìm một nơi mà bạn có thể đi bộ an toàn, không cần có sự hỗ trợ, trong khoảng %1$d bước theo đường thẳng. + Cho điện thoại vào túi hoặc vào giỏ và làm theo lời hướng dẫn. + Bây giờ, đứng yên trong %1$s. + Đứng yên trong %1$s. + Quay đầu lại và đi bộ về nơi bạn bắt đầu. + Đi bộ tối đa %1$d bước theo đường thẳng. + + Tìm một nơi mà bạn có thể đi bộ an toàn về phía sau và phía trước theo đường thẳng. Cố gắng đi bộ liên tục bằng cách rẽ cuối đường, như thể bạn đang đi bộ dọc quanh hình nón.\n\nTiếp theo, bạn sẽ được hướng dẫn đi vòng hình tròn, sau đó đứng yên với hai cánh tay ở bên sườn và hai chân dang rộng tầm chiều rộng của vai. + Chạm Bắt đầu khi bạn đã sẵn sàng bắt đầu.\nSau đó, cho điện thoại vào túi hoặc vào giỏ và làm theo hướng dẫn. + Đi bộ về phía sau và phía trước theo đường thẳng trong %1$s. Đi bộ bình thường. + Xoay một vòng tròn rồi đứng yên trong %1$s. + Bạn đã hoàn thành hoạt động. + + Tốc độ Chạm + Tay Phải + Tay Trái + Hoạt động này sẽ đo tốc độ chạm của bạn. + Đặt điện thoại trên một bề mặt phẳng. + Sử dụng hai ngón tay trên cùng bàn tay bạn để luân phiên chạm vào các nút trên màn hình. + Sử dụng hai ngón tay trên bàn tay phải bạn để luân phiên chạm vào các nút trên màn hình. + Sử dụng hai ngón tay trên bàn tay trái bạn để luân phiên chạm vào các nút trên màn hình. + Bây giờ, hãy lặp lại bài kiểm tra tương tự bằng tay phải của bạn. + Bây giờ, hãy lặp lại bài kiểm tra tương tự bằng tay trái của bạn. + Chạm một ngón tay, sau đó chạm ngón còn lại. Cố gắng tính thời gian số lần chạm chẵn nhất có thể. Tiếp tục chạm trong %1$s. + Chạm vào Bắt đầu để bắt đầu. + Chạm Tiếp để bắt đầu. + Chạm + Tổng Lần chạm + Chạm vào các nút bằng hai ngón tay liên tục nhất có thể. + Chạm nút bằng tay PHẢI của bạn. + Chạm nút bằng tay TRÁI của bạn. + Bỏ qua tay này + + Giọng nói + Chạm vào Bắt đầu để bắt đầu. + Nói “Aaaaah” vào micrô với thời gian dài nhất mà bạn có thể. + Hít một hơi thật sâu và nói “Aaaaah” lâu hết mức có thể vào micrô. Giữ thật đều giọng nói để các thanh tiếng vẫn ở trong vùng màu lam. + Hoạt động này sẽ đánh giá giọng nói của bạn bằng cách ghi âm bằng micrô ở dưới đáy điện thoại. + Quá To + Không thể ghi âm + Vui lòng đợi trong khi chúng tôi kiểm tra độ ồn trong nền. + Độ ồn xung quanh quá lớn để ghi âm giọng nói của bạn. Vui lòng di chuyển đến nơi nào đó yên tĩnh hơn và thử lại. + Chạm Tiếp khi sẵn sàng. + + Trắc thính Âm báo + Hoạt động này giúp đánh giá khả năng nghe các âm thanh khác nhau của bạn. + Trước khi bạn bắt đầu, hãy cắm và đeo tai nghe. + Chạm vào Bắt đầu để bắt đầu. + Bây giờ, bạn sẽ nghe thấy âm báo. Điều chỉnh âm lượng bằng các bộ điều khiển ở bên hông thiết bị.\n\nChạm vào nút khi bạn sẵn sàng bắt đầu. + Chạm vào nút mỗi khi bạn bắt đầu nghe thấy âm thanh. + %1$s Hz, Trái + %1$s Hz, Phải + + Trí nhớ Không gian + Hoạt động này sẽ đánh giá trí nhớ không gian ngắn hạn của bạn bằng cách yêu cầu bạn lặp lại thứ tự sáng lên của %1$s. + những bông hoa + những bông hoa + Một số %1$s sẽ sáng lên lần lượt. Hãy chạm vào các %2$s này theo thứ tự sáng lên của chúng. + Một số %1$s sẽ sáng lên lần lượt. Hãy chạm vào các %2$s này theo thứ tự ngược với thứ tự sáng lên của chúng. + Để bắt đầu, chạm Bắt đầu, sau đó theo dõi chặt chẽ. + %1$s + Điểm + Theo dõi %1$s sáng lên + Chạm vào %1$s theo thứ tự chúng sáng lên + Chạm vào %1$s theo thứ tự ngược lại + Chuỗi Hoàn thành + Để tiếp tục, hãy chạm vào Tiếp + Thử lại + Lần trước bạn chưa thực sự thành công. Hãy chạm vào Tiếp để tiếp tục. + Hết Thời gian + Bạn đã hết thời gian.\nHãy chạm vào Tiếp để tiếp tục. + Hoàn tất Trò chơi + Đã tạm dừng + Để tiếp tục, hãy chạm vào Tiếp + + Thời gian Phản ứng + Hoạt động này giúp đánh giá thời gian bạn phản ứng trước gợi ý bằng hình ảnh. + Lắc thiết bị theo bất kỳ hướng nào ngay khi chấm màu lam xuất hiện trên màn hình. Bạn sẽ được yêu cầu làm việc này %D lần. + Chạm vào Bắt đầu để bắt đầu. + Lần thử %1$s / %2$s + Lắc nhanh thiết bị khi vòng tròn màu lam xuất hiện + + Tháp Hà Nội + Hoạt động này đánh giá các kỹ năng giải đố của bạn. + Di chuyển toàn bộ chồng đĩa đến bục được tô sáng với số lần di chuyển ít nhất có thể. + Chạm vào Bắt đầu để bắt đầu + Giải đố + Số lần Di chuyển: %1$s \n %2$s + Tôi không thể giải câu đố này + + Đi bộ Tính giờ + Hoạt động này đo lường chức năng chi dưới của bạn. + Tìm một địa điểm, tốt nhất là ngoài trời, mà bạn có thể đi bộ trong khoảng %1$s theo đường thẳng nhanh nhất có thể, nhưng an toàn. Không giảm tốc độ cho tới khi bạn đã vượt qua vạch đích. + Chạm Tiếp để bắt đầu. + Thiết bị hỗ trợ + Sử dụng cùng thiết bị hỗ trợ cho từng bài kiểm tra. + Bạn có đeo thiết bị chỉnh hình mắt cá chân không? + Bạn có sử dụng thiết bị hỗ trợ không? + Chạm vào đây để chọn một câu trả lời. + Không có + Gậy chống Đơn + Nạng Đơn + Gậy chống Đôi + Nạng Đôi + Khung tập đi/Xe lăn + Đi bộ tối đa %1$s theo đường thẳng. + Quay đầu lại và đi bộ về nơi bạn bắt đầu. + Chạm Xong khi hoàn tất. + + PASAT + PVSAT + PAVSAT + Bài Kiểm tra Tính tổng Tuần tự bằng Âm thanh Theo tốc độ đo lường tốc độ xử lý thông tin âm thanh cùng với khả năng tính toán của bạn. + Bài Kiểm tra Tính tổng Tuần tự bằng Hình ảnh Theo tốc độ đo lường tốc độ xử lý thông tin hình ảnh cùng với khả năng tính toán của bạn. + Bài Kiểm tra Tính tổng Tuần tự bằng Âm thanh và Hình ảnh Theo tốc độ đo lường tốc độ xử lý thông tin âm thanh và hình ảnh cùng với khả năng tính toán của bạn. + Các số duy nhất được đưa ra cứ %1$s giây một lần.\nBạn phải cộng từng số mới với số ngay trước nó.\nLưu ý: bạn không được tính tổng liên tục mà chỉ được tính tổng của hai số sau cùng. + Chạm vào Bắt đầu để bắt đầu. + Ghi nhớ số đầu tiên này. + Cộng số mới này với số trước đó. + - + + Kiểm tra Bảng %1$s Lỗ + Hoạt động này đánh giá chức năng phần thân trên của bạn bằng cách yêu cầu bạn đặt một miếng gỗ vào trong lỗ. Bạn sẽ được yêu cầu thực hiện việc này %1$s lần. + Cả tay trái và phải của bạn sẽ được kiểm tra.\nBạn phải nhặt miếng gỗ nhanh nhất có thể, đặt vào trong lỗ, và khi đã thực hiện xong %1$s lần, hãy lấy ra %2$s lần nữa. + Cả tay phải và trái của bạn sẽ được kiểm tra.\nBạn phải nhặt miếng gỗ nhanh nhất có thể, đặt vào trong lỗ, và khi đã thực hiện xong %1$s lần, hãy lấy ra %2$s lần nữa. + Chạm vào Bắt đầu để bắt đầu. + Đặt miếng gỗ vào trong lỗ bằng tay trái của bạn. + Đặt miếng gỗ vào trong lỗ bằng tay phải của bạn. + Đặt miếng gỗ phía sau dòng kẻ bằng tay trái của bạn. + Đặt miếng gỗ phía sau dòng kẻ bằng tay phải của bạn. + Nhặt miếng gỗ bằng hai ngón tay. + Nhấc các ngón tay để thả rơi miếng gỗ. + + Hoạt động Rung + Hoạt động này sẽ đo độ rung của các tay bạn ở những vị trí khác nhau. Tìm một địa điểm nơi bạn có thể ngồi thoải mái trong suốt hoạt động này. + Cầm điện thoại bằng tay thuận hơn như được hiển thị trong hình bên dưới. + Cầm điện thoại bằng tay PHẢI như được hiển thị trong hình bên dưới. + Cầm điện thoại bằng tay TRÁI như được hiển thị trong hình bên dưới. + Bạn sẽ được yêu cầu thực hiện %1$s trong khi ngồi với điện thoại trên tay của mình. + một nhiệm vụ + hai nhiệm vụ + ba nhiệm vụ + bốn nhiệm vụ + năm nhiệm vụ + Chạm tiếp để tiếp tục. + Chuẩn bị giữ điện thoại trong vạt áo của bạn. + Chuẩn bị giữ điện thoại trong vạt áo bằng tay TRÁI của bạn. + Chuẩn bị giữ điện thoại trong vạt áo bằng tay PHẢI của bạn. + Tiếp tục giữ điện thoại trong vạt áo bạn trong %1$d giây. + Bây giờ, hãy cầm điện thoại với tay của bạn duỗi ngang vai. + Bây giờ, hãy cầm điện thoại với tay TRÁI của bạn duỗi ngang vai. + Bây giờ, hãy cầm điện thoại với tay PHẢI của bạn duỗi ngang vai. + Tiếp tục cầm điện thoại với tay của bạn duỗi thẳng trong %1$d giây. + Bây giờ, hãy cầm điện thoại cao bằng vai cùng khủy tay bị gập. + Bây giờ, hãy cầm điện thoại với tay TRÁI của bạn cao bằng vai cùng khủy tay bị gập. + Bây giờ, hãy cầm điện thoại với tay PHẢI của bạn cao bằng vai cùng khủy tay bị gập. + Tiếp tục cầm điện thoại với khuỷu tay của bạn bị gập trong %1$d giây + Bây giờ, hãy gập khuỷu tay bạn, chạm điện thoại vào mũi nhiều lần. + Bây giờ, hãy giữ khuỷu tay bạn gập với điện thoại ở tay TRÁI, chạm điện thoại vào mũi nhiều lần. + Bây giờ, hãy gập khuỷu tay bạn với điện thoại ở tay PHẢI, chạm điện thoại vào mũi nhiều lần. + Tiếp tục chạm điện thoại vào mũi bạn trong %1$d giây + Chuẩn bị vẫy tay (vẫy bằng cách xoay cổ tay của bạn). + Chuẩn bị vẫy tay với điện thoại ở tay TRÁI (vẫy bằng cách xoay cổ tay của bạn). + Chuẩn bị vẫy tay với điện thoại ở tay PHẢI (vẫy bằng cách xoay cổ tay của bạn). + Tiếp tục thực hiện vẫy tay trong %1$d giây. + Bây giờ, hãy chuyển điện thoại sang tay TRÁI và tiếp tục nhiệm vụ tiếp theo. + Bây giờ, hãy chuyển điện thoại sang tay PHẢI và tiếp tục nhiệm vụ tiếp theo. + Tiếp tục đến nhiệm vụ tiếp theo. + Hoạt động đã hoàn thành. + Bạn sẽ được yêu cầu thực hiện %1$s trong khi ngồi với điện thoại trên một tay trước tiên, sau đó đặt lại lên tay còn lại. + Tôi không thể thực hiện hoạt động này bằng tay TRÁI. + Tôi không thể thực hiện hoạt động này bằng tay PHẢI. + Tôi có thể thực hiện hoạt động này bằng hai tay. + + Không thể tạo tệp + Không thể xóa đủ số tệp bản ghi để đạt tới ngưỡng + Lỗi khi đặt thuộc tính + Đã xóa tệp không được đánh dấu (không được đánh dấu là đã tải lên) + Nhiều lỗi trong khi xóa bản ghi + Không tìm thấy dữ liệu được thu thập. + Không có thư mục đầu ra nào được chỉ định + + Không có Dữ liệu + + Quay lại + Hình minh họa của %1$s + Trường chữ ký được chỉ định + Chạm vào màn hình và di chuyển ngón tay của bạn để ký + Đã ký + Chưa ký + Đã chọn + Đã bỏ chọn + Thanh trượt phản hồi. Phạm vi từ %1$s đến %2$s + Hình ảnh không có nhãn + Bắt đầu nhiệm vụ + hoạt động + đúng + sai + yên lặng + Ô trò chơi ghi nhớ + Chụp bản xem trước + Ảnh được chụp + Xem trước quay video + Video được quay + Không thể đặt đĩa có kích cỡ %1$s lên trên đĩa có kích cỡ %2$s + Đích + Tháp + Chạm hai lần để đặt đĩa + Chạm hai lần để chọn đĩa trên cùng + Có đĩa có kích cỡ %1$s + Trống + Trong khoảng từ %1$s đến %2$s + Ngăn xếp bao gồm + + Điểm: %1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-zh-rHK/strings.xml b/backbone/src/main/res/values-zh-rHK/strings.xml new file mode 100644 index 000000000..2144f128d --- /dev/null +++ b/backbone/src/main/res/values-zh-rHK/strings.xml @@ -0,0 +1,396 @@ + + + + + 同意書 + 名字 + 姓氏 + 必填 + 同意書檢閱 + 請檢閲以下的文件。如果你準備好繼續,請點一下「同意」。 + 同意書檢閱 + 簽名 + 請用手指在下方的橫線上簽名。 + 在此簽名 + 第 %1$ld 頁(共 %2$ld 頁) + + 歡迎 + 數據收集 + 私隱 + 數據使用 + 研究問卷 + 研究任務 + 所需時間 + 退出研究 + 了解更多 + + 進一步了解數據的收集方法 + 進一步了解數據的使用方法 + 進一步了解你的私隱及個人身份如何得到保護 + 開始前先了解此研究 + 進一步了解研究的問卷 + 進一步了解研究所花費的時間 + 進一步了解相關的任務 + 進一步了解退出研究 + + 分享選項 + 與「%1$s」及全世界合資格的研究者分享我的數據 + 只與「%1$s」分享我的數據 + 你參與是次研究後,「%1$s」將會接收到你的研究數據。\n\n廣泛分享編碼後的研究數據(而不包括姓名等資料)將會對本次及將來的研究工作有幫助。 + 進一步了解數據共享 + + %1$s 的姓名(正楷) + %1$s 的簽名 + 日期 + + 步驟 %1$s/%2$s + + 無效值 + %1$s 超過了上限(%2$s)。 + %1$s 低於下限(%2$s)。 + %1$s 不是有效的數值。 + + 無效的電郵地址:%1$s + + 輸入地址 + 找不到指定的地址 + 無法找出現時位置,請輸入地址或移動至 GPS 訊號較佳的位置(如適用)。 + 定位服務存取遭拒,請前往「設定」以授權此 App 使用定位服務。 + 找不到輸入的地址,請確定地址是否正確。 + 你可能未連接互聯網,或地址查詢請求的次數已超出上限。如果你未連接互聯網,請開啟 Wi-Fi 以回答此問題(如果顯示「略過」按鈕,則可略過此問題),或在連接互聯網後返回此問卷。否則,請在幾分鐘後再試一次。 + + 文字內容超出長度上限:%1$s + + 分割螢幕無法使用相機。 + + 電郵 + jappleseed@example.com + 密碼 + 輸入密碼 + 確認 + 請再次輸入密碼 + 密碼不符。 + 其他資料 + 大明 + + 性別 + 選擇性別 + 出生日期 + 選擇日期 + + 驗證 + 驗證你的電郵 + 如果你未收到驗證電郵,並希望系統再傳送一次,請點一下下方的連結。 + 重新傳送驗證電郵 + + 登入 + 忘記密碼? + + 輸入密碼 + 確認密碼 + 密碼已儲存 + 已認證密碼 + 輸入舊密碼 + 輸入新密碼 + 確認新密碼 + 密碼不正確 + 密碼不符,請再試一次。 + 請使用 Touch ID 認證 + Touch ID 錯誤 + 只允許輸入數字。 + 密碼輸入進度指示器 + 已輸入 %1$s 個數字(共 %2$s 個) + 忘記密碼? + + 無法加入「鑰匙圈」項目。 + 無法更新「鑰匙圈」項目。 + 無法刪除「鑰匙圈」項目。 + 找不到「鑰匙圈」項目。 + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + 女性 + 男性 + 其他 + + 不是 + + + 厘米 + + + + 點一下回答 + 選擇答案 + 點一下選擇 + 點一下編寫 + + 同意 + 取消 + + 清除 + 不同意 + 完成 + 開始 + 了解更多 + 下一步 + 略過 + 略過此問題 + 開始計時器 + 儲存作日後之用 + 放棄測試結果 + 結束測試任務 + 儲存 + 清除答案 + 無法修改此答案。 + + 測試活動倒數 + 測試活動已完成 + 測試活動將會進行分析。當得到結果時,你將會收到通知。 + 剩餘 %1$s 秒。 + + 截取圖案 + 重新截取圖案 + 找不到相機,無法完成此步驟。 + 如要完成此步驟,請前往「設定」以允許此 App 使用相機。 + 未指定截取圖案的輸出目錄。 + 無法儲存截取的圖案。 + + 開始錄音 + 停止錄影 + 重新截取影片 + + 體能 + 距離(%1$s) + 心跳率(每分鐘次數) + 安坐 %1$s。 + 用最快的速度步行 %1$s。 + 此測試活動能監測你的心跳率,並量度你在 %1$s內能步行的最遠距離。 + 在室外以盡可能最快的速度步行 %1$s。完成後,安坐並休息 %2$s。點一下「開始」以進行測試。 + + 步態及平衡 + 此測試活動可測量你步行及站立時的步態及平衡。如果你行動不便或行走時需要輔助,請勿繼續進行。 + 找一個安全的地方,讓你可以不靠輔助地直線步行大約 %1$d 步。 + 將電話放進口袋、手袋或背包裏,並跟隨語音指示。 + 現在站立 %1$s。 + 站立 %1$s。 + 掉頭,然後步行回到起點。 + 直線步行最多 %1$d 步。 + + 找一個安全的地方,讓你可以直線來回步行。請嘗試不停步行,遇到盡頭時請有如繞過雪糕筒般掉頭。\n\n下一步,你將需要轉一整個圈,然後站着不動,雙手放在兩旁,而雙腳張開至肩膊的闊度。 + 如果你準備好開始,請點一下「開始」。\n然後將電話放進口袋、手袋或背包裏,並跟隨語音指示。 + 以平常姿態,直線來回步行 %1$s。 + 轉一個圈,然後站立 %1$s。 + 你已完成測試活動。 + + 點按速度 + 右手 + 左手 + 此測試活動能評估你的點按速度。 + 請先平放電話。 + 請用同一隻手的兩指梅花間竹地點按螢幕上的按鈕。 + 請用右手的兩指梅花間竹地點按螢幕上的按鈕。 + 請用左手的兩指梅花間竹地點按螢幕上的按鈕。 + 現在用右手進行相同的測試。 + 現在用左手進行相同的測試。 + 先用一指點按,然後用另一指點按。請嘗試以最平穩的速度,持續點按 %1$s。 + 點一下「開始」以進行測試。 + 點一下「下一步」開始。 + 點按 + 點按總次數 + 用兩指以最平穩的速度點按這些按鈕。 + 用你的右手點按這些按鈕。 + 用你的左手點按這些按鈕。 + 略過這隻手 + + 語音 + 點一下「開始」以進行測試。 + 對着咪高風發出「呀─」的聲音,時間愈長愈好。 + 深呼吸,然後對着咪高風發出「呀─」的聲音,時間愈長愈好。請保持穩定的音量,以使音量棒圖維持藍色。 + 此測試活動會使用電話底部的咪高風錄音,以評估你的聲音。 + 太大聲 + 無法錄音 + 正在檢查背景雜音音量,請稍候。 + 環境雜音太大聲,無法錄取你的聲音。請前往較靜的地方,然後再試一次。 + 準備好後,請點一下「下一步」。 + + 純音聽力檢查 + 此測試活動能評估你聆聽不同聲音的能力。 + 在開始之前,請插入並戴上耳筒。 + 點一下「開始」以進行測試。 + 你現在應該聽到一個音頻。請使用裝置旁的音量按鈕以調整音量。\n\n如果你準備好開始,請點一下按鈕。 + 每次當你開始聽到聲音時,請點一下按鈕。 + %1$s Hz,左 + %1$s Hz,右 + + 空間記憶 + 此測試活動需要你重覆%1$s圖案亮起的次序,以測量你的短期空間記憶。 + 花朵 + 花朵 + 部分%1$s圖案會逐個亮起。請以亮起的次序點按這些%2$s圖案。 + 部分%1$s圖案會逐個亮起。請以亮起的相反次序點按這些%2$s圖案。 + 點一下「開始」以進行測試,然後細心觀看。 + %1$s + 分數 + 看亮起的%1$s + 以剛才亮起的順序點按%1$s圖案 + 按相反次序點按%1$s圖案 + 已完成順序 + 如要繼續,點一下「下一步」 + 再試一次 + 你似乎記錯了順序。點一下「下一步」繼續。 + 時間到了 + 時間到了。\n點一下「下一步」繼續。 + 遊戲結束 + 已暫停 + 如要繼續,點一下「下一步」 + + 反應時間 + 此測試活動會評估你對視覺提示作出反應所需的時間。 + 當螢幕顯示藍色圓點時,請立刻以任何方向搖動裝置。你將需要執行此操作 %D 次。 + 點一下「開始」以進行測試。 + 第 %1$s/%2$s 次嘗試 + 當藍色圓圈出現時,請快速搖動裝置 + + 河內塔 + 此測試活動會評估你的解題能力。 + 用最少的次數將所有圓盤移到有顏色標示的平台。 + 點一下「開始」以進行測試 + 解開謎題 + 移動次數:%1$s \n %2$s + 我無法解開此謎題 + + 定時步行 + 此測試活動可測量你的下肢功能。 + 找一個安全的地方(最好在室外),讓你可以快速地直線步行大約 %1$s。通過終點線前請勿放慢速度。 + 點一下「下一步」開始。 + 輔助器材 + 每次測試時請使用相同的輔助器材。 + 你是否穿戴足踝矯形器? + 你是否使用輔助器材? + 點一下此處以選擇答案。 + + 單邊手杖 + 單邊拐杖 + 雙邊手杖 + 雙邊拐杖 + 助行架或四輪助行架 + 直線步行最多 %1$s。 + 掉頭,然後步行回到起點。 + 完成後,請點一下「完成」。 + + PASAT + PVSAT + PAVSAT + 定時聽覺連續加法測試(PASAT)可測量你的聽覺資料處理速度及計算能力。 + 定時視覺連續加法測試(PVSAT)可測量你的視覺資料處理速度及計算能力。 + 定時聽覺及視覺連續加法測試(PAVSAT)可測量你的聽覺及視覺資料處理速度及計算能力。 + 每 %1$s 秒會顯示一個數字。\n你必須將新的數字與前一個數字相加。\n注意:請勿計算所有的數字總和,只需計算最後兩個數字的總和。 + 點一下「開始」以進行測試。 + 記住第一個數字。 + 將這個新的數字與前一個數字相加。 + - + + %1$s 孔柱測試 + 此測試活動需要你將圓柱放進洞內,以測量你的上肢功能。你將需要執行此操作 %1$s 次。 + 測試活動將會測試你的左手及右手。\n你必須以最快的速度拿起圓柱並放進洞中,完成 %1$s 次後需要拿走 %2$s 次。 + 測試活動將會測試你的右手及左手。\n你必須以最快的速度拿起圓柱並放進洞中,完成 %1$s 次後需要拿走 %2$s 次。 + 點一下「開始」以進行測試。 + 請用左手將圓柱放進洞中。 + 請用右手將圓柱放進洞中。 + 請用左手將圓柱放進線後。 + 請用右手將圓柱放進線後。 + 用兩指拿起圓柱。 + 放開手指以放下圓柱。 + + 顫抖測試活動 + 此測試活動可測量你雙手不同位置的顫抖情況。請找一個地方,讓你可以在此測試活動期間舒適坐下。 + 請用較受影響的手拿着電話,如下圖所示。 + 請用右手拿着電話,如下圖所示。 + 請用左手拿着電話,如下圖所示。 + 你將需要坐着,並拿着電話進行%1$s。 + 一個任務 + 兩個任務 + 三個任務 + 四個任務 + 五個任務 + 點一下「下一步」繼續。 + 請準備將手放在大腿上,然後拿着電話。 + 請準備將左手放在大腿上,然後拿着電話。 + 請準備將右手放在大腿上,然後拿着電話。 + 將手放在大腿上,然後拿着電話 %1$d 秒。 + 現在伸直手臂,並將電話拿到肩膊的高度。 + 現在伸直左手手臂,並將電話拿到肩膊的高度。 + 現在伸直右手手臂,並將電話拿到肩膊的高度。 + 伸直手臂,然後拿着電話 %1$d 秒。 + 現在彎曲手肘,並將電話拿到肩膊的高度。 + 現在彎曲左手手肘,並將電話拿到肩膊的高度。 + 現在彎曲右手手肘,並將電話拿到肩膊的高度。 + 彎曲手肘,然後拿着電話 %1$d 秒 + 現在繼續彎曲手肘,並用電話重複觸碰鼻子。 + 現在繼續彎曲左手手肘,並拿着電話重複觸碰鼻子。 + 現在繼續彎曲右手手肘,並拿着電話重複觸碰鼻子。 + 拿着電話觸碰鼻子 %1$d 秒 + 請準備進行揮手動作(只揮動手腕)。 + 請準備用左手拿着電話,進行揮手動作(只揮動手腕)。 + 請準備用右手拿着電話,進行揮手動作(只揮動手腕)。 + 繼續進行揮手動作 %1$d 秒。 + 現在用左手拿着電話,並繼續進行下一個任務。 + 現在用右手拿着電話,並繼續進行下一個任務。 + 繼續進行下一個任務。 + 測試活動已完成。 + 你將需要坐着,先用一隻手拿着電話進行%1$s,然後再用另一隻手進行。 + 我無法用左手進行此測試活動。 + 我無法用右手進行此測試活動。 + 我可以用雙手進行此測試活動。 + + 無法製作檔案 + 無法移除足夠的記錄檔案以達到臨界值 + 設定屬性錯誤 + 檔案未標示為已刪除(未標示為已上載) + 移除記錄檔時發生多個錯誤 + 找不到已收集的數據。 + 未指定輸出目錄 + + 沒有數據 + + 上一步 + %1$s圖案 + 指定簽名欄位 + 觸碰螢幕並移動手指簽名 + 已簽名 + 未簽名 + 已選擇 + 未選擇 + 回應滑桿。範圍由 %1$s 至 %2$s + 未標記的圖案 + 開始任務 + 動態 + 正確 + 不正確 + 靜態 + 記憶遊戲圖樣 + 截取預覽 + 截取的圖案 + 截取影片預覽 + 截取的影片 + 無法將 %1$s 號的圓盤放在 %2$s 號的圓盤上 + 目標 + 塔座 + 點兩下放置圓盤 + 點兩下選擇最上方的圓盤 + 其中的圓盤大小是 %1$s + 空白 + 範圍介乎 %1$s 至 %2$s + 圓盤柱的組成方式: + + 點數:%1$d + + \ No newline at end of file diff --git a/backbone/src/main/res/values-zh/strings.xml b/backbone/src/main/res/values-zh/strings.xml new file mode 100644 index 000000000..8334ff2f8 --- /dev/null +++ b/backbone/src/main/res/values-zh/strings.xml @@ -0,0 +1,398 @@ + + + + + + + 同意 + + + 必填 + 检查 + 请检查下方的表单,确认后请轻点“同意”。 + 检查 + 签名 + 请用手指在下方的横线上签名。 + 在此签名 + 第 %1$ld/%2$ld 页 + + 欢迎使用 + 数据收集 + 隐私权 + 数据使用 + 研究调查 + 研究任务 + 时间保证 + 退出研究 + 了解更多 + + 了解有关数据收集方式的更多信息 + 了解有关数据使用方式的更多信息 + 了解有关隐私和身份保护方式的更多信息 + 首先了解有关研究的更多信息 + 了解有关研究调查的更多信息 + 了解有关研究所占用时间的更多信息 + 了解有关所涉及任务的更多信息 + 了解有关退出研究的更多信息 + + 共享选项 + 与“%1$s”及全世界符合资格的研究者共享我的数据 + 仅与“%1$s”共享我的数据 + “%1$s”将从您参与的此研究中接收研究数据。\n\n更广泛地共享经加密的研究数据(不包含诸如姓名的信息)可能有助于本次以及未来的研究。 + 了解有关数据共享的更多信息 + + %1$s的姓名(正楷书写) + %1$s的签名 + 日期 + + 第 %1$s/%2$s 步 + + 无效值 + %1$s 超出了允许的最大值 (%2$s)。 + %1$s 低于允许的最小值 (%2$s)。 + %1$s 不是有效值。 + + 无效的电子邮件地址:%1$s + + 输入地址 + 无法找到指定地址 + 未能解析您的当前位置。请键入地址或转移到 GPS 信号较好的位置(若条件允许)。 + 访问位置服务已被拒绝。请通过“设置”来授权此应用使用位置服务。 + 未能找到所输入地址的相关结果。请确定该地址是否有效。 + 您未连接到互联网,或已超出地址查询请求的次数上限。如果未连接到互联网,请打开您的 Wi-Fi 以回答此问题,跳过按钮可用时可跳过此问题,或在连接到互联网后继续参与该调查。如果超出请求次数上限,请等待几分钟后再次尝试。 + + 文本内容超过最大长度:%1$s + + 相机在拆分屏幕中不可用。 + + 电子邮件 + jappleseed@example.com + 密码 + 输入密码 + 确认 + 再次输入密码 + 密码不匹配。 + 其他信息 + John + Appleseed + 性别 + 选择性别 + 出生日期 + 选择日期 + + 验证 + 验证您的电子邮件 + 如果您未收到验证电子邮件并希望再收一次,请轻点下方的链接。 + 重新发送验证电子邮件 + + 登录 + 忘记了密码? + + 输入口令 + 确认口令 + 口令已存储 + 口令已鉴定 + 输入旧口令 + 输入新口令 + 确认新口令 + 口令不正确 + 口令不匹配。请再试一次。 + 请使用 Touch ID 进行鉴定 + Touch ID 错误 + 仅允许数字字符。 + 口令输入进度指示器 + 已输入 %1$s/%2$s 位 + 忘记了密码? + + 无法添加钥匙串项。 + 无法更新钥匙串项。 + 无法删除钥匙串项。 + 无法找到钥匙串项。 + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + + + 其他 + + + + + 厘米 + 英尺 + 英寸 + + 轻点来回答 + 选择一个答案 + 轻点来选择 + 轻点来书写 + + 同意 + 取消 + + 清除 + 不同意 + 完成 + 开始 + 了解更多 + 下一步 + 跳过 + 跳过此问题 + 启动定时器 + 存储以稍后查看 + 放弃结果 + 结束任务 + 存储 + 清除答案 + 此答案无法修改。 + + 即将开始活动 + 活动完成 + 将对您的数据进行分析并在结果可用时通知您。 + 还剩 %1$s 秒钟。 + + 捕捉图像 + 重新捕捉图像 + 未找到相机。此步骤无法完成。 + 为了完成此步骤,请在“设置”中允许本应用程序访问相机。 + 未指定捕捉图像的输出目录。 + 无法存储捕捉的图像。 + + 开始录制 + 停止录制 + 重新采集视频 + + 健身 + 距离(%1$s) + 心率(次/分) + 请静坐 %1$s。 + 以最快速度步行 %1$s。 + 本活动监控您的心率并测量 %1$s内的步行距离。 + 请在室外以最快的速度步行 %1$s。步行完成后,请静坐 %2$s。若要开始,请轻点“开始”。 + + 步态和平衡 + 本活动测量您在步行和站立时的步态和平衡。请仅在步行安全区域(无外力辅助条件下)进行。 + 请在步行安全区域(无外力辅助条件下)直线行走大约 %1$d 步。 + 请将手机放入口袋或背包中,然后跟随音频指示操作。 + 现在请站立 %1$s。 + 请站立 %1$s。 + 转身并走回起点。 + 请直线步行最多 %1$d 步。 + + 找到一处您可以沿直线安全往返的地方。在路线的尽头转弯,不要停下来,就好像绕着交通锥转圈一样。\n\n接下来会要求您转一圈,然后站立不动,双手放在两侧,双脚张开,与肩同宽。 + 准备就绪后,轻点“开始”。\n将手机放入口袋或背包中,然后跟随音频指示操作。 + 沿直线往返 %1$s。尽可能跟平时一样走动。 + 走完一圈后站立不动,坚持 %1$s。 + 您已经完成了本活动。 + + 轻点速度 + 右手 + 左手 + 本活动测量您的轻点速度。 + 把手机放在平面上。 + 用同一只手上的两个手指来轮流轻点屏幕上的按钮。 + 用右手上的两个手指来轮流轻点屏幕上的按钮。 + 用左手上的两个手指来轮流轻点屏幕上的按钮。 + 现在用右手重复相同的测试。 + 现在用左手重复相同的测试。 + 用一个手指轻点,然后用另一个手指轻点。尽量保持均匀的轻点间隔。持续轻点 %1$s。 + 轻点“开始”来进行测试。 + 轻点“下一步”来开始。 + 轻点 + 轻点总次数 + 用两个手指尽可能以均匀的速度轻点按钮。 + 使用右手轻点按钮。 + 使用左手轻点按钮。 + 跳过这只手 + + 嗓音 + 轻点“开始”来进行测试。 + 请对着麦克风说“啊啊啊”,时间越长越好。 + 请深呼吸并对着麦克风说“啊啊啊”,时间越长越好。保持平稳的声音大小以使音频条保持蓝色。 + 本活动通过将您的嗓音录制到手机底部的麦克风来对其进行评估。 + 太大声 + 无法录音 + 请耐心等待,我们正在检查背景噪声级。 + 环境噪声级太高,无法录制您的声音。请移至较为安静的地方,然后再试一次。 + 就绪后,轻点“下一步”。 + + 纯音测听 + 本活动测量您是否可听见不同的声音。 + 请在开始前插入并戴上耳机。 + 轻点“开始”来进行测试。 + 现在您应该听到一个音调。使用设备一侧的控制来调整音量。\n\n请在准备好后轻点该按钮来开始测试。 + 请在每次开始听到声音时轻点该按钮。 + %1$s Hz,左 + %1$s Hz,右 + + 空间记忆 + 本活动通过重现%1$s亮起的顺序来测量您的短期空间记忆能力。 + 花朵 + 花朵 + 部分%1$s一次只亮起一个。请以亮起的顺序轻点这些%2$s。 + 部分%1$s一次只亮起一个。请以与亮起相反的顺序轻点这些%2$s。 + 若要开始,请轻点“开始”,然后留心观察。 + %1$s + 得分 + 观察亮起的%1$s + 请以%1$s亮起的顺序轻点它们 + 请以相反的顺序轻点%1$s + 顺序完成 + 轻点“下一步”来继续 + 再试一次 + 您好像记错了顺序。轻点“下一步”来继续。 + 时间已到 + 时间已到。\n轻点“下一步”来继续。 + 游戏完成 + 暂停 + 轻点“下一步”来继续 + + 反应时间 + 本活动评估您对视觉提示的反应时间。 + 屏幕出现蓝色圆点时立刻朝任意方向摇动设备。您将需要完成此操作 %D 次。 + 轻点“开始”来进行测试。 + 第 %1$s/%2$s 次尝试 + 蓝色圆圈出现时快速摇动设备 + + 汉诺塔 + 本活动评估您的解谜能力。 + 将整个圆盘堆以尽可能少的步数移到高亮显示的平台。 + 轻点“开始”来进行测试 + 解谜 + 移动次数:%1$s \n %2$s + 我解不开这个谜 + + 计时步行 + 本活动测量您的下肢功能。 + 找到一处地点,最好是户外,能够安全地以最快速度直线步行约 %1$s。请不要放慢脚步,直到跨过终点线。 + 轻点“下一步”来开始。 + 辅助设备 + 在每次测试中使用相同的辅助设备。 + 您是否穿戴了踝足矫形器? + 您是否使用了辅助设备? + 轻点此处来选择答案。 + + 单杖 + 单拐 + 双杖 + 双拐 + 轮椅/助行车 + 直线步行最多 %1$s。 + 转身并走回起点。 + 完成后,轻点“完成”。 + + PASAT + PVSAT + PAVSAT + “步进式听觉累加实验”测量您的听觉信息处理速度和计算能力。 + “步进式视觉累加实验”测量您的视觉信息处理速度和计算能力。 + “步进式听觉和视觉累加实验”测量您的听觉和视觉信息处理速度和计算能力。 + 每隔 %1$s 秒钟会显示一个一位数。\n您必须将每个新数字与前面相邻的数字相加。\n注意,您不准计算累加值,只能计算最后两个数字之和。 + 轻点“开始”来进行测试。 + 记住这第一个数字。 + 将这个新数字与前一个数字相加。 + - + + %1$s 孔插棒测试 + 本活动通过将圆饼放入圆孔来测量您的上肢功能。您将需要完成此操作 %1$s 次。 + 您的左手和右手都需要进行测试。\n您必须以最快速度拿起圆饼,将其放入圆孔中,完成 %1$s 次后,再将其移除 %2$s 次。 + 您的右手和左手都需要进行测试。\n您必须以最快速度拿起圆饼,将其放入圆孔中,完成 %1$s 次后,再将其移除 %2$s 次。 + 轻点“开始”来进行测试。 + 用您的左手将圆饼放入圆孔中。 + 用您的右手将圆饼放入圆孔中。 + 用您的左手将圆饼放在线条后方。 + 用您的右手将圆饼放在线条后方。 + 用两个手指拿起圆饼。 + 抬起手指以放下圆饼。 + + 颤抖活动 + 本活动测试在不同姿势下,您双手的颤抖情况。请找到一处舒适的地方坐下并完成此活动。 + 如下图所示,用更易受到影响的手握住手机。 + 如下图所示,用右手握住手机。 + 如下图所示,用左手握住手机。 + 请坐下并用手握住手机,然后完成%1$s。 + 一个任务 + 两个任务 + 三个任务 + 四个任务 + 五个任务 + 轻点下一步来继续。 + 准备握住手机并放在膝盖上。 + 准备用左手握住手机并放在膝盖上。 + 准备用右手握住手机并放在膝盖上。 + 握住手机并放在膝盖上,坚持 %1$d 秒钟。 + 现在伸直手臂并将手机举至肩高。 + 现在伸直左手臂并将手机举至肩高。 + 现在伸直右手臂并将手机举至肩高。 + 握住手机并伸直手臂,坚持 %1$d 秒钟。 + 现在把手机举至肩高并弯曲手肘。 + 现在用左手握住手机,举至肩高并弯曲手肘。 + 现在用右手握住手机,举至肩高并弯曲手肘。 + 握住手机并弯曲手肘,坚持 %1$d 秒钟 + 现在弯曲手肘,重复用手机接触您的鼻子。 + 现在弯曲手肘并用左手握住手机,重复用手机接触您的鼻子。 + 现在弯曲手肘并用右手握住手机,重复用手机接触您的鼻子。 + 用手机接触鼻子,坚持 %1$d 秒钟 + 准备挥手(通过转动手腕来挥手)。 + 用左手握住手机并准备挥手(通过转动手腕来挥手)。 + 用右手握住手机并准备挥手(通过转动手腕来挥手)。 + 持续挥手,坚持 %1$d 秒钟。 + 现在换左手握住手机并继续下一个任务。 + 现在换右手握住手机并继续下一个任务。 + 继续下一个任务。 + 活动完成。 + 请坐下并用一只手握住手机,然后完成%1$s,接着换另一只手再完成一次。 + 我不能用左手完成本活动。 + 我不能用右手完成本活动。 + 我能用两只手完成本活动。 + + 无法创建文件 + 无法移除足够的日志文件来达到阈值 + 设定属性时出错 + 文件未标记为删除(未标记为上传) + 移除日志时出现多个错误 + 未找到任何已收集的数据。 + 未指定输出目录 + + 无数据 + + 上一步 + “%1$s”插图 + 指定签名栏 + 触摸屏幕并移动手指来签名 + 已签名 + 未签名 + 已选择 + 未选择 + 响应滑块。范围:%1$s ~ %2$s + 无标签图像 + 开始任务 + 活跃 + 正确 + 不正确 + 不活跃 + 记忆游戏拼贴 + 捕捉预览 + 捕捉的图像 + 视频采集预览 + 采集的视频 + 无法将大小为“%1$s”的圆盘放在大小为“%2$s”的圆盘上 + 目标圆盘 + + 轻点两下来放置圆盘 + 轻点两下来选择最顶端的圆盘 + 包含圆盘,大小为“%1$s” + + 范围:%1$s ~ %2$s + 圆盘堆组成: + + 分数:%1$d + + \ No newline at end of file diff --git a/skin/src/main/res/values/arrays.xml b/backbone/src/main/res/values/arrays.xml similarity index 77% rename from skin/src/main/res/values/arrays.xml rename to backbone/src/main/res/values/arrays.xml index 06ea6928d..2d14dcd25 100644 --- a/skin/src/main/res/values/arrays.xml +++ b/backbone/src/main/res/values/arrays.xml @@ -1,7 +1,7 @@ - + 1 minute 5 minutes 10 minutes @@ -10,7 +10,7 @@ 45 minutes - + 1 5 10 diff --git a/backbone/src/main/res/values/attrs.xml b/backbone/src/main/res/values/attrs.xml index 89d65f4ce..a5018c952 100644 --- a/backbone/src/main/res/values/attrs.xml +++ b/backbone/src/main/res/values/attrs.xml @@ -1,6 +1,10 @@ + + + + diff --git a/backbone/src/main/res/values/colors.xml b/backbone/src/main/res/values/colors.xml index 35770d03d..c70c72519 100644 --- a/backbone/src/main/res/values/colors.xml +++ b/backbone/src/main/res/values/colors.xml @@ -7,14 +7,62 @@ #000 #33000000 #66000000 + #FFFFFF #CCFFFFFF + #e5e5e5 #FF757575 #33757575 #EEEEEE #979797 - #e5e5e5 - #ffff5722 + @color/rsb_light_gray + #ffff5722 + #43D551 #2196f3 #662196f3 + + @color/rsb_red + @color/rsb_green + @color/rsb_warm_gray + + @color/rsb_white + @color/rsb_green + + @color/rsb_submit_bar_negative + @color/rsb_colorPrimary + + + @color/rsb_colorPrimary + @color/rsb_light_gray + + + @color/rsb_black + + + @color/rsb_colorPrimary + @color/rsb_black + + @color/rsb_white + + + @color/rsb_white + + + @color/rsb_white + + + @color/rsb_warm_gray + @color/rsb_colorPrimary + + #3f51b5 + #4caf50 + #009891 + #99000000 + #1d92f4 + + #FDB447 + #22AEF4 + #EA3A57 + #9240D3 + @color/rsb_light_gray diff --git a/backbone/src/main/res/values/dimens.xml b/backbone/src/main/res/values/dimens.xml index 861d46016..dcd334bdc 100644 --- a/backbone/src/main/res/values/dimens.xml +++ b/backbone/src/main/res/values/dimens.xml @@ -13,11 +13,39 @@ @dimen/rsb_item_size_small 1dp + 56dp + + 200dp + + 80dp + 48dp 16dp 20sp + 14sp + + 10dp + 40sp + + 80sp + + @dimen/rsb_text_size_title + + + 200dp + @dimen/rsb_step_layout_image_height + @dimen/rsb_step_layout_image_height + + 100dp + + + 2dp + 4dp + 4dp + 6dp + 24dp \ No newline at end of file diff --git a/backbone/src/main/res/values/integers.xml b/backbone/src/main/res/values/integers.xml index 0caf8d2cc..d0cfa8e79 100644 --- a/backbone/src/main/res/values/integers.xml +++ b/backbone/src/main/res/values/integers.xml @@ -1,4 +1,26 @@ 300 + 800 + + @integer/rsb_config_mediumAnimTime + @integer/rsb_config_longAnimTime + + + 2000 + 2500 + 1600 + + + 500 + + + 100 + @integer/rsb_sensor_frequency_default + @integer/rsb_sensor_frequency_default + @integer/rsb_sensor_frequency_default + + 16 + 100 + 100 \ No newline at end of file diff --git a/backbone/src/main/res/values/strings.xml b/backbone/src/main/res/values/strings.xml index f4500d602..eb8a40c51 100644 --- a/backbone/src/main/res/values/strings.xml +++ b/backbone/src/main/res/values/strings.xml @@ -28,6 +28,7 @@ Study Tasks Withdrawing Learn More + By agreeing you confirm that you read the consent and that you wish to take part in this research study. Learn more @@ -50,11 +51,7 @@ Sharing Options - Share my data with %1$s and qualified researchers - worldwide - Not sharing your data - Only share my data with %1$s
    @@ -103,22 +100,55 @@ Get Started Done Skip - next + Next What to Expect Name + Enter full name Email - Disagree + jappleseed@example.com + External ID + Enter external ID + Password + Enter password + Confirm + Enter password again + Date of Birth + Pick a date + Gender + Pick a gender + Male + Female + Other + Disagreersb_email Agree OK Cancel Yes No - Step %1$s of %2$s - Hours - Minutes + Not sure + Step %1$d of %2$d + Years + Months + Weeks + Days + Hours + Minutes + Seconds + More than %s ago + Less than %s ago + %s ago + Loading... + Forgot your Password? + Log out + Enable + meters + feet + Please check your network connection and try again. + Recording + Recording - Failed to load encrypted data, try again! + FailedP to load encrypted data, try again! Signature Invalid Invalid answer, try again! Invalid answer, date must be before %1$s @@ -130,7 +160,6 @@ You must be %1$s years or younger You must be at least %1$s years old - Enter your passcode Enter your %1$d-%2$s code to confirm your identity. @@ -139,6 +168,16 @@ character That code doesn’t match the one you entered, Try again! + + Protect your data + Use your device\'s fingerprint sensor to confirm your identity. + Touch sensor + Fingerprint not recognized. Try again + Fingerprint recognized + Device fingerprint has either changed or been removed. You must login again to continue. + A passcode is less secure; however, it may be more convenient given your usage requirements. + Use passcode + Enter a number %1$d-%2$d Enter a number %1$.2f-%2$.2f @@ -161,4 +200,596 @@ Can\'t play this video. OK + + + Change Passcode + Passcode + Choose a passcode + Enter a secure, %1$d-%2$s code to protect your data and log in + faster. + + Confirm your passcode + Enter your %1$d-%2$s code one more time to confirm. + That code doesn’t match the one you entered. Try again + or go back to choose a different %1$d-%2$s code. + + Forgot your Passcode? + Reset Passcode + In order to reset your passcode, you\'ll need to log out of the app completely and log back in using your email and password. + + + Permissions + Location + The app needs location permissions to show accurate + location-based data + Microphone + The app needs microphone recording permission to read accurate audio-based data + Notifications + The app needs your permission in order to show + notifications when you complete surveys. This permission is optional and can be enabled in + the App\'s Settings screen. + This activity is serving as an example of + how you would implement outside auth. In other words, this activity represents an 3rd party + auth process like ones found in Google+, Google Drive, or Google Fit. This example is simple + and the result only controls whether notifications will show .\n\nYou can still enable app + notifications via the settings screen. The button for this screen is in the upper right hand + corner of the main screen (gear icon). + Participation in the study requires some permissions + from your device. + + Please allow %1$s permissions to continue. + + Allow + Granted + Optional + + + GPS Required + The app needs GPS enabled to record accurate + location-based data + + + + Invalid Password + Passwords do not match + Invalid Email + Invalid Name + + + Confirm + Check your email + We’ve emailed %1$s with a link to confirm your account. After + visiting the link, come back and touch “continue” below. + + ENTER A DIFFERENT EMAIL + Resend + + Wrong email address? Tap here. + Once verified, tap below + Continue + Resend Verification Email + %1$s has send you a verification email at %2$s + + Please check your email to verify your account before + continuing + + Change + Change Email + To verify email + + + Login + Registration + Consent + Consent Quiz + Consent Review + Eligibility + Permissions + Verify Email + Profile + Onboarding Complete + Are you sure? + Discard Results + Enter a different email + + + Spread the Word + Help us spread the word + Please help us find study participants by sharing information about the study + Please override rsb_share_the_app_message string resource to write your own share message + Enter google play app url for string resource rsb_share_app_url + Share on Twitter + Share on Facebook + Share via SMS + Share via Email + facebook + Subject + + + If you need to skip this activity, then tap the \"Skip this activity\" link below. Otherwise, tap Next to begin. + Skip this activity + + + + + Consent + First Name + Last Name + Required + Review + Review the form below, and tap Agree if you\'re ready to continue. + Review + Signature + Please sign using your finger on the line below. + Sign Here + Page %1$ld of %2$ld + + Welcome + Data Gathering + Privacy + Data Use + Study Survey + Study Tasks + Time Commitment + Withdrawing + Learn More + + Learn more about how data is gathered + Learn more about how data is used + Learn more about how your privacy and identity are protected + Learn more about the study first + Learn more about the study survey + Learn more about the study\'s impact on your time + Learn more about the tasks involved + Learn more about withdrawing + + Sharing Options + Share my data with %1$s and qualified researchers worldwide + Only share my data with %1$s + %1$s will receive your study data from your participation in this study.\n\nSharing your coded study data more broadly (without information such as your name) may benefit this and future research. + Learn more about data sharing + + %1$s\'s Name (printed) + %1$s\'s Signature + Date + + Step %1$s of %2$s + + Invalid value + %1$s exceeds the maximum allowed value (%2$s). + %1$s is less than the minimum allowed value (%2$s). + %1$s is not a valid value. + + Invalid email address: %1$s + + Enter an address + Could not Find Specified Address + Unable to resolve your current location. Please type in an address or move to a location with better GPS signal if applicable. + Access to location services has been denied. Please grant this app permission to use location services through Settings. + Unable to find a result for the entered address. Please make sure the address is valid. + Either you are not connected to the internet or you have exceeded the maximum amount of address lookup requests. If you are not connected to the internet, please turn on your Wi-Fi to answer this question, skip this question if skip button is available, or come back to the survey when you are connected to the internet. Otherwise, please try again in a few minutes. + + Text content exceeding maximum length: %1$s + + Camera not available in split screen. + + Email + jappleseed@example.com + Password + Enter password + Confirm + Enter password again + Passwords do not match. + Additional Information + John + Appleseed + Gender + Pick a gender + Date of Birth + Pick a date + + Verification + Verify your Email + Tap on the link below if you did not receive a verification email and would like it sent again. + Resend Verification Email + + Login + Forgot password? + + Enter passcode + Confirm passcode + Passcode saved + Passcode authenticated + Enter your old passcode + Enter your new passcode + Confirm your new passcode + Passcode Incorrect + Passcodes did not match. Try again. + Please authenticate with Touch ID + Touch ID error + Only numeric characters allowed. + Passcode entry progress indicator + %1$s of %2$s digits entered + Forgot Passcode? + + Could not add Keychain item. + Could not update Keychain item. + Could not delete Keychain item. + Could not find Keychain item. + + A+ + A- + AB+ + AB- + B+ + B- + O+ + O- + + Type I + Type II + Type III + Type IV + Type V + Type VI + + Female + Male + Other + + No + Yes + + cm + ft + in + + left + right + + Tap to answer + Select an answer + Tap to select + Tap to write + + Agree + Cancel + OK + Clear + Disagree + Done + Get Started + Learn more + Next + Skip + Skip this question + Start Timer + Save for Later + Discard Results + End Task + Save + Clear answer + This answer cannot be modified. + Copyright + + Starting activity in + Activity Complete + Your data will be analyzed and you will be notified when your results are ready. + %1$s seconds remaining. + Touch anywhere to continue + + Capture Image + Recapture Image + No camera found.\u0020\u0020This step cannot be completed. + In order to complete this step, allow this app access to the camera in Settings. + No output directory was specified for captured images. + The captured image could not be saved. + + Start Recording + Stop Recording + Recapture Video + + Fitness + Distance (%1$s) + Heart Rate (bpm) + Sit comfortably for %1$s. + Walk as fast as you can for %1$s. + This activity monitors your heart rate and measures how far you can walk in %1$s. + Walk outdoors at your fastest possible pace for %1$s. When you\'re done, sit and rest comfortably for %2$s. To begin, tap Get Started. + + Gait and Balance + This activity measures your gait and balance as you walk and stand still. Do not continue if you cannot safely walk unassisted. + Find a place where you can safely walk unassisted for about %1$d steps in a straight line. + Put your phone in a pocket or bag and follow the audio instructions. + Now stand still for %1$s. + Stand still for %1$s. + Turn around, and walk back to where you started. + Walk up to %1$d steps in a straight line. + + Find a place where you can safely walk back and forth in a straight line. Try to walk continuously by turning at the ends of your path, as if you are walking around a cone.\n\nNext you will be instructed to turn around in a full circle, then stand still with your arms at your sides and your feet about shoulder-width apart. + Tap Get Started when you are ready to begin.\nThen place your phone in a pocket or bag and follow the audio instructions. + Walk back and forth in a straight line for %1$s. Walk as you would normally. + Turn in a full circle and then stand still for %1$s. + You have completed the activity. + + Tapping Speed + Right Hand + Left Hand + This activity measures your tapping speed. + Put your phone on a flat surface. + Use two fingers on the same hand to alternately tap the buttons on the screen. + Use two fingers on your right hand to alternately tap the buttons on the screen. + Use two fingers on your left hand to alternately tap the buttons on the screen. + Now repeat the same test using your right hand. + Now repeat the same test using your left hand. + Tap one finger, then the other. Try to time your taps to be as even as possible. Keep tapping for %1$s. + Tap Get Started to begin. + Tap Next to begin. + Tap + Total Taps + Tap the buttons as consistently as you can using two fingers. + Tap the buttons using your RIGHT hand. + Tap the buttons using your LEFT hand. + Skip this hand + + Voice + Tap Get Started to begin. + Say “Aaaaah” into the microphone for as long as you can. + Take a deep breath and say “Aaaaah” into the microphone for as long as you can. Keep a steady vocal volume so the audio bars remain blue. + This activity evaluates your voice by recording it with the microphone at the bottom of your phone. + Too Loud + Unable to record audio + Please wait while we check the background noise level. + The ambient noise level is too loud to record your voice. Please move somewhere quieter and try again. + Tap Next when ready. + + Tone Audiometry + This activity measures your ability to hear different sounds. + Before you begin, plug in and put your headphones on. + Tap Get Started to begin. + You should now hear a tone. Adjust the volume using the controls on the side of your device.\n\nTap the button when you are ready to begin. + Tap the button every time you start hearing a sound. + %1$s Hz, Left + %1$s Hz, Right + + Spatial Memory + This activity measures your short-term spatial memory by asking you to repeat the order in which %1$s light up. + flowers + flowers + Some of the %1$s will light up one at a time. Tap those %2$s in the same order they lit up. + Some of the %1$s will light up one at a time. Tap those %2$s in the reverse of the order they lit up. + To begin, tap Get Started, then watch closely. + %1$s + Score + Watch the %1$s light up + Tap the %1$s in the order they lit up + Tap the %1$s in reverse order + Sequence Complete + To continue, tap Next + Try Again + You didn\'t quite make it through that time. Tap Next to continue. + Time\'s Up + You ran out of time.\nTap Next to continue. + Game Complete + Paused + To continue, tap Next + The memory activity was developed with assistance from Katherine Possin, PhD and Joel Kramer, PhD from University of California, San Francisco. + + Reaction Time + This activity evaluates the time it takes for you to respond to a visual cue. + Shake the device in any direction as soon as the blue dot appears on screen. You will be asked to do this %D times. + Tap Get Started to begin. + Attempt %1$s of %2$s + Quickly shake the device when the blue circle appears + + Tower of Hanoi + This activity evaluates your puzzle solving skills. + Move the entire stack to the highlighted platform in as few moves as possible. + Tap Get Started to begin + Solve the Puzzle + Number of Moves: %1$s \n %2$s + I cannot solve this puzzle + + limbOption must be left or right + + %1$s Knee Range of Motion + This activity measures how far you can extend your %1$s knee. + Sit down on the edge of a chair. When you begin you will put your device on your %1$s knee for a measurement. Please turn the sound on your device on so you can hear instructions. + Place your device on your %1$s knee with the screen facing out, as pictured. + When ready, tap the screen to begin and extend your %1$s knee as far as you can. Tap again when you are done. + Place your device on your %1$s knee + Extend your %1$s knee. Then tap anywhere. + + %1$s Shoulder Range of Motion + This activity measures how far you can extend your %1$s shoulder. + When you begin you will put your device on your %1$s shoulder for a measurement. Please turn the sound on your device on so you can hear instructions. + Place your device on your %1$s shoulder with the screen facing out, as pictured. + When ready, tap the screen to begin and raise your %1$s arm as far as you can. Tap again when you are done. + Place your device on your %1$s shoulder + Lift your %1$s arm up. Then tap anywhere. + + Timed Walk + This activity measures your lower extremity function. + Find a place, preferably outside, where you can walk for about %1$s in a straight line as quickly as possible, but safely. Do not slow down until after you\'ve passed the finish line. + Tap Next to begin. + Assistive device + Use the same assistive device for each test. + Do you wear an ankle foot orthosis? + Do you use assistive device? + Tap here to select an answer. + None + Unilateral Cane + Unilateral Crutch + Bilateral Cane + Bilateral Crutch + Walker/Rollator + Walk up to %1$s in a straight line. + Turn around. + Walk back to where you started. + Tap Done when complete. + + PASAT + PVSAT + PAVSAT + The Paced Auditory Serial Addition Test measures your auditory information processing speed and calculation ability. + The Paced Visual Serial Addition Test measures your visual information processing speed and calculation ability. + The Paced Auditory and Visual Serial Addition Test measures your auditory and visual information processing speed and calculation ability. + Single digits are presented every %1$s seconds.\nYou must add each new digit to the one immediately prior to it.\nAttention, you mustn\'t calculate a running total, but only the sum of the last two numbers. + Tap Get Started to begin. + Remember this first digit. + Add this new digit to the previous one. + - + + %1$s-Hole Peg Test + This activity measures your upper extremity function by asking you to place a peg in a hole. You will be asked to do this %1$s times. + Both your left and right hands will be tested.\nYou must pick up the peg as quickly as possible, put it in the hole, and, once done %1$s times, remove it again %2$s times. + Both your right and left hands will be tested.\nYou must pick up the peg as quickly as possible, put it in the hole, and, once done %1$s times, remove it again %2$s times. + Tap Get Started to begin. + Put the peg in the hole using your left hand. + Put the peg in the hole using your right hand. + Put the peg behind the line using your left hand. + Put the peg behind the line using your right hand. + Pick up the peg using two fingers. + Lift fingers to drop the peg. + + Tremor Activity + This activity measures the tremor of your hands in various positions. Find a place where you can sit comfortably for the duration of this activity. + Hold the phone in your more affected hand as shown in the image below. + Hold the phone in your RIGHT hand as shown in the image below. + Hold the phone in your LEFT hand as shown in the image below. + You will be asked to perform %1$s while sitting with the phone in your hand. + a task + two tasks + three tasks + four tasks + five tasks + Tap next to proceed. + Prepare to hold your phone in your lap. + Prepare to hold your phone in your lap with your LEFT hand. + Prepare to hold your phone in your lap with your RIGHT hand. + Keep holding your phone in your lap for %1$d seconds. + Now hold your phone with your hand extended out at shoulder height. + Now hold your phone with your LEFT hand extended out at shoulder height. + Now hold your phone with your RIGHT hand extended out at shoulder height. + Keep holding your phone with your hand extended for %1$d seconds. + Now hold your phone at shoulder height with your elbow bent. + Now hold your phone with your LEFT hand at shoulder height with your elbow bent. + Now hold your phone with your RIGHT hand at shoulder height with your elbow bent. + Keep holding your phone with your elbow bent for %1$d seconds + Now keeping your elbow bent, touch your phone to your nose repeatedly. + Now keeping your elbow bent with your phone in your LEFT hand, touch your phone to your nose repeatedly. + Now keeping your elbow bent with your phone in your RIGHT hand, touch your phone to your nose repeatedly. + Keep touching your phone to your nose for %1$d seconds + Prepare to do a queen wave (wave by turning your wrist). + Prepare to do a queen wave with your phone in your LEFT hand (wave by turning your wrist). + Prepare to do a queen wave with your phone in your RIGHT hand (wave by turning your wrist). + Keep performing a queen wave for %1$d seconds. + Now switch the phone to your LEFT hand and continue to the next task. + Now switch the phone to your RIGHT hand and continue to the next task. + Continue to the next task. + Activity completed. + You will be asked to perform %1$s while sitting with the phone first in one hand, then again with the other hand. + I cannot perform this activity with my LEFT hand. + I cannot perform this activity with my RIGHT hand. + I can perform this activity with both hands. + + Trail Making Test + This activity evaluates your visual attention and task switching by recording the time required to connect a series of dots. + After the countdown, tap dots in alternating order between numbers and letters. Begin by tapping the first number \'1\' followed by tapping the first letter \'A\', and then 2 – B – 3 – C… until you reach the end. + Do this as quickly as you can without making mistakes. + Tap Get Started to begin. + + Daily Check-In + Weekly Check-In + Tell us how you feel. We\'ll ask you to rate your mental clarity, mood, and pain level today as well as how well you slept and how much exercise you have done in the last day. + Tell us how you feel. We\'ll ask you to rate your mental clarity, mood, pain level, how well you slept and how much exercise you have done in the past week. + This activity should take less than two minutes to complete. + Great + Good + Average + Bad + Terrible + Today, my thinking is: + This week, my thinking has been: + perfectly crisp! + crisp + \"not great, but not too bad\" + foggy + poor + Today, my mood is: + This week, my mood has been: + fantastic! + better than usual + normal + down + at my lowest + Today, my pain level is: + This week, my pain level has been: + no hurt + hurts a little bit + hurts even more + hurts a whole lot + hurts worst + The quality of my sleep last night was: + The quality of my sleep this week was: + best sleep ever + better sleep than usual + OK sleep + I wish I slept more + no sleep + The most I exercised in the last day was: + The most I exercised in the last week was: + strenuous exercise (heart beats rapidly) + moderate exercise (tiring but not exhausting) + mild exercise (some effort) + minimal exercise (no effort) + no exercise + + Could not create file + Could not remove sufficient log files to reach threshold + Error setting attribute + File not marked deleted (not marked uploaded) + Multiple errors removing logs + No collected data was found. + No output directory specified + + No Data + + Back + Illustration of %1$s + Designated signature field + Touch the screen and move your finger to sign + Signed + Unsigned + Selected + Un-selected + Response slider. Range from %1$s to %2$s + Unlabeled image + Begin task + active + correct + incorrect + quiescent + Memory game tile + Capture preview + Captured image + Video capture preview + Video capture preview + Video capture complete + Unable to place disk with size %1$s on disk with size %2$s + Target + Tower + Double-tap to place disk + Double-tap to select top-most disk + Has disk with sizes %1$s + Empty + Range from %1$s to %2$s + Stack composed of + and + Point: %1$d + Audio Bar Graph + diff --git a/backbone/src/main/res/values/strings2.xml b/backbone/src/main/res/values/strings2.xml new file mode 100644 index 000000000..8d2d9a95a --- /dev/null +++ b/backbone/src/main/res/values/strings2.xml @@ -0,0 +1,116 @@ + + + + Sign In + Send Email + %1$s Consent Form + Join The Study + Read consent document + Email consent Document + Already Participating? + Skip Signup + + + Eligibility + Ineligible + You are eligible to join the study! + Touch “NEXT” below to get started with the consent process. + + Unfortunately, you are ineligible to join this study. + + + + Participant + + + Please answer a few questions to ensure that you + understand the consent form. + + That\'s Correct! + That\'s Incorrect! + You are <b><i>correct.</i></b> + %1$s + + The correct answer is <b><i>%1$s.</i></b> + %2$s + + + + Sign Up + Thank you + name@example.com + Create your study login password + Enter password + Enter username or email + Username + username_123 + + + Activities + Dashboard + Learn + Settings + + + Profile + Name + Birthdate + Reminders + Task reminders + You’ll be reminded to complete tasks + Privacy + Sharing Options + Share with … + Review consent document + Privacy policy + Security + Auto-Lock on exit + App will automatically lock + Auto-Lock time + The time it takes for auto-lock to + trigger + + 1 + Change passcode + General + Software Notices + Leave Study + Join Study + Build number %1$s (%2$d) + Unknown version + Are you sure you want to leave the study? This + action cannot be undone and you will need to provide consent in order to re-enroll. + + + + True + False + + + Consent Withdrawn + Failed to Withdraw + Please review the consent documentation to continue participating in the study. + GO + Your account has been signed out, please sign in to continue + SIGN IN + Please upgrade the application to restore functionality + UPGRADE + Something happened with network, try again later + Whoops! looks like something bad happened when talking + to the server, try again later! + + Changing Passcode Failed + Passcode Changed + Unable to load the selected task + Please select an answer + + + Today, %1$s, %2$s %3$s + To start an activity, select from the list below. + Yesterday + Below are your incomplete tasks from yesterday.\nThese are for reference only. + Keep Going! + Try one of these activities to enhance your experience in your study. + + Tell us how you feel. We\'ll ask you to rate your mental clarity, mood, and pain level today as well as how well you slept and how much exercise you have done in the last day. You will also have an opportunity to track any activity or thought that you choose yourself. + diff --git a/backbone/src/main/res/values/styles.xml b/backbone/src/main/res/values/styles.xml index 31d7b0abb..5b3ccc83c 100644 --- a/backbone/src/main/res/values/styles.xml +++ b/backbone/src/main/res/values/styles.xml @@ -21,6 +21,16 @@ @dimen/rsb_padding_small + + + + + + + + + + + + + + + + - - \ No newline at end of file diff --git a/skin/src/main/res/values/attrs.xml b/skin/src/main/res/values/attrs.xml deleted file mode 100644 index 498a750ee..000000000 --- a/skin/src/main/res/values/attrs.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/skin/src/main/res/values/colors.xml b/skin/src/main/res/values/colors.xml deleted file mode 100644 index b74203cd7..000000000 --- a/skin/src/main/res/values/colors.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - #3f51b5 - #4caf50 - #009891 - #99000000 - #1d92f4 - diff --git a/skin/src/main/res/values/dimens.xml b/skin/src/main/res/values/dimens.xml deleted file mode 100644 index d2363bb68..000000000 --- a/skin/src/main/res/values/dimens.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - 2dp - 4dp - 4dp - 6dp - \ No newline at end of file diff --git a/skin/src/main/res/values/integers.xml b/skin/src/main/res/values/integers.xml deleted file mode 100644 index c4820daa8..000000000 --- a/skin/src/main/res/values/integers.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - 16 - 100 - 100 - \ No newline at end of file diff --git a/skin/src/main/res/values/strings.xml b/skin/src/main/res/values/strings.xml deleted file mode 100644 index d413d21b5..000000000 --- a/skin/src/main/res/values/strings.xml +++ /dev/null @@ -1,168 +0,0 @@ - - - - Change Passcode - Passcode - Choose a passcode - Enter a secure, %1$d-%2$s code to protect your data and log in - faster. - - Confirm your passcode - Enter your %1$d-%2$s code one more time to confirm. - That code doesn’t match the one you entered. Try again - or go back to choose a different %1$d-%2$s code. - - - - Sign In - Send Email - %1$s Consent Form - Join The Study - Read consent document - Email consent Document - Already Participating? - Skip Signup - - - Eligibility - Ineligible - You are eligible to join the study! - Touch “NEXT” below to get started with the consent process. - - Unfortunately, you are ineligible to join this study. - - - - Participant - - - Quiz - Quiz Evaluation - Please answer a few questions to ensure that you - understand the consent form. - - That\'s Correct! - That\'s Incorrect! - You are <b><i>correct.</i></b> - %1$s - - The correct answer is <b><i>%1$s.</i></b> - %2$s - - - - Sign Up - Password - Forgot password - Thank you - name@example.com - Create your study login password - Enter password - Enter username or email - Username - username_123 - - - Confirm - Check your email - We’ve emailed %1$s with a link to confirm your account. After - visiting the link, come back and touch “continue” below. - - ENTER A DIFFERENT EMAIL - Resend - - Wrong email address? Tap here. - Once verified, tap below - Continue - Resend Verification Email - %1$s has send you a verification email at %2$s - - Please check your email to verify your account before - continuing - - Change Email - - - Activities - Dashboard - Learn - Settings - - - Profile - Name - Birthdate - Reminders - Task reminders - You’ll be reminded to complete tasks - Privacy - Sharing Options - Share with … - Review consent document - Privacy policy - Security - Auto-Lock on exit - App will automatically lock - Auto-Lock time - The time it takes for auto-lock to - trigger - - 1 - Change passcode - General - Software Notices - Leave Study - Join Study - Build number %1$s (%2$s) - Unknown version - Are you sure you want to leave the study? This - action cannot be undone and you will need to provide consent in order to re-enroll. - - - - True - False - - - Consent Withdrawn - Failed to Withdraw - Please review the consent documentation to continue participating in the study. - GO - Your account has been signed out, please sign in to continue - SIGN IN - Something happened with network, try again later - Whoops! looks like something bad happened when talking - to the server, try again later! - - Changing Passcode Failed - Passcode Changed - Unable to load the selected task - Please select an answer - Invalid Password - Invalid Email - - - Permissions - Location - The app needs location permissions to show accurate - location-based data - Notifications - The app needs your permission in order to show - notifications when you complete surveys. This permission is optional and can be enabled in - the App\'s Settings screen. - This activity is serving as an example of - how you would implement outside auth. In other words, this activity represents an 3rd party - auth process like ones found in Google+, Google Drive, or Google Fit. This example is simple - and the result only controls whether notifications will show .\n\nYou can still enable app - notifications via the settings screen. The button for this screen is in the upper right hand - corner of the main screen (gear icon). - Participation in the study requires some permissions - from your device. - - Please allow %1$s permissions to continue. - - Allow - Granted - Optional - - diff --git a/skin/src/main/res/values/styles.xml b/skin/src/main/res/values/styles.xml deleted file mode 100644 index c4a3b919c..000000000 --- a/skin/src/main/res/values/styles.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - -