| /* |
| * 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 static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_WRITE_LOG = 4; |
| private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_CLEAR_LOG = 5; |
| |
| private static final String LOG_FILENAME = "qstep_log.txt"; |
| |
| private Toolbar toolbar; |
| LocalBroadcastManager broadcastManager; |
| private SimpleLogger logger; |
| private WaltDevice waltDevice; |
| public Menu menu; |
| |
| public Handler handler = new Handler(); |
| |
| private Fragment mRobotAutomationFragment; |
| |
| |
| /** |
| * 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"); |
| } |
| |
| // Handle robot automation originating from adb shell am |
| if (intent != null && Intent.ACTION_SEND.equals(intent.getAction())) { |
| Log.e(TAG, "Received Intent: " + intent.toString()); |
| String test = intent.getStringExtra("StartTest"); |
| if (test != null) { |
| Log.e(TAG, "Extras \"StartTest\" = " + test); |
| if ("TapLatencyTest".equals(test)) { |
| mRobotAutomationFragment = new TapLatencyFragment(); |
| switchScreen(mRobotAutomationFragment, "Tap Latency"); |
| } else if ("ScreenResponseTest".equals(test)) { |
| mRobotAutomationFragment = new ScreenResponseFragment(); |
| switchScreen(mRobotAutomationFragment, "Screen Response"); |
| } else if ("DragLatencyTest".equals(test)) { |
| mRobotAutomationFragment = new DragLatencyFragment(); |
| switchScreen(mRobotAutomationFragment, "Drag Latency"); |
| } |
| } |
| |
| String robotEvent = intent.getStringExtra("RobotAutomationEvent"); |
| if (robotEvent != null && mRobotAutomationFragment != null) { |
| Log.e(TAG, "Received robot automation event=\"" + robotEvent + "\", Fragment = " + |
| mRobotAutomationFragment); |
| // Writing and clearing the log is not fragment-specific, so handle them here. |
| if (robotEvent.equals(RobotAutomationListener.WRITE_LOG_EVENT)) { |
| attemptSaveLog(); |
| } else if (robotEvent.equals(RobotAutomationListener.CLEAR_LOG_EVENT)) { |
| attemptClearLog(); |
| } else { |
| // All other robot automation events are forwarded to the current fragment. |
| ((RobotAutomationListener) mRobotAutomationFragment) |
| .onRobotAutomationEvent(robotEvent); |
| } |
| } |
| } |
| } |
| |
| @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); |
| } |
| } |
| |
| private void attemptSaveLog() { |
| int currentPermission = ContextCompat.checkSelfPermission(this, |
| Manifest.permission.WRITE_EXTERNAL_STORAGE); |
| if (currentPermission == PackageManager.PERMISSION_GRANTED) { |
| saveLogToFile(); |
| } else { |
| ActivityCompat.requestPermissions(this, |
| new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, |
| PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_WRITE_LOG); |
| } |
| } |
| |
| private void attemptClearLog() { |
| int currentPermission = ContextCompat.checkSelfPermission(this, |
| Manifest.permission.WRITE_EXTERNAL_STORAGE); |
| if (currentPermission == PackageManager.PERMISSION_GRANTED) { |
| clearLogFile(); |
| } else { |
| ActivityCompat.requestPermissions(this, |
| new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, |
| PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_CLEAR_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; |
| case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_WRITE_LOG: |
| attemptSaveLog(); |
| break; |
| case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_CLEAR_LOG: |
| attemptClearLog(); |
| 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. |
| |
| // 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; |
| |
| try { |
| if (!path.exists()) { |
| path.mkdirs(); |
| } |
| file = new File(path, LOG_FILENAME); |
| logger.log("Saving log to: " + file + " at " + new Date()); |
| |
| outStream = new FileOutputStream(file); |
| outStream.write(logger.getLogText().getBytes()); |
| |
| outStream.close(); |
| logger.log("Log saved"); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| logger.log("Failed to write log: " + e.getMessage()); |
| } |
| return file.getPath(); |
| } |
| |
| public void clearLogFile() { |
| File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); |
| try { |
| File file = new File(path, LOG_FILENAME); |
| file.delete(); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| logger.log("Failed to clear log: " + e.getMessage()); |
| } |
| } |
| |
| 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); |
| } |
| } |
| } |
| |
| } |