How to create an Android Studio plugin with the ADB connection and reading messages from the Logcat.

Ievgenii Tkachenko
ProAndroidDev
Published in
8 min readNov 21, 2020

--

More than two years ago, I faced the issue that to debug requests from the Android device or emulator, you should use some third-party tools, and none of them was built inside the Android Studio. Then I saw that many people use the OkHttp Interceptor (I use OkHttp or Retrofit libraries in Android development), which just prints HTTP requests as plain text to the console.

But wait, why can’t we just parse this uncomfortable pure text format to the tree? Why should we search in tons of lines to get our request?

Finally, I decided to create an Android Studio plugin to parse that console output to some human-friendly format with the list of requests, the JSON trees, the human-readable headers, etc.

OkHttpProfiler

So this was an idea of the OkHttpProfiler plugin, and now I am going to tell you how to create something similar.

Step 1. Use the IntelliJ IDEA.

To develop for the Android Studio, you should use IntelliJ IDEA (Comunity Edition will fit your needs). There are two main ways to create a project: XML based and Gradle. I had some experience with the first option, so I definitely can advise you to use the Gradle.

Let’s create it: choose File->New Project->Gradle, and only then we should select the options below (Java, Kotlin, IntelliJ Platform Plugin) and proceed.

New project creation

Now we have the project with the initial configuration for the plugin development, but we should set up it more.

Before Android Studio version 4.1, it was enough to use IntelliJ IDEA files for plugins development with ddmlib library usage. Unfortunately, after 4.1, Android Studio and IntelliJ IDEA has some different realization of the same classes, so we MUST include libraries from the Android Studio if we are creating it for AS, and vice versa (see this thread).

That's why we should specify StudioCompilePath in the gradle.properties file of the project. Add the next line to the file:

StudioCompilePath=PATH_TO_YOUR_ANDROID_STUDIO

For mac users, the PATH is /Applications/Android Studio.app/Contents (If you are using a different OS and you don’t know where is it located — just use Google)

Going next, open the build.gradle file, and modify it a little bit to set proper values for your new plugin:

plugins {
id 'java'
id 'org.jetbrains.intellij' version '0.6.3'
id 'org.jetbrains.kotlin.jvm' version '1.3.72'
}

group 'com.itkacher.okhttpprofiler' //Use your own id
version '1.0.14' //Use your own version

repositories {
mavenCentral()
jcenter()
}
//Just add this check below to be sure, that StudioCompilePath is defined
if (!hasProperty('StudioCompilePath')) {
throw new GradleException("No StudioCompilePath value was set, please create gradle.properties file")
}


dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testCompile group: 'junit', name: 'junit', version: '4.12'
implementation fileTree(dir: "libs", include: ['*.jar'])
//Add the lines below to compile libraries from the Android Studio
compileOnly fileTree(dir: "$StudioCompilePath/plugins/android/lib", include: ['*.jar'])
compileOnly fileTree(dir: "$StudioCompilePath/lib", include: ['*.jar'])

}
//These lines for defining the target IDE to test your plugin when you run it. We rewrote ItellijIDEA with the Android Studio.
intellij {
pluginName 'OkHttpProfiler'
updateSinceUntilBuild false

plugins 'android'

intellij.localPath = project.hasProperty("StudioRunPath") ? StudioRunPath : StudioCompilePath
}

compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
patchPluginXml {
changeNotes """
Add change notes here.<br>
<em>most HTML tags may be used</em>"""
}
//Another check that everything was setup correctly.
task(verifySetup) {
doLast {
def ideaJar = "$StudioCompilePath/lib/idea.jar"
if (!file(ideaJar).exists()) {
throw new GradleException("$ideaJar not found, set StudioCompilePath in gradle.properties")
}
}
}

compileJava.dependsOn verifySetup

The source of the plugin information and configuration is still an XML file resources/META-INF/plugin.xml

<idea-plugin> <!-- USE YOUR DATA INSTEAD OF BOLD TEXT BELOW -->
<id>com.itkacher.okhttpprofiler</id>
<name>OkHttp Profiler</name>
<vendor email="email@email.com" url="http://localebro.com">localebro.com</vendor>
<version>1.0.14</version>
<!-- since-build is a minimal version of the Android Studio for your plugin. Android Studio 4.1 has version 201.8743.12 -->
<idea-version since-build="201.8743.12"/>
<description><![CDATA[ USE YOUR DESCRIPTION ]]></description>
<!-- The depending below limits the plugin for Android Studio only. -->
<depends>org.jetbrains.android</depends>
<depends>com.intellij.modules.androidstudio</depends>
<depends>com.intellij.modules.platform</depends>

<!-- OkHttpProfiler is a Toolwindow, so we should define it here -->
<extensions defaultExtensionNs="com.intellij">
<defaultProjectTypeProvider type="Android"/>
<toolWindow id="OkHttp Profiler" secondary="true" icon="/icons/help-network.png" anchor="bottom"
factoryClass="com.itkacher.DebuggerToolWindowFactory"/>
</extensions>
</idea-plugin>

You can find more information about plugin types here. We will use “toolWindow” because it’s our goal. As we can see, the IDEA will search for a class com.itkacher.DebuggerToolWindowFactory, and you should create your own class and set it instead of my DebuggerToolWindowFactory. How? See next chapter.

Step 2. Write the code.

The nice news — you can use Kotlin for the plugin development. The bad — you should use java (swing) too. Let’s create our own DebuggerToolWindowFactory.kt class (don’t forget to create package folders before):

class DebuggerToolWindowFactory : ToolWindowFactory, DumbAware {    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {

}
}

This is the main class, and it will be called when you open your plugin in the studio. It should extend the ToolWindowFactory class and implements the DumbAware interface. First of all, we need to create a form. Left-click on your DebuggerToolWindowFactory file and choose New -> Swing UI Designed -> GUI Form and create the MainForm.

The GUI form creation

If you never worked with Swing before and used to work only with Android, as am I: My congrats! Probably, you will have some pain :) Swing blew up my mind, not in a good way. The form editor will be opened when you choose GUI form creation. You should drag needed components from the right palette to the form. And don’t forget to set the ids (field names):

Visual editor

When you have done, you can pass the form panel to your tool window.

class DebuggerToolWindowFactory : ToolWindowFactory, DumbAware {
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val mainForm = MainForm()
toolWindow.component.add(mainForm.panel)

}
}

In the code “mainForm.panel” — panel it’s the name of the root container.

If you run the project (use “Run” button or gradle command “buildPlugin runIde”), you will see something like this (depends on your form):

My sincere congratulations! You created your first Android Studio plugin!

Step 3. Add the ADB connection logic.

In my case, I created two more classes to encapsulate the logic:

The result of the DebuggerToolWindowFactory is:

class DebuggerToolWindowFactory : ToolWindowFactory, DumbAware {

private var adbController: AdbController? = null

override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val preferences = PluginPreferences(PropertiesComponent.getInstance(project))
val mainForm = MainForm()
adbController = AdbController(mainForm, project, preferences, toolWindow)
toolWindow.component.add(mainForm.panel)
}
}

In the scope of this article, I will not go into the preferences, but let's dive into the AdbController — it has some magic inside :)

The top of the class contains a very useful import of the ddmlib:

import com.android.ddmlib.AndroidDebugBridge
import com.android.ddmlib.Client
import com.android.ddmlib.IDevice
import com.android.ddmlib.logcat.LogCatMessage
import com.android.tools.idea.logcat.AndroidLogcatService
import org.jetbrains.android.sdk.AndroidSdkUtils

All of these classes are going from the Android Studio libs folder, gradle will take care of it and you will be able to use them from the plugin.

AndroidDebugBridge — as we can see from the name, it’s a class, which represents communication with the ADB. And it has a few helpful listeners:

public interface IClientChangeListener {
void clientChanged(Client client, int changeMask);
}

public interface IDeviceChangeListener {
void deviceConnected(IDevice device);
void deviceDisconnected(IDevice device);
void deviceChanged(IDevice device, int var2);
}

public interface IDebugBridgeChangeListener {
void bridgeChanged(AndroidDebugBridge briedge);
default void restartInitiated() {}
default void restartCompleted(boolean isSuccessful) {}
}

You can add all of these listeners to the AndroidDebugBridge with static methods:

AndroidDebugBridge.addDeviceChangeListener
AndroidDebugBridge.addDebugBridgeChangeListener
AndroidDebugBridge.addClientChangeListener

Easy enough, right? Going next.

The thing is you should force your studio to use the android device bridge. And now class AndroidSdkUtils is going to help us. Just call the:

val bridge: AndroidDebugBridge? = AndroidSdkUtils.getDebugBridge(project)
log("initDeviceList bridge ${bridge?.isConnected}")

AndroidSdkUtils will parse the project info and find the ADB information there.

That’s it, now your plugin is connected, and all of your listeners are receiving information about adb, devices, processes.

The only one left: to get access to the logcat.

And again, it’s not a big deal! We can use AndroidLogcatService and LogcatListener:

private val logCatListener = AndroidLogcatService.getInstance()

private val deviceListener = object : AndroidLogcatService.LogcatListener {
override fun onLogLineReceived(line: LogCatMessage) {}
}

and methods:

logCatListener.addListener(device, deviceListener)
logCatListener.removeListener(device, deviceListener)

So you know that the device was connected from the IDeviceChangeListener, and you can add logcat listener to it. Here is the LogCatMessage class structure, it contains all the needed information.

In my case, I receive all messages for a target PID. All the rest depends on your imagination, how you can use it. My way was to create my own communication standard between OkHttpProfiler Android Library (to produce the log messages to logcat from the application side), and the OkHttpProfiler Plugin for Android Studio to read these logs.

Elementary!

Step 4. Help and Thank You.

I made the plugin to help others better understand the processes inside their application and made this article to show, that plugin development is not so hard as it can look.

You can use the OkHttpProfiler for free.

If you want to support me — use another my “child” LocaleBro — localization platform for mobile applications, which also can help you, but with the localization of mobile applications!

Thanks for the reading, and take care of yourself in such a strange time.

Useful links

--

--