Comment fonctionne SystemUI dans Android



Dans cet article, j'analyserai l'architecture et le principe de fonctionnement de la principale application Android - SystemUI. Je m'intéressais à ce sujet, car je me demande comment fonctionne le système, qui est utilisé par un si grand nombre d'utilisateurs et pour lequel des milliers d'applications sont téléchargées quotidiennement sur Google Play ou simplement sur Internet. De plus, je m'intéresse à la question de la sécurité de l'information d'Android et des applications créées pour celui-ci.

Sur Android, SystemUI est une application dont le chemin du code source se trouve dans platform_frameworks_base / packages / SystemUI / , sur l'appareil, il se trouve dans system / priv-app / -SystemUI.

L'application priv est le répertoire où sont stockées les applications privilégiées. Soit dit en passant, sur le chemin du système / de l'application, il y a des applications préinstallées, et les applications ordinaires que nous installons nous-mêmes sur notre appareil sont stockées dans les données / l'application.

Cela soulève immédiatement la question: pourquoi toutes les applications préinstallées et privilégiées ne peuvent-elles pas être poussées dans un répertoire, pourquoi cette séparation est-elle nécessaire?

Le fait est que certaines applications sont plus systémiques que d'autres :) Et cette séparation est nécessaire afin de réduire la couverture d'exploitation des applications système, afin d'accéder aux opérations protégées. Vous pouvez créer une application qui aura un ApplicationInfo.FLAG_SYSTEM spécial et dans le système obtiendra plus de droits, cependant, un fichier apk avec cette autorisation sera placé dans la section système.

Donc, SystemUI est un fichier apk, qui est essentiellement une application normale. Cependant, si vous regardez le périphérique SystemUI complexe, il semble qu'il ne s'agisse que d'une simple application, non?

Cette application remplit des fonctions très importantes:


  • La navigation
  • Applications récentes
  • Paramètres rapides
  • Barre de notification
  • Écran de verrouillage
  • Contrôle du volume
  • Écran d'accueil
  • ...

Lancer SystemUI


Comme je l'ai dit plus haut, SystemUI n'est pas comme une application régulière, donc son lancement ne s'accompagne pas du lancement d'une activité, comme c'est le cas avec la plupart des applications. SystemUI est une interface utilisateur globale qui démarre pendant le processus de démarrage du système et ne peut pas être terminée.

<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 nous entrons dans SystemServer, qui est l'un des deux piliers du monde Android (le deuxième est Zygote, mais j'en parlerai une autre fois), alors nous pouvons trouver l'endroit où SystemUI démarre au démarrage du système.

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

Nous voyons ici comment le service SystemUI commence à utiliser l'API non publique startServiceAsUser. Si vous vouliez l'utiliser, vous devrez recourir à la réflexion. Mais si vous décidez d'utiliser l'API de réflexion dans Android, réfléchissez à plusieurs fois si cela en vaut la peine. Pensez cent fois :)

Donc, ici, nous créons un processus distinct pour l'application et, en fait, chaque section de SystemUI est un service distinct ou un module indépendant.

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

La méthode start () est appelée pour démarrer chaque service , qui sont répertoriés ci-dessous.

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

Contrôle du volume


Nous utilisons régulièrement les boutons de volume sur nos appareils, mais nous ne pensons pas aux processus qui devraient se produire dans le système afin que nous puissions augmenter ou diminuer le son. L'opération semble assez simple en termes de mots, mais si vous regardez le VolumeUI, qui est situé dans un sous-dossier de SystenUI / volume , l'interface a sa propre variation dans différents modes.


J'ai déjà dit que les services SystemUI sont démarrés par la méthode start (). Si nous regardons la classe VolumeUI , elle hérite également 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(); } … 

Ici, nous voyons qu'avec mEnabled, nous déterminons si nous devons afficher un panneau avec les paramètres sonores. Et à en juger par le VolumeDialogComponent, VolumeUI affiche la barre de son sous forme de dialogue. Mais toutes les actions concernant la pression des touches de volume sont traitées dans 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; } … 

Pour autant que nous puissions voir, KEYCODE_VOLUME_UP (+) n'est pas traité et passera au traitement KEYCODE_VOLUME_DOWN (-). Dans les deux événements, onKeyDown et onKeyUp, la méthode dispatchVolumeButtonEventAsSystemService est appelée.

  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, ... } 

Donc, ici, nous appelons la méthode adjustVolume, afin que nous puissions vérifier notre direction à laquelle le paramètre d'événement sera affecté.

En conséquence, lorsque nous arrivons à l' AudioService , où sendVolumeUpdate sera appelé, où en plus d'appeler la méthode postVolumeChanged, l'interface HDMI sera installée.

  // 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 dans Android agit comme un joueur. Il hérite également de SystemUI et dans la méthode start () , nous voyons:

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

Ici, nous installons mCallback, qui est essentiellement une instance d' 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) { ... } }; 

À la fin, vous pouvez contrôler le RingtonePlayerService à l'aide du classeur pour lire les fichiers audio.

Powerui


PowerUI est responsable de la gestion de l'alimentation et des notifications. Il est également hérité de SystemUI et possède une méthode 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(); } 

Comme nous pouvons le voir dans le code ci-dessus, les modifications de Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL sont abonnées, et après cela, l'appel à mReceiver.init () est appelé.

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

Ici, le récepteur de diffusion est enregistré, à l'aide duquel le suivi des modifications est effectué.

Les tâches


Récents est une fonctionnalité principale et fréquemment utilisée dans les appareils mobiles Android.

Les fonctions principales:


  • Afficher toutes les tâches
  • Basculer entre les tâches
  • Suppression de tâches


En outre, Recents est également hérité de SystemUI. RecentsActivity crée et met à jour les dernières tâches afin que nous puissions les voir sur notre écran.


Et en utilisant RecentTaskInfo, nous pouvons obtenir des informations sur une tâche spécifique.

 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 général, les tâches en cours d'exécution peuvent être placées dans une rubrique distincte. Je l'ai étudié de tous les côtés, car je voulais flouter l'écran de l'application avant de basculer l'application en arrière-plan, afin qu'une version illisible de l'instantané soit affichée dans RecentsTask. Cependant, le problème est que l'instantané de l'application est pris avant l'appel de onPause (). Ce problème peut être résolu de plusieurs manières. Ou définissez l'indicateur de sorte que le système cache simplement le contenu de l'écran avec

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

Ce dont j'ai parlé dans un article précédent sur les instantanés.

Vous pouvez généralement vous assurer que l'activité spécifique de l'application n'apparaît pas dans les tâches en inscrivant dans le manifeste

 android:excludeFromRecents = "true" 

Ou vous pouvez utiliser l'astuce avec

 Intent.FLAG_ACTIVITY_MULTIPLE_TASK 

Vous pouvez définir l'activité principale au-dessus de l'indicateur excludeFromRecents = true , afin que son écran ne se trouve pas dans les tâches en cours d'exécution, mais lorsque l'application se charge, exécutez une tâche distincte qui affichera soit une capture d'écran floue de l'activité principale, soit toute autre image. Plus en détail, comment cela peut être fait est décrit dans la documentation officielle sur l'exemple de Google Drive.

Écran de verrouillage


Le clavier est déjà plus compliqué que tous les modules ci-dessus. Il s'agit d'un service qui s'exécute dans SystemUI et est géré à l'aide de 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); ... } 

Cependant, en réalité, KeyguardService ne fonctionne pas indépendamment avec l'interface de l'écran de verrouillage, il transfère uniquement des informations au module StatusBar, où des actions sont déjà prises concernant l'apparence visuelle de l'écran et l'affichage des informations.

Barre de notification


SystemBars a un dispositif et une structure assez complexes. Son travail est divisé en deux étapes:
  1. Initialisation de SystemBars
  2. Afficher les notifications

Si vous regardez le lancement de SystemBars

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

Ensuite, nous voyons un lien vers la ressource à partir de laquelle le nom de classe est lu et une instance est créée.

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

Ainsi, nous voyons qu'un StatusBar est appelé ici, ce qui fonctionnera avec la notification et la sortie de l'interface utilisateur.

Je pense que personne ne doutait qu'Android soit très compliqué et contienne de nombreuses astuces décrites dans un grand nombre de lignes de code. SystemUI est l'une des parties les plus importantes de ce système et j'ai apprécié de l'apprendre. Étant donné qu'il y a très peu de matériel sur ce sujet, si vous remarquez des erreurs, veuillez me corriger.

PS J'expose toujours une sélection de documents et d'articles plus courts sur @paradisecurity dans les télégrammes.

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


All Articles