diff options
Diffstat (limited to 'android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java')
-rw-r--r-- | android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java | 536 |
1 files changed, 536 insertions, 0 deletions
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); + } + } + } + +} |