package com.tns; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.Date; import android.Manifest; import android.app.Activity; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.support.design.widget.TabLayout; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.text.method.ScrollingMovementMethod; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; class ErrorReport implements TabLayout.OnTabSelectedListener { public static final String ERROR_FILE_NAME = "hasError"; private static AppCompatActivity activity; private TabLayout tabLayout; private ViewPager viewPager; private Context context; private static String exceptionMsg; private static String logcatMsg; private static boolean checkingForPermissions = false; private final static String EXTRA_NATIVESCRIPT_ERROR_REPORT = "NativeScriptErrorMessage"; private final static String EXTRA_ERROR_REPORT_MSG = "msg"; private final static String EXTRA_PID = "pID"; private final static int EXTRA_ERROR_REPORT_VALUE = 1; private static final int REQUEST_EXTERNAL_STORAGE = 1; private static String[] PERMISSIONS_STORAGE = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; // Will prevent error activity from killing process if permission request dialog pops up public static boolean isCheckingForPermissions() { return checkingForPermissions; } public static void resetCheckingForPermissions() { checkingForPermissions = false; } // The following will not compile if uncommented with compileSdk lower than 23 public static void verifyStoragePermissions(Activity activity) { // Check if we have write permission final int version = Build.VERSION.SDK_INT; if (version >= 23) { try { // Necessary to work around compile errors with compileSdk 22 and lower Method checkSelfPermissionMethod; try { checkSelfPermissionMethod = ActivityCompat.class.getMethod("checkSelfPermission", Context.class, String.class); } catch (NoSuchMethodException e) { // method wasn't found, so there is no need to handle permissions explicitly e.printStackTrace(); return; } int permission = (int) checkSelfPermissionMethod.invoke(null, activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); if (permission != PackageManager.PERMISSION_GRANTED) { // We don't have permission so prompt the user Method requestPermissionsMethod = ActivityCompat.class.getMethod("requestPermissions", Activity.class, PERMISSIONS_STORAGE.getClass(), int.class); checkingForPermissions = true; requestPermissionsMethod.invoke(null, activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); } } catch (Exception e) { Toast.makeText(activity, "Couldn't resolve permissions", Toast.LENGTH_LONG).show(); e.printStackTrace(); return; } } } public ErrorReport(AppCompatActivity activity) { ErrorReport.activity = activity; this.context = activity.getApplicationContext(); } static boolean startActivity(final Context context, String errorMessage) { final Intent intent = getIntent(context); if (intent == null) { return false; // (if in release mode) don't do anything } intent.putExtra(EXTRA_ERROR_REPORT_MSG, errorMessage); String PID = Integer.toString(android.os.Process.myPid()); intent.putExtra(EXTRA_PID, PID); createErrorFile(context); try { startPendingErrorActivity(context, intent); } catch (CanceledException e) { Log.d("ErrorReport", "Couldn't send pending intent! Exception: " + e.getMessage()); } killProcess(context); return true; } static void killProcess(Context context) { // finish current activity and all below it first if (context instanceof Activity) { ((Activity) context).finishAffinity(); } // kill process android.os.Process.killProcess(android.os.Process.myPid()); } static void startPendingErrorActivity(Context context, Intent intent) throws CanceledException { PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); pendingIntent.send(context, 0, intent); } static String getErrorMessage(Throwable ex) { String content; PrintStream ps = null; try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ps = new PrintStream(baos); ex.printStackTrace(ps); try { content = baos.toString("US-ASCII"); } catch (UnsupportedEncodingException e) { content = e.getMessage(); } } finally { if (ps != null) { ps.close(); } } return content; } /* * Gets the process Id of the running app and filters all * output that doesn't belong to that process * */ public static String getLogcat(String pId) { String content; try { String logcatCommand = "logcat -d"; Process process = java.lang.Runtime.getRuntime().exec(logcatCommand); BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(process.getInputStream())); StringBuilder log = new StringBuilder(); String line = ""; String lineSeparator = System.getProperty("line.separator"); while ((line = bufferedReader.readLine()) != null) { if (line.contains(pId)) { log.append(line); log.append(lineSeparator); } } content = log.toString(); } catch (IOException e) { content = "Failed to read logcat"; Log.e("TNS.Android", content); } return content; } static Intent getIntent(Context context) { Class<?> errorActivityClass; if (Util.isDebuggableApp(context)) { errorActivityClass = ErrorReportActivity.class; } else { return null; } Intent intent = new Intent(context, errorActivityClass); intent.putExtra(EXTRA_NATIVESCRIPT_ERROR_REPORT, EXTRA_ERROR_REPORT_VALUE); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); return intent; } static boolean hasIntent(Intent intent) { int value = intent.getIntExtra(EXTRA_NATIVESCRIPT_ERROR_REPORT, 0); return value == EXTRA_ERROR_REPORT_VALUE; } void buildUI() { Intent intent = activity.getIntent(); exceptionMsg = intent.getStringExtra(EXTRA_ERROR_REPORT_MSG); String processId = intent.getStringExtra(EXTRA_PID); logcatMsg = getLogcat(processId); int errActivityId = this.context.getResources().getIdentifier("error_activity", "layout", this.context.getPackageName()); activity.setContentView(errActivityId); int toolBarId = this.context.getResources().getIdentifier("toolbar", "id", this.context.getPackageName()); Toolbar toolbar = (Toolbar) activity.findViewById(toolBarId); activity.setSupportActionBar(toolbar); final int tabLayoutId = this.context.getResources().getIdentifier("tabLayout", "id", this.context.getPackageName()); tabLayout = (TabLayout) activity.findViewById(tabLayoutId); tabLayout.addTab(tabLayout.newTab().setText("Exception")); tabLayout.addTab(tabLayout.newTab().setText("Logcat")); tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); int pagerId = this.context.getResources().getIdentifier("pager", "id", this.context.getPackageName()); viewPager = (ViewPager) activity.findViewById(pagerId); Pager adapter = new Pager(activity.getSupportFragmentManager(), tabLayout.getTabCount()); viewPager.setAdapter(adapter); viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { tabLayout.getTabAt(position).select(); viewPager.setCurrentItem(position); } @Override public void onPageScrollStateChanged(int state) { } }); tabLayout.setOnTabSelectedListener(this); } private static void createErrorFile(final Context context) { try { File errFile = new File(context.getFilesDir(), ERROR_FILE_NAME); errFile.createNewFile(); } catch (IOException e) { Log.d("ErrorReport", e.getMessage()); } } @Override public void onTabSelected(TabLayout.Tab tab) { viewPager.setCurrentItem(tab.getPosition()); } @Override public void onTabUnselected(TabLayout.Tab tab) { } @Override public void onTabReselected(TabLayout.Tab tab) { viewPager.setCurrentItem(tab.getPosition()); } private class Pager extends FragmentStatePagerAdapter { int tabCount; public Pager(FragmentManager fm, int tabCount) { super(fm); this.tabCount = tabCount; } @Override public Fragment getItem(int position) { switch (position) { case 0: return new ExceptionTab(); case 1: return new LogcatTab(); default: return null; } } @Override public int getCount() { return tabCount; } } public static class ExceptionTab extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { int exceptionTabId = container.getContext().getResources().getIdentifier("exception_tab", "layout", container.getContext().getPackageName()); View view = inflater.inflate(exceptionTabId, container, false); int txtViewId = container.getContext().getResources().getIdentifier("txtErrorMsg", "id", container.getContext().getPackageName()); TextView txtErrorMsg = (TextView) view.findViewById(txtViewId); txtErrorMsg.setText(exceptionMsg); txtErrorMsg.setMovementMethod(new ScrollingMovementMethod()); int btnCopyExceptionId = container.getContext().getResources().getIdentifier("btnCopyException", "id", container.getContext().getPackageName()); Button copyToClipboard = (Button) view.findViewById(btnCopyExceptionId); copyToClipboard.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("nsError", exceptionMsg); clipboard.setPrimaryClip(clip); } }); return view; } } public static class LogcatTab extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { int logcatTabId = container.getContext().getResources().getIdentifier("logcat_tab", "layout", container.getContext().getPackageName()); View view = inflater.inflate(logcatTabId, container, false); int textViewId = container.getContext().getResources().getIdentifier("logcatMsg", "id", container.getContext().getPackageName()); TextView txtlogcatMsg = (TextView) view.findViewById(textViewId); txtlogcatMsg.setText(logcatMsg); txtlogcatMsg.setMovementMethod(new ScrollingMovementMethod()); final String logName = "Log-" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()); int btnCopyLogcatId = container.getContext().getResources().getIdentifier("btnCopyLogcat", "id", container.getContext().getPackageName()); Button copyToClipboard = (Button) view.findViewById(btnCopyLogcatId); copyToClipboard.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { verifyStoragePermissions(activity); if (!isCheckingForPermissions()) { try { File dir = new File(Environment.getExternalStorageDirectory().getPath() + "/logcat-reports/"); dir.mkdirs(); File logcatReportFile = new File(dir, logName); FileOutputStream stream = new FileOutputStream(logcatReportFile); OutputStreamWriter writer = new OutputStreamWriter(stream, "UTF-8"); writer.write(logcatMsg); writer.close(); String logPath = dir.getPath() + "/" + logName; ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("logPath", logPath); clipboard.setPrimaryClip(clip); Toast.makeText(activity, "Path copied to clipboard: " + logPath, Toast.LENGTH_LONG).show(); } catch (Exception e) { String err = "Could not write logcat report to sdcard. Make sure you have allowed access to external storage!"; Toast.makeText(activity, err, Toast.LENGTH_LONG).show(); e.printStackTrace(); } } } }); return view; } } }