aboutsummaryrefslogtreecommitdiff
path: root/android/WALT/app
diff options
context:
space:
mode:
authorAndrew Lehmer <alehmer@google.com>2017-04-26 14:58:59 -0700
committerAndrew Lehmer <alehmer@google.com>2017-04-26 14:58:59 -0700
commite76dcf96b0c451e46cddfa695de8feeb92533937 (patch)
treeed9a45d409f988f517e6c3f3a685cbf81ac45a5a /android/WALT/app
parentbcf013dda8ffac9fd76937be6441b44bb9f3586f (diff)
downloadwalt-e76dcf96b0c451e46cddfa695de8feeb92533937.tar.gz
Import google/walt
Cloned from https://github.com/google/walt.git without modification. Bug: 36896528 Test: N/A
Diffstat (limited to 'android/WALT/app')
-rw-r--r--android/WALT/app/.gitignore2
-rw-r--r--android/WALT/app/build.gradle50
-rw-r--r--android/WALT/app/proguard-rules.pro17
-rw-r--r--android/WALT/app/src/main/AndroidManifest.xml38
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/AboutFragment.java57
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/AudioFragment.java310
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/AudioTest.java458
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/AutoRunFragment.java233
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/BaseTest.java50
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/BaseUsbConnection.java223
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/CrashLogActivity.java43
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/CustomNumberPicker.java83
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/DiagnosticsFragment.java82
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/DragLatencyFragment.java415
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/FastPathSurfaceView.java90
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/FrontPageFragment.java53
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/HistogramChart.java277
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/LogFragment.java84
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/LogUploader.java83
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java536
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/MidiFragment.java146
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/MidiTest.java372
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/NumberPickerPreference.java134
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/RemoteClockInfo.java117
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java573
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/SettingsFragment.java102
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/SimpleLogger.java80
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/TapLatencyFragment.java306
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/TouchCatcherView.java101
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/TraceLogger.java113
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/UsMotionEvent.java146
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/Utils.java187
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/WaltConnection.java43
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java411
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/WaltTcpConnection.java238
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/WaltUsbConnection.java182
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/BootloaderConnection.java80
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/DeviceConstants.java22
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/FirmwareImage.java127
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/programmer/Programmer.java104
-rw-r--r--android/WALT/app/src/main/jni/Android.mk30
-rw-r--r--android/WALT/app/src/main/jni/Application.mk17
-rw-r--r--android/WALT/app/src/main/jni/Makefile17
-rw-r--r--android/WALT/app/src/main/jni/README.md112
-rwxr-xr-xandroid/WALT/app/src/main/jni/findteensy.py30
-rw-r--r--android/WALT/app/src/main/jni/player.c520
-rw-r--r--android/WALT/app/src/main/jni/sync_clock.c327
-rw-r--r--android/WALT/app/src/main/jni/sync_clock.h50
-rw-r--r--android/WALT/app/src/main/jni/sync_clock_jni.c62
-rw-r--r--android/WALT/app/src/main/jni/sync_clock_linux.c80
-rw-r--r--android/WALT/app/src/main/res/color/button_tint.xml5
-rw-r--r--android/WALT/app/src/main/res/drawable/border.xml6
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_brightness_7_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_brightness_medium_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_check_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_equalizer_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_file_upload_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_help_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_help_outline_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_import_export_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_input_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_mic_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_music_note_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_output_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_radio_button_checked_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_receipt_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_refresh_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_schedule_black_24dp.xml10
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_search_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_settings_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_share_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_stop_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_swap_horiz_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_swap_vert_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_system_update_alt_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_timelapse_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_usb_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/drawable/ic_volume_up_black_24dp.xml9
-rw-r--r--android/WALT/app/src/main/res/layout/activity_crash_log.xml11
-rw-r--r--android/WALT/app/src/main/res/layout/activity_main.xml27
-rw-r--r--android/WALT/app/src/main/res/layout/dialog_upload.xml22
-rw-r--r--android/WALT/app/src/main/res/layout/fragment_about.xml57
-rw-r--r--android/WALT/app/src/main/res/layout/fragment_audio.xml68
-rw-r--r--android/WALT/app/src/main/res/layout/fragment_auto_run.xml15
-rw-r--r--android/WALT/app/src/main/res/layout/fragment_diagnostics.xml206
-rw-r--r--android/WALT/app/src/main/res/layout/fragment_drag_latency.xml123
-rw-r--r--android/WALT/app/src/main/res/layout/fragment_front_page.xml248
-rw-r--r--android/WALT/app/src/main/res/layout/fragment_log.xml17
-rw-r--r--android/WALT/app/src/main/res/layout/fragment_midi.xml54
-rw-r--r--android/WALT/app/src/main/res/layout/fragment_screen_response.xml76
-rw-r--r--android/WALT/app/src/main/res/layout/fragment_tap_latency.xml99
-rw-r--r--android/WALT/app/src/main/res/layout/histogram.xml23
-rw-r--r--android/WALT/app/src/main/res/layout/line_chart.xml23
-rw-r--r--android/WALT/app/src/main/res/layout/numberpicker_dialog.xml19
-rw-r--r--android/WALT/app/src/main/res/layout/toolbar.xml8
-rw-r--r--android/WALT/app/src/main/res/menu/menu_main.xml28
-rw-r--r--android/WALT/app/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 1975 bytes
-rw-r--r--android/WALT/app/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 1353 bytes
-rw-r--r--android/WALT/app/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 2633 bytes
-rw-r--r--android/WALT/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 3964 bytes
-rw-r--r--android/WALT/app/src/main/res/raw/walt.hex1397
-rw-r--r--android/WALT/app/src/main/res/values-w820dp/dimens.xml6
-rw-r--r--android/WALT/app/src/main/res/values/attrs.xml12
-rw-r--r--android/WALT/app/src/main/res/values/color.xml9
-rw-r--r--android/WALT/app/src/main/res/values/dimens.xml5
-rw-r--r--android/WALT/app/src/main/res/values/strings.xml45
-rw-r--r--android/WALT/app/src/main/res/values/styles.xml61
-rw-r--r--android/WALT/app/src/main/res/xml/device_filter.xml14
-rw-r--r--android/WALT/app/src/main/res/xml/preferences.xml135
-rw-r--r--android/WALT/app/src/test/java/org/chromium/latency/walt/HistogramChartTest.java81
-rw-r--r--android/WALT/app/src/test/java/org/chromium/latency/walt/TraceLoggerTest.java46
-rw-r--r--android/WALT/app/src/test/java/org/chromium/latency/walt/UtilsTest.java162
114 files changed, 11374 insertions, 0 deletions
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 <SDK dir>/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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.chromium.latency.walt">
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme">
+ <activity
+ android:name="org.chromium.latency.walt.MainActivity"
+ android:label="@string/app_name"
+ android:launchMode="singleTask"
+ android:screenOrientation="portrait">
+ <meta-data
+ android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
+ android:resource="@xml/device_filter" />
+
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name="org.chromium.latency.walt.CrashLogActivity"
+ android:label="@string/title_activity_crash_log"
+ android:theme="@style/AppTheme" />
+ </application>
+
+</manifest>
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<CharSequence> 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<Entry> 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<Double> deltas_mic = new ArrayList<>();
+ private ArrayList<Double> deltas_play2queue = new ArrayList<>();
+ ArrayList<Double> deltas_queue2wire = new ArrayList<>();
+ private ArrayList<Double> 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<String, UsbDevice> 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<UsMotionEvent> touchEventList = new ArrayList<>();
+ ArrayList<WaltDevice.TriggerMessage> 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<lt.length; i++) {
+ sideIdx[i] = ((i+1) / 2) % 2;
+ }
+ /*
+ logger.log("ft = " + Utils.array2string(ft, "%.2f"));
+ logger.log("fy = " + Utils.array2string(fy, "%.2f"));
+ logger.log("lt = " + Utils.array2string(lt, "%.2f"));
+ logger.log("sideIdx = " + Arrays.toString(sideIdx));*/
+
+ double averageBestShift = 0;
+ for(int side = 0; side < 2; side++) {
+ double[] lts = Utils.extract(sideIdx, side, lt);
+ // TODO: time this call
+ double bestShift = Utils.findBestShift(lts, ft, fy);
+ logger.log(String.format(Locale.US, "bestShift = %.2f", bestShift));
+ averageBestShift += bestShift / 2;
+ }
+
+ drawLatencyGraph(ft, fy, lt, averageBestShift);
+ logger.log(String.format(Locale.US, "Drag latency is %.1f [ms]", averageBestShift));
+ }
+
+ private void drawLatencyGraph(double[] ft, double[] fy, double[] lt, double averageBestShift) {
+ final ArrayList<Entry> touchEntries = new ArrayList<>();
+ final ArrayList<Entry> 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<IBarDataSet> dataSets = new ArrayList<>(numDataSets);
+ for (int i = 0; i < numDataSets; i++) {
+ final BarDataSet dataSet = new BarDataSet(new ArrayList<BarEntry>(), "");
+ 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<ArrayList<Double>> 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<Double>());
+ }
+ }
+
+ 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<Double> 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<Integer> {
+
+ 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<Integer>() {
+ @Override
+ public void onLoadComplete(Loader<Integer> 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<Double> deltasToSys = new ArrayList<>();
+ ArrayList<Double> deltasInputTotal = new ArrayList<>();
+ ArrayList<Double> 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<Double> deltas_w2b = new ArrayList<>();
+ ArrayList<Double> deltas_b2w = new ArrayList<>();
+ ArrayList<Double> 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<CharSequence> 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<Entry> 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<UsMotionEvent> eventList = new ArrayList<>();
+ ArrayList<Double> p2kDown = new ArrayList<>();
+ ArrayList<Double> p2kUp = new ArrayList<>();
+ ArrayList<Double> k2cDown = new ArrayList<>();
+ ArrayList<Double> 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<TraceEvent> 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<Double> arrList) {
+ ArrayList<Double> 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<arr.length; i++) {
+ if (indicator[i] == value) {
+ newx[j] = arr[i];
+ j++;
+ }
+ }
+ return newx;
+ }
+
+ public static String array2string(double[] a, String format) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("array([");
+ for (double x: a) {
+ sb.append(String.format(format, x));
+ sb.append(", ");
+ }
+ sb.append("])");
+ return sb.toString();
+ }
+
+
+ public static int argmin(double[] a) {
+ int imin = 0;
+ for (int i=1; i<a.length; i++) if (a[i] < a[imin]) imin = i;
+ return imin;
+ }
+
+ private static double getShiftError(double[] laserT, double[] touchT, double[] touchY, double shift) {
+ double[] T = new double[laserT.length];
+ for (int j=0; j<T.length; j++) {
+ T[j] = laserT[j] + shift;
+ }
+ double [] laserY = Utils.interp(T, touchT, touchY);
+ // TODO: Think about throwing away a percentile of most distanced points for noise reduction
+ return Utils.stdev(laserY);
+ }
+
+ /**
+ * Simplified Java re-implementation or py/qslog/minimization.py.
+ * This is very specific to the drag latency algorithm.
+ *
+ * tl;dr: Shift laser events by some time delta and see how well they fit on a horizontal line.
+ * Delta that results in the best looking straight line is the latency.
+ */
+ public static double findBestShift(double[] laserT, double[] touchT, double[] touchY) {
+ int steps = 1500;
+ double[] shiftSteps = new double[]{0.1, 0.01}; // milliseconds
+ double[] stddevs = new double[steps];
+ double bestShift = shiftSteps[0]*steps/2;
+ for (final double shiftStep : shiftSteps) {
+ for (int i = 0; i < steps; i++) {
+ stddevs[i] = getShiftError(laserT, touchT, touchY, bestShift + shiftStep * i - shiftStep * steps / 2);
+ }
+ bestShift = argmin(stddevs) * shiftStep + bestShift - shiftStep * steps / 2;
+ }
+ return bestShift;
+ }
+
+ static byte[] char2byte(char c) {
+ return new byte[]{(byte) c};
+ }
+
+ static int getIntPreference(Context context, @StringRes int keyId, int defaultValue) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return preferences.getInt(context.getString(keyId), defaultValue);
+ }
+
+ static boolean getBooleanPreference(Context context, @StringRes int keyId, boolean defaultValue) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return preferences.getBoolean(context.getString(keyId), defaultValue);
+ }
+
+ static String getStringPreference(Context context, @StringRes int keyId, String defaultValue) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return preferences.getString(context.getString(keyId), defaultValue);
+ }
+
+ public enum ListenerState {
+ RUNNING,
+ STARTING,
+ STOPPED,
+ STOPPING
+ }
+
+}
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltConnection.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltConnection.java
new file mode 100644
index 0000000..98835af
--- /dev/null
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltConnection.java
@@ -0,0 +1,43 @@
+/*
+ * 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 java.io.IOException;
+
+
+public interface WaltConnection {
+
+ void connect();
+
+ boolean isConnected();
+
+ void sendByte(char c) throws IOException;
+
+ int blockingRead(byte[] buffer);
+
+ RemoteClockInfo syncClock() throws IOException;
+
+ void updateLag();
+
+ void setConnectionStateListener(ConnectionStateListener connectionStateListener);
+
+ interface ConnectionStateListener {
+ void onConnect();
+ void onDisconnect();
+ }
+}
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java
new file mode 100644
index 0000000..96ddcfd
--- /dev/null
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java
@@ -0,0 +1,411 @@
+/*
+ * 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.res.Resources;
+import android.hardware.usb.UsbDevice;
+import android.os.Handler;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * A singleton used as an interface for the physical WALT device.
+ */
+public class WaltDevice implements WaltConnection.ConnectionStateListener {
+
+ private static final int DEFAULT_DRIFT_LIMIT_US = 1500;
+ private static final String TAG = "WaltDevice";
+ public static final String PROTOCOL_VERSION = "5";
+
+ // Teensy side commands. Each command is a single char
+ // Based on #defines section in walt.ino
+ static final char CMD_PING_DELAYED = 'D'; // Ping with a delay
+ static final char CMD_RESET = 'F'; // Reset all vars
+ static final char CMD_SYNC_SEND = 'I'; // Send some digits for clock sync
+ static final char CMD_PING = 'P'; // Ping with a single byte
+ static final char CMD_VERSION = 'V'; // Determine WALT's firmware version
+ static final char CMD_SYNC_READOUT = 'R'; // Read out sync times
+ static final char CMD_GSHOCK = 'G'; // Send last shock time and watch for another shock.
+ static final char CMD_TIME_NOW = 'T'; // Current time
+ static final char CMD_SYNC_ZERO = 'Z'; // Initial zero
+ static final char CMD_AUTO_SCREEN_ON = 'C'; // Send a message on screen color change
+ static final char CMD_AUTO_SCREEN_OFF = 'c';
+ static final char CMD_SEND_LAST_SCREEN = 'E'; // Send info about last screen color change
+ static final char CMD_BRIGHTNESS_CURVE = 'U'; // Probe screen for brightness vs time curve
+ static final char CMD_AUTO_LASER_ON = 'L'; // Send messages on state change of the laser
+ static final char CMD_AUTO_LASER_OFF = 'l';
+ static final char CMD_SEND_LAST_LASER = 'J';
+ static final char CMD_AUDIO = 'A'; // Start watching for signal on audio out line
+ static final char CMD_BEEP = 'B'; // Generate a tone into the mic and send timestamp
+ static final char CMD_BEEP_STOP = 'S'; // Stop generating tone
+ static final char CMD_MIDI = 'M'; // Start listening for a MIDI message
+ static final char CMD_NOTE = 'N'; // Generate a MIDI NoteOn message
+
+ private static final int BYTE_BUFFER_SIZE = 1024 * 4;
+ private byte[] buffer = new byte[BYTE_BUFFER_SIZE];
+
+ private Context context;
+ protected SimpleLogger logger;
+ private WaltConnection connection;
+ public RemoteClockInfo clock;
+ private WaltConnection.ConnectionStateListener connectionStateListener;
+
+ private static final Object LOCK = new Object();
+ private static WaltDevice instance;
+
+ public static WaltDevice getInstance(Context context) {
+ synchronized (LOCK) {
+ if (instance == null) {
+ instance = new WaltDevice(context.getApplicationContext());
+ }
+ return instance;
+ }
+ }
+
+ private WaltDevice(Context context) {
+ this.context = context;
+ triggerListener = new TriggerListener();
+ logger = SimpleLogger.getInstance(context);
+ }
+
+ public void onConnect() {
+ try {
+ // TODO: restore
+ softReset();
+ checkVersion();
+ syncClock();
+ } catch (IOException e) {
+ logger.log("Unable to communicate with WALT: " + e.getMessage());
+ }
+
+ if (connectionStateListener != null) {
+ connectionStateListener.onConnect();
+ }
+ }
+
+ // Called when disconnecting from WALT
+ // TODO: restore this, not called from anywhere
+ public void onDisconnect() {
+ if (!isListenerStopped()) {
+ stopListener();
+ }
+
+ if (connectionStateListener != null) {
+ connectionStateListener.onDisconnect();
+ }
+ }
+
+ public void connect() {
+ if (WaltTcpConnection.probe()) {
+ logger.log("Using TCP bridge for ChromeOS");
+ connection = WaltTcpConnection.getInstance(context);
+ } else {
+ // USB connection
+ logger.log("No TCP bridge detected, using direct USB connection");
+ connection = WaltUsbConnection.getInstance(context);
+ }
+ connection.setConnectionStateListener(this);
+ connection.connect();
+ }
+
+ public void connect(UsbDevice usbDevice) {
+ // This happens when apps starts as a result of plugging WALT into USB. In this case we
+ // receive an intent with a usbDevice
+ WaltUsbConnection usbConnection = WaltUsbConnection.getInstance(context);
+ connection = usbConnection;
+ connection.setConnectionStateListener(this);
+ usbConnection.connect(usbDevice);
+ }
+
+ public boolean isConnected() {
+ return connection.isConnected();
+ }
+
+
+ public String readOne() throws IOException {
+ if (!isListenerStopped()) {
+ throw new IOException("Can't do blocking read while listener is running");
+ }
+
+ byte[] buff = new byte[64];
+ int ret = connection.blockingRead(buff);
+
+ if (ret < 0) {
+ throw new IOException("Timed out reading from WALT");
+ }
+ String s = new String(buff, 0, ret);
+ Log.i(TAG, "readOne() received data: " + s);
+ return s;
+ }
+
+
+ private String sendReceive(char c) throws IOException {
+ connection.sendByte(c);
+ return readOne();
+ }
+
+ public void sendAndFlush(char c) {
+
+ try {
+ connection.sendByte(c);
+ while(connection.blockingRead(buffer) > 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 <android/log.h>
+#include <assert.h>
+#include <jni.h>
+#include <malloc.h>
+#include <math.h>
+#include <sys/types.h>
+
+// for native audio
+#include <SLES/OpenSLES.h>
+#include <SLES/OpenSLES_Android.h>
+#include <SLES/OpenSLES_AndroidConfiguration.h>
+
+#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 <asm/byteorder.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/usbdevice_fs.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+#include <sys/time.h>
+#include <time.h>
+#include <unistd.h>
+
+
+#ifdef __ANDROID__
+ #include <android/log.h>
+ #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 <inttypes.h>
+
+#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 <android/log.h>
+#include <jni.h>
+
+
+#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 <errno.h>
+#include <fcntl.h>
+#include <linux/usbdevice_fs.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+
+int main(int argc, char ** argv) {
+ if(argc < 2) {
+ printf("Usage %s <device_path>\n"
+ "Try `lsusb | grep eensy` and use /dev/bus/usb/<Bus>/<Device>\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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false" android:color="#FFABABAB"/>
+ <item android:state_enabled="true" android:color="#FF000000"/>
+</selector>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="2dp"/>
+ <stroke android:width="1dp" android:color="#000000"/>
+</shape>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69L23.31,12 20,8.69zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6zm0,-10c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M20,15.31L23.31,12 20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69zM12,18V6c3.31,0 6,2.69 6,6s-2.69,6 -6,6z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M10,20h4V4h-4v16zm-6,0h4v-8H4v8zM16,9v11h4V9h-4z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zm-4,2h14v2H5z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,17h-2v-2h2v2zm2.07,-7.75l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2H8c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M11,18h2v-2h-2v2zm1,-16C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm0,18c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zm0,-14c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9,3L5,6.99h3V14h2V6.99h3L9,3zm7,14.01V10h-2v7.01h-3L15,21l4,-3.99h-3z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+android:width="24dp"
+android:height="24dp"
+android:viewportWidth="24.0"
+android:viewportHeight="24.0">
+<path
+ android:fillColor="#FF000000"
+ android:pathData="m 21,3.01 -12,0 c -1.1,0 -2,0.9 -2,2 L 7,9 9,9 9,4.99 l 12,0 0,14.03 -12,0 0,-4.02 -2,0 0,4.01 c 0,1.1 0.9,1.98 2,1.98 l 12,0 c 1.1,0 2,-0.88 2,-1.98 l 0,-14 c 0,-1.11 -0.9,-2 -2,-2 z M 11,16 l 4,-4 -4,-4 0,3 -10,0 0,2 10,0 z"/>
+</vector> \ 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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zm5.3,-3c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+android:width="24dp"
+android:height="24dp"
+android:viewportWidth="24.0"
+android:viewportHeight="24.0">
+<path
+ android:fillColor="#FF000000"
+ android:pathData="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
+</vector> \ 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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+android:width="24dp"
+android:height="24dp"
+android:viewportWidth="24.0"
+android:viewportHeight="24.0">
+<path
+ android:fillColor="#FF000000"
+ android:pathData="m1,19.01c0,1.1,0.9,1.98,2,1.98h12c1.1,0,1.509-0.99564,2-1.98v-4.01h-2v4.02h-12v-14.03h12v4.01h2v-3.99c0-1.11-0.9-2-2-2h-12c-1.1,0-2,0.9-2,2m18,10.99l4-4-4-4v3h-10v2h10z"/>
+</vector> \ 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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M8,5v14l11,-7z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5 5,-2.24 5,-5 -2.24,-5 -5,-5zm0,-5C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm0,18c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M18,17H6v-2h12v2zm0,-4H6v-2h12v2zm0,-4H6V7h12v2zM3,22l1.5,-1.5L6,22l1.5,-1.5L9,22l1.5,-1.5L12,22l1.5,-1.5L15,22l1.5,-1.5L18,22l1.5,-1.5L21,22V2l-1.5,1.5L18,2l-1.5,1.5L15,2l-1.5,1.5L12,2l-1.5,1.5L9,2 7.5,3.5 6,2 4.5,3.5 3,2v20z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"
+ android:fillAlpha=".9"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zm-6,0C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M6,6h12v12H6z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M6.99,11L3,15l3.99,4v-3H14v-2H6.99v-3zM21,9l-3.99,-4v3H10v2h7.01v3L21,9z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M16,17.01V10h-2v7.01h-3L15,21l4,-3.99h-3zM9,3L5,6.99h3V14h2V6.99h3L9,3z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12 16.5l4-4h-3v-9h-2v9H8l4 4zm9-13h-6v1.99h6v14.03H3V5.49h6V3.5H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2v-14c0-1.1-.9-2-2-2z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M16.24,7.76C15.07,6.59 13.54,6 12,6v6l-4.24,4.24c2.34,2.34 6.14,2.34 8.49,0 2.34,-2.34 2.34,-6.14 -0.01,-8.48zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm0,18c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M15,7v4h1v2h-3V5h2l-3,-4 -3,4h2v8H8v-2.07c0.7,-0.37 1.2,-1.08 1.2,-1.93 0,-1.21 -0.99,-2.2 -2.2,-2.2 -1.21,0 -2.2,0.99 -2.2,2.2 0,0.85 0.5,1.56 1.2,1.93V13c0,1.11 0.89,2 2,2h3v3.05c-0.71,0.37 -1.2,1.1 -1.2,1.95 0,1.22 0.99,2.2 2.2,2.2 1.21,0 2.2,-0.98 2.2,-2.2 0,-0.85 -0.49,-1.58 -1.2,-1.95V15h3c1.11,0 2,-0.89 2,-2v-2h1V7h-4z"/>
+</vector>
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M3,9v6h4l5,5V4L7,9H3zm13.5,3c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
+</vector>
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 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="vertical"
+ android:id="@+id/txt_crash_log"/>
+
+</RelativeLayout>
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 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+
+ <include
+ android:id="@+id/toolbar_main"
+ layout="@layout/toolbar" />
+
+ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+
+ </FrameLayout>
+
+ </LinearLayout>
+
+</RelativeLayout>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp">
+
+ <EditText
+ android:id="@+id/edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="Enter URL"
+ android:inputType="textUri" />
+
+ <ProgressBar
+ android:id="@+id/progress_bar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:layout_centerInParent="true" />
+
+</RelativeLayout>
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 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context="org.chromium.latency.walt.AboutFragment">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:autoLink="all"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:paddingTop="32dp"
+ android:scrollbars="vertical"
+ android:text="@string/disclaimer"
+ android:textStyle="bold" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:paddingTop="20dp"
+ android:scrollbars="vertical"
+ android:text="@string/about_description" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:autoLink="web"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:paddingTop="20dp"
+ android:scrollbars="vertical"
+ android:text="@string/more_info" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:autoLink="web"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:paddingTop="20dp"
+ android:scrollbars="vertical"
+ android:text="@string/privacy_policy" />
+
+ <TextView
+ android:id="@+id/txt_build_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:paddingTop="20dp"
+ android:scrollbars="vertical" />
+
+</LinearLayout>
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 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:walt="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="org.chromium.latency.walt.TapLatencyFragment">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/button_stop_audio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_stop_black_24dp" />
+
+ <ImageButton
+ android:id="@+id/button_start_audio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_play_arrow_black_24dp" />
+
+ <Spinner
+ android:id="@+id/spinner_audio_mode"
+ android:layout_height="45dp"
+ android:layout_width="fill_parent"
+ android:layout_toRightOf="@id/button_stop_audio"
+ android:layout_toLeftOf="@id/button_start_audio"
+ android:prompt="@string/audio_mode"/>
+
+ </RelativeLayout>
+
+ <include
+ android:id="@+id/chart_layout"
+ layout="@layout/line_chart" />
+
+ <org.chromium.latency.walt.HistogramChart
+ android:id="@+id/latency_chart"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="gone"
+ walt:description="Latency [ms]"
+ walt:binWidth="0.1" />
+
+ <!-- For now the results are just listed in this textView -->
+ <TextView
+ android:id="@+id/txt_box_audio"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="2"
+ android:background="#aaaaaa"
+ android:gravity="bottom"
+ android:scrollbars="vertical" />
+
+ </LinearLayout>
+</FrameLayout>
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 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="org.chromium.latency.walt.AutoRunFragment"
+ android:id="@+id/fragment_auto_run">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/txt_log_auto_run"
+ android:scrollbars = "vertical"
+ android:gravity="bottom" />
+
+</FrameLayout>
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 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context="org.chromium.latency.walt.DiagnosticsFragment">
+
+ <!-- The whole list of options -->
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="3">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:orientation="vertical">
+
+ <!-- Reconnect -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickReconnect">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_usb_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Reconnect" />
+
+ <TextView
+ android:visibility="gone"
+ style="@style/MenuTextBottom"
+ android:text="TBD: Conn status" />
+
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of Reconnect -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- Ping -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickPing">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_swap_horiz_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Ping" />
+
+ <TextView
+ style="@style/MenuTextBottom"
+ android:text="Ping over USB with 1 byte" />
+
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of Ping -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- ReSync -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickSync">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_schedule_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Re-sync clocks" />
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of ReSync -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- CheckDrift -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickCheckDrift">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_timelapse_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Check clock drift" />
+
+ <TextView
+ style="@style/MenuTextBottom"
+ android:text="Check how much clocks diverged" />
+
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of CheckDrift -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- Program -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickProgram">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_system_update_alt_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Update WALT Firmware" />
+
+ <TextView
+ style="@style/MenuTextBottom"
+ android:text="Please press the button on the Teensy first" />
+
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- Program -->
+
+ <!--<View style="@style/MenuDivider" />-->
+
+ <!-- Send T TODO: replace with send any char, it says nothing on the log, broadcast? -->
+ <!--
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickSendT">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_swap_horiz_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Send 'T'" />
+
+ <TextView
+ style="@style/MenuTextBottom"
+ android:text="..." />
+
+
+ </LinearLayout>
+ </LinearLayout>
+ -->
+ <!-- End of Send T -->
+ </LinearLayout>
+ </ScrollView>
+
+ <TextView
+ android:id="@+id/txt_log_diag"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="2"
+ android:background="#BBBBBB"
+ android:gravity="bottom"
+ android:scrollbars="vertical" />
+
+</LinearLayout>
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 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="org.chromium.latency.walt.TapLatencyFragment">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/button_restart_drag"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_refresh_black_24dp" />
+
+ <ImageButton
+ android:id="@+id/button_start_drag"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_play_arrow_black_24dp" />
+
+ <ImageButton
+ android:id="@+id/button_finish_drag"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_check_black_24dp" />
+ </LinearLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="#000000"
+ android:gravity="right"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/txt_cross_counts"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#000000"
+ android:padding="4dp"
+ android:text=""
+
+ android:textColor="#00ff00" />
+
+ <TextView
+ android:id="@+id/txt_drag_counts"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#000000"
+ android:padding="4dp"
+ android:text=""
+
+ android:textColor="#ff0000" />
+ </LinearLayout>
+
+ <RelativeLayout
+ android:id="@+id/latency_chart_layout"
+ android:layout_width="match_parent"
+ android:visibility="gone"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_margin="5dp"
+ android:background="@drawable/border">
+
+ <com.github.mikephil.charting.charts.ScatterChart
+ android:id="@+id/latency_chart"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="5dp" />
+
+ <Button
+ android:id="@+id/button_close_chart"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:tint="@color/button_tint"
+ android:layout_margin="5dp"
+ android:text="Close" />
+ </RelativeLayout>
+
+ <TextView
+ android:id="@+id/txt_log_drag_latency"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:background="#000000"
+ android:gravity="bottom"
+ android:textColor="#ffffff"
+ android:scrollbars="vertical" />
+
+ </LinearLayout>
+
+ <!-- Overlay semi-transparent view that catches the touch events -->
+ <org.chromium.latency.walt.TouchCatcherView
+ android:id="@+id/tap_catcher"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#01000000" />
+
+ </FrameLayout>
+ </LinearLayout>
+</FrameLayout>
diff --git a/android/WALT/app/src/main/res/layout/fragment_front_page.xml b/android/WALT/app/src/main/res/layout/fragment_front_page.xml
new file mode 100644
index 0000000..ec3a95a
--- /dev/null
+++ b/android/WALT/app/src/main/res/layout/fragment_front_page.xml
@@ -0,0 +1,248 @@
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ tools:context="org.chromium.latency.walt.FrontPageFragment">
+ <!-- The whole list of options -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- Diagnostics -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickClockSync">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_search_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Diagnostics" />
+
+ <TextView
+ style="@style/MenuTextBottom"
+ android:text="TBD: Connection/sync status"
+ android:visibility="gone" />
+
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of Diagnostics -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- Tap latency -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickTapLatency">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_radio_button_checked_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Tap latency" />
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of Tap latency -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- Drag latency -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickDragLatency">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_swap_vert_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Drag latency" />
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End drag latency -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- Screen response -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickScreenResponse">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_brightness_medium_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Screen response" />
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of Screen response -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- Audio -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickAudio">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_volume_up_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Audio latency" />
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of Audio -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- MIDI -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickMIDI" >
+
+ <ImageView
+ android:id="@+id/midi_image"
+ style="@style/MenuIconStyle"
+ android:tint="@color/ColorDisabled"
+ android:src="@drawable/ic_music_note_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/midi_text"
+ style="@style/MenuTextTop"
+ android:textColor="@color/ColorDisabled"
+ android:text="MIDI latency" />
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of MIDI -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- Log -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickOpenLog">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_receipt_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="View log" />
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of Log -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- Settings -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickOpenSettings">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_settings_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="Settings" />
+
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of Settings -->
+
+ <View style="@style/MenuDivider" />
+
+ <!-- About / Help -->
+ <LinearLayout
+ style="@style/MenuItemStyle"
+ android:onClick="onClickOpenAbout">
+
+ <ImageView
+ style="@style/MenuIconStyle"
+ android:src="@drawable/ic_help_outline_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ style="@style/MenuTextTop"
+ android:text="About" />
+
+ </LinearLayout>
+ </LinearLayout>
+ <!-- End of About -->
+
+
+ </LinearLayout>
+</ScrollView>
diff --git a/android/WALT/app/src/main/res/layout/fragment_log.xml b/android/WALT/app/src/main/res/layout/fragment_log.xml
new file mode 100644
index 0000000..0d953d9
--- /dev/null
+++ b/android/WALT/app/src/main/res/layout/fragment_log.xml
@@ -0,0 +1,17 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="org.chromium.latency.walt.LogFragment">
+
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/txt_log"
+ android:scrollbars = "vertical"
+ android:gravity="bottom"
+
+ />
+
+</FrameLayout>
diff --git a/android/WALT/app/src/main/res/layout/fragment_midi.xml b/android/WALT/app/src/main/res/layout/fragment_midi.xml
new file mode 100644
index 0000000..70f9be5
--- /dev/null
+++ b/android/WALT/app/src/main/res/layout/fragment_midi.xml
@@ -0,0 +1,54 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:walt="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="org.chromium.latency.walt.TapLatencyFragment">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/button_start_midi_in"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_input_black_24dp" />
+
+ <ImageButton
+ android:id="@+id/button_start_midi_out"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_output_black_24dp" />
+ </RelativeLayout>
+
+ <org.chromium.latency.walt.HistogramChart
+ android:id="@+id/latency_chart"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="gone"
+ walt:binWidth="0.5" />
+
+ <!-- For now the results are just listed in this textView -->
+ <TextView
+ android:id="@+id/txt_box_midi"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="2"
+ android:background="#aaaaaa"
+ android:gravity="bottom"
+ android:scrollbars="vertical" />
+
+ </LinearLayout>
+</FrameLayout>
diff --git a/android/WALT/app/src/main/res/layout/fragment_screen_response.xml b/android/WALT/app/src/main/res/layout/fragment_screen_response.xml
new file mode 100644
index 0000000..b789579
--- /dev/null
+++ b/android/WALT/app/src/main/res/layout/fragment_screen_response.xml
@@ -0,0 +1,76 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:walt="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="org.chromium.latency.walt.TapLatencyFragment">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:id="@+id/button_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/button_stop_screen_response"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_stop_black_24dp" />
+
+ <ImageButton
+ android:id="@+id/button_start_screen_response"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_play_arrow_black_24dp" />
+
+ <Spinner
+ android:id="@+id/spinner_screen_response"
+ android:layout_height="45dp"
+ android:layout_width="fill_parent"
+ android:layout_toRightOf="@id/button_stop_screen_response"
+ android:layout_toLeftOf="@id/button_start_screen_response"
+ android:prompt="@string/screen_response_mode"/>
+ </RelativeLayout>
+
+ <include
+ android:id="@+id/brightness_chart_layout"
+ layout="@layout/line_chart" />
+
+ <org.chromium.latency.walt.HistogramChart
+ android:id="@+id/latency_chart"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="gone"
+ walt:description="Blink Latency [ms]"
+ walt:numDataSets="2"
+ walt:binWidth="5" />
+
+ <!-- The big box that flickers between black and white -->
+ <TextView
+ android:id="@+id/txt_black_box_screen"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="2"
+ android:background="#aaaaaa"
+ android:gravity="bottom"
+ android:scrollbars="vertical" />
+
+ <org.chromium.latency.walt.FastPathSurfaceView
+ android:id="@+id/fast_path_surface"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="2"
+ android:visibility="gone" />
+
+ </LinearLayout>
+</FrameLayout>
diff --git a/android/WALT/app/src/main/res/layout/fragment_tap_latency.xml b/android/WALT/app/src/main/res/layout/fragment_tap_latency.xml
new file mode 100644
index 0000000..2c701d2
--- /dev/null
+++ b/android/WALT/app/src/main/res/layout/fragment_tap_latency.xml
@@ -0,0 +1,99 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:walt="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="org.chromium.latency.walt.TapLatencyFragment">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageButton
+ android:id="@+id/button_finish_tap"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_check_black_24dp" />
+
+ <ImageButton
+ android:id="@+id/button_restart_tap"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:tint="@color/button_tint"
+ android:src="@drawable/ic_play_arrow_black_24dp" />
+ </RelativeLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="#000000"
+ android:gravity="right"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/txt_tap_counts"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#000000"
+ android:padding="4dp"
+ android:text=""
+ android:textColor="#00ff00" />
+
+ <TextView
+ android:id="@+id/txt_move_count"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#000000"
+ android:padding="4dp"
+ android:text=""
+ android:textColor="#ff0000" />
+ </LinearLayout>
+
+ <org.chromium.latency.walt.HistogramChart
+ android:id="@+id/latency_chart"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="gone"
+ walt:description="Tap Latency [ms]"
+ walt:binWidth="5"
+ walt:numDataSets="2" />
+
+ <TextView
+ android:id="@+id/txt_log_tap_latency"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:background="#000000"
+ android:gravity="bottom"
+ android:textColor="#ffffff"
+ android:scrollbars="vertical" />
+ </LinearLayout>
+
+ <!-- Overlay semi-transparent view that catches the touch events -->
+ <TextView
+ android:id="@+id/tap_catcher"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#01000000" />
+
+ </FrameLayout>
+ </LinearLayout>
+</FrameLayout>
diff --git a/android/WALT/app/src/main/res/layout/histogram.xml b/android/WALT/app/src/main/res/layout/histogram.xml
new file mode 100644
index 0000000..ca6dc1e
--- /dev/null
+++ b/android/WALT/app/src/main/res/layout/histogram.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/latency_chart_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="5dp"
+ android:background="@drawable/border">
+ <com.github.mikephil.charting.charts.BarChart
+ android:id="@+id/bar_chart"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="10dp"
+ android:gravity="bottom" />
+ <Button
+ android:id="@+id/button_close_bar_chart"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:tint="@color/button_tint"
+ android:layout_margin="5dp"
+ android:text="Close" />
+</RelativeLayout>
diff --git a/android/WALT/app/src/main/res/layout/line_chart.xml b/android/WALT/app/src/main/res/layout/line_chart.xml
new file mode 100644
index 0000000..7de4cfb
--- /dev/null
+++ b/android/WALT/app/src/main/res/layout/line_chart.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="5dp"
+ android:background="@drawable/border"
+ android:visibility="gone">
+ <com.github.mikephil.charting.charts.LineChart
+ android:id="@+id/chart"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="10dp"
+ android:gravity="bottom" />
+ <Button
+ android:id="@+id/button_close_chart"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:tint="@color/button_tint"
+ android:layout_margin="5dp"
+ android:text="Close" />
+</RelativeLayout>
diff --git a/android/WALT/app/src/main/res/layout/numberpicker_dialog.xml b/android/WALT/app/src/main/res/layout/numberpicker_dialog.xml
new file mode 100644
index 0000000..05c9e99
--- /dev/null
+++ b/android/WALT/app/src/main/res/layout/numberpicker_dialog.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical" android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="4dp"
+ android:paddingStart="6dp"
+ android:paddingEnd="6dp" >
+ <TextView
+ android:id="@android:id/message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+ <org.chromium.latency.walt.CustomNumberPicker
+ android:id="@+id/numpicker_pref"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"/>
+</LinearLayout>
diff --git a/android/WALT/app/src/main/res/layout/toolbar.xml b/android/WALT/app/src/main/res/layout/toolbar.xml
new file mode 100644
index 0000000..d02028d
--- /dev/null
+++ b/android/WALT/app/src/main/res/layout/toolbar.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/ColorPrimary"
+ android:elevation="4dp">
+
+</android.support.v7.widget.Toolbar> \ No newline at end of file
diff --git a/android/WALT/app/src/main/res/menu/menu_main.xml b/android/WALT/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..6333e6d
--- /dev/null
+++ b/android/WALT/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,28 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
+
+
+
+ <item
+ android:id="@+id/action_upload"
+ android:orderInCategory="180"
+ android:title="Upload"
+ android:icon="@drawable/ic_file_upload_black_24dp"
+ app:showAsAction="ifRoom" />
+
+ <item
+ android:id="@+id/action_share"
+ android:orderInCategory="190"
+ android:title="Share"
+ android:icon="@drawable/ic_share_black_24dp"
+ app:showAsAction="ifRoom" />
+
+ <item
+ android:id="@+id/action_help"
+ android:orderInCategory="200"
+ android:title="Help"
+ android:icon="@drawable/ic_help_outline_black_24dp"
+ app:showAsAction="ifRoom" />
+
+</menu>
diff --git a/android/WALT/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/WALT/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..b7c8868
--- /dev/null
+++ b/android/WALT/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/android/WALT/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/WALT/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..3f22278
--- /dev/null
+++ b/android/WALT/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/android/WALT/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/WALT/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..86877de
--- /dev/null
+++ b/android/WALT/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/android/WALT/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/WALT/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..6ff2e63
--- /dev/null
+++ b/android/WALT/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/android/WALT/app/src/main/res/raw/walt.hex b/android/WALT/app/src/main/res/raw/walt.hex
new file mode 100644
index 0000000..21fccf6
--- /dev/null
+++ b/android/WALT/app/src/main/res/raw/walt.hex
@@ -0,0 +1,1397 @@
+:1000000000180020C100000069250000352500000F
+:100010003525000035250000352500003525000078
+:100020003525000035250000352500006925000034
+:100030006925000035250000692500002525000000
+:100040006925000069250000692500006925000078
+:100050006925000069250000692500006925000068
+:100060006925000069250000692500006925000058
+:10007000A92B000049300000E1350000692500008F
+:100080006925000069250000692500006925000038
+:100090006925000069250000013B0000692500007A
+:1000A0004113000069250000692500006925000052
+:1000B0006925000069250000692500006925000008
+:1000C00038B502F055FA454A454B1A60454A464B49
+:1000D0001A60464A464B1A60464B0822197811426C
+:1000E00002D019780A431A70434B2A221A7000234F
+:1000F000424A43499A188A4204D24249C9580433B1
+:100100001160F5E7404B41491A1F181D00238A4230
+:1001100002D21360031CF6E73D493E4A5958101CB1
+:1001200099500433C02BF7D100239A083A49920022
+:10013000521803211940C9001568FF248C40A543BB
+:100140002C1C80258D40291C214301331160202B5C
+:10015000EBD1324B8A221860314B1A70314B24227A
+:100160005A70A0221A709A799107FCD59A79D5060F
+:10017000FCD498790C212B4A01400829F9D103219C
+:100180001171402151719A799106FCD59A795506E1
+:10019000FCD52549254A1160224A20211170997900
+:1001A0000C220A400C2AFAD1214A224B1A60224A18
+:1001B000224B1A60224B00221A60224B07221A603F
+:1001C000214A224B1A6062B600F092FF04F044FB11
+:1001D00002F0D4F902F04CF9FEE7C046300004F01A
+:1001E00034800440823F0000388004400100000F4A
+:1001F0003C80044002D0074000E00740700200202D
+:10020000600400205055000064040020D80800203D
+:100210000000000000F9FF1F00E400E008ED00E02E
+:100220000050064000400640000001104480044099
+:10023000C0000505048004407FBB000014E000E01E
+:1002400018E000E010E000E00000202020ED00E0D9
+:10025000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE
+:10026000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E
+:10027000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E
+:10028000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E
+:10029000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E
+:1002A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E
+:1002B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E
+:1002C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E
+:1002D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E
+:1002E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E
+:1002F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E
+:10030000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD
+:10031000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED
+:10032000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD
+:10033000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD
+:10034000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD
+:10035000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD
+:10036000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D
+:10037000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D
+:10038000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D
+:10039000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D
+:1003A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D
+:1003B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D
+:1003C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D
+:1003D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D
+:1003E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D
+:1003F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D
+:10040000FFFFFFFFFFFFFFFFFFFFFFFFDEF9FFFF23
+:1004100010B5064C2378002B07D1054B002B02D0DA
+:10042000044800E000BF0123237010BD60040020D9
+:10043000000000005055000008B5084B002B03D009
+:100440000748084900E000BF07480368002B03D0B5
+:10045000064B002B00D0984708BDC04600000000A6
+:1004600050550000640400206004002000000000DB
+:1004700010B5041C00F0FEFD2368C01A10BDFFFF7C
+:1004800010B50948FFF7F4FF084C20600E2000F07B
+:100490009BFDA36841424141C9B201330C20616018
+:1004A000A36000F071FD10BD100500209C04002029
+:1004B00008B5011C014801F0CFFF08BDEC04002085
+:1004C00008B5011C014801F0C0FF08BDEC04002084
+:1004D00000B58DB0021C084904A804F0D1FA04A9A3
+:1004E000684601F033FF6846FFF7EAFF684601F00F
+:1004F000E9FE0DB000BDC0460C51000000B58DB046
+:10050000021C084904A804F0BBFA04A9684601F0DB
+:100510001DFF6846FFF7D4FF684601F0D3FE0DB01B
+:1005200000BDC0461051000030B5134B85B01D789A
+:10053000124C002D0AD02068A16801F0EFFA01F0FA
+:100540001FFB201CF02101F03BFF12E00C48A268C9
+:10055000036821685B68984701F042FD291C6846E2
+:1005600001F0F4FE201C694601F022FF684601F00C
+:10057000A9FE05B030BDC0460C050020EC040020EB
+:100580009806002008B5054B0A201B78002B00D0E8
+:10059000F720FFF78DFFFFF7C7FF08BD0C05002010
+:1005A00010B5141CCAB01A0CD2B2031C00910194ED
+:1005B0000AA8084904F064FA0AA906A801F0C6FED0
+:1005C00006A8FFF77DFF06A801F07CFEFFF7DAFF23
+:1005D0004AB010BD14510000031C413B192B01D837
+:1005E000203004E0031C613B192B01D82038C0B235
+:1005F000704708B5FFF7F0FFFFF75AFFFFF7C2FF9C
+:1006000008BDFFFF10B5054C0021201C2C2204F072
+:1006100075F901235B42636010BDC046BC04002035
+:1006200010B572B6FFF7EEFF00240D4B0021E05825
+:100630001022043404F062F9142CF6D1094B4C2238
+:100640009A73094B53229A73084B47229A73084BAB
+:1006500041229A73074B4D229A7362B610BDC04671
+:10066000740200209C04002014050020FC040020DB
+:10067000AC0400207C04002010B50B20012100F008
+:10068000B5FC0C20012100F0B1FC0D20012100F08F
+:10069000ADFC1420002100F0A9FC0F20002100F087
+:1006A000A5FC0E20022100F0A1FC0E200C49042222
+:1006B00000F02CFC0B4B00221A700124FFF7B0FF56
+:1006C000094B211C0D201C7000F05EFC211C0B202E
+:1006D00000F05AFC0C20211C00F056FC10BDC04656
+:1006E000810400000C050020E804002030B50B2038
+:1006F00085B0012100F048FCFA24A400142000F089
+:1007000057FB051C1248FFF7B3FEFFF7E1FE202060
+:10071000FFF7CEFE28B2FFF7DBFEFFF733FFE12342
+:100720005B01013BFDD1013C002CE7D1211C0B20DA
+:1007300000F02AFC0749684601F008FE6846FFF70A
+:10074000BFFE684601F0BEFDFFF71CFF05B030BDDF
+:1007500010050020235100007FB5041E5A2C16D12D
+:1007600072B600F087FCB14B1860FFF74BFFB04B3F
+:1007700001221A7062B60025AE4B291C0B201D7099
+:1007800000F002FC0C20291C00F0FEFBECE05428D9
+:100790000FD1A949684601F0D9FD6846FFF790FEE0
+:1007A000684601F08FFDA148FFF762FEFFF7A6FE45
+:1007B00039E1502800D1D7E0442803D10A2000F0C5
+:1007C0007DFCD1E0031C313B082B0AD89748FFF78A
+:1007D0004FFE2F3C964B0122A4005242E0505A603B
+:1007E00023E1522811D1924A00245368591C5160C8
+:1007F000082902DCD91C89008C58981CFFF768FE78
+:100800003A20FFF755FE201CFDE0492819D1884BFE
+:10081000012252425A6000241D1C884B013BFDD12D
+:100820008248FFF725FEA300EB189860201C3130AA
+:10083000C0B20134FFF73CFEFFF7A4FE092CECD157
+:10084000F3E0462802D1FFF7EBFE8DE0562811D1E8
+:10085000FFF7C2FEFFF72CFE2020FFF729FE7849A4
+:10086000684601F073FD6846FFF72AFE684601F00E
+:1008700029FDD8E047280BD1724C2068FFF728FEED
+:10088000FFF780FE00232360A36001232373CCE0E5
+:10089000412801D16C4B1DE0422812D16348FFF77B
+:1008A000E7FD6A490022051C172001F049FC201CC5
+:1008B000FFF792FEFFF7FCFD2020FFF7F9FD281C53
+:1008C000A1E0532803D1172001F0CEFC4CE04D28C5
+:1008D00006D15F4B00221A609A6001221A7342E02F
+:1008E0004E2820D1514E5B4A316868468B180093E0
+:1008F000FFF7BEFD051C201CFFF76EFEFFF7D8FDBD
+:100900002020FFF7D5FD281CFFF7F8FDFFF73AFE82
+:10091000301CFFF7ADFDA842FAD34F4801F0B4F800
+:1009200001F02EF981E043280ED1142000F040FAA6
+:100930004A4C0123191C221C6E2800DC0023636032
+:1009400051731173432017E0632804D1434B0022F5
+:100950005A731A7308E0452801D1404C12E04C2824
+:1009600004D13F4B01225A73201C05E06C2806D1AC
+:100970003B4B00225A736C20FFF73BFE55E04A28A0
+:1009800009D1374CE36820686168A268FFF708FE68
+:100990000023A36049E0552804D1FFF72AFEFFF7A2
+:1009A000A5FE42E0512831D1FFF716FEFFF780FD8A
+:1009B0002C49684601F0CAFC6846FFF781FD68468D
+:1009C00001F080FC0F2000F0F3F9FFF781FD2649CC
+:1009D000684601F0BBFC6846FFF772FD684601F00F
+:1009E00071FC142000F0E4F9FFF772FD1F4968461E
+:1009F00001F0ACFC6846FFF763FD684601F062FC5D
+:100A00000E2000F0D5F9FFF763FD0CE018496846A9
+:100A100001F09CFC6846FFF753FD684601F052FC6C
+:100A2000201CFFF745FDFFF7ADFD7FBD1005002041
+:100A3000BC040020E804002027510000102E000014
+:100A40002A510000FC040020AC04002088130000A0
+:100A50007C040020F0D8FFFF09903C6314050020BF
+:100A60009C0400202C510000305100003C5100003B
+:100A700047510000F0B5624C87B021780D2000F09E
+:100A800083FA604D2B7B002B13D00F2000F090F9E0
+:100A9000E1239B0098420CDD5B48FFF7E9FCAB6863
+:100AA00001220133AB6000232B732378286053406D
+:100AB0002370564D2B7B002B11D0162000F078F9B7
+:100AC00014280CDD5048FFF7D3FCAB68012201333A
+:100AD000AB6000232B7323782860534023700120E0
+:100AE00001F068F8061C701E8641F6B20196002ED1
+:100AF00007D0474D2B7B002B03D0464B1F78002F90
+:100B00006ED0454D2B7B002B19D0142000F050F9EE
+:100B10006968002902D16E2802DC10E059280EDC39
+:100B20003948FFF7A5FCAB6801220133AB6023789D
+:100B30006E68534023707342734128606B600024D9
+:100B400072B6364B0021E3585A7B8A420BD09F681D
+:100B50008F4208DD324A1D1C101CC4CDC4C02D6854
+:100B600005609960012162B6002906D02C4B1868F7
+:100B700059689A68DB68FFF713FD0434142CDFD141
+:100B8000019E002E22D0234B1B78042B18D1254B1D
+:100B900002A8197801394B425941234BC9B2197047
+:100BA000002903D0F02101F015FC01E001F0CEFB9B
+:100BB00002A91E4801F0FCFB02A801F083FB05E03E
+:100BC000052B03D1174B1878FFF7C6FD01F066F926
+:100BD000002812D001F01AF9C0B2FFF7BDFD0CE0F9
+:100BE0000948FFF745FCAB6801220133AB6023786D
+:100BF000286053402F73237083E707B0F0BDC046D1
+:100C0000E8040020FC04002010050020AC040020B3
+:100C10007C040020860800201405002074020020B7
+:100C20008C040020880800200C050020EC04002023
+:100C300010B500F01FFA064B064C18600021201C6E
+:100C400001F084FB201C0449044A03F0EDFD10BDB3
+:100C500010050020EC040020C522000070020020D6
+:100C6000F0B51E4B1A6880231A40FAD172B61C4B9D
+:100C70001978002930D01B490C681B490F681B49A3
+:100C80003C190E681A49A4190D681A4964190868B4
+:100C900019492418096819486418A4B21849640845
+:100CA0000C43A4B20460174C174800682568174C21
+:100CB000AC462768164C60442668164CC019256857
+:100CC000154C801924684019001980B2400801436E
+:100CD000124889B201601A7062B6F0BD24B00340B8
+:100CE0002505002038B003403CB0034040B003402D
+:100CF00044B0034048B003404CB003402CB0034024
+:100D00000080FFFF5CB0034058B0034060B0034078
+:100D100064B0034068B003406CB0034030B003409F
+:100D20001B4B1C4A19781C4B082901D1002102E0F9
+:100D30000A2903D138211160132206E00C2901D1C0
+:100D4000342100E03C21116012221A60134B1A7802
+:100D5000134B002A01D0002200E001221A60114B3F
+:100D60001A78114B012A01D880220CE0042A01D8FC
+:100D7000842208E0082A01D8852204E0102A01D83C
+:100D8000862200E087221A60084B01221A70704701
+:100D90008802002008B003400CB003402705002063
+:100DA00020B003408902002024B003402505002024
+:100DB000031C70B50020272B32D81A4AD45CFF2CB4
+:100DC0002ED0194B1B78834201D0FFF749FF72B632
+:100DD000164B1022610605D51968914319603F230F
+:100DE0001C4002E019680A431A60114B114D0122A0
+:100DF0001C602A7062B61E1C72B633681A0608D5CB
+:100E00000D4B186800232B7062B60C4B1B781841F1
+:100E100006E02B78002BDBD062B601F05FFBEBE73E
+:100E200070BDC04659510000250500200CB003409C
+:100E300000B003402605002010B003402405002028
+:100E40007047FFFF38B5074B1C681C60064BDA681B
+:100E50001D1CA30700D590472B69620700D5984752
+:100E600038BDC046A09004408C02002038B5244B09
+:100E7000244C1D681D60636A2A0700D59847A36A41
+:100E8000EA0600D59847E36A6A0600D59847236BBF
+:100E90002A0600D59847636BAA0600D59847E36BEE
+:100EA000EA0700D59847A36DAA0700D59847E36DD8
+:100EB0006A0700D59847144B1D681D60A368EA07B0
+:100EC00000D5984763692A0600D59847A369EA06C2
+:100ED00000D59847E3696A0700D59847236A2A072F
+:100EE00000D59847A36BAA0700D59847236DAA069B
+:100EF00000D59847636D6A0600D5984738BDC0464F
+:100F0000A0B004408C020020A0C0044070B5041EB4
+:100F10001A2C2CD8042A2AD8101C02F0BDFE07096E
+:100F2000050B03000B2306E00A2304E0082302E07C
+:100F30000C2300E009230C2262430D480D4E821859
+:100F40000D485268051CB8352E600C4DBC3005604C
+:100F500072B60B481568A40028400A4D1060615114
+:100F60008021490408431B041843106062B670BD19
+:100F700084510000450E000000F9FF1F6D0E0000B7
+:100F8000FFFFF0FF8C02002010B51A2818D80C23A0
+:100F900058430C4A835810181C7D027A224205D00F
+:100FA000002901D01A710BE01A7209E043681A682F
+:100FB000002902D003210A4301E002218A431A607A
+:100FC00010BDC0468451000000231A280AD80C2303
+:100FD0005843054A81581018027A0B7C13405A1E58
+:100FE0009341DBB2181C704784510000F0B51A28F9
+:100FF0003DD80C273D1C45431D4A54196368161CF7
+:10100000012901D0042912D1784332583018147DB7
+:10101000007A20431075A22252001A60202204296F
+:1010200001D1196814E0196891430A1C1EE0AA58FE
+:10103000247A107DA0431075002902D08A1E012A4F
+:1010400012D8802252001A60022903D11A680321A3
+:101050000A430BE003290AD11A6802210A431A60E5
+:101060001A6801218A4301E00422FF321A60F0BDB0
+:101070008451000072B60C4B0C491A680C4B1B686B
+:10108000096862B6480104D532219142894149423A
+:101090005B18FA20800043430648821A0648424300
+:1010A000920D98187047C04618E000E004ED00E08B
+:1010B000280500207FBB00005555010038B5041CF1
+:1010C000FFF7D8FF051C002C0FD0FFF7D3FF074B0D
+:1010D000401B984206D9013C002C06D0FA239B0005
+:1010E000ED18F2E701F0FAF9EFE738BDE703000089
+:1010F0001D4B8022D20510B51A60802212061A609C
+:101100001A4B002018601A491A4B1B4A19602823F1
+:1011100013601A4A1A4C13601A4A13601A4A136071
+:101120001A4A13601A4A136009222260194C20607F
+:10113000194C2160194C2360194C2360194C226012
+:10114000194C20601948016019490B6019490B605E
+:10115000194B1A60FFF7E4FDC8204000FFF7AEFF0F
+:1011600000F0D8FB10BDC04600E100E00480034061
+:10117000FFBF0000088003400C8003401480034040
+:10118000008003401C800340248003402C800340E7
+:101190003480034004900340089003400C900340C7
+:1011A000149003400090034004A0034008A00340B3
+:1011B0000CA0034014A0034000A00340F0B50B4F67
+:1011C00002223B780A4E1A43D2000A4DB4186060DE
+:1011D00028788824002800D0C82409040C43B4507F
+:1011E00001225040534028703B70F0BDA005002004
+:1011F00000F8FF1F20060020431E10B5042B0FD857
+:1012000072B6084A99008858002807D044685B00E5
+:101210008C50054A01889C5A611A995262B600E0C6
+:10122000002010BD8C0500207C080020431E0020FB
+:10123000042B09D872B6054A9B009B58002B02D09C
+:1012400001305B68FAE762B67047C046E4050020EB
+:1012500038B572B60123164A9A18013A1278110766
+:101260001CD5144A5C01A5589900002D05D113190D
+:1012700008305860104BA3500AE001242143C900F4
+:101280008C58002C0AD15318083058600B4B8B50E7
+:101290000B4B1A78013A1A7062B608E00133062B3C
+:1012A000D9D162B6064B00221A7000F0A7FB38BDF8
+:1012B000F052000000F8FF1F88004000C800400006
+:1012C00016060020421E10B5042A30D802238000E2
+:1012D0001843174BC0001B1872B6164CA05C0328AD
+:1012E00009D802F0D9FC140216050833022011E0D7
+:1012F000083304200EE01048920014580F4B002CC5
+:1013000001D1115001E09858416099500EE003203E
+:1013100000E00520A0540A1C08325A6088221807F1
+:1013200000D5C822098809040A431A6062B610BDB4
+:1013300000F8FF1F44050020E4050020F805002008
+:10134000F7B5B6490C780E1CE4B262071FD5B44B52
+:101350001B78002B18D0B34A1378DBB2002B05D0D2
+:10136000013BDBB21370002B00D100BEAE4A1378F4
+:10137000DBB2002B06D0013BDBB21370002B01D196
+:1013800000F05CFE00F0FCFBA44D04232B7008254C
+:10139000221C2A4000D154E2A44BA54C1B78DBB29E
+:1013A0001E099A08002E00D0DDE1D50065192F68CE
+:1013B0006A68B806000F01380C2800D9D0E102F0A5
+:1013C00075FC93019301CF01CF01CF01CF01CF0174
+:1013D000CF01AC01CF01CF01CF010D0095491368BA
+:1013E000954F526895482960954D964900263B6077
+:1013F0007A609BB201222E602661A66102708B4248
+:1014000018D8D021C9008B4200D3E5E0812191405A
+:101410008B4200D1C2E006D8802B00D1A9E0822BFC
+:1014200000D1ABE0F5E0884A934200D1C5E0A022AC
+:10143000D20017E085498B4200D152E10DD88821B6
+:1014400009018B4200D18EE090221201934200D01C
+:10145000DFE0BA78724B1A7009E07D4A934200D1FE
+:10146000CFE07C4A0026934200D115E1D1E0A319D8
+:10147000196A090603D5586A083800F0BFFA083619
+:10148000A02EF4D100252E1C734AA858002805D0A0
+:101490004368019300F0B2FA0198F7E76E496F4B89
+:1014A0006F4A4851E850A858002805D04368019376
+:1014B00000F0A4FA0198F7E76A4B6B4AE850730012
+:1014C000D0526749694B4851F05C0238032807D86D
+:1014D00002F0E2FB02040204002200E001229A551D
+:1014E00001360435052ECFD1614A00231370012542
+:1014F000161C604BEA18604B9200EB18013B1B78FE
+:101500001370190720D500F05BFA6B01002805D095
+:10151000E21808305060594AE25003E0E050337856
+:101520000133337000F04CFA0122AB001343DB00AF
+:10153000002805D0E218083050603E4AE25003E02F
+:10154000E050337801333370AB0002221A43D200EB
+:101550000021A1500322134393400135E150062D91
+:10156000C7D10E1C98E02E4B454C1B78161C2370DF
+:1015700093E0434C2670667002268EE0BB88052BF4
+:1015800047D83F4C3F4D9B00267066705B191B7817
+:101590000226334200D180E022707EE0BA887F23A9
+:1015A0001340052B35DC7E88002E32D135499B0057
+:1015B0005B181A7802218A4329E0BA887F231340F6
+:1015C000052B26DC7E88002E23D12E4A9B009B18FB
+:1015D0001A7802210A431AE07A88B9882A4B5C6893
+:1015E000002C16D01888904209D15888884206D11C
+:1015F000120A032A01D126784FE01E894DE00C33F0
+:10160000EDE7224B1A68224B1A60BA78214B1A7008
+:1016100042E01C4B0F221A7063E0C0468020074056
+:101620001F060020A10500205C0600209020074036
+:1016300000F8FF1FC80040000C0600202006002014
+:101640002C0500208106000002030000212000007C
+:1016500021220000212300008C05002030050020FD
+:10166000E4050020F80500207C0800204405002047
+:101670001606002030C80110F0520000880040001B
+:1016800017060020C0200740F8520000280500205F
+:10169000D00800206C060020341CFB889E4200D934
+:1016A0001E1C371E402F00D94027201C391CFFF775
+:1016B00085FDE419F61B03D1351C402F04D010E042
+:1016C0004025B54200D9351C201C291CFFF776FDAA
+:1016D0006419761B01D1402D03D1754D754B2C60DB
+:1016E0001E800122744B3AE0744B1988744B994266
+:1016F00010D10023D05C7349C8540133072BF9D1B2
+:101700000B68862B02D1704B0F221A700020011C2F
+:10171000FFF754FD6D4B2B6022E0654F3D68002DB7
+:1017200013D064490E88341E402C00D94024281C54
+:10173000211CFFF743FD361B5E4AB6B21680002E11
+:1017400001D1402C00D12E193E605C4BA02219889B
+:10175000D200914204D100225A709A785C4B1A70E0
+:101760000122554B61E0D200A4186068013E0838A0
+:10177000F6B22B4036D000F041F95649B00042583D
+:10178000554B002A1BD055684550111C985D0831F7
+:10179000616003280BD802F07FFA020406080321D7
+:1017A00004E0022102E0052100E00421995588238C
+:1017B000270700D5C8231288120413433CE0985D24
+:1017C000032808D802F068FA39390204002200E040
+:1017D00001229A5531E02C40624262410324A41A4E
+:1017E0009C552AE062880280002A20D03B4D43802D
+:1017F0004360B3005F593A49002F01D1585101E0CD
+:10180000CD586860C850374B7600F15A5218F252E2
+:1018100000F0D6F8002804D00830606021070AD50F
+:1018200007E0314B20601A7801321A7005E02C4233
+:1018300001D0264B00E02D4B23602D4A0823137066
+:101840007FE5012004421ED01B4902230B70294B67
+:10185000264C1A70284B294D1C609C60284C1A613C
+:10186000DC609A615D60274B0D241C70264CFF23C1
+:101870002370174C33702270244A1370244B9F221C
+:101880001A70087013E063B2002B04DA1D4B0D22AE
+:101890001A708023337002231C4204D01A4A117834
+:1018A000C9B21170337010231C4200D03370F7BDE1
+:1018B0002C05002014060020942007400C06002070
+:1018C00021200000C8080020A1050020C800400019
+:1018D00098200740E4050020440500208C050020E6
+:1018E000300500207C0800201606002088004000FB
+:1018F00080200740A005002000F8FF1F4C050020B5
+:10190000A4050020C0200740882007408C20074005
+:101910008420074010B501F0D9FE00221A4B0021A7
+:10192000D150981808324160A82AF7D1174A802070
+:101930001468C00220431060180A154AC0B2107023
+:10194000180C144AC0B21070134A1B0E1370134ABD
+:10195000FF231370124A13481370134A13700122A5
+:1019600002701248017012490A70124A11689943B4
+:101970000B1C70210B4313600F4B802252041A6022
+:101980000E4B10221A7010BD00F8FF1F3480044067
+:101990009C200740B0200740B4200740802007402B
+:1019A000882007409420074010200740002107406E
+:1019B0008420074018E400E000E100E0082107402F
+:1019C00038B572B60B4C2568281C02F003FA1D28A6
+:1019D00002D962B600200CE080231B06C3409D4361
+:1019E000256062B648235843034B18180023036050
+:1019F000436038BDF802002000FAFF1F10B5041C38
+:101A00000E484821201A02F05BF91D2815D80C4B0E
+:101A10001B78002B07D00B4B1B78002B03D0201C0E
+:101A2000FFF716FC09E072B68022074B1206C2408F
+:101A3000101C1A681043186062B610BD00FAFF1F30
+:101A4000160600201F060020F802002038B50B4CB7
+:101A5000051C21783C2908D1094B1B68002B04D0B8
+:101A60000848002298470023237023783B2B03D893
+:101A70005A1C2270034AD55438BDC046870800203E
+:101A80004C0600208B080020F8B51F4D1F4C012389
+:101A90002B702368061C002B1DD11D4F1D4B1B787E
+:101AA000002B2FD00420FFF7C1FB052804D9013FEC
+:101AB000194B002F06D108E0FFF782FF20600028B5
+:101AC000F5D008E01A78002A02D001221A7019E035
+:101AD00000F004FDE2E7104B002221681A704A88EA
+:101AE000531C92008A1896600F2B01D84B8007E098
+:101AF00040230B800420FFF7E5FBFFF761FF206028
+:101B000000232B70F8BDC0463C0600204406002090
+:101B1000E14200001F0600204806002038B5041CE2
+:101B20000D1C032D0ED92078637800021B04184386
+:101B300004231843A378033D1B061843FFF7A4FFB3
+:101B40000334EEE7032D09D12078637800021B04EB
+:101B5000184307231843A3781B060DE0022D06D176
+:101B60002078637800021B041843062304E0012D4B
+:101B700005D12078052300021843FFF785FF38BD03
+:101B800010B50A4B1B78002B0ED1094C2168002997
+:101B90000AD04B88002B07D09B000B800420FFF756
+:101BA00091FBFFF70DFF206010BDC0463C060020F2
+:101BB00044060020F8B5834C071C2368251C002B25
+:101BC00012D1814B1B78002B00D1E2E00520FFF7FA
+:101BD00013FB2060002800D1DBE00688002E03D133
+:101BE000FFF70CFF2660D4E0286843889A0892002B
+:101BF0008218946802880433934201D2438005E03E
+:101C0000FFF7FCFE0520FFF7F7FA28600F23251CDD
+:101C10001D40200A061C2A1C1E40083A0136062ACE
+:101C200000D980E0002F02D0B74200D0B1E0220BF3
+:101C30001340082D0AD1082B00D0AAE0634B0022E4
+:101C40001A70634B1B68934232D163E0092D1AD19D
+:101C5000092B00D09DE0220E5C4B210C002A09D0FC
+:101C6000012018705B4B1B68002B53D0F0B2C9B237
+:101C7000D2B206E01A70564B1B68002B4AD0F0B265
+:101C8000C9B2984746E00A2D07D10A2B00D080E060
+:101C90004E4B02221A70504B07E00B2D0DD10B2B2F
+:101CA00077D14A4B03221A704C4B1B68002B31D062
+:101CB000210CF0B2C9B2220EE3E70C2D06D10C2B99
+:101CC00067D1424B04221A70454B07E00D2D0DD110
+:101CD0000D2B5ED13D4B05221A70424B1B68002B29
+:101CE00018D0210CF0B2C9B2984713E00E2D50D194
+:101CF0000E2B4ED1354B06221A703B4B1B68002B26
+:101D000008D0FE21620C890111406202520EF0B22D
+:101D100011439847354B220C1E70354B240E1A7018
+:101D2000344B1C704CE0042D08D1C0B2FFF78EFE7E
+:101D3000200CC0B2FFF78AFE200E28E06B1F022B9A
+:101D40001ED8C0B2FFF782FE052D08D0200CC0B20D
+:101D5000FFF77CFE072D02D1200EFFF777FE264B02
+:101D6000234A1978117000221A70184B07221A7032
+:101D7000224B1B68002B23D02148012298471FE0EB
+:101D80000F2D12D11C4B1B78002B04D0C0B2FFF7D3
+:101D90005DFE002015E00D4B08221A70194B1B68E0
+:101DA000002BB7D0C0B29847B4E7022DF1D1074B52
+:101DB00009221A70144B1B68002B01D0200C984785
+:101DC0000120F8BD500600201F06002086080020D4
+:101DD0002806002058060020340600205406002063
+:101DE0002C06002038060020300600208A0800203B
+:101DF0008808002089080020870800204C06002061
+:101E00008B080020400600202406002038B5114C25
+:101E10002368002B0CD020684288531C8218157A46
+:101E20000288934212D3FFF7E9FD002323600EE0FE
+:101E3000094B1B78002B02D10120404208E0022010
+:101E4000FFF7DAF920600028E5D1F5E74380281C88
+:101E500038BDC046680600201F06002010B50D4C96
+:101E60002368002B03D02368002B0ED103E00A4B1C
+:101E70001B78002B02D10120404209E00220FFF72D
+:101E8000BBF920600028EED1F5E75A889B18187A34
+:101E900010BDC046680600201F060020054B58886C
+:101EA000054B1B68002B03D01A885B88D31AC01817
+:101EB0007047C0467C0800206806002010B50A4B19
+:101EC0001B78002B0FD0094C2068002803D0FFF7A7
+:101ED00095FD002323600220FFF78EF9002802D031
+:101EE000FFF78CFDF7E710BD1F06002068060020F5
+:101EF000F7B5304D0123071C0C1C2B70002C56D05D
+:101F00002D4E3068002827D12C4A01922C4B1B788B
+:101F1000DBB2002B03D12B700120404249E00320AB
+:101F2000FFF784F9072808D801232B70FFF748FD35
+:101F3000214B186000280FD128700198214B0138DF
+:101F40000190002802D01A78002A02D001221A70CB
+:101F5000E2E700F0C3FAD9E71A4B002231681A70A1
+:101F60004B884020C01A0094844200D90090181C6D
+:101F7000009A08309B1808184B800190A41A00237F
+:101F80000098834204D0F85C019AD0540133F7E7FB
+:101F9000FF184B883F2B06D940230B800320FFF707
+:101FA00091F900233360084B05221A70A6E72C70C4
+:101FB000201CFEBD5D0600206006002009750000A3
+:101FC0001F060020640600205C06002007B56B4653
+:101FD000D8710733181C0121FFF78AFF0EBDFFFFE0
+:101FE000F8B5124B1B78002B1ED0114F114B3C68DB
+:101FF00001261E70104D002C09D000262E7063881B
+:1020000003202380211CFFF75DF93E6009E0FFF704
+:10201000D7FC011E04D003202C70FFF753F900E019
+:102020002E70044A00231370F8BDC0461F0600201E
+:10203000600600205D0600205C06002038B50E4BCF
+:102040001D78EDB2002D15D10C4C2168002906D069
+:102050004B8803200B80FFF735F925600AE0FFF776
+:10206000AFFC011E03D00320FFF72CF902E0044B64
+:1020700001221A7038BDC0465D06002060060020AF
+:102080005C06002010B5041C01F0FCFD201C10BDF6
+:10209000214B70B518681E1C2049214A002809D020
+:1020A000127809680A71421E511D01D10322524261
+:1020B0001A6030E00B6812781A4D1A702B68002BEA
+:1020C00023D0194B00211C68201C01F065FE00285C
+:1020D00016D1201C154901F069FE002810D11449C1
+:1020E000201C02F091F8FC21890502F0B7F901F0FB
+:1020F0008FFE021E272A03D90E480F4901F0AAFDC0
+:102100002B68336000232B6005E00A4801F0BAFD1C
+:102110000A4BFF221A7070BD900600208406002032
+:102120008C0600208806002094060020ABAA2A4DC9
+:102130000000C0417006002091200000FC02002039
+:10214000F8B5051C0C1C1A2874D8002A08D0101CDD
+:102150004843FA21890001F0B3FD4600013601E051
+:1021600003267642201C02F00FFB011C324801F0CE
+:1021700067FE0C236B43314A041CD3185F6872B6A8
+:102180002F4B19788D4215D12E4D296801F0F4FDA1
+:102190002D4B002806D0186801220240013A9619FA
+:1021A0001E6046E0294A1968166001220A402C6028
+:1021B0001A603EE0264B1A2903D826491868097888
+:1021C00001701F490D700C214D4351595519081CC0
+:1021D000083018601F4A287A1070087272B61B689F
+:1021E0001278197B0A431A7362B6A2235B003B6024
+:1021F000154B201C1E60134B00211C6001F0CCFD10
+:10220000002816D1201C144901F0D0FD002810D15F
+:102210001249201C01F0F8FFFC21890502F01EF98B
+:1022200001F0F6FD021E272A03D90D480D4901F0E1
+:1022300011FD62B6F8BDC0460024F4488451000088
+:10224000FC02002094060020900600208806002052
+:10225000840600208C060020ABAA2A4D0000C04155
+:10226000700600209120000010B51A280FD872B611
+:10227000074C2378834209D1064801F003FD064B41
+:10228000064A1B6812781A70FF23237062B610BDCD
+:10229000FC02002070060020840600208C0600202E
+:1022A00008B505480023037004498023044A43739A
+:1022B00002F0BAFA08BDC046700600208520000072
+:1022C0007002002010B5041C006802F0EBFA201C1C
+:1022D00010BD38B5041C0D1C0068013102F0B2FBC2
+:1022E000002801D021C4012038BD436810B5041C6A
+:1022F0008B4208D2FFF7EDFF002805D0A368002B22
+:1023000001D122681370012010BDF8B5041C0F1C08
+:10231000151E05D10368002B00D01A7000230DE0B4
+:10232000111CFFF7E2FF061C2068002E08D10028D0
+:1023300002D002F0B7FA266000236360A36003E0D6
+:10234000A560391C02F0BEFB201CF8BD38B5002387
+:10235000041C0D1C0360436083600373994207D023
+:10236000081C02F0B7FB291C021C201CFFF7CDFF44
+:10237000201C38BD38B5051C00680C1C00280CD08A
+:102380006A688B689A4206D3096802F09BFBA268D0
+:102390000023AA600AE002F085FA236862682B60D5
+:1023A000A3686A60AB60002323606360A36038BDEC
+:1023B00010B5041E8C4201D0FFF7DCFF201C10BDBD
+:1023C00007B5002201AB19705A70191C0122FFF7E2
+:1023D0009CFF0EBD10B50023041C036043608360A6
+:1023E0000373FFF7EDFF201C10BDF7B50368171C42
+:1023F0000022041C0D1C86680092994206D39A198B
+:10240000914203D2CB1A019301230093002F1AD0DB
+:10241000BE19201C311CFFF768FF002813D0009B59
+:10242000A0682168002B09D0019A081889183A1C65
+:1024300002F042FA22680023935503E00818291C91
+:1024400002F040FBA660201CFEBD08B50B1C9A687C
+:102450000968FFF7CAFF08BD13B5002201AB041CD1
+:1024600019705A70191C0122FFF7BFFF201C16BDFE
+:1024700008B5FEF701F9FEF7FDFA00F02FF8FAE7CC
+:1024800008B5FFF70BFD08BD08B5FFF7BFFC08BD99
+:1024900008B5FFF7E3FC08BD08B5FFF7A1FD08BDCF
+:1024A00008B5FFF70BFD08BD08B5081CFFF78EFD4A
+:1024B00008BD08B5081C111CFFF71AFD08BD7047C0
+:1024C000044B00221A711A73FA21034A8900996099
+:1024D0001A60704798060020D052000010B5104CCA
+:1024E0002378002B1BD101232370FFF7D7FC002892
+:1024F00001D0FFF7E4FF00F0E5FA002801D001F079
+:10250000AFF900F02FFD002801D001F049FA00F0EA
+:10251000F5FF002801D001F0E3FA0023237010BD7D
+:10252000A8060020024B1A6801321A607047C046A4
+:102530002805002010B50B4C23685A0301D5FEF77F
+:10254000FFFE23685A0501D500F02EFB23681A050B
+:1025500001D500F079FD2368DA04ECD501F040F8EC
+:10256000E9E7C0463480044008B5FFF7E3FFFFFF0A
+:10257000014B00221A607047008104407047FFFF42
+:10258000024A136818181060181C704700030020D6
+:10259000EFF31380002821D1EFF31082002A1FD11E
+:1025A000EFF30583002B0FD0101C0F2B0ED91A1C34
+:1025B000103A92080B4903209200034052188340BE
+:1025C0001068D840C0B201E080204000EFF31182D3
+:1025D000002A05D0824203D2101C01E001204042B3
+:1025E0007047C04600E400E0F8B5061C0D1C171C3F
+:1025F0000024301C391C01F0A7FB092901D8303117
+:1026000000E03731C9B22955301C391C01F058FBA4
+:10261000061E01D00134ECE72B1958702B1C5A1BF5
+:10262000A24206DA1A78295D19702A550133013C55
+:10263000F5E7281CF8BDFFFF264B80221968D20061
+:102640000A431A60244A00231370244A1370244A50
+:102650001370244A1370244A1370244B1B78032BE5
+:1026600009D0152B04D0002B08D1214A214B04E0BE
+:102670001F4A214B01E0214A214B1A60214B1B7854
+:10268000042B0BD0052B05D0012B0BD1D1229200AE
+:102690001D4B06E0D12292001C4B02E091221C4B04
+:1026A00092001A601B4BC204D20E1A70C0B20022F4
+:1026B00058709A702C22DA70174BFF2219689143D8
+:1026C0000A1C40210A431A60144B802252011A60EE
+:1026D0007047C0463480044035070020B106002012
+:1026E0003C07002036070020B0060020F20600203C
+:1026F0001303000040A0044018C00440130200006F
+:10270000049004400403002044A004401CC0044082
+:102710000890044000A006400CE400E000E100E066
+:1027200010B5174B132299789143032202400A43B4
+:10273000410701D510210A439A700F221049024027
+:10274000042A03D18A79402422438A715A791021BC
+:102750008A43084200D00A435A7199791022914362
+:10276000840600D51143064A9971C10506D55378F0
+:1027700011782020DBB201431170537010BDC046A8
+:1027800000A0064008B5124B1B685A051ED5114B18
+:102790001B78DBB2002B02D0FFF7A0FEF7E70E4A52
+:1027A0008021490111600D4A0D49D3700422FF3286
+:1027B0000A600C490A600C4A13700C4A13700C4BE7
+:1027C0001B68002B02D00B4A12781A7108BDC04654
+:1027D00034800440B006002080E100E000A0064004
+:1027E00040A0044044A0044035070020B10600206A
+:1027F000AC060020F306002038B5041C0B4B1D78F6
+:10280000EDB2002DFAD1201C0121FEF7EFFB291CAF
+:10281000201CFEF7B9FB0C225443054B054AE15836
+:102820001C191160044B227A1A7038BDB0060020C2
+:1028300084510000380700203407002030B50029FB
+:1028400001D080231843224A1378141C98423ED0AA
+:10285000204A1268550539D57F221340042B0FD02A
+:1028600004D8012B0FD100221B4B0BE0052B04D009
+:10287000182B08D10022194B04E00022184B01E06C
+:10288000184B00221A604423002900D060237F22C5
+:102890000240042A15D006D8012A17D1C02292007E
+:1028A00013430D4A11E0052A06D0182A0ED18022C2
+:1028B000D2001343094A08E0C02292001343084A99
+:1028C00003E0802292001343064A1360207030BD5B
+:1028D000040300203480044044A0044050D004404D
+:1028E0001CC004400890044010B51A4A137898425E
+:1028F0002ED0194909684C0529D5032B0FD004D8CF
+:10290000002B0FD115490B600CE0152B04D0192BAF
+:1029100008D10021124B04E00021124B01E0124BC0
+:102920000021196003280FD004D800280FD10F49C7
+:102930000A4B0BE0152804D0192808D10C49084B84
+:1029400004E00A49074B01E00A49074B196010707F
+:1029500010BDC046F20600203480044040A0044070
+:1029600054D0044018C004400490044013030000F5
+:10297000130400001302000038B50E4B1A68002340
+:10298000510514D50C4C1A2810D80C2343430B4A7C
+:102990000B4D9958D3181B7A216001212B70FEF73B
+:1029A00025FB23682A781A72012300E02360181C93
+:1029B00038BDC04634800440AC060020845100007D
+:1029C000F306002000207047F8B5214B071C1B6858
+:1029D0005A053BD51F4B1B68002B02D01E4A1278AC
+:1029E0001A711E4E002334783F220134A2425B410B
+:1029F0005B421C401A4D2B78A3421CD1FFF7C8FD47
+:102A0000402813DC174A13795BB2002BF2DA2B78DB
+:102A1000002101333F209842494149420B4012496D
+:102A2000C95CDBB2C9B2D1712B70E3E7FF28E1DDED
+:102A3000FFF754FDDEE70C4BFFB21F550B4B012295
+:102A40001A70084BE4B2AC223470DA70F8BDC0469C
+:102A50003480044038070020340700203C07002061
+:102A60003607002000A00640B2060020B006002075
+:102A700038B5041C4518AC4204D02078FFF7A4FFF9
+:102A80000134F8E738BDFFFF08B5044B1B78002B75
+:102A900002D0FFF723FDF8E708BDC046B0060020CE
+:102AA000064B074A1B781078DBB2C0B2834201D3D1
+:102AB0003F3000E00138C01A7047C0463C07002094
+:102AC00036070020054B1878054BC0B21B78DBB2E7
+:102AD000984200D24030C01A7047C04635070020E7
+:102AE000B106002010B5124B12491A780B78D2B2F9
+:102AF000DBB29A4219D001333F2400209C4240416E
+:102B0000404203400C48DCB2C05C0C700B49C0B2C0
+:102B1000096800290BD09A4200D24032D31A1A2BEE
+:102B200005DC074B1B780B7201E00120404210BD11
+:102B300035070020B1060020F4060020AC06002076
+:102B4000F30600200A4B1A780A4B1B78DBB29A4234
+:102B50000AD0013300223F21994252415242134090
+:102B6000054AD05CC0B201E0012040427047C04637
+:102B700035070020B1060020F4060020064B1A7825
+:102B8000064BD2B21A70064B1B68002B02D0054AC6
+:102B900012781A727047C046B1060020350700202F
+:102BA000AC060020F306002030B5254A13799906BB
+:102BB00012D52449D4790B78002001333F259D425A
+:102BC0004041404203402048E4B20078834203D0B1
+:102BD0001E48C454DBB20B70D178194CC9B24BB249
+:102BE000002B1ADA23795BB2002B16DA184B19483E
+:102BF0001D780378DBB29D4202D16C23E3700CE0B8
+:102C0000013300243F259D42644164422340124C1D
+:102C1000E45CDBB2E4B2D4710370402319420ED0FD
+:102C2000117919420BD00D4B002119700C4B1B6808
+:102C30008B4202D00B49097819722C23D37030BD16
+:102C400000A0064035070020B1060020F406002051
+:102C50003C07002036070020B2060020B006002006
+:102C60003807002034070020164B80221968120113
+:102C70000A431A60144A00231370144A144913704B
+:102C8000144A1370144A1370144A1370144A1160D2
+:102C9000144AD12189001160134AC104C90EC0B27F
+:102CA0001170507093702C23D370104B104A196818
+:102CB0000A408021C9010A431A600E4B802292010A
+:102CC0001A60704734800440B00700204507002098
+:102CD00013030000B8070020B107002044070020BC
+:102CE0000CB0044010B0044000B006400CE400E01A
+:102CF000FF00FFFF00E100E010B5174B13229978A9
+:102D00009143032202400A43410701D510210A439F
+:102D10009A700F2210490240042A03D18A79402474
+:102D200022438A715A7910218A43084200D00A430B
+:102D30005A71997910229143840600D51143064AAD
+:102D40009971C10506D5537811782020DBB2014373
+:102D50001170537010BDC04600B0064008B5124B4C
+:102D60001B681A051ED5114B1B78DBB2002B02D055
+:102D7000FFF7B4FBF7E70E4A8021890111600D4A85
+:102D80000D49D3700422FF320A600C490A600C4AD4
+:102D900013700C4A13700C4B1B68002B02D00B4AAB
+:102DA00012781A7108BDC0463480044044070020E0
+:102DB00080E100E000B006400CB0044010B00440D8
+:102DC000B007002045070020400700206E070020C4
+:102DD00038B5041C0B4B1D78EDB2002DFAD1201C28
+:102DE0000121FEF703F9291C201CFEF7CDF80C2267
+:102DF0005443054B054AE1581C191160044B227AD3
+:102E00001A7038BD4407002084510000B407002028
+:102E1000AF0700207047704738B50E4B1A68002383
+:102E2000110514D50C4C1A2810D80C2343430B4A17
+:102E30000B4D9958D3181B7A216001212B70FEF796
+:102E4000D5F823682A781A72012300E02360181C41
+:102E500038BDC04634800440400700208451000043
+:102E60006E07002000207047F8B5214B071C1B6837
+:102E70001A053BD51F4B1B68002B02D01E4A127847
+:102E80001A711E4E0023347827220134A2425B417E
+:102E90005B421C401A4D2B78A3421CD1FFF778FBF4
+:102EA000402813DC174A13795BB2002BF2DA2B7837
+:102EB0000021013327209842494149420B401249E1
+:102EC000C95CDBB2C9B2D1712B70E3E7FF28E1DD49
+:102ED000FFF704FBDEE70C4BFFB21F550B4B012243
+:102EE0001A70084BE4B2AC223470DA70F8BDC046F8
+:102EF00034800440B4070020AF070020B80700204A
+:102F0000B107002000B0064046070020440700201B
+:102F100038B5041C4518AC4204D02078FFF7A4FF54
+:102F20000134F8E738BDFFFF08B5044B1B78002BD0
+:102F300002D0FFF7D3FAF8E708BDC04644070020E7
+:102F4000064B074A1B781078DBB2C0B2834201D32C
+:102F5000273000E00138C01A7047C046B80700208B
+:102F6000B1070020054B1878054BC0B21B78DBB2C7
+:102F7000984200D24030C01A7047C046B0070020C7
+:102F80004507002010B5124B12491A780B78D2B2BF
+:102F9000DBB29A4219D001333F2400209C424041C9
+:102FA000404203400C48DCB2C05C0C700B49C0B21C
+:102FB000096800290BD09A4200D24032D31A1A2B4A
+:102FC00005DC074B1B780B7201E00120404210BD6D
+:102FD000B0070020450700206F07002040070020B1
+:102FE0006E0700200A4B1A780A4B1B78DBB29A4214
+:102FF0000AD0013300223F219942524152421340EC
+:10300000054AD05CC0B201E0012040427047C04692
+:10301000B0070020450700206F070020064B1A78F4
+:10302000064BD2B21A70064B1B68002B02D0054A21
+:1030300012781A727047C04645070020B00700207A
+:10304000400700206E07002030B5254A1379990605
+:1030500012D52449D4790B78002001333F259D42B5
+:103060004041404203402048E4B20078834203D00C
+:103070001E48C454DBB20B70D178194CC9B24BB2A4
+:10308000002B1ADA23795BB2002B16DA184B194899
+:103090001D780378DBB29D4202D16C23E3700CE013
+:1030A0000133002427259D42644164422340124C91
+:1030B000E45CDBB2E4B2D4710370402319420ED059
+:1030C000117919420BD00D4B002119700C4B1B6864
+:1030D0008B4202D00B49097819722C23D37030BD72
+:1030E00000B00640B0070020450700206F07002011
+:1030F000B8070020B1070020460700204407002041
+:10310000B4070020AF070020204B8022196852012D
+:103110000A431A601E4A002313701E4A13701E4A87
+:1031200013701E4A13701E4A13701E4B1B78062B19
+:1031300004D0072B05D11C4A1C4B01E01A4A1C4B3A
+:103140001A601C4B1B78082B05D0142B07D1D122F9
+:103150009200194B02E0D122184B92001A60184BD2
+:10316000C204D20E1A70C0B2002258709A702C227B
+:10317000DA70144B144A19680A408021C9030A43C3
+:103180001A60124B8022D2011A60704734800440CA
+:103190002C080020C1070020340800202D08002042
+:1031A000C0070020050300201303000008C00440EE
+:1031B00010C004400603002014C004400CC00440AA
+:1031C00000C006400CE400E0FFFF00FF00E100E06B
+:1031D00010B5174B132299789143032202400A43FA
+:1031E000410701D510210A439A700F22104902406D
+:1031F000042A03D18A79402422438A715A79102102
+:103200008A43084200D00A435A71997910229143A7
+:10321000840600D51143064A9971C10506D5537835
+:1032200011782020DBB201431170537010BDC046ED
+:1032300000C0064008B5124B1B68DA041ED5114BBE
+:103240001B78DBB2002B02D0FFF748F9F7E70E4AF4
+:103250008021C90111600D4A0D49D3700422FF324B
+:103260000A600C490A600C4A13700C4A13700C4B2C
+:103270001B68002B02D00B4A12781A7108BDC04699
+:1032800034800440C007002080E100E000C0064018
+:1032900008C004400CC004402C080020C1070020D6
+:1032A000BC070020EA07002038B5041C0B4B1D7832
+:1032B000EDB2002DFAD1201C0121FDF797FE291C4B
+:1032C000201CFDF761FE0C225443054B054AE158D2
+:1032D0001C191160044B227A1A7038BDC0070020F7
+:1032E00084510000300800202B08002030B5002950
+:1032F00001D080231843154A1378141C984224D017
+:10330000134A1268D5041FD57F221340082B04D01E
+:10331000142B05D100220F4B01E00F4B00221A6045
+:103320004423002900D060237F220240082A06D0CF
+:10333000142A09D1C02292001343064A03E0C02296
+:1033400092001343044A1360207030BD060300202E
+:103350003480044014C004400CC0044010B50F4A2F
+:103360001378984218D00E490968CC0413D5062B5F
+:1033700004D0072B05D100210A4B01E00A4B0021A4
+:103380001960062804D0072805D10849054B01E03B
+:103390000649054B1960107010BDC046050300209A
+:1033A0003480044008C0044010C0044013030000EF
+:1033B00038B50E4B1A680023D10414D50C4C1A28CA
+:1033C00010D80C2343430B4A0B4D9958D3181B7A42
+:1033D000216001212B70FDF709FE23682A781A72FB
+:1033E000012300E02360181C38BDC046348004402F
+:1033F000BC07002084510000EA070020002070472D
+:10340000F8B5214B071C1B68DA043BD51F4B1B6822
+:10341000002B02D01E4A12781A711E4E00233478F7
+:1034200027220134A2425B415B421C401A4D2B789B
+:10343000A3421CD1FFF7ACF8402813DC174A1379DC
+:103440005BB2002BF2DA2B7800210133272098425F
+:10345000494149420B401249C95CDBB2C9B2D17142
+:103460002B70E3E7FF28E1DDFFF738F8DEE70C4BD0
+:10347000FFB21F550B4B01221A70084BE4B2AC226D
+:103480003470DA70F8BDC046348004403008002043
+:103490002B080020340800202D08002000C0064022
+:1034A000C2070020C007002038B5041C4518AC42F4
+:1034B00004D02078FFF7A4FF0134F8E738BDFFFF00
+:1034C00008B5044B1B78002B02D0FFF707F8F8E78C
+:1034D00008BDC046C0070020064B074A1B7810787D
+:1034E000DBB2C0B2834201D3273000E00138C01AFA
+:1034F0007047C046340800202D080020054B18787E
+:10350000054BC0B21B78DBB2984200D24030C01AE3
+:103510007047C0462C080020C107002010B5124B90
+:1035200012491A780B78D2B2DBB29A4219D0013321
+:103530003F2400209C424041404203400C48DCB202
+:10354000C05C0C700B49C0B2096800290BD09A42CC
+:1035500000D24032D31A1A2B05DC074B1B780B72B2
+:1035600001E00120404210BD2C080020C1070020CE
+:10357000EB070020BC070020EA0700200A4B1A785E
+:103580000A4B1B78DBB29A420AD0013300223F215A
+:103590009942524152421340054AD05CC0B201E008
+:1035A000012040427047C0462C080020C10700207F
+:1035B000EB070020064B1A78064BD2B21A70064B66
+:1035C0001B68002B02D0054A12781A727047C04659
+:1035D000C10700202C080020BC070020EA070020BB
+:1035E00030B52E4A1379990624D52D49D4790B7814
+:1035F000002001333F259D4240414042034029487D
+:10360000E4B20078834203D02748C454D8B208708B
+:103610002649086800280DD025490978C9B28B428F
+:1036200001D35B1A01E05B1A4033272B02DD214BEB
+:103630001B780371D178194CC9B24BB2002B1ADA3E
+:1036400023795BB2002B16DA1B4B19481D780378DF
+:10365000DBB29D4202D16C23E3700CE00133002405
+:1036600027259D42644164422340144CE45CDBB254
+:10367000E4B2D4710370402319420ED0117919427B
+:103680000BD00F4B002119700E4B1B688B4202D0E0
+:103690000D49097819722C23D37030BD00C0064043
+:1036A0002C080020C1070020EB070020BC070020E9
+:1036B0002D080020EA07002034080020C20700205F
+:1036C000C0070020300800202B0800201FB572B66C
+:1036D000154B70221A70154A41211170144A0F219E
+:1036E000117080221A701A7852B2002AFBDA114B3C
+:1036F000186862B6104B984201D80A23584301ACAF
+:10370000211C0A22FEF770FF0023E15C0B4A00290E
+:1037100005D058001018013341800A2BF5D1013330
+:103720005B0013701FBDC04600000240070002404E
+:1037300006000240080002407F969800E403002043
+:1037400008B50368C9B21B68984708BD08B5036887
+:10375000C9B21B68984708BD08B50368C9B21B68A1
+:10376000984708BD08B50368C9B21B68984708BDEB
+:1037700008B5044B4808C01800F0A2FAFEF75CFF39
+:1037800008BDC046C0C62D0010B5064B4808C0187D
+:10379000141C00F095FAFEF74FFF201CFEF7C0FF47
+:1037A00010BDC046C0C62D0008B5FEF7EBFF08BD32
+:1037B00008B5081CFFF720F808BD08B5081CFFF77E
+:1037C00093F808BD08B5081C111CFFF737F808BDB1
+:1037D00008B5081CFFF7D0F8431E9841C0B208BDD9
+:1037E00008B5081CFFF7EEF8431E9841C0B208BDAB
+:1037F00008B5FFF767F908BD08B5FFF7A3F908BDDD
+:1038000008B5FFF76FF908BD08B5FFF73DF908BD2A
+:1038100008B5FFF7B3F908BD08B5FFF741F908BDD2
+:1038200008B5081CFFF7D0F8012008BD08B5081C32
+:10383000FFF7CAF8012008BD10B5081C141C111CA4
+:10384000FFF716F9201C10BD38B5081C0D1C01F03F
+:1038500041F9041C211C281CFFF70AF9201C38BD63
+:103860007047FFFF044B00221A711A73FA21034AB2
+:10387000890099601A60704738080020585300008A
+:1038800008B50368C9B21B68984708BD08B5036846
+:10389000C9B21B68984708BD08B50368C9B21B6860
+:1038A000984708BD08B50368C9B21B68984708BDAA
+:1038B00008B5044B4808C01800F002FAFFF7D4F925
+:1038C00008BDC04660E3160010B5064B4808C01896
+:1038D000141C00F0F5F9FFF7C7F9201CFFF70CFAEC
+:1038E00010BDC04660E3160008B5FFF737FA08BD03
+:1038F00008B5081CFFF76CFA08BD08B5081CFFF7EF
+:103900008AFA08BD08B5081C111CFFF783FA08BD28
+:1039100008B5081CFFF780FA431E9841C0B208BDE5
+:1039200008B5081CFFF79EFA431E9841C0B208BDB7
+:1039300008B5FFF717FB08BD08B5FFF753FB08BD37
+:1039400008B5FFF71FFB08BD08B5FFF7EDFA08BD86
+:1039500008B5FFF763FB08BD08B5FFF7F1FA08BD2E
+:1039600008B5081CFFF780FA012008BD08B5081C3F
+:10397000FFF77AFA012008BD10B5081C141C111CB1
+:10398000FFF7C6FA201C10BD38B5081C0D1C01F04D
+:10399000A1F8041C211C281CFFF7BAFA201C38BD12
+:1039A0007047FFFF044B00221A711A73FA21034A71
+:1039B000890099601A60704748080020B8530000D9
+:1039C00008B50368C9B21B68984708BD08B5036805
+:1039D000C9B21B68984708BD08B50368C9B21B681F
+:1039E000984708BD08B50368C9B21B68984708BD69
+:1039F00008B5044B4808C01800F062F9FFF784FBD3
+:103A000008BDC04660E3160010B5064B4808C01854
+:103A1000141C00F055F9FFF777FB201CFFF7D8FBCB
+:103A200010BDC04660E3160008B5FFF703FC08BDF3
+:103A300008B5081CFFF738FC08BD08B5081CFFF7DF
+:103A40008DFC08BD08B5081C111CFFF74FFC08BD14
+:103A500008B5081CFFF7ACFC431E9841C0B208BD76
+:103A600008B5081CFFF7CAFC431E9841C0B208BD48
+:103A700008B5FFF743FD08BD08B5FFF77FFD08BD9A
+:103A800008B5FFF74BFD08BD08B5FFF719FD08BDE8
+:103A900008B5FFF78FFD08BD08B5FFF71DFD08BD90
+:103AA00008B5081CFFF7ACFC012008BD08B5081CD0
+:103AB000FFF7A6FC012008BD10B5081C141C111C42
+:103AC000FFF7F2FC201C10BD38B5081C0D1C01F0DE
+:103AD00001F8041C211C281CFFF7E6FC201C38BD43
+:103AE0007047FFFF044B00221A711A73FA21034A30
+:103AF000890099601A607047580800201854000027
+:103B000008B50B4B1A68002A04D001221A60094B31
+:103B10001B689847084B1B78002B08D0074B1A6886
+:103B2000002A04D001221A60024B5B68984708BD46
+:103B30000C7103406C0800206A0800201C710340CF
+:103B4000064B8022196812040A431A60044B0022B3
+:103B50001A60044B01221A707047C0463C80044032
+:103B6000007003406A080020054B01221A60054BD3
+:103B7000054A19680A401A60044B00221A707047FF
+:103B8000007003403C800440FFFF7FFF6A08002074
+:103B900070B544780E4D0F4E23015A199B190E4DE6
+:103BA0000669A40042606651002483601C601160B5
+:103BB00003221A60094B417B094A18680904024034
+:103BC0000A431A60074B8022D2031A6070BDC046B8
+:103BD00000710340087103406C08002014E400E009
+:103BE000FFFF00FF00E100E0F8B50C4B061C1B785E
+:103BF0000F1C002B01D1FFF7A3FF094D2C78002CDF
+:103C000004D06B780020834207D10124301C7470EB
+:103C1000391CFFF7BDFF01202855F8BD6A080020B8
+:103C20006808002008B5836800221A608021074BCD
+:103C3000C90319604178064B5A541A78002A04D1F6
+:103C40005B78002B01D1FFF78FFF08BD80E100E01A
+:103C50006808002070B50378041C0E1C151C002B8E
+:103C600003D0FFF7DFFF002323702661201C291CEF
+:103C7000FFF7BAFF002802D00123237000E0207074
+:103C8000207870BD037810B5041C002B01D0FFF71D
+:103C9000C9FF0023237010BD02B4714649084900D2
+:103CA000095C49008E4402BC7047C04603B47146AB
+:103CB000490840004900095A49008E4403BC704736
+:103CC000002934D00123002210B488422CD30124CF
+:103CD0002407A14204D2814202D209011B01F8E764
+:103CE000E400A14204D2814202D249005B00F8E71D
+:103CF000884201D3401A1A434C08A04202D3001B49
+:103D00005C0822438C08A04202D3001B9C0822437B
+:103D1000CC08A04202D3001BDC082243002803D0B9
+:103D20001B0901D00909E3E7101C10BC70470028EB
+:103D300001D00020C04307B4024802A140180290FD
+:103D400003BDC046190000000029F0D003B5FFF7FD
+:103D5000B9FF0EBC4243891A1847C0467047C04697
+:103D60008446081C6146FFE71FB500F001FA0028F1
+:103D700001D40021C8421FBD10B500F087F94042B0
+:103D8000013010BD10B500F0F3F9002801DB002070
+:103D900010BD012010BDC04610B500F0E9F90028A3
+:103DA00001DD002010BD012010BDC04610B500F09F
+:103DB00097F9002801DC002010BD012010BDC0468D
+:103DC00010B500F08DF9002801DA002010BD0120A7
+:103DD00010BDC0461C2101231B04984201D3000CD6
+:103DE00010391B0A984201D3000A08391B0998426E
+:103DF00001D30009043902A2105C40187047C04684
+:103E000004030202010101010000000000000000A3
+:103E10009E2110B5C905041CFFF7D2FF002803D16D
+:103E2000201C00F091FC10BD9E21C905201C00F053
+:103E300015FB00F089FC80231B06C018F3E7C04681
+:103E4000F0B55F4656464D464446F0B4460245003E
+:103E5000C00F85B00F1C760A2D0E804641D0FF2D75
+:103E600026D08024240400212643F6007F3D894685
+:103E70008B46F90F7C027800640A000E00918A4696
+:103E80003CD0FF2834D080231B041C430023E400D3
+:103E90007F380193009F4346019A7B4049469C46E8
+:103EA00011430F2900D971E0764F89007F58BF4632
+:103EB000002E3ED10822022391469B46D9E75A465E
+:103EC000341CC24601920199022937D0032900D13E
+:103ED000CFE0012900D0ABE053460B400022002682
+:103EE00032E0002E19D10421012289469346C0E711
+:103EF000221C531E9A4102320192CBE701270197FF
+:103F0000002CC7D0201CFFF765FF431F9C40762381
+:103F10005B420021181A0191BCE7301CFFF75AFFE1
+:103F20007625431F9E406D4200232D1A99469B46DD
+:103F30009FE70C23032199468B469AE7D446012339
+:103F400067463B40FF2200267602D205700ADB0757
+:103F50001043184305B03CBC90469946A246AB4678
+:103F6000F0BD80260023F603FF22EDE700220026A5
+:103F7000EAE78020C00306423BD0044239D1061C48
+:103F800026437602760A009BFF22DDE7281A03907B
+:103F900076016401A64239D3361B1A22012301207F
+:103FA000311C5B007600002901DBB44201D8361BCE
+:103FB0000343013A002AF3DC741EA641341C1C435F
+:103FC000039A7F32002A27DD630704D00F232340A2
+:103FD000042B00D00434270103D52B4B039A1C403B
+:103FE0008032FE2A0BDD012361460B40FF220026B2
+:103FF000AAE706437602760A4346FF22A4E7A40115
+:1040000001236746660AD2B23B409DE7039F1B220D
+:10401000013F03970023C2E77E23039F5B42DB1B24
+:104020001B2B07DD012361460B40002200268BE796
+:10403000D446C5E7221CDA40039B9E339C40231CD8
+:104040005C1EA34113435A0704D00F221A40042ACE
+:1040500000D004335F0105D5012361460B400122E6
+:10406000002671E79E01624601231340760A002272
+:104070006AE78026F60326437602760A5346FF2235
+:1040800062E7C04670540000FFFFFFF74A02430298
+:1040900070B55C0A550A43004A001B0EC60F120E8B
+:1040A000C90FFF2B05D0FF2A08D0012093420BD067
+:1040B00070BD0120002CFBD1FF2AF6D10120002D7C
+:1040C000F6D101209342F3D1AC42F1D18E4205D01A
+:1040D000002BEDD1201C441EA041E9E70020E7E7BA
+:1040E0004A024302F0B55C0A550A43004A001B0E1F
+:1040F000C60F120EC90FFF2B31D0FF2A34D0002B70
+:1041000016D1604260418446002A14D0002820D194
+:104110008E4217D1934215DC04DBAC4212D800204A
+:10412000AC4212D2704270414042012318430CE06D
+:10413000002AEDD194466F426F416046002805D1B8
+:10414000002FE5D0704201231843F0BD0020002F5E
+:10415000FBD148424841404201231843F5E7002C77
+:10416000CBD002204042F0E7002DC8D0F9E7C0468E
+:104170004A024302F0B55C0A550A43004A001B0E8E
+:10418000C60F120EC90FFF2B27D0FF2A29D0002BF4
+:1041900010D0002A15D194466F426F416046002826
+:1041A00015D00020002F04D148424841404201234D
+:1041B0001843F0BD604260418446002AECD00028DC
+:1041C000F2D18E4211D0704201231843F1E7002F43
+:1041D000F7D0704201231843EBE70220002CE8D10E
+:1041E000D3E70220002DE4D1D1E79342EBDC04DBDE
+:1041F000AC42E8D80020AC42DBD270427041404271
+:1042000001231843D5E7C046F0B55F4656464D46F4
+:104210004446F0B44402460083B00F1C640A360ED4
+:10422000C50F002E41D0FF2E22D080231B0400207A
+:104230001C43E4007F3E82468046391C4B007F02CF
+:10424000C90F7F0A1B0E8B463BD0FF2B34D0802238
+:1042500012041743FF007F3B00215A466A40019237
+:1042600052460A430F2A63D87A48920082589746EA
+:10427000002C3FD10822022392469846DDE70195A3
+:10428000404602282AD1019A01251540FF23002427
+:104290006402DB05600AED071843284303B03CBC09
+:1042A00090469946A246AB46F0BD002C27D104208B
+:1042B000012282469046C0E7391C4A1E91410231D4
+:1042C000CBE70121002FC8D0381CFFF783FD431F27
+:1042D0009F4076235B421B1A0021BEE7032800D1D2
+:1042E000AEE001284FD1019842461040C5B20023EC
+:1042F0000024CDE70C2303209A4680469DE7201C2E
+:10430000FFF768FD7626431F9C4076420023361A4D
+:104310009A46984691E780240025E403FF23B7E7F7
+:104320005B463C1C01938846AAE73C1C8846A7E7ED
+:10433000250C24043A0C240C3F04F6183F0C211CD5
+:10434000231C794353436F435543FB180A0C9B18B6
+:10435000B1469F4202D980225202AD1809041A04C4
+:10436000090C521894011B0C611E8C41920EED1821
+:104370001443AD012C43230105D50122630801201C
+:10438000144081441C434B467F33002B2DDD6007D6
+:1043900004D00F222240042A00D00434220103D585
+:1043A0002D4B1C404B468033FE2B17DD019B012516
+:1043B0001D400024FF236BE78020C003044208D087
+:1043C000074206D1041C3C436402640A5D46FF2395
+:1043D0005EE704436402640AFF2359E70198A401DD
+:1043E0000125640ADBB2054052E77E235B424A4660
+:1043F0009B1A1B2B05DD019B01251D40002400237A
+:1044000046E7221CDA404B469E339C40231C5C1E30
+:10441000A3411343580704D00F221A40042A00D0A6
+:1044200004335A0105D5019B01251D4000240123B9
+:104430002EE701989C010125640A0540002327E727
+:104440008027FF03019B3C43640201251D40640A51
+:10445000FF231DE7B0540000FFFFFFF7F8B5C20FC0
+:10446000430244004D024800240E161C9B09000E16
+:10447000C90FAD09FF2800D183E0012779408A42A6
+:104480005CD0221A002A00DC8EE000281ED1002D0C
+:1044900000D07AE0580704D00F221A40042A00D036
+:1044A00004338021C90401221940324000293AD046
+:1044B0000134FF2C00D183E09B015B0A5B02E4B274
+:1044C000E405580AD20720431043F8BDFF2CE1D081
+:1044D0008021C9040D431B2A00DD31E1291C202065
+:1044E000D140821A95406A1E95410D435B1B5801CD
+:1044F000D0D59B019F09381CFFF76CFC421F9740E9
+:1045000094425FDC141B1F231B1B3A1C9F40611C41
+:104510003B1CCA405F1EBB4113430024BAE7131E75
+:10452000B8D100230022DB08FF2C04D1002B47D098
+:104530008020C00303435B025B0ABFE7211A002906
+:1045400044DD002827D0FF2CA4D08020C0040543E0
+:104550001B2900DDF2E0281C2027C840791A8D4075
+:10456000691E8D4105435B19590100D492E701345E
+:10457000FF2C59D0734901221A400B405B081343AA
+:1045800088E7002D00D07AE777E7013A002AADD01E
+:10459000FF2CA0D17EE7002D00D17BE70139002957
+:1045A000E1D0FF2CD4D175E7002A1BD1621CD2B216
+:1045B000012A4BDD5F1B7A0123D5EF1A0E1C9AE707
+:1045C00000237BE75F4BA41A3B4063E7002946D1F9
+:1045D000611CC8B2012829DDFF2924D0EB185B0833
+:1045E0000C1C57E7002C13D0FF2818D08024E404BB
+:1045F000524223431B2A4DDD0123EB1A041C0E1CDF
+:1046000075E7002F00D076E70023002200248AE718
+:10461000002B3BD0D243002AEFD0FF28EAD12B1C3D
+:10462000FF240E1C36E7FF2400237CE7002C5CD11E
+:10463000002B00D180E0002D00D12BE75B19580141
+:1046400000D427E73F4A0124134023E7002C15D16B
+:10465000002B40D1002D63D02B1C0E1C1AE7002C20
+:1046600021D1002B54D0C943002904D0FF284CD0BD
+:104670001B2958DD01235B19041C75E7002B19D198
+:10468000002D48D02B1C0E1CFF2403E72B1C041C00
+:104690000E1CFFE61C1C2026D440B21A93405A1E62
+:1046A00093412343A9E7FF282FD08024E404494203
+:1046B0002343DDE7FF24002D00D1EBE68022DB0859
+:1046C000D203134204D0ED08154201D12B1C0E1C5D
+:1046D000DB00FF24DEE6002D00D1DBE65A1B500193
+:1046E00000D41CE7EB1A0E1CD4E6002B0DD0FF24DF
+:1046F000002D00D1CEE68022DB08D2031342E7D0A2
+:10470000ED081542E4D12B1CE2E72B1CFF24C1E687
+:104710002B1C041CBEE6802300229B04FF2402E71E
+:10472000231C0022FFE61C1C2027CC40791A8B405A
+:10473000591E8B4123439EE72B1CABE6012512E754
+:104740000125D3E6FFFFFFFB4302590A4300C20FD6
+:104750001B0E00207E2B0DDD9D2B0CDC8020000429
+:104760000143952B0ADC9620C31AD9404842002AFF
+:1047700000D1081C7047034BD018FBE7963B9940CB
+:10478000F4E7C046FFFFFF7F10B5041E33D0FFF7EC
+:1047900021FB9E231B1A962B09DC083884406402F7
+:1047A000640ADBB26402DB05600A184310BD992B72
+:1047B0000ADD0522121A211CD1400A1C011C1B31E2
+:1047C0008C40611E8C411443052801DD421F94403A
+:1047D000144A2240610704D00F210C40042C00D061
+:1047E000043251010AD59F23181AFF2816D09401CC
+:1047F000640AC3B2D6E700230024D3E7D208FF2B14
+:1048000003D05402640ADBB2CCE7002A06D080242D
+:10481000E40314436402640AFF23C3E7FF23002474
+:10482000C0E7C046FFFFFFFB08B5031C081C191CAE
+:1048300000F002F808BDFFFF38B5051C05480C1C48
+:10484000131C002804D00220291C221C00E000BFF9
+:1048500038BDC0460000000070B50E4B0E4D002460
+:10486000ED1AAD101E1CAC4204D0A300F3589847BB
+:104870000134F8E700F058FE084B094D0024ED1A0A
+:10488000AD101E1CAC4204D0A300F358984701346D
+:10489000F8E770BD34550000345500003455000071
+:1048A0005055000008B5034B011C186800F02EF8A5
+:1048B00008BDC0465C04002010B50023934203D01D
+:1048C000CC5CC4540133F9E710BD70B5814201D30B
+:1048D00000230CE08C18A042FAD28518131C013B6F
+:1048E0000BD351426618F65C6918CE54F7E7934231
+:1048F00003D0CC5CC4540133F9E770BD031C8218AB
+:10490000934202D019700133FAE7704730B500299D
+:1049100040D004390B68002B00DAC9181E4A13680E
+:10492000141C002B02D14B60116033E099420FD26E
+:1049300008680A189A4205D113685268C0180860BE
+:104940004A6000E04B60216024E08A4203D8131CD7
+:104950005A68002AF9D11D685C198C420BD109688C
+:10496000691858181960904214D1146852680919CE
+:1049700019605A600EE08C4202D90C23036009E0F2
+:1049800008680C18944203D1146852680019086032
+:104990004A60596030BDC0467808002070B50323D6
+:1049A000CD1C9D430835061C0C2D01D20C2501E0C1
+:1049B000002D3FDB8D423DD3204B1C681A1C211C6F
+:1049C000002913D00868431B0DD40B2B02D90B60B0
+:1049D000CC181EE08C4202D1636813601AE048686C
+:1049E00060600C1C16E00C1C4968E9E7144C206858
+:1049F000002803D1301C00F031F82060301C291C45
+:104A000000F02CF8431C15D0C41C03239C438442A3
+:104A10000AD12560201C0B300722231D9043C31AA6
+:104A20000BD05A42E25008E0211A301C00F016F870
+:104A30000130EED10C233360002070BD78080020D7
+:104A40007408002008B50A1C0349031C0868191CD7
+:104A500000F02DFB08BDC0465C04002038B5074CB3
+:104A60000023051C081C2360FDF78AFD431C03D1AD
+:104A70002368002B00D02B6038BDC046D40800202E
+:104A80000EB400B59CB01DAB04CB822202A99200EB
+:104A90008A810A4A02908A604A6101225242CA818E
+:104AA000074A086110681D9A019300F07BF8029A8A
+:104AB000002313701CB008BC03B01847FFFFFF7F32
+:104AC0005C040020031C0A7801311A700133002AAB
+:104AD000F9D170470023C25C0133002AFBD1581E74
+:104AE0007047FFFFF0B58D6885B0071C0C1C039262
+:104AF0000193AB4245D390228B89D20013423DD023
+:104B000062690326564309692068F20F401A961914
+:104B10000290021C0198013212187610964200D2BF
+:104B2000161C381C5A050FD5311CFFF737FF051E20
+:104B300013D0029A2169FFF7BFFEA289184B1340D8
+:104B400080221343A38111E0321C00F0B0FA051E4D
+:104B50000CD1381C2169FFF7D9FE0C233B60A389D7
+:104B6000402213430120A381404217E0029B2561AC
+:104B7000ED1825606661019DF61AA6600198A842AD
+:104B800000D2019D2A1C20680399FFF79EFEA268AF
+:104B90000020531BA36023685D19256005B0F0BD9C
+:104BA0007FFBFFFFF0B59FB0039005938B890E1C30
+:104BB000171C19060FD53269002A0CD14021FFF7C6
+:104BC000EDFE30603061002803D103990C230B60A7
+:104BD000C9E04023736106AD00236B6120236B762F
+:104BE0003023AB763C1C2378002B03D1E21B0292CE
+:104BF00011D003E0252BF9D00134F4E70398311CE0
+:104C00003A1C029BFFF76EFF013000D1A6E06969F4
+:104C1000029A8B186B612378002B00D19EE0012251
+:104C200052426A606A4600235B3201342B60EB60BB
+:104C3000AB601370AB654E4F2178381C052200F035
+:104C40002BFA002807D0C71B2B680120B840184357
+:104C500028600134EFE72B68D90603D56A46202186
+:104C60005B3211701A0703D56A462B215B32117033
+:104C700022782A2A01D0099B0EE0059A111D12689C
+:104C80000591002A01DB099204E05242EA60022207
+:104C900013432B60013409E02278303A092A04D802
+:104CA0000A214B4301349B18F6E7099323782E2BF6
+:104CB00018D163782A2B09D1059B02341A1D1B6871
+:104CC0000592002B0DDA01235B420AE00134002338
+:104CD0002278303A092A04D80A214B4301349B1820
+:104CE000F6E70793234F2178381C032200F0D4F90C
+:104CF000002806D0C71B2B684020B8401843286006
+:104D0000013421781C480622671C297600F0C4F97A
+:104D1000002812D0194B002B06D1059B0722073320
+:104D200093430833059314E005AB00930398291CC3
+:104D3000321C134B00E000BF07E005AB0093039863
+:104D4000291C321C0E4B00F091F80490049901319B
+:104D500004D06A69049953186B6143E7B3895A0612
+:104D600001D40B9801E0012040421FB0F0BDC046C5
+:104D7000F2540000F8540000FC5400000000000051
+:104D8000E54A0000F7B5151C01930A698B68061CFB
+:104D90000C1C934200DA131C221C2B604332127845
+:104DA000002A01D001332B602068800602D52B68D1
+:104DB00002332B60216806270F401FD0231C43338A
+:104DC0001B785A1E9341226892061FD5E118403184
+:104DD0003020C870211C5A1C45310978A218403275
+:104DE0000233D17012E0221C301C019919320123C8
+:104DF000089FB847013011D0009F01370097E06845
+:104E00002968009F431A9F42EDDBD7E7221C301C24
+:104E100001994332089FB847013002D10120404236
+:104E200023E0206806212B68E26801400025042960
+:104E300003D1D51AEB43DB171D40A26823699A42C0
+:104E400001DDD31AED1800270097009FAF420BDA5F
+:104E5000221C301C01991A320123089FB8470130E7
+:104E6000DCD0009F0137EFE70020FEBDF0B50D1C40
+:104E70008BB0433506920590079304950B7E0C1C6E
+:104E8000109A6E2B00D1A7E011D8632B22D009D83D
+:104E9000002B00D1B0E0582B00D0C0E045310B70A2
+:104EA0007B4D4EE0642B1CD0692B1AD0B7E0732BDE
+:104EB00000D1A5E009D86F2B29D0702B00D0AEE02F
+:104EC0000E68202333430B6036E0752B1FD0782B00
+:104ED00032D0A4E013680D1C191D423511601B6807
+:104EE0009FE0216813680E0603D5191D11601E6826
+:104EF00005E04806F9D5191D116000215E5E644B7E
+:104F0000002E3BDA049D2D2276422A7036E021687D
+:104F100013680E0603D5191D11601E6804E04806CB
+:104F2000F9D5191D1E881160594B227E039308275D
+:104F30006F2A1ED00A271CE0231C78214533554DCB
+:104F4000197011682368081D039510601E0601D5AD
+:104F50000E6802E05806FBD50E88D90702D520223C
+:104F6000134323601027002E03D1226820239A4385
+:104F70002260231C002243331A7001E003930A27A6
+:104F80006368A360002B03DB25680422954325603A
+:104F9000002E02D1049D002B0ED0049D301C391C24
+:104FA000FEF7D2FE0398013D435C301C2B70391C88
+:104FB000FEF786FE061EF1D1082F09D12168C90728
+:104FC00006D5626823699A4202DC013D30232B70CA
+:104FD000049E731B23612AE008681368496905066B
+:104FE00004D5181D10601B68196005E04606F8D549
+:104FF000181D10601B68198000232361049D16E0B2
+:105000001368191D11601D68281CFFF763FD636894
+:105010002061984200D923612069606004E0251C6A
+:1050200042352B7001232361049E00233370079EB9
+:1050300005980096211C09AA069BFFF7A3FE0130E4
+:1050400002D10120404221E02A1C059806992369DB
+:10505000079DA8470130F4D02668B60705D4099B00
+:10506000E068984212DA181C10E00025E0680999FF
+:10507000431A9D42F3DA221C05980699193201233E
+:10508000079EB0470130DCD00135EFE70BB0F0BD33
+:105090000355000014550000C9B28218904204D094
+:1050A00003788B4202D00130F8E700207047F8B552
+:1050B000061C0C1C151C002904D1111CFFF76EFCEA
+:1050C000041C18E0002A03D1FFF720FC2C1C12E07E
+:1050D00000F013F8A8420ED2301C291CFFF75EFC2A
+:1050E000071E07D0211C2A1CFFF7E6FB301C211CE1
+:1050F000FFF70CFC3C1C201CF8BD04390B68181F82
+:10510000002B02DAC8581B18181F7047256C640062
+:10511000256C75004720256320256C6420256420BC
+:10512000256400656E6400742000350020473A0055
+:105130002050445F73637265656E3A002050445F8F
+:105140006C617365723A00556E6B6E6F776E20639B
+:105150006F6D6D616E643A2000050E08090D0C0636
+:10516000070F0B004417FF050E08090D0C06070F6B
+:105170000B004417FFFFFFFFFFFFFFFFFFFFFF1ABA
+:105180001BFFFFFF420000F840A0044001000000A8
+:10519000420000F844A0044002000000C00000F8F3
+:1051A00000C0044001000000000000F8049004402A
+:1051B00002000000000000F8089004400400000015
+:1051C000C00000F81CC0044080000000C00000F8CF
+:1051D00010C0044010000000C00000F808C00440E7
+:1051E00004000000C00000F80CC0044008000000EB
+:1051F000800000F80CB0044008000000800000F8B7
+:1052000010B0044010000000800000F818B0044006
+:1052100040000000800000F81CB004408000000046
+:10522000800000F814B0044020000000C00000F826
+:1052300004C0044002000000800000F800B00440F8
+:1052400001000000400000F800A004400100000040
+:10525000400000F804A0044002000000400000F8F4
+:105260000CA0044008000000400000F808A0044022
+:1052700004000000C00000F814C00440200000003A
+:10528000C00000F818C0044040000000800000F892
+:1052900004B0044002000000800000F808B00440A0
+:1052A00004000000020100F850D00440100000008B
+:1052B000020100F854D0044020000000030100F86F
+:1052C00078D0044040000000000000000000000012
+:1052D000A9240000B32400008124000089240000D8
+:1052E0009124000099240000A12400000000000087
+:1052F0001519151519FFFFFF000100000703002015
+:10530000120000000002000032030020950000009F
+:1053100000030000E0030020000000000103090476
+:105320001A0300200000000002030904C803002043
+:105330000000000003030904E40300200000000053
+:10534000000000000000000000000000FFFFFFFF61
+:105350000000000000000000213800003938000083
+:10536000F137000001380000F9370000093800006B
+:105370007137000089370000A9370000B1370000FD
+:10538000BB370000C5370000D1370000E13700000F
+:105390001138000019380000413700004D37000077
+:1053A0005937000065370000493800002D380000EB
+:1053B00000000000000000006139000079390000A1
+:1053C0003139000041390000393900004939000005
+:1053D000B1380000C9380000E9380000F138000099
+:1053E000FB380000053900001139000021390000A8
+:1053F0005139000059390000813800008D38000013
+:1054000099380000A5380000893900006D39000086
+:105410000000000000000000A13A0000B93A0000BE
+:10542000713A0000813A0000793A0000893A0000A0
+:10543000F1390000093A0000293A0000313A000031
+:105440003B3A0000453A0000513A0000613A000042
+:10545000913A0000993A0000C1390000CD390000AE
+:10546000D9390000E5390000C93A0000AD3A000022
+:105470008C3F00003E3F00006C3F0000C63E000035
+:105480006C3F0000623F00006C3F0000C63E000021
+:105490003E3F00003E3F0000623F0000C63E00006D
+:1054A000BE3E0000BE3E0000BE3E0000723F000057
+:1054B000304300002A4300002A430000204300003C
+:1054C000804200008042000016430000204300009C
+:1054D000804200001643000080420000204300008C
+:1054E0007E4200007E4200007E420000B843000081
+:1054F0004300232D302B2000686C4C006566674507
+:10550000464700303132333435363738394142433B
+:1055100044454600303132333435363738396162EC
+:105520006364656600FFFFFFF8B5C046F8BC08BCC1
+:105530009E46704739040000310C0000A122000093
+:10554000C124000065380000A5390000E53A0000DC
+:10555000000000009C04002014050020AC04002082
+:105560007C040020FC0400200A040000410E00001E
+:10557000410E0000410E0000410E0000410E0000EF
+:10558000410E0000410E0000410E0000410E0000DF
+:10559000410E0000410E0000410E0000410E0000CF
+:1055A000410E0000410E0000410E0000410E0000BF
+:1055B000410E0000410E0000410E0000410E0000AF
+:1055C000410E0000410E0000410E0000410E00009F
+:1055D000410E0000410E0000FFFFFFFFFF00000032
+:1055E000D80800200107081201010100000040C096
+:1055F000168904000201020301001803540065002B
+:1056000065006E00730079006400750069006E002B
+:105610006F0009029500030100C032080B0002026E
+:105620000201040904000001020201000524001027
+:1056300001052401010104240206052406000107D6
+:1056400005810310004009040100020A0000000760
+:105650000502024000000705830240000009040221
+:10566000000201030000072401000141000624029A
+:1056700001010006240202020009240301030102C1
+:10568000010009240302040101010009050502408B
+:105690000000000005250101010905840240000009
+:1056A0000000052501010300180354006500650092
+:1056B0006E007300790020004D004900440049004D
+:1056C000040309040C0300000000000000000000B7
+:1056D00000000000000000000000000000000000CA
+:1056E00000000000000000000000000000000000BA
+:1056F000000000000000000000000000F054000066
+:105700000000000000000000000000000000000099
+:105710000000000000000000000000000000000089
+:105720000000000000000000000000000000000079
+:10573000000000000000000000000000FC0300204A
+:00000001FF
diff --git a/android/WALT/app/src/main/res/values-w820dp/dimens.xml b/android/WALT/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/android/WALT/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+ (such as screen margins) for screens with more than 820dp of available width. This
+ would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+ <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/android/WALT/app/src/main/res/values/attrs.xml b/android/WALT/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..9aad7b9
--- /dev/null
+++ b/android/WALT/app/src/main/res/values/attrs.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <declare-styleable name="NumberPickerPreference">
+ <attr name="minValue" format="integer" />
+ <attr name="maxValue" format="integer" />
+ </declare-styleable>
+ <declare-styleable name="HistogramChart">
+ <attr name="description" format="string" />
+ <attr name="numDataSets" format="integer" />
+ <attr name="binWidth" format="float" />
+ </declare-styleable>
+</resources>
diff --git a/android/WALT/app/src/main/res/values/color.xml b/android/WALT/app/src/main/res/values/color.xml
new file mode 100644
index 0000000..8519775
--- /dev/null
+++ b/android/WALT/app/src/main/res/values/color.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="ColorPrimary">#F5F5F5</color>
+ <color name="ColorPrimaryDark">#757575</color>
+ <color name="ColorAccent">#757575</color>
+ <color name="ColorBackground">#FFFFFF</color>
+ <color name="DarkGreen">#026402</color>
+ <color name="ColorDisabled">#aaaaaa</color>
+</resources>
diff --git a/android/WALT/app/src/main/res/values/dimens.xml b/android/WALT/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..47c8224
--- /dev/null
+++ b/android/WALT/app/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/android/WALT/app/src/main/res/values/strings.xml b/android/WALT/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..7204eaa
--- /dev/null
+++ b/android/WALT/app/src/main/res/values/strings.xml
@@ -0,0 +1,45 @@
+<resources>
+ <string name="app_name">WALT</string>
+
+ <string name="action_settings">Settings</string>
+ <string name="title_activity_crash_log">Crash Log</string>
+ <string name="protocol_version_mismatch">WALT reports protocol version %1$s, which is not
+ supported. Please program WALT to a firmware with protocol version %2$s. To do this
+ from the app, choose "Update WALT Firmware" from the "Diagnostics" menu.</string>
+ <string name="audio_mode">Audio Testing Mode</string>
+ <string name="screen_response_mode">Screen Testing Mode</string>
+ <string name="about_description">WALT is designed to measure the latency of physical sensors
+ and outputs on phones and computers. It can currently perform the following measurements:
+ tap latency, drag latency (scroll), screen draw latency, audio output/microphone
+ latencies, and MIDI input/output latencies.
+ </string>
+ <string name="disclaimer">DISCLAIMER: This is not an official Google product.</string>
+ <string name="more_info">A WALT device is required to run the latency tests. For more information, visit github.com/google/walt</string>
+ <string name="privacy_policy">Privacy policy:\ngithub.com/google/walt/blob/master/docs/PrivacyPolicy.md</string>
+ <string name="preference_screen_blinks" translatable="false">pref_screen_blinks</string>
+ <string name="preference_audio_in_reps" translatable="false">pref_audio_in_reps</string>
+ <string name="preference_audio_in_threshold" translatable="false">pref_audio_in_threshold</string>
+ <string name="preference_audio_out_reps" translatable="false">pref_audio_out_reps</string>
+ <string name="preference_midi_in_reps" translatable="false">pref_midi_in_reps</string>
+ <string name="preference_midi_out_reps" translatable="false">pref_midi_out_reps</string>
+ <string name="preference_auto_increase_brightness">auto_increase_brightness</string>
+ <string name="preference_show_tap_histogram">pref_show_tap_histogram</string>
+ <string name="preference_show_blink_histogram">pref_show_blink_histogram</string>
+ <string name="preference_systrace">pref_systrace</string>
+ <string name="preference_screen_fullscreen">pref_screen_fullscreen</string>
+ <string name="preference_log_url">pref_log_url</string>
+ <string name="preference_auto_upload_log">pref_auto_upload_log</string>
+ <string-array name="audio_mode_array">
+ <item>Continuous Playback Latency</item>
+ <item>Continuous Recording Latency</item>
+ <item>Cold Playback Latency</item>
+ <item>Cold Recording Latency</item>
+ <item>Display Recorded Waveform</item>
+ </string-array>
+ <string-array name="screen_response_mode_array">
+ <item>Blink Latency</item>
+ <item>Brightness Curve</item>
+ <item>Fast Path Graphics</item>
+ </string-array>
+
+</resources>
diff --git a/android/WALT/app/src/main/res/values/styles.xml b/android/WALT/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..02095e7
--- /dev/null
+++ b/android/WALT/app/src/main/res/values/styles.xml
@@ -0,0 +1,61 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
+ <item name="colorPrimary">@color/ColorPrimary</item>
+ <item name="colorPrimaryDark">@color/ColorPrimaryDark</item>
+ <item name="colorAccent">@color/ColorAccent</item>
+ <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
+ <item name="android:imageButtonStyle">@style/ImageButtonStyle</item>
+ <item name="imageButtonStyle">@style/ImageButtonStyle</item>
+ <!-- the homeAsUpIndicator doesn't work with either png or xml icons -->
+ <!--<item name="android:homeAsUpIndicator">@drawable/ic_chevron_left_black_24dp</item> -->
+
+ </style>
+
+ <style name="MenuDivider">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">1dp</item>
+ <item name="android:background">?android:attr/listDivider</item>
+ <item name="android:layout_marginLeft">72dp</item>
+ </style>
+
+ <style name="MenuTextTop">
+ <!--<item name="android:layout_marginTop">16dp</item>-->
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textColor">@android:color/black</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textSize">22sp</item>
+ </style>
+
+ <style name="MenuTextBottom">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:paddingLeft">2dp</item>
+ </style>
+
+ <style name="MenuIconStyle">
+ <item name="android:layout_width">40dp</item>
+ <item name="android:layout_height">40dp</item>
+ <item name="android:layout_marginLeft">@dimen/activity_horizontal_margin</item>
+ <item name="android:layout_marginRight">@dimen/activity_horizontal_margin</item>
+ <!--<item name="android:layout_gravity">center_vertical</item>-->
+ </style>
+
+ <style name="MenuItemStyle">
+ <item name="android:orientation">horizontal</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:layout_height">72dp</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:clickable">true</item>
+ </style>
+
+ <style name="ImageButtonStyle" parent="Widget.AppCompat.ImageButton">
+ <item name="android:padding">14dp</item>
+ </style>
+
+
+</resources>
diff --git a/android/WALT/app/src/main/res/xml/device_filter.xml b/android/WALT/app/src/main/res/xml/device_filter.xml
new file mode 100644
index 0000000..f4f4a19
--- /dev/null
+++ b/android/WALT/app/src/main/res/xml/device_filter.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Teensy in Serial mode, hex 16C0:0483 -->
+ <usb-device vendor-id="5824" product-id="1155" />
+
+ <!-- Teensy in Serial + keyboard +... mode. Hex 16C0:0483 -->
+ <usb-device vendor-id="5824" product-id="1159" />
+
+ <!-- Teensy in Serial + MIDI mode. Hex 16C0:0485 -->
+ <usb-device vendor-id="5824" product-id="1157" />
+
+ <!-- Teensy in Serial + MIDI mode with Teensyduion v1.31+. Hex 16C0:0489 -->
+ <usb-device vendor-id="5824" product-id="1161" />
+</resources>
diff --git a/android/WALT/app/src/main/res/xml/preferences.xml b/android/WALT/app/src/main/res/xml/preferences.xml
new file mode 100644
index 0000000..6482571
--- /dev/null
+++ b/android/WALT/app/src/main/res/xml/preferences.xml
@@ -0,0 +1,135 @@
+<android.support.v7.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:walt="http://schemas.android.com/apk/res/org.chromium.latency.walt">
+
+ <android.support.v7.preference.PreferenceScreen
+ android:key="pref_general_screen"
+ android:persistent="false"
+ android:title="General">
+
+ <SwitchPreference
+ android:key="@string/preference_systrace"
+ android:title="Enable systrace logging"
+ android:defaultValue="true" />
+
+ <PreferenceCategory android:title="Log Uploading">
+
+ <EditTextPreference
+ android:key="@string/preference_log_url"
+ android:title="URL to upload logs"
+ android:dialogTitle="Enter URL to upload logs"
+ android:defaultValue=""
+ android:inputType="textUri" />
+
+ <SwitchPreference
+ android:key="@string/preference_auto_upload_log"
+ android:title="Auto-upload logs"
+ android:summary="Will upload logs to URL after each test"
+ android:defaultValue="false" />
+
+ </PreferenceCategory>
+
+ </android.support.v7.preference.PreferenceScreen>
+
+ <android.support.v7.preference.PreferenceScreen
+ android:key="pref_tap_screen"
+ android:persistent="false"
+ android:title="Tap latency">
+
+ <SwitchPreference
+ android:key="@string/preference_show_tap_histogram"
+ android:title="Show live histogram for tap test"
+ android:defaultValue="true" />
+
+ </android.support.v7.preference.PreferenceScreen>
+
+ <android.support.v7.preference.PreferenceScreen
+ android:key="pref_screen_response_screen"
+ android:persistent="false"
+ android:title="Screen response">
+
+ <org.chromium.latency.walt.NumberPickerPreference
+ android:defaultValue="20"
+ android:dialogTitle="Number of blinks for screen latency measurement"
+ android:key="@string/preference_screen_blinks"
+ android:summary="%s blinks per test"
+ android:title="Blink latency test length"
+ walt:maxValue="1000"
+ walt:minValue="1" />
+
+ <SwitchPreference
+ android:key="@string/preference_auto_increase_brightness"
+ android:title="Automatically increase brightness for test"
+ android:defaultValue="true" />
+
+ <SwitchPreference
+ android:key="@string/preference_show_blink_histogram"
+ android:title="Show live histogram for blink latency test"
+ android:defaultValue="true" />
+
+ <SwitchPreference
+ android:key="@string/preference_screen_fullscreen"
+ android:title="Test in fullscreen mode"
+ android:defaultValue="true" />
+
+ </android.support.v7.preference.PreferenceScreen>
+
+ <android.support.v7.preference.PreferenceScreen
+ android:key="pref_audio_screen"
+ android:persistent="false"
+ android:title="Audio">
+
+ <org.chromium.latency.walt.NumberPickerPreference
+ android:defaultValue="5"
+ android:dialogTitle="Number of repetitions for audio input latency"
+ android:key="@string/preference_audio_in_reps"
+ android:summary="%s repetitions per test"
+ android:title="Audio input test length"
+ walt:maxValue="1000"
+ walt:minValue="1" />
+
+ <org.chromium.latency.walt.NumberPickerPreference
+ android:defaultValue="10"
+ android:dialogTitle="Number of repetitions for audio output latency"
+ android:key="@string/preference_audio_out_reps"
+ android:summary="%s repetitions per test"
+ android:title="Audio output test length"
+ walt:maxValue="1000"
+ walt:minValue="1" />
+
+ <org.chromium.latency.walt.NumberPickerPreference
+ android:defaultValue="5000"
+ android:dialogTitle="Threshold for audio recording test"
+ android:key="@string/preference_audio_in_threshold"
+ android:summary="%s"
+ android:title="Threshold for audio recording test"
+ walt:maxValue="100000"
+ walt:minValue="1" />
+
+ </android.support.v7.preference.PreferenceScreen>
+
+ <android.support.v7.preference.PreferenceScreen
+ android:key="pref_midi_screen"
+ android:persistent="false"
+ android:title="MIDI">
+
+ <org.chromium.latency.walt.NumberPickerPreference
+ android:defaultValue="100"
+ android:dialogTitle="Number of repetitions for MIDI input measurement"
+ android:key="@string/preference_midi_in_reps"
+ android:summary="%s repetitions per test"
+ android:title="MIDI input test length"
+ walt:maxValue="1000"
+ walt:minValue="1" />
+
+ <org.chromium.latency.walt.NumberPickerPreference
+ android:defaultValue="10"
+ android:dialogTitle="Number of repetitions for MIDI output measurement"
+ android:key="@string/preference_midi_out_reps"
+ android:summary="%s repetitions per test"
+ android:title="MIDI output test length"
+ walt:maxValue="1000"
+ walt:minValue="1" />
+
+ </android.support.v7.preference.PreferenceScreen>
+
+</android.support.v7.preference.PreferenceScreen>
diff --git a/android/WALT/app/src/test/java/org/chromium/latency/walt/HistogramChartTest.java b/android/WALT/app/src/test/java/org/chromium/latency/walt/HistogramChartTest.java
new file mode 100644
index 0000000..8365719
--- /dev/null
+++ b/android/WALT/app/src/test/java/org/chromium/latency/walt/HistogramChartTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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 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.interfaces.datasets.IBarDataSet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.util.ArrayList;
+
+import static junit.framework.Assert.assertEquals;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(android.graphics.Color.class)
+public class HistogramChartTest {
+
+ private BarData barData;
+ private HistogramChart.HistogramData data;
+
+ @Before
+ public void setUp() {
+ mockStatic(android.graphics.Color.class);
+ when(android.graphics.Color.rgb(anyInt(), anyInt(), anyInt())).thenReturn(0);
+ barData = new BarData();
+ barData.setBarWidth((1f - HistogramChart.GROUP_SPACE)/1);
+ barData.addDataSet(new BarDataSet(new ArrayList<BarEntry>(), "SomeLabel"));
+ data = new HistogramChart.HistogramData(1, 5f);
+ data.addEntry(barData, 0, 12);
+ data.addEntry(barData, 0, 14);
+ data.addEntry(barData, 0, 16);
+ data.addEntry(barData, 0, 21);
+ }
+
+ @Test
+ public void testBinHeights() {
+ final IBarDataSet barDataSet = barData.getDataSetByIndex(0);
+ assertEquals(3, barDataSet.getEntryCount());
+ assertEquals(2d, barDataSet.getEntryForIndex(0).getY(), 0.000001);
+ assertEquals(1d, barDataSet.getEntryForIndex(1).getY(), 0.000001);
+ assertEquals(1d, barDataSet.getEntryForIndex(2).getY(), 0.000001);
+ }
+
+ @Test
+ public void testBinXPositions() {
+ final IBarDataSet barDataSet = barData.getDataSetByIndex(0);
+ assertEquals(3, barDataSet.getEntryCount());
+ assertEquals(0d + 0.05d + 0.45d, barDataSet.getEntryForIndex(0).getX(), 0.000001);
+ assertEquals(1d + 0.05d + 0.45d, barDataSet.getEntryForIndex(1).getX(), 0.000001);
+ assertEquals(2d + 0.05d + 0.45d, barDataSet.getEntryForIndex(2).getX(), 0.000001);
+ }
+
+ @Test
+ public void testDisplayValue() {
+ assertEquals(10d, data.getMinBin(), 0.000001);
+ assertEquals(15d, data.getDisplayValue(1), 0.000001);
+ }
+}
diff --git a/android/WALT/app/src/test/java/org/chromium/latency/walt/TraceLoggerTest.java b/android/WALT/app/src/test/java/org/chromium/latency/walt/TraceLoggerTest.java
new file mode 100644
index 0000000..ed617cb
--- /dev/null
+++ b/android/WALT/app/src/test/java/org/chromium/latency/walt/TraceLoggerTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import static junit.framework.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(android.os.Process.class)
+public class TraceLoggerTest {
+
+ @Test
+ public void testLogText() {
+ final TraceLogger traceLogger = TraceLogger.getInstance();
+ traceLogger.log(30012345, 30045678, "SomeTitle", "Some description here");
+ traceLogger.log(40012345, 40045678, "AnotherTitle", "Another description here");
+ mockStatic(android.os.Process.class);
+ when(android.os.Process.myPid()).thenReturn(42);
+ String expected =
+ "WALTThread-[0-9]+ \\(42\\) \\[[0-9]+] .{4} 30\\.012345: tracing_mark_write: B\\|42\\|SomeTitle\\|description=Some description here\\|WALT\n" +
+ "WALTThread-[0-9]+ \\(42\\) \\[[0-9]+] .{4} 30\\.045678: tracing_mark_write: E\\|42\\|SomeTitle\\|\\|WALT\n" +
+ "WALTThread-[0-9]+ \\(42\\) \\[[0-9]+] .{4} 40\\.012345: tracing_mark_write: B\\|42\\|AnotherTitle\\|description=Another description here\\|WALT\n" +
+ "WALTThread-[0-9]+ \\(42\\) \\[[0-9]+] .{4} 40\\.045678: tracing_mark_write: E\\|42\\|AnotherTitle\\|\\|WALT\n";
+ assertTrue(traceLogger.getLogText().matches(expected));
+ }
+}
diff --git a/android/WALT/app/src/test/java/org/chromium/latency/walt/UtilsTest.java b/android/WALT/app/src/test/java/org/chromium/latency/walt/UtilsTest.java
new file mode 100644
index 0000000..bf77e05
--- /dev/null
+++ b/android/WALT/app/src/test/java/org/chromium/latency/walt/UtilsTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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 org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+import static java.lang.Double.NaN;
+import static junit.framework.Assert.assertEquals;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public class UtilsTest {
+
+ @Test
+ public void testMedian_singleNumber() {
+ ArrayList<Double> arr = new ArrayList<>();
+ arr.add(0d);
+ assertThat(Utils.median(arr), is(0d));
+ }
+
+ @Test
+ public void testMedian_evenSize() {
+ ArrayList<Double> arr = new ArrayList<>();
+ arr.add(1d); arr.add(2d); arr.add(3d); arr.add(4d);
+ assertThat(Utils.median(arr), is(2.5d));
+ }
+
+ @Test
+ public void testMedian_oddSize() {
+ ArrayList<Double> arr = new ArrayList<>();
+ arr.add(1d); arr.add(2d); arr.add(3d); arr.add(4d); arr.add(5d);
+ assertThat(Utils.median(arr), is(3d));
+ }
+
+ @Test
+ public void testMean() {
+ assertThat(Utils.mean(new double[]{-1,1,2,3}), is(1.25d));
+ }
+
+ @Test
+ public void testMean_singleNumber() {
+ assertThat(Utils.mean(new double[]{0}), is(0d));
+ }
+
+ @Test
+ public void testMean_empty() {
+ assertThat(Utils.mean(new double[]{}), is(NaN));
+ }
+
+ @Test
+ public void testMean_repeatedNumbers() {
+ assertThat(Utils.mean(new double[]{5,5,5,5}), is(5d));
+ }
+
+ @Test
+ public void testInterp() {
+ assertThat(Utils.interp(new double[]{5,6,16,17}, new double[]{0, 10, 12, 18},
+ new double[]{35, 50, 75, 93}), is(new double[]{42.5, 44, 87, 90}));
+ }
+
+ @Test
+ public void testInterp_singleNumber() {
+ assertThat(Utils.interp(new double[]{5}, new double[]{0, 10},
+ new double[]{35, 50}), is(new double[]{42.5}));
+ }
+
+ @Test
+ public void testInterp_twoNumbers() {
+ assertThat(Utils.interp(new double[]{0}, new double[]{0, 10},
+ new double[]{35, 50}), is(new double[]{35}));
+ }
+
+ @Test
+ public void testInterp_numberContained() {
+ assertThat(Utils.interp(new double[]{5, 10}, new double[]{0, 5, 10},
+ new double[]{35, 19, 50}), is(new double[]{19, 50}));
+ }
+
+ @Test
+ public void testStdev() {
+ assertThat(Utils.stdev(new double[]{10,12,14,18}), is(Math.sqrt(8.75)));
+ }
+
+ @Test
+ public void testStdev_empty() {
+ assertThat(Utils.stdev(new double[]{}), is(NaN));
+ }
+
+ @Test
+ public void testStdev_singleNumber() {
+ assertThat(Utils.stdev(new double[]{42}), is(0d));
+ }
+
+ @Test
+ public void testStdev_manyNumbers() {
+ assertThat(Utils.stdev(new double[]{-1,0,1}), is(Math.sqrt(2d/3d)));
+ }
+
+ @Test
+ public void testExtract() {
+ assertThat(Utils.extract(new int[]{1, 2, 2, 1, 2, 2, 1, 2, 2}, 1,
+ new double[]{1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5}),
+ is(new double[]{1.5, 4.5, 7.5}));
+ }
+
+ @Test
+ public void testExtract_empty() {
+ assertThat(Utils.extract(new int[]{}, 1, new double[]{}), is(new double[]{}));
+ }
+
+ @Test
+ public void testArgmin() {
+ assertThat(Utils.argmin(new double[]{5, 2, 1, -10, -20, 5, 19, 100}), is(4));
+ }
+
+ @Test
+ public void testArgmin_empty() {
+ assertThat(Utils.argmin(new double[]{}), is(0));
+ }
+
+ @Test
+ public void testFindBestShift() {
+ Random rand = new Random(42);
+ double latency = 12.34;
+ double[] touchTimes = new double[4000];
+ for (int i = 0; i < touchTimes.length; i++) {
+ // touch events every millisecond with some jitter
+ touchTimes[i] = i + rand.nextDouble()*0.2 - 0.1;
+ }
+ double[] touchY = new double[touchTimes.length];
+ for (int i = 0; i < touchY.length; i++) {
+ // sine wave will oscillate 1 time
+ touchY[i] = 1000*Math.cos((touchTimes[i] - latency) * Math.PI/500) + rand.nextDouble()*0.02 - 0.01;
+ }
+ double[] laserTimes = new double[4];
+ int i = 0;
+ for (int root = 0; root < 1000; root+=1000) {
+ laserTimes[i++] = root + 250 - 10;
+ laserTimes[i++] = root + 250 + 10;
+ laserTimes[i++] = root + 750 - 10;
+ laserTimes[i++] = root + 750 + 10;
+ }
+ assertEquals(latency, Utils.findBestShift(laserTimes, touchTimes, touchY), 1e-6);
+ }
+}