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 uncaughtException(Thread thread, Throwable ex)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 onResume()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 onNewIntent(Intent intent)179 protected void onNewIntent(Intent intent) { 180 super.onNewIntent(intent); 181 setIntent(intent); 182 } 183 184 @Override onCreate(Bundle savedInstanceState)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 onCreateOptionsMenu(Menu menu)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 toast(String msg)245 public void toast(String msg) { 246 logger.log(msg); 247 Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); 248 } 249 250 @Override onSupportNavigateUp()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 onOptionsItemSelected(MenuItem item)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 switchScreen(Fragment newFragment, String title)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 onClickClockSync(View view)292 public void onClickClockSync(View view) { 293 DiagnosticsFragment diagnosticsFragment = new DiagnosticsFragment(); 294 switchScreen(diagnosticsFragment, "Diagnostics"); 295 } 296 onClickTapLatency(View view)297 public void onClickTapLatency(View view) { 298 TapLatencyFragment newFragment = new TapLatencyFragment(); 299 requestSystraceWritePermission(); 300 switchScreen(newFragment, "Tap Latency"); 301 } 302 onClickScreenResponse(View view)303 public void onClickScreenResponse(View view) { 304 ScreenResponseFragment newFragment = new ScreenResponseFragment(); 305 requestSystraceWritePermission(); 306 switchScreen(newFragment, "Screen Response"); 307 } 308 onClickAudio(View view)309 public void onClickAudio(View view) { 310 AudioFragment newFragment = new AudioFragment(); 311 switchScreen(newFragment, "Audio Latency"); 312 } 313 onClickMIDI(View view)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 onClickDragLatency(View view)323 public void onClickDragLatency(View view) { 324 DragLatencyFragment newFragment = new DragLatencyFragment(); 325 switchScreen(newFragment, "Drag Latency"); 326 } 327 onClickOpenLog(View view)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 onClickOpenAbout(View view)334 public void onClickOpenAbout(View view) { 335 AboutFragment aboutFragment = new AboutFragment(); 336 switchScreen(aboutFragment, "About"); 337 } 338 onClickOpenSettings(View view)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 //////////////////////////////////////////////////////////////////////////////////////////////// onClickReconnect(View view)347 public void onClickReconnect(View view) { 348 waltDevice.connect(); 349 } 350 onClickPing(View view)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 onClickStartListener(View view)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 onClickSync(View view)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 onClickCheckDrift(View view)384 public void onClickCheckDrift(View view) { 385 waltDevice.checkDrift(); 386 } 387 onClickProgram(View view)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 attemptSaveAndShareLog()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 attemptSaveLog()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 attemptClearLog()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 onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)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 saveLogToFile()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 clearLogFile()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 shareLogFile(String filepath)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 startsWithHttp(String url)543 private static boolean startsWithHttp(String url) { 544 return url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://"); 545 } 546 showUploadLogDialog()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 requestSystraceWritePermission()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