Introduction
Logcat is an Android mechanism used to collect and view system debug output. This is extremely useful for debugging and QA testing, as it provides valuable information about what the apps are doing.
Under normal conditions, logcat is visible via Android Device Monitor or directly executed from the ADB shell via the logcat command. The purpose of this tutorial is to present a simple mechanism to programmatically capture and filter logcat entries.
Logcat recorder
First thing’s first, we need a nice way to handle the logcat recorder’s status (recording, new log entry, idle). As a result, we create a simple OnLogcatRecorderListener.java interface to define these events:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/** * An interface used to notify the log recording status. */ public interface OnLogcatRecorderListener { /** * Called when recording has started. */ void onStartRecording(); /** * Called when a new log entry is recorded. * * @param logEntry Log entry. */ void onNewLogEntry(final String logEntry); /** * Called when recording has stopped. * * @param log Returns the recorded log. */ void onStopRecording(final String log); } |
Next, we need to define a basic LogcatRecorder.java class and introduce our listener:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * Class used to capture the logcat output. * Contains a listener interface for the recorder status. */ public class LogcatRecorder { private Process continuousLogging; private StringBuilder log; private boolean recording; private OnLogcatRecorderListener onLogcatRecorderListener; /** * LogcatSpy constructor with a predefined OnLogcatRecorderListener. * * @param onLogcatRecorderListener OnLogcatRecorderListener to handle recorder states. */ public LogcatRecorder(OnLogcatRecorderListener onLogcatRecorderListener) { this.recording = false; this.onLogcatRecorderListener = onLogcatRecorderListener; } |
Now, we need a mechanism to start() recording the logcat. This will essentially create a new thread (to avoid locking the main UI thread), containing a Runtime Process to execute the logcat shell command and continuously record its output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
/** * Start recording the log output with a specific filter. * * @param filter Log filter. * @throws IllegalStateException When log recording is already in place. */ public void start(final String filter) throws IllegalStateException { if (!recording) { recording = true; if (onLogcatRecorderListener != null) { onLogcatRecorderListener.onStartRecording(); } Thread thread = new Thread() { public void run() { log = new StringBuilder(); String line; try { //Clear all logcat entries, up until this point. continuousLogging = Runtime.getRuntime().exec("logcat -c"); if (filter != null && filter.length() > 0) { //Apply filter to logcat output. continuousLogging = Runtime.getRuntime().exec("logcat | grep " + filter); } else { //Get full logcat output. continuousLogging = Runtime.getRuntime().exec("logcat"); } //Get the Runtime Process' output. BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(continuousLogging.getInputStream())); while ((line = bufferedReader.readLine()) != null) { final String logEntry = line + "\n"; log.append(logEntry); if (onLogcatRecorderListener != null) { onLogcatRecorderListener.onNewLogEntry(logEntry); } } } catch (Exception e) { e.printStackTrace(); recording = false; } } }; thread.start(); } else { throw new IllegalStateException("Unable to call start(): Already recording."); } } |
We first need to clear the current log via the “logcat -c” runtime process call. Once the log is clear, we can start recording it Runtime.getRuntime().exec(“logcat“) and, optionally, add a filter to our recorder Runtime.getRuntime().exec(“logcat | grep ” + filter). Whilst recording, we call our interface’s onNewLogEntry() method to pass on each new log entry.
To wrap this up, we also need a method to stop() the recording process:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/** * Stops recording the log output. * * @throws IllegalStateException When log recording has already been stopped. */ public void stop() throws IllegalStateException { if (recording) { if (onLogcatRecorderListener != null) { if (log == null || log.toString().length() == 0) { log = new StringBuilder("n/a"); } onLogcatRecorderListener.onStopRecording(log.toString()); } if (continuousLogging != null) { //Kill the Runtime Process. continuousLogging.destroy(); } recording = false; } else { throw new IllegalStateException("Unable to call stop(): Currently not recording."); } } |
In order to do this, we first check our recorder’s state and simply destroy the logging Runtime Process. Once the recording is done, our interface will serve the full list of recorded log entries via onLogcatRecorderListener.onStopRecording(log.toString());
Finally
We just need to add the LogcatRecorder to a new Activity. First, we define ../res/layout/activity_main.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.screechstudios.logcatrecorder.MainActivity"> <ToggleButton android:id="@+id/toggleButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_alignParentTop="true"/> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBottom="@+id/toggleButton" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignParentTop="true" android:layout_toLeftOf="@+id/toggleButton" android:layout_toStartOf="@+id/toggleButton" android:gravity="center" android:text="Record Logcat:" android:textAppearance="?android:attr/textAppearanceLarge"/> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignEnd="@+id/toggleButton" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignRight="@+id/toggleButton" android:layout_below="@+id/textView" android:text="Write to console"/> <ScrollView android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_below="@+id/button"> <TextView android:id="@+id/outputTextView" android:layout_width="match_parent" android:layout_height="match_parent" android:textAppearance="?android:attr/textAppearanceMedium"/> </ScrollView> </RelativeLayout> |
Finally, we define MainActivity.java and add a new instance of our LogcatRecorder to it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
public class MainActivity extends Activity { private LogcatRecorder logcatRecorder; private TextView outputText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); outputText = (TextView) findViewById(R.id.outputTextView); //Initialize the LogcatRecorder logcatRecorder = new LogcatRecorder(new OnLogcatRecorderListener() { @Override public void onStartRecording() { outputText.setText(""); Toast.makeText(MainActivity.this, "Recording logcat...", Toast.LENGTH_SHORT).show(); } @Override public void onNewLogEntry(final String logEntry) { //Run on the main UI thread, since the call is made from a separate thread. runOnUiThread(new Runnable() { @Override public void run() { outputText.append(logEntry); } }); } @Override public void onStopRecording(final String log) { outputText.setText(log); Toast.makeText(MainActivity.this, "Recording stopped...", Toast.LENGTH_SHORT).show(); } }); //Record button ((ToggleButton) findViewById(R.id.toggleButton)).setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { try { //Start recording the logcat and filter it by the app's own PID. logcatRecorder.start(); } catch (IllegalStateException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Already recording...", Toast.LENGTH_SHORT).show(); } } else { try { //Stop recording the logcat. logcatRecorder.stop(); } catch (IllegalStateException e) { e.printStackTrace(); Toast.makeText(MainActivity.this, "Already stopped...", Toast.LENGTH_SHORT).show(); } } } }); //Add some random output to the logcat. findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("Some output: " + Math.random()); Log.d("LogcatRecorder", "Some log entry: " + Math.random()); } }); } } |
Get the full project on GitHub.