SystemUI如何在Android中工作



在本文中,我将分析主要Android应用程序-SystemUI的体系结构和操作原理。 我对这个主题很感兴趣,因为我想知道系统是如何工作的,有这么多的用户使用该系统,每天要在Google Play或仅在Internet上下载数千个应用程序。 另外,我对Android的信息安全问题以及为此创建的应用程序很感兴趣。

在Android上,SystemUI是一个应用程序,其源代码路径位于platform_frameworks_base / packages / SystemUI /中 ,在设备上,其位于system / priv-app / -SystemUI中。

priv-app是存储特权应用程序的目录。 顺便说一句,在系统/应用程序路径上有预安装的应用程序,而我们自己在设备上安装的普通应用程序存储在数据/应用程序中。

这立即引发了一个问题:为什么不能将所有预安装和特权应用程序都推送到一个目录中,为什么需要这种分隔?

事实是,某些应用程序比其他应用程序更具系统性:)并且这种分离是必要的,以便减小系统应用程序的利用范围,从而获得对受保护操作的访问。 您可以创建一个具有特殊ApplicationInfo.FLAG_SYSTEM的应用程序,并在系统中获得更多权限,但是,具有此权限的apk文件将放置在system部分中。

因此,SystemUI是一个apk文件,本质上是一个正常的应用程序。 但是,如果您查看复杂的SystemUI设备,它似乎不再只是一个简单的应用程序,对吗?

该应用程序执行非常重要的功能:


  • 导览
  • 最近的应用
  • 快速设定
  • 通知栏
  • 锁屏
  • 音量控制
  • 主画面
  • ...

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

在这里,我们看到SystemUI服务如何使用非公共API startServiceAsUser启动。 如果要使用此功能,则必须求助于反思。 但是,如果您决定在Android中使用反射API,请考虑一下是否值得这样做。 想一百遍:)

因此,这里我们为应用程序创建了一个单独的过程,实际上, 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> 

音量控制


我们会定期使用设备上的音量按钮,但我们并未考虑系统中应该进行哪些处理,从而可以提高或降低声音。 从字面上看,该操作似乎非常简单,但是如果您查看位于SystenUI / volume子文件夹中的VolumeUI ,则该接口在不同模式下会有其自身的变化。


我已经说过,SystemUI服务是通过start()方法启动的。 如果我们看一下VolumeUI类,它也继承自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(); } … 

在这里,我们看到使用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方法,以便我们可以检查将事件参数分配给的方向。

结果,当我们到达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); ... } 

铃声播放器


Android中的RingtonePlayer充当播放器。 它还继承自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播放声音文件。

宝瑞


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

在此注册广播接收机,并借助它进行更改跟踪。

任务


最新消息是Android移动设备中的主要且经常使用的功能。

主要功能:


  • 显示所有任务
  • 在任务之间切换
  • 删除任务


此外,Recents也从SystemUI继承。 LatestsActivity创建并更新最新任务,以便我们可以在屏幕上看到它们。


并且,通过使用LatestTaskInfo,我们可以获得有关特定任务的信息。

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

通常,可以将正在运行的任务放在单独的主题中。 我从各个方面进行了研究,因为我想在将应用程序切换到后台之前模糊应用程序的屏幕,以便在LastsTask中显示不可读的快照版本。 但是,问题在于应用程序快照是在调用onPause()之前获取的。 这个问题可以通过几种方式解决。 或设置标志,以便系统仅使用以下命令隐藏屏幕内容

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

我在上一篇文章中谈到的只是快照。

通常,您可以通过放置清单来确保应用程序的特定活动不会出现在任务中

 android:excludeFromRecents = "true" 

或者您可以将技巧与

 Intent.FLAG_ACTIVITY_MULTIPLE_TASK 

您可以将主活动设置为标志excludeFromRecents = true上方,以使其屏幕不在正在运行的任务中,但是在应用程序加载时,运行单独的任务,该任务将显示主活动的屏幕截图模糊或其他任何图像。 在Google云端硬盘示例的官方文档中详细介绍了如何完成此操作。

锁屏


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. 初始化SystemBar
  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输出一起使用。

我认为没有人会怀疑Android是否非常复杂,并包含大量代码行中描述的许多技巧。 SystemUI是该系统最重要的部分之一,我喜欢它。 由于该主题的资料很少,如果您发现任何错误,请更正我。

PS:我总是在电报中公开有关@paradisecurity的材料和简短文章的选择。

Source: https://habr.com/ru/post/zh-CN433620/


All Articles