
في هذه المقالة سوف أقوم بتحليل بنية ومبدأ تشغيل تطبيق Android الرئيسي - SystemUI. لقد كنت مهتمًا بهذا الموضوع ، لأني أتساءل كيف يعمل النظام ، والذي يستخدمه هذا العدد الهائل من المستخدمين والذي يتم تنزيل الآلاف من التطبيقات يوميًا على Google Play أو ببساطة على الإنترنت. بالإضافة إلى ذلك ، أنا مهتم بمسألة أمان المعلومات في Android والتطبيقات التي تم إنشاؤها من أجله.
على Android ، SystemUI هو تطبيق يكون مسار شفرة المصدر الخاص به في
platform_frameworks_base / الحزم / SystemUI / ، على الجهاز يكون في system / priv-app / -SystemUI.
التطبيق الخاص هو الدليل الذي يتم فيه تخزين التطبيقات المميزة. بالمناسبة ، توجد تطبيقات مثبتة مسبقًا على مسار النظام / التطبيق ، ويتم تخزين التطبيقات العادية التي نقوم بتثبيتها على أجهزتنا في بيانات / تطبيق.
هذا يطرح على الفور السؤال التالي: لماذا لا يمكن دفع جميع التطبيقات المثبتة مسبقًا والمتميزة في دليل واحد ، لماذا هذا الفصل ضروري؟
الحقيقة هي أن بعض التطبيقات أكثر نظامية من غيرها :) وهذا الفصل ضروري من أجل الحد من استغلال تغطية تطبيقات النظام ، من أجل الوصول إلى العمليات المحمية. يمكنك إنشاء تطبيق يحتوي على ApplicationInfo.FLAG_SYSTEM خاص والحصول على مزيد من الحقوق في النظام ، ومع ذلك ، سيتم وضع ملف apk مع هذا الإذن في قسم النظام.
لذلك ، 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);
هنا نرى كيف تبدأ خدمة SystemUI باستخدام API startServiceAsUser غير العام. إذا كنت تريد استخدام هذا ، فسوف تضطر إلى اللجوء إلى التفكير. ولكن إذا قررت استخدام واجهة برمجة تطبيقات الانعكاس في 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>
التحكم في مستوى الصوت
نحن نستخدم بانتظام أزرار مستوى الصوت على أجهزتنا ، لكننا لا نفكر في العمليات التي يجب أن تحدث في النظام حتى نتمكن من زيادة أو خفض الصوت. تبدو العملية بسيطة جدًا في الكلمات ، ولكن إذا نظرت إلى وحدة التخزين ، الموجودة في مجلد فرعي من
SystenUI / وحدة التخزين ، فإن الواجهة لها تنوعها الخاص في أوضاع مختلفة.

لقد قلت بالفعل أن خدمات 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: {
بقدر ما نرى ، لم تتم معالجة 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 ، حيث سيتم تثبيت واجهة HDMI بالإضافة إلى استدعاء طريقة postVolumeChanged.
نغمة الرنين
تعمل 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() {
هنا يتم تسجيل مستقبل البث ، والذي يتم من خلاله تتبع التغيير.
المهام
Recents هي ميزة أساسية
وشائعة الاستخدام في أجهزة Android المحمولة.
المهام الرئيسية:
- عرض جميع المهام
- التبديل بين المهام
- حذف المهام
بالإضافة إلى ذلك ، يتم توريث Recents أيضًا من SystemUI. تقوم RecentsActivity بإنشاء وتحديث أحدث المهام حتى نتمكن من رؤيتها على الشاشة.

وباستخدام
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; ...
بشكل عام ، يمكن وضع المهام قيد التشغيل في موضوع منفصل. لقد درستها من جميع الجوانب ، لأنني أردت طمس شاشة التطبيق قبل تحويل التطبيق إلى الخلفية ، بحيث يتم عرض نسخة غير قابلة للقراءة من اللقطة في RecentsTask. ومع ذلك ، فإن المشكلة هي أن لقطة التطبيق تؤخذ قبل أن يسمى onPause (). يمكن حل هذه المشكلة بعدة طرق. أو اضبط العلامة بحيث يخفي النظام ببساطة محتويات الشاشة
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
ما تحدثت عنه في
مقال سابق عن لقطات فقط.
يمكنك عمومًا التأكد من أن النشاط المحدد للتطبيق لا يظهر في المهام عن طريق وضع في البيان
android:excludeFromRecents = "true"
أو يمكنك استخدام الخدعة مع
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
يمكنك تعيين النشاط الرئيسي أعلى العلامة
excludeFromRecents = true ، بحيث لا تكون شاشته في المهام قيد التشغيل ، ولكن عندما يتم تحميل التطبيق ، قم بتشغيل مهمة منفصلة تعرض إما لقطة شاشة ضبابية من النشاط الرئيسي أو أي صورة أخرى. بمزيد من التفصيل ، يمكن شرح كيفية القيام بذلك في
الوثائق الرسمية في مثال Google Drive.
قفل الشاشة
Keyguard هو بالفعل أكثر تعقيدا من جميع الوحدات المذكورة أعلاه. إنها خدمة تعمل في SystemUI ، وتتم إدارتها باستخدام KeyguardViewMediator.
private void setupLocked() { ...
ومع ذلك ، في الواقع ، KeyguardService لا يعمل بشكل مستقل مع واجهة شاشة القفل ، فهو ينقل المعلومات فقط إلى وحدة StatusBar ، حيث يتم اتخاذ الإجراءات بالفعل بشأن المظهر المرئي للشاشة وعرض المعلومات.
شريط الإعلام
SystemBars لديه جهاز وبنية معقدة إلى حد ما. ينقسم عمله إلى مرحلتين:
- تهيئة SystemBars
- عرض الإخطارات
إذا نظرتم إلى إطلاق 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 يسمى هنا ، والذي سيعمل مع إخطار واجهة المستخدم.
أعتقد أن لا أحد يشك في أن Android معقدة للغاية ويحتوي على الكثير من الحيل الموضحة في عدد كبير من أسطر التعليمات البرمجية. يعد SystemUI أحد أهم أجزاء هذا النظام وقد استمتعت بالتعرف عليه. نظرًا لحقيقة وجود مواد قليلة جدًا حول هذا الموضوع ، إذا لاحظت أي أخطاء ، فيرجى تصحيحها.
ملاحظة: دائمًا ما فضح اختيار المواد والمقالات القصيرة حول
paradisecurity في البرقيات.