Cómo funciona SystemUI en Android



En este artículo, analizaré la arquitectura y el principio de funcionamiento de la aplicación principal de Android: SystemUI. Me interesó este tema, porque me pregunto cómo funciona el sistema, que utilizan tantos usuarios y para el que miles de aplicaciones se descargan diariamente en Google Play o simplemente en Internet. Además, estoy interesado en el problema de seguridad de la información de Android y las aplicaciones creadas para él.

En Android, SystemUI es una aplicación cuya ruta de código fuente está en platform_frameworks_base / packages / SystemUI / , en el dispositivo está en system / priv-app / -SystemUI.

La aplicación priv es el directorio donde se almacenan las aplicaciones privilegiadas. Por cierto, en la ruta del sistema / aplicación hay aplicaciones preinstaladas, y las aplicaciones comunes que instalamos en nuestro dispositivo se almacenan en datos / aplicación.

Esto plantea inmediatamente la pregunta: ¿por qué no todas las aplicaciones preinstaladas y privilegiadas se pueden insertar en un directorio, por qué es necesaria esta separación?

El hecho es que algunas aplicaciones son más sistémicas que otras :) Y esta separación es necesaria para reducir la cobertura de explotación de las aplicaciones del sistema, a fin de obtener acceso a operaciones protegidas. Puede crear una aplicación que tendrá un ApplicationInfo.FLAG_SYSTEM especial y obtener más derechos en el sistema, sin embargo, se colocará un archivo apk con este permiso en la sección del sistema.

Entonces, SystemUI es un archivo apk, que es esencialmente una aplicación normal. Sin embargo, si observa el complejo dispositivo SystemUI, deja de parecer que es solo una aplicación simple, ¿verdad?

Esta aplicación realiza funciones muy importantes:


  • La navegación
  • Aplicaciones Recientes
  • Ajustes rápidos
  • Barra de notificaciones
  • Pantalla de bloqueo
  • Control de volumen
  • Pantalla de inicio
  • ...

Iniciar SystemUI


Como dije anteriormente, SystemUI no es como una aplicación normal, por lo que su lanzamiento no va acompañado del lanzamiento de actividad, como es el caso con la mayoría de las aplicaciones. SystemUI es una interfaz de usuario global que se inicia durante el proceso de inicio del sistema y no se puede completar.

<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"> 

Si entramos en SystemServer, que es uno de los dos pilares en el mundo de Android (el segundo es Zygote, pero hablaré de eso en otro momento), entonces podemos encontrar el lugar donde se inicia SystemUI cuando se inicia el sistema.

  static final void startSystemUi(Context context, WindowManagerService windowManager) { Intent intent = new Intent(); intent.setComponent(new ComponentName("com.android.systemui", "com.android.systemui.SystemUIService")); intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING); //Slog.d(TAG, "Starting service: " + intent); context.startServiceAsUser(intent, UserHandle.SYSTEM); windowManager.onSystemUiStarted(); } 

Aquí vemos cómo el servicio SystemUI comienza a usar la API no pública startServiceAsUser. Si quisieras usar esto, tendrías que recurrir a la reflexión. Pero si decide usar la API de reflexión en Android, piense algunas veces si vale la pena. Piensa cien veces :)

Entonces, aquí creamos un proceso separado para la aplicación y, de hecho, cada sección de SystemUI es un servicio separado o un módulo independiente.

 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); } } 

Se llama al método start () para iniciar cada servicio , que se enumeran a continuación.

 <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> 

Control de volumen


Usamos regularmente los botones de volumen en nuestros dispositivos, pero no pensamos en los procesos que deberían ocurrir en el sistema para que podamos aumentar o disminuir el sonido. La operación parece bastante simple en palabras, pero si observa la VolumeUI, que se encuentra en una subcarpeta de SystenUI / volume , la interfaz tiene su propia variación en diferentes modos.


Ya he dicho que los servicios de SystemUI se inician mediante el método start (). Si miramos la clase VolumeUI , también hereda de SystemUI.

 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(); } … 

Aquí vemos que con mEnabled determinamos si mostrarnos un panel con ajustes de sonido. Y a juzgar por el VolumeDialogComponent, VolumeUI muestra la barra de sonido como un cuadro de diálogo. Pero todas las acciones relacionadas con presionar las teclas de volumen se procesan en 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; } … 

Hasta donde podemos ver, KEYCODE_VOLUME_UP (+) no se procesa y se procesará KEYCODE_VOLUME_DOWN (-). En ambos eventos, tanto onKeyDown como onKeyUp, se llama al método 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, ... } 

Entonces, aquí llamamos al método ajustarVolumen, para que podamos verificar nuestra dirección a la que se asignará el parámetro de evento.

Como resultado, cuando lleguemos al AudioService , donde se llamará a sendVolumeUpdate , además de llamar al método postVolumeChanged, se instalará la interfaz 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 en Android actúa como jugador. También hereda de SystemUI y en el método start () vemos:

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

Aquí instalamos mCallback, que es esencialmente una instancia de 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) { ... } }; 

Al final, puede controlar el RingtonePlayerService utilizando Binder para reproducir archivos de sonido.

Powerui


PowerUI es responsable de la administración de energía y las notificaciones. Se hereda de manera similar de SystemUI y tiene un método 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(); } 

Como podemos ver en el código anterior, se suscriben los cambios de Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL y luego se llama a 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); } 

Aquí se registra el receptor de difusión, con la ayuda de la cual se realiza el seguimiento de cambios.

Las tareas


Recientes es una característica principal y de uso frecuente en dispositivos móviles Android.

Las funciones principales:


  • Mostrar todas las tareas
  • Cambiar entre tareas
  • Eliminar tareas


Además, Recientes también se hereda de SystemUI. RecentsActivity crea y actualiza las últimas tareas para que podamos verlas en nuestra pantalla.


Y al usar RecentTaskInfo podemos obtener información sobre una tarea específica.

 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; ... 

En general, las tareas en ejecución se pueden colocar en un tema separado. Lo estudié por todos lados, porque quería desenfocar la pantalla de la aplicación antes de cambiar la aplicación a segundo plano, de modo que se mostrara una versión ilegible de la instantánea en RecentsTask. Sin embargo, el problema es que la instantánea de la aplicación se toma antes de que se llame a onPause (). Este problema puede resolverse de varias maneras. O configure la bandera para que el sistema simplemente oculte el contenido de la pantalla con

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

De lo que hablé en un artículo anterior sobre solo instantáneas.

En general, puede asegurarse de que la actividad específica de la aplicación no aparezca en las tareas colocando en el manifiesto

 android:excludeFromRecents = "true" 

O puedes usar el truco con

 Intent.FLAG_ACTIVITY_MULTIPLE_TASK 

Puede establecer la actividad principal sobre el indicador excludeFromRecents = true , para que su pantalla no esté en las tareas en ejecución, pero cuando se carga la aplicación, ejecute una tarea separada que mostrará una captura de pantalla borrosa de la actividad principal o cualquier otra imagen. Más detalladamente cómo se puede hacer esto se describe en la documentación oficial sobre el ejemplo de Google Drive.

Pantalla de bloqueo


Keyguard ya es más complicado que todos los módulos anteriores. Es un servicio que se ejecuta en SystemUI y se administra mediante 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); ... } 

Sin embargo, en realidad, KeyguardService no funciona de forma independiente con la interfaz de la pantalla de bloqueo, solo transfiere información al módulo StatusBar, donde ya se toman medidas con respecto a la apariencia visual de la pantalla y la visualización de la información.

Barra de notificaciones


SystemBars tiene un dispositivo y una estructura bastante complejos. Su obra se divide en dos etapas:
  1. Inicializando SystemBars
  2. Mostrar notificaciones

Si nos fijamos en el lanzamiento de SystemBars

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

Luego vemos un enlace al recurso desde el cual se lee el nombre de la clase y se crea una instancia.

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

Por lo tanto, vemos que aquí se llama una barra de estado, que funcionará con la notificación y la salida de la interfaz de usuario.

Creo que nadie dudaba de que Android es muy complicado y contiene muchos trucos que se describen en una gran cantidad de líneas de código. SystemUI es una de las partes más importantes de este sistema y disfruté aprendiendo sobre ello. Debido al hecho de que hay muy poco material sobre este tema, si nota algún error, corríjame.

PD: Siempre expongo la selección de material y artículos más cortos sobre @paradisecurity en telegramas.

Source: https://habr.com/ru/post/es433620/


All Articles