惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Jina AI
Jina AI
博客园 - Franky
Apple Machine Learning Research
Apple Machine Learning Research
酷 壳 – CoolShell
酷 壳 – CoolShell
阮一峰的网络日志
阮一峰的网络日志
量子位
雷峰网
雷峰网
宝玉的分享
宝玉的分享
V
Visual Studio Blog
博客园_首页
小众软件
小众软件
The Cloudflare Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
大猫的无限游戏
大猫的无限游戏
博客园 - 聂微东
S
SegmentFault 最新的问题
博客园 - 【当耐特】
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
人人都是产品经理
人人都是产品经理
WordPress大学
WordPress大学

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Разработка плагина для Android Studio или Чистим безумную...
Alexandr Tsvetkov · 2026-06-16 · via Все публикации подряд на Хабре

Каждый Android-разработчик проводит в окне Logcat примерно треть своей жизни. И пока проект маленький, там всё хорошо. Но когда приложение разрастается до десятков изолированных модулей, Logcat превращается в филиал ада. Туда одновременно сыплется аналитика, сетевые запросы, логи инициализации, внутренние «шорохи» библиотек и куча другого мусора.

Недавно я дошёл до точки кипения. Локализовать плавающий баг в этом бесконечном водопаде текста стало физически больно - глаза просто вытекали. Стандартные фильтры Android Studio, конечно, работают, но когда тебе нужно постоянно переключаться между логами пяти разных фич и при этом не видеть спам от остальных, ручная настройка регулярных выражений начинает сниться в кошмарах. Нужно постоянно помнить точные названия тегов всех модулей, собирать длинные конструкции с исключениями через минусы и вертикальные черты, а через пять минут переписывать всё заново под другую задачу.

Сначала я пробовал стандартные фильтры LogCat, затем написал скрипты которые чистили по нужным тегам, затем переключался на скармливание этих логов в AI. Выжранные токены заставили чистить уже подготовленные логи. Но все равно это было неудобно и отнимало время.

Поняв, что так дальше жить нельзя, я решил сделать инструмент под себя. Результатом стал плагин TAO LogExt для Android Studio, который превращает эту рутину в удобное управление в несколько кликов.

Идея плагина в том, чтобы дать разработчику возможность управления прямо внутри студии в виде отдельного Tool Window. Вместо того чтобы держать в голове названия тегов или копировать их из кода, они автоматически заводятся по мере поступления в интерфейс плагина и их можно активировать обычными чекбоксами. То, чего не хватало - это возможность объединять эти теги в группы или пресеты. Например, можно собрать профиль для сети, куда войдут теги OkHttp и вашего API, профиль для навигации или отдельный профиль под конкретную фичу. Переключение между ними занимает долю секунды: ткнул нужный пресет, и рабочая область моментально очистилась от лишнего шума.

Точно так же плагин решает проблему со спамящими сторонними SDK. Если какая-то библиотека начинает забивать консоль сообщениями каждую секунду, её тег можно отправить в игнор, и он исчезнет из выдачи (вернуть можно). При этом плагин не ломает стандартное поведение Android Studio и не пытается переписать сам Logcat - он просто автоматизирует рутину на основе выбранных вами галочек.

Инструмент создавался исключительно для себя, чтобы спасти остатки нервов на проекте с легаси и огромным количеством логов. В итоге процесс дебага ускорился в разы: вместо бесконечного набора текста в строке поиска я теперь просто переключаю профили мышкой.

Ниже я расскажу, как этот плагин устроен «под капотом», чтобы вы могли создать свой инструмент или разобраться, как работают расширения для Android Studio.

С чего начинается плагин: Инфраструктура

Если вы привыкли к стандартным Android-проектам, то разработка плагинов может сначала сбить с толку. Мне так было непривычно. Здесь используется IntelliJ Platform SDK, который диктует свои правила игры. На самом деле все оказалось не слишком страшным.

Для удобства при начале разработки я сначала запустил IntelliJ IDEA, так как там есть визард для создания плагинов. После создания я загрузил проект в Android Studio и продолжил разработку в ней чтобы была возможность сразу тестировать вживую.

Итак, о коде. Сразу скажу, что если кусочков кода недостаточно, то их можно посмотреть целиком в репозитории.

build.gradle.kts - Начинаем настройку

В блоке intellijPlatform пишем целевую платформу, указывая через local(file(…)) путь к установленной Android Studio. Здесь же в pluginConfiguration задаются метаданные плагина и параметры ideaVersion. Маленький нюанс, поля sinceBuild и untilBuild определяют и ограничивают версии Студии, в которых плагин сможет запуститься. Например, значение 261 означает поддержку версий начиная с 2026.1. Тут нужно помнить что версии для Android Studio и IntelliJ IDEA отличаются. Для себя они это отмечают префиксами AI- IC- IU- IE-, но в данной настройке только цифрами ¯\_(ツ)_/¯ и это может иногда вызвать путаницу.

Очень важно в runIde настроить параметры JVM и системные свойства. Здесь нужно прописать disable.android.first.run, чтобы при каждом запуске отладки не запускался Setup Wizard, а память maxHeapSize выставить с запасом, чтобы среда разработки работала быстрее. И наконец, блок signing подготавливает плагин к публикации: здесь указываются пути к ключам и сертификатам для подписи дистрибутива, без чего размещение в Marketplace невозможно. В проекте я добавил бат файлы, которые упрощают это дело.

intellijPlatform {
    pluginConfiguration {
        name = "TAO LogExt"
        ideaVersion {
            sinceBuild = "261" // Начиная с Android Studio 2026.1
            untilBuild = "271.*"
        }
    }
    signing {
        certificateChainFile = file("certificate_chain.crt")
        privateKeyFile = file("private_key.pem")
    }
}

plugin.xml - манифест для плагина

Этот файл лежит в src/main/resources/META-INF/ и выполняет роль AndroidManifest.xml. Как вы понимаете, он необходим. Поле id служит уникальным идентификатором, который не стоит менять после публикации. В тегах depends указываем нужные нам модули: com.intellij.modules.platform дает доступ к базовым окнам и редакторам, а org.jetbrains.android открывает возможности работы с ADB и управления устройствами. Без этой связки классы Android не будут доступны.

Именно здесь прописываем наш toolWindow, создающий боковую панель, или projectService для хранения пользовательских настроек. Команды управления описываются в блоке actions. Здесь регистрируются наши ...Action и привязываются к группам меню ViewMenu или ToolsMenu, чтобы плагин можно было найти в панелях Студии. Также заполняются данные с информацией об авторе. В общем есть аналогии с регистрацией Activity и Service.

<idea-plugin>
    <id>ua.at.tsvetkov.logext</id>
    <name>TAO LogExt</name>
    <vendor url="https://github.com/lordtao">Alexandr Tsvetkov</vendor>

    <depends>com.intellij.modules.platform</depends>
    <depends>org.jetbrains.android</depends>

    <extensions defaultExtensionNs="com.intellij">
        <toolWindow id="TAO LogExt"
                    anchor="bottom"
                    icon="/icons/toolWindowIcon.svg"
                    factoryClass="ua.at.tsvetkov.logext.ui.LogExtToolWindowFactory"/>
        
        <projectService serviceImplementation="ua.at.tsvetkov.logext.services.LogExtSettingsService"/>
    </extensions>

    <actions>
        <action id="ua.at.tsvetkov.logext.actions.OpenLogExtAction"
                class="ua.at.tsvetkov.logext.actions.OpenLogCatAction"
                text="TAO LogExt"
                icon="/icons/toolWindowIcon.svg">
            <add-to-group group-id="ViewMenu" anchor="last"/>
        </action>
    </actions>
</idea-plugin>

Ресурсы: Иконки и графика

Графика плагина живет в директории src/main/resources/icons/, но есть одно исключение - иконка самого плагина, которая отображается в Marketplace и списке установленных расширений. Файл pluginIcon.svg (и его версия для темной темы pluginIcon_dark.svg) должен лежать строго в папке src/main/resources/META-INF/. Если этого не сделать, в каталоге плагинов будет отображаться стандартная заглушка.

Для всех остальных элементов интерфейса принято использовать формат SVG, чтобы они корректно масштабировались на любых экранах. Если вы создаете иконку toolWindowIcon.svg, то для темной темы IDE автоматически попытается найти файл с суффиксом dark, например toolWindowIcon_dark.svg. Это позволяет иконкам оставаться контрастными при переключении оформления. В TAO LogExt также используются вспомогательные иконки, такие как индикаторы активности фильтров (лампочки), которые динамически меняются в интерфейсе в зависимости от состояния плагина по такому же принципу.

Добываем логи: Рефлексия и ADB

Самое интересное начинается при попытке достать логи. Нам нужна библиотека ddmlib, которая уже есть в Студии. Но версии Студии меняются, и чтобы плагин не переставал работать при обновлении, я использовал рефлексию. В LogExtListenerService находим метод executeShellCommand у устройства и запускаем команду logcat -v threadtime. Это и есть наши любимые логи. Такой вариант, надеюсь, позволит сохранять независимость от внутренних изменений Logcat в новых версиях IDE.

Чтобы перехватить поток данных, мы создаем динамический прокси для интерфейса IShellOutputReceiver. Это позволяет нам подписаться на события вывода команды без прямой компиляции против внутренних классов ddmlib.

private fun setupDeviceLogcat(device: Any, callback: (String) -> Unit) {
    val receiverClass = Class.forName("com.android.ddmlib.IShellOutputReceiver")
    val proxyReceiver = java.lang.reflect.Proxy.newProxyInstance(
        receiverClass.classLoader,
        arrayOf(receiverClass)
    ) { _, method, args ->
        if (method.name == "addOutput") {
            val data = args[0] as ByteArray
            val offset = args[1] as Int
            val length = args[2] as Int
            callback(String(data, offset, length))
        }
        null
    }

    val executeMethod = device.javaClass.getMethod("executeShellCommand", 
        String::class.java, receiverClass)
    
    Thread {
        executeMethod.invoke(device, "logcat -v threadtime", proxyReceiver)
    }.apply { 
        isDaemon = true
        start() 
    }
}

UI: Строим панель управления

Разработка интерфейса для плагина отличается от Android: вместо XML-лейаутов и View/Compose здесь используется Swing и набор компонентов от JetBrains (JB-компоненты). Если в Android мы привыкли к RecyclerView, то здесь его роль часто выполняют JBList или динамически наполняемые JPanel. Вместо ConstraintLayout чаще используются BorderLayout или GridBagLayout. В общем, пришлось со скрипом вспоминать десктоп Java, хотя пишем все на Kotlin. Процесс начинается с LogCatToolWindowFactory, которая служит входной точкой и создает панель при запуске.

Точка входа: ToolWindowFactory

Наш главный класс должен реализовывать интерфейс ToolWindowFactory. Его единственная задача - создать содержимое окна при обращении пользователя. Мы создаем экземпляр нашей панели и регистрируем его в менеджере контента.

class LogExtToolWindowFactory : ToolWindowFactory {
    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        val logExtPanel = LogExtPanel(project)
        val content = ContentFactory.getInstance().createContent(logExtPanel, "", false)
        toolWindow.contentManager.addContent(content)
        
        // регистрируем панель для очистки ресурсов при закрытии
        Disposer.register(toolWindow.contentManager, logExtPanel)
    }
}

Основной экран: LogExtPane

Сама панель наследуется от стандартного JPanel (обычно с BorderLayout). Сердцем плагина является ConsoleView - текстовый движок Студии, который поддерживает раскраску строк и кликабельные ссылки. Мы размещаем его в центре панели, а сверху и сбоку добавляем элементы управления.

class LogExtPanel(private val project: Project) : JPanel(BorderLayout()), Disposable {
    // Используем готовый ConsoleView из IntelliJ SDK
    private val consoleView: ConsoleView = object : ConsoleViewImpl(project, true) {
        override fun createCompositeFilter(): com.intellij.execution.filters.CompositeFilter {
            val compositeFilter = com.intellij.execution.filters.CompositeFilter(project)
            compositeFilter.addFilter(LogSourceLinkFilter(project))
            return compositeFilter
        }
    }

    init {
        // Компонуем интерфейс
        add(createHeader(), BorderLayout.NORTH) // Выбор девайса и процесса
        add(consoleView.component, BorderLayout.CENTER) // Поле с логами
        createToolbar() // Добавляем кнопки управления
    }
}

Для работы плагина пользователю необходимо управлять настройками. Вот базовые элементы.

Диалоговые окна: DialogWrapper

Для создания окон настроек (например, TagFilterDialog) используем DialogWrapper. Это обертка над стандартными диалогами Swing, которая берет на себя управление кнопками OK/Cancel и размерами окна.

class LogExportDialog(private val project: Project) : DialogWrapper(project) {

    private val globalSettings = LogExtGlobalSettingsService.getInstance()
    private val pathField = TextFieldWithBrowseButton()
    private val minimizeCheck = JBCheckBox("Minimize for AI", globalSettings.state.minimizeForAi)

    init {
        title = "Export Logs"
        pathField.text = globalSettings.state.lastExportPath ?: ""
        
        pathField.addActionListener {
            ....
        }
        init()
    }

    override fun createCenterPanel(): JComponent {
        val panel = FormBuilder.createFormBuilder()
            .addLabeledComponent("Export to:", pathField)
            .addComponent(minimizeCheck)
            .panel
        panel.preferredSize = Dimension(600, panel.preferredSize.height)
        return panel
    }

....

    override fun doOKAction() {
        val state = globalSettings.state
....
        super.doOKAction()
    }
}

Настройки

Куда же сохранять настройки плагина? К примеру в плагине предусмотрена настройка цветов для каждого уровня логирования. Предпочтения пользователя сохраняются используя Storage при помощи PersistentStateComponent.

@Service(Service.Level.PROJECT)
@State(name = "LogExtSettings", storages = [Storage("logext_plugin_settings.xml")]) // <- Вот где храним
class LogExtSettingsService : PersistentStateComponent<LogExtSettingsService.State> {

    class State { // это наши разные настройки
        var ignoredTags: MutableSet<String> = mutableSetOf()
        var levelColors: MutableMap<String, LevelAttributes> = mutableMapOf()
        
        var showDate: Boolean = true
        var showTime: Boolean = true
        var showMillis: Boolean = true
        var showPid: Boolean = true
        var showTid: Boolean = true
        var tagWidth: Int = 23
        var maxHistorySize: Int = 100000
        
        var clearLogOnStart: Boolean = true
        var openOnStart: Boolean = true
        
        var lastExportPath: String? = null
        var lastTagsPath: String? = null
        var minimizeForAi: Boolean = false
        var showDuplicateTags: Boolean = false
    }

    private var myState = State()

    override fun getState(): State = myState // тут банально

    override fun loadState(state: State) { // тут тоже банально
        myState = state
    }
  }

Итог

Создание плагина началось с желания упростить ежедневную работу с логами. TAO LogExt помог мне сделать процесс дебага более наглядным и удобным. Я уверен что есть еще много фич которые можно встроить в плагин для удобства. Буду рад конструктивной критике и фидбекам.

Исходники проекта открыты на GitHub в репозитории https://github.com/lordtao/LogExt. Скачать и установить плагин можно либо из релиза https://github.com/lordtao/LogExt/releases, из каталога плагинов https://plugins.jetbrains.com/plugin/32270-tao-logext или найти в Android Studio в Settings -> Plugins -> Marketplace поиском TAO LogExt.

Благодарю за помощь в вычитке и критических замечаниях Gemini