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