├── .github └── workflows │ ├── scheduled_snyk.yaml │ └── snyk.yaml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── build.gradle ├── libs │ └── slf4j-handroid-2.0.0-SNAPSHOT.jar ├── proguard-rules.pro └── src │ ├── debug │ └── .gitignore │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── org │ │ │ ├── radarbase │ │ │ └── android │ │ │ │ └── widget │ │ │ │ ├── Extensions.kt │ │ │ │ └── TextDrawable.kt │ │ │ └── radarcns │ │ │ └── detail │ │ │ ├── AuthServiceImpl.kt │ │ │ ├── InfoActivity.kt │ │ │ ├── LoginActivityImpl.kt │ │ │ ├── MainActivityBootStarter.kt │ │ │ ├── MainActivityViewImpl.kt │ │ │ ├── PrivacyPolicyFragment.kt │ │ │ ├── QrException.kt │ │ │ ├── RadarApplicationImpl.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── SourceRowView.kt │ │ │ ├── SplashActivityImpl.kt │ │ │ └── UncaughtExceptionHandlerContentProvider.kt │ └── res │ │ ├── drawable │ │ ├── avd_anim_ready.xml │ │ ├── avd_connected_circle.xml │ │ ├── avd_connecting.xml │ │ ├── avd_logo_heartbeat.xml │ │ ├── avd_logo_splash.xml │ │ ├── baseline_battery_2_bar_red_700_24dp.xml │ │ ├── baseline_battery_3_bar_orange_700_24dp.xml │ │ ├── baseline_battery_5_bar_green_700_24dp.xml │ │ ├── baseline_battery_alert_red_700_24dp.xml │ │ ├── baseline_battery_full_green_700_24dp.xml │ │ ├── baseline_battery_unknown_gray_24dp.xml │ │ ├── baseline_change_circle_blue_grey_500_24dp.xml │ │ ├── baseline_circle_red_700_24dp.xml │ │ ├── baseline_filter_alt_black_24dp.xml │ │ ├── baseline_refresh_black_24dp.xml │ │ ├── baseline_status_search.xml │ │ ├── baseline_update_black_24dp.xml │ │ ├── baseline_wifi_grey_600_24dp.xml │ │ ├── vd_logo.xml │ │ └── vd_logo_white.xml │ │ ├── font │ │ ├── roboto.xml │ │ ├── roboto_light.xml │ │ └── roboto_medium.xml │ │ ├── layout │ │ ├── action_layout.xml │ │ ├── activity_login.xml │ │ ├── activity_overview_source_row.xml │ │ ├── activity_settings.xml │ │ ├── activity_splash.xml │ │ ├── compact_info.xml │ │ ├── compact_overview.xml │ │ ├── dialog_login_token.xml │ │ └── fragment_privacy_policy.xml │ │ ├── menu │ │ └── menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── colors.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── drawables.xml │ │ ├── font_certs.xml │ │ ├── ic_launcher_background.xml │ │ ├── ids.xml │ │ ├── preloaded_fonts.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── backup_descriptor.xml │ │ ├── provider_paths.xml │ │ └── remote_config_defaults.xml │ ├── playStore │ ├── .gitignore │ └── java │ │ └── org │ │ └── radarcns │ │ └── detail │ │ ├── MainActivityImpl.kt │ │ └── RadarServiceImpl.kt │ ├── release │ └── .gitignore │ └── selfRelease │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── radarcns │ │ └── detail │ │ ├── DownloadProgress.kt │ │ ├── GithubAssetClient.kt │ │ ├── MainActivityImpl.kt │ │ ├── OnlineAsset.kt │ │ ├── PackageUtil.kt │ │ ├── RadarServiceImpl.kt │ │ ├── SemVer.kt │ │ ├── UpdateAlarmReceiver.kt │ │ └── UpdatesActivity.kt │ └── res │ ├── drawable │ └── baseline_update_black_24dp.xml │ ├── layout │ └── activity_updates.xml │ └── menu │ └── menu.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── man ├── E4_connected.png ├── E4_pebble_connected.png ├── E4_pebble_connected2.png ├── patient_id_input.png ├── prmt-main-activity.jpg ├── screen20161215_edited.png ├── serial_number.png └── upload_1000.png └── settings.gradle /.github/workflows/scheduled_snyk.yaml: -------------------------------------------------------------------------------- 1 | name: Snyk scheduled test 2 | on: 3 | schedule: 4 | - cron: '0 2 * * 1' 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | security: 11 | runs-on: ubuntu-latest 12 | env: 13 | REPORT_FILE: test.json 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: snyk/actions/setup@master 18 | with: 19 | snyk-version: v1.1032.0 20 | 21 | - uses: actions/setup-java@v3 22 | with: 23 | distribution: temurin 24 | java-version: 17 25 | 26 | - name: Setup Gradle 27 | uses: gradle/gradle-build-action@v2 28 | 29 | - name: Run Snyk 30 | env: 31 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 32 | run: > 33 | snyk test 34 | --all-sub-projects 35 | --configuration-matching='^releaseRuntimeClasspath$' 36 | --fail-on=upgradable 37 | --json-file-output=${{ env.REPORT_FILE }} 38 | --org=radar-base 39 | --policy-path=$PWD/.snyk 40 | 41 | - name: Report new vulnerabilities 42 | uses: thehyve/report-vulnerability@master 43 | if: success() || failure() 44 | with: 45 | report-file: ${{ env.REPORT_FILE }} 46 | env: 47 | TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/snyk.yaml: -------------------------------------------------------------------------------- 1 | name: Snyk test 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | - dev 7 | 8 | jobs: 9 | security: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: snyk/actions/setup@master 14 | with: 15 | snyk-version: v1.1032.0 16 | 17 | - uses: actions/setup-java@v3 18 | with: 19 | distribution: temurin 20 | java-version: 17 21 | 22 | - name: Setup Gradle 23 | uses: gradle/gradle-build-action@v2 24 | 25 | - name: Run Snyk to check for vulnerabilities 26 | env: 27 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 28 | run: > 29 | snyk test 30 | --all-sub-projects 31 | --configuration-matching="^runtimeClasspath$" 32 | --fail-on=upgradable 33 | --org=radar-base 34 | --policy-path=.snyk 35 | --severity-threshold=high 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/src/debug/google-services.json 2 | app/google-services.json 3 | 4 | # Local configuration file 5 | local.properties 6 | 7 | # Java class files 8 | *.class 9 | 10 | # Built native files 11 | *.o 12 | *.so 13 | obj/ 14 | 15 | # Built application files 16 | *.apk 17 | *.aar 18 | *.ap_ 19 | 20 | # Generated files 21 | bin/ 22 | gen/ 23 | out/ 24 | build/ 25 | 26 | # OSX stuff 27 | .DS_Store 28 | 29 | # Proguard folder 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # EmpaLink lib 36 | empalink-2.0/*.aar 37 | 38 | # Android Studio Navigation editor temp files 39 | .navigation/ 40 | 41 | # Android Studio captures folder 42 | captures/ 43 | 44 | # Keystore files 45 | *.jks 46 | 47 | ### Intellij+iml ### 48 | # Created by https://www.gitignore.io/api/intellij+iml 49 | 50 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 51 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 52 | 53 | # User-specific stuff: 54 | .idea/workspace.xml 55 | .idea/tasks.xml 56 | .idea/dictionaries 57 | .idea/vcs.xml 58 | .idea/jsLibraryMappings.xml 59 | .idea/encodings.xml 60 | 61 | # Sensitive or high-churn files: 62 | .idea/dataSources.ids 63 | .idea/dataSources.xml 64 | .idea/dataSources.local.xml 65 | .idea/sqlDataSources.xml 66 | .idea/dynamic.xml 67 | .idea/uiDesigner.xml 68 | 69 | # Gradle: 70 | .idea/gradle.xml 71 | .gradle/ 72 | .idea/libraries 73 | .gradletasknamecache 74 | 75 | # Mongo Explorer plugin: 76 | .idea/mongoSettings.xml 77 | 78 | ## File-based project format: 79 | *.iws 80 | 81 | ## Plugin-specific files: 82 | 83 | # mpeltonen/sbt-idea plugin 84 | .idea_modules/ 85 | 86 | # JIRA plugin 87 | atlassian-ide-plugin.xml 88 | 89 | # Crashlytics plugin (for Android Studio and IntelliJ) 90 | com_crashlytics_export_strings.xml 91 | crashlytics.properties 92 | crashlytics-build.properties 93 | fabric.properties 94 | 95 | ### Intellij+iml Patch ### 96 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 97 | 98 | *.iml 99 | modules.xml 100 | /.idea/ 101 | *.ipr 102 | 103 | ## Pebble 2 104 | .lock* 105 | 106 | releases/ 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RADAR-pRMT 2 | 3 | Application to be run on an Android 7 (or later) device. If Bluetooth devices are to be used, the Android device should support Bluetooth Low Energy (Bluetooth 4.0 or later). 4 | 5 | ![Screenshot](/man/prmt-main-activity.jpg?raw=True "Screenshot 2023-05-04") 6 | 7 | To clone this repository, use the command 8 | 9 | ```shell 10 | git clone https://github.com/RADAR-base/radar-prmt-android.git 11 | ``` 12 | 13 | ## Configuration 14 | 15 | Parameters are described in the README of [radar-commons-android](https://github.com/RADAR-base/radar-commons-android) and those from the plugins below. Modify `app/src/main/res/xml/remote_config_defaults.xml` to change their defaults. 16 | 17 | ### Plugins 18 | 19 | This application depends on plugins to collect information. Project-supported plugins are listed in the `plugins` directory of [radar-commons-android](https://github.com/radar-base/radar-commons-android). A plugin can be added to this app in three steps 20 | 21 | 1. Add it as a dependency in `app/build.gradle` 22 | 2. Add the respective `SourceProvider` to the `plugins` value in `app/src/main/java/org/radarbase/passive/app/RadarServiceImpl.kt` 23 | 3. In the `plugins` variable in Firebase or `app/src/main/res/xml/remote_config_defaults.xml`, add the plugin name. 24 | 25 | See the plugin documentation on what link to each plugin for its configuration options and data collection settings. 26 | 27 | ### Setup Firebase Remote Configuration 28 | 29 | Firebase can be used to remotely configure some device and system parameters, e.g. the E4 API key, kafka server address and upload rate. The default parameters are also stored locally in `app/src/main/res/xml/remote_config_defaults.xml`, which will be used if the remote parameters cannot be accessed. 30 | 31 | 1. [Install the Firebase SDK](https://firebase.google.com/docs/android/setup) in Android Studio. 32 | 2. Login to a Google account. 33 | 3. In the [Firebase console](https://console.firebase.google.com/), add the app (`org.radarcns.android`) to a new Firebase project. 34 | 4. Download the `google-services.json` from the Firebase console (under Project Settings) and move the file to the `app/src/release/` folder for release config or `app/src/debug/` folder for debug configuration. 35 | 5. [Optional] Set the parameter values on the server. The available parameters can be found in `app/src/main/res/xml/remote_config_defaults.xml`. 36 | Note - Set the `unsafe_kafka_connection` parameter to `true` if the server with kafka and schema-registry is using a self-signed certificate over SSL. If the certificate is issued by a valid CA then leave it to `false`. In production, do NOT set this value to `true`. 37 | 38 | [Full Firebase guide](https://firebase.google.com/docs/remote-config/use-config-android) 39 | 40 | ## Android Installation 41 | 42 | The guide for installing Android on Raspberry Pi3 and UDOO boards is available [here](https://github.com/RADAR-base/RADAR-AndroidApplication/wiki) 43 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "com.google.android.gms.oss-licenses-plugin" 3 | apply plugin: "kotlin-android" 4 | 5 | android { 6 | compileSdkVersion 34 7 | buildToolsVersion "34.0.0" 8 | namespace "org.radarcns.detail" 9 | 10 | defaultConfig { 11 | applicationId "org.radarcns.detail" 12 | minSdkVersion 26 13 | targetSdkVersion 34 14 | versionCode 69 15 | versionName "1.2.7" 16 | manifestPlaceholders = ["appAuthRedirectScheme": "org.radarbase.passive.app"] 17 | multiDexEnabled true 18 | ndkVersion "25.2.9519653" 19 | } 20 | buildTypes { 21 | release { 22 | shrinkResources true 23 | minifyEnabled true 24 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 25 | ndk { 26 | debugSymbolLevel = "full" 27 | } 28 | } 29 | debug { 30 | shrinkResources false 31 | minifyEnabled true 32 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 33 | } 34 | } 35 | packagingOptions { 36 | resources { 37 | excludes += ["META-INF/ASL2.0", "META-INF/LICENSE"] 38 | } 39 | } 40 | compileOptions { 41 | targetCompatibility JavaVersion.VERSION_17 42 | sourceCompatibility JavaVersion.VERSION_17 43 | } 44 | buildFeatures { 45 | viewBinding true 46 | } 47 | flavorDimensions "channel" 48 | productFlavors { 49 | playStore { 50 | dimension "channel" 51 | versionNameSuffix "-playStore" 52 | } 53 | selfRelease { 54 | dimension "channel" 55 | versionNameSuffix "-selfRelease" 56 | } 57 | } 58 | lint { 59 | abortOnError false 60 | } 61 | } 62 | 63 | configurations.all { 64 | resolutionStrategy.cacheDynamicVersionsFor 0, "seconds" 65 | resolutionStrategy.cacheChangingModulesFor 0, "seconds" 66 | } 67 | 68 | repositories { 69 | flatDir { dirs "libs" } 70 | google() 71 | mavenLocal() 72 | mavenCentral() 73 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } 74 | maven { url 'https://repo.thehyve.nl/content/repositories/releases' } 75 | maven { url 'https://jitpack.io' } 76 | } 77 | 78 | dependencies { 79 | api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 80 | api "org.radarbase:radar-commons-android:$radar_commons_android_version" 81 | 82 | implementation "org.radarbase:radar-android-login-qr:$radar_commons_android_version" 83 | 84 | implementation platform("com.google.firebase:firebase-bom:$firebase_bom_version") 85 | implementation "com.google.android.gms:play-services-oss-licenses:$play_services_oss_licenses_version" 86 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 87 | 88 | implementation "org.slf4j:slf4j-api:$slf4j_api_version" 89 | 90 | implementation "com.google.firebase:firebase-config" 91 | implementation "com.google.firebase:firebase-crashlytics" 92 | implementation "com.google.firebase:firebase-analytics" 93 | 94 | implementation "com.gitlab.mvysny.slf4j:slf4j-handroid:$slf4j_handroid_version" 95 | 96 | implementation "org.radarbase:radar-android-phone:$radar_commons_android_version" 97 | implementation "org.radarbase:radar-android-phone-usage:$radar_commons_android_version" 98 | selfReleaseImplementation "org.radarbase:radar-android-phone-telephony:$radar_commons_android_version" 99 | implementation "org.radarbase:radar-android-empatica:$radar_commons_android_version" 100 | implementation "org.radarbase:radar-android-application-status:$radar_commons_android_version" 101 | implementation "org.radarbase:radar-android-weather:$radar_commons_android_version" 102 | implementation "org.radarbase:radar-android-audio:$radar_commons_android_version" 103 | implementation "org.radarbase:radar-android-faros:$radar_commons_android_version" 104 | // implementation "org.radarbase:radar-android-polar:$radar_commons_android_version" 105 | implementation "org.radarbase:radar-android-google-sleep:$radar_commons_android_version" 106 | implementation "org.radarbase:radar-android-google-activity:$radar_commons_android_version" 107 | implementation "org.radarbase:radar-android-google-places:$radar_commons_android_version" 108 | implementation "org.radarbase:radar-android-phone-audio-input:$radar_commons_android_version" 109 | 110 | implementation "androidx.lifecycle:lifecycle-process:$lifecycle_process_version" 111 | implementation "androidx.legacy:legacy-support-v4:$legacy_support_version" 112 | implementation "com.google.android.material:material:$material_version" 113 | implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version" 114 | implementation "androidx.fragment:fragment-ktx:$fragment_version" 115 | 116 | testImplementation "junit:junit:$junit_version" 117 | testRuntimeOnly "org.slf4j:slf4j-simple:$slf4j_simple_version" 118 | } 119 | 120 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 121 | kotlinOptions { 122 | jvmTarget = "17" 123 | apiVersion = "1.7" 124 | languageVersion = "1.7" 125 | } 126 | } 127 | 128 | // Needed for Firebase. Put at the bottom so it can detect the Firebase version. 129 | apply plugin: "com.google.gms.google-services" 130 | apply plugin: "com.google.firebase.crashlytics" 131 | -------------------------------------------------------------------------------- /app/libs/slf4j-handroid-2.0.0-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RADAR-base/radar-prmt-android/7e20fe06b1e9fc8a6414c724bf6b2bdffc195fb2/app/libs/slf4j-handroid-2.0.0-SNAPSHOT.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/andrea/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | -keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | 27 | -keep public class * implements org.radarbase.android.source.SourceProvider { *; } 28 | -keep class * implements org.radarbase.android.source.SourceManager { *; } 29 | -keep class com.empatica.empalink.delegate.** { *; } 30 | -keep class com.empatica.empalink.** { *; } 31 | 32 | -keep interface com.empatica.empalink.delegate.** { *; } 33 | -keep interface com.empatica.empalink.** { *; } 34 | 35 | # Native methods: https://www.guardsquare.com/en/products/proguard/manual/examples#native 36 | # note that means any method 37 | -keepclasseswithmembernames,includedescriptorclasses class * { 38 | native ; 39 | static native ; 40 | } 41 | 42 | # Avro 43 | -keep class org.apache.avro.** { *; } 44 | -keep @org.apache.avro.specific.AvroGenerated class * { 45 | public ; 46 | public ; 47 | } 48 | -keep class org.codehaus.jackson.** { *; } 49 | -dontwarn org.apache.avro.** 50 | -dontwarn org.codehaus.jackson.map.ext.** 51 | 52 | -keep class com.google.firebase.crashlytics.FirebaseCrashlytics { 53 | static com.google.firebase.crashlytics.FirebaseCrashlytics getInstance(); 54 | void log(java.lang.String); 55 | void recordException(java.lang.Throwable); 56 | } 57 | 58 | # ==== OkHttp3 ==== # 59 | # JSR 305 annotations are for embedding nullability information. 60 | -dontwarn javax.annotation.** 61 | 62 | # A resource is loaded with a relative path so the package of this class must be preserved. 63 | -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase 64 | 65 | # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. 66 | -dontwarn org.codehaus.mojo.animal_sniffer.* 67 | 68 | # OkHttp platform used only on JVM and when Conscrypt dependency is available. 69 | -dontwarn okhttp3.internal.platform.ConscryptPlatform 70 | 71 | -dontwarn java.beans.ConstructorProperties 72 | -dontwarn java.beans.Transient 73 | -dontwarn org.bouncycastle.jsse.BCSSLParameters 74 | -dontwarn org.bouncycastle.jsse.BCSSLSocket 75 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider 76 | -dontwarn org.conscrypt.Conscrypt$Version 77 | -dontwarn org.conscrypt.Conscrypt 78 | -dontwarn org.conscrypt.ConscryptHostnameVerifier 79 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters 80 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket 81 | -dontwarn org.openjsse.net.ssl.OpenJSSE 82 | -------------------------------------------------------------------------------- /app/src/debug/.gitignore: -------------------------------------------------------------------------------- 1 | google-services.json 2 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 53 | 56 | 57 | 58 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 96 | 97 | 98 | 101 | 102 | 103 | 109 | 110 | 114 | 119 | 123 | 127 | 128 | 132 | 133 | 134 | 135 | 136 | 140 | 143 | 144 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarbase/android/widget/Extensions.kt: -------------------------------------------------------------------------------- 1 | package org.radarbase.android.widget 2 | 3 | import android.content.Intent 4 | import android.graphics.drawable.Animatable2 5 | import android.graphics.drawable.AnimatedVectorDrawable 6 | import android.graphics.drawable.Drawable 7 | import android.net.Uri 8 | import android.os.Build 9 | import android.view.View 10 | import android.widget.ImageView 11 | import android.widget.TextView 12 | import androidx.annotation.MainThread 13 | import androidx.appcompat.app.AppCompatActivity 14 | import org.radarbase.android.RadarApplication.Companion.radarApp 15 | import org.radarcns.detail.InfoActivity 16 | import org.radarcns.detail.R 17 | 18 | @MainThread 19 | fun ImageView.repeatAnimation() { 20 | val avd = drawable as? AnimatedVectorDrawable ?: return 21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 22 | avd.registerAnimationCallback( 23 | object : Animatable2.AnimationCallback() { 24 | override fun onAnimationEnd(drawable: Drawable?) { 25 | post { avd.start() } 26 | } 27 | } 28 | ) 29 | } 30 | avd.start() 31 | } 32 | 33 | @MainThread 34 | fun AppCompatActivity.addPrivacyPolicy(view: TextView) { 35 | radarApp.configuration.config.observe(this) { config -> 36 | val privacyPolicyUrl = config.optString(InfoActivity.PRIVACY_POLICY) 37 | if (privacyPolicyUrl != null) { 38 | view.setText(R.string.privacy_policy) 39 | view.setOnClickListener { 40 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(privacyPolicyUrl))) 41 | } 42 | view.visibility = View.VISIBLE 43 | } else { 44 | view.visibility = View.GONE 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarbase/android/widget/TextDrawable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Ali Muzaffar 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.radarbase.android.widget 18 | 19 | import android.graphics.* 20 | import android.graphics.drawable.Drawable 21 | import android.text.Editable 22 | import android.text.TextWatcher 23 | import android.widget.TextView 24 | 25 | /** 26 | * Create a TextDrawable using the given paint object and string 27 | * 28 | * @param paint 29 | * @param s 30 | */ 31 | class TextDrawable(textView: TextView, mText: String) : Drawable(), TextWatcher { 32 | private val heightBounds = Rect() 33 | 34 | //Since this can change the font used, we need to recalculate bounds. 35 | private val paint = Paint(textView.paint) 36 | 37 | //Since this can change the bounds of the text, we need to recalculate. 38 | var text: String = mText 39 | set(value) { 40 | field = value 41 | calculateBounds() 42 | invalidateSelf() 43 | } 44 | 45 | init { 46 | calculateBounds() 47 | } 48 | 49 | override fun draw(canvas: Canvas) { 50 | canvas.drawText(text, 0f, bounds.height().toFloat(), paint) 51 | } 52 | 53 | override fun setAlpha(alpha: Int) { 54 | paint.alpha = alpha 55 | } 56 | 57 | override fun setColorFilter(colorFilter: ColorFilter?) { 58 | paint.colorFilter = colorFilter 59 | } 60 | 61 | @Deprecated("Super getOpacity is deprecated") 62 | override fun getOpacity(): Int = when (paint.alpha) { 63 | 0 -> PixelFormat.TRANSPARENT 64 | 255 -> PixelFormat.OPAQUE 65 | else -> PixelFormat.TRANSLUCENT 66 | } 67 | 68 | private fun calculateBounds() { 69 | paint.getTextBounds("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 0, 1, heightBounds) 70 | 71 | //We want to use some character to determine the max height of the text. 72 | //Otherwise if we draw something like "..." they will appear centered 73 | //Here I'm just going to use the entire alphabet to determine max height. 74 | //This doesn't account for leading or training white spaces. 75 | //mPaint.getTextBounds(mText, 0, mText.length(), bounds); 76 | bounds.apply { 77 | top = heightBounds.top 78 | bottom = heightBounds.bottom 79 | right = paint.measureText(text).toInt() 80 | left = 0 81 | } 82 | } 83 | 84 | override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit 85 | 86 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit 87 | 88 | override fun afterTextChanged(s: Editable) { 89 | text = s.toString() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarcns/detail/AuthServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package org.radarcns.detail 2 | 3 | import org.radarbase.android.auth.AppAuthState 4 | import org.radarbase.android.auth.AuthService 5 | import org.radarbase.android.auth.LoginManager 6 | import org.radarbase.android.auth.portal.ManagementPortalLoginManager 7 | 8 | class AuthServiceImpl : AuthService() { 9 | override fun createLoginManagers(appAuth: AppAuthState): List = listOf( 10 | ManagementPortalLoginManager(this, appAuth), 11 | ) 12 | 13 | override fun showLoginNotification() = Unit 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarcns/detail/InfoActivity.kt: -------------------------------------------------------------------------------- 1 | package org.radarcns.detail 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.view.View 7 | import android.widget.TextView 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.google.android.gms.oss.licenses.OssLicensesMenuActivity 10 | import org.radarbase.android.RadarApplication.Companion.radarConfig 11 | import org.radarbase.android.RadarConfiguration.Companion.BASE_URL_KEY 12 | 13 | class InfoActivity : AppCompatActivity() { 14 | private var policyUrl: String? = null 15 | 16 | public override fun onCreate(bundle: Bundle?) { 17 | super.onCreate(bundle) 18 | 19 | setContentView(R.layout.compact_info) 20 | 21 | findViewById(R.id.app_name).setText(R.string.app_name) 22 | findViewById(R.id.version).text = BuildConfig.VERSION_NAME 23 | 24 | radarConfig.config.observe(this) { config -> 25 | findViewById(R.id.server_base_url).text = config.getString(BASE_URL_KEY, "") 26 | 27 | policyUrl = config.optString(PRIVACY_POLICY) 28 | 29 | if (policyUrl == null) { 30 | findViewById(R.id.generalPrivacyPolicyStatement).apply { 31 | visibility = View.GONE 32 | } 33 | } 34 | } 35 | 36 | setSupportActionBar(findViewById(R.id.toolbar)) 37 | 38 | supportActionBar?.apply { 39 | setDisplayShowHomeEnabled(true) 40 | setDisplayHomeAsUpEnabled(true) 41 | } 42 | 43 | findViewById(R.id.generalPrivacyPolicyStatement).setOnClickListener { v -> openPrivacyPolicy(v) } 44 | findViewById(R.id.licenses_button).setOnClickListener { v -> showLicenses(v) } 45 | } 46 | 47 | private fun showLicenses(@Suppress("UNUSED_PARAMETER") view: View) { 48 | startActivity(Intent(this, OssLicensesMenuActivity::class.java)) 49 | } 50 | 51 | private fun openPrivacyPolicy(@Suppress("UNUSED_PARAMETER") view: View) { 52 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(policyUrl))) 53 | } 54 | 55 | companion object { 56 | const val PRIVACY_POLICY = "privacy_policy" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarcns/detail/MainActivityBootStarter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 The Hyve 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.radarcns.detail 18 | 19 | import android.content.BroadcastReceiver 20 | import android.content.Context 21 | import android.content.Intent 22 | import android.provider.Settings 23 | import android.widget.Toast 24 | import androidx.core.app.NotificationCompat.CATEGORY_ALARM 25 | import com.google.firebase.crashlytics.FirebaseCrashlytics 26 | import org.radarbase.android.RadarApplication.Companion.radarApp 27 | import org.radarbase.android.util.Boast 28 | import org.radarbase.android.util.NotificationHandler.Companion.NOTIFICATION_CHANNEL_ALERT 29 | 30 | /** 31 | * Starts MainActivity on boot if configured to do so 32 | */ 33 | class MainActivityBootStarter : BroadcastReceiver() { 34 | override fun onReceive(context: Context, intent: Intent) { 35 | if ((context.applicationContext as RadarApplicationImpl).isInForeground) { 36 | return 37 | } 38 | when (intent.action) { 39 | Intent.ACTION_MY_PACKAGE_REPLACED -> { 40 | context.startApp() 41 | Boast.makeText(context, R.string.appUpdated, Toast.LENGTH_LONG).show() 42 | } 43 | Intent.ACTION_BOOT_COMPLETED, "android.intent.action.QUICKBOOT_POWERON" -> { 44 | context.startApp() 45 | } 46 | } 47 | } 48 | 49 | private fun Context.startApp() { 50 | if (!Settings.canDrawOverlays(this)) { 51 | radarApp.notificationHandler.notify( 52 | id = BOOT_START_NOTIFICATION_ID, 53 | channel = NOTIFICATION_CHANNEL_ALERT, 54 | includeStartIntent = true, 55 | ) { 56 | setContentTitle(getString(R.string.bootstart_title)) 57 | setContentTitle(getString(R.string.bootstart_text)) 58 | setCategory(CATEGORY_ALARM) 59 | setAutoCancel(true) 60 | setOngoing(true) 61 | } 62 | } else { 63 | packageManager.getLaunchIntentForPackage(packageName)?.also { 64 | startActivity(it) 65 | } ?: FirebaseCrashlytics.getInstance() 66 | .recordException(IllegalStateException("Cannot start RADAR app $packageName without launch intent")) 67 | } 68 | } 69 | 70 | companion object { 71 | const val BOOT_START_NOTIFICATION_ID = 35002 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarcns/detail/MainActivityViewImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 The Hyve 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.radarcns.detail 18 | 19 | import android.view.View 20 | import android.view.ViewGroup 21 | import android.widget.* 22 | import androidx.appcompat.widget.Toolbar 23 | import org.radarbase.android.IRadarBinder 24 | import org.radarbase.android.MainActivityView 25 | import org.radarbase.android.source.SourceProvider 26 | import org.radarbase.android.util.ChangeApplier 27 | import org.radarbase.android.util.ChangeRunner 28 | import org.radarbase.android.util.TimedLong 29 | import org.radarbase.android.widget.repeatAnimation 30 | import org.slf4j.LoggerFactory 31 | import java.text.SimpleDateFormat 32 | import java.util.* 33 | 34 | class MainActivityViewImpl( 35 | private val mainActivity: MainActivityImpl 36 | ) : MainActivityView { 37 | private val connectionRows = ChangeRunner>>(emptyList()) 38 | private val actionsCells = ChangeRunner>(emptyList()) 39 | 40 | private val timestampCache = ChangeApplier({ numberOfRecords -> 41 | val msg = if (numberOfRecords.value >= 0) R.string.last_upload_succeeded 42 | else R.string.last_upload_failed 43 | mainActivity.getString(msg, timeFormat.format(numberOfRecords.time)) 44 | }, { b -> time == b?.time }) 45 | private val serverStatusCache = ChangeRunner() 46 | 47 | // View elements 48 | private val mServerMessage: TextView 49 | private val mUserId: TextView 50 | private val mSourcesTable: ViewGroup 51 | private val mProjectId: TextView 52 | private val mActionLayout: GridLayout 53 | private val mActionWrapperLayout: LinearLayout 54 | private val mDevicesNoneText: View 55 | 56 | private val userIdCache = ChangeRunner() 57 | private val projectIdCache = ChangeRunner() 58 | private var rows: List = emptyList() 59 | 60 | private val serverStatusMessage: String? 61 | get() { 62 | return mainActivity.radarService?.latestNumberOfRecordsSent?.let { numberOfRecords -> 63 | if (numberOfRecords.time >= 0) { 64 | timestampCache.applyIfChanged(numberOfRecords) 65 | } else { 66 | null 67 | } 68 | } 69 | } 70 | 71 | init { 72 | logger.debug("Creating main activity view") 73 | mainActivity.apply { 74 | setContentView(R.layout.compact_overview) 75 | 76 | setSupportActionBar(findViewById(R.id.toolbar).apply { 77 | setTitle(R.string.radar_prmt_title) 78 | }) 79 | 80 | mServerMessage = findViewById(R.id.statusServerMessage) 81 | 82 | mUserId = findViewById(R.id.inputUserId) 83 | mProjectId = findViewById(R.id.inputProjectId) 84 | mSourcesTable = findViewById(R.id.sourcesTable) 85 | 86 | mActionLayout = findViewById(R.id.actionLayout) 87 | mActionWrapperLayout = findViewById(R.id.actionWrapperLayout) 88 | 89 | mDevicesNoneText = findViewById(R.id.no_devices) 90 | 91 | findViewById(R.id.logo).repeatAnimation() 92 | this@MainActivityViewImpl.update() 93 | } 94 | } 95 | 96 | override fun onRadarServiceBound(binder: IRadarBinder) { 97 | logger.debug("Radar service bound") 98 | } 99 | 100 | override fun update() { 101 | val providers = mainActivity.radarService 102 | ?.connections 103 | ?.filter { it.isDisplayable } 104 | ?: emptyList() 105 | 106 | val currentActions = mainActivity.radarService 107 | ?.connections 108 | ?.flatMap { it.actions } 109 | ?: emptyList() 110 | 111 | rows.forEach(SourceRowView::update) 112 | 113 | mainActivity.runOnUiThread { 114 | connectionRows.applyIfChanged(providers) { p -> 115 | val root = mSourcesTable.apply { 116 | while (childCount > 1) { 117 | removeView(getChildAt(1)) 118 | } 119 | } 120 | 121 | if (p.isEmpty()) { 122 | mDevicesNoneText.visibility = View.VISIBLE 123 | mSourcesTable.visibility = View.GONE 124 | rows = listOf() 125 | } else { 126 | mDevicesNoneText.visibility = View.GONE 127 | mSourcesTable.visibility = View.VISIBLE 128 | rows = p.map { 129 | SourceRowView(mainActivity, it, root).apply { 130 | update() 131 | } 132 | } 133 | } 134 | } 135 | actionsCells.applyIfChanged(currentActions) { actionList -> 136 | if (actionList.isNotEmpty()) { 137 | mActionWrapperLayout.visibility = View.VISIBLE 138 | mActionLayout.apply { 139 | visibility = View.VISIBLE 140 | removeAllViews() 141 | actionList.forEach { action -> 142 | addView(Button(mainActivity).apply { 143 | text = action.name 144 | setOnClickListener { mainActivity.apply(action.activate) } 145 | }) 146 | } 147 | } 148 | } else { 149 | mActionWrapperLayout.visibility = View.GONE 150 | mActionLayout.apply { 151 | visibility = View.GONE 152 | removeAllViews() 153 | } 154 | } 155 | } 156 | 157 | rows.forEach(SourceRowView::display) 158 | updateServerStatus() 159 | setUserId() 160 | } 161 | } 162 | 163 | private fun updateServerStatus() { 164 | serverStatusCache.applyIfChanged(serverStatusMessage ?: "\u2014") { 165 | mServerMessage.text = it 166 | } 167 | } 168 | 169 | private fun setUserId() { 170 | userIdCache.applyIfChanged(mainActivity.userId ?: "") { id -> 171 | mUserId.apply { 172 | if (id.isEmpty()) { 173 | visibility = View.GONE 174 | } else { 175 | visibility = View.VISIBLE 176 | text = id 177 | } 178 | 179 | } 180 | } 181 | projectIdCache.applyIfChanged(mainActivity.projectId ?: "") { id -> 182 | mProjectId.apply { 183 | if (id.isEmpty()) { 184 | visibility = View.GONE 185 | } else { 186 | visibility = View.VISIBLE 187 | text = id 188 | } 189 | } 190 | } 191 | } 192 | 193 | companion object { 194 | private val logger = LoggerFactory.getLogger(MainActivityViewImpl::class.java) 195 | private val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarcns/detail/PrivacyPolicyFragment.kt: -------------------------------------------------------------------------------- 1 | package org.radarcns.detail 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.text.Html 8 | import android.text.Spanned 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import androidx.annotation.StringRes 13 | import androidx.fragment.app.Fragment 14 | import org.radarbase.android.RadarApplication.Companion.radarConfig 15 | import org.radarbase.android.RadarConfiguration.Companion.PROJECT_ID_KEY 16 | import org.radarbase.android.RadarConfiguration.Companion.USER_ID_KEY 17 | import org.radarbase.android.auth.AppAuthState 18 | import org.radarbase.android.auth.AuthService.Companion.BASE_URL_PROPERTY 19 | import org.radarbase.android.auth.AuthService.Companion.PRIVACY_POLICY_URL_PROPERTY 20 | import org.radarcns.detail.InfoActivity.Companion.PRIVACY_POLICY 21 | import org.radarcns.detail.databinding.FragmentPrivacyPolicyBinding 22 | import org.slf4j.LoggerFactory 23 | 24 | class PrivacyPolicyFragment : Fragment() { 25 | private var mListener: OnFragmentInteractionListener? = null 26 | private var privacyPolicyUrl: String? = null 27 | private var projectId: String? = null 28 | private var userId: String? = null 29 | private var baseUrl: String? = null 30 | private var binding: FragmentPrivacyPolicyBinding? = null 31 | 32 | private var dataCollectionUrl: String? = null 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | val args = checkNotNull(arguments) { "Cannot start without AppAuthState arguments" } 37 | 38 | projectId = args.getString(PROJECT_ID_KEY) 39 | userId = args.getString(USER_ID_KEY) 40 | baseUrl = args.getString(BASE_URL_PROPERTY) 41 | privacyPolicyUrl = args.getString(PRIVACY_POLICY) //args.getString(PRIVACY_POLICY_URL_PROPERTY) 42 | dataCollectionUrl = "https://radar-base.org/docs/4048-2/" 43 | } 44 | 45 | override fun onCreateView( 46 | inflater: LayoutInflater, 47 | container: ViewGroup?, 48 | savedInstanceState: Bundle? 49 | ): View = FragmentPrivacyPolicyBinding.inflate(inflater, container, false) 50 | .also { binding = it } 51 | .root 52 | 53 | override fun onDestroyView() { 54 | super.onDestroyView() 55 | binding = null 56 | } 57 | 58 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 59 | binding?.apply { 60 | dataCollectionDescriptionStatement.setOnClickListener { openUrl(updatePrivacyStatement()) } 61 | 62 | generalPrivacyPolicyStatement.apply { 63 | visibility = if (privacyPolicyUrl != null) { 64 | setOnClickListener { openUrl(privacyPolicyUrl) } 65 | View.VISIBLE 66 | } else { 67 | View.INVISIBLE 68 | } 69 | } 70 | 71 | acceptPrivacyPolicyButton.setOnClickListener { acceptPrivacyPolicy() } 72 | rejectPrivacyPolicyButton.setOnClickListener { rejectPrivacyPolicy() } 73 | 74 | inputProjectId.text = projectId 75 | inputUserId.text = userId 76 | 77 | val baseUrl = baseUrl ?: "Unknown server" 78 | consentPrivacyPolicy.apply { 79 | text = fromHtml(R.string.consent_privacy_policy, Html.FROM_HTML_MODE_LEGACY) 80 | setOnCheckedChangeListener { _, isChecked -> 81 | acceptPrivacyPolicyButton.isEnabled = consentCollectedData.isChecked && consentServer.isChecked && isChecked 82 | } 83 | } 84 | consentCollectedData.apply{ 85 | text = fromHtml(R.string.consent_collected_data, Html.FROM_HTML_MODE_LEGACY) 86 | setOnCheckedChangeListener { _, isChecked -> 87 | acceptPrivacyPolicyButton.isEnabled = consentPrivacyPolicy.isChecked && consentServer.isChecked && isChecked 88 | } 89 | } 90 | consentServer.apply{ 91 | text = fromHtml(R.string.consent_server, baseUrl, Html.FROM_HTML_MODE_LEGACY) 92 | setOnCheckedChangeListener { _, isChecked -> 93 | acceptPrivacyPolicyButton.isEnabled = consentPrivacyPolicy.isChecked && consentCollectedData.isChecked && isChecked 94 | } 95 | } 96 | inputDestinationUrl.text = baseUrl 97 | } 98 | } 99 | 100 | 101 | @Suppress("DEPRECATION") 102 | private fun fromHtml(@StringRes resourceId: Int, vararg parameters: Any): Spanned { 103 | val s = getString(resourceId, *parameters) 104 | return Html.fromHtml(s, Html.FROM_HTML_MODE_LEGACY) 105 | } 106 | 107 | override fun onAttach(context: Context) { 108 | super.onAttach(context) 109 | if (context is OnFragmentInteractionListener) { 110 | mListener = context 111 | } else { 112 | throw RuntimeException("$context must implement OnFragmentInteractionListener") 113 | } 114 | } 115 | 116 | private fun updatePrivacyStatement(): String? { 117 | val url = dataCollectionUrl 118 | logger.info("Setting privacy policy {}", url) 119 | 120 | binding?.dataCollectionDescriptionStatement?.apply { 121 | if (url != null) { 122 | visibility = View.VISIBLE 123 | setText(R.string.collected_data_description) 124 | } else { 125 | visibility = View.INVISIBLE 126 | } 127 | } 128 | return url 129 | } 130 | 131 | 132 | private fun openUrl(url: String?) { 133 | url?.also { 134 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) 135 | } 136 | } 137 | 138 | private fun acceptPrivacyPolicy() { 139 | logger.info("Policy accepted. Redirecting to DetailedMainView...") 140 | mListener?.onAcceptPrivacyPolicy() 141 | } 142 | 143 | private fun rejectPrivacyPolicy() { 144 | logger.info("Policy rejected. Redirecting to LoginActivity...") 145 | mListener?.onRejectPrivacyPolicy() 146 | } 147 | 148 | /** 149 | * This interface must be implemented by activities that contain this 150 | * fragment to allow an interaction in this fragment to be communicated 151 | * to the activity and potentially other fragments contained in that 152 | * activity. 153 | * 154 | * 155 | * See the Android Training lesson [Communicating with Other Fragments](http://developer.android.com/training/basics/fragments/communicating.html) for more information. 156 | */ 157 | interface OnFragmentInteractionListener { 158 | fun onAcceptPrivacyPolicy() 159 | fun onRejectPrivacyPolicy() 160 | } 161 | 162 | companion object { 163 | private val logger = LoggerFactory.getLogger(PrivacyPolicyFragment::class.java) 164 | 165 | /** 166 | * Use this factory method to create a new instance of 167 | * this fragment using the provided parameters. 168 | * 169 | * @return A new instance of fragment PrivacyPolicyFragment. 170 | */ 171 | fun newInstance(context: Context, state: AppAuthState): PrivacyPolicyFragment { 172 | return PrivacyPolicyFragment().apply { 173 | arguments = Bundle().apply { 174 | putString(PROJECT_ID_KEY, state.projectId) 175 | putString(USER_ID_KEY, state.userId) 176 | putString(BASE_URL_PROPERTY, state.getAttribute(BASE_URL_PROPERTY)) 177 | putString(PRIVACY_POLICY_URL_PROPERTY, state.getAttribute(PRIVACY_POLICY_URL_PROPERTY)) 178 | putString(PRIVACY_POLICY, context.radarConfig.latestConfig.optString(PRIVACY_POLICY)) 179 | } 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarcns/detail/QrException.kt: -------------------------------------------------------------------------------- 1 | package org.radarcns.detail 2 | 3 | class QrException : IllegalArgumentException { 4 | constructor(message: String) : super(message) 5 | constructor(message: String, ex: Throwable) : super(message, ex) 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarcns/detail/RadarApplicationImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 The Hyve 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.radarcns.detail 18 | 19 | import android.graphics.Bitmap 20 | import android.graphics.Canvas 21 | import android.graphics.drawable.Drawable 22 | import android.util.Log 23 | import androidx.appcompat.content.res.AppCompatResources 24 | import androidx.core.graphics.drawable.toBitmap 25 | import androidx.lifecycle.* 26 | import com.google.firebase.analytics.FirebaseAnalytics 27 | import com.google.firebase.crashlytics.FirebaseCrashlytics 28 | import org.radarbase.android.AbstractRadarApplication 29 | import org.radarbase.android.RadarConfiguration 30 | import org.radarbase.android.config.AppConfigRadarConfiguration 31 | import org.radarbase.android.config.FirebaseRemoteConfiguration 32 | import org.radarbase.android.config.RemoteConfig 33 | import org.slf4j.impl.HandroidLoggerAdapter 34 | 35 | /** 36 | * Radar application class for the detailed application. 37 | */ 38 | class RadarApplicationImpl : AbstractRadarApplication(), LifecycleEventObserver { 39 | var enableCrashRecovery: Boolean = false 40 | private set 41 | 42 | override fun onCreate() { 43 | super.onCreate() 44 | ProcessLifecycleOwner.get().lifecycle.addObserver(this) 45 | } 46 | 47 | fun enableCrashProcessing() { 48 | enableCrashRecovery = true 49 | } 50 | 51 | override fun setupLogging() { 52 | HandroidLoggerAdapter.APP_NAME = "pRMT" 53 | HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG 54 | if (FirebaseCrashlytics.getInstance().didCrashOnPreviousExecution()) { 55 | Log.e("pRMT", "Crashed on previous boot") 56 | } 57 | HandroidLoggerAdapter.enableLoggingToFirebaseCrashlytics() 58 | } 59 | 60 | @Volatile 61 | var isInForeground: Boolean = false 62 | private set 63 | 64 | val largeIcon: Bitmap 65 | get() = AppCompatResources.getDrawable(this, R.mipmap.ic_launcher)!!.toBitmap() 66 | 67 | val smallIcon = R.drawable.ic_bt_connected 68 | 69 | override fun createRemoteConfiguration(): List = listOf( 70 | FirebaseRemoteConfiguration(this, BuildConfig.DEBUG, R.xml.remote_config_defaults), 71 | AppConfigRadarConfiguration(this) 72 | ) 73 | 74 | override fun createConfiguration(): RadarConfiguration { 75 | FirebaseAnalytics.getInstance(this).apply { 76 | setUserProperty(TEST_PHASE, if (BuildConfig.DEBUG) "dev" else "production") 77 | } 78 | return super.createConfiguration().apply { 79 | fetch() 80 | } 81 | } 82 | 83 | override val mainActivity: Class = MainActivityImpl::class.java 84 | 85 | override val loginActivity: Class = LoginActivityImpl::class.java 86 | 87 | override val authService: Class = AuthServiceImpl::class.java 88 | 89 | override val radarService: Class = RadarServiceImpl::class.java 90 | 91 | companion object { 92 | private const val TEST_PHASE = "test_phase" 93 | } 94 | 95 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 96 | isInForeground = when (event) { 97 | Lifecycle.Event.ON_STOP -> false 98 | Lifecycle.Event.ON_START -> true 99 | else -> isInForeground 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarcns/detail/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package org.radarcns.detail 2 | 3 | import android.app.AlertDialog 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.appcompat.widget.SwitchCompat 8 | import androidx.appcompat.widget.Toolbar 9 | import com.google.android.material.button.MaterialButton 10 | import org.radarbase.android.RadarApplication.Companion.radarConfig 11 | import org.radarbase.android.RadarConfiguration 12 | 13 | class SettingsActivity : AppCompatActivity() { 14 | private lateinit var config: RadarConfiguration 15 | 16 | override fun onCreate(bundle: Bundle?) { 17 | super.onCreate(bundle) 18 | config = radarConfig 19 | setContentView(R.layout.activity_settings) 20 | 21 | setSupportActionBar(findViewById(R.id.toolbar).apply { 22 | setTitle(R.string.settings) 23 | }) 24 | supportActionBar?.apply { 25 | setDisplayShowHomeEnabled(true) 26 | setDisplayHomeAsUpEnabled(true) 27 | } 28 | 29 | val enableDataButton: SwitchCompat = findViewById(R.id.enable_data_switch) 30 | enableDataButton.setOnCheckedChangeListener { _, isChecked -> 31 | config.put(RadarConfiguration.SEND_ONLY_WITH_WIFI, !isChecked) 32 | config.persistChanges() 33 | } 34 | val enableDataPriorityButton: SwitchCompat = findViewById(R.id.enable_data_high_priority_switch) 35 | enableDataPriorityButton.setOnCheckedChangeListener { _, isChecked -> 36 | config.put(RadarConfiguration.SEND_OVER_DATA_HIGH_PRIORITY, isChecked) 37 | config.persistChanges() 38 | } 39 | 40 | config.config.observe(this) { config -> 41 | val useData = !config.getBoolean(RadarConfiguration.SEND_ONLY_WITH_WIFI, true) 42 | val useHighPriority = config.getBoolean(RadarConfiguration.SEND_OVER_DATA_HIGH_PRIORITY, false) 43 | enableDataButton.isChecked = useData 44 | enableDataPriorityButton.isEnabled = useData 45 | enableDataPriorityButton.isChecked = useData && useHighPriority 46 | } 47 | 48 | findViewById(R.id.reset_settings_button).setOnClickListener { v -> startReset(v) } 49 | } 50 | 51 | private fun startReset(@Suppress("UNUSED_PARAMETER") view: View) { 52 | AlertDialog.Builder(this).apply { 53 | setTitle("Reset") 54 | setMessage("Do you really want to reset to default settings?") 55 | setIcon(android.R.drawable.ic_dialog_alert) 56 | setPositiveButton(android.R.string.ok) { _, _ -> 57 | config.reset(*MANAGED_SETTINGS) 58 | } 59 | setNegativeButton(android.R.string.cancel, null) 60 | }.show() 61 | } 62 | 63 | companion object { 64 | private val MANAGED_SETTINGS = arrayOf( 65 | RadarConfiguration.SEND_ONLY_WITH_WIFI, 66 | RadarConfiguration.SEND_OVER_DATA_HIGH_PRIORITY, 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/org/radarcns/detail/SourceRowView.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 The Hyve 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.radarcns.detail 18 | 19 | import android.app.AlertDialog 20 | import android.content.Context 21 | import android.content.SharedPreferences 22 | import android.text.InputType 23 | import android.view.LayoutInflater 24 | import android.view.View 25 | import android.view.ViewGroup 26 | import android.widget.* 27 | import org.radarbase.android.MainActivity 28 | import org.radarbase.android.source.BaseSourceState 29 | import org.radarbase.android.source.SourceProvider 30 | import org.radarbase.android.source.SourceServiceConnection 31 | import org.radarbase.android.source.SourceStatusListener 32 | import org.radarbase.android.util.Boast 33 | import org.radarbase.android.util.ChangeRunner 34 | import org.radarbase.android.widget.repeatAnimation 35 | import org.slf4j.LoggerFactory 36 | 37 | /** 38 | * Displays a single source row. 39 | */ 40 | class SourceRowView internal constructor( 41 | private val mainActivity: MainActivity, 42 | private val provider: SourceProvider<*>, root: ViewGroup 43 | ) { 44 | private val connection: SourceServiceConnection<*> = provider.connection 45 | private val mStatusIcon: ImageView 46 | private val mBatteryLabel: ImageView 47 | private val mSourceNameLabel: TextView 48 | private val devicePreferences: SharedPreferences = 49 | mainActivity.getSharedPreferences("device." + connection.serviceClassName, Context.MODE_PRIVATE) 50 | private val filter = ChangeRunner("") 51 | private var sourceState: BaseSourceState? = null 52 | private var sourceName: String? = null 53 | private val batteryLevelCache = ChangeRunner() 54 | private val sourceNameCache = ChangeRunner() 55 | private val statusCache = ChangeRunner() 56 | 57 | private val splitRegex = this.mainActivity.getString(R.string.filter_split_regex).toRegex() 58 | 59 | init { 60 | logger.info("Creating source row for provider {} and connection {}", provider, connection) 61 | val inflater = this.mainActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 62 | inflater.inflate(R.layout.activity_overview_source_row, root) 63 | 64 | (root.getChildAt(root.childCount - 1) as LinearLayout).apply { 65 | mStatusIcon = findViewById(R.id.status_icon) 66 | mSourceNameLabel = findViewById(R.id.sourceNameLabel) 67 | mBatteryLabel = findViewById(R.id.batteryStatusLabel) 68 | findViewById