Хабрахабр

Как работает SystemUI в Android

Меня заинтересовала эта тема, потому что мне интересно, как устроена система, которой пользуется такое огромное количество пользователей и для которой ежедневно выкатываются тысячи приложений в Google Play или просто на просторы интернета. В этой статье я разберу архитектуру и принцип работы основного приложения Android — SystemUI. Помимо этого меня интересует вопрос информационной безопасности Android и создаваемых под него приложений.

В системе Android, SystemUI — это приложение, путь к исходному коду которого находится в platform_frameworks_base/packages/SystemUI/, на девайсе оно находится в system/priv-app/-SystemUI.

К слову, по пути system/app лежат предустановленные приложения, а обычные приложения, которые мы устанавливаем на свой девайс самостоятельно, хранятся в data/app. priv-app — это каталог, где хранятся привилегированные приложения.

Тут сразу возникает вопрос: почему нельзя засунуть все предустановленные и привилегированные приложения в один каталог, зачем нужно это разделение?

Можно создавать приложение, которое будет иметь специальный ApplicationInfo. Дело в том, что некоторые приложения более системные, чем другие:) И это разделение необходимо для того чтобы уменьшить покрытие эксплойтами системных приложений, для получения доступа к защищенным операциям. FLAG_SYSTEM и в системе получит больше прав, однако apk файл с таким разрешением будет помещен в раздел system.

Однако, если посмотреть на сложное устройство SystemUI, перестает казаться, что это всего лишь простое приложение, верно? Итак, SystemUI — это apk-файл, который по сути своей обычное приложение.

Данное приложение выполняет весьма важные функции:

  • Навигация
  • Недавние приложения
  • Быстрые настройки
  • Панель уведомлений
  • Экран блокировки
  • Регулятор громкости
  • Главный экран
  • ...

Запуск SystemUI

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

<application android:name=".SystemUIApplication" android:persistent="true" android:allowClearUserData="false" android:allowBackup="false" android:hardwareAccelerated="true" android:label="@string/app_label" android:icon="@drawable/icon" android:process="com.android.systemui" android:supportsRtl="true" android:theme="@style/Theme.SystemUI" android:defaultToDeviceProtectedStorage="true" android:directBootAware="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory">

Если мы залезем в SystemServer, который является одним из двух столпов в мире Android (второй — Zygote, но об этом я расскажу как-нибудь в другой раз), то мы можешь найти место, где стартует SystemUI при загрузке системы.

static final void startSystemUi(Context context, WindowManagerService windowManager)

Тут мы видим как запускается сервис SystemUI с помощью непубличного API startServiceAsUser. Если бы вы захотели использовать это, то вам пришлось бы обратиться к рефлексии. Но если вы решите использовать reflection API в Android — подумайте несколько раз, стоит ли это того. Подумайте раз сто:)

Итак, тут создается отдельный процесс для приложения и по факту каждый раздел SystemUI является отдельным сервисом или независимым модулем.

public abstract class SystemUI implements SysUiServiceProvider { public Context mContext; public Map<Class<?>, Object> mComponents; public abstract void start(); protected void onConfigurationChanged(Configuration newConfig) { } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { } protected void onBootCompleted() { } @SuppressWarnings("unchecked") public <T> T getComponent(Class<T> interfaceType) { return (T) (mComponents != null ? mComponents.get(interfaceType) : null); } public <T, C extends T> void putComponent(Class<T> interfaceType, C component) { if (mComponents != null) { mComponents.put(interfaceType, component); } } public static void overrideNotificationAppName(Context context, Notification.Builder n, boolean system) { final Bundle extras = new Bundle(); String appName = system ? context.getString(com.android.internal.R.string.notification_app_name_system) : context.getString(com.android.internal.R.string.notification_app_name_settings); extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, appName); n.addExtras(extras); }
}

Метод start() вызывается для запуска каждой службы, которые перечислены ниже.

<string-array name="config_systemUIServiceComponents" translatable="false"> <item>com.android.systemui.Dependency</item> <item>com.android.systemui.util.NotificationChannels</item> <item>com.android.systemui.statusbar.CommandQueue$CommandQueueStart</item> <item>com.android.systemui.keyguard.KeyguardViewMediator</item> <item>com.android.systemui.recents.Recents</item> <item>com.android.systemui.volume.VolumeUI</item> <item>com.android.systemui.stackdivider.Divider</item> <item>com.android.systemui.SystemBars</item> <item>com.android.systemui.usb.StorageNotification</item> <item>com.android.systemui.power.PowerUI</item> <item>com.android.systemui.media.RingtonePlayer</item> <item>com.android.systemui.keyboard.KeyboardUI</item> <item>com.android.systemui.pip.PipUI</item> <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item> <item>@string/config_systemUIVendorServiceComponent</item> <item>com.android.systemui.util.leak.GarbageMonitor$Service</item> <item>com.android.systemui.LatencyTester</item> <item>com.android.systemui.globalactions.GlobalActionsComponent</item> <item>com.android.systemui.ScreenDecorations</item> <item>com.android.systemui.fingerprint.FingerprintDialogImpl</item> <item>com.android.systemui.SliceBroadcastRelayHandler</item> </string-array>

Регулирование громкости

Мы регулярно пользуемся кнопками громкости на своих устройствах, но не задумываемся какие процессы должны произойти в системе для того чтобы мы могли прибавить или убавить звук. Операция кажется довольно простой на словах, но если заглянуть в VolumeUI, который находится в подпапке SystenUI/volume, в разных режимах интерфейс имеет свою вариацию.

Если мы посмотрим на класс VolumeUI, то он тоже наследуется от SystemUI.
Я уже говорила о том, что сервисы SystemUI запускаются методом start().

public class VolumeUI extends SystemUI { private static final String TAG = "VolumeUI"; private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); private final Handler mHandler = new Handler(); private boolean mEnabled; private VolumeDialogComponent mVolumeComponent; @Override public void start() { boolean enableVolumeUi = mContext.getResources().getBoolean(R.bool.enable_volume_ui); boolean enableSafetyWarning = mContext.getResources().getBoolean(R.bool.enable_safety_warning); mEnabled = enableVolumeUi || enableSafetyWarning; if (!mEnabled) return; mVolumeComponent = new VolumeDialogComponent(this, mContext, null); mVolumeComponent.setEnableDialogs(enableVolumeUi, enableSafetyWarning); putComponent(VolumeComponent.class, getVolumeComponent()); setDefaultVolumeController(); }

Тут мы видим что с помощью mEnabled мы определяем, следует ли нам показывать панель с настройкой звука. И судя по VolumeDialogComponent, VolumeUI отображает звуковую панель в виде диалога. Но все действия относительно нажатия на клавиши громкости обрабатываются в PhoneWindow.

protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) { ... switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_MUTE: { // If we have a session send it the volume command, otherwise // use the suggested stream. if (mMediaController != null) { mMediaController.dispatchVolumeButtonEventAsSystemService(event); } else { getMediaSessionManager().dispatchVolumeKeyEventAsSystemService(event, mVolumeControlStreamType); } return true; } ... protected boolean onKeyUp(int featureId, int keyCode, KeyEvent event) { final KeyEvent.DispatcherState dispatcher = mDecor != null ? mDecor.getKeyDispatcherState() : null; if (dispatcher != null) { dispatcher.handleUpEvent(event); } //Log.i(TAG, "Key up: repeat=" + event.getRepeatCount() // + " flags=0x" + Integer.toHexString(event.getFlags())); switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: { // If we have a session send it the volume command, otherwise // use the suggested stream. if (mMediaController != null) { mMediaController.dispatchVolumeButtonEventAsSystemService(event); } else { getMediaSessionManager().dispatchVolumeKeyEventAsSystemService( event, mVolumeControlStreamType); } return true; }

Насколько мы видим, KEYCODE_VOLUME_UP (+) не обрабатывается и перейдет в обработку KEYCODE_VOLUME_DOWN (-). И в обоих событиях, как в onKeyDown, так и в onKeyUp вызывается метод dispatchVolumeButtonEventAsSystemService.

public void dispatchVolumeButtonEventAsSystemService(@NonNull KeyEvent keyEvent) { switch (keyEvent.getAction()) { case KeyEvent.ACTION_DOWN: { int direction = 0; switch (keyEvent.getKeyCode()) { case KeyEvent.KEYCODE_VOLUME_UP: direction = AudioManager.ADJUST_RAISE; break; ... mSessionBinder.adjustVolume(mContext.getPackageName(), mCbStub, true, direction, ... }

Итак, тут у нас вызывается метод adjustVolume, для того чтобы мы могли проверить наш direction, которому будет присвоен параметр события.

В итоге когда мы доберемся до AudioService, где будет вызван sendVolumeUpdate, где помимо вызова метода postVolumeChanged, будет установлен интерфейс HDMI.

// UI update and Broadcast Intent protected void sendVolumeUpdate(int streamType, int oldIndex, int index, int flags) { ... mVolumeController.postVolumeChanged(streamType, flags); } private int updateFlagsForSystemAudio(int flags) { ... if (mHdmiSystemAudioSupported && ((flags & AudioManager.FLAG_HDMI_SYSTEM_AUDIO_VOLUME) == 0)) { flags &= ~AudioManager.FLAG_SHOW_UI; } ... } return flags; } public void postVolumeChanged(int streamType, int flags) { ... mController.volumeChanged(streamType, flags); ... }

RingtonePlayer

RingtonePlayer в Android выполняет роль проигрывателя. Он так же наследуется от SystemUI и в методе start() мы видим:

@Override public void start() { ... mAudioService.setRingtonePlayer(mCallback); ... }

Здесь у нас устанавливается mCallback, который по сути является экземпляром IRingtonePlayer.

private IRingtonePlayer mCallback = new IRingtonePlayer.Stub() { @Override public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping) throws RemoteException { ... } @Override public void stop(IBinder token) { ... } @Override public boolean isPlaying(IBinder token) { ... } @Override public void setPlaybackProperties(IBinder token, float volume, boolean looping) { ... } @Override public void playAsync(Uri uri, UserHandle user, boolean looping, AudioAttributes aa) { ... } @Override public void stopAsync() { ... } @Override public String getTitle(Uri uri) { ... } @Override public ParcelFileDescriptor openRingtone(Uri uri) { ... } };

В итоге можно управлять RingtonePlayerService с помощью Binder для воспроизведения звуковых файлов.

PowerUI

PowerUI отвечает за управление питанием и уведомлениями. Аналогично наследуется от SystemUI и имеет метод start().

public void start() { mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mHardwarePropertiesManager = (HardwarePropertiesManager) mContext.getSystemService(Context.HARDWARE_PROPERTIES_SERVICE); mScreenOffTime = mPowerManager.isScreenOn() ? -1 : SystemClock.elapsedRealtime(); mWarnings = Dependency.get(WarningsUI.class); mEnhancedEstimates = Dependency.get(EnhancedEstimates.class); mLastConfiguration.setTo(mContext.getResources().getConfiguration()); ContentObserver obs = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { updateBatteryWarningLevels(); } }; final ContentResolver resolver = mContext.getContentResolver(); resolver.registerContentObserver(Settings.Global.getUriFor( Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL), false, obs, UserHandle.USER_ALL); updateBatteryWarningLevels(); mReceiver.init(); showThermalShutdownDialog(); initTemperatureWarning(); }

Как мы видим из приведенного выше кода, происодит подписка на изменения Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, а после — вызов mReceiver.init().

public void init() { // Register for Intent broadcasts for... IntentFilter filter = new IntentFilter(); filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); filter.addAction(Intent.ACTION_BATTERY_CHANGED); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_USER_SWITCHED); mContext.registerReceiver(this, filter, null, mHandler); }

Тут регистрируется широковещательный приемник, с помощью которого происходит отслеживание изменений.

Задачи

Recents — это основная и часто используемая функция в мобильных устройствах на базе Android.

Главные функции:

  • Отображение всех задач
  • Переключение между задачами
  • Удаление задач

В RecentsActivity происходит создание и обновление последних задач, чтобы мы могли увидеть их на нашем экране. Помимо этого Recents так же наследуется от SystemUI.


А в с помощью RecentTaskInfo мы можем получить информацию о конкретной задаче.

public static class RecentTaskInfo implements Parcelable { public int id; public int persistentId; public Intent baseIntent; public ComponentName origActivity; public ComponentName realActivity; public CharSequence description; public int stackId; ...

Вообще, запущенные задачи можно вынести в отдельную тему. Я изучила ее со всех сторон, так как хотела размывать экран приложения перед переходом приложения в background, чтобы в RecentsTask отображалась нечитаемая версия снапшота. Однако, проблема заключается в том, что снапшот приложения берется раньше, чем вызывается onPause(). Эту проблему можно решить несколькими способами. Либо выставлять флаг, чтобы система просто скрывала содержимое экрана с помощью

getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);

О чем я говорила в предыдущей статье, посвященной как раз снапшотам.

Можно вообще сделать так, чтобы конкретная activity приложения не отображалось в задачах, проставив в манифесте

android:excludeFromRecents = "true"

Либо можно воспользоваться хитростью с помощью

Intent.FLAG_ACTIVITY_MULTIPLE_TASK

Можно задать основной активности выше приведенный флаг excludeFromRecents = true, для того чтобы ее экран отсутствовал в запущенных задачах, но во время загрузки приложения запустить отдельную задачу, которая будет показывать либо размытый скриншот с основной активности, либо любое другое изображение. Более подробно, как это можно сделать описано в официальной документации на примере Google Drive.

Экран блокировки

Keyguard уже посложнее всех вышеприведенных модулей. Он представляет из себя сервис, который запускается в SystemUI, а управляется при помощи KeyguardViewMediator.

private void setupLocked() { ... // Assume keyguard is showing (unless it's disabled) until we know for sure, unless Keyguard // is disabled. if (mContext.getResources().getBoolean( com.android.keyguard.R.bool.config_enableKeyguardService)) { setShowingLocked(!shouldWaitForProvisioning() && !mLockPatternUtils.isLockScreenDisabled( KeyguardUpdateMonitor.getCurrentUser()), mAodShowing, mSecondaryDisplayShowing, true /* forceCallbacks */); } else { // The system's keyguard is disabled or missing. setShowingLocked(false, mAodShowing, mSecondaryDisplayShowing, true); } ... mLockSounds = new SoundPool(1, AudioManager.STREAM_SYSTEM, 0); String soundPath = Settings.Global.getString(cr, Settings.Global.LOCK_SOUND); if (soundPath != null) { mLockSoundId = mLockSounds.load(soundPath, 1); } ... int lockSoundDefaultAttenuation = mContext.getResources().getInteger( com.android.internal.R.integer.config_lockSoundVolumeDb); mLockSoundVolume = (float)Math.pow(10, (float)lockSoundDefaultAttenuation/20); ... }

Однако на самом деле KeyguardService самостоятельно не работает с интерфейсом экрана блокировки, он лишь передает информацию в модуль StatusBar, где уже и производятся действия относительно визуального вида экрана и отображения информации.

Панель уведомлений

SystemBars имеет довольно сложное устройство и структуру. Его работа разделяется на два этапа:

  1. Инициализация SystemBars
  2. Отображение уведомлений

Если посмотреть на запуск SystemBars

private void createStatusBarFromConfig() { ... final String clsName = mContext.getString(R.string.config_statusBarComponent); ... cls = mContext.getClassLoader().loadClass(clsName); ... mStatusBar = (SystemUI) cls.newInstance(); ... }

То мы видим ссылку на ресурс из которого читается имя класса и создается его экземпляр.

<string name="config_statusBarComponent" translatable="false">com.android.systemui.statusbar.phone.StatusBar</string>

Таким образом мы видим что тут вызывается StatusBar, который будет работать с выводом уведомлений и UI.

SystemUI является одной из самых важных частей этой системы и мне понравилось изучать ее. Я думаю никто и не сомневался в том, что Android устроен очень сложно и заключает в себе много хитростей, которые описаны в огромном количестве строчек кода. Из-за того что материала на эту тему очень мало, если вы заметите какие-либо ошибки, прошу исправить меня.

S. P. Подбор материала и более короткие статьи я всегда выставляю на @miproblema в телеграм.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть