/* Android IMSI-Catcher Detector | (c) AIMSICD Privacy Project * ----------------------------------------------------------- * LICENSE: http://git.io/vki47 | TERMS: http://git.io/vki4o * ----------------------------------------------------------- */ package com.secupwn.aimsicd.ui.activities; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.NavUtils; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.ScrollView; import android.widget.TextView; import com.secupwn.aimsicd.R; import com.secupwn.aimsicd.utils.Helpers; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import io.freefair.android.injection.annotation.Inject; import io.freefair.android.injection.annotation.InjectView; import io.freefair.android.injection.annotation.XmlLayout; import io.freefair.android.injection.annotation.XmlMenu; import io.freefair.android.util.logging.Logger; /** * Description: This class is providing for the Debug log feature in the swipe menu. * It reads the last 500 lines from the Logcat ring buffers: main and radio. * * Dependencies: * menu/activity_debug_logs.xml ** * layout/activity_debug_logs.xml * values/strings.xml * Issues: * * [ ] Are we clearing logcat when starting it? If we are, we miss all previous errors * if any has occurred. We need to clear the logcat when app starts. Also there * is no reason to clear it if we only catch the last 500 lines anyway. * * [ ] Add the output of "getprop |sort". But Java CLI processes doesn't handle pipes. * Try with: Collections.sort(list, String.CASE_INSENSITIVE_ORDER) * * [ ] Apparently the button for radio log has been added to the strings.xml, * but never implemented here. We need to add the buffer selector button * to the top bar, next to email icon button. ** * * TODO: [ ] We should add an XPrivacy button (or automatic) to add XPrivacy filters when used. */ @XmlLayout(R.layout.activity_debug_logs) @XmlMenu(R.menu.activity_debug_logs) public class DebugLogs extends BaseActivity { @Inject private Logger log; private LogUpdaterThread logUpdater = null; private boolean updateLogs = true; private boolean isRadioLogs = true; // Including this, should be a toggle. @InjectView(R.id.debug_log_view) private TextView logView; @InjectView(R.id.btnClear) private Button btnClear; @InjectView(R.id.btnCopy) private Button btnCopy; @InjectView(R.id.btnStopLogs) private Button btnStop; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Show the Up button in the action bar. getSupportActionBar().setDisplayHomeAsUpEnabled(true); runOnUiThread(new Runnable() { @Override public void run() { logView.setFocusable(false); } }); btnClear.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { try { clearLogs(); } catch (IOException e) { log.error("Error clearing logs", e); } } }); btnCopy.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); ClipData cd = ClipData.newPlainText("log", logView.getText()); clipboard.setPrimaryClip(cd); Helpers.msgShort(DebugLogs.this, getString(R.string.msg_copied_to_clipboard)); } }); btnStop.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (updateLogs) { updateLogs = false; btnStop.setText(getString(R.string.btn_start_logs)); } else { startLogging(); } } }); /* // logcat radio buffer toggle on/off btnRadio.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (isRadioLogs) { isRadioLogs = false; btnRadio.setText(getString(R.string.btn_radio_logs)); } else { isRadioLogs = true; btnRadio.setText(getString(R.string.btn_main_logs)); } } }); */ } @Override protected void onPause() { updateLogs = false; // exit the log updater super.onPause(); } @Override protected void onResume() { super.onResume(); startLogging(); } private void startLogging() { updateLogs = true; logUpdater = new LogUpdaterThread(); logUpdater.start(); btnStop.setText(getString(R.string.btn_stop_logs)); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_send_logs: sendEmail(); return true; case android.R.id.home: // This ID represents the Home or Up button. In the case of this // activity, the Up button is shown. Use NavUtils to allow users // to navigate up one level in the application structure. For // more details, see the Navigation pattern on Android Design: // // http://developer.android.com/design/patterns/navigation.html#up-vs-back // NavUtils.navigateUpFromSameTask(this); return true; } return super.onOptionsItemSelected(item); } public void sendEmail() { new Thread() { @Override public void run() { // Send Error Log try { String helpUs = getString(R.string.describe_the_problem_you_had); String log = helpUs + "\n\n" + "GETPROP:" + "\n\n" + getProp() + "\n\n" + "LOGCAT:" + "\n\n" + getLogs() + "\n\n" + helpUs; // show a share intent Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/html"); // E-Mail address will ONLY be handed out when a DEVELOPER asked for the logs! intent.putExtra(Intent.EXTRA_EMAIL, new String[]{"See GitHub Issues first!"}); intent.putExtra(Intent.EXTRA_SUBJECT, "AIMSICD Error Log"); intent.putExtra(Intent.EXTRA_TEXT, log); startActivity(Intent.createChooser(intent, "Send Error Log")); } catch (IOException e) { log.warn("Error reading logs", e); } } }.start(); } /** * Read getprop and return the sorted result as a string * * TODO: Need a way to sort properties for easy reading * * @return * @throws IOException */ public String getProp() throws IOException { return runProcess("/system/bin/getprop"); } /** * Description: Read logcat and return as a string * * Notes: * * 1) " *:V" makes log very spammy due to verbose OemRilRequestRaw debug output (AIMSICD_Helpers). * ==> Now disabled! * 2) Need to silent some spammy Samsung Galaxy's: " AbsListView:S PackageInfo:S" * 3) Need to silent some Qualcomm QMI: " RILQ:S" * 4) Need to silent some Qualcomm GPS: " LocSvc_eng:S LocSvc_adapter:S LocSvc_afw:S" * 5) "-d" is not necessary when using "-t". * 6) Need to silent some spammy HTC's: "QC-QMI:S AudioPolicyManager:S" * 7) Need to silent some spammy XPrivacy items: "XPrivacy/XRuntime:S Xposed:S" * 8) Need to silent even more XPrivacy items: "XPrivacy/XTelephonyManager:S XPrivacy/XLocationManager:S XPrivacy/XPackageManager:S" * */ private String getLogs() throws IOException { return runProcess( "logcat -t 500 -v brief -b main" + (isRadioLogs ? " -b radio RILQ:S" : "") + " AbsListView:S PackageInfo:S" + " LocSvc_eng:S LocSvc_adapter:S LocSvc_afw:S" + " QC-QMI:S AudioPolicyManager:S" + " XPrivacy/XRuntime:S Xposed:S" + " XPrivacy/XTelephonyManager:S XPrivacy/XLocationManager:S XPrivacy/XPackageManager:S" + " *:D" ); } /** * Run a shell command and return the results */ private String runProcess(String command) throws IOException { return runProcess(new String[]{command}); } /** * Run a shell command and return the results * * @param command * @return * @throws IOException */ private String runProcess(String[] command) throws IOException { Process process = null; if (command.length == 1) { process = Runtime.getRuntime().exec(command[0]); } else { Runtime.getRuntime().exec(command); } BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(process.getInputStream())); StringBuilder log = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { log.append(line); log.append("\n"); } bufferedReader.close(); return log.toString(); } /** * Clear logcat * @return * @throws IOException */ private void clearLogs() throws IOException { new Thread() { @Override public void run() { try { Runtime.getRuntime().exec("logcat -c -b main -b system -b radio -b events"); } catch (IOException e) { log.error("Error clearing logs", e); } runOnUiThread(new Runnable() { @Override public void run() { logView.setText(""); } }); } }.start(); } class LogUpdaterThread extends Thread { @Override public void run() { while (updateLogs) { try { final String logs = getLogs(); if (!logs.equals(logView.getText().toString())) { runOnUiThread(new Runnable() { @Override public void run() { // update log display logView.setText(logs); // scroll to the bottom of the log display final ScrollView scroll = ((ScrollView) logView.getParent()); scroll.post(new Runnable() { @Override public void run() { scroll.fullScroll(View.FOCUS_DOWN); } }); } }); } } catch (IOException e) { log.warn("Error updating logs", e); } try { Thread.sleep(1000); } catch (InterruptedException e) { log.warn("Thread was interrupted", e); } } } } }