Home | History | Annotate | Download | only in walt
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package org.chromium.latency.walt;
     18 
     19 import android.Manifest;
     20 import android.content.DialogInterface;
     21 import android.content.Intent;
     22 import android.content.SharedPreferences;
     23 import android.content.pm.PackageManager;
     24 import android.hardware.usb.UsbDevice;
     25 import android.hardware.usb.UsbManager;
     26 import android.media.AudioManager;
     27 import android.net.Uri;
     28 import android.os.Build;
     29 import android.os.Bundle;
     30 import android.os.Environment;
     31 import android.os.Handler;
     32 import android.os.StrictMode;
     33 import android.preference.PreferenceManager;
     34 import android.support.annotation.NonNull;
     35 import android.support.v4.app.ActivityCompat;
     36 import android.support.v4.app.Fragment;
     37 import android.support.v4.app.FragmentManager;
     38 import android.support.v4.app.FragmentTransaction;
     39 import android.support.v4.content.ContextCompat;
     40 import android.support.v4.content.Loader;
     41 import android.support.v4.content.LocalBroadcastManager;
     42 import android.support.v7.app.AlertDialog;
     43 import android.support.v7.app.AppCompatActivity;
     44 import android.support.v7.widget.Toolbar;
     45 import android.util.Log;
     46 import android.view.Menu;
     47 import android.view.MenuItem;
     48 import android.view.View;
     49 import android.widget.EditText;
     50 import android.widget.Toast;
     51 
     52 import org.chromium.latency.walt.programmer.Programmer;
     53 
     54 import java.io.File;
     55 import java.io.FileOutputStream;
     56 import java.io.IOException;
     57 import java.io.PrintWriter;
     58 import java.io.StringWriter;
     59 import java.util.Date;
     60 import java.util.Locale;
     61 
     62 import static org.chromium.latency.walt.Utils.getBooleanPreference;
     63 
     64 public class MainActivity extends AppCompatActivity {
     65     private static final String TAG = "WALT";
     66     private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SHARE_LOG = 2;
     67     private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SYSTRACE = 3;
     68     private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_WRITE_LOG = 4;
     69     private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_CLEAR_LOG = 5;
     70 
     71     private static final String LOG_FILENAME = "qstep_log.txt";
     72 
     73     private Toolbar toolbar;
     74     LocalBroadcastManager broadcastManager;
     75     private SimpleLogger logger;
     76     private WaltDevice waltDevice;
     77     public Menu menu;
     78 
     79     public Handler handler = new Handler();
     80 
     81     private Fragment mRobotAutomationFragment;
     82 
     83 
     84     /**
     85      * A method to display exceptions on screen. This is very useful because our USB port is taken
     86      * and we often need to debug without adb.
     87      * Based on this article:
     88      * https://trivedihardik.wordpress.com/2011/08/20/how-to-avoid-force-close-error-in-android/
     89      */
     90     public class LoggingExceptionHandler implements java.lang.Thread.UncaughtExceptionHandler {
     91 
     92         @Override
     93         public void uncaughtException(Thread thread, Throwable ex) {
     94             StringWriter stackTrace = new StringWriter();
     95             ex.printStackTrace(new PrintWriter(stackTrace));
     96             String msg = "WALT crashed with the following exception:\n" + stackTrace;
     97 
     98             // Fire a new activity showing the stack trace
     99             Intent intent = new Intent(MainActivity.this, CrashLogActivity.class);
    100             intent.putExtra("crash_log", msg);
    101             MainActivity.this.startActivity(intent);
    102 
    103             // Terminate this process
    104             android.os.Process.killProcess(android.os.Process.myPid());
    105             System.exit(10);
    106         }
    107     }
    108 
    109     @Override
    110     protected void onResume() {
    111         super.onResume();
    112 
    113         final UsbDevice usbDevice;
    114         Intent intent = getIntent();
    115         if (intent != null && intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
    116             setIntent(null); // done with the intent
    117             usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
    118         } else {
    119             usbDevice = null;
    120         }
    121 
    122         // Connect and sync clocks, but a bit later as it takes time
    123         handler.postDelayed(new Runnable() {
    124             @Override
    125             public void run() {
    126                 if (usbDevice == null) {
    127                     waltDevice.connect();
    128                 } else {
    129                     waltDevice.connect(usbDevice);
    130                 }
    131             }
    132         }, 1000);
    133 
    134         if (intent != null && AutoRunFragment.TEST_ACTION.equals(intent.getAction())) {
    135             getSupportFragmentManager().popBackStack("Automated Test",
    136                     FragmentManager.POP_BACK_STACK_INCLUSIVE);
    137             Fragment autoRunFragment = new AutoRunFragment();
    138             autoRunFragment.setArguments(intent.getExtras());
    139             switchScreen(autoRunFragment, "Automated Test");
    140         }
    141 
    142         // Handle robot automation originating from adb shell am
    143         if (intent != null && Intent.ACTION_SEND.equals(intent.getAction())) {
    144             Log.e(TAG, "Received Intent: " + intent.toString());
    145             String test = intent.getStringExtra("StartTest");
    146             if (test != null) {
    147                 Log.e(TAG, "Extras \"StartTest\" = " + test);
    148                 if ("TapLatencyTest".equals(test)) {
    149                     mRobotAutomationFragment = new TapLatencyFragment();
    150                     switchScreen(mRobotAutomationFragment, "Tap Latency");
    151                 } else if ("ScreenResponseTest".equals(test)) {
    152                     mRobotAutomationFragment = new ScreenResponseFragment();
    153                     switchScreen(mRobotAutomationFragment, "Screen Response");
    154                 } else if ("DragLatencyTest".equals(test)) {
    155                     mRobotAutomationFragment = new DragLatencyFragment();
    156                     switchScreen(mRobotAutomationFragment, "Drag Latency");
    157                 }
    158             }
    159 
    160             String robotEvent = intent.getStringExtra("RobotAutomationEvent");
    161             if (robotEvent != null && mRobotAutomationFragment != null) {
    162                 Log.e(TAG, "Received robot automation event=\"" + robotEvent + "\", Fragment = " +
    163                         mRobotAutomationFragment);
    164                 // Writing and clearing the log is not fragment-specific, so handle them here.
    165                 if (robotEvent.equals(RobotAutomationListener.WRITE_LOG_EVENT)) {
    166                     attemptSaveLog();
    167                 } else if (robotEvent.equals(RobotAutomationListener.CLEAR_LOG_EVENT)) {
    168                     attemptClearLog();
    169                 } else {
    170                     // All other robot automation events are forwarded to the current fragment.
    171                     ((RobotAutomationListener) mRobotAutomationFragment)
    172                             .onRobotAutomationEvent(robotEvent);
    173                 }
    174             }
    175         }
    176     }
    177 
    178     @Override
    179     protected void onNewIntent(Intent intent) {
    180         super.onNewIntent(intent);
    181         setIntent(intent);
    182     }
    183 
    184     @Override
    185     protected void onCreate(Bundle savedInstanceState) {
    186         super.onCreate(savedInstanceState);
    187         Thread.setDefaultUncaughtExceptionHandler(new LoggingExceptionHandler());
    188         setContentView(R.layout.activity_main);
    189 
    190         // App bar
    191         toolbar = (Toolbar) findViewById(R.id.toolbar_main);
    192         setSupportActionBar(toolbar);
    193         getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
    194             @Override
    195             public void onBackStackChanged() {
    196                 int stackTopIndex = getSupportFragmentManager().getBackStackEntryCount() - 1;
    197                 if (stackTopIndex >= 0) {
    198                     toolbar.setTitle(getSupportFragmentManager().getBackStackEntryAt(stackTopIndex).getName());
    199                 } else {
    200                     toolbar.setTitle(R.string.app_name);
    201                     getSupportActionBar().setDisplayHomeAsUpEnabled(false);
    202                     // Disable fullscreen mode
    203                     getSupportActionBar().show();
    204                     getWindow().getDecorView().setSystemUiVisibility(0);
    205                 }
    206             }
    207         });
    208 
    209         waltDevice = WaltDevice.getInstance(this);
    210 
    211         // Create front page fragment
    212         FrontPageFragment frontPageFragment = new FrontPageFragment();
    213         FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    214         transaction.add(R.id.fragment_container, frontPageFragment);
    215         transaction.commit();
    216 
    217         logger = SimpleLogger.getInstance(this);
    218         broadcastManager = LocalBroadcastManager.getInstance(this);
    219 
    220         // Add basic version and device info to the log
    221         logger.log(String.format("WALT v%s  (versionCode=%d)",
    222                 BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
    223         logger.log("WALT protocol version " + WaltDevice.PROTOCOL_VERSION);
    224         logger.log("DEVICE INFO:");
    225         logger.log("  " + Build.FINGERPRINT);
    226         logger.log("  Build.SDK_INT=" + Build.VERSION.SDK_INT);
    227         logger.log("  os.version=" + System.getProperty("os.version"));
    228 
    229         // Set volume buttons to control media volume
    230         setVolumeControlStream(AudioManager.STREAM_MUSIC);
    231         requestSystraceWritePermission();
    232         // Allow network operations on the main thread
    233         StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
    234         StrictMode.setThreadPolicy(policy);
    235     }
    236 
    237     @Override
    238     public boolean onCreateOptionsMenu(Menu menu) {
    239         // Inflate the menu; this adds items to the action bar if it is present.
    240         getMenuInflater().inflate(R.menu.menu_main, menu);
    241         this.menu = menu;
    242         return true;
    243     }
    244 
    245     public void toast(String msg) {
    246         logger.log(msg);
    247         Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    248     }
    249 
    250     @Override
    251     public boolean onSupportNavigateUp() {
    252         // Go back when the back or up button on toolbar is clicked
    253         getSupportFragmentManager().popBackStack();
    254         return true;
    255     }
    256 
    257     @Override
    258     public boolean onOptionsItemSelected(MenuItem item) {
    259         // Handle action bar item clicks here. The action bar will
    260         // automatically handle clicks on the Home/Up button, so long
    261         // as you specify a parent activity in AndroidManifest.xml.
    262 
    263         Log.i(TAG, "Toolbar button: " + item.getTitle());
    264 
    265         switch (item.getItemId()) {
    266             case R.id.action_help:
    267                 return true;
    268             case R.id.action_share:
    269                 attemptSaveAndShareLog();
    270                 return true;
    271             case R.id.action_upload:
    272                 showUploadLogDialog();
    273                 return true;
    274             default:
    275                 return super.onOptionsItemSelected(item);
    276         }
    277     }
    278 
    279     ////////////////////////////////////////////////////////////////////////////////////////////////
    280     // Handlers for main menu clicks
    281     ////////////////////////////////////////////////////////////////////////////////////////////////
    282 
    283     private void switchScreen(Fragment newFragment, String title) {
    284         getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    285         toolbar.setTitle(title);
    286         FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    287         transaction.replace(R.id.fragment_container, newFragment);
    288         transaction.addToBackStack(title);
    289         transaction.commit();
    290     }
    291 
    292     public void onClickClockSync(View view) {
    293         DiagnosticsFragment diagnosticsFragment = new DiagnosticsFragment();
    294         switchScreen(diagnosticsFragment, "Diagnostics");
    295     }
    296 
    297     public void onClickTapLatency(View view) {
    298         TapLatencyFragment newFragment = new TapLatencyFragment();
    299         requestSystraceWritePermission();
    300         switchScreen(newFragment, "Tap Latency");
    301     }
    302 
    303     public void onClickScreenResponse(View view) {
    304         ScreenResponseFragment newFragment = new ScreenResponseFragment();
    305         requestSystraceWritePermission();
    306         switchScreen(newFragment, "Screen Response");
    307     }
    308 
    309     public void onClickAudio(View view) {
    310         AudioFragment newFragment = new AudioFragment();
    311         switchScreen(newFragment, "Audio Latency");
    312     }
    313 
    314     public void onClickMIDI(View view) {
    315         if (MidiFragment.hasMidi(this)) {
    316             MidiFragment newFragment = new MidiFragment();
    317             switchScreen(newFragment, "MIDI Latency");
    318         } else {
    319             toast("This device does not support MIDI");
    320         }
    321     }
    322 
    323     public void onClickDragLatency(View view) {
    324         DragLatencyFragment newFragment = new DragLatencyFragment();
    325         switchScreen(newFragment, "Drag Latency");
    326     }
    327 
    328     public void onClickOpenLog(View view) {
    329         LogFragment logFragment = new LogFragment();
    330         // menu.findItem(R.id.action_help).setVisible(false);
    331         switchScreen(logFragment, "Log");
    332     }
    333 
    334     public void onClickOpenAbout(View view) {
    335         AboutFragment aboutFragment = new AboutFragment();
    336         switchScreen(aboutFragment, "About");
    337     }
    338 
    339     public void onClickOpenSettings(View view) {
    340         SettingsFragment settingsFragment = new SettingsFragment();
    341         switchScreen(settingsFragment, "Settings");
    342     }
    343 
    344     ////////////////////////////////////////////////////////////////////////////////////////////////
    345     // Handlers for diagnostics menu clicks
    346     ////////////////////////////////////////////////////////////////////////////////////////////////
    347     public void onClickReconnect(View view) {
    348         waltDevice.connect();
    349     }
    350 
    351     public void onClickPing(View view) {
    352         long t1 = waltDevice.clock.micros();
    353         try {
    354             waltDevice.command(WaltDevice.CMD_PING);
    355             long dt = waltDevice.clock.micros() - t1;
    356             logger.log(String.format(Locale.US,
    357                     "Ping reply in %.1fms", dt / 1000.
    358             ));
    359         } catch (IOException e) {
    360             logger.log("Error sending ping: " + e.getMessage());
    361         }
    362     }
    363 
    364     public void onClickStartListener(View view) {
    365         if (waltDevice.isListenerStopped()) {
    366             try {
    367                 waltDevice.startListener();
    368             } catch (IOException e) {
    369                 logger.log("Error starting USB listener: " + e.getMessage());
    370             }
    371         } else {
    372             waltDevice.stopListener();
    373         }
    374     }
    375 
    376     public void onClickSync(View view) {
    377         try {
    378             waltDevice.syncClock();
    379         } catch (IOException e) {
    380             logger.log("Error syncing clocks: " + e.getMessage());
    381         }
    382     }
    383 
    384     public void onClickCheckDrift(View view) {
    385         waltDevice.checkDrift();
    386     }
    387 
    388     public void onClickProgram(View view) {
    389         if (waltDevice.isConnected()) {
    390             // show dialog telling user to first press white button
    391             final AlertDialog dialog = new AlertDialog.Builder(this)
    392                 .setTitle("Press white button")
    393                 .setMessage("Please press the white button on the WALT device.")
    394                 .setCancelable(false)
    395                 .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
    396                     @Override
    397                     public void onClick(DialogInterface dialog, int which) {}
    398                 }).show();
    399 
    400             waltDevice.setConnectionStateListener(new WaltConnection.ConnectionStateListener() {
    401                 @Override
    402                 public void onConnect() {}
    403 
    404                 @Override
    405                 public void onDisconnect() {
    406                     dialog.cancel();
    407                     handler.postDelayed(new Runnable() {
    408                         @Override
    409                         public void run() {
    410                             new Programmer(MainActivity.this).program();
    411                         }
    412                     }, 1000);
    413                 }
    414             });
    415         } else {
    416             new Programmer(this).program();
    417         }
    418     }
    419 
    420     private void attemptSaveAndShareLog() {
    421         int currentPermission = ContextCompat.checkSelfPermission(this,
    422                 Manifest.permission.WRITE_EXTERNAL_STORAGE);
    423         if (currentPermission == PackageManager.PERMISSION_GRANTED) {
    424             String filePath = saveLogToFile();
    425             shareLogFile(filePath);
    426         } else {
    427             ActivityCompat.requestPermissions(this,
    428                     new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
    429                     PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SHARE_LOG);
    430         }
    431     }
    432 
    433     private void attemptSaveLog() {
    434         int currentPermission = ContextCompat.checkSelfPermission(this,
    435                 Manifest.permission.WRITE_EXTERNAL_STORAGE);
    436         if (currentPermission == PackageManager.PERMISSION_GRANTED) {
    437             saveLogToFile();
    438         } else {
    439             ActivityCompat.requestPermissions(this,
    440                     new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
    441                     PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_WRITE_LOG);
    442         }
    443     }
    444 
    445     private void attemptClearLog() {
    446         int currentPermission = ContextCompat.checkSelfPermission(this,
    447                 Manifest.permission.WRITE_EXTERNAL_STORAGE);
    448         if (currentPermission == PackageManager.PERMISSION_GRANTED) {
    449             clearLogFile();
    450         } else {
    451             ActivityCompat.requestPermissions(this,
    452                     new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
    453                     PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_CLEAR_LOG);
    454         }
    455     }
    456 
    457     @Override
    458     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    459         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    460         final boolean isPermissionGranted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
    461         if (!isPermissionGranted) {
    462             logger.log("Could not get permission to write file to storage");
    463             return;
    464         }
    465         switch (requestCode) {
    466             case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SHARE_LOG:
    467                 attemptSaveAndShareLog();
    468                 break;
    469             case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_WRITE_LOG:
    470                 attemptSaveLog();
    471                 break;
    472             case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_CLEAR_LOG:
    473                 attemptClearLog();
    474                 break;
    475         }
    476     }
    477 
    478     public String saveLogToFile() {
    479 
    480         // Save to file to later fire an Intent.ACTION_SEND
    481         // This allows to either send the file as email attachment
    482         // or upload it to Drive.
    483 
    484         // The permissions for attachments are a mess, writing world readable files
    485         // is frowned upon, but deliberately giving permissions as part of the intent is
    486         // way too cumbersome.
    487 
    488         // A reasonable world readable location,on many phones it's /storage/emulated/Documents
    489         // TODO: make this location configurable?
    490         File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
    491         File file = null;
    492         FileOutputStream outStream = null;
    493 
    494         try {
    495             if (!path.exists()) {
    496                 path.mkdirs();
    497             }
    498             file = new File(path, LOG_FILENAME);
    499             logger.log("Saving log to: " + file + " at " + new Date());
    500 
    501             outStream = new FileOutputStream(file);
    502             outStream.write(logger.getLogText().getBytes());
    503 
    504             outStream.close();
    505             logger.log("Log saved");
    506         } catch (Exception e) {
    507             e.printStackTrace();
    508             logger.log("Failed to write log: " + e.getMessage());
    509         }
    510         return file.getPath();
    511     }
    512 
    513     public void clearLogFile() {
    514         File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
    515         try {
    516             File file = new File(path, LOG_FILENAME);
    517             file.delete();
    518         } catch (Exception e) {
    519             e.printStackTrace();
    520             logger.log("Failed to clear log: " + e.getMessage());
    521         }
    522     }
    523 
    524     public void shareLogFile(String filepath) {
    525         File file = new File(filepath);
    526         logger.log("Firing Intent.ACTION_SEND for file:");
    527         logger.log(file.getPath());
    528 
    529         Intent i = new Intent(Intent.ACTION_SEND);
    530         i.setType("text/plain");
    531 
    532         i.putExtra(Intent.EXTRA_SUBJECT, "WALT log");
    533         i.putExtra(Intent.EXTRA_TEXT, "Attaching log file " + file.getPath());
    534         i.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
    535 
    536         try {
    537             startActivity(Intent.createChooser(i, "Send mail..."));
    538         } catch (android.content.ActivityNotFoundException ex) {
    539             toast("There are no email clients installed.");
    540         }
    541     }
    542 
    543     private static boolean startsWithHttp(String url) {
    544         return url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://");
    545     }
    546 
    547     private void showUploadLogDialog() {
    548         final AlertDialog dialog = new AlertDialog.Builder(this)
    549                 .setTitle("Upload log to URL")
    550                 .setView(R.layout.dialog_upload)
    551                 .setPositiveButton("Upload", new DialogInterface.OnClickListener() {
    552                     @Override
    553                     public void onClick(DialogInterface dialog, int which) {}
    554                 })
    555                 .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
    556                     @Override
    557                     public void onClick(DialogInterface dialog, int which) {}
    558                 })
    559                 .show();
    560         final EditText editText = (EditText) dialog.findViewById(R.id.edit_text);
    561         editText.setText(Utils.getStringPreference(
    562                 MainActivity.this, R.string.preference_log_url, ""));
    563         dialog.getButton(AlertDialog.BUTTON_POSITIVE).
    564                 setOnClickListener(new View.OnClickListener() {
    565             @Override
    566             public void onClick(View v) {
    567                 View progress = dialog.findViewById(R.id.progress_bar);
    568                 String urlString = editText.getText().toString();
    569                 if (!startsWithHttp(urlString)) {
    570                     urlString = "http://" + urlString;
    571                 }
    572                 editText.setVisibility(View.GONE);
    573                 progress.setVisibility(View.VISIBLE);
    574                 LogUploader uploader = new LogUploader(MainActivity.this, urlString);
    575                 final String finalUrlString = urlString;
    576                 uploader.registerListener(1, new Loader.OnLoadCompleteListener<Integer>() {
    577                     @Override
    578                     public void onLoadComplete(Loader<Integer> loader, Integer data) {
    579                         dialog.cancel();
    580                         if (data == -1) {
    581                             Toast.makeText(MainActivity.this,
    582                                     "Failed to upload log", Toast.LENGTH_SHORT).show();
    583                             return;
    584                         } else if (data / 100 == 2) {
    585                             Toast.makeText(MainActivity.this,
    586                                     "Log successfully uploaded", Toast.LENGTH_SHORT).show();
    587                         } else {
    588                             Toast.makeText(MainActivity.this,
    589                                     "Failed to upload log. Server returned status code " + data,
    590                                     Toast.LENGTH_SHORT).show();
    591                         }
    592                         SharedPreferences preferences = PreferenceManager
    593                                 .getDefaultSharedPreferences(MainActivity.this);
    594                         preferences.edit().putString(
    595                                 getString(R.string.preference_log_url), finalUrlString).apply();
    596                     }
    597                 });
    598                 uploader.startUpload();
    599             }
    600         });
    601     }
    602 
    603     private void requestSystraceWritePermission() {
    604         if (getBooleanPreference(this, R.string.preference_systrace, true)) {
    605             int currentPermission = ContextCompat.checkSelfPermission(this,
    606                     Manifest.permission.WRITE_EXTERNAL_STORAGE);
    607             if (currentPermission != PackageManager.PERMISSION_GRANTED) {
    608                 ActivityCompat.requestPermissions(this,
    609                         new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
    610                         PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SYSTRACE);
    611             }
    612         }
    613     }
    614 
    615 }
    616