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

推荐订阅源

P
Privacy & Cybersecurity Law Blog
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
B
Blog RSS Feed
罗磊的独立博客
V
V2EX
V
Visual Studio Blog
博客园 - 叶小钗
W
WeLiveSecurity
小众软件
小众软件
K
Kaspersky official blog
美团技术团队
雷峰网
雷峰网
阮一峰的网络日志
阮一峰的网络日志
Martin Fowler
Martin Fowler
Recorded Future
Recorded Future
Project Zero
Project Zero
Hugging Face - Blog
Hugging Face - Blog
Engineering at Meta
Engineering at Meta
Security Latest
Security Latest
Microsoft Azure Blog
Microsoft Azure Blog
V
Vulnerabilities – Threatpost
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
Help Net Security
Help Net Security
博客园 - Franky
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
F
Fortinet All Blogs
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Forbes - Security
Forbes - Security
M
MIT News - Artificial intelligence
H
Hacker News: Front Page
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
Spread Privacy
Spread Privacy
The Register - Security
The Register - Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
量子位
Google Online Security Blog
Google Online Security Blog
PCI Perspectives
PCI Perspectives
The Last Watchdog
The Last Watchdog
AI
AI
N
News | PayPal Newsroom
D
DataBreaches.Net
Cloudbric
Cloudbric
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 司徒正美
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
«У нас было 230 тысяч строк легаси-кода». Как мы изменяли приложение «Настройки»
Aleks-Dev (Y · 2026-05-06 · via Все публикации подряд на Хабре

Привет, Хабр! Меня зовут Александр. В YADRO я разрабатываю приложения внутри оболочки kvadraOS: да, у нас есть своя оболочка на основе AOSP — в команде One UI, Color OS и MiUi прибыло. 

Сегодня расскажу о тайнах и тонкостях приложения «Системные настройки» — это наша реликвия, очень старое приложение с нагромождением костылей стилей и подходов. Когда наша команда взялась его перерабатывать, у нас было 230 000 строк legacy-кода на Java и около 300 активностей и фрагментов. Как вы понимаете, задача была не из легких.

Через что нам пришлось пройти, чтобы улучшить приложение, и почему мы не убежали не переписали его с нуля, расскажу дальше. Спойлер: было жестко, но все закончилось хорошо. Теперь у нас есть все основания утверждать, что работа над «внутренностями» Android — это высшая лига.

Кратко о видах приложений 

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

Платформенные приложения (собственные, системные)

Это приложения, которые родились вместе с операционной системой, то есть часть «тела и души» Android. Входят в состав AOSP (в нашем случае версии 12L) и собираются Soong — из этого и вытекают их ограничения. Soong-приложения могут использовать Java, XML и Kotlin 1.4 (а поднимать версию языка в рамках платформы, поверьте, задание не простое), Compose, но с ограниченной версией платформы из дерева AOSP. Можно разрабатывать и на С++, но это редкость. 

Версии библиотек зафиксированы. Поддерживать единый стиль в некоторых системных приложениях сложно, ведь они тесно привязаны к платформе, а для переезда на новую версию Android требуется много ресурсов.

  • Примеры платформенных приложений: «Настройки», System UI, «Телефон», «Сообщения».

  • В чем особенность: они «свои» для системы, поэтому имеют доступ к скрытому и системному API.

Вендорские приложения, предустановленные производителем

Это приложения от производителя вашего девайса — Kvadra, Samsung, Xiaomi и так далее. Некоторые могут быть интегрированы в манифесты и системные разделы. Обычно их сложно удалить, так как на них могут опираться другие функции устройства. Но бывают и просто установленные автоматически при первом включении. Они могут выглядеть как встроенные, но чаще всего их можно удалить стандартными средствами. Для многих вендоров такие приложения — это снижение зависимости от Google Play Store. 

Еще бывают приложения, которые поставляет Chipset Vendor (QC, MTK, RockChip и другие). ODM тоже может предоставлять свои приложения.

  • Примеры таких приложений: «Фото» или «Галерея», «Заметки», Galaxy Store (Samsung), GetApps (Xiaomi), Mi Browser, Samsung Internet, Mi Cloud, Samsung Cloud, «Антивирусы», «Очистители памяти».

  • В чем особенность: в отличие от сторонних приложений, вендор сам решает, быть приложению системным или обычным. Это позволяет устанавливать свои сервисы как стандарт по умолчанию — и не только, но это уже материал для отдельной статьи. 

Standalone-приложения (сторонние)

Это приложения, которые вы сами скачали из Google Play или других магазинов и выбрали из списка для установки при первом запуске устройства. Некоторые из них могут устанавливаться через MDM-решения. 

Создают такие приложения сторонние разработчики — от огромных корпораций до программистов-одиночек. Разработчики OEM-компаний тоже могут их поставлять в качестве обновлений для предустановленных приложений на устройстве.

  • Примеры таких приложений: Telegram, Wildberries, YouTube.

  • В чем особенность: приложения не всегда сразу подстраиваются под новый дизайн системы (с отдельными оговорками), так как в основном не полагаются на системные темы. Если после смены обоев калькулятор стал розовым, а Telegram остался синим — это потому, что «сторонняя» программа живет по своим правилам.  

Settings — не просто приложение

Что такое собственные (они же «системные») приложения, понятно. Теперь давайте поговорим о «Настройках» — самом важном и интересном лично для меня приложении. До работы над kvadraOS мне попадались под руку только сторонние приложения — как нативные, так и веб. Так что работа над «Настройками» была для меня очень увлекательным и даже экстремальным опытом мобильной разработки в Android. В чем тут особенность? 

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

Что здесь интересного:

  • «Настройки» по умолчанию находятся в привилегированной директории системного раздела /system/priv-app/;

  • в манифесте у них заявленные привилегии пользователя android:sharedUserId="android.uid.system";

  • подписаны платформенным сертификатом;

  • имеют набор привилегированных разрешений.

Все это в совокупности открывает доступ к скрытым и системным API и дает приложению права пользователя UID System, которые обычно недоступны для сторонних приложений. Такие возможности позволяют «Настройкам» управлять правами доступа, изменять конфигурацию устройства и получать доступ к чувствительным частям платформы. Думаю, тут уже становится понятно, что в неумелых руках это может привести к непоправимым последствиям для любого пользователя.

Поскольку «Настройки» — это часть AOSP, каждый производитель (Kvadra, Samsung, Xiaomi и так далее) берет «базу» от Google и кастомизирует ее визуальную часть под свою оболочку. Поэтому на разных устройствах одно и то же системное приложение выглядит по-разному, хотя функции внутри одни и те же. Так поступили и мы, изменив внешний вид, структуру разделов и не только. Об этом расскажу чуть позже.

Пока хочу отметить, что разработка системных «Настроек» — это работа для терпеливых. Это мир, где обычно нет места модным библиотекам, но есть огромная ответственность: одна ошибка в XML может превратить устройство в «кирпич», который даже не загрузится.

Добро пожаловать в ад? Почему разработчики боятся «Настроек»

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

Немного деталей:

Смерть от тысячи XML. Современный разработчик мобильных приложений, в основном пишет на Compose интерфейс кодом — это быстро и наглядно. В «Настройках» же вы попадаете в океан XML-файлов. 

Статистика ужаса. В типичной сборке Settings для Android 12L находится более 1500 layout-файлов и еще столько же файлов с ресурсами: строки, стили, анимации. К тому же поиск нужного экрана превращается в детективное расследование: вы ищете фрагмент, который ссылается на контроллер (Привет MVC!), который загружает XML, в котором прописан другой XML.

При этом XML имеет и ряд преимуществ. У вас есть разделение кода и дизайна, а еще вы получаете зрелую технологию с огромным комьюнити. 90% проблем, которые у вас возникнут, скорее всего, уже были разобраны, так что вы легко найдете решения. 

Проклятие Preference Framework. В «Настройках» все строится на фреймворке Preference: он еще поддерживается в виде androidx.preference. Каждый пункт меню — это не просто кнопка, а объект Preference. Мы получаем высокоуровневую абстракцию над RecyclerView. Вместо отдельного описания адаптеров и холдеров описываем структуру в XML через <PreferenceScreen>, хотя иногда и приходится спуститься «вниз» и начать работать с тем же PreferenceGroupAdapter

Библиотека androidx.preference пытается упростить разработку, но в системных настройках она обрастает костылями для поддержки специфических кейсов и кастомного дизайна. Разобраться в иерархии вызовов становится достаточно тяжело – и да, до ее исходного кода мы тоже добрались, и это позволило сделать ее более гибкой для наших задач.

SettingsLib — черная дыра логики. Большая часть логики (статус Wi-Fi, уровень заряда, права доступа) вынесена в общую библиотеку SettingsLib. Она общая для «Настроек», «Системного интерфейса» (SystemUI) и нескольких других приложений и, кстати, еще и позволяет им добиться того самого «системного» вида. Но есть проблема: изменив одну строчку, чтобы поправить баг в отображении батареи в «Настройках», вы можете случайно «сломать» иконку батареи в строке состояния. Тестирование таких изменений превращается в достаточно сложный процесс.

Стоит сказать, что в умелых руках, этот минус автоматически становится плюсом, так как все необходимое агрегировано в одном месте. А про прелести следить за контролем корпоративных политик (через те же RestrictedPreferences) можно тоже написать отдельную статью. 

Лабиринт Overlays (Наложения). Помните, мы говорили, что в Kvadra, Samsung и Google «Настройки» выглядят по-разному? Внешний вид приложения изменяется через подмену XML-файлов при сборке платформы — это механизм Static Overlays. Отладка превращается в гадание: «Почему моя кнопка синяя, если в коде она красная? Точно! Ее перекрасил overlay из такой-то папки типа сборки»

Сборка без Gradle (и тут не все просто). Обычный программист стороннего приложения нажимает кнопку Run в Android Studio и через ~30 секунд (если это не тяжелое приложение) видит результат. Системный инженер запускает сборку через make или soong, часто используя для удобства не простую Android Studio, а Android Studio For Platform. Хотя никто, конечно, не мешает строиться из консоли. На мощном сервере пересборка части системы может занимать от 5 до 20 минут. Никакого мгновенного Hot Reload из коробки и превью интерфейса здесь нет. 

Стоит отметить, что у вас есть возможность максимально приблизиться к желаемому результату по сборке и доставке через сборку отдельных модулей. Для этого используется комбинация системных инструментов и подходов. 

Испуг для непосвященных?

Чтобы понять мою боль от разработки сторонних приложений, представьте ситуацию: нам нужно просто создать экран с одним переключателем (Switch) и заголовком. 

Как это делает «белый человек» (Jetpack Compose)? В современном приложении такая задача — это несколько строк прямо в Kotlin-файле. Ты сразу видишь дизайн в превью:

@Composable
fun SettingsScreen() {
	Column(modifier = Modifier.padding(dimensionResource(R.dimen.settings_padding_medium))) {
    	Text(
        	text = stringResource(R.string.notification_settings_title),
        	style = MaterialTheme.typography.h6,
        	color = MaterialTheme.colors.onBackground
    	)
    	Row(
        	modifier = Modifier
            	.fillMaxWidth()
            	.padding(vertical = dimensionResource(R.dimen.row_padding_small)),
        	verticalAlignment = Alignment.CenterVertically
    	) {
        	Text(
            	text = stringResource(R.string.enable_sounds_label),
            	style = MaterialTheme.typography.body1
        	)
        	
        	Spacer(Modifier.weight(1f))
    	// Состояние Switch обычно приходит из ViewModel
    	// но для наглядности пока не будем её добавлять
        	Switch(
            	checked = true,
            	onCheckedChange = { /* some logic here */ },
            	colors = SwitchDefaults.colors(
                	checkedThumbColor = MaterialTheme.colors.primary
            	)
        	)
    	}
	}
}
@Preview(
	name = "Dark Mode",
	showBackground = true,
	uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
fun SettingsScreenPreview() {
	MyApplicationTheme {
    	Surface(color = MaterialTheme.colors.background) {
        	SettingsScreen()
    	}
	}
}

Итог: 1 файл, 0 XML, сборка за 5 секунд через Gradle. Сиди, наслаждайся и смотри за своим свитчом.

Как это происходит в системных Settings (AOSP)? А здесь у нас начинается «Путь героя» по разным папкам и технологиям. 

  • Шаг 1: XML-декларация —res/xml/notification_settings.xml

Ты не рисуешь интерфейс, ты описываешь его иерархию в статичном файле, используя androidx.preference:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
	xmlns:android=" http://schemas.android.com/apk/res/android"
	xmlns:settings="http://schemas.android.com/apk/res-auto"
	android:key="notification_settings_screen"
	android:title="@string/notification_settings_title">
 
	<PreferenceCategory
    	android:key="sound_category"
    	android:title="@string/sound_category_title">
 
    	<SwitchPreference
        	android:key="notification_sound_key"
        	android:title="@string/notification_sound_title"
        	android:summary="@string/notification_sound_summary"
        	android:icon="@drawable/ic_notifications_sound"
        	settings:controller="com.android.settings.notification.SoundPreferenceController"
        	settings:allowDividerAbove="true" />
 
	</PreferenceCategory>
</PreferenceScreen>
  • Шаг 2: Java/Kotlin Контроллер — src/com/android/settings/...

Теперь тебе нужно создать Controller (а делается это для каждого Preference отдельно), который будет «оживлять» этот свитч. И тут в игру вступает SettingsLib:

public class SoundPreferenceController extends BasePreferenceController {
// В системных настройках мы часто используем Helper-классы из SettingsLib
	private final AudioHelper mAudioHelper;
	private final NotificationBackend mBackend;
 
	public SoundPreferenceController(Context context, String key) {
    	super(context, key);
    	// Вот тут мы показываем власть системного доступа
    	mAudioHelper = new AudioHelper(context);
    	mBackend = new NotificationBackend();
	}
 
	@Override
	public int getAvailabilityStatus() {
    	// Мы не просто показываем свитч, мы спрашиваем систему:
    	// "А можно ли сейчас вообще менять звук?"
    	return mAudioHelper.isSingleVolume() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE;
	}
 
	@Override
	public boolean setChecked(boolean isChecked) {
    	// Обращаемся к менеджеру уведомлений
      mBackend.setNotificationsEnabledForPackage(mContext.getPackageName(), isChecked);
    	
    	// Или дергаем системный сервис звука через SettingsLib обертку
    	mAudioHelper.setStreamVolume(AudioManager.STREAM_NOTIFICATION, isChecked ? 7 : 0);
    	
    	return true;
	}
 
	@Override
	public void updateState(Preference preference) {
    	super.updateState(preference);
    	// А вдруг настройка "залочена" администратором?
    	EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
            	mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId());
    	
    	if (admin != null) {
        	((RestrictedPreference) preference).setDisabledByAdmin(admin);
    	}
	}
}
  • Шаг 3: регистрация в манифесте и поиск индекса. Чтобы этот экран вообще открылся через поиск в «Настройках», его нужно прописать для поисковой системы приложения.

  • Шаг 4: фрагмент-клей — NotificationSettings.java.

Если в Compose мы просто вызываем функцию, то в «Настройках» нам нужно наследоваться от тяжеловесного DashboardFragment. Это своего рода «диспетчерская», которая управляет жизненным циклом экрана.

public class NotificationSettings extends DashboardFragment {
	private static final String TAG = "NotificationSettings";
 
	// 1. Указываем, какой XML-файл с разметкой (из тех самых 1500+) мы используем
	@Override
	protected int getPreferenceScreenResId() {
    	return R.xml.notification_settings;
	}
 
	// 2. Регистрируем наш контроллер, чтобы он оживил Switch
	@Override
	protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
    	final List<AbstractPreferenceController> controllers = new ArrayList<>();
    	controllers.add(new SoundPreferenceController(context)); // Тот самый контроллер, конкретно этот можно было указать в xml, но для наглядности добавим его тут
    	return controllers;
	}
 
	// 3. Интеграция в поиск настроек
	public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
        	new BaseSearchIndexProvider(R.xml.notification_settings);
 
	@Override
	public int getMetricsCategory() {
    	// Каждая страница должна иметь ID для аналитики
    	return SettingsEnums.CONFIGURE_NOTIFICATION;
	}
}

Почему «больно» даже на этом этапе:

  1. Жесткая иерархия: мы должны наследоваться от DashboardFragment, иначе не сможем использовать современные контроллеры и интеграцию с SettingsLib.

  2. Поиск (Search Index): если забудешь прописать SEARCH_INDEX_DATA_PROVIDER, твой новый экран будет «призраком»: зайти на него можно, но найти через поиск вверху «Настроек» — нет.

  3. MetricsCategory: в системных настройках за каждым твоим кликом следит аналитика (метрики). Забыл указать ID категории — получишь невалидный отчет.

Чуть не забыл! Чтобы каждый экран можно было открыть напрямую из другого приложения, у него должна быть своя Activity или связанный с ней activity-alias. Например, когда вы нажимаете «Настройки батареи» в шторке, система шлет Intent, который сопоставляется на вполне конкретный «псевдоним» <activity-alias>. То есть каждый экран открывается в отдельной активности (хоть многие из них просто наследуют SettingsActivity или его потомка SubSettings), а каждая активность может содержать всего лишь один фрагмент на момент отрисовки. 

Активность — это чаще всего просто контейнер, и вся логика находится во фрагменте. На выходе у нас получается несколько сотен активностей и еще больше фрагментов.

Ниже — пример из нашего манифеста. В Android 12L там 4000 строк, дальше еще больше. Помним, что все exported-активности, прописанные в манифесте, — это точки входа в приложение.

Увлекательно, правда? Но подождите, мало просто менять Activity одну на другую — у нас же планшет. Значит, было бы хорошо реализовать Activity Embedding! 

Activity Embedding — планшетный «костыль» на стероидах

В Android 12L ребята из Google решили, что для планшетов и складных устройств (Foldables) не очень подходит, есть «Настройки» выглядят как одна растянутая на 13 дюймов строка. В качестве решения представили технологию Activity Embedding. В чем идея? Вместо того чтобы переписывать все и ломать архитектуру «одна активность один фрагмент» (что в Settings заняло бы вечность), компания Google сказала: «А давайте просто показывать две Activity рядом!». Слева — список разделов, справа — конкретный раздел.

Думаете, это делается одной кнопкой? Как бы не так. Для этого используется библиотека Jetpack WindowManager, но в системных приложениях это превращается в квест:

  • нужно создать отдельный XML-конфиг main_navigation_split_config.xml;

  • прописать правила (SplitPairRule): какая Activity с какой и как должна инициироваться;

  • если вы забыли прописать правило для нового экрана, то при повороте планшета ваше приложение превратится в тыкву — точнее, в одну растянутую Activity среди двухпанельного интерфейса.

В Android 12L эта технология была настолько свежей, что у системных разработчиков на первых порах возникли новые трудности:

  • Z-order и тени: иногда правая Activity перекрывала левую так, что тени отрисовывались поверх контента.

  • Жизненный цикл (Lifecycle): Activity в сплите могут умирать и воскресать в непредсказуемом порядке. Обработка onSaveInstanceState превращается в своеобразную «лотерею».

  • Backstack: нажимая кнопку «Назад» на планшете в Android 12L, вы должны следить, сейчас закроется только правая панель или все приложение. А еще — поняла ли система, куда вернуть фокус.

Контраст с Compose (Adaptive Layouts)

В то время как системные разработчики работают с Activity Embedding, в Compose все решается гораздо более элегантно: 

// В Compose мы просто меняем количество колонок в зависимости от ширины экрана
val windowSize = calculateWindowSizeClass(activity)
if (windowSize.widthSizeClass == WindowWidthSizeClass.Expanded) {
	TwoPaneLayout(mainContent, detailContent)
} else {
	SinglePaneLayout(mainContent)
}

Никаких XML-конфигов на 500 строк, никаких проблем со стеком Activity — просто декларативное описание интерфейса.

Но вот чего точно не сможет Compose, так это отобразить две Activity из разных приложений, помогая незаметно мимикрировать второму под «Настройки». Как раз это возможно благодаря Activity Embedding. А еще не забываем, что выше мы говорили про SettingsLib и androidx.preference: благодаря им отдельные приложения, такие как PermissionController или BuiltInPrintService, в правой части экрана будут выглядеть как родные экраны приложения «Настроек».

Подытожим? Activity Embedding в Android 12L — это попытка Google войти в мир многооконного режима, а заодно придать коду «Настроек» современный вид на планшетах без тотального рефакторинга. Это хорошо работает и в современных версиях. Правда, внутри 12L Android местами держится на «синей изоленте».

«Конец!». Хотел бы я сказать еще в начале своего пути, но трудности же нас только закаляют.

Почему нельзя просто взять и переписать «Настройки»?

Итак, у нас есть общая библиотека Compose-элементов для наших вендорских Kvadra приложений. Казалось бы, вариант решения — снести старый XML и собрать все на Compose. Но в реальности мы упираемся в три стены: Наследие, Архитектуру и Концепцию.

1. Наследие, мешающее единообразию: визуальный разрыв. Мы стремимся к тому, чтобы kvadraOS ощущалась как единый организм. Пользователь не должен замечать швов, переходя из «Галереи» в «Настройки». Но пока наши основные приложения сияют свежим дизайном из Figma, «Настройки» остаются консервативным родственником.

Есть нюанс: чтобы внедрить новую фишку в UI, нам приходится вручную «вытачивать» ее на XML специально для этого приложения. Можно было бы рисовать на Canvas, но при нашем специфическом дизайне это сожжет столько ресурсов, что проще построить ракету.

2. Архитектурный долг: когда код не твой. Основная проблема: «Настройки» не принадлежат нам до конца. Хотя мы и под Apache-лицензией, но надо помнить, что у нас форк (ответвление) глобального кода Google, изменения в котором мы хотим переносить и на новые версии Android в обновленную версию AOSP Settings. Мы ограничены архитектурой текущей версией AOSP. 

3. Кризис концепции: талмуд против современности. Давайте честно: сама идея «Настроек» как списка из 150 экранов несколько устарела. В эпоху ИИ-ассистентов заставлять пользователя вручную пробираться через лабиринты подменю — это база для не самого лучшего пользовательского опыта. Мы понимаем, что будущее за предиктивными действиями в поиске, но обязаны поддерживать существующую иерархию. Ведь это фундамент для сложнейшего узла управления всей системой, на котором нужно воздвигать новые решения. Поэтому наш путь — это эволюция внутри жестких рамок. Мы должны поддерживать и улучшать существующее, принося с собой новое.

Какие тогда у нас остаются варианты? Нет, не убежать. А развивать существующее, добавляя новые модули в существующий код настроек (как на примере расширения раздела биометрии, внедрения двухфакторной аутентификации, менеджмента подключенных устройств и не только). И написать свое приложение с интеграцией в текущее. 

Итак, встречайте SettingsLite! Наше APK, интегрируемое в «Настройки».

SettingsLite — Poc-Corn

С апдейтом AOSP «Настроек» понятно: переделываем разделы, учитывая пользовательский опыт, добавляем свои стили, layout, создаем кастомные элементы, наследуясь от тех же androidx.preference. А что же тогда будет в нашем интегрируемом приложении?

 Основная концепция SettingsLite:

  • современный Settings Framework на основе Google SPA (Settings Platform Architecture), поддерживающий реализацию экранов настроек, аналогичный android.preference, с поддержкой Legacy API;

  • использование общей библиотеки Compose UI-элементов;

  • разработка с использованием современного стека: JetPack Compose, Hilt, Koltin Coroutines, Gradle Kotlin Dsl, Detekt, Kover;

  • реализующий шаблон MVVM;

  • построенный по принципам Clean Architecture и поддерживающий модульную структуру;

  • поддержка Feature Toggle (Managed Redirection) для возможности откатить или выключить нашу функциональность, если что-то пойдет не так.

Что взяли за референс

Референсом стал так называемый SPA — это как раз та самая попытка Google навести порядок в хаосе, о котором мы говорили выше. Если в вебе SPA — это Single Page Application, то в мире Android 14+ это фреймворк, призванный перетащить «Настройки» в современную эру. Правда, в итоге мы сильно все изменили. Почему?

Сначала мы сделали на SPA небольшую демку из трех экранов и оценили результат. Оказалось, архитектура Google противоречит архитектурным рекомендациям Google: даты и UI смешаны в одну кучу. С этой проблемой мы сталкивались уже и в старых «Настройках». К тому же оказалось, что в глубинах не совсем новая архитектура, в нее просто пытаются завернуть все то же Legacy. Наконец, когда мы начали «прикручивать» уже реальную функциональность, она просто не стала реализовываться. То есть проект тогда был еще сырым: ребята из Маунтин-Вью его придумали, но не продумали.

Первые результаты миссии

Сначала мы выбрали простейший раздел на планшете — без динамических настроек и с минимумом ограничений по доступам. Накидали план. Главными целями было обкатать framework и убедиться, что мы можем на нем быстро и просто реализовывать новые экраны с гибким UI.

 В итоге центральный паттерн позволяет:

  • описывать структуру страниц декларативно;

  • динамически строить иерархию «Настроек»;

  • интегрироваться с системой поиска;

  • управлять доступностью страниц на основе feature-флагов.

Мы создали собственное приложение настроек на базе Android, сохраняя при этом возможность использовать существующие технологии из стандартного приложения Settings. А благодаря модульной архитектуре, четкому разделению ответственности и современным технологиям, SettingsLite обеспечивает отличную основу для развития и поддержки будущих проектов.

Эстетику же вы можете оценить по результату:

Или экрана обновлений ПО:

Это до

Это до

Это после

Это после

Эпилог: почему все это мучение того стоит?

Казалось бы, после 1500 XML-файлов и борьбы с Activity Embedding хочется все бросить и уйти писать лендинги. Но есть причины, по которым работа над системными приложениями Android — это высшая лига и уникальный опыт.

Вы управляете реальностью, а не картинкой. Когда вы пишете стороннее приложение, то живете в «песочнице». Система позволяет вам ровно то, что разрешено всем. Но в Settings вы хозяин положения: работаете с менеджерами над модулями связи, драйверами дисплея, контроллерами питания и не только. Ваш код дает вам полную власть над устройством.

Масштаб, который кружит голову. Ваш экран «Настроек» увидят все пользователи системы. Буквально. Каждое ваше решение влияет на пользовательский опыт и пользовательские возможности. Благодаря вам пользователь может настраивать устройство полностью под себя. Сделаете удобно — сэкономите огромное количество человеко-часов. 

Мастерство «Старой школы». Работа с AOSP превращает вас в инженера-универсала. Вы начинаете понимать Android не как набор библиотек, а как сложнейший механизм:

  • знаете, как работает межпроцессное взаимодействие (AIDL/Binder);

  • понимаете, как система распределяет ресурсы и права;

  • учитесь писать код, который обязан быть стабильным.

Рождение эстетики и создание своей системы. Для меня именно работа над системными приложениями стала драйвовой. А учитывая работу над SettingsLite, мы получили элегантное решение для создания масштабируемых и поддерживаемых приложений настроек, сочетающее лучшие практики Clean Architecture с современными технологиями Android-разработки.


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

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

Учитывая все вышесказанное, теперь у нас есть все права утверждать: разработка «Настроек» — это не чтение мануалов, а постоянный критический анализ. Мы не просто копируем то, что делает Google — мы расширяем существующее, чтобы пользователь kvadraOS получил стабильный и современный продукт.

На сегодня историй об испытаниях достаточно. Если есть вопросы или дополнения по теме, задавайте их в комментариях — буду рад обменяться опытом.