كيف تتعامل مع الروابط العميقة (Deep Linking) والمخططات المخصصة في Android WebView؟ تتطلب معالجة الروابط العميقة في Android WebView توسيع فئة WebViewClient، وتجاوز (override) الدالة shouldOverrideUrlLoading للتحقق من isForMainFrame، والتحقق من صحة المخططات المخصصة مقابل قائمة مسموح بها، وإطلاق Intent خارجي مصرح به بأمان لمنع بقاء المخططات غير الخاصة بالويب في مسار تحميل WebView.
إن اعتراض الروابط العميقة في Android WebView هو ممارسة برمجية لتجاوز ردود الاتصال (callbacks) الخاصة بالتنقل داخل WebViewClient مخصص لالتقاط البروتوكولات غير الخاصة بـ HTTP، مثل مخططات URI المخصصة وChrome Intent URIs. من خلال تقييم بيانات الطلب والتحقق من حمولات الوجهة، يقوم التطبيق المضيف بتنفيذ إطلاق Intent محلي مع منع حدوث تعطل في البروتوكولات غير المعالجة.
| المصطلح | التعريف | الكيان ذو الصلة | دور نية البحث |
|---|---|---|---|
| WebView | مكون عرض مضمن يعتمد على Chromium يقوم بعرض محتوى الويب داخل تطبيقات Android. | Android SDK | معلوماتي / تجاري |
| Custom URL Scheme | بروتوكول URI خاص يسمح لروابط الويب باستهداف التطبيقات المحلية. | توجيه الروابط العميقة | تقني / معلوماتي |
| Web to App | عملية نقل المستخدمين من صفحات الويب إلى واجهات التطبيقات المحلية. | الروابط العميقة للجوال | معلوماتي |

لماذا تُظهر Android WebViews أخطاء net ERR UNKNOWN URL SCHEME
قيود بروتوكول Chromium: لماذا تتعرف WebViews أصلاً على مخططات الويب القياسية فقط
يعمل مكون Android WebView بواسطة محرك عرض Chromium الأساسي المكون بشكل أساسي لتحليل وعرض بروتوكولات موارد الويب القياسية: http:// وhttps:// وfile:// وdata:// وabout: وjavascript:.
افتراضياً، عند عدم تعيين WebViewClient مخصص، يقوم إطار عمل Android بتفويض عناوين URL غير المعالجة إلى مدير النشاط (Activity Manager) في النظام. ومع ذلك، بمجرد أن يوفر التطبيق WebViewClient مخصصاً، يتم توجيه المخططات غير المعتمدة على HTTP(S) (مثل myapp://product/detail/1024 أو alipay:// أو whatsapp://) مباشرة إلى ردود الاتصال الخاصة بالتنقل في العميل. إذا سمح التطبيق المضيف لمخطط مخصص بالبقاء داخل مسار تحميل WebView بدلاً من اعتراضه، يحاول محرك Chromium حل المخطط المخصص كمورد ويب عادي. ولأن البروتوكولات غير القياسية لا يمكن حلها مقابل DNS أو طبقات نقل خادم الويب، فإن المحرك يظهر فشلاً في التنقل على مستوى منخفض.
تشريح التعطل لـ net::ERR_UNKNOWN_URL_SCHEME: من نقرة المستخدم إلى فشل تحميل مورد الويب
يحدث الانتقال من تفاعل المستخدم إلى صفحة الخطأ في تسلسل سريع ومحدد:
- مشغل المستخدم: ينقر المستخدم على زر دعوة لاتخاذ إجراء أو رابط تشعبي داخل صفحة هبوط H5 مضمنة.
- طلب التنقل: ينشئ محرك الويب طلب تنقل يحتوي على سلسلة URI المستهدفة (مثال:
myapp://promo/summer). - تقييم المسار: إذا أرجعت
shouldOverrideUrlLoading()القيمةfalseأو فشلت في اعتراض URI غير HTTP(S)، يعامل محرك Chromium عنوان URL كتحميل مورد ويب قياسي. - إرسال الخطأ: عند عدم العثور على معالج داخلي للبروتوكول، يقوم المحرك بتشغيل
onReceivedError()ويستبدل محتوى WebView بقالب خطأ Chromium الافتراضي:net::ERR_UNKNOWN_URL_SCHEME.
توقف صفحة الخطأ هذه رحلة المستخدم، وتمنع تتبع التحويل، وتؤدي إلى تدهور الاستقرار المدرك للتطبيق المضيف.
مخاطر تعطل ActivityNotFoundException غير المعالج عند غياب التطبيق المستهدف
عندما يحاول المطورون لأول مرة حل net::ERR_UNKNOWN_URL_SCHEME، يكون الخطأ الشائع هو تحليل سلسلة URI واستدعاء context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) بدون معالجة استثناءات كافية.
إذا لم يكن لدى المستخدم التطبيق المستهدف مثبتاً على جهازه، لا يمكن لنظام تشغيل Android العثور على Activity مؤهل لحل الـ Intent. في تنفيذ Android القياسي، يؤدي استدعاء startActivity() بدون معالج مطابق إلى طرح android.content.ActivityNotFoundException غير معالج. إذا لم يتم التقاط هذا الاستثناء داخل كتلة try-catch منظمة، تنتهي عملية التطبيق المضيف بالكامل فجأة، مما يؤدي إلى تعطل فادح.
كيف تقوم shouldOverrideUrlLoading باعتراض بروتوكولات الروابط العميقة
تطور الواجهة البرمجية (API): مقارنة تجاوزات WebResourceRequest في API 24+ مع ردود الاتصال النصية القديمة
توفر WebViewClient في Android رد اتصال shouldOverrideUrlLoading للسماح للتطبيقات المضيفة بفحص واعتراض طلبات التنقل قبل أن ينفذها محرك الويب. تطور توقيع وإمكانيات هذه الطريقة عبر مستويات API لـ Android:
- API 24+ (Android 7.0 وما فوق): يتجاوز
shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean. توفر واجهةWebResourceRequestبيانات وصفية سياقية غنية، بما في ذلك عنوان URL المستهدف (request.url)، وطريقة طلب HTTP (request.method)، ورؤوس الطلب، وعلامات تسلسل الإطارات الحرجة (request.isForMainFrame،request.hasGesture()). - التجاوز القديم (مهمل في API 24): يتجاوز
shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean. يتلقى رد الاتصال القديم هذا سلسلة URL الخام فقط بدون بيانات وصفية تتعلق بالإطار الطالب أو حالة تفاعل المستخدم.
وفقاً لـ مرجع مطوري Android لـ WebViewClient shouldOverrideUrlLoading، يجب على التطبيقات الحديثة تنفيذ تجاوز WebResourceRequest. عند دعم إصدارات Android القديمة، يجب تكوين رد الاتصال النصي المهمل بشكل متحفظ لمنع تجاوزات الأمان.
الأهمية الأمنية لـ request.isForMainFrame: حظر اختطاف الإطارات الفرعية (Subframes) وIframe

وفقاً لـ دليل مطوري Android حول WebResourceRequest، يمكن تشغيل رد اتصال shouldOverrideUrlLoading للإطارات الفرعية، وiframes، وعمليات إعادة توجيه الموارد في الخلفية بالإضافة إلى عمليات التنقل في المستند من المستوى الأعلى.
فرض request.isForMainFrame ضروري لأمان التطبيق:
- عزل حدود الإطار: يضمن التحقق من
request.isForMainFrameأن الإطارات الفرعية وiframes لا يمكنها تشغيل عمليات إطلاق تطبيقات خارجية أو Intent مخططات مخصصة. إذا حاول إطار فرعي التنقل، يجب أن تُرجع أحمال HTTP(S) القياسيةfalseللاستمرار في العرض داخل الإطار، بينما يجب أن تُرجع المخططات غير HTTP(S) القيمةtrueلإيقاف التنفيذ دون طرح أخطاء. - التمييز بين إيماءات المستخدم: بينما يؤكد
isForMainFrameأن الطلب ينطبق على المستند الأساسي، فإنه لا يثبت تنشيط المستخدم. إذا كانت بنيتك تتطلب التحقق من إيماءة المستخدم، قم بتقييمrequest.hasGesture()أو فرض نوافذ تأكيد على مستوى التطبيق، مع ملاحظة أن وثائق Android تسلط الضوء على بعض تدفقات المستخدم الصالحة حيث قد تُرجعhasGesture()قيمة false.
التمييز بين تنقل الويب العادي وتسليمات الروابط العميقة: متى تُرجع false مقابل true
تتحكم القيمة المنطقية التي تُرجعها shouldOverrideUrlLoading في دورة حياة عرض WebView:
- إرجاع
false: يبلغ إطار عمل Android أن التطبيق المضيف لم يتعامل مع عنوان URL. يستمرWebViewفي تنقل الويب القياسي، حيث يقوم بتحميل عنوان URL داخل حاوية Chromium الداخلية الخاصة به. هذه القيمة المرجعة إلزامية لصفحات الويب القياسيةhttp://وhttps://التي يجب عرضها داخل المتصفح المضمن. - إرجاع
true: يبلغ إطار العمل أن التطبيق المضيف قد اعترض وتعامل مع عنوان URL. يقومWebViewفوراً بإلغاء عملية التحميل الداخلية الخاصة به. هذه القيمة المرجعة إلزامية عند إطلاقIntentخارجي، أو التوجيه إلى نشاط محلي، أو قمع مخطط غير مصرح به.
معالجة روابط التطبيقات (App Links) الموثقة للتطبيق المضيف داخل الحاوية المضمنة
عندما يشير رابط داخل WebView إلى نطاق HTTPS موثق خاص بالتطبيق المضيف (مثل https://app.example.com/product/1024)، يتسبب تنفيذ التنقل الافتراضي في قيام WebView بتحميل نسخة الويب من صفحة المنتج.
لتقديم تجربة محلية، يجب على WebViewClient فحص عناوين URL الواردة عبر HTTPS. إذا تطابق المضيف مع النطاق المرتبط بالتطبيق، يقوم العميل باعتراض الطلب، والتحقق من المسار ومعلمات الاستعلام مقابل مدقق مسار داخلي (AppLinkRouteValidator)، وإرسال البيانات مباشرة إلى الموجه المحلي (internalRouter(validatedRoute))، مع إرجاع true لإلغاء عرض الويب الزائد.
هيكلة معالجات Intent الآمنة للمخططات المخصصة وIntent URIs
تشريح بنية Chrome Intent URI: المخططات، والحزم، وS.browser_fallback_url
على Android، تستخدم صفحات الويب بشكل متكرر Chrome Intent URIs لهيكلة الروابط العميقة المحلية مع آليات احتياطية للمتصفح مدمجة. يستخدم Intent URI مخطط intent:// ويقوم بتشفير معلمات التوجيه، وأهداف الحزم، وعناوين URL الاحتياطية في سلسلة متسلسلة:
intent://product/detail/1024#Intent;scheme=myapp;package=com.example.app;S.browser_fallback_url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.example.app;end
كما هو موثق في دليل Chrome للمطورين حول Android Intent URIs، ينقسم URI إلى مكونات مميزة:
- المضيف والمسار (
product/detail/1024): يحدد وجهة التطبيق الداخلية. scheme=myapp: يحدد البروتوكول المخصص الأساسي.package=com.example.app: يقيد حل intent حصرياً بحزمة Android المحددة.S.browser_fallback_url=...: معلمة إضافية اختيارية (String extra) تحدد عنوان URL ويب أو متجر صريح للتحميل إذا لم يكن التطبيق المستهدف مثبتاً.
خطر تنفيذ Intents الخام المحللة: تخفيف ثغرات حقن Intent
نمط تنفيذي عكسي شائع هو تحليل URI وارد باستخدام Intent.parseUri(url, Intent.URI_INTENT_SCHEME) وتمرير Intent الناتج مباشرة إلى context.startActivity(intent).
وفقاً لـ دليل اختبار أمان تطبيقات الجوال الخاص بـ OWASP حول الروابط العميقة غير الآمنة، يعرض تنفيذ intents المحللة الخام التطبيق لهجمات حقن Intent. يمكن لمهاجم يتحكم في صفحة الويب صياغة Intent URI يحتوي على أهداف مكونات صريحة ضارة (component=com.example.app/.InternalAdminActivity)، أو إجراءات حساسة، أو إضافات intent ذات امتيازات (S.is_admin=true). سيقوم WebView بإلغاء تسلسل هذه المعلمات وإطلاق أنشطة غير مصدرة أو داخلية، متجاوزاً حواجز المصادقة.

إعادة بناء Intent الآمنة: تجريد المكونات وقصر الإجراءات على ACTION_VIEW
للقضاء على ثغرات حقن Intent، يجب ألا تقوم التطبيقات المضيفة أبداً بإطلاق Intent خام تم إلغاء تسلسله مباشرة. بدلاً من ذلك، يجب على WebViewClient تحليل URI، واستخراج حمولة البيانات الموثقة فقط، وإعادة بناء Intent نظيف ومخصص:
- استخراج البيانات: تحليل URI واستخراج حزمة الوجهة الموثقة وURI البيانات.
- التحقق من الحزمة: التحقق من أن الحزمة المعلنة تطابق قائمة بيضاء صريحة من الشركاء المصرح بهم أو الحزم الداخلية.
- إعادة البناء: إنشاء
Intent(Intent.ACTION_VIEW, validatedDataUri)جديد. - العزل: تطبيق
intent.addCategory(Intent.CATEGORY_BROWSABLE)لضمان أن الأنشطة القابلة للتصفح فقط يمكنها حل الطلب، وتعيين علامات صريحة (FLAG_ACTIVITY_NEW_TASK)، والتأكد من بقاءintent.componentوintent.selectorفارغين.
فرض قوائم بيضاء صارمة للمخطط والمضيف والمسار ومعلمات الاستعلام
يجب أن تمر كل من المخططات المخصصة (myapp://) وIntent URIs المعاد بناؤها عبر مدقق صارم قبل التنفيذ.
يتعامل هذا التنفيذ المرجعي تحديداً مع مخطط myapp:// الخاص بالتطبيق المضيف، مع تثبيت تنفيذ intent بشكل صريح على com.example.app. بالنسبة لأنظمة الشركاء متعددة التطبيقات، يجب على المطورين الحفاظ على تعيين صارم للمخطط إلى الحزمة أو إعطاء الأولوية لروابط تطبيقات HTTPS الموثقة.
يتحقق المدقق من:
- المخطط: يطابق المخططات المخصصة المصرح بها (
myapp). - المضيف: يطابق فئات التوجيه المحددة مسبقاً (
open،product،event). - بادئة المسار: تتوافق مع هياكل المسار المتوقعة (
/detail/،/view/،/main/). - معلمات الاستعلام: تفرض مفاتيح استعلام مسموح بها (
target،id،promo،utm_source) وتتحقق من أن قيم المعلمات تتوافق مع حدود الطول (مثلحرفاً) ومجموعات الأحرف الأبجدية الرقمية.
[المستخدم ينقر على رابط داخل Android WebView]
│
▼
[WebViewClient.shouldOverrideUrlLoading]
│
┌─────────────┴─────────────┐
▼ ▼
[!request.isForMainFrame] [request.isForMainFrame]
│ │
▼ ▼
[التحقق من مخطط الإطار الفرعي] [فحص مخطط URL]
├─ HTTP(S) -> إرجاع false │
└─ Non-Web -> إرجاع true ┌────┴───────────────────────────┐
(قمع الخطأ) ▼ ▼
[HTTP / HTTPS] [مخطط Non-HTTP(S)]
│ │
▼ ▼
[التحقق من نطاق المضيف] [CustomSchemeValidator]
├─ رابط تطبيق داخلي ├─ صالح -> تشغيل Intent مثبت
│ -> الموجه الداخلي └─ Intent -> إعادة بناء & احتياطي
└─ ويب خارجي
-> إرجاع false
كيفية التعامل مع رؤية الحزمة في Android 11 والاحتياطيات لمتجر التطبيقات

تحول رؤية الحزمة في Android 11: لماذا تتطلب استعلامات ما قبل الرحلة (Preflight) إعلاناً
بدءاً من Android 11 (مستوى API 30)، قدمت Google تصفية رؤية الحزمة (Package Visibility) لتعزيز خصوصية المستخدم. في ظل هذا الإطار، يتم تقييد رؤية التطبيق للتطبيقات الأخرى المثبتة افتراضياً.
من المهم فهم التمييز بين إطلاق intent واستعلام intent:
- إطلاق Intent المباشر: لا تتطلب الاستدعاءات القياسية لـ
context.startActivity(intent)إعلانات رؤية الحزمة. إذا كان التطبيق المستهدف مثبتاً، يقوم نظام التشغيل بالتوجيه وإطلاقه بنجاح بغض النظر عما إذا كان مرئياً لاستعلامات الحزمة. - استعلامات Intent ما قبل الرحلة: إذا كانت بنية
WebViewClientالخاصة بك تقوم بفحص ما قبل الرحلة—مثل استدعاءpackageManager.resolveActivity(intent, 0)أوpackageManager.queryIntentActivities(intent, 0)للتحقق من وجود التطبيق قبل الإطلاق—فإن الاستعلام يرجعnullأو قائمة فارغة ما لم يتم التصريح بالحزمة أو المخطط المستهدف فيAndroidManifest.xml.
الإعلان عن عناصر <queries> في AndroidManifest.xml للمخططات المخصصة وأهداف الحزم
وفقاً لـ دليل مطوري Android حول رؤية الحزمة في Android 11، يجب على التطبيقات التي تجري فحوصات ما قبل الرحلة قبل تنفيذ intent التصريح عن حزمها المستهدفة أو توقيعات intent في ملف البيان:
<manifest package="com.example.app">
<!-- إعلانات استعلام ما قبل الرحلة الاختيارية لاكتشاف الحزمة الصريح -->
<queries>
<package android:name="com.example.partner" />
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" android:host="app.example.com" />
</intent>
</queries>
</manifest>
إذا كانت بنيتك تفضل أنماط الإطلاق القياسية، فإن تنفيذ startActivity() داخل كتلة try-catch التي تصطاد ActivityNotFoundException يوفر تنفيذاً مرناً ومستقلاً عن الاستعلام.
تنفيذ سلاسل الاحتياط (Fallback Chains): التوجيه الرشيق للمتجر عند غياب التطبيق المستهدف
عندما لا يمكن إطلاق مخطط مخصص معترض أو Intent URI لأن التطبيق المستهدف غير مثبت على جهاز المستخدم، يجب على WebViewClient تنفيذ سلسلة احتياطية صريحة:
- استخراج وربط عنوان URL الاحتياطي: بالنسبة لـ
intent://URIs، قم باستخراج معلمة سلسلةS.browser_fallback_urlوالتحقق من أنها تشير إلى وجهة مصرح بها. بالنسبة لعناوين URL الخاصة بـ Google Play، تحقق من أن معلمةidتطابق اسم الحزمة المستهدفة بدقة (id=targetPackage) لمنع استغلالات إعادة التوجيه المفتوحة. - توليد URI المتجر: إذا لم يكن هناك عنوان URL احتياطي موجود، قم بإنشاء intent متجر:
market://details?id=com.example.app. - احتياطي ويب متجر Play: إذا تم تعطيل Google Play أو غيابه، قم باصطياد
ActivityNotFoundExceptionفي intent المتجر وقم بتحميلhttps://play.google.com/store/apps/details?id=com.example.app.
ربط المعلمات السياقية عبر عمليات إعادة التوجيه الاحتياطية مع OpoInstall
عند توجيه المستخدمين غير المثبتين إلى Google Play، تسقط روابط المتجر الثابتة المعلمات الديناميكية. تقوم OpoInstall، وهي منصة إسناد للجوال وروابط عميقة، بسد هذه الفجوة من خلال التقاط رموز الإحالة وسياق الحملة على خلفية الإسناد قبل إعادة التوجيه إلى المتجر. تحقق من سلوك استرداد المعلمات المؤجلة وواجهات برمجة التطبيقات (APIs) للتهيئة مقابل إصدار OpoInstall Android SDK المنشور في الإنتاج.
عند التثبيت اللاحق وأول إطلاق للتطبيق، يسترد OpoInstall Android SDK المعلمات المخزنة مؤقتاً، مما يمكن التطبيق المضيف من استعادة مشهد المستخدم المستهدف. راجع وثائق تكامل SDK للحصول على تعليمات حول تلقي المعلمات المؤجلة بعد التثبيت.
تنفيذ WebViewClient للإنتاج لاعتراض المخططات بشكل آمن
هيكلة بنية WebViewClient المخصصة في Kotlin
يقوم WebViewClient للإنتاج بتنسيق التحقق من الإطار، والقائمة البيضاء للمخطط، وإعادة بناء intent، واستخراج الاحتياطي، والتوجيه الداخلي. يغلف التنفيذ قواعد التحقق داخل كائنات مدقق مخصصة (AppLinkRouteValidator وCustomSchemeValidator) للحفاظ على نظافة وقابلية صيانة ردود الاتصال الخاصة بالتنقل.
تنفيذ نماذج AppLinkRouteValidator وCustomSchemeValidator
تفرض كائنات المدقق أماناً صارماً (فشل مغلق):
AppLinkRouteValidatorيتحقق من نطاقات HTTPS الداخلية، وبادئات المسار المسموح بها، ومفاتيح الاستعلام المصرح بها (target،id،promo،utm_source)، ويرجع فئة بياناتValidatedAppRouteمنظمة.CustomSchemeValidatorيتحقق من المخططات المخصصة المصرح بها (myapp)، ويتحقق من المضيفين المسموح بهم (open،product،event)، ويفرض بادئات المسار، ويطهر قيم معلمات الاستعلام مقابل مجموعات الأحرف الأبجدية الرقمية وقيود الطول (حرفاً).
يوضح تنفيذ الكود أدناه كيفية تكوين إعلانات رؤية حزمة AndroidManifest.xml اختيارية وتنفيذ CustomWebViewClient آمن وخالٍ من مشاكل الخيوط (thread-safe) في Kotlin مع التعامل التلقائي مع الاحتياطيات. يمكن الحصول على ملفات SDK الثنائية المعتمدة ومغلفات التكامل من مركز تنزيل OpoInstall SDK.
<!-- AndroidManifest.xml: إعلانات رؤية حزمة اختيارية لاستعلامات Intent ما قبل الرحلة (Android 11+) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<!-- التصريح عن الحزم الخارجية والمخططات المخصصة فقط في حالة استخدام استعلامات resolveActivity لما قبل الرحلة -->
<queries>
<!-- تفويض اكتشاف ما قبل الرحلة لحزم شركاء أو مستهدفة محددة -->
<package android:name="com.example.partner" />
<!-- تفويض حل ما قبل الرحلة لمخططات الروابط العميقة المخصصة -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent>
<!-- تفويض حل ما قبل الرحلة لنطاقات روابط تطبيقات HTTPS الموثقة -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" android:host="app.example.com" />
</intent>
</queries>
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<!-- مكونات التطبيق -->
</application>
</manifest>
// Android: WebViewClient مخصص مع أمان حدود الإطار، والتحقق من القائمة البيضاء، وتوجيه الاحتياط
// مثال تكامل مرجعي؛ تحقق من توقيعات الطريقة مقابل بنية Android المنشورة.
package com.example.app.webview
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
data class ValidatedAppRoute(val path: String, val queryParams: Map<String, String>)
object AppLinkRouteValidator {
private val allowedPrefixes = setOf("/open/", "/product/", "/promo/")
private val allowedQueryKeys = setOf("target", "id", "promo", "utm_source")
fun validate(uri: Uri): ValidatedAppRoute? {
val path = uri.path ?: ""
if (allowedPrefixes.none { path.startsWith(it) }) {
return null
}
val sanitizedParams = mutableMapOf<String, String>()
for (name in uri.queryParameterNames) {
// موقف أمني صارم: رفض الطلب إذا كانت هناك مفاتيح استعلام غير معروفة
if (name !in allowedQueryKeys) {
return null
}
val value = uri.getQueryParameter(name) ?: ""
if (value.length <= 64 && value.matches(Regex("^[A-Za-z0-9_-]+$"))) {
sanitizedParams[name] = value
} else {
return null
}
}
return ValidatedAppRoute(path, sanitizedParams)
}
}
object CustomSchemeValidator {
private val allowedSchemes = setOf("myapp")
private val allowedHosts = setOf("open", "product", "event")
private val allowedPathPrefixes = setOf("/detail/", "/view/", "/main/")
private val allowedQueryKeys = setOf("target", "id", "promo", "utm_source")
fun validate(uri: Uri): Uri? {
val scheme = uri.scheme?.lowercase() ?: return null
val host = uri.host?.lowercase() ?: return null
val path = uri.path ?: ""
if (!allowedSchemes.contains(scheme) || !allowedHosts.contains(host)) {
return null
}
if (allowedPathPrefixes.none { path.startsWith(it) }) {
return null
}
for (name in uri.queryParameterNames) {
if (name !in allowedQueryKeys) return null
val value = uri.getQueryParameter(name) ?: ""
if (value.length > 128 || !value.matches(Regex("^[A-Za-z0-9_-]+$"))) {
return null
}
}
return uri
}
}
class CustomWebViewClient(
private val context: Context,
private val internalRouter: (ValidatedAppRoute) -> Unit
) : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
request ?: return false
// متطلب أمني: قصر توجيه التطبيق الخارجي على عمليات تنقل الإطار الرئيسي فقط
if (!request.isForMainFrame) {
val scheme = request.url.scheme?.lowercase()
// السماح بتحميل إطار فرعي HTTP(S) قياسي، ولكن قمع المخططات غير الخاصة بالويب لمنع اختطاف iframe والأخطاء
return if (scheme == "http" || scheme == "https") {
false
} else {
Log.w("WebViewClient", "تم قمع مخطط غير ويب في إطار فرعي/iframe: $scheme")
true // استهلاك/إلغاء بدون إطلاق intent خارجي أو إظهار خطأ Chromium
}
}
val url = request.url.toString()
return handleUrlNavigation(view, url)
}
@Deprecated("مهمل في Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (url == null) return false
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase()
// يفتقر رد الاتصال القديم إلى بيانات وصفية موثوقة لـ isForMainFrame؛ لا تقم بتوجيه المخططات الخارجية
return if (scheme == "http" || scheme == "https") {
false // السماح بتحميل محتوى الويب القياسي
} else {
Log.w("WebViewClient", "تم حظر مخطط خارجي في رد الاتصال القديم: $scheme")
true // قمع أخطاء المخطط غير المعالج بدون إطلاق intents خارجية
}
}
private fun handleUrlNavigation(view: WebView?, url: String): Boolean {
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase() ?: return false
// 1. التعامل مع HTTP / HTTPS وروابط التطبيقات الداخلية
if (scheme == "http" || scheme == "https") {
// التحقق مما إذا كان URL يطابق نطاق رابط التطبيق الخاص بالتطبيق المضيف
if (scheme == "https" && uri.host?.lowercase() == "app.example.com") {
val validatedRoute = AppLinkRouteValidator.validate(uri)
if (validatedRoute != null) {
Log.i("WebViewClient", "مسار HTTPS داخلي موثق. يتم الإرسال إلى الموجه الداخلي.")
internalRouter(validatedRoute)
} else {
Log.w("WebViewClient", "تم رفض رابط تطبيق داخلي مشوه أو غير مصرح به: $uri")
}
return true // تمت المعالجة داخلياً؛ إلغاء تحميل webview
}
return false // السماح لـ WebView بتحميل محتوى الويب القياسي محلياً
}
// 2. التحقق من المخططات المخصصة بقائمة بيضاء صارمة وتثبيت الحزمة لمنع الاختطاف
val validatedUri = CustomSchemeValidator.validate(uri)
if (validatedUri != null) {
val pinnedIntent = Intent(Intent.ACTION_VIEW, validatedUri).apply {
`package` = "com.example.app" // تثبيت حزمة التطبيق المضيف صراحة
addCategory(Intent.CATEGORY_BROWSABLE)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
return launchSafeIntent(pinnedIntent, fallbackAction = {
handleStoreFallback(view, "com.example.app")
})
}
// 3. التعامل مع Chrome Intent URIs بإعادة بناء آمنة واستخراج احتياطي
if (scheme == "intent") {
return try {
val parsed = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
val targetPackage = parsed.`package`
val allowedPackages = setOf("com.example.app", "com.example.partner")
val fallbackUrl = parsed.getStringExtra("browser_fallback_url")
if (targetPackage != null && allowedPackages.contains(targetPackage)) {
val rawDataUri = parsed.data
if (rawDataUri != null) {
val validatedDataUri = CustomSchemeValidator.validate(rawDataUri)
if (validatedDataUri != null) {
// إعادة بناء ACTION_VIEW Intent نظيف ومخصص بدلاً من تنفيذ كائن تم تحليله خام
val safeIntent = Intent(Intent.ACTION_VIEW, validatedDataUri).apply {
`package` = targetPackage
addCategory(Intent.CATEGORY_BROWSABLE)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
launchSafeIntent(safeIntent, fallbackAction = {
handleIntentFallback(view, fallbackUrl, targetPackage)
})
} else {
Log.w("WebViewClient", "تم رفض intent مع URI بيانات غير صالح: $rawDataUri")
true
}
} else {
Log.w("WebViewClient", "تم رفض intent يفتقر إلى حمولة URI بيانات")
true
}
} else {
Log.w("WebViewClient", "تم رفض intent يستهدف حزمة غير موثقة: $targetPackage")
true // إلغاء intent حزمة غير موثق
}
} catch (e: Exception) {
Log.e("WebViewClient", "فشل تحليل intent URI: $url", e)
true // قمع أخطاء المخطط غير المعالج
}
}
// رفض المخططات غير المعروفة لتجنب أخطاء net::ERR_UNKNOWN_URL_SCHEME
Log.w("WebViewClient", "تم حظر تنقل مخطط غير موثق: $scheme")
return true
}
private fun launchSafeIntent(intent: Intent, fallbackAction: () -> Unit): Boolean {
return try {
context.startActivity(intent)
true
} catch (e: ActivityNotFoundException) {
Log.w("WebViewClient", "التطبيق المستهدف غير مثبت: ${intent.data}. يتم تشغيل الاحتياط.")
fallbackAction()
true
} catch (e: SecurityException) {
Log.e("WebViewClient", "استثناء أمني أثناء إطلاق intent: ${intent.data}", e)
true
}
}
private fun handleIntentFallback(view: WebView?, fallbackUrl: String?, targetPackage: String) {
if (!fallbackUrl.isNullOrBlank() && isAuthorizedFallbackUrl(fallbackUrl, targetPackage)) {
Log.i("WebViewClient", "التوجيه إلى عنوان URL احتياطي للمتصفح مصرح به: $fallbackUrl")
view?.loadUrl(fallbackUrl)
} else {
handleStoreFallback(view, targetPackage)
}
}
private fun handleStoreFallback(view: WebView?, packageName: String) {
val marketUri = Uri.parse("market://details?id=$packageName")
val marketIntent = Intent(Intent.ACTION_VIEW, marketUri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
try {
context.startActivity(marketIntent)
} catch (e: ActivityNotFoundException) {
val webStoreUrl = "https://play.google.com/store/apps/details?id=$packageName"
Log.i("WebViewClient", "تطبيق متجر Play غائب. يتم تحميل عنوان URL لمتجر الويب: $webStoreUrl")
view?.loadUrl(webStoreUrl)
}
}
private fun isAuthorizedFallbackUrl(url: String, targetPackage: String): Boolean {
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase()
val host = uri.host?.lowercase()
if (scheme != "https" || host == null) return false
return when (host) {
"play.google.com" -> {
// فرض أن عنوان URL الاحتياطي لـ Google Play يشير تحديداً إلى الحزمة المستهدفة
uri.path == "/store/apps/details" && uri.getQueryParameter("id") == targetPackage
}
"app.example.com", "www.example.com" -> {
// التحقق من مسار الاحتياطي للنطاق المملوك
AppLinkRouteValidator.validate(uri) != null
}
else -> false
}
}
}
التنفيذ الخالي من المشاكل (Thread-Safe): إرسال المسارات الداخلية وإدارة حالات دورة الحياة
عندما يؤكد AppLinkRouteValidator مساراً داخلياً صالحاً، يجب أن تنفذ انتقالات واجهة المستخدم على خيط واجهة المستخدم الرئيسي لـ Android. إذا قام internalRouter بتشغيل أجزاء (fragments)، أو انتقالات نشاط، أو تنقل Compose، فقم بتغليف استدعاء الإرسال داخل runOnUiThread أو دورة حياة coroutine للخيط الرئيسي لمنع تعطل الخيوط.
مصفوفة أخطاء إعادة توجيه Android WebView والحلول التشخيصية
دليل استكشاف أخطاء الروابط العميقة في Android WebView الشامل
توضح المصفوفة أدناه توقيعات الأخطاء الشائعة التي تمت مواجهتها عند التعامل مع المخططات المخصصة في Android WebViews، جنباً إلى جنب مع الأسباب الجذرية الأولية والمعالجات الهندسية:
| توقيع الخطأ / العرض | السبب الجذري الأولي | مستويات API المتأثرة | نقطة التحقق التشخيصية | المعالجة الموصى بها |
|---|---|---|---|---|
net::ERR_UNKNOWN_URL_SCHEME |
مخطط غير ويب غير معالج في محمل WebView | جميع مستويات API | shouldOverrideUrlLoading مفقود أو يرجع false |
تجاوز المندوب، اعتراض المخطط، إرجاع true |
ActivityNotFoundException |
التطبيق المستهدف غائب عن الجهاز | جميع مستويات API | context.startActivity() غير معالج |
التقاط الاستثناء وتنفيذ S.browser_fallback_url أو مسار متجر Play |
| اختطاف Intent للإطار Iframe | إطار فرعي يشغل intent خارجياً | API 24+ | request.isForMainFrame غير متحقق منه |
الحماية بـ if (!request.isForMainFrame) وقمع المخططات غير الخاصة بالويب |
| استعلام ما قبل الرحلة يرجع Null | عامل تصفية رؤية الحزمة في Android 11+ | API 30+ (Android 11+) | استدعاء resolveActivity بدون إعلان البيان |
التصريح عن <queries> في البيان أو استخدام startActivity try-catch مباشرة |
| حلقة إعادة تحميل رابط التطبيق الداخلي | رابط تطبيق مضيف معاد توجيهه إلى WebView | جميع مستويات API | نطاق المضيف غير معالج في المندوب | اعتراض النطاق الخاص، التحقق من المسار، الإرسال إلى الموجه المحلي |
الأسئلة الشائعة (FAQ)
لماذا يتعطل Android WebView الخاص بي عند فتح مخططات URL المخصصة؟
كيف أتعامل بأمان مع intent:// URLs في WebView بدون ثغرات أمنية؟
كيف تؤثر رؤية الحزمة في Android 11 على اعتراض الروابط العميقة في WebViews؟
إطار الملخص والقرار
تتطلب معالجة الروابط العميقة والمخططات المخصصة داخل Android WebViews سد الفجوة بين صناديق رمال عرض الويب وتوجيه Intent لنظام التشغيل المحلي. يؤدي الاعتماد على سلوك WebView الافتراضي إلى أخطاء بروتوكول غير معالجة، وتجربة مستخدم سيئة، وثغرات أمنية محتملة.
من خلال تنفيذ WebViewClient قوي يتحقق من حدود الإطار، ويتحقق من المخططات المخصصة مقابل قوائم بيضاء صريحة، ويعيد بناء intents ACTION_VIEW آمنة، ويتعامل مع احتياطيات المتجر الرشيقة، تقلل فرق الهندسة من حالات فشل التعامل مع مخططات WebView وتدعم تنقلاً أكثر مرونة. إن دمج توجيه موثوق من جانب العميل مع أطر عمل إسناد الهاتف المحمول الشاملة يقلل من احتكاك التوجيه عبر تدفقات الإسناد ويدعم رحلات مستخدم أكثر اتساقاً.
لاستكشاف بنيات الروابط العميقة وتوجيه المعلمات لنظام Android المحلي، راجع وثائق تكامل SDK، وقم بتنزيل مكتبات العميل من مركز تنزيل OpoInstall SDK، واستكشف مرجع تنفيذ إسناد الجوال، أو قم بتسجيل تطبيقك على وحدة تحكم مطوري OpoInstall.
المواد ذات الصلة
- المفاهيم: التنقل في Android WebView، اعتراض المخطط URL المخصص، إعادة بناء Intent URI، رؤية الحزمة
- التقنيات: Android WebViewClient، Chromium WebEngine، جسر تصحيح أخطاء Android (ADB)، OpoInstall Android SDK
- المعايير: IETF RFC 3986 معرف الموارد الموحد، مواصفات Android Intent، دليل اختبار أمان تطبيقات الجوال (MASTG) الخاص بـ OWASP
- APIs: Android
shouldOverrideUrlLoading،WebResourceRequest.isForMainFrame،Intent.parseUri،PackageManager.queryIntentActivities - الوثائق والمراجع الرسمية:
Share this article



