From e76dcf96b0c451e46cddfa695de8feeb92533937 Mon Sep 17 00:00:00 2001 From: Andrew Lehmer Date: Wed, 26 Apr 2017 14:58:59 -0700 Subject: Import google/walt Cloned from https://github.com/google/walt.git without modification. Bug: 36896528 Test: N/A --- android/WALT/.gitignore | 10 + android/WALT/WALT.iml | 19 + android/WALT/app/.gitignore | 2 + android/WALT/app/build.gradle | 50 + android/WALT/app/proguard-rules.pro | 17 + android/WALT/app/src/main/AndroidManifest.xml | 38 + .../org/chromium/latency/walt/AboutFragment.java | 57 + .../org/chromium/latency/walt/AudioFragment.java | 310 +++++ .../java/org/chromium/latency/walt/AudioTest.java | 458 +++++++ .../org/chromium/latency/walt/AutoRunFragment.java | 233 ++++ .../java/org/chromium/latency/walt/BaseTest.java | 50 + .../chromium/latency/walt/BaseUsbConnection.java | 223 ++++ .../chromium/latency/walt/CrashLogActivity.java | 43 + .../chromium/latency/walt/CustomNumberPicker.java | 83 ++ .../chromium/latency/walt/DiagnosticsFragment.java | 82 ++ .../chromium/latency/walt/DragLatencyFragment.java | 415 ++++++ .../chromium/latency/walt/FastPathSurfaceView.java | 90 ++ .../chromium/latency/walt/FrontPageFragment.java | 53 + .../org/chromium/latency/walt/HistogramChart.java | 277 ++++ .../org/chromium/latency/walt/LogFragment.java | 84 ++ .../org/chromium/latency/walt/LogUploader.java | 83 ++ .../org/chromium/latency/walt/MainActivity.java | 536 ++++++++ .../org/chromium/latency/walt/MidiFragment.java | 146 ++ .../java/org/chromium/latency/walt/MidiTest.java | 372 ++++++ .../latency/walt/NumberPickerPreference.java | 134 ++ .../org/chromium/latency/walt/RemoteClockInfo.java | 117 ++ .../latency/walt/ScreenResponseFragment.java | 573 ++++++++ .../chromium/latency/walt/SettingsFragment.java | 102 ++ .../org/chromium/latency/walt/SimpleLogger.java | 80 ++ .../chromium/latency/walt/TapLatencyFragment.java | 306 +++++ .../chromium/latency/walt/TouchCatcherView.java | 101 ++ .../org/chromium/latency/walt/TraceLogger.java | 113 ++ .../org/chromium/latency/walt/UsMotionEvent.java | 146 ++ .../main/java/org/chromium/latency/walt/Utils.java | 187 +++ .../org/chromium/latency/walt/WaltConnection.java | 43 + .../java/org/chromium/latency/walt/WaltDevice.java | 411 ++++++ .../chromium/latency/walt/WaltTcpConnection.java | 238 ++++ .../chromium/latency/walt/WaltUsbConnection.java | 182 +++ .../walt/programmer/BootloaderConnection.java | 80 ++ .../latency/walt/programmer/DeviceConstants.java | 22 + .../latency/walt/programmer/FirmwareImage.java | 127 ++ .../latency/walt/programmer/Programmer.java | 104 ++ android/WALT/app/src/main/jni/Android.mk | 30 + android/WALT/app/src/main/jni/Application.mk | 17 + android/WALT/app/src/main/jni/Makefile | 17 + android/WALT/app/src/main/jni/README.md | 112 ++ android/WALT/app/src/main/jni/findteensy.py | 30 + android/WALT/app/src/main/jni/player.c | 520 ++++++++ android/WALT/app/src/main/jni/sync_clock.c | 327 +++++ android/WALT/app/src/main/jni/sync_clock.h | 50 + android/WALT/app/src/main/jni/sync_clock_jni.c | 62 + android/WALT/app/src/main/jni/sync_clock_linux.c | 80 ++ .../WALT/app/src/main/res/color/button_tint.xml | 5 + android/WALT/app/src/main/res/drawable/border.xml | 6 + .../res/drawable/ic_brightness_7_black_24dp.xml | 9 + .../drawable/ic_brightness_medium_black_24dp.xml | 9 + .../src/main/res/drawable/ic_check_black_24dp.xml | 9 + .../res/drawable/ic_chevron_left_black_24dp.xml | 9 + .../main/res/drawable/ic_equalizer_black_24dp.xml | 9 + .../res/drawable/ic_file_upload_black_24dp.xml | 9 + .../src/main/res/drawable/ic_help_black_24dp.xml | 9 + .../res/drawable/ic_help_outline_black_24dp.xml | 9 + .../res/drawable/ic_import_export_black_24dp.xml | 9 + .../src/main/res/drawable/ic_input_black_24dp.xml | 9 + .../src/main/res/drawable/ic_mic_black_24dp.xml | 9 + .../main/res/drawable/ic_music_note_black_24dp.xml | 9 + .../src/main/res/drawable/ic_output_black_24dp.xml | 9 + .../main/res/drawable/ic_play_arrow_black_24dp.xml | 9 + .../ic_radio_button_checked_black_24dp.xml | 9 + .../main/res/drawable/ic_receipt_black_24dp.xml | 9 + .../main/res/drawable/ic_refresh_black_24dp.xml | 9 + .../main/res/drawable/ic_schedule_black_24dp.xml | 10 + .../src/main/res/drawable/ic_search_black_24dp.xml | 9 + .../main/res/drawable/ic_settings_black_24dp.xml | 9 + .../src/main/res/drawable/ic_share_black_24dp.xml | 9 + .../src/main/res/drawable/ic_stop_black_24dp.xml | 9 + .../main/res/drawable/ic_swap_horiz_black_24dp.xml | 9 + .../main/res/drawable/ic_swap_vert_black_24dp.xml | 9 + .../drawable/ic_system_update_alt_black_24dp.xml | 9 + .../main/res/drawable/ic_timelapse_black_24dp.xml | 9 + .../src/main/res/drawable/ic_usb_black_24dp.xml | 9 + .../main/res/drawable/ic_volume_up_black_24dp.xml | 9 + .../app/src/main/res/layout/activity_crash_log.xml | 11 + .../WALT/app/src/main/res/layout/activity_main.xml | 27 + .../WALT/app/src/main/res/layout/dialog_upload.xml | 22 + .../app/src/main/res/layout/fragment_about.xml | 57 + .../app/src/main/res/layout/fragment_audio.xml | 68 + .../app/src/main/res/layout/fragment_auto_run.xml | 15 + .../src/main/res/layout/fragment_diagnostics.xml | 206 +++ .../src/main/res/layout/fragment_drag_latency.xml | 123 ++ .../src/main/res/layout/fragment_front_page.xml | 248 ++++ .../WALT/app/src/main/res/layout/fragment_log.xml | 17 + .../WALT/app/src/main/res/layout/fragment_midi.xml | 54 + .../main/res/layout/fragment_screen_response.xml | 76 ++ .../src/main/res/layout/fragment_tap_latency.xml | 99 ++ android/WALT/app/src/main/res/layout/histogram.xml | 23 + .../WALT/app/src/main/res/layout/line_chart.xml | 23 + .../src/main/res/layout/numberpicker_dialog.xml | 19 + android/WALT/app/src/main/res/layout/toolbar.xml | 8 + android/WALT/app/src/main/res/menu/menu_main.xml | 28 + .../app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1975 bytes .../app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1353 bytes .../app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2633 bytes .../app/src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3964 bytes android/WALT/app/src/main/res/raw/walt.hex | 1397 ++++++++++++++++++++ .../WALT/app/src/main/res/values-w820dp/dimens.xml | 6 + android/WALT/app/src/main/res/values/attrs.xml | 12 + android/WALT/app/src/main/res/values/color.xml | 9 + android/WALT/app/src/main/res/values/dimens.xml | 5 + android/WALT/app/src/main/res/values/strings.xml | 45 + android/WALT/app/src/main/res/values/styles.xml | 61 + .../WALT/app/src/main/res/xml/device_filter.xml | 14 + android/WALT/app/src/main/res/xml/preferences.xml | 135 ++ .../chromium/latency/walt/HistogramChartTest.java | 81 ++ .../org/chromium/latency/walt/TraceLoggerTest.java | 46 + .../java/org/chromium/latency/walt/UtilsTest.java | 162 +++ android/WALT/build.gradle | 20 + android/WALT/gradle.properties | 18 + android/WALT/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53637 bytes .../WALT/gradle/wrapper/gradle-wrapper.properties | 6 + android/WALT/gradlew | 160 +++ android/WALT/gradlew.bat | 90 ++ android/WALT/settings.gradle | 1 + 123 files changed, 11698 insertions(+) create mode 100644 android/WALT/.gitignore create mode 100644 android/WALT/WALT.iml create mode 100644 android/WALT/app/.gitignore create mode 100644 android/WALT/app/build.gradle create mode 100644 android/WALT/app/proguard-rules.pro create mode 100644 android/WALT/app/src/main/AndroidManifest.xml create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/AboutFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/AudioFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/AudioTest.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/AutoRunFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/BaseTest.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/BaseUsbConnection.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/CrashLogActivity.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/CustomNumberPicker.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/DiagnosticsFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/DragLatencyFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/FastPathSurfaceView.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/FrontPageFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/HistogramChart.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/LogFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/LogUploader.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/MidiFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/MidiTest.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/NumberPickerPreference.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/RemoteClockInfo.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/SettingsFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/SimpleLogger.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/TapLatencyFragment.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/TouchCatcherView.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/TraceLogger.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/UsMotionEvent.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/Utils.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/WaltConnection.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/WaltTcpConnection.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/WaltUsbConnection.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/BootloaderConnection.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/DeviceConstants.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/FirmwareImage.java create mode 100644 android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/Programmer.java create mode 100644 android/WALT/app/src/main/jni/Android.mk create mode 100644 android/WALT/app/src/main/jni/Application.mk create mode 100644 android/WALT/app/src/main/jni/Makefile create mode 100644 android/WALT/app/src/main/jni/README.md create mode 100755 android/WALT/app/src/main/jni/findteensy.py create mode 100644 android/WALT/app/src/main/jni/player.c create mode 100644 android/WALT/app/src/main/jni/sync_clock.c create mode 100644 android/WALT/app/src/main/jni/sync_clock.h create mode 100644 android/WALT/app/src/main/jni/sync_clock_jni.c create mode 100644 android/WALT/app/src/main/jni/sync_clock_linux.c create mode 100644 android/WALT/app/src/main/res/color/button_tint.xml create mode 100644 android/WALT/app/src/main/res/drawable/border.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_brightness_7_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_brightness_medium_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_check_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_equalizer_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_file_upload_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_help_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_help_outline_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_import_export_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_input_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_mic_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_music_note_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_output_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_radio_button_checked_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_receipt_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_refresh_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_schedule_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_search_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_settings_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_share_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_stop_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_swap_horiz_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_swap_vert_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_system_update_alt_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_timelapse_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_usb_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/drawable/ic_volume_up_black_24dp.xml create mode 100644 android/WALT/app/src/main/res/layout/activity_crash_log.xml create mode 100644 android/WALT/app/src/main/res/layout/activity_main.xml create mode 100644 android/WALT/app/src/main/res/layout/dialog_upload.xml create mode 100644 android/WALT/app/src/main/res/layout/fragment_about.xml create mode 100644 android/WALT/app/src/main/res/layout/fragment_audio.xml create mode 100644 android/WALT/app/src/main/res/layout/fragment_auto_run.xml create mode 100644 android/WALT/app/src/main/res/layout/fragment_diagnostics.xml create mode 100644 android/WALT/app/src/main/res/layout/fragment_drag_latency.xml create mode 100644 android/WALT/app/src/main/res/layout/fragment_front_page.xml create mode 100644 android/WALT/app/src/main/res/layout/fragment_log.xml create mode 100644 android/WALT/app/src/main/res/layout/fragment_midi.xml create mode 100644 android/WALT/app/src/main/res/layout/fragment_screen_response.xml create mode 100644 android/WALT/app/src/main/res/layout/fragment_tap_latency.xml create mode 100644 android/WALT/app/src/main/res/layout/histogram.xml create mode 100644 android/WALT/app/src/main/res/layout/line_chart.xml create mode 100644 android/WALT/app/src/main/res/layout/numberpicker_dialog.xml create mode 100644 android/WALT/app/src/main/res/layout/toolbar.xml create mode 100644 android/WALT/app/src/main/res/menu/menu_main.xml create mode 100644 android/WALT/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/WALT/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/WALT/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/WALT/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/WALT/app/src/main/res/raw/walt.hex create mode 100644 android/WALT/app/src/main/res/values-w820dp/dimens.xml create mode 100644 android/WALT/app/src/main/res/values/attrs.xml create mode 100644 android/WALT/app/src/main/res/values/color.xml create mode 100644 android/WALT/app/src/main/res/values/dimens.xml create mode 100644 android/WALT/app/src/main/res/values/strings.xml create mode 100644 android/WALT/app/src/main/res/values/styles.xml create mode 100644 android/WALT/app/src/main/res/xml/device_filter.xml create mode 100644 android/WALT/app/src/main/res/xml/preferences.xml create mode 100644 android/WALT/app/src/test/java/org/chromium/latency/walt/HistogramChartTest.java create mode 100644 android/WALT/app/src/test/java/org/chromium/latency/walt/TraceLoggerTest.java create mode 100644 android/WALT/app/src/test/java/org/chromium/latency/walt/UtilsTest.java create mode 100644 android/WALT/build.gradle create mode 100644 android/WALT/gradle.properties create mode 100644 android/WALT/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/WALT/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/WALT/gradlew create mode 100644 android/WALT/gradlew.bat create mode 100644 android/WALT/settings.gradle (limited to 'android/WALT') diff --git a/android/WALT/.gitignore b/android/WALT/.gitignore new file mode 100644 index 0000000..579b981 --- /dev/null +++ b/android/WALT/.gitignore @@ -0,0 +1,10 @@ +.gradle +.idea +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +/app/src/main/obj +/app/src/main/libs diff --git a/android/WALT/WALT.iml b/android/WALT/WALT.iml new file mode 100644 index 0000000..628d221 --- /dev/null +++ b/android/WALT/WALT.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/WALT/app/.gitignore b/android/WALT/app/.gitignore new file mode 100644 index 0000000..cc037c4 --- /dev/null +++ b/android/WALT/app/.gitignore @@ -0,0 +1,2 @@ +/build +app.iml diff --git a/android/WALT/app/build.gradle b/android/WALT/app/build.gradle new file mode 100644 index 0000000..531142e --- /dev/null +++ b/android/WALT/app/build.gradle @@ -0,0 +1,50 @@ +apply plugin: 'com.android.model.application' + +model { + android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + + defaultConfig { + applicationId "org.chromium.latency.walt" + minSdkVersion.apiLevel 17 + targetSdkVersion.apiLevel 23 + versionCode 8 + versionName "0.1.7" + } + ndk { + moduleName "sync_clock_jni" + CFlags.addAll "-I${project.rootDir}/app/src/main/jni".toString(), "-g", "-DUSE_LIBLOG", "-Werror" + ldLibs.addAll "OpenSLES", "log" + } + buildTypes { + release { + minifyEnabled false + proguardFiles.add(file("proguard-rules.pro")) + } + debug { + ndk { + debuggable true + } + } + } + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:25.1.0' + compile 'com.android.support:design:25.1.0' + compile 'com.android.support:preference-v7:25.1.0' + compile 'com.android.support:preference-v14:25.1.0' + compile 'com.github.PhilJay:MPAndroidChart:v3.0.1' + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile ('org.powermock:powermock-api-mockito:1.6.2') { + exclude module: 'hamcrest-core' + exclude module: 'objenesis' + } + testCompile ('org.powermock:powermock-module-junit4:1.6.2') { + exclude module: 'hamcrest-core' + exclude module: 'objenesis' + } +} diff --git a/android/WALT/app/proguard-rules.pro b/android/WALT/app/proguard-rules.pro new file mode 100644 index 0000000..2d2fcf0 --- /dev/null +++ b/android/WALT/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/android/WALT/app/src/main/AndroidManifest.xml b/android/WALT/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..869b5e8 --- /dev/null +++ b/android/WALT/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/AboutFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/AboutFragment.java new file mode 100644 index 0000000..08b4e4f --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/AboutFragment.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.chromium.latency.walt; + + +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + + +/** + * A screen that shows information about WALT. + */ +public class AboutFragment extends Fragment { + + public AboutFragment() { + // Required empty public constructor + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_about, container, false); + } + + @Override + public void onResume() { + super.onResume(); + TextView textView = (TextView) getActivity().findViewById(R.id.txt_build_info); + String text = String.format("WALT v%s (versionCode=%d)\n", + BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE); + text += "WALT protocol version: " + WaltDevice.PROTOCOL_VERSION + "\n"; + text += "Android Build ID: " + Build.DISPLAY + "\n"; + text += "Android API Level: " + Build.VERSION.SDK_INT + "\n"; + text += "Android OS Version: " + System.getProperty("os.version"); + textView.setText(text); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/AudioFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/AudioFragment.java new file mode 100644 index 0000000..65452ff --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/AudioFragment.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.media.AudioManager; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.text.method.ScrollingMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TextView; + +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.Description; +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.chromium.latency.walt.Utils.getIntPreference; + +/** + * A simple {@link Fragment} subclass. + */ +public class AudioFragment extends Fragment implements View.OnClickListener, + BaseTest.TestStateListener { + + enum AudioTestType { + CONTINUOUS_PLAYBACK, + CONTINUOUS_RECORDING, + COLD_PLAYBACK, + COLD_RECORDING, + DISPLAY_WAVEFORM + } + + private SimpleLogger logger; + private TextView textView; + private AudioTest audioTest; + private View startButton; + private View stopButton; + private Spinner modeSpinner; + private LineChart chart; + private HistogramChart latencyChart; + private View chartLayout; + + private static final int PERMISSION_REQUEST_RECORD_AUDIO = 1; + + public AudioFragment() { + // Required empty public constructor + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + logger = SimpleLogger.getInstance(getContext()); + + audioTest = new AudioTest(getActivity()); + audioTest.setTestStateListener(this); + + // Inflate the layout for this fragment + View view = inflater.inflate(R.layout.fragment_audio, container, false); + textView = (TextView) view.findViewById(R.id.txt_box_audio); + textView.setMovementMethod(new ScrollingMovementMethod()); + startButton = view.findViewById(R.id.button_start_audio); + stopButton = view.findViewById(R.id.button_stop_audio); + chartLayout = view.findViewById(R.id.chart_layout); + chart = (LineChart) view.findViewById(R.id.chart); + latencyChart = (HistogramChart) view.findViewById(R.id.latency_chart); + + view.findViewById(R.id.button_close_chart).setOnClickListener(this); + enableButtons(); + + // Configure the audio mode spinner + modeSpinner = (Spinner) view.findViewById(R.id.spinner_audio_mode); + ArrayAdapter modeAdapter = ArrayAdapter.createFromResource(getContext(), + R.array.audio_mode_array, android.R.layout.simple_spinner_item); + modeAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item); + modeSpinner.setAdapter(modeAdapter); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + // Register this fragment class as the listener for some button clicks + startButton.setOnClickListener(this); + stopButton.setOnClickListener(this); + + textView.setText(logger.getLogText()); + logger.registerReceiver(logReceiver); + } + + @Override + public void onPause() { + logger.unregisterReceiver(logReceiver); + super.onPause(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + audioTest.teardown(); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.button_start_audio: + chartLayout.setVisibility(View.GONE); + disableButtons(); + AudioTestType testType = getSelectedTestType(); + switch (testType) { + case CONTINUOUS_PLAYBACK: + case CONTINUOUS_RECORDING: + case DISPLAY_WAVEFORM: + audioTest.setAudioMode(AudioTest.AudioMode.CONTINUOUS); + audioTest.setPeriod(AudioTest.CONTINUOUS_TEST_PERIOD); + break; + case COLD_PLAYBACK: + case COLD_RECORDING: + audioTest.setAudioMode(AudioTest.AudioMode.CONTINUOUS); + audioTest.setPeriod(AudioTest.COLD_TEST_PERIOD); + break; + } + if (testType == AudioTestType.DISPLAY_WAVEFORM) { + // Only need to record 1 beep to display wave + audioTest.setRecordingRepetitions(1); + } else { + audioTest.setRecordingRepetitions( + getIntPreference(getContext(), R.string.preference_audio_in_reps, 5)); + } + if (testType == AudioTestType.CONTINUOUS_PLAYBACK || + testType == AudioTestType.COLD_PLAYBACK || + testType == AudioTestType.CONTINUOUS_RECORDING || + testType == AudioTestType.COLD_RECORDING) { + latencyChart.setVisibility(View.VISIBLE); + latencyChart.clearData(); + latencyChart.setLegendEnabled(false); + final String description = + getResources().getStringArray(R.array.audio_mode_array)[ + modeSpinner.getSelectedItemPosition()] + " [ms]"; + latencyChart.setDescription(description); + } + switch (testType) { + case CONTINUOUS_RECORDING: + case COLD_RECORDING: + case DISPLAY_WAVEFORM: + attemptRecordingTest(); + break; + case CONTINUOUS_PLAYBACK: + case COLD_PLAYBACK: + // Set media volume to max + AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); + am.setStreamVolume(AudioManager.STREAM_MUSIC, am.getStreamMaxVolume(AudioManager.STREAM_MUSIC), 0); + audioTest.beginPlaybackMeasurement(); + break; + } + break; + case R.id.button_stop_audio: + audioTest.stopTest(); + break; + case R.id.button_close_chart: + chartLayout.setVisibility(View.GONE); + break; + } + } + + private AudioTestType getSelectedTestType() { + return AudioTestType.values()[modeSpinner.getSelectedItemPosition()]; + } + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String msg = intent.getStringExtra("message"); + textView.append(msg + "\n"); + } + }; + + private void attemptRecordingTest() { + // first see if we already have permission to record audio + int currentPermission = ContextCompat.checkSelfPermission(this.getContext(), + Manifest.permission.RECORD_AUDIO); + if (currentPermission == PackageManager.PERMISSION_GRANTED) { + disableButtons(); + audioTest.beginRecordingMeasurement(); + } else { + requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, + PERMISSION_REQUEST_RECORD_AUDIO); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + switch (requestCode) { + case PERMISSION_REQUEST_RECORD_AUDIO: + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + disableButtons(); + audioTest.beginRecordingMeasurement(); + } else { + logger.log("Could not get permission to record audio"); + } + return; + } + } + + @Override + public void onTestStopped() { + if (getSelectedTestType() == AudioTestType.DISPLAY_WAVEFORM) { + drawWaveformChart(); + } else { + if (!audioTest.deltas_mic.isEmpty()) { + latencyChart.setLegendEnabled(true); + latencyChart.setLabel(String.format(Locale.US, "Median=%.1f ms", Utils.median(audioTest.deltas_mic))); + } else if (!audioTest.deltas_queue2wire.isEmpty()) { + latencyChart.setLegendEnabled(true); + latencyChart.setLabel(String.format(Locale.US, "Median=%.1f ms", Utils.median(audioTest.deltas_queue2wire))); + } + } + LogUploader.uploadIfAutoEnabled(getContext()); + enableButtons(); + } + + @Override + public void onTestStoppedWithError() { + enableButtons(); + latencyChart.setVisibility(View.GONE); + } + + @Override + public void onTestPartialResult(double value) { + latencyChart.addEntry(value); + } + + private void drawWaveformChart() { + final short[] wave = AudioTest.getRecordedWave(); + List entries = new ArrayList<>(); + int frameRate = audioTest.getOptimalFrameRate(); + for (int i = 0; i < wave.length; i++) { + float timeStamp = (float) i / frameRate * 1000f; + entries.add(new Entry(timeStamp, (float) wave[i])); + } + LineDataSet dataSet = new LineDataSet(entries, "Waveform"); + dataSet.setColor(Color.BLACK); + dataSet.setValueTextColor(Color.BLACK); + dataSet.setCircleColor(ContextCompat.getColor(getContext(), R.color.DarkGreen)); + dataSet.setCircleRadius(1.5f); + dataSet.setCircleColorHole(Color.DKGRAY); + LineData lineData = new LineData(dataSet); + chart.setData(lineData); + + LimitLine line = new LimitLine(audioTest.getThreshold(), "Threshold"); + line.setLineColor(Color.RED); + line.setLabelPosition(LimitLine.LimitLabelPosition.LEFT_TOP); + line.setLineWidth(2f); + line.setTextColor(Color.DKGRAY); + line.setTextSize(10f); + chart.getAxisLeft().addLimitLine(line); + + final Description desc = new Description(); + desc.setText("Wave [digital level -32768 to +32767] vs. Time [ms]"); + desc.setTextSize(12f); + chart.setDescription(desc); + chart.getLegend().setEnabled(false); + chart.invalidate(); + chartLayout.setVisibility(View.VISIBLE); + } + + private void disableButtons() { + startButton.setEnabled(false); + stopButton.setEnabled(true); + } + + private void enableButtons() { + startButton.setEnabled(true); + stopButton.setEnabled(false); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/AudioTest.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/AudioTest.java new file mode 100644 index 0000000..6987d7c --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/AudioTest.java @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Handler; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; + +import static org.chromium.latency.walt.Utils.getIntPreference; + +class AudioTest extends BaseTest { + + static { + System.loadLibrary("sync_clock_jni"); + } + + static final int CONTINUOUS_TEST_PERIOD = 500; + static final int COLD_TEST_PERIOD = 5000; + + enum AudioMode {COLD, CONTINUOUS} + + private Handler handler = new Handler(); + private boolean userStoppedTest = false; + + // Sound params + private final double duration = 0.3; // seconds + private final int sampleRate = 8000; + private final int numSamples = (int) (duration * sampleRate); + private final byte generatedSnd[] = new byte[2 * numSamples]; + private final double freqOfTone = 880; // hz + + private AudioMode audioMode; + private int period = 500; // time between runs in ms + + // Audio in + private long last_tb = 0; + private int msToRecord = 1000; + private final int frameRate; + private final int framesPerBuffer; + + private int initiatedBeeps, detectedBeeps; + private int playbackRepetitions; + private static final int playbackSyncAfterRepetitions = 20; + + // Audio out + private int requestedBeeps; + private int recordingRepetitions; + private static int recorderSyncAfterRepetitions = 10; + private final int threshold; + + ArrayList deltas_mic = new ArrayList<>(); + private ArrayList deltas_play2queue = new ArrayList<>(); + ArrayList deltas_queue2wire = new ArrayList<>(); + private ArrayList deltasJ2N = new ArrayList<>(); + + long lastBeepTime; + + public static native long playTone(); + public static native void startWarmTest(); + public static native void stopTests(); + public static native void createEngine(); + public static native void destroyEngine(); + public static native void createBufferQueueAudioPlayer(int frameRate, int framesPerBuffer); + + public static native void startRecording(); + public static native void createAudioRecorder(int frameRate, int framesToRecord); + public static native short[] getRecordedWave(); + public static native long getTeRec(); + public static native long getTcRec(); + public static native long getTePlay(); + + AudioTest(Context context) { + super(context); + playbackRepetitions = getIntPreference(context, R.string.preference_audio_out_reps, 10); + recordingRepetitions = getIntPreference(context, R.string.preference_audio_in_reps, 5); + threshold = getIntPreference(context, R.string.preference_audio_in_threshold, 5000); + + //Check for optimal output sample rate and buffer size + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + String frameRateStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); + String framesPerBufferStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); + logger.log("Optimal frame rate is: " + frameRateStr); + logger.log("Optimal frames per buffer is: " + framesPerBufferStr); + + //Convert to ints + frameRate = Integer.parseInt(frameRateStr); + framesPerBuffer = Integer.parseInt(framesPerBufferStr); + + //Create the audio engine + createEngine(); + createBufferQueueAudioPlayer(frameRate, framesPerBuffer); + logger.log("Audio engine created"); + } + + AudioTest(Context context, AutoRunFragment.ResultHandler resultHandler) { + this(context); + this.resultHandler = resultHandler; + } + + void setPlaybackRepetitions(int beepCount) { + playbackRepetitions = beepCount; + } + + void setRecordingRepetitions(int beepCount) { + recordingRepetitions = beepCount; + } + + void setPeriod(int period) { + this.period = period; + } + + void setAudioMode(AudioMode mode) { + audioMode = mode; + } + + AudioMode getAudioMode() { + return audioMode; + } + + int getOptimalFrameRate() { + return frameRate; + } + + int getThreshold() { + return threshold; + } + + void stopTest() { + userStoppedTest = true; + } + + void teardown() { + destroyEngine(); + logger.log("Audio engine destroyed"); + } + + void beginRecordingMeasurement() { + userStoppedTest = false; + deltas_mic.clear(); + deltas_play2queue.clear(); + deltas_queue2wire.clear(); + deltasJ2N.clear(); + + int framesToRecord = (int) (0.001 * msToRecord * frameRate); + createAudioRecorder(frameRate, framesToRecord); + logger.log("Audio recorder created; starting test"); + + requestedBeeps = 0; + doRecordingTestRepetition(); + } + + private void doRecordingTestRepetition() { + if (requestedBeeps >= recordingRepetitions || userStoppedTest) { + finishRecordingMeasurement(); + return; + } + + if (requestedBeeps % recorderSyncAfterRepetitions == 0) { + try { + waltDevice.syncClock(); + } catch (IOException e) { + logger.log("Error syncing clocks: " + e.getMessage()); + if (testStateListener != null) testStateListener.onTestStoppedWithError(); + return; + } + } + + requestedBeeps++; + startRecording(); + switch (audioMode) { + case CONTINUOUS: + handler.postDelayed(requestBeepRunnable, msToRecord / 2); + break; + case COLD: // TODO: find a more accurate method to measure cold input latency + requestBeepRunnable.run(); + break; + } + handler.postDelayed(stopBeepRunnable, msToRecord); + } + + void beginPlaybackMeasurement() { + userStoppedTest = false; + if (audioMode == AudioMode.CONTINUOUS) { + startWarmTest(); + } + try { + waltDevice.syncClock(); + waltDevice.startListener(); + } catch (IOException e) { + logger.log("Error starting test: " + e.getMessage()); + if (testStateListener != null) testStateListener.onTestStoppedWithError(); + return; + } + deltas_mic.clear(); + deltas_play2queue.clear(); + deltas_queue2wire.clear(); + deltasJ2N.clear(); + + logger.log("Starting playback test"); + + initiatedBeeps = 0; + detectedBeeps = 0; + + waltDevice.setTriggerHandler(playbackTriggerHandler); + + handler.postDelayed(playBeepRunnable, 300); + } + + private WaltDevice.TriggerHandler playbackTriggerHandler = new WaltDevice.TriggerHandler() { + @Override + public void onReceive(WaltDevice.TriggerMessage tmsg) { + // remove the far away playBeep callback(s) + handler.removeCallbacks(playBeepRunnable); + + detectedBeeps++; + long enqueueTime = getTePlay() - waltDevice.clock.baseTime; + double dt_play2queue = (enqueueTime - lastBeepTime) / 1000.; + deltas_play2queue.add(dt_play2queue); + + double dt_queue2wire = (tmsg.t - enqueueTime) / 1000.; + deltas_queue2wire.add(dt_queue2wire); + + logger.log(String.format(Locale.US, + "Beep detected, initiatedBeeps=%d, detectedBeeps=%d\n" + + "dt native playTone to Enqueue = %.2f ms\n" + + "dt Enqueue to wire = %.2f ms\n", + initiatedBeeps, detectedBeeps, + dt_play2queue, + dt_queue2wire + )); + + if (traceLogger != null) { + traceLogger.log(lastBeepTime + waltDevice.clock.baseTime, + enqueueTime + waltDevice.clock.baseTime, + "Play-to-queue", + "Bar starts at play time, ends when enqueued"); + traceLogger.log(enqueueTime + waltDevice.clock.baseTime, + tmsg.t + waltDevice.clock.baseTime, + "Enqueue-to-wire", + "Bar starts at enqueue time, ends when beep is detected"); + } + if (testStateListener != null) testStateListener.onTestPartialResult(dt_queue2wire); + + // Schedule another beep soon-ish + handler.postDelayed(playBeepRunnable, (long) (period + Math.random() * 50 - 25)); + } + }; + + private Runnable playBeepRunnable = new Runnable() { + @Override + public void run() { + // debug: logger.log("\nPlaying tone..."); + + // Check if we saw some transitions without beeping, might be noise audio cable. + if (initiatedBeeps == 0 && detectedBeeps > 1) { + logger.log("Unexpected beeps detected, noisy cable?"); + return; + } + + if (initiatedBeeps >= playbackRepetitions || userStoppedTest) { + finishPlaybackMeasurement(); + return; + } + + initiatedBeeps++; + + if (initiatedBeeps % playbackSyncAfterRepetitions == 0) { + try { + waltDevice.stopListener(); + waltDevice.syncClock(); + waltDevice.startListener(); + } catch (IOException e) { + logger.log("Error re-syncing clock: " + e.getMessage()); + finishPlaybackMeasurement(); + return; + } + } + + try { + waltDevice.command(WaltDevice.CMD_AUDIO); + } catch (IOException e) { + logger.log("Error sending command AUDIO: " + e.getMessage()); + return; + } + long javaBeepTime = waltDevice.clock.micros(); + lastBeepTime = playTone() - waltDevice.clock.baseTime; + double dtJ2N = (lastBeepTime - javaBeepTime)/1000.; + deltasJ2N.add(dtJ2N); + if (traceLogger != null) { + traceLogger.log(javaBeepTime + waltDevice.clock.baseTime, + lastBeepTime + waltDevice.clock.baseTime, "Java-to-native", + "Bar starts when Java tells native to beep and ends when buffer written in native"); + } + logger.log(String.format(Locale.US, + "Called playTone(), dt Java to native = %.3f ms", + dtJ2N + )); + + + // Repost doBeep to some far away time to blink again even if nothing arrives from + // Teensy. This callback will almost always get cancelled by onIncomingTimestamp() + handler.postDelayed(playBeepRunnable, (long) (period * 3 + Math.random() * 100 - 50)); + + } + }; + + + private Runnable requestBeepRunnable = new Runnable() { + @Override + public void run() { + // logger.log("\nRequesting beep from WALT..."); + String s; + try { + s = waltDevice.command(WaltDevice.CMD_BEEP); + } catch (IOException e) { + logger.log("Error sending command BEEP: " + e.getMessage()); + return; + } + last_tb = Integer.parseInt(s); + logger.log("Beeped, reply: " + s); + handler.postDelayed(processRecordingRunnable, (long) (msToRecord * 2 + Math.random() * 100 - 50)); + } + }; + + private Runnable stopBeepRunnable = new Runnable() { + @Override + public void run() { + try { + waltDevice.command(WaltDevice.CMD_BEEP_STOP); + } catch (IOException e) { + logger.log("Error stopping tone from WALT: " + e.getMessage()); + } + } + }; + + private Runnable processRecordingRunnable = new Runnable() { + @Override + public void run() { + long te = getTeRec() - waltDevice.clock.baseTime; // When a buffer was enqueued for recording + long tc = getTcRec() - waltDevice.clock.baseTime; // When callback receiving a recorded buffer fired + long tb = last_tb; // When WALT started a beep (according to WALT clock) + short[] wave = getRecordedWave(); + int noisyAtFrame = 0; // First frame when some noise starts + while (noisyAtFrame < wave.length && wave[noisyAtFrame] < threshold) + noisyAtFrame++; + if (noisyAtFrame == wave.length) { + logger.log("WARNING: No sound detected"); + doRecordingTestRepetition(); + return; + } + + // Length of recorded buffer + double duration_us = wave.length * 1e6 / frameRate; + + // Duration in microseconds of the initial silent part of the buffer, and the remaining + // part after the beep started. + double silent_us = noisyAtFrame * 1e6 / frameRate; + double remaining_us = duration_us - silent_us; + + // Time from the last frame in the buffer until the callback receiving the buffer fired + double latencyCb_ms = (tc - tb - remaining_us) / 1000.; + + // Time from the moment a buffer was enqueued for recording until the first frame in + // the buffer was recorded + double latencyEnqueue_ms = (tb - te - silent_us) / 1000.; + + logger.log(String.format(Locale.US, + "Processed: L_cb = %.3f ms, L_eq = %.3f ms, noisy frame = %d", + latencyCb_ms, + latencyEnqueue_ms, + noisyAtFrame + )); + + if (testStateListener != null) testStateListener.onTestPartialResult(latencyCb_ms); + if (traceLogger != null) { + traceLogger.log((long) (tb + waltDevice.clock.baseTime + remaining_us), + tc + waltDevice.clock.baseTime, + "Beep-to-rec-callback", + "Bar starts when WALT plays beep and ends when recording callback received"); + } + + deltas_mic.add(latencyCb_ms); + doRecordingTestRepetition(); + } + }; + + private void finishPlaybackMeasurement() { + stopTests(); + waltDevice.stopListener(); + waltDevice.clearTriggerHandler(); + waltDevice.checkDrift(); + + // Debug: logger.log("deltas_play2queue = array(" + deltas_play2queue.toString() +")"); + logger.log(String.format(Locale.US, + "\n%s audio playback results:\n" + + "Detected %d beeps out of %d initiated\n" + + "Median Java to native time is %.3f ms\n" + + "Median native playTone to Enqueue time is %.1f ms\n" + + "Buffer length is %d frames at %d Hz = %.2f ms\n" + + "-------------------------------\n" + + "Median time from Enqueue to wire is %.1f ms\n" + + "-------------------------------\n", + audioMode == AudioMode.COLD? "Cold" : "Continuous", + detectedBeeps, initiatedBeeps, + Utils.median(deltasJ2N), + Utils.median(deltas_play2queue), + framesPerBuffer, frameRate, 1000.0 / frameRate * framesPerBuffer, + Utils.median(deltas_queue2wire) + )); + + if (resultHandler != null) { + resultHandler.onResult(deltas_play2queue, deltas_queue2wire); + } + if (testStateListener != null) testStateListener.onTestStopped(); + if (traceLogger != null) traceLogger.flush(context); + } + + private void finishRecordingMeasurement() { + waltDevice.checkDrift(); + + // Debug: logger.log("deltas_mic: " + deltas_mic.toString()); + + logger.log(String.format(Locale.US, + "\nAudio recording/microphone results:\n" + + "Recorded %d beeps.\n" + + "-------------------------------\n" + + "Median callback latency - " + + "time from sampling the last frame to recorder callback is %.1f ms\n" + + "-------------------------------\n", + deltas_mic.size(), + Utils.median(deltas_mic) + )); + + if (resultHandler != null) { + resultHandler.onResult(deltas_mic); + } + if (testStateListener != null) testStateListener.onTestStopped(); + if (traceLogger != null) traceLogger.flush(context); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/AutoRunFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/AutoRunFragment.java new file mode 100644 index 0000000..f2f2a7f --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/AutoRunFragment.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.text.method.ScrollingMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.Iterator; + +public class AutoRunFragment extends Fragment { + + static final String TEST_ACTION = "org.chromium.latency.walt.START_TEST"; + static final String MODE_COLD = "Cold"; + + private WaltDevice waltDevice; + private AudioTest toTearDown; // TODO: figure out a better way to destroy the engine + Handler handler = new Handler(); + + private class AudioResultHandler implements ResultHandler { + private FileWriter fileWriter; + + AudioResultHandler(String fileName) throws IOException { + fileWriter = new FileWriter(fileName); + } + + @Override + public void onResult(Iterable[] results) { + if (results.length == 0) { + logger.log("Can't write empty data!"); + return; + } + logger.log("Writing data file"); + + Iterator its[] = new Iterator[results.length]; + + for (int i = 0; i < results.length; i++) { + its[i] = results[i].iterator(); + } + try { + while (its[0].hasNext()) { + for (Iterator it : its) { + if (it.hasNext()) { + fileWriter.write(it.next().toString() + ","); + } + } + fileWriter.write("\n"); + } + } catch (IOException e) { + logger.log("Error writing output file: " + e.getMessage()); + } finally { + try { + fileWriter.close(); + } catch (IOException e) { + logger.log("Error closing output file: " + e.getMessage()); + } + } + } + } + + private void doTest(@NonNull Bundle args) { + final int reps = args.getInt("Reps", 10); + String fileName = args.getString("FileName", null); + ResultHandler r = null; + if (fileName != null) { + try { + r = new AudioResultHandler(fileName); + } catch (IOException e) { + logger.log("Unable to open output file " + e.getMessage()); + return; + } + } + final String mode = args.getString("Mode", ""); + final ResultHandler resultHandler = r; + Runnable testRunnable = null; + switch (args.getString("TestType", "")) { + case "MidiIn": { + testRunnable = new Runnable() { + @Override + public void run() { + MidiTest midiTest = new MidiTest(getContext(), resultHandler); + midiTest.setInputRepetitions(reps); + midiTest.testMidiIn(); + } + }; + break; + } + case "MidiOut": { + testRunnable = new Runnable() { + @Override + public void run() { + MidiTest midiTest = new MidiTest(getContext(), resultHandler); + midiTest.setOutputRepetitions(reps); + midiTest.testMidiOut(); + } + }; + break; + } + case "AudioIn": { + testRunnable = new Runnable() { + @Override + public void run() { + AudioTest audioTest = new AudioTest(getContext(), resultHandler); + audioTest.setRecordingRepetitions(reps); + audioTest.setAudioMode(MODE_COLD.equals(mode) ? + AudioTest.AudioMode.COLD : AudioTest.AudioMode.CONTINUOUS); + audioTest.beginRecordingMeasurement(); + toTearDown = audioTest; + } + }; + break; + } + case "AudioOut": { + final int period = args.getInt("Period", -1); + testRunnable = new Runnable() { + @Override + public void run() { + AudioTest audioTest = new AudioTest(getContext(), resultHandler); + audioTest.setPlaybackRepetitions(reps); + audioTest.setAudioMode(MODE_COLD.equals(mode) ? + AudioTest.AudioMode.COLD : AudioTest.AudioMode.CONTINUOUS); + if (period > 0) { + audioTest.setPeriod(period); + } else { + audioTest.setPeriod(MODE_COLD.equals(mode) ? + AudioTest.COLD_TEST_PERIOD : AudioTest.CONTINUOUS_TEST_PERIOD); + } + audioTest.beginPlaybackMeasurement(); + toTearDown = audioTest; + } + }; + break; + } + } + + // Not sure we need the handler.post() here, but just in case. + final Runnable finalTestRunnable = testRunnable; + waltDevice.setConnectionStateListener(new WaltConnection.ConnectionStateListener() { + @Override + public void onConnect() { + handler.post(finalTestRunnable); + } + + @Override + public void onDisconnect() {} + }); + + } + + interface ResultHandler { + void onResult(Iterable... r); + } + + @Override + public void onDestroyView() { + if (toTearDown != null) { + toTearDown.teardown(); + } + super.onDestroyView(); + } + + private TextView txtLogAutoRun; + private SimpleLogger logger; + + public AutoRunFragment() { + // Required empty public constructor + } + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String msg = intent.getStringExtra("message"); + txtLogAutoRun.append("\n" + msg); + } + }; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + logger = SimpleLogger.getInstance(getContext()); + waltDevice = WaltDevice.getInstance(getContext()); + + View view = inflater.inflate(R.layout.fragment_auto_run, container, false); + + Bundle args = getArguments(); + if (args != null) { + doTest(args); + } + + return view; + } + + @Override + public void onResume() { + super.onResume(); + txtLogAutoRun = (TextView) getActivity().findViewById(R.id.txt_log_auto_run); + txtLogAutoRun.setMovementMethod(new ScrollingMovementMethod()); + txtLogAutoRun.setText(logger.getLogText()); + logger.registerReceiver(logReceiver); + } + + @Override + public void onPause() { + logger.unregisterReceiver(logReceiver); + super.onPause(); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/BaseTest.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/BaseTest.java new file mode 100644 index 0000000..e0e3b17 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/BaseTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; + +import static org.chromium.latency.walt.Utils.getBooleanPreference; + +abstract class BaseTest { + + interface TestStateListener { + void onTestStopped(); + void onTestStoppedWithError(); + void onTestPartialResult(double value); + } + + Context context; + SimpleLogger logger; + TraceLogger traceLogger = null; + WaltDevice waltDevice; + TestStateListener testStateListener = null; + AutoRunFragment.ResultHandler resultHandler = null; + + BaseTest(Context context) { + this.context = context; + waltDevice = WaltDevice.getInstance(context); + logger = SimpleLogger.getInstance(context); + if (getBooleanPreference(context, R.string.preference_systrace, true)) { + traceLogger = TraceLogger.getInstance(); + } + } + + void setTestStateListener(TestStateListener listener) { + this.testStateListener = listener; + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/BaseUsbConnection.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/BaseUsbConnection.java new file mode 100644 index 0000000..f0e6c62 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/BaseUsbConnection.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbManager; +import android.support.v4.content.LocalBroadcastManager; + +import java.util.HashMap; +import java.util.Locale; + +public abstract class BaseUsbConnection { + private static final String USB_PERMISSION_RESPONSE_INTENT = "usb-permission-response"; + private static final String CONNECT_INTENT = "org.chromium.latency.walt.CONNECT"; + + protected SimpleLogger logger; + protected Context context; + private LocalBroadcastManager broadcastManager; + private BroadcastReceiver currentConnectReceiver; + private WaltConnection.ConnectionStateListener connectionStateListener; + + private UsbManager usbManager; + protected UsbDevice usbDevice = null; + protected UsbDeviceConnection usbConnection; + + public BaseUsbConnection(Context context) { + this.context = context; + usbManager = (UsbManager) this.context.getSystemService(Context.USB_SERVICE); + logger = SimpleLogger.getInstance(context); + broadcastManager = LocalBroadcastManager.getInstance(context); + } + + public abstract int getVid(); + public abstract int getPid(); + + // Used to distinguish between bootloader and normal mode that differ by PID + // TODO: change intent strings to reduce dependence on PID + protected abstract boolean isCompatibleUsbDevice(UsbDevice usbDevice); + + public void onDisconnect() { + if (connectionStateListener != null) { + connectionStateListener.onDisconnect(); + } + } + + public void onConnect() { + if (connectionStateListener != null) { + connectionStateListener.onConnect(); + } + } + + + private String getConnectIntent() { + return CONNECT_INTENT + getVid() + ":" + getPid(); + } + + private String getUsbPermissionResponseIntent() { + return USB_PERMISSION_RESPONSE_INTENT + getVid() + ":" + getPid(); + } + + public boolean isConnected() { + return usbConnection != null; + } + + public void registerConnectCallback(final Runnable r) { + if (currentConnectReceiver != null) { + broadcastManager.unregisterReceiver(currentConnectReceiver); + currentConnectReceiver = null; + } + + if (isConnected()) { + r.run(); + return; + } + + currentConnectReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + broadcastManager.unregisterReceiver(this); + r.run(); + } + }; + broadcastManager.registerReceiver(currentConnectReceiver, + new IntentFilter(getConnectIntent())); + } + + public void connect() { + UsbDevice usbDevice = findUsbDevice(); + connect(usbDevice); + } + + public void connect(UsbDevice usbDevice) { + if (usbDevice == null) { + logger.log("Device not found."); + return; + } + + if (!isCompatibleUsbDevice(usbDevice)) { + logger.log("Not a valid device"); + return; + } + + this.usbDevice = usbDevice; + + // Request permission + // This displays a dialog asking user for permission to use the device. + // No dialog is displayed if the permission was already given before or the app started as a + // result of intent filter when the device was plugged in. + + PendingIntent permissionIntent = PendingIntent.getBroadcast(context, 0, + new Intent(getUsbPermissionResponseIntent()), 0); + context.registerReceiver(respondToUsbPermission, + new IntentFilter(getUsbPermissionResponseIntent())); + logger.log("Requesting permission for USB device."); + usbManager.requestPermission(this.usbDevice, permissionIntent); + } + + public void disconnect() { + onDisconnect(); + + usbConnection.close(); + usbConnection = null; + usbDevice = null; + + context.unregisterReceiver(disconnectReceiver); + } + + private BroadcastReceiver disconnectReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (isConnected() && BaseUsbConnection.this.usbDevice.equals(usbDevice)) { + logger.log("WALT was detached"); + disconnect(); + } + } + }; + + private BroadcastReceiver respondToUsbPermission = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + + if (usbDevice == null) { + logger.log("USB device was not properly opened"); + return; + } + + if(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) && + usbDevice.equals(intent.getParcelableExtra(UsbManager.EXTRA_DEVICE))){ + usbConnection = usbManager.openDevice(usbDevice); + + BaseUsbConnection.this.context.registerReceiver(disconnectReceiver, + new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED)); + + onConnect(); + + broadcastManager.sendBroadcast(new Intent(getConnectIntent())); + } else { + logger.log("Could not get permission to open the USB device"); + } + BaseUsbConnection.this.context.unregisterReceiver(respondToUsbPermission); + } + }; + + public UsbDevice findUsbDevice() { + + logger.log(String.format("Looking for TeensyUSB VID=0x%x PID=0x%x", getVid(), getPid())); + + HashMap deviceHash = usbManager.getDeviceList(); + if (deviceHash.size() == 0) { + logger.log("No connected USB devices found"); + return null; + } + + logger.log("Found " + deviceHash.size() + " connected USB devices:"); + + UsbDevice usbDevice = null; + + for (String key : deviceHash.keySet()) { + + UsbDevice dev = deviceHash.get(key); + + String msg = String.format(Locale.US, + "USB Device: %s, VID:PID - %x:%x, %d interfaces", + key, dev.getVendorId(), dev.getProductId(), dev.getInterfaceCount() + ); + + if (isCompatibleUsbDevice(dev)) { + usbDevice = dev; + msg = "Using " + msg; + } else { + msg = "Skipping " + msg; + } + + logger.log(msg); + } + return usbDevice; + } + + public void setConnectionStateListener(WaltConnection.ConnectionStateListener connectionStateListener) { + this.connectionStateListener = connectionStateListener; + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/CrashLogActivity.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/CrashLogActivity.java new file mode 100644 index 0000000..00e80ed --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/CrashLogActivity.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.text.method.ScrollingMovementMethod; +import android.widget.TextView; + + +/** + * A separate activity to display exception trace on the screen in case of a crash. + * This is useful because we dont have the USB cable connected for debugging in many cases, because + * the USB port is taken by the WALT device. + */ +public class CrashLogActivity extends AppCompatActivity { + + TextView txtCrashLog; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_crash_log); + txtCrashLog = (TextView) findViewById(R.id.txt_crash_log); + txtCrashLog.setText(getIntent().getStringExtra("crash_log")); + txtCrashLog.setMovementMethod(new ScrollingMovementMethod()); + } + +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/CustomNumberPicker.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/CustomNumberPicker.java new file mode 100644 index 0000000..27f5b50 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/CustomNumberPicker.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.NumberPicker; + +public class CustomNumberPicker extends NumberPicker { + + public CustomNumberPicker(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void addView(View child) { + super.addView(child); + initEditText(child); + } + + @Override + public void addView(View child, int index) { + super.addView(child, index); + initEditText(child); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + super.addView(child, index, params); + initEditText(child); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + super.addView(child, params); + initEditText(child); + } + + @Override + public void addView(View child, int width, int height) { + super.addView(child, width, height); + initEditText(child); + } + + private void initEditText(View view) { + if (view instanceof EditText) { + EditText inputText = (EditText) view; + inputText.addTextChangedListener(new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + try { + CustomNumberPicker.this.setValue(Integer.parseInt(s.toString())); + } catch (NumberFormatException ignored) {} + } + + @Override + public void afterTextChanged(Editable s) {} + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + }); + } + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/DiagnosticsFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/DiagnosticsFragment.java new file mode 100644 index 0000000..65ec3bf --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/DiagnosticsFragment.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.method.ScrollingMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + + +/** + * This screen allows to perform different tasks useful for diagnostics. + */ +public class DiagnosticsFragment extends Fragment { + + private SimpleLogger logger; + private TextView logTextView; + + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String msg = intent.getStringExtra("message"); + DiagnosticsFragment.this.appendLogText(msg); + } + }; + + public DiagnosticsFragment() { + // Required empty public constructor + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + logger = SimpleLogger.getInstance(getContext()); + // Inflate the layout for this fragment + final View view = inflater.inflate(R.layout.fragment_diagnostics, container, false); + logTextView = (TextView) view.findViewById(R.id.txt_log_diag); + return view; + } + + @Override + public void onResume() { + super.onResume(); + logTextView.setMovementMethod(new ScrollingMovementMethod()); + logTextView.setText(logger.getLogText()); + logger.registerReceiver(logReceiver); + } + + @Override + public void onPause() { + logger.unregisterReceiver(logReceiver); + super.onPause(); + } + + + public void appendLogText(String msg) { + logTextView.append(msg + "\n"); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/DragLatencyFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/DragLatencyFragment.java new file mode 100644 index 0000000..109fcf8 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/DragLatencyFragment.java @@ -0,0 +1,415 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.method.ScrollingMovementMethod; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.github.mikephil.charting.charts.ScatterChart; +import com.github.mikephil.charting.components.Description; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.ScatterData; +import com.github.mikephil.charting.data.ScatterDataSet; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; + +public class DragLatencyFragment extends Fragment implements View.OnClickListener { + + private SimpleLogger logger; + private WaltDevice waltDevice; + private TextView logTextView; + private TouchCatcherView touchCatcher; + private TextView crossCountsView; + private TextView dragCountsView; + private View startButton; + private View restartButton; + private View finishButton; + private ScatterChart latencyChart; + private View latencyChartLayout; + int moveCount = 0; + + ArrayList touchEventList = new ArrayList<>(); + ArrayList laserEventList = new ArrayList<>(); + + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String msg = intent.getStringExtra("message"); + DragLatencyFragment.this.appendLogText(msg); + } + }; + + private View.OnTouchListener touchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + int histLen = event.getHistorySize(); + for (int i = 0; i < histLen; i++){ + UsMotionEvent eh = new UsMotionEvent(event, waltDevice.clock.baseTime, i); + touchEventList.add(eh); + } + UsMotionEvent e = new UsMotionEvent(event, waltDevice.clock.baseTime); + touchEventList.add(e); + moveCount += histLen + 1; + + updateCountsDisplay(); + return true; + } + }; + + public DragLatencyFragment() { + // Required empty public constructor + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + logger = SimpleLogger.getInstance(getContext()); + waltDevice = WaltDevice.getInstance(getContext()); + + // Inflate the layout for this fragment + final View view = inflater.inflate(R.layout.fragment_drag_latency, container, false); + logTextView = (TextView) view.findViewById(R.id.txt_log_drag_latency); + startButton = view.findViewById(R.id.button_start_drag); + restartButton = view.findViewById(R.id.button_restart_drag); + finishButton = view.findViewById(R.id.button_finish_drag); + touchCatcher = (TouchCatcherView) view.findViewById(R.id.tap_catcher); + crossCountsView = (TextView) view.findViewById(R.id.txt_cross_counts); + dragCountsView = (TextView) view.findViewById(R.id.txt_drag_counts); + latencyChart = (ScatterChart) view.findViewById(R.id.latency_chart); + latencyChartLayout = view.findViewById(R.id.latency_chart_layout); + logTextView.setMovementMethod(new ScrollingMovementMethod()); + view.findViewById(R.id.button_close_chart).setOnClickListener(this); + restartButton.setEnabled(false); + finishButton.setEnabled(false); + return view; + } + + @Override + public void onResume() { + super.onResume(); + + logTextView.setText(logger.getLogText()); + logger.registerReceiver(logReceiver); + + // Register this fragment class as the listener for some button clicks + startButton.setOnClickListener(this); + restartButton.setOnClickListener(this); + finishButton.setOnClickListener(this); + } + + @Override + public void onPause() { + logger.unregisterReceiver(logReceiver); + super.onPause(); + } + + public void appendLogText(String msg) { + logTextView.append(msg + "\n"); + } + + void updateCountsDisplay() { + crossCountsView.setText(String.format(Locale.US, "↕ %d", laserEventList.size())); + dragCountsView.setText(String.format(Locale.US, "⇄ %d", moveCount)); + } + + /** + * @return true if measurement was successfully started + */ + boolean startMeasurement() { + logger.log("Starting drag latency test"); + try { + waltDevice.syncClock(); + } catch (IOException e) { + logger.log("Error syncing clocks: " + e.getMessage()); + return false; + } + // Register a callback for triggers + waltDevice.setTriggerHandler(triggerHandler); + try { + waltDevice.command(WaltDevice.CMD_AUTO_LASER_ON); + waltDevice.startListener(); + } catch (IOException e) { + logger.log("Error: " + e.getMessage()); + waltDevice.clearTriggerHandler(); + return false; + } + touchCatcher.setOnTouchListener(touchListener); + touchCatcher.startAnimation(); + touchEventList.clear(); + laserEventList.clear(); + moveCount = 0; + updateCountsDisplay(); + return true; + } + + void restartMeasurement() { + logger.log("\n## Restarting drag latency test. Re-sync clocks ..."); + try { + waltDevice.syncClock(); + } catch (IOException e) { + logger.log("Error syncing clocks: " + e.getMessage()); + } + + touchCatcher.startAnimation(); + touchEventList.clear(); + laserEventList.clear(); + moveCount = 0; + updateCountsDisplay(); + } + + void finishAndShowStats() { + touchCatcher.stopAnimation(); + waltDevice.stopListener(); + try { + waltDevice.command(WaltDevice.CMD_AUTO_LASER_OFF); + } catch (IOException e) { + logger.log("Error: " + e.getMessage()); + } + touchCatcher.setOnTouchListener(null); + waltDevice.clearTriggerHandler(); + + waltDevice.checkDrift(); + + logger.log(String.format(Locale.US, + "Recorded %d laser events and %d touch events. ", + laserEventList.size(), + touchEventList.size() + )); + + if (touchEventList.size() < 100) { + logger.log("Insufficient number of touch events (<100), aborting."); + return; + } + + if (laserEventList.size() < 8) { + logger.log("Insufficient number of laser events (<8), aborting."); + return; + } + + // TODO: Log raw data if enabled in settings, touch events add lots of text to the log. + // logRawData(); + reshapeAndCalculate(); + LogUploader.uploadIfAutoEnabled(getContext()); + } + + // Data formatted for processing with python script, y.py + void logRawData() { + logger.log("#####> LASER EVENTS #####"); + for (int i = 0; i < laserEventList.size(); i++){ + logger.log(laserEventList.get(i).t + " " + laserEventList.get(i).value); + } + logger.log("#####< END OF LASER EVENTS #####"); + + logger.log("=====> TOUCH EVENTS ====="); + for (UsMotionEvent e: touchEventList) { + logger.log(String.format(Locale.US, + "%d %.3f %.3f", + e.kernelTime, + e.x, e.y + )); + } + logger.log("=====< END OF TOUCH EVENTS ====="); + } + + void reshapeAndCalculate() { + double[] ft, lt; // All time arrays are in _milliseconds_ + double[] fy; + int[] ldir; + + // Use the time of the first touch event as time = 0 for debugging convenience + long t0_us = touchEventList.get(0).kernelTime; + long tLast_us = touchEventList.get(touchEventList.size() - 1).kernelTime; + + int fN = touchEventList.size(); + ft = new double[fN]; + fy = new double[fN]; + + for (int i = 0; i < fN; i++){ + ft[i] = (touchEventList.get(i).kernelTime - t0_us) / 1000.; + fy[i] = touchEventList.get(i).y; + } + + // Remove all laser events that are outside the time span of the touch events + // they are not usable and would result in errors downstream + int j = laserEventList.size() - 1; + while (j >= 0 && laserEventList.get(j).t > tLast_us) { + laserEventList.remove(j); + j--; + } + + while (laserEventList.size() > 0 && laserEventList.get(0).t < t0_us) { + laserEventList.remove(0); + } + + // Calculation assumes that the first event is generated by the finger obstructing the beam. + // Remove the first event if it was generated by finger going out of the beam (value==1). + while (laserEventList.size() > 0 && laserEventList.get(0).value == 1) { + laserEventList.remove(0); + } + + int lN = laserEventList.size(); + + if (lN < 8) { + logger.log("ERROR: Insufficient number of laser events overlapping with touch events," + + "aborting." + ); + return; + } + + lt = new double[lN]; + ldir = new int[lN]; + for (int i = 0; i < lN; i++){ + lt[i] = (laserEventList.get(i).t - t0_us) / 1000.; + ldir[i] = laserEventList.get(i).value; + } + + calculateDragLatency(ft,fy, lt, ldir); + } + + /** + * Handler for all the button clicks on this screen. + */ + @Override + public void onClick(View v) { + if (v.getId() == R.id.button_restart_drag) { + latencyChartLayout.setVisibility(View.GONE); + restartButton.setEnabled(false); + restartMeasurement(); + restartButton.setEnabled(true); + return; + } + + if (v.getId() == R.id.button_start_drag) { + latencyChartLayout.setVisibility(View.GONE); + startButton.setEnabled(false); + boolean startSuccess = startMeasurement(); + if (startSuccess) { + finishButton.setEnabled(true); + restartButton.setEnabled(true); + } else { + startButton.setEnabled(true); + } + return; + } + + if (v.getId() == R.id.button_finish_drag) { + finishButton.setEnabled(false); + restartButton.setEnabled(false); + finishAndShowStats(); + startButton.setEnabled(true); + return; + } + + if (v.getId() == R.id.button_close_chart) { + latencyChartLayout.setVisibility(View.GONE); + } + } + + private WaltDevice.TriggerHandler triggerHandler = new WaltDevice.TriggerHandler() { + @Override + public void onReceive(WaltDevice.TriggerMessage tmsg) { + laserEventList.add(tmsg); + updateCountsDisplay(); + } + }; + + public void calculateDragLatency(double[] ft, double[] fy, double[] lt, int[] ldir) { + // TODO: throw away several first laser crossings (if not already) + double[] ly = Utils.interp(lt, ft, fy); + double lmid = Utils.mean(ly); + // Assume first crossing is into the beam = light-off = 0 + if (ldir[0] != 0) { + // TODO: add more sanity checks here. + logger.log("First laser crossing is not into the beam, aborting"); + return; + } + + // label sides, one simple label is i starts from 1, then side = (i mod 4) / 2 same as the 2nd LSB bit or i. + int[] sideIdx = new int[lt.length]; + + // This is one way of deciding what laser events were on which side + // It should go above, below, below, above, above + // The other option is to mirror the python code that uses position and velocity for this + for (int i = 0; i touchEntries = new ArrayList<>(); + final ArrayList laserEntries = new ArrayList<>(); + final double[] laserT = new double[lt.length]; + for (int i = 0; i < ft.length; i++) { + touchEntries.add(new Entry((float) ft[i], (float) fy[i])); + } + for (int i = 0; i < lt.length; i++) { + laserT[i] = lt[i] + averageBestShift; + } + final double[] laserY = Utils.interp(laserT, ft, fy); + for (int i = 0; i < laserY.length; i++) { + laserEntries.add(new Entry((float) laserT[i], (float) laserY[i])); + } + + final ScatterDataSet dataSetTouch = new ScatterDataSet(touchEntries, "Touch Events"); + dataSetTouch.setScatterShape(ScatterChart.ScatterShape.CIRCLE); + dataSetTouch.setScatterShapeSize(8f); + + final ScatterDataSet dataSetLaser = new ScatterDataSet(laserEntries, + String.format(Locale.US, "Laser Events Latency=%.1f ms", averageBestShift)); + dataSetLaser.setColor(Color.RED); + dataSetLaser.setScatterShapeSize(10f); + dataSetLaser.setScatterShape(ScatterChart.ScatterShape.X); + + final ScatterData scatterData = new ScatterData(dataSetTouch, dataSetLaser); + final Description desc = new Description(); + desc.setText("Y-Position [pixels] vs. Time [ms]"); + desc.setTextSize(12f); + latencyChart.setDescription(desc); + latencyChart.setData(scatterData); + latencyChartLayout.setVisibility(View.VISIBLE); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/FastPathSurfaceView.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/FastPathSurfaceView.java new file mode 100644 index 0000000..449627f --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/FastPathSurfaceView.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.util.AttributeSet; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.widget.Toast; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class FastPathSurfaceView extends SurfaceView implements SurfaceHolder.Callback { + + private boolean isActive = false; + + public FastPathSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + getHolder().addCallback(this); + setZOrderOnTop(true); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Surface surface = holder.getSurface(); + if (surface == null) + return; + + try { + Method setSharedBufferMode = Surface.class.getMethod("setSharedBufferMode", boolean.class); + setSharedBufferMode.invoke(surface, true); + displayMessage("Using shared buffer mode."); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + displayMessage("Shared buffer mode is not supported."); + } + Canvas canvas = surface.lockCanvas(null); + canvas.drawColor(Color.GRAY); + surface.unlockCanvasAndPost(canvas); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + isActive = true; + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + isActive = false; + } + + private void displayMessage(String message) { + Toast toast = Toast.makeText(getContext(), message, Toast.LENGTH_SHORT); + toast.show(); + } + + public void setRectColor(int color) { + Surface surface = getHolder().getSurface(); + if (surface == null || !isActive) + return; + Rect rect = new Rect(10, 10, 310, 310); + Canvas canvas = surface.lockCanvas(rect); + Paint paint = new Paint(); + paint.setColor(color); + canvas.drawRect(rect, paint); + surface.unlockCanvasAndPost(canvas); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/FrontPageFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/FrontPageFragment.java new file mode 100644 index 0000000..cb125e3 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/FrontPageFragment.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.graphics.Color; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + + +/** + * A simple {@link Fragment} subclass. + */ +public class FrontPageFragment extends Fragment { + + public FrontPageFragment() { + // Required empty public constructor + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View view = inflater.inflate(R.layout.fragment_front_page, container, false); + + if (MidiFragment.hasMidi(container.getContext())) { + final ImageView midiImage = (ImageView) view.findViewById(R.id.midi_image); + final TextView midiText = (TextView) view.findViewById(R.id.midi_text); + midiImage.setColorFilter(Color.TRANSPARENT); + midiText.setTextColor(Color.BLACK); + } + return view; + } + +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/HistogramChart.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/HistogramChart.java new file mode 100644 index 0000000..3fc68f6 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/HistogramChart.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RelativeLayout; + +import com.github.mikephil.charting.charts.BarChart; +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.components.Description; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; +import com.github.mikephil.charting.utils.ColorTemplate; + +import java.text.DecimalFormat; +import java.util.ArrayList; + +public class HistogramChart extends RelativeLayout implements View.OnClickListener { + + static final float GROUP_SPACE = 0.1f; + private HistogramData histogramData; + private BarChart barChart; + + public HistogramChart(Context context, AttributeSet attrs) { + super(context, attrs); + inflate(getContext(), R.layout.histogram, this); + + barChart = (BarChart) findViewById(R.id.bar_chart); + findViewById(R.id.button_close_bar_chart).setOnClickListener(this); + + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HistogramChart); + final String descString; + final int numDataSets; + final float binWidth; + try { + descString = a.getString(R.styleable.HistogramChart_description); + numDataSets = a.getInteger(R.styleable.HistogramChart_numDataSets, 1); + binWidth = a.getFloat(R.styleable.HistogramChart_binWidth, 5f); + } finally { + a.recycle(); + } + + ArrayList dataSets = new ArrayList<>(numDataSets); + for (int i = 0; i < numDataSets; i++) { + final BarDataSet dataSet = new BarDataSet(new ArrayList(), ""); + dataSet.setColor(ColorTemplate.MATERIAL_COLORS[i]); + dataSets.add(dataSet); + } + + BarData barData = new BarData(dataSets); + barData.setBarWidth((1f - GROUP_SPACE)/numDataSets); + barChart.setData(barData); + histogramData = new HistogramData(numDataSets, binWidth); + groupBars(barData); + final Description desc = new Description(); + desc.setText(descString); + desc.setTextSize(12f); + barChart.setDescription(desc); + + XAxis xAxis = barChart.getXAxis(); + xAxis.setGranularityEnabled(true); + xAxis.setGranularity(1); + xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); + xAxis.setValueFormatter(new IAxisValueFormatter() { + DecimalFormat df = new DecimalFormat("#.##"); + + @Override + public String getFormattedValue(float value, AxisBase axis) { + return df.format(histogramData.getDisplayValue(value)); + } + }); + + barChart.setFitBars(true); + barChart.invalidate(); + } + + BarChart getBarChart() { + return barChart; + } + + /** + * Re-implementation of BarData.groupBars(), but allows grouping with only 1 BarDataSet + * This adjusts the x-coordinates of entries, which centers the bars between axis labels + */ + static void groupBars(final BarData barData) { + IBarDataSet max = barData.getMaxEntryCountSet(); + int maxEntryCount = max.getEntryCount(); + float groupSpaceWidthHalf = GROUP_SPACE / 2f; + float barWidthHalf = barData.getBarWidth() / 2f; + float interval = barData.getGroupWidth(GROUP_SPACE, 0); + float fromX = 0; + + for (int i = 0; i < maxEntryCount; i++) { + float start = fromX; + fromX += groupSpaceWidthHalf; + + for (IBarDataSet set : barData.getDataSets()) { + fromX += barWidthHalf; + if (i < set.getEntryCount()) { + BarEntry entry = set.getEntryForIndex(i); + if (entry != null) { + entry.setX(fromX); + } + } + fromX += barWidthHalf; + } + + fromX += groupSpaceWidthHalf; + float end = fromX; + float innerInterval = end - start; + float diff = interval - innerInterval; + + // correct rounding errors + if (diff > 0 || diff < 0) { + fromX += diff; + } + } + barData.notifyDataChanged(); + } + + public void clearData() { + histogramData.clear(); + for (IBarDataSet dataSet : barChart.getBarData().getDataSets()) { + dataSet.clear(); + } + barChart.getBarData().notifyDataChanged(); + barChart.invalidate(); + } + + public void addEntry(int dataSetIndex, double value) { + histogramData.addEntry(barChart.getBarData(), dataSetIndex, value); + recalculateXAxis(); + } + + public void addEntry(double value) { + addEntry(0, value); + } + + private void recalculateXAxis() { + final XAxis xAxis = barChart.getXAxis(); + xAxis.setAxisMinimum(0); + xAxis.setAxisMaximum(histogramData.getNumBins()); + barChart.notifyDataSetChanged(); + barChart.invalidate(); + } + + public void setLabel(int dataSetIndex, String label) { + barChart.getBarData().getDataSetByIndex(dataSetIndex).setLabel(label); + barChart.getLegendRenderer().computeLegend(barChart.getBarData()); + barChart.invalidate(); + } + + public void setLabel(String label) { + setLabel(0, label); + } + + public void setDescription(String description) { + getBarChart().getDescription().setText(description); + } + + public void setLegendEnabled(boolean enabled) { + barChart.getLegend().setEnabled(enabled); + barChart.notifyDataSetChanged(); + barChart.invalidate(); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.button_close_bar_chart: + this.setVisibility(GONE); + } + } + + static class HistogramData { + private float binWidth; + private final ArrayList> rawData; + private double minBin = 0; + private double maxBin = 100; + private double min = 0; + private double max = 100; + + HistogramData(int numDataSets, float binWidth) { + this.binWidth = binWidth; + rawData = new ArrayList<>(numDataSets); + for (int i = 0; i < numDataSets; i++) { + rawData.add(new ArrayList()); + } + } + + float getBinWidth() { + return binWidth; + } + + double getMinBin() { + return minBin; + } + + void clear() { + for (int i = 0; i < rawData.size(); i++) { + rawData.get(i).clear(); + } + } + + private boolean isEmpty() { + for (ArrayList data : rawData) { + if (!data.isEmpty()) return false; + } + return true; + } + + void addEntry(BarData barData, int dataSetIndex, double value) { + if (isEmpty()) { + min = value; + max = value; + } else { + if (value < min) min = value; + if (value > max) max = value; + } + + rawData.get(dataSetIndex).add(value); + recalculateDataSet(barData); + } + + void recalculateDataSet(final BarData barData) { + minBin = Math.floor(min / binWidth) * binWidth; + maxBin = Math.floor(max / binWidth) * binWidth; + + int[][] bins = new int[rawData.size()][getNumBins()]; + + for (int setNum = 0; setNum < rawData.size(); setNum++) { + for (Double d : rawData.get(setNum)) { + ++bins[setNum][(int) (Math.floor((d - minBin) / binWidth))]; + } + } + + for (int setNum = 0; setNum < barData.getDataSetCount(); setNum++) { + final IBarDataSet dataSet = barData.getDataSetByIndex(setNum); + dataSet.clear(); + for (int i = 0; i < bins[setNum].length; i++) { + dataSet.addEntry(new BarEntry(i, bins[setNum][i])); + } + } + groupBars(barData); + barData.notifyDataChanged(); + } + + int getNumBins() { + return (int) (((maxBin - minBin) / binWidth) + 1); + } + + double getDisplayValue(float value) { + return value * getBinWidth() + getMinBin(); + } + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/LogFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/LogFragment.java new file mode 100644 index 0000000..069d032 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/LogFragment.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.method.ScrollingMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + + +/** + * A screen that shows the log. + */ +public class LogFragment extends Fragment { + + private Activity activity; + private SimpleLogger logger; + TextView textView; + + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String msg = intent.getStringExtra("message"); + LogFragment.this.appendLogText(msg); + } + }; + + public LogFragment() { + // Required empty public constructor + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + activity = getActivity(); + logger = SimpleLogger.getInstance(getContext()); + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_log, container, false); + } + + @Override + public void onResume() { + super.onResume(); + textView = (TextView) activity.findViewById(R.id.txt_log); + textView.setMovementMethod(new ScrollingMovementMethod()); + textView.setText(logger.getLogText()); + logger.registerReceiver(logReceiver); + } + + @Override + public void onPause() { + logger.unregisterReceiver(logReceiver); + super.onPause(); + } + + public void appendLogText(String msg) { + textView.append(msg + "\n"); + } + +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/LogUploader.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/LogUploader.java new file mode 100644 index 0000000..a73f456 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/LogUploader.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.support.v4.content.AsyncTaskLoader; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; + +class LogUploader extends AsyncTaskLoader { + + private String urlString; + private SimpleLogger logger; + + LogUploader(Context context) { + super(context); + urlString = Utils.getStringPreference(context, R.string.preference_log_url, ""); + logger = SimpleLogger.getInstance(context); + + } + + LogUploader(Context context, String urlString) { + super(context); + this.urlString = urlString; + logger = SimpleLogger.getInstance(context); + } + + @Override + public Integer loadInBackground() { + if (urlString.isEmpty()) return -1; + try { + URL url = new URL(urlString); + HttpURLConnection urlConnection = + (HttpURLConnection) url.openConnection(); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoOutput(true); + urlConnection.setRequestProperty("Content-Type", "text/plain"); + BufferedOutputStream out = + new BufferedOutputStream(urlConnection.getOutputStream()); + PrintWriter writer = new PrintWriter(out); + writer.write(logger.getLogText()); + writer.flush(); + final int responseCode = urlConnection.getResponseCode(); + if (responseCode / 100 == 2) { + logger.log("Log successfully uploaded"); + } else { + logger.log("Log upload may have failed. Server return status code " + responseCode); + } + return responseCode; + } catch (IOException e) { + logger.log("Failed to upload log"); + return -1; + } + } + + void startUpload() { + super.forceLoad(); + } + + static void uploadIfAutoEnabled(Context context) { + if (Utils.getBooleanPreference(context, R.string.preference_auto_upload_log, false)) { + new LogUploader(context).startUpload(); + } + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java new file mode 100644 index 0000000..7efee00 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java @@ -0,0 +1,536 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.Manifest; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.StrictMode; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.Loader; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.Toast; + +import org.chromium.latency.walt.programmer.Programmer; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Date; +import java.util.Locale; + +import static org.chromium.latency.walt.Utils.getBooleanPreference; + +public class MainActivity extends AppCompatActivity { + private static final String TAG = "WALT"; + private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SHARE_LOG = 2; + private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SYSTRACE = 3; + + private Toolbar toolbar; + LocalBroadcastManager broadcastManager; + private SimpleLogger logger; + private WaltDevice waltDevice; + public Menu menu; + + public Handler handler = new Handler(); + + + /** + * A method to display exceptions on screen. This is very useful because our USB port is taken + * and we often need to debug without adb. + * Based on this article: + * https://trivedihardik.wordpress.com/2011/08/20/how-to-avoid-force-close-error-in-android/ + */ + public class LoggingExceptionHandler implements java.lang.Thread.UncaughtExceptionHandler { + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + StringWriter stackTrace = new StringWriter(); + ex.printStackTrace(new PrintWriter(stackTrace)); + String msg = "WALT crashed with the following exception:\n" + stackTrace; + + // Fire a new activity showing the stack trace + Intent intent = new Intent(MainActivity.this, CrashLogActivity.class); + intent.putExtra("crash_log", msg); + MainActivity.this.startActivity(intent); + + // Terminate this process + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(10); + } + } + + @Override + protected void onResume() { + super.onResume(); + + final UsbDevice usbDevice; + Intent intent = getIntent(); + if (intent != null && intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { + setIntent(null); // done with the intent + usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + } else { + usbDevice = null; + } + + // Connect and sync clocks, but a bit later as it takes time + handler.postDelayed(new Runnable() { + @Override + public void run() { + if (usbDevice == null) { + waltDevice.connect(); + } else { + waltDevice.connect(usbDevice); + } + } + }, 1000); + + if (intent != null && AutoRunFragment.TEST_ACTION.equals(intent.getAction())) { + getSupportFragmentManager().popBackStack("Automated Test", + FragmentManager.POP_BACK_STACK_INCLUSIVE); + Fragment autoRunFragment = new AutoRunFragment(); + autoRunFragment.setArguments(intent.getExtras()); + switchScreen(autoRunFragment, "Automated Test"); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Thread.setDefaultUncaughtExceptionHandler(new LoggingExceptionHandler()); + setContentView(R.layout.activity_main); + + // App bar + toolbar = (Toolbar) findViewById(R.id.toolbar_main); + setSupportActionBar(toolbar); + getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() { + @Override + public void onBackStackChanged() { + int stackTopIndex = getSupportFragmentManager().getBackStackEntryCount() - 1; + if (stackTopIndex >= 0) { + toolbar.setTitle(getSupportFragmentManager().getBackStackEntryAt(stackTopIndex).getName()); + } else { + toolbar.setTitle(R.string.app_name); + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + // Disable fullscreen mode + getSupportActionBar().show(); + getWindow().getDecorView().setSystemUiVisibility(0); + } + } + }); + + waltDevice = WaltDevice.getInstance(this); + + // Create front page fragment + FrontPageFragment frontPageFragment = new FrontPageFragment(); + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.add(R.id.fragment_container, frontPageFragment); + transaction.commit(); + + logger = SimpleLogger.getInstance(this); + broadcastManager = LocalBroadcastManager.getInstance(this); + + // Add basic version and device info to the log + logger.log(String.format("WALT v%s (versionCode=%d)", + BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); + logger.log("WALT protocol version " + WaltDevice.PROTOCOL_VERSION); + logger.log("DEVICE INFO:"); + logger.log(" " + Build.FINGERPRINT); + logger.log(" Build.SDK_INT=" + Build.VERSION.SDK_INT); + logger.log(" os.version=" + System.getProperty("os.version")); + + // Set volume buttons to control media volume + setVolumeControlStream(AudioManager.STREAM_MUSIC); + requestSystraceWritePermission(); + // Allow network operations on the main thread + StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.setThreadPolicy(policy); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_main, menu); + this.menu = menu; + return true; + } + + public void toast(String msg) { + logger.log(msg); + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); + } + + @Override + public boolean onSupportNavigateUp() { + // Go back when the back or up button on toolbar is clicked + getSupportFragmentManager().popBackStack(); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + + Log.i(TAG, "Toolbar button: " + item.getTitle()); + + switch (item.getItemId()) { + case R.id.action_help: + return true; + case R.id.action_share: + attemptSaveAndShareLog(); + return true; + case R.id.action_upload: + showUploadLogDialog(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Handlers for main menu clicks + //////////////////////////////////////////////////////////////////////////////////////////////// + + private void switchScreen(Fragment newFragment, String title) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + toolbar.setTitle(title); + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace(R.id.fragment_container, newFragment); + transaction.addToBackStack(title); + transaction.commit(); + } + + public void onClickClockSync(View view) { + DiagnosticsFragment diagnosticsFragment = new DiagnosticsFragment(); + switchScreen(diagnosticsFragment, "Diagnostics"); + } + + public void onClickTapLatency(View view) { + TapLatencyFragment newFragment = new TapLatencyFragment(); + requestSystraceWritePermission(); + switchScreen(newFragment, "Tap Latency"); + } + + public void onClickScreenResponse(View view) { + ScreenResponseFragment newFragment = new ScreenResponseFragment(); + requestSystraceWritePermission(); + switchScreen(newFragment, "Screen Response"); + } + + public void onClickAudio(View view) { + AudioFragment newFragment = new AudioFragment(); + switchScreen(newFragment, "Audio Latency"); + } + + public void onClickMIDI(View view) { + if (MidiFragment.hasMidi(this)) { + MidiFragment newFragment = new MidiFragment(); + switchScreen(newFragment, "MIDI Latency"); + } else { + toast("This device does not support MIDI"); + } + } + + public void onClickDragLatency(View view) { + DragLatencyFragment newFragment = new DragLatencyFragment(); + switchScreen(newFragment, "Drag Latency"); + } + + public void onClickOpenLog(View view) { + LogFragment logFragment = new LogFragment(); + // menu.findItem(R.id.action_help).setVisible(false); + switchScreen(logFragment, "Log"); + } + + public void onClickOpenAbout(View view) { + AboutFragment aboutFragment = new AboutFragment(); + switchScreen(aboutFragment, "About"); + } + + public void onClickOpenSettings(View view) { + SettingsFragment settingsFragment = new SettingsFragment(); + switchScreen(settingsFragment, "Settings"); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Handlers for diagnostics menu clicks + //////////////////////////////////////////////////////////////////////////////////////////////// + public void onClickReconnect(View view) { + waltDevice.connect(); + } + + public void onClickPing(View view) { + long t1 = waltDevice.clock.micros(); + try { + waltDevice.command(WaltDevice.CMD_PING); + long dt = waltDevice.clock.micros() - t1; + logger.log(String.format(Locale.US, + "Ping reply in %.1fms", dt / 1000. + )); + } catch (IOException e) { + logger.log("Error sending ping: " + e.getMessage()); + } + } + + public void onClickStartListener(View view) { + if (waltDevice.isListenerStopped()) { + try { + waltDevice.startListener(); + } catch (IOException e) { + logger.log("Error starting USB listener: " + e.getMessage()); + } + } else { + waltDevice.stopListener(); + } + } + + public void onClickSync(View view) { + try { + waltDevice.syncClock(); + } catch (IOException e) { + logger.log("Error syncing clocks: " + e.getMessage()); + } + } + + public void onClickCheckDrift(View view) { + waltDevice.checkDrift(); + } + + public void onClickProgram(View view) { + if (waltDevice.isConnected()) { + // show dialog telling user to first press white button + final AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle("Press white button") + .setMessage("Please press the white button on the WALT device.") + .setCancelable(false) + .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) {} + }).show(); + + waltDevice.setConnectionStateListener(new WaltConnection.ConnectionStateListener() { + @Override + public void onConnect() {} + + @Override + public void onDisconnect() { + dialog.cancel(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + new Programmer(MainActivity.this).program(); + } + }, 1000); + } + }); + } else { + new Programmer(this).program(); + } + } + + private void attemptSaveAndShareLog() { + int currentPermission = ContextCompat.checkSelfPermission(this, + Manifest.permission.WRITE_EXTERNAL_STORAGE); + if (currentPermission == PackageManager.PERMISSION_GRANTED) { + String filePath = saveLogToFile(); + shareLogFile(filePath); + } else { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SHARE_LOG); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + final boolean isPermissionGranted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; + if (!isPermissionGranted) { + logger.log("Could not get permission to write file to storage"); + return; + } + switch (requestCode) { + case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SHARE_LOG: + attemptSaveAndShareLog(); + break; + } + } + + public String saveLogToFile() { + + // Save to file to later fire an Intent.ACTION_SEND + // This allows to either send the file as email attachment + // or upload it to Drive. + + // The permissions for attachments are a mess, writing world readable files + // is frowned upon, but deliberately giving permissions as part of the intent is + // way too cumbersome. + + String fname = "qstep_log.txt"; + // A reasonable world readable location,on many phones it's /storage/emulated/Documents + // TODO: make this location configurable? + File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); + File file = null; + FileOutputStream outStream = null; + + Date now = new Date(); + logger.log("Saving log to:\n" + path.getPath() + "/" + fname); + logger.log("On: " + now.toString()); + + try { + if (!path.exists()) { + path.mkdirs(); + } + file = new File(path, fname); + outStream = new FileOutputStream(file); + outStream.write(logger.getLogText().getBytes()); + + outStream.close(); + logger.log("Log saved"); + } catch (Exception e) { + e.printStackTrace(); + logger.log("Exception:\n" + e.getMessage()); + } + return file.getPath(); + } + + public void shareLogFile(String filepath) { + File file = new File(filepath); + logger.log("Firing Intent.ACTION_SEND for file:"); + logger.log(file.getPath()); + + Intent i = new Intent(Intent.ACTION_SEND); + i.setType("text/plain"); + + i.putExtra(Intent.EXTRA_SUBJECT, "WALT log"); + i.putExtra(Intent.EXTRA_TEXT, "Attaching log file " + file.getPath()); + i.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)); + + try { + startActivity(Intent.createChooser(i, "Send mail...")); + } catch (android.content.ActivityNotFoundException ex) { + toast("There are no email clients installed."); + } + } + + private static boolean startsWithHttp(String url) { + return url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://"); + } + + private void showUploadLogDialog() { + final AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle("Upload log to URL") + .setView(R.layout.dialog_upload) + .setPositiveButton("Upload", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) {} + }) + .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) {} + }) + .show(); + final EditText editText = (EditText) dialog.findViewById(R.id.edit_text); + editText.setText(Utils.getStringPreference( + MainActivity.this, R.string.preference_log_url, "")); + dialog.getButton(AlertDialog.BUTTON_POSITIVE). + setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + View progress = dialog.findViewById(R.id.progress_bar); + String urlString = editText.getText().toString(); + if (!startsWithHttp(urlString)) { + urlString = "http://" + urlString; + } + editText.setVisibility(View.GONE); + progress.setVisibility(View.VISIBLE); + LogUploader uploader = new LogUploader(MainActivity.this, urlString); + final String finalUrlString = urlString; + uploader.registerListener(1, new Loader.OnLoadCompleteListener() { + @Override + public void onLoadComplete(Loader loader, Integer data) { + dialog.cancel(); + if (data == -1) { + Toast.makeText(MainActivity.this, + "Failed to upload log", Toast.LENGTH_SHORT).show(); + return; + } else if (data / 100 == 2) { + Toast.makeText(MainActivity.this, + "Log successfully uploaded", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(MainActivity.this, + "Failed to upload log. Server returned status code " + data, + Toast.LENGTH_SHORT).show(); + } + SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(MainActivity.this); + preferences.edit().putString( + getString(R.string.preference_log_url), finalUrlString).apply(); + } + }); + uploader.startUpload(); + } + }); + } + + private void requestSystraceWritePermission() { + if (getBooleanPreference(this, R.string.preference_systrace, true)) { + int currentPermission = ContextCompat.checkSelfPermission(this, + Manifest.permission.WRITE_EXTERNAL_STORAGE); + if (currentPermission != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SYSTRACE); + } + } + } + +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/MidiFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/MidiFragment.java new file mode 100644 index 0000000..c6f1118 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/MidiFragment.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.method.ScrollingMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.Locale; + +public class MidiFragment extends Fragment + implements View.OnClickListener, BaseTest.TestStateListener { + + private SimpleLogger logger; + private TextView textView; + private View startMidiInButton; + private View startMidiOutButton; + private HistogramChart latencyChart; + private MidiTest midiTest; + + public MidiFragment() { + // Required empty public constructor + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + logger = SimpleLogger.getInstance(getContext()); + midiTest = new MidiTest(getActivity()); + midiTest.setTestStateListener(this); + + final View view = inflater.inflate(R.layout.fragment_midi, container, false); + textView = (TextView) view.findViewById(R.id.txt_box_midi); + startMidiInButton = view.findViewById(R.id.button_start_midi_in); + startMidiOutButton = view.findViewById(R.id.button_start_midi_out); + latencyChart = (HistogramChart) view.findViewById(R.id.latency_chart); + textView.setMovementMethod(new ScrollingMovementMethod()); + return view; + } + + @Override + public void onResume() { + super.onResume(); + + // Register this fragment class as the listener for some button clicks + startMidiInButton.setOnClickListener(this); + startMidiOutButton.setOnClickListener(this); + + textView.setText(logger.getLogText()); + logger.registerReceiver(logReceiver); + } + + @Override + public void onPause() { + logger.unregisterReceiver(logReceiver); + super.onPause(); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.button_start_midi_in: + disableButtons(); + latencyChart.setVisibility(View.VISIBLE); + latencyChart.clearData(); + latencyChart.setLegendEnabled(false); + latencyChart.getBarChart().getDescription().setText("MIDI Input Latency [ms]"); + midiTest.testMidiIn(); + break; + case R.id.button_start_midi_out: + disableButtons(); + latencyChart.setVisibility(View.VISIBLE); + latencyChart.clearData(); + latencyChart.setLegendEnabled(false); + latencyChart.getBarChart().getDescription().setText("MIDI Output Latency [ms]"); + midiTest.testMidiOut(); + break; + } + } + + private void disableButtons() { + startMidiInButton.setEnabled(false); + startMidiOutButton.setEnabled(false); + } + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String msg = intent.getStringExtra("message"); + textView.append(msg + "\n"); + } + }; + + @Override + public void onTestStopped() { + if (!midiTest.deltasOutputTotal.isEmpty()) { + latencyChart.setLegendEnabled(true); + latencyChart.setLabel(String.format( + Locale.US, "Median=%.1f ms", Utils.median(midiTest.deltasOutputTotal))); + } else if (!midiTest.deltasInputTotal.isEmpty()) { + latencyChart.setLegendEnabled(true); + latencyChart.setLabel(String.format( + Locale.US, "Median=%.1f ms", Utils.median(midiTest.deltasInputTotal))); + } + LogUploader.uploadIfAutoEnabled(getContext()); + startMidiInButton.setEnabled(true); + startMidiOutButton.setEnabled(true); + } + + @Override + public void onTestStoppedWithError() { + onTestStopped(); + latencyChart.setVisibility(View.GONE); + } + + @Override + public void onTestPartialResult(double value) { + latencyChart.addEntry(value); + } + + public static boolean hasMidi(Context context) { + return context.getPackageManager(). + hasSystemFeature("android.software.midi"); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/MidiTest.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/MidiTest.java new file mode 100644 index 0000000..27df929 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/MidiTest.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.midi.MidiDevice; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiInputPort; +import android.media.midi.MidiManager; +import android.media.midi.MidiOutputPort; +import android.media.midi.MidiReceiver; +import android.os.Handler; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; + +import static org.chromium.latency.walt.Utils.getIntPreference; + +@TargetApi(23) +class MidiTest extends BaseTest { + + private Handler handler = new Handler(); + + private static final String TEENSY_MIDI_NAME = "Teensyduino Teensy MIDI"; + private static final byte[] noteMsg = {(byte) 0x90, (byte) 99, (byte) 0}; + + private MidiManager midiManager; + private MidiDevice midiDevice; + // Output and Input here are with respect to the MIDI device, not the Android device. + private MidiOutputPort midiOutputPort; + private MidiInputPort midiInputPort; + private boolean isConnecting = false; + private long last_tWalt = 0; + private long last_tSys = 0; + private long last_tJava = 0; + private int inputSyncAfterRepetitions = 100; + private int outputSyncAfterRepetitions = 20; // TODO: implement periodic clock sync for output + private int inputRepetitions; + private int outputRepetitions; + private int repetitionsDone; + private ArrayList deltasToSys = new ArrayList<>(); + ArrayList deltasInputTotal = new ArrayList<>(); + ArrayList deltasOutputTotal = new ArrayList<>(); + + private static final int noteDelay = 300; + private static final int timeout = 1000; + + MidiTest(Context context) { + super(context); + inputRepetitions = getIntPreference(context, R.string.preference_midi_in_reps, 100); + outputRepetitions = getIntPreference(context, R.string.preference_midi_out_reps, 10); + midiManager = (MidiManager) context.getSystemService(Context.MIDI_SERVICE); + findMidiDevice(); + } + + MidiTest(Context context, AutoRunFragment.ResultHandler resultHandler) { + this(context); + this.resultHandler = resultHandler; + } + + void setInputRepetitions(int repetitions) { + inputRepetitions = repetitions; + } + + void setOutputRepetitions(int repetitions) { + outputRepetitions = repetitions; + } + + void testMidiOut() { + if (midiDevice == null) { + if (isConnecting) { + logger.log("Still connecting..."); + handler.post(new Runnable() { + @Override + public void run() { + testMidiOut(); + } + }); + } else { + logger.log("MIDI device is not open!"); + if (testStateListener != null) testStateListener.onTestStoppedWithError(); + } + return; + } + try { + setupMidiOut(); + } catch (IOException e) { + logger.log("Error setting up test: " + e.getMessage()); + if (testStateListener != null) testStateListener.onTestStoppedWithError(); + return; + } + handler.postDelayed(cancelMidiOutRunnable, noteDelay * inputRepetitions + timeout); + } + + void testMidiIn() { + if (midiDevice == null) { + if (isConnecting) { + logger.log("Still connecting..."); + handler.post(new Runnable() { + @Override + public void run() { + testMidiIn(); + } + }); + } else { + logger.log("MIDI device is not open!"); + if (testStateListener != null) testStateListener.onTestStoppedWithError(); + } + return; + } + try { + setupMidiIn(); + } catch (IOException e) { + logger.log("Error setting up test: " + e.getMessage()); + if (testStateListener != null) testStateListener.onTestStoppedWithError(); + return; + } + handler.postDelayed(requestNoteRunnable, noteDelay); + } + + private void setupMidiOut() throws IOException { + repetitionsDone = 0; + deltasInputTotal.clear(); + deltasOutputTotal.clear(); + + midiInputPort = midiDevice.openInputPort(0); + + waltDevice.syncClock(); + waltDevice.command(WaltDevice.CMD_MIDI); + waltDevice.startListener(); + waltDevice.setTriggerHandler(triggerHandler); + + scheduleNotes(); + } + + private void findMidiDevice() { + MidiDeviceInfo[] infos = midiManager.getDevices(); + for(MidiDeviceInfo info : infos) { + String name = info.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME); + logger.log("Found MIDI device named " + name); + if(TEENSY_MIDI_NAME.equals(name)) { + logger.log("^^^ using this device ^^^"); + isConnecting = true; + midiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() { + @Override + public void onDeviceOpened(MidiDevice device) { + if (device == null) { + logger.log("Error, unable to open MIDI device"); + } else { + logger.log("Opened MIDI device successfully!"); + midiDevice = device; + } + isConnecting = false; + } + }, null); + break; + } + } + } + + private WaltDevice.TriggerHandler triggerHandler = new WaltDevice.TriggerHandler() { + @Override + public void onReceive(WaltDevice.TriggerMessage tmsg) { + last_tWalt = tmsg.t + waltDevice.clock.baseTime; + double dt = (last_tWalt - last_tSys) / 1000.; + + deltasOutputTotal.add(dt); + logger.log(String.format(Locale.US, "Note detected: latency of %.3f ms", dt)); + if (testStateListener != null) testStateListener.onTestPartialResult(dt); + if (traceLogger != null) { + traceLogger.log(last_tSys, last_tWalt, "MIDI Output", + "Bar starts when system sends audio and ends when WALT receives note"); + } + + last_tSys += noteDelay * 1000; + repetitionsDone++; + + if (repetitionsDone < outputRepetitions) { + try { + waltDevice.command(WaltDevice.CMD_MIDI); + } catch (IOException e) { + logger.log("Failed to send command CMD_MIDI: " + e.getMessage()); + } + } else { + finishMidiOut(); + } + } + }; + + private void scheduleNotes() { + if(midiInputPort == null) { + logger.log("midiInputPort is not open"); + return; + } + long t = System.nanoTime() + ((long) noteDelay) * 1000000L; + try { + // TODO: only schedule some, then sync clock + for (int i = 0; i < outputRepetitions; i++) { + midiInputPort.send(noteMsg, 0, noteMsg.length, t + ((long) noteDelay) * 1000000L * i); + } + } catch(IOException e) { + logger.log("Unable to schedule note: " + e.getMessage()); + return; + } + last_tSys = t / 1000; + } + + private void finishMidiOut() { + logger.log("All notes detected"); + logger.log(String.format( + Locale.US, "Median total output latency %.1f ms", Utils.median(deltasOutputTotal))); + + handler.removeCallbacks(cancelMidiOutRunnable); + + if (resultHandler != null) { + resultHandler.onResult(deltasOutputTotal); + } + if (testStateListener != null) testStateListener.onTestStopped(); + if (traceLogger != null) traceLogger.flush(context); + teardownMidiOut(); + } + + private Runnable cancelMidiOutRunnable = new Runnable() { + @Override + public void run() { + logger.log("Timed out waiting for notes to be detected by WALT"); + if (testStateListener != null) testStateListener.onTestStoppedWithError(); + teardownMidiOut(); + } + }; + + private void teardownMidiOut() { + try { + midiInputPort.close(); + } catch(IOException e) { + logger.log("Error, failed to close input port: " + e.getMessage()); + } + + waltDevice.stopListener(); + waltDevice.clearTriggerHandler(); + waltDevice.checkDrift(); + } + + private Runnable requestNoteRunnable = new Runnable() { + @Override + public void run() { + logger.log("Requesting note from WALT..."); + String s; + try { + s = waltDevice.command(WaltDevice.CMD_NOTE); + } catch (IOException e) { + logger.log("Error sending NOTE command: " + e.getMessage()); + if (testStateListener != null) testStateListener.onTestStoppedWithError(); + return; + } + last_tWalt = Integer.parseInt(s); + handler.postDelayed(finishMidiInRunnable, timeout); + } + }; + + private Runnable finishMidiInRunnable = new Runnable() { + @Override + public void run() { + waltDevice.checkDrift(); + + logger.log("deltas: " + deltasToSys.toString()); + logger.log("MIDI Input Test Results:"); + logger.log(String.format(Locale.US, + "Median MIDI subsystem latency %.1f ms\nMedian total latency %.1f ms", + Utils.median(deltasToSys), Utils.median(deltasInputTotal) + )); + + if (resultHandler != null) { + resultHandler.onResult(deltasToSys, deltasInputTotal); + } + if (testStateListener != null) testStateListener.onTestStopped(); + if (traceLogger != null) traceLogger.flush(context); + teardownMidiIn(); + } + }; + + private class WaltReceiver extends MidiReceiver { + public void onSend(byte[] data, int offset, + int count, long timestamp) throws IOException { + if(count > 0 && data[offset] == (byte) 0x90) { // NoteOn message on channel 1 + handler.removeCallbacks(finishMidiInRunnable); + last_tJava = waltDevice.clock.micros(); + last_tSys = timestamp / 1000 - waltDevice.clock.baseTime; + + final double d1 = (last_tSys - last_tWalt) / 1000.; + final double d2 = (last_tJava - last_tSys) / 1000.; + final double dt = (last_tJava - last_tWalt) / 1000.; + logger.log(String.format(Locale.US, + "Result: Time to MIDI subsystem = %.3f ms, Time to Java = %.3f ms, " + + "Total = %.3f ms", + d1, d2, dt)); + deltasToSys.add(d1); + deltasInputTotal.add(dt); + if (testStateListener != null) { + handler.post(new Runnable() { + @Override + public void run() { + testStateListener.onTestPartialResult(dt); + } + }); + } + if (traceLogger != null) { + traceLogger.log(last_tWalt + waltDevice.clock.baseTime, + last_tSys + waltDevice.clock.baseTime, "MIDI Input Subsystem", + "Bar starts when WALT sends note and ends when received by MIDI subsystem"); + traceLogger.log(last_tSys + waltDevice.clock.baseTime, + last_tJava + waltDevice.clock.baseTime, "MIDI Input Java", + "Bar starts when note received by MIDI subsystem and ends when received by app"); + } + + repetitionsDone++; + if (repetitionsDone % inputSyncAfterRepetitions == 0) { + try { + waltDevice.syncClock(); + } catch (IOException e) { + logger.log("Error syncing clocks: " + e.getMessage()); + handler.post(finishMidiInRunnable); + return; + } + } + if (repetitionsDone < inputRepetitions) { + handler.post(requestNoteRunnable); + } else { + handler.post(finishMidiInRunnable); + } + } else { + logger.log(String.format(Locale.US, "Expected 0x90, got 0x%x and count was %d", + data[offset], count)); + } + } + } + + private void setupMidiIn() throws IOException { + repetitionsDone = 0; + deltasInputTotal.clear(); + deltasOutputTotal.clear(); + midiOutputPort = midiDevice.openOutputPort(0); + midiOutputPort.connect(new WaltReceiver()); + waltDevice.syncClock(); + } + + private void teardownMidiIn() { + handler.removeCallbacks(requestNoteRunnable); + handler.removeCallbacks(finishMidiInRunnable); + try { + midiOutputPort.close(); + } catch (IOException e) { + logger.log("Error, failed to close output port: " + e.getMessage()); + } + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/NumberPickerPreference.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/NumberPickerPreference.java new file mode 100644 index 0000000..9d71d42 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/NumberPickerPreference.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.preference.DialogPreference; +import android.support.v7.preference.PreferenceDialogFragmentCompat; +import android.util.AttributeSet; +import android.view.View; + +public class NumberPickerPreference extends DialogPreference { + private int currentValue; + private int maxValue; + private int minValue; + + private static final int DEFAULT_value = 0; + private static final int DEFAULT_maxValue = 0; + private static final int DEFAULT_minValue = 0; + + private final String defaultSummary; + + public NumberPickerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + defaultSummary = getSummary().toString(); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumberPickerPreference); + + try { + maxValue = a.getInt(R.styleable.NumberPickerPreference_maxValue, DEFAULT_maxValue); + minValue = a.getInt(R.styleable.NumberPickerPreference_minValue, DEFAULT_minValue); + } finally { + a.recycle(); + } + + setDialogLayoutResource(R.layout.numberpicker_dialog); + setPositiveButtonText(android.R.string.ok); + setNegativeButtonText(android.R.string.cancel); + + setDialogIcon(null); + + } + + public int getValue() { + return currentValue; + } + + public void setValue(int value) { + currentValue = value; + persistInt(currentValue); + setSummary(String.format(defaultSummary, getValue())); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getInt(index, DEFAULT_value); + } + + @Override + protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { + setValue(restorePersistedValue ? getPersistedInt(currentValue) : (Integer) defaultValue); + } + + public static class NumberPickerPreferenceDialogFragmentCompat + extends PreferenceDialogFragmentCompat { + private static final String SAVE_STATE_VALUE = "NumberPickerPreferenceDialogFragment.value"; + private CustomNumberPicker picker; + private int currentValue = 1; + + public NumberPickerPreferenceDialogFragmentCompat() { + } + + public static NumberPickerPreferenceDialogFragmentCompat newInstance(String key) { + NumberPickerPreferenceDialogFragmentCompat fragment = + new NumberPickerPreferenceDialogFragmentCompat(); + Bundle b = new Bundle(1); + b.putString(ARG_KEY, key); + fragment.setArguments(b); + return fragment; + } + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState == null) { + currentValue = getNumberPickerPreference().getValue(); + } else { + currentValue = savedInstanceState.getInt(SAVE_STATE_VALUE); + } + } + + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putInt(SAVE_STATE_VALUE, currentValue); + } + + private NumberPickerPreference getNumberPickerPreference() { + return (NumberPickerPreference) this.getPreference(); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + picker = (CustomNumberPicker) view.findViewById(R.id.numpicker_pref); + picker.setMaxValue(getNumberPickerPreference().maxValue); + picker.setMinValue(getNumberPickerPreference().minValue); + picker.setValue(currentValue); + } + + @Override + public void onDialogClosed(boolean b) { + if (b) { + int value = picker.getValue(); + if(getPreference().callChangeListener(value)) { + getNumberPickerPreference().setValue(value); + } + } + } + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/RemoteClockInfo.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/RemoteClockInfo.java new file mode 100644 index 0000000..1a42eb5 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/RemoteClockInfo.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.util.Log; + +import java.lang.reflect.Method; + +/** + * Representation of our best knowledge of the remote clock. + * All time variables here are stored in microseconds. + * + * Which time reporting function is used locally on Android: + * This app uses SystemClock.uptimeMillis() for keeping local time which, up to + * units, is the same time reported by System.nanoTime() and by + * clock_gettime(CLOCK_MONOTONIC, &ts) from time.h and is, roughly, the time + * elapsed since last boot, excluding sleep time. + * + * base_time is the local Android time when remote clock was zeroed. + * + * micros() is our best available approximation of the current reading of the remote clock. + * + * Immediately after synchronization minLag is set to zero and the remote clock guaranteed to lag + * behind what micros() reports by at most maxLag. + * + * Immediately after synchronization or an update of the bounds (minLag, maxLag) the following holds + * t_remote + minLag < micros() < t_rmote + maxLag + * + * For more details about clock synchronization refer to + * https://github.com/google/walt/blob/master/android/WALT/app/src/main/jni/README.md + * and sync_clock.c + */ + +public class RemoteClockInfo { + public int minLag; + public int maxLag; + public long baseTime; + + + public long micros() { + return microTime() - baseTime; + } + + public static long microTime() { + return System.nanoTime() / 1000; + } + + + /** + Find the wall time when uptime was zero = CLOCK_REALTIME - CLOCK_MONOTONIC + + Needed for TCP bridge because Python prior to 3.3 has no direct access to CLOCK_MONOTONIC + so the bridge returns timestamps as wall time and we need to convert them to CLOCK_MONOTONIC. + + See: + [1] https://docs.python.org/3/library/time.html#time.CLOCK_MONOTONIC + [2] http://stackoverflow.com/questions/14270300/what-is-the-difference-between-clock-monotonic-clock-monotonic-raw + [3] http://stackoverflow.com/questions/1205722/how-do-i-get-monotonic-time-durations-in-python + + android.os.SystemClock.currentTimeMicros() is hidden by @hide which means it can't be called + directly - calling it via reflection. + + See: + http://stackoverflow.com/questions/17035271/what-does-hide-mean-in-the-android-source-code + */ + public static long uptimeZero() { + long t = -1; + long dt = Long.MAX_VALUE; + try { + Class cls = Class.forName("android.os.SystemClock"); + Method myTimeGetter = cls.getMethod("currentTimeMicro"); + t = (long) myTimeGetter.invoke(null); + dt = t - microTime(); + } catch (Exception e) { + Log.i("WALT.uptimeZero", e.getMessage()); + } + + return dt; + } + + public static long currentTimeMicro() { + + long t = -1; + try { + Class cls = Class.forName("android.os.SystemClock"); + Method myTimeGetter = cls.getMethod("currentTimeMicro"); + t = (long) myTimeGetter.invoke(null); + } catch (Exception e) { + Log.i("WALT.currentTimeMicro", e.getMessage()); + } + + return t; + } + + public int getMeanLag() { + return (minLag + maxLag) / 2; + } + + public String toString(){ + return "Remote clock [us]: current time = " + micros() + " baseTime = " + baseTime + + " lagBounds = (" + minLag + ", " + maxLag + ")"; + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java new file mode 100644 index 0000000..cfe6a53 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java @@ -0,0 +1,573 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.text.method.ScrollingMovementMethod; +import android.view.Choreographer; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TextView; + +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.Description; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.chromium.latency.walt.Utils.getBooleanPreference; +import static org.chromium.latency.walt.Utils.getIntPreference; + +/** + * Measurement of screen response time when switching between black and white. + */ +public class ScreenResponseFragment extends Fragment implements View.OnClickListener { + + private static final int CURVE_TIMEOUT = 1000; // milliseconds + private static final int CURVE_BLINK_TIME = 250; // milliseconds + private static final int W2B_INDEX = 0; + private static final int B2W_INDEX = 1; + private SimpleLogger logger; + private TraceLogger traceLogger = null; + private WaltDevice waltDevice; + private Handler handler = new Handler(); + private TextView blackBox; + private View startButton; + private View stopButton; + private Spinner spinner; + private LineChart brightnessChart; + private HistogramChart latencyChart; + private View brightnessChartLayout; + private View buttonBarView; + private FastPathSurfaceView fastSurfaceView; + private int timesToBlink; + private boolean shouldShowLatencyChart = false; + private boolean isTestRunning = false; + private boolean enableFullScreen = false; + private boolean isFastPathGraphics = false; + int initiatedBlinks = 0; + int detectedBlinks = 0; + boolean isBoxWhite = false; + long lastFrameStartTime; + long lastFrameCallbackTime; + long lastSetBackgroundTime; + ArrayList deltas_w2b = new ArrayList<>(); + ArrayList deltas_b2w = new ArrayList<>(); + ArrayList deltas = new ArrayList<>(); + private static final int color_gray = Color.argb(0xFF, 0xBB, 0xBB, 0xBB); + private StringBuilder brightnessCurveData; + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!isTestRunning) { + String msg = intent.getStringExtra("message"); + blackBox.append(msg + "\n"); + } + } + }; + + public ScreenResponseFragment() { + // Required empty public constructor + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + timesToBlink = getIntPreference(getContext(), R.string.preference_screen_blinks, 20); + shouldShowLatencyChart = getBooleanPreference(getContext(), R.string.preference_show_blink_histogram, true); + enableFullScreen = getBooleanPreference(getContext(), R.string.preference_screen_fullscreen, true); + if (getBooleanPreference(getContext(), R.string.preference_systrace, true)) { + traceLogger = TraceLogger.getInstance(); + } + waltDevice = WaltDevice.getInstance(getContext()); + logger = SimpleLogger.getInstance(getContext()); + + // Inflate the layout for this fragment + final View view = inflater.inflate(R.layout.fragment_screen_response, container, false); + stopButton = view.findViewById(R.id.button_stop_screen_response); + startButton = view.findViewById(R.id.button_start_screen_response); + blackBox = (TextView) view.findViewById(R.id.txt_black_box_screen); + fastSurfaceView = (FastPathSurfaceView) view.findViewById(R.id.fast_path_surface); + spinner = (Spinner) view.findViewById(R.id.spinner_screen_response); + buttonBarView = view.findViewById(R.id.button_bar); + ArrayAdapter modeAdapter = ArrayAdapter.createFromResource(getContext(), + R.array.screen_response_mode_array, android.R.layout.simple_spinner_item); + modeAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item); + spinner.setAdapter(modeAdapter); + stopButton.setEnabled(false); + blackBox.setMovementMethod(new ScrollingMovementMethod()); + brightnessChartLayout = view.findViewById(R.id.brightness_chart_layout); + view.findViewById(R.id.button_close_chart).setOnClickListener(this); + brightnessChart = (LineChart) view.findViewById(R.id.chart); + latencyChart = (HistogramChart) view.findViewById(R.id.latency_chart); + + if (getBooleanPreference(getContext(), R.string.preference_auto_increase_brightness, true)) { + increaseScreenBrightness(); + } + return view; + } + + @Override + public void onResume() { + super.onResume(); + logger.registerReceiver(logReceiver); + // Register this fragment class as the listener for some button clicks + startButton.setOnClickListener(this); + stopButton.setOnClickListener(this); + } + + @Override + public void onPause() { + logger.unregisterReceiver(logReceiver); + super.onPause(); + } + + void startBlinkLatency() { + setFullScreen(enableFullScreen); + deltas.clear(); + deltas_b2w.clear(); + deltas_w2b.clear(); + if (shouldShowLatencyChart) { + latencyChart.clearData(); + latencyChart.setVisibility(View.VISIBLE); + latencyChart.setLabel(W2B_INDEX, "White-to-black"); + latencyChart.setLabel(B2W_INDEX, "Black-to-white"); + } + initiatedBlinks = 0; + detectedBlinks = 0; + if (isFastPathGraphics) { + blackBox.setVisibility(View.GONE); + fastSurfaceView.setVisibility(View.VISIBLE); + fastSurfaceView.setRectColor(Color.WHITE); + } else { + blackBox.setText(""); + blackBox.setBackgroundColor(Color.WHITE); + } + isBoxWhite = true; + + handler.postDelayed(startBlinking, enableFullScreen ? 800 : 300); + } + + Runnable startBlinking = new Runnable() { + @Override + public void run() { + try { + // Check for PWM + WaltDevice.TriggerMessage tmsg = waltDevice.readTriggerMessage(WaltDevice.CMD_SEND_LAST_SCREEN); + logger.log("Blink count was: " + tmsg.count); + + waltDevice.softReset(); + waltDevice.syncClock(); // Note, sync also sends CMD_RESET (but not simpleSync). + waltDevice.command(WaltDevice.CMD_AUTO_SCREEN_ON); + waltDevice.startListener(); + } catch (IOException e) { + logger.log("Error: " + e.getMessage()); + } + + // Register a callback for triggers + waltDevice.setTriggerHandler(triggerHandler); + + // post doBlink runnable + handler.postDelayed(doBlinkRunnable, 100); + } + }; + + Runnable doBlinkRunnable = new Runnable() { + @Override + public void run() { + if (!isTestRunning) return; + logger.log("======\ndoBlink.run(), initiatedBlinks = " + initiatedBlinks + " detectedBlinks = " + detectedBlinks); + // Check if we saw some transitions without blinking, this would usually mean + // the screen has PWM enabled, warn and ask the user to turn it off. + if (initiatedBlinks == 0 && detectedBlinks > 1) { + logger.log("Unexpected blinks detected, probably PWM, turn it off"); + isTestRunning = false; + stopButton.setEnabled(false); + startButton.setEnabled(true); + showPwmDialog(); + return; + } + + if (initiatedBlinks >= timesToBlink) { + isTestRunning = false; + finishAndShowStats(); + return; + } + + // * 2 flip the screen, save time as last flip time (last flip direction?) + + isBoxWhite = !isBoxWhite; + int nextColor = isBoxWhite ? Color.WHITE : Color.BLACK; + initiatedBlinks++; + if (traceLogger != null) { + traceLogger.log(RemoteClockInfo.microTime(), RemoteClockInfo.microTime() + 1000, + "Request-to-" + (isBoxWhite ? "white" : "black"), + "Application has called setBackgroundColor at start of bar"); + } + if (isFastPathGraphics) { + fastSurfaceView.setRectColor(nextColor); + } else { + blackBox.setBackgroundColor(nextColor); + } + lastSetBackgroundTime = waltDevice.clock.micros(); + + // Set up a callback to run on next frame render to collect the timestamp + Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + // frameTimeNanos is he time in nanoseconds when the frame started being + // rendered, in the nanoTime() timebase. + lastFrameStartTime = frameTimeNanos / 1000 - waltDevice.clock.baseTime; + lastFrameCallbackTime = System.nanoTime() / 1000 - waltDevice.clock.baseTime; + } + }); + + + // Repost doBlink to some far away time to blink again even if nothing arrives from + // Teensy. This callback will almost always get cancelled by onIncomingTimestamp() + handler.postDelayed(doBlinkRunnable, 550 + (long) (Math.random()*100)); + } + }; + + private void showPwmDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage("Detected extra blinks, please set your brightness to max") + .setTitle("Unexpected Blinks") + .setPositiveButton("OK", null); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + private WaltDevice.TriggerHandler triggerHandler = new WaltDevice.TriggerHandler() { + @Override + public void onReceive(WaltDevice.TriggerMessage tmsg) { + // Remove the far away doBlink callback + handler.removeCallbacks(doBlinkRunnable); + + detectedBlinks++; + logger.log("blink counts " + initiatedBlinks + " " + detectedBlinks); + if (initiatedBlinks == 0) { + if (detectedBlinks < 5) { + logger.log("got incoming but initiatedBlinks = 0"); + return; + } else { + logger.log("Looks like PWM is used for this screen, turn auto brightness off and set it to max brightness"); + showPwmDialog(); + return; + } + } + + final long startTimeMicros = lastFrameStartTime + waltDevice.clock.baseTime; + final long finishTimeMicros = tmsg.t + waltDevice.clock.baseTime; + if (traceLogger != null) { + traceLogger.log(startTimeMicros, finishTimeMicros, + isBoxWhite ? "Black-to-white" : "White-to-black", + "Bar starts at beginning of frame and ends when photosensor detects blink"); + } + + double dt = (tmsg.t - lastFrameStartTime) / 1000.; + deltas.add(dt); + if (isBoxWhite) { // Current color is the color we transitioned to + deltas_b2w.add(dt); + } else { + deltas_w2b.add(dt); + } + if (shouldShowLatencyChart) latencyChart.addEntry(isBoxWhite ? B2W_INDEX : W2B_INDEX, dt); + + // Other times can be important, logging them to allow more detailed analysis + logger.log(String.format(Locale.US, + "Times [ms]: setBG:%.3f callback:%.3f physical:%.3f black2white:%d", + (lastSetBackgroundTime - lastFrameStartTime) / 1000.0, + (lastFrameCallbackTime - lastFrameStartTime) / 1000.0, + dt, + isBoxWhite ? 1 : 0 + )); + if (traceLogger != null) { + traceLogger.log(lastFrameCallbackTime + waltDevice.clock.baseTime, + lastFrameCallbackTime + waltDevice.clock.baseTime + 1000, + isBoxWhite ? "FrameCallback Black-to-white" : "FrameCallback White-to-black", + "FrameCallback was called at start of bar"); + } + // Schedule another blink soon-ish + handler.postDelayed(doBlinkRunnable, 40 + (long) (Math.random()*20)); + } + }; + + + void finishAndShowStats() { + setFullScreen(false); + // Stop the USB listener + waltDevice.stopListener(); + + // Unregister trigger handler + waltDevice.clearTriggerHandler(); + + waltDevice.sendAndFlush(WaltDevice.CMD_AUTO_SCREEN_OFF); + + waltDevice.checkDrift(); + + // Show deltas and the median + /* // Debug printouts + logger.log("deltas = array(" + deltas.toString() + ")"); + logger.log("deltas_w2b = array(" + deltas_w2b.toString() + ")"); + logger.log("deltas_b2w = array(" + deltas_b2w.toString() + ")"); + */ + + double median_b2w = Utils.median(deltas_b2w); + double median_w2b = Utils.median(deltas_w2b); + logger.log(String.format(Locale.US, + "\n-------------------------------\n" + + "Median screen response latencies (N=%d):\n" + + "Black to white: %.1f ms (N=%d)\n" + + "White to black: %.1f ms (N=%d)\n" + + "Average: %.1f ms\n" + + "-------------------------------\n", + deltas.size(), + median_b2w, deltas_b2w.size(), + median_w2b, deltas_w2b.size(), + (median_b2w + median_w2b) / 2 + )); + + if (traceLogger != null) traceLogger.flush(getContext()); + fastSurfaceView.setVisibility(View.GONE); + blackBox.setVisibility(View.VISIBLE); + blackBox.setText(logger.getLogText()); + blackBox.setMovementMethod(new ScrollingMovementMethod()); + blackBox.setBackgroundColor(color_gray); + stopButton.setEnabled(false); + startButton.setEnabled(true); + if (shouldShowLatencyChart) { + latencyChart.setLabel(W2B_INDEX, String.format(Locale.US, "White-to-black m=%.1f ms", median_w2b)); + latencyChart.setLabel(B2W_INDEX, String.format(Locale.US, "Black-to-white m=%.1f ms", median_b2w)); + } + LogUploader.uploadIfAutoEnabled(getContext()); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.button_stop_screen_response) { + isTestRunning = false; + handler.removeCallbacks(doBlinkRunnable); + handler.removeCallbacks(startBlinking); + finishAndShowStats(); + return; + } + + if (v.getId() == R.id.button_start_screen_response) { + brightnessChartLayout.setVisibility(View.GONE); + latencyChart.setVisibility(View.GONE); + if (!waltDevice.isConnected()) { + logger.log("Error starting test: WALT is not connected"); + return; + } + + isTestRunning = true; + startButton.setEnabled(false); + blackBox.setBackgroundColor(Color.BLACK); + blackBox.setText(""); + isFastPathGraphics = false; + final int spinnerPosition = spinner.getSelectedItemPosition(); + if (spinnerPosition == 0) { + logger.log("Starting screen response measurement"); + stopButton.setEnabled(true); + startBlinkLatency(); + } else if (spinnerPosition == 1) { + logger.log("Starting screen brightness curve measurement"); + startBrightnessCurve(); + } else if (spinnerPosition == 2) { + logger.log("Starting fast-path screen response measurement"); + isFastPathGraphics = true; + startBlinkLatency(); + } else { + logger.log("ERROR: Spinner position is out of range"); + } + return; + } + + if (v.getId() == R.id.button_close_chart) { + brightnessChartLayout.setVisibility(View.GONE); + return; + } + } + + private WaltDevice.TriggerHandler brightnessTriggerHandler = new WaltDevice.TriggerHandler() { + @Override + public void onReceive(WaltDevice.TriggerMessage tmsg) { + logger.log("ERROR: Brightness curve trigger got a trigger message, " + + "this should never happen." + ); + } + + @Override + public void onReceiveRaw(String s) { + brightnessCurveData.append(s); + if (s.trim().equals("end")) { + // Remove the delayed callback and run it now + handler.removeCallbacks(finishBrightnessCurve); + handler.post(finishBrightnessCurve); + } + } + }; + + void startBrightnessCurve() { + try { + brightnessCurveData = new StringBuilder(); + waltDevice.syncClock(); + waltDevice.startListener(); + } catch (IOException e) { + logger.log("Error starting test: " + e.getMessage()); + isTestRunning = false; + startButton.setEnabled(true); + return; + } + setFullScreen(enableFullScreen); + blackBox.setText(""); + blackBox.setBackgroundColor(Color.BLACK); + handler.postDelayed(startBrightness, enableFullScreen ? 1000 : CURVE_BLINK_TIME); + } + + Runnable startBrightness = new Runnable() { + @Override + public void run() { + waltDevice.setTriggerHandler(brightnessTriggerHandler); + long tStart = waltDevice.clock.micros(); + + try { + waltDevice.command(WaltDevice.CMD_BRIGHTNESS_CURVE); + } catch (IOException e) { + logger.log("Error sending command CMD_BRIGHTNESS_CURVE: " + e.getMessage()); + isTestRunning = false; + startButton.setEnabled(true); + return; + } + + blackBox.setBackgroundColor(Color.WHITE); + + logger.log("=== Screen brightness curve: ===\nt_start: " + tStart); + + handler.postDelayed(finishBrightnessCurve, CURVE_TIMEOUT); + + // Schedule the screen to flip back to black in CURVE_BLINK_TIME ms + handler.postDelayed(new Runnable() { + @Override + public void run() { + long tBack = waltDevice.clock.micros(); + blackBox.setBackgroundColor(Color.BLACK); + logger.log("t_back: " + tBack); + + } + }, CURVE_BLINK_TIME); + } + }; + + Runnable finishBrightnessCurve = new Runnable() { + @Override + public void run() { + waltDevice.stopListener(); + waltDevice.clearTriggerHandler(); + + // TODO: Add option to save this data into a separate file rather than the main log. + logger.log(brightnessCurveData.toString()); + logger.log("=== End of screen brightness data ==="); + + blackBox.setText(logger.getLogText()); + blackBox.setMovementMethod(new ScrollingMovementMethod()); + blackBox.setBackgroundColor(color_gray); + isTestRunning = false; + startButton.setEnabled(true); + setFullScreen(false); + drawBrightnessChart(); + LogUploader.uploadIfAutoEnabled(getContext()); + } + }; + + private void drawBrightnessChart() { + final String brightnessCurveString = brightnessCurveData.toString(); + List entries = new ArrayList<>(); + + // "u" marks the start of the brightness curve data + int startIndex = brightnessCurveString.indexOf("u") + 1; + int endIndex = brightnessCurveString.indexOf("end"); + if (endIndex == -1) endIndex = brightnessCurveString.length(); + + String[] brightnessStrings = + brightnessCurveString.substring(startIndex, endIndex).trim().split("\n"); + for (String str : brightnessStrings) { + String[] arr = str.split(" "); + final float timestampMs = Integer.parseInt(arr[0]) / 1000f; + final float brightness = Integer.parseInt(arr[1]); + entries.add(new Entry(timestampMs, brightness)); + } + LineDataSet dataSet = new LineDataSet(entries, "Brightness"); + dataSet.setColor(Color.BLACK); + dataSet.setValueTextColor(Color.BLACK); + dataSet.setCircleColor(Color.BLACK); + dataSet.setCircleRadius(1.5f); + dataSet.setCircleColorHole(Color.DKGRAY); + LineData lineData = new LineData(dataSet); + brightnessChart.setData(lineData); + final Description desc = new Description(); + desc.setText("Screen Brightness [digital level 0-1023] vs. Time [ms]"); + desc.setTextSize(12f); + brightnessChart.setDescription(desc); + brightnessChart.getLegend().setEnabled(false); + brightnessChart.invalidate(); + brightnessChartLayout.setVisibility(View.VISIBLE); + } + + private void increaseScreenBrightness() { + final WindowManager.LayoutParams layoutParams = getActivity().getWindow().getAttributes(); + layoutParams.screenBrightness = 1f; + getActivity().getWindow().setAttributes(layoutParams); + } + + private void setFullScreen(boolean enable) { + final AppCompatActivity activity = (AppCompatActivity) getActivity(); + final ActionBar actionBar = activity != null ? activity.getSupportActionBar() : null; + int newVisibility = 0; + if (enable) { + if (actionBar != null) actionBar.hide(); + buttonBarView.setVisibility(View.GONE); + newVisibility |= View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + } else { + if (actionBar != null) actionBar.show(); + buttonBarView.setVisibility(View.VISIBLE); + } + if (activity != null) activity.getWindow().getDecorView().setSystemUiVisibility(newVisibility); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/SettingsFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/SettingsFragment.java new file mode 100644 index 0000000..4f74fc4 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/SettingsFragment.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.content.ContextCompat; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceFragmentCompat; +import android.support.v7.preference.PreferenceScreen; +import android.support.v7.widget.Toolbar; +import android.view.View; + + +public class SettingsFragment extends PreferenceFragmentCompat implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback { + + private Toolbar toolbar; + + public SettingsFragment() { + // Required empty public constructor + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + // Load the preferences from an XML resource + setPreferencesFromResource(R.xml.preferences, rootKey); + + PreferenceScreen prefMidiScreen = + (PreferenceScreen) getPreferenceScreen().findPreference("pref_midi_screen"); + if (prefMidiScreen != null) { + boolean hasMidi = + getContext().getPackageManager().hasSystemFeature("android.software.midi"); + prefMidiScreen.setVisible(hasMidi); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar_main); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.ColorBackground)); + } + + @Override + public void onDisplayPreferenceDialog(Preference preference) { + if (preference instanceof NumberPickerPreference) { + DialogFragment fragment = NumberPickerPreference. + NumberPickerPreferenceDialogFragmentCompat.newInstance(preference.getKey()); + fragment.setTargetFragment(this, 0); + fragment.show(getFragmentManager(), + "android.support.v7.preference.PreferenceFragment.DIALOG"); + } else { + super.onDisplayPreferenceDialog(preference); + } + } + + @Override + public Fragment getCallbackFragment() { + return this; + } + + @Override + public boolean onPreferenceStartScreen(PreferenceFragmentCompat preferenceFragmentCompat, + PreferenceScreen preferenceScreen) { + SettingsFragment fragment = new SettingsFragment(); + Bundle args = new Bundle(); + args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, preferenceScreen.getKey()); + fragment.setArguments(args); + + FragmentTransaction ft = preferenceFragmentCompat.getFragmentManager().beginTransaction(); + ft.add(R.id.fragment_container, fragment, preferenceScreen.getKey()); + ft.addToBackStack(preferenceScreen.getTitle().toString()); + ft.commit(); + + toolbar.setTitle(preferenceScreen.getTitle()); + return true; + } + +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/SimpleLogger.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/SimpleLogger.java new file mode 100644 index 0000000..6059e0f --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/SimpleLogger.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +/** + * A very simple logger that keeps its data in a StringBuilder. We need on screen log because the + * USB port is often taken and we don't have easy access to adb log. + */ +public class SimpleLogger { + private static final String LOG_INTENT = "log-message"; + public static final String TAG = "WaltLogger"; + + private static final Object LOCK = new Object(); + private static SimpleLogger instance; + + private StringBuilder sb = new StringBuilder(); + private LocalBroadcastManager broadcastManager; + + public static SimpleLogger getInstance(Context context) { + synchronized (LOCK) { + if (instance == null) { + instance = new SimpleLogger(context.getApplicationContext()); + } + return instance; + } + } + + private SimpleLogger(Context context) { + broadcastManager = LocalBroadcastManager.getInstance(context); + } + + public synchronized void log(String msg) { + Log.i(TAG, msg); + sb.append(msg); + sb.append('\n'); + if (broadcastManager != null) { + Intent intent = new Intent(LOG_INTENT); + intent.putExtra("message", msg); + broadcastManager.sendBroadcast(intent); + } + } + + public void registerReceiver(BroadcastReceiver broadcastReceiver) { + broadcastManager.registerReceiver(broadcastReceiver, new IntentFilter(LOG_INTENT)); + } + + public void unregisterReceiver(BroadcastReceiver broadcastReceiver) { + broadcastManager.unregisterReceiver(broadcastReceiver); + } + + public String getLogText() { + return sb.toString(); + } + + public void clear() { + sb = new StringBuilder(); + } + +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/TapLatencyFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/TapLatencyFragment.java new file mode 100644 index 0000000..64e333d --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/TapLatencyFragment.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.method.ScrollingMovementMethod; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; + +import static org.chromium.latency.walt.Utils.getBooleanPreference; + +public class TapLatencyFragment extends Fragment + implements View.OnClickListener { + + private static final int ACTION_DOWN_INDEX = 0; + private static final int ACTION_UP_INDEX = 1; + private SimpleLogger logger; + private TraceLogger traceLogger; + private WaltDevice waltDevice; + private TextView logTextView; + private TextView tapCatcherView; + private TextView tapCountsView; + private TextView moveCountsView; + private ImageButton finishButton; + private ImageButton restartButton; + private HistogramChart latencyChart; + private int moveCount = 0; + private int allDownCount = 0; + private int allUpCount = 0; + private int okDownCount = 0; + private int okUpCount = 0; + private boolean shouldShowLatencyChart = false; + + ArrayList eventList = new ArrayList<>(); + ArrayList p2kDown = new ArrayList<>(); + ArrayList p2kUp = new ArrayList<>(); + ArrayList k2cDown = new ArrayList<>(); + ArrayList k2cUp = new ArrayList<>(); + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String msg = intent.getStringExtra("message"); + TapLatencyFragment.this.appendLogText(msg); + } + }; + + private View.OnTouchListener touchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + UsMotionEvent tapEvent = new UsMotionEvent(event, waltDevice.clock.baseTime); + + if(tapEvent.action != MotionEvent.ACTION_UP && tapEvent.action != MotionEvent.ACTION_DOWN) { + moveCount++; + updateCountsDisplay(); + return true; + } + + // Debug: logger.log("\n"+ action + " event received: " + tapEvent.toStringLong()); + tapEvent.physicalTime = waltDevice.readLastShockTime(); + + tapEvent.isOk = checkTapSanity(tapEvent); + // Save it in any case so we can do stats on bad events later + eventList.add(tapEvent); + + final double physicalToKernelTime = (tapEvent.kernelTime - tapEvent.physicalTime) / 1000.; + final double kernelToCallbackTime = (tapEvent.createTime - tapEvent.kernelTime) / 1000.; + if (tapEvent.action == MotionEvent.ACTION_DOWN) { + allDownCount++; + if (tapEvent.isOk) { + okDownCount++; + p2kDown.add(physicalToKernelTime); + k2cDown.add(kernelToCallbackTime); + if (shouldShowLatencyChart) latencyChart.addEntry(ACTION_DOWN_INDEX, physicalToKernelTime); + logger.log(String.format(Locale.US, + "ACTION_DOWN:\ntouch2kernel: %.1f ms\nkernel2java: %.1f ms", + physicalToKernelTime, kernelToCallbackTime)); + } + } else if (tapEvent.action == MotionEvent.ACTION_UP) { + allUpCount++; + if (tapEvent.isOk) { + okUpCount++; + p2kUp.add(physicalToKernelTime); + k2cUp.add(kernelToCallbackTime); + if (shouldShowLatencyChart) latencyChart.addEntry(ACTION_UP_INDEX, physicalToKernelTime); + logger.log(String.format(Locale.US, + "ACTION_UP:\ntouch2kernel: %.1f ms\nkernel2java: %.1f ms", + physicalToKernelTime, kernelToCallbackTime)); + } + } + traceLogEvent(tapEvent); + + updateCountsDisplay(); + return true; + } + }; + + private void traceLogEvent(UsMotionEvent tapEvent) { + if (!tapEvent.isOk) return; + if (traceLogger == null) return; + if (tapEvent.action != MotionEvent.ACTION_DOWN && tapEvent.action != MotionEvent.ACTION_UP) return; + final String title = tapEvent.action == MotionEvent.ACTION_UP ? "Tap-Up" : "Tap-Down"; + traceLogger.log(tapEvent.physicalTime + waltDevice.clock.baseTime, + tapEvent.kernelTime + waltDevice.clock.baseTime, title + " Physical", + "Bar starts at accelerometer shock and ends at kernel time of tap event"); + traceLogger.log(tapEvent.kernelTime + waltDevice.clock.baseTime, + tapEvent.createTime + waltDevice.clock.baseTime, title + " App Callback", + "Bar starts at kernel time of tap event and ends at app callback time"); + } + + public TapLatencyFragment() { + // Required empty public constructor + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + shouldShowLatencyChart = getBooleanPreference(getContext(), R.string.preference_show_tap_histogram, true); + if (getBooleanPreference(getContext(), R.string.preference_systrace, true)) { + traceLogger = TraceLogger.getInstance(); + } + waltDevice = WaltDevice.getInstance(getContext()); + logger = SimpleLogger.getInstance(getContext()); + // Inflate the layout for this fragment + final View view = inflater.inflate(R.layout.fragment_tap_latency, container, false); + restartButton = (ImageButton) view.findViewById(R.id.button_restart_tap); + finishButton = (ImageButton) view.findViewById(R.id.button_finish_tap); + tapCatcherView = (TextView) view.findViewById(R.id.tap_catcher); + logTextView = (TextView) view.findViewById(R.id.txt_log_tap_latency); + tapCountsView = (TextView) view.findViewById(R.id.txt_tap_counts); + moveCountsView = (TextView) view.findViewById(R.id.txt_move_count); + latencyChart = (HistogramChart) view.findViewById(R.id.latency_chart); + logTextView.setMovementMethod(new ScrollingMovementMethod()); + finishButton.setEnabled(false); + return view; + } + + @Override + public void onResume() { + super.onResume(); + + logTextView.setText(logger.getLogText()); + logger.registerReceiver(logReceiver); + + // Register this fragment class as the listener for some button clicks + restartButton.setOnClickListener(this); + finishButton.setOnClickListener(this); + } + + @Override + public void onPause() { + logger.unregisterReceiver(logReceiver); + super.onPause(); + } + + public void appendLogText(String msg) { + logTextView.append(msg + "\n"); + } + + public boolean checkTapSanity(UsMotionEvent e) { + String action = e.getActionString(); + double dt = (e.kernelTime - e.physicalTime) / 1000.0; + + if (e.physicalTime == 0) { + logger.log(action + " no shock found"); + return false; + } + + if (dt < 0 || dt > 200) { + logger.log(action + " bogus kernelTime, ignored, dt=" + dt); + return false; + } + return true; + } + + void updateCountsDisplay() { + String tpl = "N ↓%d (%d) ↑%d (%d)"; + tapCountsView.setText(String.format(Locale.US, + tpl, + okDownCount, + allDownCount, + okUpCount, + allUpCount + )); + + moveCountsView.setText(String.format(Locale.US, "⇄ %d", moveCount)); + } + + void restartMeasurement() { + logger.log("\n## Restarting tap latency measurement. Re-sync clocks ..."); + try { + waltDevice.softReset(); + waltDevice.syncClock(); + } catch (IOException e) { + logger.log("Error syncing clocks: " + e.getMessage()); + restartButton.setImageResource(R.drawable.ic_play_arrow_black_24dp); + finishButton.setEnabled(false); + latencyChart.setVisibility(View.GONE); + return; + } + + eventList.clear(); + p2kDown.clear(); + p2kUp.clear(); + k2cDown.clear(); + k2cUp.clear(); + + moveCount = 0; + allDownCount = 0; + allUpCount = 0; + okDownCount = 0; + okUpCount = 0; + + updateCountsDisplay(); + tapCatcherView.setOnTouchListener(touchListener); + } + + void finishAndShowStats() { + tapCatcherView.setOnTouchListener(null); + waltDevice.checkDrift(); + logger.log("\n-------------------------------"); + logger.log(String.format(Locale.US, + "Tap latency results:\n" + + "Number of events recorded:\n" + + " ACTION_DOWN %d (bad %d)\n" + + " ACTION_UP %d (bad %d)\n" + + " ACTION_MOVE %d", + okDownCount, + allDownCount - okDownCount, + okUpCount, + allUpCount - okUpCount, + moveCount + )); + + logger.log("ACTION_DOWN median times:"); + logger.log(String.format(Locale.US, + " Touch to kernel: %.1f ms\n Kernel to Java: %.1f ms", + Utils.median(p2kDown), + Utils.median(k2cDown) + )); + logger.log("ACTION_UP median times:"); + logger.log(String.format(Locale.US, + " Touch to kernel: %.1f ms\n Kernel to Java: %.1f ms", + Utils.median(p2kUp), + Utils.median(k2cUp) + )); + logger.log("-------------------------------"); + if (traceLogger != null) traceLogger.flush(getContext()); + + if (shouldShowLatencyChart) { + latencyChart.setLabel(ACTION_DOWN_INDEX, String.format(Locale.US, "ACTION_DOWN median=%.1f ms", Utils.median(p2kDown))); + latencyChart.setLabel(ACTION_UP_INDEX, String.format(Locale.US, "ACTION_UP median=%.1f ms", Utils.median(p2kUp))); + } + LogUploader.uploadIfAutoEnabled(getContext()); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.button_restart_tap) { + restartButton.setImageResource(R.drawable.ic_refresh_black_24dp); + finishButton.setEnabled(true); + if (shouldShowLatencyChart) { + latencyChart.setVisibility(View.VISIBLE); + latencyChart.clearData(); + latencyChart.setLabel(ACTION_DOWN_INDEX, "ACTION_DOWN"); + latencyChart.setLabel(ACTION_UP_INDEX, "ACTION_UP"); + } + restartMeasurement(); + return; + } + + if (v.getId() == R.id.button_finish_tap) { + finishButton.setEnabled(false); + finishAndShowStats(); + restartButton.setImageResource(R.drawable.ic_play_arrow_black_24dp); + return; + } + + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/TouchCatcherView.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/TouchCatcherView.java new file mode 100644 index 0000000..9e04056 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/TouchCatcherView.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + + + +public class TouchCatcherView extends View { + + private Paint linePaint = new Paint(); + private WaltDevice waltDevice; + private boolean isAnimated = false; + + private double animationAmplitude = 0.4; // Fraction of view height + private double lineLength = 0.6; // Fraction of view width + public final int animationPeriod_us = 1000000; + + public void startAnimation() { + isAnimated = true; + invalidate(); + } + + public void stopAnimation() { + isAnimated = false; + invalidate(); + } + + public TouchCatcherView(Context context, AttributeSet attrs) { + super(context, attrs); + waltDevice = WaltDevice.getInstance(context); + initialisePaint(); + } + + private void initialisePaint() { + float density = getResources().getDisplayMetrics().density; + float lineWidth = 10f * density; + linePaint.setColor(Color.GREEN); + linePaint.setStrokeWidth(lineWidth); + } + + public static double markerPosition(long t_us, int period_us) { + // Normalized time within a period, goes from 0 to 1 + double t = (t_us % period_us) / (double) period_us; + + // Triangular wave with unit amplitude + // 1| * * + // | * * * + // 0-----*-------*---|---*-----> t + // | * * 1 * + // -1| * * + double y_tri = -1 + 4 * Math.abs(t - 0.5); + + // Apply some smoothing to get a feeling of deceleration and acceleration at the edges. + // f(y) = y / {1 + exp(b(|y|-1))/(b-1)} + // This is inspired by Fermi function and adjusted to have continuous derivative at extrema. + // b = beta is a dimensionless smoothing parameter, value selected by experimentation. + // Higher value gives less smoothing = closer to original triangular wave. + double beta = 4; + double y_smooth = y_tri / (1 + Math.exp(beta*(Math.abs(y_tri)-1))/(beta - 1)); + return y_smooth; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (!isAnimated) return; + + int h = getHeight(); + double normPos = markerPosition(waltDevice.clock.micros(), animationPeriod_us); + int pos = (int) (h * (0.5 + animationAmplitude * normPos)); + // Log.i("AnimatedView", "Pos is " + pos); + int w = getWidth(); + + int lineStart = (int) (w * (1 - lineLength) / 2); + int lineEnd = (int) (w * (1 + lineLength) / 2); + canvas.drawLine(lineStart, pos, lineEnd, pos, linePaint); + + // Run every frame + invalidate(); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/TraceLogger.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/TraceLogger.java new file mode 100644 index 0000000..6fdb8d9 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/TraceLogger.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.os.Environment; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.text.DecimalFormat; +import java.util.ArrayList; + +/** + * Used to log events for Android systrace + */ +class TraceLogger { + + private static final Object LOCK = new Object(); + private static TraceLogger instance; + + private ArrayList traceEvents; + + public static TraceLogger getInstance() { + synchronized (LOCK) { + if (instance == null) { + instance = new TraceLogger(); + } + return instance; + } + } + + private TraceLogger() { + traceEvents = new ArrayList<>(); + } + + public synchronized void log(long startTimeMicros, long finishTimeMicros, String title, String description) { + traceEvents.add(new TraceEvent(startTimeMicros, finishTimeMicros, title, description)); + } + + public String getLogText() { + DecimalFormat df = new DecimalFormat(".000000"); + StringBuilder sb = new StringBuilder(); + int pid = android.os.Process.myPid(); + for (TraceEvent e : traceEvents) { + sb.append(String.format( + "WALTThread-1234 (%d) [000] ...1 %s: tracing_mark_write: B|%d|%s|description=%s|WALT\n", + pid, df.format(e.startTimeMicros / 1e6), pid, e.title, e.description)); + sb.append(String.format( + "WALTThread-1234 (%d) [000] ...1 %s: tracing_mark_write: E|%d|%s||WALT\n", + pid, df.format(e.finishTimeMicros / 1e6), pid, e.title)); + } + return sb.toString(); + } + + void flush(Context context) { + SimpleLogger logger = SimpleLogger.getInstance(context); + if (!isExternalStorageWritable()) { + logger.log("ERROR: could not write systrace logs to file"); + return; + } + writeSystraceLogs(context); + traceEvents.clear(); + } + + private void writeSystraceLogs(Context context) { + File file = new File(context.getExternalFilesDir(null), "trace.txt"); + SimpleLogger logger = SimpleLogger.getInstance(context); + try { + OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file, true)); + writer.write(getLogText()); + writer.close(); + logger.log(String.format("TraceLogger wrote %d events to %s", + traceEvents.size(), file.getAbsolutePath())); + } catch (IOException e) { + logger.log("ERROR: IOException writing to trace.txt"); + e.printStackTrace(); + } + } + + private boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } + + private class TraceEvent { + long startTimeMicros; + long finishTimeMicros; + String title; + String description; + TraceEvent(long startTimeMicros, long finishTimeMicros, String title, String description) { + this.startTimeMicros = startTimeMicros; + this.finishTimeMicros = finishTimeMicros; + this.title = title; + this.description = description; + } + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/UsMotionEvent.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/UsMotionEvent.java new file mode 100644 index 0000000..e961949 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/UsMotionEvent.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.util.Log; +import android.view.MotionEvent; + +import java.lang.reflect.Method; + +/** + * A convenient representation of MotionEvent events + * - microsecond accuracy + * - no bundling of ACTION_MOVE events + */ + +public class UsMotionEvent { + + public long physicalTime, kernelTime, createTime; + public float x, y; + public int slot; + public int action; + public int num; + public String metadata; + public long baseTime; + + public boolean isOk = false; + + /** + * + * @param event - MotionEvent as received by the handler. + * @param baseTime - base time of the last clock sync. + */ + public UsMotionEvent(MotionEvent event, long baseTime) { + createTime = RemoteClockInfo.microTime() - baseTime; + this.baseTime = baseTime; + slot = -1; + kernelTime = getEventTimeMicro(event) - baseTime; + x = event.getX(); + y = event.getY(); + action = event.getAction(); + } + + public UsMotionEvent(MotionEvent event, long baseTime, int pos) { + createTime = RemoteClockInfo.microTime() - baseTime; + this.baseTime = baseTime; + slot = pos; + action = MotionEvent.ACTION_MOVE; // Only MOVE events get bundled with history + + kernelTime = getHistoricalEventTimeMicro(event, pos) - baseTime; + x = event.getHistoricalX(pos); + y = event.getHistoricalY(pos); + } + + public String getActionString() { + return actionToString(action); + } + + + public String toString() { + return String.format("%d %f %f", + kernelTime, x, y); + + } + + public String toStringLong() { + return String.format("Event: t=%d x=%.1f y=%.1f slot=%d num=%d %s", + kernelTime, x, y, slot, num, actionToString(action)); + + } + + // The MotionEvent.actionToString is not present before API 19 + public static String actionToString(int action) { + switch (action) { + case MotionEvent.ACTION_DOWN: + return "ACTION_DOWN"; + case MotionEvent.ACTION_UP: + return "ACTION_UP"; + case MotionEvent.ACTION_CANCEL: + return "ACTION_CANCEL"; + case MotionEvent.ACTION_OUTSIDE: + return "ACTION_OUTSIDE"; + case MotionEvent.ACTION_MOVE: + return "ACTION_MOVE"; + case MotionEvent.ACTION_HOVER_MOVE: + return "ACTION_HOVER_MOVE"; + case MotionEvent.ACTION_SCROLL: + return "ACTION_SCROLL"; + case MotionEvent.ACTION_HOVER_ENTER: + return "ACTION_HOVER_ENTER"; + case MotionEvent.ACTION_HOVER_EXIT: + return "ACTION_HOVER_EXIT"; + } + return "UNKNOWN_ACTION"; + } + + /** + MotionEvent.getEventTime() function only provides millisecond resolution. + There is a MotionEvent.getEventTimeNano() function but for some reason it + is hidden by @hide which means it can't be called directly. + Calling is via reflection. + + See: + http://stackoverflow.com/questions/17035271/what-does-hide-mean-in-the-android-source-code + */ + private long getEventTimeMicro(MotionEvent event) { + long t_nanos = -1; + try { + Class cls = Class.forName("android.view.MotionEvent"); + Method myTimeGetter = cls.getMethod("getEventTimeNano"); + t_nanos = (long) myTimeGetter.invoke(event); + } catch (Exception e) { + Log.i("WALT.MsMotionEvent", e.getMessage()); + } + + return t_nanos / 1000; + } + + private long getHistoricalEventTimeMicro(MotionEvent event, int pos) { + long t_nanos = -1; + try { + Class cls = Class.forName("android.view.MotionEvent"); + Method myTimeGetter = cls.getMethod("getHistoricalEventTimeNano", new Class[] {int.class}); + t_nanos = (long) myTimeGetter.invoke(event, new Object[]{pos}); + } catch (Exception e) { + Log.i("WALT.MsMotionEvent", e.getMessage()); + } + + return t_nanos / 1000; + } + +} + diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/Utils.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/Utils.java new file mode 100644 index 0000000..19c7488 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/Utils.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.StringRes; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Kitchen sink for small utility functions + */ +public class Utils { + public static double median(ArrayList arrList) { + ArrayList lst = new ArrayList<>(arrList); + Collections.sort(lst); + int len = lst.size(); + if (len == 0) { + return Double.NaN; + } + + if (len % 2 == 1) { + return lst.get(len / 2); + } else { + return 0.5 * (lst.get(len / 2) + lst.get(len / 2 - 1)); + } + } + + public static double mean(double[] x) { + double s = 0; + for (double v: x) s += v; + return s / x.length; + } + + /** + * Linear interpolation styled after numpy.interp() + * returns values at points x interpolated using xp, yp data points + * Both x and xp must be monotonically increasing. + */ + public static double[] interp(double[] x, double[] xp, double[] yp) { + // assuming that x and xp are already sorted. + // go over x and xp as if we are merging them + double[] y = new double[x.length]; + int i = 0; + int ip = 0; + + // skip x points that are outside the data + while (i < x.length && x[i] < xp[0]) i++; + + while (ip < xp.length && i < x.length) { + // skip until we see an xp >= current x + while (ip < xp.length && xp[ip] < x[i]) ip++; + if (ip >= xp.length) break; + if (xp[ip] == x[i]) { + y[i] = yp[ip]; + } else { + double dy = yp[ip] - yp[ip-1]; + double dx = xp[ip] - xp[ip-1]; + y[i] = yp[ip-1] + dy/dx * (x[i] - xp[ip-1]); + } + i++; + } + return y; + } + + public static double stdev(double[] a) { + double m = mean(a); + double sumsq = 0; + for (double v : a) sumsq += (v-m)*(v-m); + return Math.sqrt(sumsq / a.length); + } + + /** + * Similar to numpy.extract() + * returns a shorter array with values taken from x at indices where indicator == value + */ + public static double[] extract(int[] indicator, int value, double[] arr) { + if (arr.length != indicator.length) { + throw new IllegalArgumentException("Length of arr and indicator must be the same."); + } + int newLen = 0; + for (int v: indicator) if (v == value) newLen++; + double[] newx = new double[newLen]; + + int j = 0; + for (int i=0; i 0) { + // flushing all incoming data + } + } catch (Exception e) { + logger.log("Exception in sendAndFlush: " + e.getMessage()); + e.printStackTrace(); + } + } + + public void softReset() { + sendAndFlush(CMD_RESET); + } + + String command(char cmd, char ack) throws IOException { + if (!isListenerStopped()) { + connection.sendByte(cmd); // TODO: check response even if the listener is running + return ""; + } + String response = sendReceive(cmd); + if (!response.startsWith(String.valueOf(ack))) { + throw new IOException("Unexpected response from WALT. Expected \"" + ack + + "\", got \"" + response + "\""); + } + return response.substring(1).trim(); + } + + String command(char cmd) throws IOException { + return command(cmd, flipCase(cmd)); + } + + private char flipCase(char c) { + if (Character.isUpperCase(c)) { + return Character.toLowerCase(c); + } else if (Character.isLowerCase(c)) { + return Character.toUpperCase(c); + } else { + return c; + } + } + + public void checkVersion() throws IOException { + if (!isConnected()) throw new IOException("Not connected to WALT"); + if (!isListenerStopped()) throw new IOException("Listener is running"); + + String s = command(CMD_VERSION); + if (!PROTOCOL_VERSION.equals(s)) { + Resources res = context.getResources(); + throw new IOException(String.format(res.getString(R.string.protocol_version_mismatch), + s, PROTOCOL_VERSION)); + } + } + + public void syncClock() throws IOException { + clock = connection.syncClock(); + } + + // Simple way of syncing clocks. Used for diagnostics. Accuracy of several ms. + public void simpleSyncClock() throws IOException { + byte[] buffer = new byte[1024]; + clock = new RemoteClockInfo(); + clock.baseTime = RemoteClockInfo.microTime(); + String reply = sendReceive(CMD_SYNC_ZERO); + logger.log("Simple sync reply: " + reply); + clock.maxLag = (int) clock.micros(); + logger.log("Synced clocks, the simple way:\n" + clock); + } + + public void checkDrift() { + if (! isConnected()) { + logger.log("ERROR: Not connected, aborting checkDrift()"); + return; + } + connection.updateLag(); + int drift = Math.abs(clock.getMeanLag()); + String msg = String.format("Remote clock delayed between %d and %d us", + clock.minLag, clock.maxLag); + // TODO: Convert the limit to user editable preference + if (drift > DEFAULT_DRIFT_LIMIT_US) { + msg = "WARNING: High clock drift. " + msg; + } + logger.log(msg); + } + + public long readLastShockTime_mock() { + return clock.micros() - 15000; + } + + public long readLastShockTime() { + String s; + try { + s = sendReceive(CMD_GSHOCK); + } catch (IOException e) { + logger.log("Error sending GSHOCK command: " + e.getMessage()); + return -1; + } + Log.i(TAG, "Received S reply: " + s); + long t = 0; + try { + t = Integer.parseInt(s.trim()); + } catch (NumberFormatException e) { + logger.log("Bad reply for shock time: " + e.getMessage()); + } + + return t; + } + + static class TriggerMessage { + public char tag; + public long t; + public int value; + public int count; + // TODO: verify the format of the message while parsing it + TriggerMessage(String s) { + String[] parts = s.trim().split("\\s+"); + tag = parts[0].charAt(0); + t = Integer.parseInt(parts[1]); + value = Integer.parseInt(parts[2]); + count = Integer.parseInt(parts[3]); + } + + static boolean isTriggerString(String s) { + return s.trim().matches("G\\s+[A-Z]\\s+\\d+\\s+\\d+.*"); + } + } + + TriggerMessage readTriggerMessage(char cmd) throws IOException { + String response = command(cmd, 'G'); + return new TriggerMessage(response); + } + + + /*********************************************************************************************** + Trigger Listener + A thread that constantly polls the interface for incoming triggers and passes them to the handler + + */ + + private TriggerListener triggerListener; + private Thread triggerListenerThread; + + abstract static class TriggerHandler { + private Handler handler; + + TriggerHandler() { + handler = new Handler(); + } + + private void go(final String s) { + handler.post(new Runnable() { + @Override + public void run() { + onReceiveRaw(s); + } + }); + } + + void onReceiveRaw(String s) { + if (TriggerMessage.isTriggerString(s)) { + TriggerMessage tmsg = new TriggerMessage(s.substring(1).trim()); + onReceive(tmsg); + } else { + Log.i(TAG, "Malformed trigger data: " + s); + } + } + + abstract void onReceive(TriggerMessage tmsg); + } + + private TriggerHandler triggerHandler; + + void setTriggerHandler(TriggerHandler triggerHandler) { + this.triggerHandler = triggerHandler; + } + + void clearTriggerHandler() { + triggerHandler = null; + } + + private class TriggerListener implements Runnable { + static final int BUFF_SIZE = 1024 * 4; + public Utils.ListenerState state = Utils.ListenerState.STOPPED; + private byte[] buffer = new byte[BUFF_SIZE]; + + @Override + public void run() { + state = Utils.ListenerState.RUNNING; + while(isRunning()) { + int ret = connection.blockingRead(buffer); + if (ret > 0 && triggerHandler != null) { + String s = new String(buffer, 0, ret); + Log.i(TAG, "Listener received data: " + s); + if (s.length() > 0) { + triggerHandler.go(s); + } + } + } + state = Utils.ListenerState.STOPPED; + } + + public synchronized boolean isRunning() { + return state == Utils.ListenerState.RUNNING; + } + + public synchronized boolean isStopped() { + return state == Utils.ListenerState.STOPPED; + } + + public synchronized void stop() { + state = Utils.ListenerState.STOPPING; + } + } + + public boolean isListenerStopped() { + return triggerListener.isStopped(); + } + + public void startListener() throws IOException { + if (!isConnected()) { + throw new IOException("Not connected to WALT"); + } + triggerListenerThread = new Thread(triggerListener); + logger.log("Starting Listener"); + triggerListener.state = Utils.ListenerState.STARTING; + triggerListenerThread.start(); + } + + public void stopListener() { + logger.log("Stopping Listener"); + triggerListener.stop(); + try { + triggerListenerThread.join(); + } catch (Exception e) { + logger.log("Error while stopping Listener: " + e.getMessage()); + } + logger.log("Listener stopped"); + } + + public void setConnectionStateListener(WaltConnection.ConnectionStateListener connectionStateListener) { + this.connectionStateListener = connectionStateListener; + if (isConnected()) { + this.connectionStateListener.onConnect(); + } + } + +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltTcpConnection.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltTcpConnection.java new file mode 100644 index 0000000..ee9c143 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltTcpConnection.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; + + +public class WaltTcpConnection implements WaltConnection { + + // The local ip on ARC++ to connect to underlying ChromeOS + private static final String SERVER_IP = "192.168.254.1"; + private static final int SERVER_PORT = 50007; + private static final int TCP_READ_TIMEOUT_MS = 200; + + private final SimpleLogger logger; + private HandlerThread networkThread; + private Handler networkHandler; + private final Object readLock = new Object(); + private boolean messageReceived = false; + private Utils.ListenerState connectionState = Utils.ListenerState.STOPPED; + private int lastRetVal; + static final int BUFF_SIZE = 1024 * 4; + private byte[] buffer = new byte[BUFF_SIZE]; + + private final Handler mainHandler = new Handler(); + private RemoteClockInfo remoteClock = new RemoteClockInfo(); + + private Socket socket; + private OutputStream outputStream = null; + private InputStream inputStream = null; + + private WaltConnection.ConnectionStateListener connectionStateListener; + + // Singleton stuff + private static WaltTcpConnection instance; + private static final Object LOCK = new Object(); + + public static WaltTcpConnection getInstance(Context context) { + synchronized (LOCK) { + if (instance == null) { + instance = new WaltTcpConnection(context.getApplicationContext()); + } + return instance; + } + } + + private WaltTcpConnection(Context context) { + logger = SimpleLogger.getInstance(context); + } + + public void connect() { + connectionState = Utils.ListenerState.STARTING; + networkThread = new HandlerThread("NetworkThread"); + networkThread.start(); + networkHandler = new Handler(networkThread.getLooper()); + logger.log("Started network thread for TCP bridge"); + networkHandler.post(new Runnable() { + @Override + public void run() { + try { + InetAddress serverAddr = InetAddress.getByName(SERVER_IP); + socket = new Socket(serverAddr, SERVER_PORT); + socket.setSoTimeout(TCP_READ_TIMEOUT_MS); + outputStream = socket.getOutputStream(); + inputStream = socket.getInputStream(); + logger.log("TCP connection established"); + connectionState = Utils.ListenerState.RUNNING; + } catch (Exception e) { + e.printStackTrace(); + logger.log("Can't connect to TCP bridge: " + e.getMessage()); + connectionState = Utils.ListenerState.STOPPED; + return; + } + + // Run the onConnect callback, but on main thread. + mainHandler.post(new Runnable() { + @Override + public void run() { + WaltTcpConnection.this.onConnect(); + } + }); + } + }); + + } + + public void onConnect() { + if (connectionStateListener != null) { + connectionStateListener.onConnect(); + } + } + + public synchronized boolean isConnected() { + return connectionState == Utils.ListenerState.RUNNING; + } + + public void sendByte(char c) throws IOException { + outputStream.write(Utils.char2byte(c)); + } + + public void sendString(String s) throws IOException { + outputStream.write(s.getBytes("UTF-8")); + } + + public synchronized int blockingRead(byte[] buff) { + + messageReceived = false; + + networkHandler.post(new Runnable() { + @Override + public void run() { + lastRetVal = -1; + try { + synchronized (readLock) { + lastRetVal = inputStream.read(buffer); + messageReceived = true; + readLock.notifyAll(); + } + } catch (SocketTimeoutException e) { + messageReceived = true; + lastRetVal = -2; + } + catch (Exception e) { + e.printStackTrace(); + messageReceived = true; + lastRetVal = -1; + // TODO: better messaging / error handling here + } + } + }); + + // TODO: make sure length is ok + // This blocks on readLock which is taken by the blocking read operation + try { + synchronized (readLock) { + while (!messageReceived) readLock.wait(TCP_READ_TIMEOUT_MS); + } + } catch (InterruptedException e) { + return -1; + } + + if (lastRetVal > 0) { + System.arraycopy(buffer, 0, buff, 0, lastRetVal); + } + + return lastRetVal; + } + + + private void updateClock(String cmd) throws IOException { + sendString(cmd); + int retval = blockingRead(buffer); + if (retval <= 0) { + throw new IOException("WaltTcpConnection, can't sync clocks"); + } + String s = new String(buffer, 0, retval); + String[] parts = s.trim().split("\\s+"); + // TODO: make sure reply starts with "clock" + long wallBaseTime = Long.parseLong(parts[1]); + remoteClock.baseTime = wallBaseTime - RemoteClockInfo.uptimeZero(); + remoteClock.minLag = Integer.parseInt(parts[2]); + remoteClock.maxLag = Integer.parseInt(parts[3]); + } + + public RemoteClockInfo syncClock() throws IOException { + updateClock("bridge sync"); + logger.log("Synced clocks via TCP bridge:\n" + remoteClock); + return remoteClock; + } + + public void updateLag() { + try { + updateClock("bridge update"); + } catch (IOException e) { + logger.log("Failed to update clock lag: " + e.getMessage()); + } + } + + public void setConnectionStateListener(ConnectionStateListener connectionStateListener) { + this.connectionStateListener = connectionStateListener; + } + + // A way to test if there is a TCP bridge to decide whether to use it. + // Some thread dancing to get around the Android strict policy for no network on main thread. + public static boolean probe() { + ProbeThread probeThread = new ProbeThread(); + probeThread.start(); + try { + probeThread.join(); + } catch (Exception e) { + e.printStackTrace(); + } + return probeThread.isReachable; + } + + private static class ProbeThread extends Thread { + public boolean isReachable = false; + private final String TAG = "ProbeThread"; + + @Override + public void run() { + Socket socket = new Socket(); + try { + InetSocketAddress remoteAddr = new InetSocketAddress(SERVER_IP, SERVER_PORT); + socket.connect(remoteAddr, 50 /* timeout in milliseconds */); + isReachable = true; + socket.close(); + } catch (Exception e) { + Log.i(TAG, "Probing TCP connection failed: " + e.getMessage()); + } + } + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltUsbConnection.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltUsbConnection.java new file mode 100644 index 0000000..f118ae2 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltUsbConnection.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt; + +import android.content.Context; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.util.Log; + +import java.io.IOException; + +/** + * A singleton used as an interface for the physical WALT device. + */ +public class WaltUsbConnection extends BaseUsbConnection implements WaltConnection { + + private static final int TEENSY_VID = 0x16c0; + // TODO: refactor to demystify PID. See BaseUsbConnection.isCompatibleUsbDevice() + private static final int TEENSY_PID = 0; + private static final int HALFKAY_PID = 0x0478; + private static final int USB_READ_TIMEOUT_MS = 200; + private static final String TAG = "WaltUsbConnection"; + + private UsbEndpoint endpointIn = null; + private UsbEndpoint endpointOut = null; + + private RemoteClockInfo remoteClock = new RemoteClockInfo(); + + private static final Object LOCK = new Object(); + + private static WaltUsbConnection instance; + + private WaltUsbConnection(Context context) { + super(context); + } + + public static WaltUsbConnection getInstance(Context context) { + synchronized (LOCK) { + if (instance == null) { + instance = new WaltUsbConnection(context.getApplicationContext()); + } + return instance; + } + } + + @Override + public int getPid() { + return TEENSY_PID; + } + + @Override + public int getVid() { + return TEENSY_VID; + } + + @Override + protected boolean isCompatibleUsbDevice(UsbDevice usbDevice) { + // Allow any Teensy, but not in HalfKay bootloader mode + // Teensy PID depends on mode (e.g: Serail + MIDI) and also changed in TeensyDuino 1.31 + return ((usbDevice.getProductId() != HALFKAY_PID) && + (usbDevice.getVendorId() == TEENSY_VID)); + } + + + // Called when WALT is physically unplugged from USB + @Override + public void onDisconnect() { + endpointIn = null; + endpointOut = null; + super.onDisconnect(); + } + + + // Called when WALT is physically plugged into USB + @Override + public void onConnect() { + // Serial mode only + // TODO: find the interface and endpoint indexes no matter what mode it is + int ifIdx = 1; + int epInIdx = 1; + int epOutIdx = 0; + + UsbInterface iface = usbDevice.getInterface(ifIdx); + + if (usbConnection.claimInterface(iface, true)) { + logger.log("Interface claimed successfully\n"); + } else { + logger.log("ERROR - can't claim interface\n"); + return; + } + + endpointIn = iface.getEndpoint(epInIdx); + endpointOut = iface.getEndpoint(epOutIdx); + + super.onConnect(); + } + + @Override + public boolean isConnected() { + return super.isConnected() && (endpointIn != null) && (endpointOut != null); + } + + + @Override + public void sendByte(char c) throws IOException { + if (!isConnected()) { + throw new IOException("Not connected to WALT"); + } + // logger.log("Sending char " + c); + usbConnection.bulkTransfer(endpointOut, Utils.char2byte(c), 1, 100); + } + + @Override + public int blockingRead(byte[] buffer) { + return usbConnection.bulkTransfer(endpointIn, buffer, buffer.length, USB_READ_TIMEOUT_MS); + } + + + @Override + public RemoteClockInfo syncClock() throws IOException { + if (!isConnected()) { + throw new IOException("Not connected to WALT"); + } + + try { + int fd = usbConnection.getFileDescriptor(); + int ep_out = endpointOut.getAddress(); + int ep_in = endpointIn.getAddress(); + + remoteClock.baseTime = syncClock(fd, ep_out, ep_in); + remoteClock.minLag = 0; + remoteClock.maxLag = getMaxE(); + } catch (Exception e) { + logger.log("Exception while syncing clocks: " + e.getStackTrace()); + } + logger.log("Synced clocks, maxE=" + remoteClock.maxLag + "us"); + Log.i(TAG, remoteClock.toString()); + return remoteClock; + } + + @Override + public void updateLag() { + if (! isConnected()) { + logger.log("ERROR: Not connected, aborting checkDrift()"); + return; + } + updateBounds(); + remoteClock.minLag = getMinE(); + remoteClock.maxLag = getMaxE(); + } + + + + // NDK / JNI stuff + // TODO: add guards to avoid calls to updateBounds and getter when listener is running. + private native long syncClock(int fd, int endpoint_out, int endpoint_in); + + private native void updateBounds(); + + private native int getMinE(); + + private native int getMaxE(); + + static { + System.loadLibrary("sync_clock_jni"); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/BootloaderConnection.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/BootloaderConnection.java new file mode 100644 index 0000000..0f2e802 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/BootloaderConnection.java @@ -0,0 +1,80 @@ +package org.chromium.latency.walt.programmer; + +import android.content.Context; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbInterface; + +import org.chromium.latency.walt.BaseUsbConnection; + +class BootloaderConnection extends BaseUsbConnection { + private static final int HALFKAY_VID = 0x16C0; + private static final int HALFKAY_PID = 0x0478; + + private static final Object LOCK = new Object(); + private static BootloaderConnection instance; + + public static BootloaderConnection getInstance(Context context) { + synchronized (LOCK) { + if (instance == null) { + instance = new BootloaderConnection(context.getApplicationContext()); + } + return instance; + } + } + + @Override + public int getPid() { + return HALFKAY_PID; + } + + @Override + public int getVid() { + return HALFKAY_VID; + } + + @Override + protected boolean isCompatibleUsbDevice(UsbDevice usbDevice) { + return ((usbDevice.getProductId() == HALFKAY_PID) && + (usbDevice.getVendorId() == HALFKAY_VID)); + } + + @Override + public void onConnect() { + int ifIdx = 0; + + UsbInterface iface = usbDevice.getInterface(ifIdx); + + if (usbConnection.claimInterface(iface, true)) { + logger.log("Interface claimed successfully\n"); + } else { + logger.log("ERROR - can't claim interface\n"); + } + + super.onConnect(); + } + + public void write(byte[] buf, int timeout) { + write(buf, 0, buf.length, timeout); + } + + public void write(byte[] buf, int index, int len, int timeout) { + if (!isConnected()) return; + + while (timeout > 0) { + // USB HID Set_Report message + int result = usbConnection.controlTransfer(0x21, 9, 0x0200, index, buf, len, timeout); + + if (result >= 0) break; + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + timeout -= 10; + } + } + + private BootloaderConnection(Context context) { + super(context); + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/DeviceConstants.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/DeviceConstants.java new file mode 100644 index 0000000..b1618c7 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/DeviceConstants.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt.programmer; + +class DeviceConstants { + static final int FIRMWARE_SIZE = 0x10000; // 64k + static final int BLOCK_SIZE = 512; // how many bytes to send at once +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/FirmwareImage.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/FirmwareImage.java new file mode 100644 index 0000000..d2feb01 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/FirmwareImage.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt.programmer; + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.ParseException; +import java.util.Arrays; + +class FirmwareImage { + private static final String TAG = "FirmwareImage"; + + private boolean atEOF = false; + private byte[] image = new byte[DeviceConstants.FIRMWARE_SIZE]; + private boolean[] mask = new boolean[DeviceConstants.FIRMWARE_SIZE]; + + boolean shouldWrite(int addr, int len) { + if (addr < 0 || addr + len > DeviceConstants.FIRMWARE_SIZE) return false; + for (int i = 0; i < len; i++) { + if (mask[addr + i]) return true; + } + return false; + } + + void getData(byte[] dest, int index, int addr, int count) { + System.arraycopy(image, addr, dest, index, count); + } + + void parseHex(InputStream stream) throws ParseException { + Arrays.fill(image, (byte) 0xFF); + Arrays.fill(mask, false); + BufferedReader in = new BufferedReader(new InputStreamReader(stream)); + try { + String line; + while ((line = in.readLine()) != null) { + parseLine(line); + } + } catch (IOException e) { + Log.e(TAG, "Reading input file: " + e); + } + + if (!atEOF) throw new ParseException("No EOF marker", -1); + Log.d(TAG, "Done parsing file"); + } + + private void parseLine(String line) throws ParseException { + if (atEOF) throw new ParseException("Line after EOF marker", -1); + int cur = 0; + final int length = line.length(); + if (length < 1 || line.charAt(cur) != ':') { + throw new ParseException("Expected ':', got '" + line.charAt(cur), cur); + } + cur++; + + int count = parseByte(line, cur); + cur += 2; + int addr = parseInt(line, cur); + cur += 4; + byte code = parseByte(line, cur); + cur += 2; + + switch (code) { + case 0x00: { + parseData(line, cur, count, image, mask, addr); + // TODO: verify checksum + break; + } + case 0x01: { + Log.d(TAG, "Got EOF marker"); + atEOF = true; + return; + } + default: { + throw new ParseException(String.format("Unknown code '%x'", code), cur); + } + } + } + + private static byte parseByte(String line, int pos) throws ParseException { + if (line.length() < pos + 2) throw new ParseException("Unexpected EOL", pos); + try { + return (byte) Integer.parseInt(line.substring(pos, pos + 2), 16); + } catch (NumberFormatException e) { + throw new ParseException("Malformed file: " + e.getMessage(), pos); + } + } + + private static int parseInt(String line, int pos) throws ParseException { + if (line.length() < pos + 4) throw new ParseException("Unexpected EOL", pos); + try { + return Integer.parseInt(line.substring(pos, pos + 4), 16); + } catch (NumberFormatException e) { + throw new ParseException("Malformed file: " + e.getMessage(), pos); + } + } + + private static void parseData(String line, int pos, int count, + byte[] dest, boolean[] mask, int addr) throws ParseException { + for (int i = 0; i < count; i++) { + try { + dest[addr + i] = parseByte(line, pos + i * 2); + mask[addr + i] = true; + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParseException(String.format("Address '%x' out of range", addr + i), + pos + i * 2); + } + } + } +} diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/Programmer.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/Programmer.java new file mode 100644 index 0000000..5deef42 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/Programmer.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.chromium.latency.walt.programmer; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; + +import org.chromium.latency.walt.R; +import org.chromium.latency.walt.SimpleLogger; +import org.chromium.latency.walt.WaltConnection; + +import java.io.InputStream; +import java.text.ParseException; +import java.util.Arrays; + +public class Programmer { + private static final String TAG = "Programmer"; + private SimpleLogger logger; + + private FirmwareImage image; + private BootloaderConnection conn; + + private Context context; + private Handler handler = new Handler(); + + public Programmer(Context context) { + this.context = context; + } + + public void program() { + logger = SimpleLogger.getInstance(context); + InputStream in = context.getResources().openRawResource(R.raw.walt); + image = new FirmwareImage(); + try { + image.parseHex(in); + } catch (ParseException e) { + Log.e(TAG, "Parsing input file: ", e); + } + + conn = BootloaderConnection.getInstance(context); + // TODO: automatically reboot into the bootloader + logger.log("\nRemember to press the button on the Teensy first\n"); + conn.setConnectionStateListener(new WaltConnection.ConnectionStateListener() { + @Override + public void onConnect() { + handler.post(programRunnable); + } + + @Override + public void onDisconnect() {} + }); + if (!conn.isConnected()) { + conn.connect(); + } + } + + private Runnable programRunnable = new Runnable() { + @Override + public void run() { + logger.log("Programming..."); + + // The logic for this is ported from + // https://github.com/PaulStoffregen/teensy_loader_cli + byte[] buf = new byte[DeviceConstants.BLOCK_SIZE + 64]; + for (int addr = 0; addr < DeviceConstants.FIRMWARE_SIZE; + addr += DeviceConstants.BLOCK_SIZE) { + if (!image.shouldWrite(addr, DeviceConstants.BLOCK_SIZE) && addr != 0) + continue; // don't need to flash this block + + buf[0] = (byte) (addr & 255); + buf[1] = (byte) ((addr >>> 8) & 255); + buf[2] = (byte) ((addr >>> 16) & 255); + Arrays.fill(buf, 3, 64, (byte) 0); + image.getData(buf, 64, addr, DeviceConstants.BLOCK_SIZE); + + conn.write(buf, (addr == 0) ? 3000 : 250); + } + + logger.log("Programming complete. Rebooting."); + + // reboot the device + buf[0] = (byte) 0xFF; + buf[1] = (byte) 0xFF; + buf[2] = (byte) 0xFF; + Arrays.fill(buf, 3, DeviceConstants.BLOCK_SIZE + 64, (byte) 0); + conn.write(buf, 250); + } + }; +} diff --git a/android/WALT/app/src/main/jni/Android.mk b/android/WALT/app/src/main/jni/Android.mk new file mode 100644 index 0000000..3307036 --- /dev/null +++ b/android/WALT/app/src/main/jni/Android.mk @@ -0,0 +1,30 @@ +# Copyright (C) 2015 The Android Open Source Project +# +# 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. +# + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := sync_clock_jni +LOCAL_SRC_FILES := sync_clock_jni.c sync_clock.c player.c + +LOCAL_CFLAGS := -g -DUSE_LIBLOG -Werror + +# needed for logcat +LOCAL_SHARED_LIBRARIES := libcutils + +LOCAL_LDLIBS := -lOpenSLES -llog + +include $(BUILD_SHARED_LIBRARY) diff --git a/android/WALT/app/src/main/jni/Application.mk b/android/WALT/app/src/main/jni/Application.mk new file mode 100644 index 0000000..7c01d06 --- /dev/null +++ b/android/WALT/app/src/main/jni/Application.mk @@ -0,0 +1,17 @@ +# Copyright (C) 2015 The Android Open Source Project +# +# 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. +# + +APP_ABI := all +APP_PLATFORM := android-9 diff --git a/android/WALT/app/src/main/jni/Makefile b/android/WALT/app/src/main/jni/Makefile new file mode 100644 index 0000000..4c54f56 --- /dev/null +++ b/android/WALT/app/src/main/jni/Makefile @@ -0,0 +1,17 @@ +# Copyright (C) 2015 The Android Open Source Project +# +# 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. +# + +all: + gcc -o ut sync_clock_linux.c sync_clock.c \ No newline at end of file diff --git a/android/WALT/app/src/main/jni/README.md b/android/WALT/app/src/main/jni/README.md new file mode 100644 index 0000000..a1316db --- /dev/null +++ b/android/WALT/app/src/main/jni/README.md @@ -0,0 +1,112 @@ +# Clock Synchronization + +How it works + +## Step 1 - rough sync + + T0 = current_time() + Tell the remote to zero clock. + Wait for confirmation from remote + maxE = current_time() - T0 + All further local time is measured from T0 + + +After this step we are sure that the remote clock lags behind the local clock by +some value E. And we know that E >= 0 because remote was zeroed *after* we +zeroed the local time (recored T0). And also E<= maxE. So 0 = minE < E < maxE. + + +## Step 2 - find better lower bound - `minE` + +Send some messages from local to remote, note the time right before sending the +message (denote it as `t_local`) and have the remote reply with his timestamps +of when it received the messages according to his clock that lags by unknown +positive value `E` behind the local clock, denote it by `t_remote`. + + + t_remote = t_local - E + travel_time + E = t_local - t_remote + travel_time > t_local - t_remote + since travel_time > 0 + E > t_local - t_remote + + set minE to max(minE, t_local - t_remote) + Repeat + +We need to first send a bunch of messages with some random small delays, and +only after that get the remote timestamps for all of them. This helps deal with +unwanted buffering and delay added by the kernel of hardware in the outgoing +direction. + +## Step 3 - find better upper bound `maxE` + +Same idea, but in the opposite direction. Remote device sends us messages and +then the timestamps according to his clock of when they were sent. We record the +local timestamps when we receive them. + + t_local = t_remote + E + travel_time + E = t_local - t_remote - travel time < t_local - t_remote + set maxE = min(maxE, t_local - t_remote) + Repeat + +## Comparison with NTP + +NTP measures the mean travel_time (latency) and assumes it to be symmetric - the +same in both directions. If the symmetry assumption is broken, there is no way +to detect this. Both, systematic asymmetry in latency and clock difference would +result in exactly the same observations - +[explanation here](http://cs.stackexchange.com/questions/103/clock-synchronization-in-a-network-with-asymmetric-delays). + +In our case the latency can be relatively small compared to network, but is +likely to be asymmetric due to the asymmetric nature of USB. The algorithm +described above guarantees that the clock difference is within the measured +bounds `minE < E < maxE`, though the resulting interval `deltaE = maxE - minE` +can be fairly large compared to synchronization accuracy of NTP on a network +with good latency symmetry. + +Observed values for `deltaE` + - Linux desktop machine (HP Z420), USB2 port: ~100us + - Same Linux machine, USB3 port: ~800us + - Nexus 5 ~100us + - Nexus 7 (both the old and the newer model) ~300us + - Samsung Galaxy S3 ~150us + + + +## Implementation notes + +General + - All times in this C code are recored in microseconds, unless otherwise + specified. + - The timestamped messages are all single byte. + +USB communication + - USB hierarchy recap: USB device has several interfaces. Each interface has + several endpoints. Endpoints are directional IN = data going into the host, + OUT = data going out of the host. To get data from the device via an IN + endpoint, we must query it. + - There are two types of endpoints - BULK and INTERRUPT. For our case it's not + really important. Currently all the comms are done via a BULK interface + exposed when you compile Teensy code in "Serial". + - All the comms are done using the Linux API declared in linux/usbdevice_fs.h + - The C code can be compiled for both Android JNI and Linux. + - The C code is partially based on the code of libusbhost from the Android OS + core code, but does not use that library because it's an overkill for our + needs. + +## There are two ways of communicating via usbdevice_fs + + // Async way + ioctl(fd, USBDEVFS_SUBMITURB, urb); + // followed by + ioctl(fd, USBDEVFS_REAPURB, &urb); // Blocks until there is a URB to read. + + // Sync way + struct usbdevfs_bulktransfer ctrl; + ctrl.ep = endpoint; + ctrl.len = length; + ctrl.data = buffer; + ctrl.timeout = timeout; // [milliseconds] Will timeout if there is nothing to read + int ret = ioctl(fd, USBDEVFS_BULK, &ctrl); + + + diff --git a/android/WALT/app/src/main/jni/findteensy.py b/android/WALT/app/src/main/jni/findteensy.py new file mode 100755 index 0000000..820bc14 --- /dev/null +++ b/android/WALT/app/src/main/jni/findteensy.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +# Copyright (C) 2015 The Android Open Source Project +# +# 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. +# + +""" +Find and print the device path for TeensyUSB + +Usage: + sudo ./ut `./findteensy.py` +""" + +import subprocess +line = subprocess.check_output("lsusb | grep eensy", shell=True) +parts = line.split() +bus = parts[1] +dev = parts[3].strip(':') +print "/dev/bus/usb/%s/%s" % (bus, dev) \ No newline at end of file diff --git a/android/WALT/app/src/main/jni/player.c b/android/WALT/app/src/main/jni/player.c new file mode 100644 index 0000000..361d0a8 --- /dev/null +++ b/android/WALT/app/src/main/jni/player.c @@ -0,0 +1,520 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * 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. + */ + +#include +#include +#include +#include +#include +#include + +// for native audio +#include +#include +#include + +#include "sync_clock.h" + +// logging +#define APPNAME "WALT" + +// engine interfaces +static SLObjectItf engineObject = NULL; +static SLEngineItf engineEngine = NULL; + +// output mix interfaces +static SLObjectItf outputMixObject = NULL; + +// buffer queue player interfaces +static SLObjectItf bqPlayerObject = NULL; +static SLPlayItf bqPlayerPlay = NULL; +static SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue = NULL; + +// recorder interfaces +static SLObjectItf recorderObject = NULL; +static SLRecordItf recorderRecord; +static SLAndroidSimpleBufferQueueItf recorderBufferQueue; +static volatile int bqPlayerRecorderBusy = 0; + +static unsigned int recorder_frames; +static short* recorderBuffer; +static unsigned recorderSize = 0; + +static unsigned int framesPerBuffer; + +#define CHANNELS 1 // 1 for mono, 2 for stereo + +// Each short represents a 16-bit audio sample +static short* beepBuffer = NULL; +static short* silenceBuffer = NULL; +static unsigned int bufferSizeInBytes = 0; + +#define MAXIMUM_AMPLITUDE_VALUE 32767 + +// how many times to play the wave table (so we can actually hear it) +#define BUFFERS_TO_PLAY 10 + +static unsigned buffersRemaining = 0; +static short warmedUp = 0; + + +// Timestamps +// te - enqueue time +// tc - callback time +int64_t te_play = 0, te_rec = 0, tc_rec = 0; + +/** + * Create wave tables for audio out. + */ +void createWaveTables(){ + bufferSizeInBytes = framesPerBuffer * sizeof(*beepBuffer); + silenceBuffer = malloc(bufferSizeInBytes); + beepBuffer = malloc(bufferSizeInBytes); + + + __android_log_print(ANDROID_LOG_VERBOSE, + APPNAME, + "Creating wave tables, 1 channel. Frames: %i Buffer size (bytes): %i", + framesPerBuffer, + bufferSizeInBytes); + + unsigned int i; + for (i = 0; i < framesPerBuffer; i++) { + silenceBuffer[i] = 0; + beepBuffer[i] = (i & 2 - 1) * MAXIMUM_AMPLITUDE_VALUE; + // This fills a buffer that looks like [min, min, max, max, min, min...] + // which is a square wave at 1/4 frequency of the sampling rate + // for 48kHz sampling this is 12kHz pitch, still well audible. + } +} + +// this callback handler is called every time a buffer finishes playing +void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) +{ + if (bq == NULL) { + __android_log_print(ANDROID_LOG_ERROR, APPNAME, "buffer queue is null"); + } + assert(bq == bqPlayerBufferQueue); + assert(NULL == context); + + if (buffersRemaining > 0) { // continue playing tone + if(buffersRemaining == BUFFERS_TO_PLAY && warmedUp) { + // Enqueue the first non-silent buffer, save the timestamp + // For cold test Enqueue happens in playTone rather than here. + te_play = uptimeMicros(); + } + buffersRemaining--; + + SLresult result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, beepBuffer, + bufferSizeInBytes); + (void)result; + assert(SL_RESULT_SUCCESS == result); + } else if (warmedUp) { // stop tone but keep playing silence + SLresult result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, silenceBuffer, + bufferSizeInBytes); + assert(SL_RESULT_SUCCESS == result); + (void) result; + } else { // stop playing completely + SLresult result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + __android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Done playing tone"); + } +} + +jlong Java_org_chromium_latency_walt_AudioTest_playTone(JNIEnv* env, jclass clazz){ + + int64_t t_start = uptimeMicros(); + te_play = 0; + + SLresult result; + + if (!warmedUp) { + result = (*bqPlayerBufferQueue)->Clear(bqPlayerBufferQueue); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // Enqueue first buffer + te_play = uptimeMicros(); + result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, beepBuffer, + bufferSizeInBytes); + assert(SL_RESULT_SUCCESS == result); + (void) result; + + result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING); + assert(SL_RESULT_SUCCESS == result); + (void) result; + + int dt_state = uptimeMicros() - t_start; + __android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "playTone() changed state to playing dt=%d us", dt_state); + // TODO: this block takes lots of time (~13ms on Nexus 7) research this and decide how to measure. + } + + __android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Playing tone"); + buffersRemaining = BUFFERS_TO_PLAY; + + return (jlong) t_start; +} + + +// create the engine and output mix objects +void Java_org_chromium_latency_walt_AudioTest_createEngine(JNIEnv* env, jclass clazz) +{ + __android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Creating audio engine"); + + SLresult result; + + // create engine + result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // realize the engine + result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // get the engine interface, which is needed in order to create other objects + result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // create output mix, + result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // realize the output mix + result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE); + assert(SL_RESULT_SUCCESS == result); + (void)result; +} + +void Java_org_chromium_latency_walt_AudioTest_destroyEngine(JNIEnv *env, jclass clazz) +{ + if (bqPlayerObject != NULL) { + (*bqPlayerObject)->Destroy(bqPlayerObject); + bqPlayerObject = NULL; + } + + if (outputMixObject != NULL) { + (*outputMixObject)->Destroy(outputMixObject); + outputMixObject = NULL; + } + + if (engineObject != NULL) { + (*engineObject)->Destroy(engineObject); + engineObject = NULL; + } +} + +// create buffer queue audio player +void Java_org_chromium_latency_walt_AudioTest_createBufferQueueAudioPlayer(JNIEnv* env, + jclass clazz, jint optimalFrameRate, jint optimalFramesPerBuffer) +{ + __android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Creating audio player with frame rate %d and frames per buffer %d", + optimalFrameRate, optimalFramesPerBuffer); + + framesPerBuffer = optimalFramesPerBuffer; + createWaveTables(); + + SLresult result; + + // configure the audio source (supply data through a buffer queue in PCM format) + SLDataLocator_AndroidSimpleBufferQueue locator_bufferqueue_source; + SLDataFormat_PCM format_pcm; + SLDataSource audio_source; + + // source location + locator_bufferqueue_source.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE; + locator_bufferqueue_source.numBuffers = 1; + + // source format + format_pcm.formatType = SL_DATAFORMAT_PCM; + format_pcm.numChannels = 1; + + // Note: this shouldn't be called samplesPerSec it should be called *framesPerSec* + // because when channels = 2 then there are 2 samples per frame. + format_pcm.samplesPerSec = (SLuint32) optimalFrameRate * 1000; + format_pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; + format_pcm.containerSize = 16; + format_pcm.channelMask = SL_SPEAKER_FRONT_CENTER; + format_pcm.endianness = SL_BYTEORDER_LITTLEENDIAN; + + audio_source.pLocator = &locator_bufferqueue_source; + audio_source.pFormat = &format_pcm; + + // configure the output: An output mix sink + SLDataLocator_OutputMix locator_output_mix; + SLDataSink audio_sink; + + locator_output_mix.locatorType = SL_DATALOCATOR_OUTPUTMIX; + locator_output_mix.outputMix = outputMixObject; + + audio_sink.pLocator = &locator_output_mix; + audio_sink.pFormat = NULL; + + // create audio player + // Note: Adding other output interfaces here will result in your audio being routed using the + // normal path NOT the fast path + const SLInterfaceID interface_ids[2] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME }; + const SLboolean interfaces_required[2] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE }; + + result = (*engineEngine)->CreateAudioPlayer( + engineEngine, + &bqPlayerObject, + &audio_source, + &audio_sink, + 2, // Number of interfaces + interface_ids, + interfaces_required + ); + + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // realize the player + result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // get the play interface + result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // get the buffer queue interface + result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, + &bqPlayerBufferQueue); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // register callback on the buffer queue + result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, NULL); + assert(SL_RESULT_SUCCESS == result); + (void)result; +} + +void Java_org_chromium_latency_walt_AudioTest_startWarmTest(JNIEnv* env, jclass clazz) { + SLresult result; + + result = (*bqPlayerBufferQueue)->Clear(bqPlayerBufferQueue); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // enqueue some silence + result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, silenceBuffer, bufferSizeInBytes); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // set the player's state to playing + result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + warmedUp = 1; +} + +void Java_org_chromium_latency_walt_AudioTest_stopTests(JNIEnv *env, jclass clazz) { + SLresult result; + + result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + warmedUp = 0; +} + +// this callback handler is called every time a buffer finishes recording +void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context) +{ + tc_rec = uptimeMicros(); + assert(bq == recorderBufferQueue); + assert(NULL == context); + + // for streaming recording, here we would call Enqueue to give recorder the next buffer to fill + // but instead, this is a one-time buffer so we stop recording + SLresult result; + result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED); + if (SL_RESULT_SUCCESS == result) { + recorderSize = recorder_frames * sizeof(short); + } + bqPlayerRecorderBusy = 0; + + //// TODO: Use small buffers and re-enqueue each time + // result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recorderBuffer, + // recorder_frames * sizeof(short)); + // assert(SL_RESULT_SUCCESS == result); +} + +// create audio recorder +jboolean Java_org_chromium_latency_walt_AudioTest_createAudioRecorder(JNIEnv* env, + jclass clazz, jint optimalFrameRate, jint framesToRecord) +{ + SLresult result; + + __android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Creating audio recorder with frame rate %d and frames to record %d", + optimalFrameRate, framesToRecord); + // Allocate buffer + recorder_frames = framesToRecord; + recorderBuffer = malloc(sizeof(*recorderBuffer) * recorder_frames); + + // configure audio source + SLDataLocator_IODevice loc_dev = { + SL_DATALOCATOR_IODEVICE, + SL_IODEVICE_AUDIOINPUT, + SL_DEFAULTDEVICEID_AUDIOINPUT, + NULL + }; + SLDataSource audioSrc = {&loc_dev, NULL}; + + // configure audio sink + SLDataLocator_AndroidSimpleBufferQueue loc_bq; + loc_bq.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE; + loc_bq.numBuffers = 2; + + + // source format + SLDataFormat_PCM format_pcm; + format_pcm.formatType = SL_DATAFORMAT_PCM; + format_pcm.numChannels = CHANNELS; + // Note: this shouldn't be called samplesPerSec it should be called *framesPerSec* + // because when channels = 2 then there are 2 samples per frame. + format_pcm.samplesPerSec = (SLuint32) optimalFrameRate * 1000; + format_pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; + format_pcm.containerSize = 16; + format_pcm.channelMask = SL_SPEAKER_FRONT_CENTER; + format_pcm.endianness = SL_BYTEORDER_LITTLEENDIAN; + + + SLDataSink audioSnk = {&loc_bq, &format_pcm}; + + // create audio recorder + // (requires the RECORD_AUDIO permission) + const SLInterfaceID id[2] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + SL_IID_ANDROIDCONFIGURATION }; + const SLboolean req[2] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; + result = (*engineEngine)->CreateAudioRecorder(engineEngine, + &recorderObject, + &audioSrc, + &audioSnk, + sizeof(id)/sizeof(id[0]), + id, req); + + // Configure the voice recognition preset which has no + // signal processing for lower latency. + SLAndroidConfigurationItf inputConfig; + result = (*recorderObject)->GetInterface(recorderObject, + SL_IID_ANDROIDCONFIGURATION, + &inputConfig); + if (SL_RESULT_SUCCESS == result) { + SLuint32 presetValue = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION; + (*inputConfig)->SetConfiguration(inputConfig, + SL_ANDROID_KEY_RECORDING_PRESET, + &presetValue, + sizeof(SLuint32)); + } + + // realize the audio recorder + result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + return JNI_FALSE; + } + + // get the record interface + result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // get the buffer queue interface + result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + &recorderBufferQueue); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // register callback on the buffer queue + result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, + NULL); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + __android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Audio recorder created, buffer size: %d frames", + recorder_frames); + + return JNI_TRUE; +} + + +// set the recording state for the audio recorder +void Java_org_chromium_latency_walt_AudioTest_startRecording(JNIEnv* env, jclass clazz) +{ + SLresult result; + + if( bqPlayerRecorderBusy) { + return; + } + // in case already recording, stop recording and clear buffer queue + result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED); + assert(SL_RESULT_SUCCESS == result); + (void)result; + result = (*recorderBufferQueue)->Clear(recorderBufferQueue); + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // the buffer is not valid for playback yet + recorderSize = 0; + + // enqueue an empty buffer to be filled by the recorder + // (for streaming recording, we would enqueue at least 2 empty buffers to start things off) + te_rec = uptimeMicros(); // TODO: investigate if it's better to time after SetRecordState + tc_rec = 0; + result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recorderBuffer, + recorder_frames * sizeof(short)); + // the most likely other result is SL_RESULT_BUFFER_INSUFFICIENT, + // which for this code example would indicate a programming error + assert(SL_RESULT_SUCCESS == result); + (void)result; + + // start recording + result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING); + assert(SL_RESULT_SUCCESS == result); + (void)result; + bqPlayerRecorderBusy = 1; +} + +jshortArray Java_org_chromium_latency_walt_AudioTest_getRecordedWave(JNIEnv *env, jclass cls) +{ + jshortArray result; + result = (*env)->NewShortArray(env, recorder_frames); + if (result == NULL) { + return NULL; /* out of memory error thrown */ + } + (*env)->SetShortArrayRegion(env, result, 0, recorder_frames, recorderBuffer); + return result; +} + +jlong Java_org_chromium_latency_walt_AudioTest_getTcRec(JNIEnv *env, jclass cls) { + return (jlong) tc_rec; +} + +jlong Java_org_chromium_latency_walt_AudioTest_getTeRec(JNIEnv *env, jclass cls) { + return (jlong) te_rec; +} + +jlong Java_org_chromium_latency_walt_AudioTest_getTePlay(JNIEnv *env, jclass cls) { + return (jlong) te_play; +} diff --git a/android/WALT/app/src/main/jni/sync_clock.c b/android/WALT/app/src/main/jni/sync_clock.c new file mode 100644 index 0000000..1591f88 --- /dev/null +++ b/android/WALT/app/src/main/jni/sync_clock.c @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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. + */ + +#include "sync_clock.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#ifdef __ANDROID__ + #include + #define LOGD(...) __android_log_print(ANDROID_LOG_VERBOSE, "ClockSyncNative", __VA_ARGS__) +#else + #define LOGD(...) printf(__VA_ARGS__) +#endif + + +// How many times to repeat the 1..9 digit sequence it's a tradeoff between +// precision and how long it takes. +// TODO: investigate better combination of constants for repeats and wait times +const int kSyncRepeats = 7; +const int kMillion = 1000000; + + +/** +uptimeMicros() - returns microseconds elapsed since boot. +Same time as Android's SystemClock.uptimeMillis() but in microseconds. + +Adapted from Android: +platform/system/core/libutils/Timers.cpp +platform/system/core/include/utils/Timers.h + +See: +http://developer.android.com/reference/android/os/SystemClock.html +https://android.googlesource.com/platform/system/core/+/master/libutils/Timers.cpp +*/ +int64_t uptimeMicros() { + struct timespec ts = {0}; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ((int64_t)ts.tv_sec) * kMillion + ts.tv_nsec / 1000; +} + + +// Sleeps us microseconds +int microsleep(int us) { + struct timespec ts = {0}; + ts.tv_sec = us / kMillion; + us %= kMillion; + ts.tv_nsec = us*1000; + nanosleep(&ts, NULL); +} + + +// *********************** Generic USB functions ******************************* + +static int send_char_async(int fd, int endpoint, char msg, char * label) { + // TODO: Do we really need a buffer longer than 1 char here? + char buffer[256] = {0}; + buffer[0] = msg; + int length = 1; + + // TODO: free() the memory used for URBs. + // Circular buffer of URBs? Cleanup at the end of clock sync? + // Several may be used simultaneously, no signal when done. + struct usbdevfs_urb *urb = calloc(1, sizeof(struct usbdevfs_urb)); + memset(urb, 0, sizeof(struct usbdevfs_urb)); + + int res; + urb->status = -1; + urb->buffer = buffer; + urb->buffer_length = length; + urb->endpoint = endpoint; + urb->type = USBDEVFS_URB_TYPE_BULK; + urb->usercontext = label; // This is hackish + do { + res = ioctl(fd, USBDEVFS_SUBMITURB, urb); + } while((res < 0) && (errno == EINTR)); + return res; +} + + +// Send or read using USBDEVFS_BULK. Allows to set a timeout. +static int bulk_talk(int fd, int endpoint, char * buffer, int length) { + // Set some reasonable timeout. 20ms is plenty time for most transfers but + // short enough to fail quickly if all transfers and retries fail with + // timeout. + const int kTimeoutMs = 20; + struct usbdevfs_bulktransfer ctrl = {0}; + // TODO: need to limit request size to avoid EINVAL + + ctrl.ep = endpoint; + ctrl.len = length; + ctrl.data = buffer; + ctrl.timeout = kTimeoutMs; + int ret = ioctl(fd, USBDEVFS_BULK, &ctrl); + return ret; +} + + +/******************************************************************************* +* Clock sync specific stuff below. +* Most data is stored in the clock_connection struct variable. +*/ + +// Send a single character to the remote in a blocking mode +int send_cmd(struct clock_connection *clk, char cmd) { + return bulk_talk(clk->fd, clk->endpoint_out, &cmd, 1); +} + +// Schedule a single character to be sent to the remote - async. +int send_async(struct clock_connection *clk, char cmd) { + return send_char_async(clk->fd, clk->endpoint_out, cmd, NULL); +} + + +int bulk_read(struct clock_connection *clk) { + memset(clk->buffer, 0, sizeof(clk->buffer)); + int ret = bulk_talk(clk->fd, clk->endpoint_in, clk->buffer, sizeof(clk->buffer)); + return ret; +} + +// microseconds elapsed since clk->t_base +int micros(struct clock_connection *clk) { + return uptimeMicros() - clk->t_base; +} + +// Clear all incoming data that's already waiting somewhere in kernel buffers +// and discard it. +void flush_incoming(struct clock_connection *clk) { + // When bulk_read times out errno = ETIMEDOUT=110, retval =-1 + // should we check for this? + + while(bulk_read(clk) >= 0) { + // TODO: fail nicely if waiting too long to avoid hangs + } +} + +// Ask the remote to send its timestamps +// for the digits previously sent to it. +void read_remote_timestamps(struct clock_connection *clk, int * times_remote) { + int i; + int t_remote; + // Go over the digits [1, 2 ... 9] + for (i = 0; i < 9; i++) { + char digit = i + '1'; + send_cmd(clk, CMD_SYNC_READOUT); + bulk_read(clk); + if (clk->buffer[0] != digit) { + LOGD("Error, bad reply for R%d: %s", i+1, clk->buffer); + } + // The reply string looks like digit + space + timestamp + // Offset by 2 to ignore the digit and the space + t_remote = atoi(clk->buffer + 2); + times_remote[i] = t_remote; + } +} + + +// Preliminary rough sync with a single message - CMD_SYNC_ZERO = 'Z'. +// This is not strictly necessary but greatly simplifies debugging +// by removing the need to look at very long numbers. +void zero_remote(struct clock_connection *clk) { + flush_incoming(clk); + clk->t_base = uptimeMicros(); + send_cmd(clk, CMD_SYNC_ZERO); + bulk_read(clk); // TODO, make sure we got 'z' + clk->maxE = micros(clk); + clk->minE = 0; + + LOGD("Sent a 'Z', reply '%c' in %d us\n", clk->buffer[0], clk->maxE); +} + + + +void improve_minE(struct clock_connection *clk) { + int times_local_sent[9] = {0}; + int times_remote_received[9] = {0}; + + // Set sleep time as 1/kSleepTimeDivider of the current bounds interval, + // but never less or more than k(Min/Max)SleepUs. All pretty random + // numbers that could use some tuning and may behave differently on + // different devices. + const int kMaxSleepUs = 700; + const int kMinSleepUs = 70; + const int kSleepTimeDivider = 10; + int minE = clk->minE; + int sleep_time = (clk->maxE - minE) / kSleepTimeDivider; + if(sleep_time > kMaxSleepUs) sleep_time = kMaxSleepUs; + if(sleep_time < kMinSleepUs) sleep_time = kMinSleepUs; + + flush_incoming(clk); + // Send digits to remote side + int i; + for (i = 0; i < 9; i++) { + char c = i + '1'; + times_local_sent[i] = micros(clk); + send_async(clk, c); + microsleep(sleep_time); + } + + // Read out receive times from the other side + read_remote_timestamps(clk, times_remote_received); + + // Do stats + for (i = 0; i < 9; i++) { + int tls = times_local_sent[i]; + int trr = times_remote_received[i]; + + int dt; + + // Look at outgoing digits + dt = tls - trr; + if (tls != 0 && trr != 0 && dt > minE) { + minE = dt; + } + + } + + clk->minE = minE; + + LOGD("E is between %d and %d us, sleep_time=%d\n", clk->minE, clk->maxE, sleep_time); +} + +void improve_maxE(struct clock_connection *clk) { + int times_remote_sent[9] = {0}; + int times_local_received[9] = {0}; + + // Tell the remote to send us digits with delays + // TODO: try tuning / configuring the delay time on remote side + send_async(clk, CMD_SYNC_SEND); + + // Read and timestamp the incoming digits, they may arrive out of order. + // TODO: Try he same with USBDEVFS_REAPURB, it might be faster + int i; + for (i = 0; i < 9; ++i) { + int retval = bulk_read(clk); + // TODO: deal with retval = (bytes returned) > 1. shouldn't happen. + // Can it happen on some devices? + int t_local = micros(clk); + int digit = atoi(clk->buffer); + if (digit <=0 || digit > 9) { + LOGD("Error, bad incoming digit: %s\n", clk->buffer); + } + times_local_received[digit-1] = t_local; + } + + // Flush whatever came after the digits. As of this writing, it's usually + // a single linefeed character. + flush_incoming(clk); + // Read out the remote timestamps of when the digits were sent + read_remote_timestamps(clk, times_remote_sent); + + // Do stats + int maxE = clk->maxE; + for (i = 0; i < 9; i++) { + int trs = times_remote_sent[i]; + int tlr = times_local_received[i]; + int dt = tlr - trs; + if (tlr != 0 && trs != 0 && dt < maxE) { + maxE = dt; + } + } + + clk->maxE = maxE; + + LOGD("E is between %d and %d us\n", clk->minE, clk->maxE); +} + + +void improve_bounds(struct clock_connection *clk) { + improve_minE(clk); + improve_maxE(clk); +} + +// get minE and maxE again after some time to check for clock drift +void update_bounds(struct clock_connection *clk) { + // Reset the bounds to some unrealistically large numbers + int i; + clk->minE = -1e7; + clk->maxE = 1e7; + // Talk to remote to get bounds on minE and maxE + for (i=0; i < kSyncRepeats; i++) { + improve_bounds(clk); + } +} + +void sync_clocks(struct clock_connection *clk) { + // Send CMD_SYNC_ZERO to remote for rough initial sync + zero_remote(clk); + + int rep; + for (rep=0; rep < kSyncRepeats; rep++) { + improve_bounds(clk); + } + + // Shift the base time to set minE = 0 + clk->t_base += clk->minE; + clk->maxE -= clk->minE; + clk->minE = 0; + LOGD("Base time shifted for zero minE\n"); +} + + diff --git a/android/WALT/app/src/main/jni/sync_clock.h b/android/WALT/app/src/main/jni/sync_clock.h new file mode 100644 index 0000000..862bb7d --- /dev/null +++ b/android/WALT/app/src/main/jni/sync_clock.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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. + */ + +#include + +#define CLOCK_BUFFER_LENGTH 512 + +// Commands, original definitions in TensyUSB side code. +#define CMD_RESET 'F' // Reset all vars +#define CMD_SYNC_SEND 'I' // Send some digits for clock sync +#define CMD_SYNC_READOUT 'R' // Read out sync times +#define CMD_SYNC_ZERO 'Z' // Initial zero + + +struct clock_connection { + int fd; + int endpoint_in; + int endpoint_out; + int64_t t_base; + char buffer[CLOCK_BUFFER_LENGTH]; + int minE; + int maxE; +}; + + +// Returns microseconds elapsed since boot +int64_t uptimeMicros(); + +// Returns microseconds elapsed since last clock sync +int micros(struct clock_connection *clk); + +// Runs clock synchronization logic +void sync_clocks(struct clock_connection *clk); + +// Run the sync logic without changing clocks, used for estimating clock drift +void update_bounds(struct clock_connection *clk); + diff --git a/android/WALT/app/src/main/jni/sync_clock_jni.c b/android/WALT/app/src/main/jni/sync_clock_jni.c new file mode 100644 index 0000000..15adfd5 --- /dev/null +++ b/android/WALT/app/src/main/jni/sync_clock_jni.c @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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. + */ + +#include "sync_clock.h" + +#include +#include + + +#define APPNAME "ClockSyncJNI" + +// This is global so that we don't have to pass it aroundbetween Java and here. +// TODO: come up with some more elegant solution. +struct clock_connection clk; + +jlong +Java_org_chromium_latency_walt_WaltUsbConnection_syncClock__III( + JNIEnv* env, + jobject thiz, + jint fd, + jint endpoint_out, + jint endpoint_in +){ + clk.fd = (int)fd; + clk.endpoint_in = (int)endpoint_in; + clk.endpoint_out = (int)endpoint_out; + clk.t_base = 0; + sync_clocks(&clk); + // __android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Returned from sync_clocks\n"); + + int64_t t_base = clk.t_base; + return (jlong) t_base; +} + +void +Java_org_chromium_latency_walt_WaltUsbConnection_updateBounds() { + update_bounds(&clk); +} + +jint +Java_org_chromium_latency_walt_WaltUsbConnection_getMinE() { + return clk.minE; +} + + +jint +Java_org_chromium_latency_walt_WaltUsbConnection_getMaxE() { + return clk.maxE; +} diff --git a/android/WALT/app/src/main/jni/sync_clock_linux.c b/android/WALT/app/src/main/jni/sync_clock_linux.c new file mode 100644 index 0000000..1287b76 --- /dev/null +++ b/android/WALT/app/src/main/jni/sync_clock_linux.c @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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. + */ + +#include "sync_clock.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +int main(int argc, char ** argv) { + if(argc < 2) { + printf("Usage %s \n" + "Try `lsusb | grep eensy` and use /dev/bus/usb//\n", + argv[0]); + return 1; + } + + printf("Opening %s\n", argv[1]); + int fd = open(argv[1], O_RDWR); + printf("open() fd=%d, errno=%d, %s\n", fd, errno, strerror(errno)); + + // The interface and endpoint numbers are defined by the TeensyUSB. They may + // be different depending on the mode (Serial vs HID) the Teensy code is + // compiled in. A real app would employ some discovery logic here. To list + // the interfaces and endpoints use `lsusb --verbose` or an app like USB + // Host Viewer on Android. Look for a "CDC Data" interface (class 0x0a). + int interface = 1; + int ep_out = 0x03; + int ep_in = 0x84; + + int ret = ioctl(fd, USBDEVFS_CLAIMINTERFACE, &interface); + printf("Interface claimed retval=%d, errno=%d, %s\n", ret, errno, strerror(errno)); + if (errno == EBUSY) { + printf("You may need to run 'sudo rmmod cdc_acm' to release the " + "interface claimed by the kernel serial driver."); + return 1; + } + + struct clock_connection clk; + clk.fd = fd; + clk.endpoint_in = ep_in; + clk.endpoint_out = ep_out; + + sync_clocks(&clk); + + printf("===========================\n" + "sync_clocks base_t=%lld, minE=%d, maxE=%d\n", + (long long int)clk.t_base, clk.minE, clk.maxE); + + // Check for clock drift. Try sleeping here to let it actually drift away. + update_bounds(&clk); + + printf("*** UPDATE ****************\n" + "Update_bounds base_t=%lld, minE=%d, maxE=%d\n", + (long long int)(clk.t_base), clk.minE, clk.maxE + ); + + + close(fd); + return 0; +} \ No newline at end of file diff --git a/android/WALT/app/src/main/res/color/button_tint.xml b/android/WALT/app/src/main/res/color/button_tint.xml new file mode 100644 index 0000000..ec76860 --- /dev/null +++ b/android/WALT/app/src/main/res/color/button_tint.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/WALT/app/src/main/res/drawable/border.xml b/android/WALT/app/src/main/res/drawable/border.xml new file mode 100644 index 0000000..5a318d9 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/border.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_brightness_7_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_brightness_7_black_24dp.xml new file mode 100644 index 0000000..e504de1 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_brightness_7_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_brightness_medium_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_brightness_medium_black_24dp.xml new file mode 100644 index 0000000..2f9cd1e --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_brightness_medium_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_check_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_check_black_24dp.xml new file mode 100644 index 0000000..3c728c5 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_check_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml new file mode 100644 index 0000000..e6bb3ca --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_equalizer_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_equalizer_black_24dp.xml new file mode 100644 index 0000000..6322102 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_equalizer_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_file_upload_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_file_upload_black_24dp.xml new file mode 100644 index 0000000..0f6fe19 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_file_upload_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_help_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_help_black_24dp.xml new file mode 100644 index 0000000..a3936eb --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_help_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_help_outline_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_help_outline_black_24dp.xml new file mode 100644 index 0000000..3e5f481 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_help_outline_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_import_export_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_import_export_black_24dp.xml new file mode 100644 index 0000000..cac4f1f --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_import_export_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_input_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_input_black_24dp.xml new file mode 100644 index 0000000..e418310 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_input_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/android/WALT/app/src/main/res/drawable/ic_mic_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_mic_black_24dp.xml new file mode 100644 index 0000000..15356bb --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_mic_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_music_note_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_music_note_black_24dp.xml new file mode 100644 index 0000000..2e5b3cc --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_music_note_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/android/WALT/app/src/main/res/drawable/ic_output_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_output_black_24dp.xml new file mode 100644 index 0000000..c2fd495 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_output_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/android/WALT/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml new file mode 100644 index 0000000..bf9b895 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_radio_button_checked_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_radio_button_checked_black_24dp.xml new file mode 100644 index 0000000..657397f --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_radio_button_checked_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_receipt_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_receipt_black_24dp.xml new file mode 100644 index 0000000..ab17085 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_receipt_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_refresh_black_24dp.xml new file mode 100644 index 0000000..8229a9a --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_refresh_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_schedule_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_schedule_black_24dp.xml new file mode 100644 index 0000000..e020fde --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_schedule_black_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_search_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_search_black_24dp.xml new file mode 100644 index 0000000..3180fac --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_search_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_settings_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_settings_black_24dp.xml new file mode 100644 index 0000000..ace746c --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_settings_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_share_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_share_black_24dp.xml new file mode 100644 index 0000000..e3fe874 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_share_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_stop_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_stop_black_24dp.xml new file mode 100644 index 0000000..c428d72 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_stop_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_swap_horiz_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_swap_horiz_black_24dp.xml new file mode 100644 index 0000000..d571dc3 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_swap_horiz_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_swap_vert_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_swap_vert_black_24dp.xml new file mode 100644 index 0000000..1c9a30b --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_swap_vert_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_system_update_alt_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_system_update_alt_black_24dp.xml new file mode 100644 index 0000000..ec96a71 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_system_update_alt_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_timelapse_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_timelapse_black_24dp.xml new file mode 100644 index 0000000..887aeaf --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_timelapse_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_usb_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_usb_black_24dp.xml new file mode 100644 index 0000000..2e7e0dc --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_usb_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/drawable/ic_volume_up_black_24dp.xml b/android/WALT/app/src/main/res/drawable/ic_volume_up_black_24dp.xml new file mode 100644 index 0000000..755e467 --- /dev/null +++ b/android/WALT/app/src/main/res/drawable/ic_volume_up_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/WALT/app/src/main/res/layout/activity_crash_log.xml b/android/WALT/app/src/main/res/layout/activity_crash_log.xml new file mode 100644 index 0000000..0f34aa5 --- /dev/null +++ b/android/WALT/app/src/main/res/layout/activity_crash_log.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/android/WALT/app/src/main/res/layout/activity_main.xml b/android/WALT/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..57e7679 --- /dev/null +++ b/android/WALT/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/android/WALT/app/src/main/res/layout/dialog_upload.xml b/android/WALT/app/src/main/res/layout/dialog_upload.xml new file mode 100644 index 0000000..be990a2 --- /dev/null +++ b/android/WALT/app/src/main/res/layout/dialog_upload.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/android/WALT/app/src/main/res/layout/fragment_about.xml b/android/WALT/app/src/main/res/layout/fragment_about.xml new file mode 100644 index 0000000..8baa80e --- /dev/null +++ b/android/WALT/app/src/main/res/layout/fragment_about.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + diff --git a/android/WALT/app/src/main/res/layout/fragment_audio.xml b/android/WALT/app/src/main/res/layout/fragment_audio.xml new file mode 100644 index 0000000..e11f157 --- /dev/null +++ b/android/WALT/app/src/main/res/layout/fragment_audio.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/WALT/app/src/main/res/layout/fragment_auto_run.xml b/android/WALT/app/src/main/res/layout/fragment_auto_run.xml new file mode 100644 index 0000000..7e8ca4a --- /dev/null +++ b/android/WALT/app/src/main/res/layout/fragment_auto_run.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/android/WALT/app/src/main/res/layout/fragment_diagnostics.xml b/android/WALT/app/src/main/res/layout/fragment_diagnostics.xml new file mode 100644 index 0000000..82a3cae --- /dev/null +++ b/android/WALT/app/src/main/res/layout/fragment_diagnostics.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/WALT/app/src/main/res/layout/fragment_drag_latency.xml b/android/WALT/app/src/main/res/layout/fragment_drag_latency.xml new file mode 100644 index 0000000..f9b65d0 --- /dev/null +++ b/android/WALT/app/src/main/res/layout/fragment_drag_latency.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +