Bagaimana SystemUI bekerja di Android



Pada artikel ini, saya akan menganalisis arsitektur dan prinsip pengoperasian aplikasi Android utama - SystemUI. Saya tertarik dengan topik ini, karena saya bertanya-tanya bagaimana sistemnya bekerja, yang digunakan oleh begitu banyak pengguna dan untuk mana ribuan aplikasi diunduh setiap hari di Google Play atau hanya di Internet. Selain itu, saya tertarik dengan masalah keamanan informasi Android dan aplikasi yang dibuat untuk itu.

Pada Android, SystemUI adalah aplikasi yang jalur kode sumbernya ada di platform_frameworks_base / packages / SystemUI / , pada perangkat yang ada di system / priv-app / -SystemUI.

Priv-app adalah direktori tempat aplikasi istimewa disimpan. Ngomong-ngomong, di jalur sistem / aplikasi ada aplikasi yang sudah diinstal sebelumnya, dan aplikasi biasa yang kita instal di perangkat kita sendiri disimpan dalam data / aplikasi.

Ini segera menimbulkan pertanyaan: mengapa semua aplikasi yang tidak diinstal dan diistimewakan dapat didorong ke satu direktori, mengapa pemisahan ini diperlukan?

Faktanya adalah bahwa beberapa aplikasi lebih sistemik daripada yang lain :) Dan pemisahan ini diperlukan untuk mengurangi jangkauan eksploitasi aplikasi sistem, untuk mendapatkan akses ke operasi yang dilindungi. Anda dapat membuat aplikasi yang akan memiliki ApplicationInfo.FLAG_SYSTEM khusus dan mendapatkan lebih banyak hak dalam sistem, namun file apk dengan izin ini akan ditempatkan di bagian sistem.

Jadi, SystemUI adalah file apk, yang pada dasarnya adalah aplikasi normal. Namun, jika Anda melihat perangkat SystemUI yang kompleks, itu tidak lagi tampak seperti aplikasi sederhana, bukan?

Aplikasi ini melakukan fungsi yang sangat penting:


  • Navigasi
  • Aplikasi Terbaru
  • Pengaturan cepat
  • Bilah pemberitahuan
  • Kunci layar
  • Kontrol volume
  • Layar beranda
  • ...

Luncurkan SystemUI


Seperti yang saya katakan di atas, SystemUI tidak seperti aplikasi biasa, jadi peluncurannya tidak disertai dengan peluncuran aktivitas, seperti halnya dengan sebagian besar aplikasi. SystemUI adalah antarmuka pengguna global yang dimulai selama proses boot sistem dan tidak dapat diselesaikan.

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

Jika kita masuk ke SystemServer, yang merupakan salah satu dari dua pilar di dunia Android (yang kedua adalah Zygote, tapi saya akan membicarakannya lain waktu), maka kita dapat menemukan tempat di mana SystemUI dimulai ketika sistem boot.

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

Di sini kita melihat bagaimana layanan SystemUI mulai menggunakan API startServiceAsUser non-publik. Jika Anda ingin menggunakan ini, Anda harus menggunakan refleksi. Tetapi jika Anda memutuskan untuk menggunakan API refleksi di Android, pikirkan beberapa kali apakah itu layak. Pikirkan seratus kali :)

Jadi, di sini kita membuat proses terpisah untuk aplikasi dan pada kenyataannya, setiap bagian dari SystemUI adalah layanan terpisah atau modul independen.

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

Metode start () dipanggil untuk memulai setiap layanan , yang tercantum di bawah ini.

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

Kontrol volume


Kami secara teratur menggunakan tombol volume pada perangkat kami, tetapi kami tidak memikirkan proses apa yang harus terjadi dalam sistem sehingga kami dapat menambah atau mengurangi suara. Operasi tampaknya cukup sederhana dalam kata-kata, tetapi jika Anda melihat VolumeUI, yang terletak di subfolder dari SystenUI / volume , antarmuka memiliki variasi sendiri dalam mode yang berbeda.


Saya sudah mengatakan bahwa layanan SystemUI dimulai dengan metode start (). Jika kita melihat kelas VolumeUI , itu juga mewarisi dari 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(); } … 

Di sini kita melihat bahwa dengan mEnabled kita menentukan apakah akan menampilkan panel dengan pengaturan suara. Dan dilihat dari VolumeDialogComponent, VolumeUI menampilkan soundbar sebagai dialog. Tetapi semua tindakan mengenai menekan tombol volume diproses di 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; } … 

Sejauh yang bisa kita lihat, KEYCODE_VOLUME_UP (+) tidak diproses dan akan masuk ke pemrosesan KEYCODE_VOLUME_DOWN (-). Dalam kedua peristiwa, baik onKeyDown dan onKeyUp, metode dispatchVolumeButtonEventAsSystemService disebut.

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

Jadi, di sini kita memanggil metode AdjustVolume, sehingga kita dapat memeriksa arah kita ke mana parameter acara akan ditetapkan.

Akibatnya, ketika kita sampai ke AudioService , di mana sendVolumeUpdate akan dipanggil, di mana selain memanggil metode postVolumeChanged, antarmuka HDMI akan diinstal.

  // 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 di Android bertindak sebagai pemain. Itu juga mewarisi dari SystemUI dan dalam metode start () kita melihat:

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

Di sini kita menginstal mCallback, yang pada dasarnya adalah turunan dari 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) { ... } }; 

Pada akhirnya, Anda dapat mengontrol RingtonePlayerService menggunakan Binder untuk memutar file suara.

Powerui


PowerUI bertanggung jawab atas manajemen daya dan pemberitahuan. Ini juga diwarisi dari SystemUI dan memiliki metode 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(); } 

Seperti yang dapat kita lihat dari kode di atas, perubahan Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL berlangganan, dan setelah itu, panggilan ke mReceiver.init () dipanggil.

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

Di sini penerima siaran terdaftar, dengan bantuan pelacakan perubahan yang dilakukan.

Tugasnya


Recents adalah fitur utama dan sering digunakan di perangkat seluler Android.

Fungsi utama:


  • Tampilkan semua tugas
  • Beralih di antara tugas
  • Menghapus Tugas


Selain itu, Terbaru juga diwarisi dari SystemUI. RecentsActivity membuat dan memperbarui tugas terbaru sehingga kami dapat melihatnya di layar kami.


Dan dalam menggunakan RecentTaskInfo kita bisa mendapatkan informasi tentang tugas tertentu.

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

Secara umum, tugas yang sedang berjalan dapat ditempatkan dalam topik yang terpisah. Saya mempelajarinya dari semua sisi, karena saya ingin mengaburkan layar aplikasi sebelum mengalihkan aplikasi ke latar belakang, sehingga versi snapshot yang tidak terbaca ditampilkan di RecentsTask. Namun, masalahnya adalah aplikasi snapshot diambil sebelum onPause () dipanggil. Masalah ini dapat diselesaikan dengan beberapa cara. Atau atur tanda sehingga sistem hanya menyembunyikan konten layar

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

Apa yang saya bicarakan di artikel sebelumnya hanya tentang snapshot.

Anda biasanya dapat memastikan bahwa aktivitas spesifik aplikasi tidak muncul dalam tugas dengan meletakkan manifes

 android:excludeFromRecents = "true" 

Atau Anda bisa menggunakan triknya bersama

 Intent.FLAG_ACTIVITY_MULTIPLE_TASK 

Anda dapat mengatur aktivitas utama di atas flag excludeFromRecents = true , sehingga layarnya tidak dalam tugas yang sedang berjalan, tetapi ketika aplikasi dimuat, jalankan tugas terpisah yang akan menampilkan tangkapan layar yang buram dari aktivitas utama, atau gambar lainnya. Secara lebih rinci, bagaimana hal ini dapat dilakukan dijelaskan dalam dokumentasi resmi pada contoh Google Drive.

Kunci layar


Keyguard sudah lebih rumit dari semua modul di atas. Ini adalah layanan yang berjalan di SystemUI, dan dikelola menggunakan 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); ... } 

Namun, pada kenyataannya, KeyguardService tidak bekerja secara independen dengan antarmuka layar kunci, itu hanya mentransfer informasi ke modul StatusBar, di mana tindakan sudah diambil mengenai tampilan visual layar dan tampilan informasi.

Bilah pemberitahuan


SystemBars memiliki perangkat dan struktur yang agak rumit. Karyanya dibagi menjadi dua tahap:
  1. Inisialisasi SystemBars
  2. Tampilkan pemberitahuan

Jika Anda melihat peluncuran SystemBars

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

Kemudian kita melihat tautan ke sumber dari mana nama kelas dibaca dan sebuah instance dibuat.

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

Jadi, kita melihat bahwa StatusBar dipanggil di sini, yang akan berfungsi dengan notifikasi dan keluaran UI.

Saya pikir tidak ada yang meragukan bahwa Android sangat rumit dan berisi banyak trik yang dijelaskan dalam sejumlah besar baris kode. SystemUI adalah salah satu bagian terpenting dari sistem ini dan saya senang mempelajarinya. Karena fakta bahwa ada sangat sedikit materi tentang topik ini, jika Anda melihat ada kesalahan, tolong perbaiki saya.

PS Saya selalu memaparkan pemilihan bahan dan artikel pendek tentang @paradisecurity di telegram.

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


All Articles