diff options
Diffstat (limited to 'android/WALT')
123 files changed, 11698 insertions, 0 deletions
diff --git a/android/WALT/.gitignore b/android/WALT/.gitignore new file mode 100644 index 0000000..579b981 --- /dev/null +++ b/android/WALT/.gitignore @@ -0,0 +1,10 @@ +.gradle +.idea +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +/app/src/main/obj +/app/src/main/libs diff --git a/android/WALT/WALT.iml b/android/WALT/WALT.iml new file mode 100644 index 0000000..628d221 --- /dev/null +++ b/android/WALT/WALT.iml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module external.linked.project.id="WALT" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> + <component name="FacetManager"> + <facet type="java-gradle" name="Java-Gradle"> + <configuration> + <option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" /> + <option name="BUILDABLE" value="false" /> + </configuration> + </facet> + </component> + <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <excludeFolder url="file://$MODULE_DIR$/.gradle" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module>
\ No newline at end of file diff --git a/android/WALT/app/.gitignore b/android/WALT/app/.gitignore new file mode 100644 index 0000000..cc037c4 --- /dev/null +++ b/android/WALT/app/.gitignore @@ -0,0 +1,2 @@ +/build +app.iml diff --git a/android/WALT/app/build.gradle b/android/WALT/app/build.gradle new file mode 100644 index 0000000..531142e --- /dev/null +++ b/android/WALT/app/build.gradle @@ -0,0 +1,50 @@ +apply plugin: 'com.android.model.application' + +model { + android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + + defaultConfig { + applicationId "org.chromium.latency.walt" + minSdkVersion.apiLevel 17 + targetSdkVersion.apiLevel 23 + versionCode 8 + versionName "0.1.7" + } + ndk { + moduleName "sync_clock_jni" + CFlags.addAll "-I${project.rootDir}/app/src/main/jni".toString(), "-g", "-DUSE_LIBLOG", "-Werror" + ldLibs.addAll "OpenSLES", "log" + } + buildTypes { + release { + minifyEnabled false + proguardFiles.add(file("proguard-rules.pro")) + } + debug { + ndk { + debuggable true + } + } + } + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:25.1.0' + compile 'com.android.support:design:25.1.0' + compile 'com.android.support:preference-v7:25.1.0' + compile 'com.android.support:preference-v14:25.1.0' + compile 'com.github.PhilJay:MPAndroidChart:v3.0.1' + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile ('org.powermock:powermock-api-mockito:1.6.2') { + exclude module: 'hamcrest-core' + exclude module: 'objenesis' + } + testCompile ('org.powermock:powermock-module-junit4:1.6.2') { + exclude module: 'hamcrest-core' + exclude module: 'objenesis' + } +} diff --git a/android/WALT/app/proguard-rules.pro b/android/WALT/app/proguard-rules.pro new file mode 100644 index 0000000..2d2fcf0 --- /dev/null +++ b/android/WALT/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in <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 Binary files differnew file mode 100644 index 0000000..b7c8868 --- /dev/null +++ b/android/WALT/app/src/main/res/mipmap-hdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000..3f22278 --- /dev/null +++ b/android/WALT/app/src/main/res/mipmap-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000..86877de --- /dev/null +++ b/android/WALT/app/src/main/res/mipmap-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000..6ff2e63 --- /dev/null +++ b/android/WALT/app/src/main/res/mipmap-xxhdpi/ic_launcher.png 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); + } +} diff --git a/android/WALT/build.gradle b/android/WALT/build.gradle new file mode 100644 index 0000000..aaedccb --- /dev/null +++ b/android/WALT/build.gradle @@ -0,0 +1,20 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle-experimental:0.8.3' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + maven { url "https://jitpack.io" } + } +} diff --git a/android/WALT/gradle.properties b/android/WALT/gradle.properties new file mode 100644 index 0000000..1d3591c --- /dev/null +++ b/android/WALT/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true
\ No newline at end of file diff --git a/android/WALT/gradle/wrapper/gradle-wrapper.jar b/android/WALT/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 0000000..05ef575 --- /dev/null +++ b/android/WALT/gradle/wrapper/gradle-wrapper.jar diff --git a/android/WALT/gradle/wrapper/gradle-wrapper.properties b/android/WALT/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..aa3841f --- /dev/null +++ b/android/WALT/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Nov 29 15:45:52 EST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/android/WALT/gradlew b/android/WALT/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/android/WALT/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android/WALT/gradlew.bat b/android/WALT/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/android/WALT/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/WALT/settings.gradle b/android/WALT/settings.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/android/WALT/settings.gradle @@ -0,0 +1 @@ +include ':app' |