วิธีจัดการ Custom URL Scheme ในการเปลี่ยนเส้นทางบน Android WebView

opoinstall
2026-09-21
5 min read

จะจัดการ deep linking และ custom scheme ใน Android WebView ได้อย่างไร? การจัดการ deep link ใน Android WebView จำเป็นต้องขยายคลาส WebViewClient, ทำการ override เมธอด shouldOverrideUrlLoading เพื่อตรวจสอบ isForMainFrame, ตรวจสอบ custom scheme กับรายการที่อนุญาต (allowlist), และสั่งเปิด intent ภายนอกที่ได้รับอนุญาตอย่างปลอดภัย เพื่อป้องกันไม่ให้ scheme ที่ไม่ใช่เว็บค้างอยู่ในกระบวนการโหลดของ WebView

การดักจับ deep link ใน Android WebView คือการเขียนโปรแกรมเพื่อ override callback ของการนำทางภายใน WebViewClient ที่กำหนดเอง เพื่อจับโปรโตคอลที่ไม่ใช่ HTTP เช่น custom URI scheme และ Chrome Intent URI การประเมินข้อมูลเมตาของคำขอและการตรวจสอบความถูกต้องของปลายทางจะช่วยให้แอปพลิเคชันหลักเปิดใช้งาน Native Intent ได้อย่างราบรื่น ในขณะที่ป้องกันการเกิดข้อผิดพลาดจากโปรโตคอลที่ไม่ได้รับการจัดการ

คำศัพท์ คำนิยาม เอนทิตีที่เกี่ยวข้อง บทบาทของ Search Intent
WebView คอมโพเนนต์มุมมองที่ใช้ Chromium เป็นพื้นฐานในการแสดงผลเนื้อหาเว็บภายในแอป Android Android SDK ข้อมูล / เชิงพาณิชย์
Custom URL Scheme โปรโตคอล URI เฉพาะที่อนุญาตให้ลิงก์เว็บเชื่อมต่อไปยังแอปพลิเคชันเนทีฟได้ การกำหนดเส้นทาง Deep Link เทคนิค / ข้อมูล
Web to App กระบวนการนำผู้ใช้เปลี่ยนผ่านจากหน้าเว็บไปยังอินเทอร์เฟซของแอปพลิเคชันเนทีฟ Mobile Deep Linking ข้อมูล

Custom scheme ต้องออกจากกระบวนการโหลดของ WebView ก่อนที่ Chromium จะทำงานผิดพลาด

สาเหตุที่ Android WebView แสดงข้อผิดพลาด net ERR UNKNOWN URL SCHEME

ข้อจำกัดของโปรโตคอล Chromium: เหตุใด WebView จึงจดจำเฉพาะมาตรฐานเว็บทั่วไปเท่านั้น

คอมโพเนนต์ WebView ของ Android ขับเคลื่อนโดยเอ็นจิ้นการแสดงผล Chromium ซึ่งถูกกำหนดค่าให้แยกวิเคราะห์และแสดงผลโปรโตคอลทรัพยากรเว็บมาตรฐานเป็นหลัก ได้แก่: http://, https://, file://, data://, about:, และ javascript:

โดยปกติแล้ว เมื่อไม่มีการกำหนด WebViewClient แบบกำหนดเอง เฟรมเวิร์กของ Android จะส่งต่อ URL ที่ไม่ได้จัดการไปยัง Activity Manager ของระบบ อย่างไรก็ตาม เมื่อแอปพลิเคชันมีการระบุ WebViewClient แบบกำหนดเอง Scheme ที่ไม่ใช่ HTTP(S) (เช่น myapp://product/detail/1024, alipay://, หรือ whatsapp://) จะถูกส่งตรงไปยัง callback การนำทางของไคลเอ็นต์ หากแอปพลิเคชันปล่อยให้ custom scheme ค้างอยู่ในกระบวนการโหลดของ WebView แทนที่จะทำการดักจับ เอ็นจิ้น Chromium จะพยายามจัดการ custom scheme นั้นเหมือนทรัพยากรเว็บทั่วไป เนื่องจากโปรโตคอลที่ไม่ใช่มาตรฐานไม่สามารถแก้ไขผ่าน DNS หรือเลเยอร์การขนส่งของเซิร์ฟเวอร์เว็บได้ เอ็นจิ้นจึงแสดงข้อผิดพลาดการนำทางระดับต่ำออกมา

กายวิภาคของความผิดพลาด net::ERR_UNKNOWN_URL_SCHEME: จากการกดของผู้ใช้สู่ความล้มเหลวในการโหลดทรัพยากรเว็บ

การเปลี่ยนจากปฏิสัมพันธ์ของผู้ใช้ไปสู่หน้าข้อผิดพลาดเกิดขึ้นตามลำดับที่ชัดเจน:

  1. User Trigger: ผู้ใช้กดปุ่ม Call-to-action หรือไฮเปอร์ลิงก์ภายในหน้า Landing Page แบบ H5 ที่ฝังอยู่
  2. Navigation Request: เอ็นจิ้นเว็บสร้างคำขอการนำทางที่มีสตริง URI ปลายทาง (เช่น myapp://promo/summer)
  3. Pipeline Evaluation: หาก shouldOverrideUrlLoading() คืนค่าเป็น false หรือไม่สามารถดักจับ URI ที่ไม่ใช่ HTTP(S) ได้ เอ็นจิ้น Chromium จะถือว่า URL นั้นเป็นโหลดทรัพยากรเว็บมาตรฐาน
  4. Error Dispatch: เมื่อไม่พบตัวจัดการสำหรับโปรโตคอลนั้น เอ็นจิ้นจะทริกเกอร์ 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 ดักจับโปรโตคอล Deep Link อย่างไร

วิวัฒนาการของ API: การเปรียบเทียบการ Overload WebResourceRequest ใน API 24+ กับ Legacy String Callbacks

WebViewClient ของ Android มี callback shouldOverrideUrlLoading เพื่อให้แอปพลิเคชันสามารถตรวจสอบและดักจับคำขอการนำทางก่อนที่เอ็นจิ้นเว็บจะดำเนินการ ซึ่งลายเซ็นและความสามารถของเมธอดนี้มีการพัฒนามาตามระดับ API ของ Android:

  • API 24+ (Android 7.0 ขึ้นไป): Overrides shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean โดยอินเทอร์เฟซ WebResourceRequest จะให้ข้อมูลเมตาบริบทที่สมบูรณ์ รวมถึง URL เป้าหมาย (request.url), วิธีการขอ HTTP (request.method), เฮดเดอร์ของคำขอ, และสถานะการลำดับชั้นของเฟรม (request.isForMainFrame, request.hasGesture())
  • Legacy Overload (เลิกใช้งานใน API 24): Overrides shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean โดย callback แบบดั้งเดิมนี้จะรับเฉพาะสตริง URL ดิบโดยไม่มีข้อมูลเมตาเกี่ยวกับเฟรมที่ขอหรือสถานะการโต้ตอบของผู้ใช้

เพื่อให้สอดคล้องกับ Android Developers Reference บน WebViewClient shouldOverrideUrlLoading แอปพลิเคชันสมัยใหม่ต้องใช้การ Overload แบบ WebResourceRequest และในการรองรับ Android เวอร์ชันเก่า ควรตั้งค่า callback แบบสตริงแบบอนุรักษ์นิยมเพื่อป้องกันการบายพาสความปลอดภัย

ความสำคัญด้านความปลอดภัยของ request.isForMainFrame: การบล็อกการจี้ Subframe และ Iframe

การตรวจสอบ Main-frame ช่วยป้องกันไม่ให้การนำทางใน iframe เปิดเส้นทางแอปภายนอก

เพื่อให้สอดคล้องกับ Android Developers Guide บน WebResourceRequest callback shouldOverrideUrlLoading สามารถถูกทริกเกอร์สำหรับ subframe, iframe, และการเปลี่ยนเส้นทางทรัพยากรเบื้องหลัง รวมถึงการนำทางเอกสารระดับบนสุด

การบังคับใช้ request.isForMainFrame เป็นสิ่งจำเป็นสำหรับความปลอดภัยของแอปพลิเคชัน:

  • การแยกขอบเขตเฟรม (Frame Boundary Isolation): การตรวจสอบ request.isForMainFrame ช่วยให้มั่นใจได้ว่า subframe และ iframe ไม่สามารถทริกเกอร์การเปิดแอปพลิเคชันภายนอกหรือ intent แบบ custom scheme ได้ หาก subframe พยายามนำทาง การโหลด HTTP(S) มาตรฐานควรคืนค่า false เพื่อให้แสดงผลในเฟรมต่อไป ในขณะที่ scheme ที่ไม่ใช่ HTTP(S) ต้องคืนค่า true เพื่อระงับการทำงานโดยไม่ทำให้เกิดข้อผิดพลาด
  • การจำแนกท่าทางของผู้ใช้ (User Gesture Distinction): แม้ isForMainFrame จะยืนยันว่าคำขอนั้นใช้กับเอกสารหลัก แต่ก็ไม่ได้พิสูจน์ถึงการกระตุ้นโดยผู้ใช้ หากสถาปัตยกรรมของคุณต้องการการยืนยันท่าทางของผู้ใช้ ให้ประเมิน request.hasGesture() หรือบังคับใช้โมดัลยืนยันระดับแอปพลิเคชัน โดยสังเกตว่าเอกสารของ Android เน้นย้ำบางโฟลว์การใช้งานจริงที่ hasGesture() อาจคืนค่า false

การแยกความแตกต่างระหว่างการนำทางเว็บปกติกับ Deep Link Handoff: เมื่อใดควรคืนค่า false เทียบกับ true

ค่าส่งคืนแบบบูลีนของ shouldOverrideUrlLoading จะควบคุมวงจรการแสดงผลของ WebView:

  • คืนค่า false: แจ้งเฟรมเวิร์ก Android ว่าแอปพลิเคชัน ไม่ได้ จัดการ URL นั้น WebView จะทำการนำทางเว็บตามมาตรฐานโดยโหลด URL ภายในคอนเทนเนอร์ Chromium ของตัวเอง ค่าส่งคืนนี้เป็นข้อบังคับสำหรับหน้าเว็บ http:// และ https:// มาตรฐานที่ควรแสดงผลภายในเบราว์เซอร์ที่ฝังอยู่
  • คืนค่า true: แจ้งเฟรมเวิร์กว่าแอปพลิเคชัน ได้ดักจับและจัดการ URL นั้นแล้ว WebView จะยกเลิกกระบวนการโหลดภายในทันที ค่าส่งคืนนี้เป็นข้อบังคับเมื่อมีการเปิด Intent ภายนอก, การกำหนดเส้นทางไปยัง native activity, หรือการระงับ scheme ที่ไม่ได้รับอนุญาต

การจัดการ App Links ของแอปพลิเคชันที่ผ่านการยืนยันภายในคอนเทนเนอร์ที่ฝังอยู่

เมื่อลิงก์ภายใน WebView ชี้ไปยังโดเมน HTTPS ที่ผ่านการยืนยันของแอปพลิเคชันเอง (เช่น https://app.example.com/product/1024) การเรียกใช้การนำทางเริ่มต้นจะทำให้ WebView โหลดเวอร์ชันเว็บของหน้าผลิตภัณฑ์นั้น

เพื่อมอบประสบการณ์แบบเนทีฟ WebViewClient ควรตรวจสอบ URL HTTPS ที่เข้ามา หากโฮสต์ตรงกับโดเมนที่เกี่ยวข้องของแอปพลิเคชัน ไคลเอ็นต์จะทำการดักจับคำขอ ตรวจสอบเส้นทางและพารามิเตอร์ของ Query กับตัวตรวจสอบเส้นทางภายใน (AppLinkRouteValidator) และส่งข้อมูลไปยังเราเตอร์เนทีฟโดยตรง (internalRouter(validatedRoute)) พร้อมส่งค่าคืนเป็น true เพื่อยกเลิกการแสดงผลบนเว็บที่ซ้ำซ้อน

การสร้างตัวจัดการ Intent ที่ปลอดภัยสำหรับ Custom Schemes และ Intent URIs

การแยกส่วนประกอบไวยากรณ์ Chrome Intent URI: Scheme, Package, และ S.browser_fallback_url

บน Android หน้าเว็บมักใช้ Chrome Intent URI เพื่อจัดโครงสร้าง deep link เนทีฟพร้อมกลไกสำรองเบราว์เซอร์ในตัว Intent URI จะใช้ scheme intent:// และเข้ารหัสพารามิเตอร์การกำหนดเส้นทาง เป้าหมายของแพ็กเกจ และ URL สำรอง ไว้ในสตริงที่ถูกทำให้เป็นอนุกรม (serialized):

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 for Developers Guide บน Android Intent URIs URI จะถูกแบ่งออกเป็นส่วนประกอบต่างๆ ดังนี้:

  • Host และ Path (product/detail/1024): กำหนดปลายทางภายในแอปพลิเคชัน
  • scheme=myapp: ระบุโปรโตคอลกำหนดเองพื้นฐาน
  • package=com.example.app: จำกัดการแก้ไข Intent ให้เฉพาะเจาะจงกับ Android แพ็กเกจที่ระบุเท่านั้น
  • S.browser_fallback_url=...: พารามิเตอร์พิเศษแบบ String ทางเลือกที่กำหนด URL เว็บหรือร้านค้าที่จะโหลดหากไม่ได้ติดตั้งแอปพลิเคชันเป้าหมาย

อันตรายของการดำเนินการ Raw Parsed Intent: การลดช่องโหว่ Intent Injection

รูปแบบที่ไม่ควรปฏิบัติทั่วไปในการดำเนินการคือการแยก URI ที่เข้ามาด้วย Intent.parseUri(url, Intent.URI_INTENT_SCHEME) และส่ง Intent ที่ได้ไปยัง context.startActivity(intent) โดยตรง

เพื่อให้สอดคล้องกับ OWASP Mobile Application Security Testing Guide คำแนะนำเกี่ยวกับ Insecure Deep Links การสั่งเรียกใช้ Intent ที่แยกส่วนประกอบแบบดิบจะทำให้แอปพลิเคชันเสี่ยงต่อการโจมตีแบบ Intent Injection ผู้โจมตีที่ควบคุมหน้าเว็บอาจสร้าง Intent URI ที่มีเป้าหมายส่วนประกอบที่ชัดเจนและเป็นอันตราย (component=com.example.app/.InternalAdminActivity), การกระทำที่ละเอียดอ่อน, หรือพารามิเตอร์เสริมของ Intent ที่ได้รับสิทธิ์สูง (S.is_admin=true) โดย WebView จะ deserialize พารามิเตอร์เหล่านี้และเปิด activity ที่ไม่ได้ส่งออก (unexported) หรือ activity ภายใน ซึ่งเป็นการข้ามการป้องกันการตรวจสอบสิทธิ์

Parsed intent URI ต้องได้รับการตรวจสอบและสร้างใหม่ก่อนการจัดส่งบน Android

การสร้าง Intent ขึ้นใหม่อย่างปลอดภัย: การลบส่วนประกอบและการจำกัดการกระทำไว้ที่ ACTION_VIEW

เพื่อกำจัดช่องโหว่ Intent injection แอปพลิเคชันต้องไม่เรียกใช้ Intent ที่ผ่านการ deserialized โดยตรง แต่ WebViewClient จะต้องแยกวิเคราะห์ URI ดึงข้อมูลเฉพาะข้อมูลที่ตรวจสอบแล้ว และสร้าง Intent ใหม่ที่สะอาดและเฉพาะเจาะจง:

  1. Extract Data: แยกวิเคราะห์ URI และดึงแพ็กเกจเป้าหมายและ URI ข้อมูลที่ยืนยันแล้ว
  2. Validate Package: ตรวจสอบว่าแพ็กเกจที่ประกาศนั้นตรงกับรายการที่อนุญาต (allowlist) ของพันธมิตรที่ได้รับอนุญาตหรือแพ็กเกจภายใน
  3. Reconstruct: สร้าง Intent(Intent.ACTION_VIEW, validatedDataUri) ใหม่
  4. Isolate: ใช้ intent.addCategory(Intent.CATEGORY_BROWSABLE) เพื่อให้แน่ใจว่าเฉพาะกิจกรรมที่เรียกดูได้เท่านั้นที่สามารถจัดการคำขอได้ ตั้งค่า Flag ที่ชัดเจน (FLAG_ACTIVITY_NEW_TASK), และตรวจสอบให้แน่ใจว่า intent.component และ intent.selector ยังคงเป็น null

การบังคับใช้รายการที่อนุญาต (Allowlist) สำหรับ Scheme, Host, Path, และ Query Parameter

ทั้ง custom scheme (myapp://) และ Intent URI ที่ถูกสร้างขึ้นใหม่จะต้องผ่านตัวตรวจสอบที่เข้มงวดก่อนดำเนินการ

ตัวอย่างการนำไปใช้นี้จัดการ scheme myapp:// ของแอปพลิเคชันเองโดยเฉพาะ และยึดการดำเนินการ Intent ไว้ที่ com.example.app สำหรับระบบนิเวศของพันธมิตรหลายแอป นักพัฒนาควรจัดการการจับคู่ scheme กับแพ็กเกจที่เข้มงวด หรือให้ความสำคัญกับ HTTPS App Links ที่ผ่านการยืนยันแล้ว

ตัวตรวจสอบจะตรวจสอบ:

  • Scheme: ตรงกับ custom scheme ที่ได้รับอนุญาต (myapp)
  • Host: ตรงกับหมวดหมู่การกำหนดเส้นทางที่กำหนดไว้ล่วงหน้า (open, product, event)
  • Path Prefix: เป็นไปตามโครงสร้างเส้นทางที่คาดหวัง (/detail/, /view/, /main/)
  • Query Parameters: บังคับใช้คีย์ Query ที่อยู่ในรายการที่อนุญาต (target, id, promo, utm_source) และตรวจสอบว่าค่าพารามิเตอร์เป็นไปตามข้อจำกัดความยาว (เช่น ≤128\le 128 ตัวอักษร) และชุดอักขระตัวเลข
[ผู้ใช้กดลิงก์ภายใน Android WebView]
                   │
                   ▼
[WebViewClient.shouldOverrideUrlLoading]
                   │
     ┌─────────────┴─────────────┐
     ▼                           ▼
[!request.isForMainFrame]   [request.isForMainFrame]
     │                           │
     ▼                           ▼
[ตรวจสอบ Subframe Scheme]     [ตรวจสอบ URL Scheme]
├─ HTTP(S) -> คืนค่า false       │
└─ Non-Web -> คืนค่า true   ┌────┴───────────────────────────┐
   (ระงับข้อผิดพลาด)         ▼                                ▼
                     [HTTP / HTTPS]                 [Non-HTTP(S) Scheme]
                            │                                │
                            ▼                                ▼
                   [ตรวจสอบโดเมนโฮสต์]              [CustomSchemeValidator]
                   ├─ Internal App Link             ├─ ถูกต้อง -> เปิด Pinned Intent
                   │  -> เราเตอร์ภายใน            └─ Intent -> สร้างใหม่ & สำรอง
                   └─ External Web
                      -> คืนค่า false

วิธีจัดการการมองเห็นแพ็กเกจ Android 11 และการสำรองข้อมูล App Store

การมองเห็นแพ็กเกจส่งผลต่อการสืบค้นล่วงหน้า ในขณะที่ความล้มเหลวในการเปิดใช้งานจะใช้การสำรองข้อมูลที่ผ่านการตรวจสอบแล้ว

การเปลี่ยนแปลงการมองเห็นแพ็กเกจใน Android 11: เหตุใดการสอบถามล่วงหน้า (Preflight) จึงต้องมีการประกาศ

เริ่มตั้งแต่ Android 11 (API level 30) Google ได้แนะนำการกรอง Package Visibility เพื่อเพิ่มความเป็นส่วนตัวของผู้ใช้ ภายใต้กรอบงานนี้ มุมมองของแอปพลิเคชันเกี่ยวกับแอปพลิเคชันอื่นที่ติดตั้งไว้จะถูกจำกัดโดยค่าเริ่มต้น

สิ่งสำคัญคือต้องเข้าใจความแตกต่างระหว่างการเปิด Intent และการสอบถาม Intent:

  • Direct Intent Launching: การเรียกมาตรฐานไปยัง context.startActivity(intent) ไม่ จำเป็นต้องมีการประกาศการมองเห็นแพ็กเกจ หากติดตั้งแอปพลิเคชันเป้าหมายไว้ ระบบปฏิบัติการจะกำหนดเส้นทางและเปิดใช้งานได้สำเร็จโดยไม่คำนึงว่าแอปนั้นจะมองเห็นการสอบถามแพ็กเกจหรือไม่
  • Preflight Intent Queries: หากสถาปัตยกรรม WebViewClient ของคุณดำเนินการตรวจสอบล่วงหน้า เช่น การเรียก packageManager.resolveActivity(intent, 0) หรือ packageManager.queryIntentActivities(intent, 0) เพื่อตรวจสอบการมีอยู่ของแอปพลิเคชันก่อนเปิดใช้งาน การสอบถามจะคืนค่า null หรือรายการว่างเปล่า เว้นแต่แพ็กเกจหรือ scheme เป้าหมายจะถูกประกาศใน AndroidManifest.xml

การประกาศองค์ประกอบ <queries> ใน AndroidManifest.xml สำหรับ Custom Schemes และเป้าหมายแพ็กเกจ

เพื่อให้สอดคล้องกับ Android Developers Guide บน Package Visibility ใน Android 11 แอปพลิเคชันที่ทำการตรวจสอบล่วงหน้าก่อนดำเนินการ Intent ควรประกาศแพ็กเกจเป้าหมายหรือลายเซ็น Intent ใน Manifest:

<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): การกำหนดเส้นทางร้านค้าอย่างสง่างามเมื่อแอปพลิเคชันเป้าหมายไม่อยู่

เมื่อ custom scheme หรือ Intent URI ที่ถูกดักจับไม่สามารถเปิดได้เนื่องจากไม่ได้ติดตั้งแอปพลิเคชันเป้าหมายไว้ในอุปกรณ์ของผู้ใช้ WebViewClient จะต้องดำเนินการตามเครือข่ายสำรองที่ชัดเจน:

  1. Extract and Bind Fallback URL: สำหรับ URI แบบ intent:// ให้แยกพารามิเตอร์สตริง S.browser_fallback_url และตรวจสอบว่ามันชี้ไปยังปลายทางที่ได้รับอนุญาต สำหรับ URL ของ Google Play ให้ตรวจสอบว่าพารามิเตอร์ id ตรงกับชื่อแพ็กเกจเป้าหมายอย่างเคร่งครัด (id=targetPackage) เพื่อป้องกันการโจมตีแบบ open-redirect
  2. Market URI Generation: หากไม่มี URL สำรอง ให้สร้าง Intent ของตลาด: market://details?id=com.example.app
  3. Web Play Store Fallback: หากปิดการใช้งานหรือไม่มี Google Play ให้ดักจับ ActivityNotFoundException ใน Intent ของตลาดและโหลด https://play.google.com/store/apps/details?id=com.example.app

การเชื่อมโยงพารามิเตอร์บริบทข้ามการเปลี่ยนเส้นทางสำรองด้วย OpoInstall

เมื่อผู้ใช้ที่ไม่ได้ติดตั้งแอปถูกส่งไปยัง Google Play ลิงก์ร้านค้าแบบคงที่จะละทิ้งพารามิเตอร์แบบไดนามิก OpoInstall ซึ่งเป็นแพลตฟอร์มการระบุแหล่งที่มา (attribution) และ deep linking บนมือถือ จะเชื่อมช่องว่างนี้โดยการจับโทเค็นอ้างอิงและบริบทแคมเปญบนแบ็กเอนด์ของ attribution ก่อนการเปลี่ยนเส้นทางร้านค้า ตรวจสอบพฤติกรรมการเรียกคืน deferred-parameter และ API การเริ่มต้นกับ OpoInstall Android SDK เวอร์ชันที่ปรับใช้ในการผลิต

เมื่อติดตั้งในภายหลังและเปิดแอปครั้งแรก OpoInstall Android SDK จะเรียกข้อมูลพารามิเตอร์ที่แคชไว้ ช่วยให้แอปพลิเคชันหลักกู้คืนฉากเป้าหมายของผู้ใช้ได้ โปรดตรวจสอบ เอกสารประกอบการรวม SDK สำหรับคำแนะนำในการรับพารามิเตอร์ deferred หลังจากติดตั้ง

การนำ WebViewClient ไปใช้ในการผลิตเพื่อการดักจับ Scheme อย่างปลอดภัย

การสร้างสถาปัตยกรรม WebViewClient แบบกำหนดเองใน Kotlin

WebViewClient ในการผลิตจะประสานการตรวจสอบเฟรม, การตรวจสอบ Allowlist ของ scheme, การสร้าง Intent ใหม่, การแยก URL สำรอง, และการกำหนดเส้นทางภายใน การนำไปใช้จะสรุปกฎการตรวจสอบความถูกต้องไว้ภายในวัตถุตัวตรวจสอบเฉพาะ (AppLinkRouteValidator และ CustomSchemeValidator) เพื่อรักษา callback การนำทางให้สะอาดและดูแลรักษาง่าย

การนำ AppLinkRouteValidator และ CustomSchemeValidator ไปใช้

วัตถุตัวตรวจสอบบังคับใช้ความปลอดภัยแบบ fail-closed ที่เข้มงวด:

  • AppLinkRouteValidator ตรวจสอบโดเมน HTTPS ภายใน, คำนำหน้าเส้นทางที่อนุญาต, และคีย์ Query ที่ได้รับอนุญาต (target, id, promo, utm_source) โดยคืนค่าเป็น data class ValidatedAppRoute ที่มีโครงสร้าง
  • CustomSchemeValidator ตรวจสอบ custom scheme ที่ได้รับอนุญาต (myapp), ตรวจสอบโฮสต์ที่อนุญาต (open, product, event), บังคับใช้คำนำหน้าเส้นทาง, และทำความสะอาดค่าพารามิเตอร์ Query กับชุดอักขระตัวเลขและข้อจำกัดความยาว (≤128\le 128 ตัวอักษร)

ตัวอย่างการนำไปใช้ในโค้ดด้านล่างแสดงให้เห็นวิธีกำหนดค่าตัวเลือกการประกาศการมองเห็นแพ็กเกจ AndroidManifest.xml และการนำ CustomWebViewClient ที่ปลอดภัยและ thread-safe ไปใช้ใน Kotlin พร้อมการจัดการสำรองอัตโนมัติ ไบนารี SDK ที่ผ่านการรับรองและ Wrapper สำหรับการรวมสามารถรับได้จาก ศูนย์ดาวน์โหลด SDK ของ OpoInstall

<!-- AndroidManifest.xml: ตัวเลือกการประกาศการมองเห็นแพ็กเกจสำหรับการสอบถามล่วงหน้า Intent (Android 11+) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app">

    <!-- ประกาศแพ็กเกจภายนอกและ custom scheme เฉพาะในกรณีที่ใช้การสอบถาม preflight resolveActivity -->
    <queries>
        <!-- อนุญาตการค้นหาล่วงหน้าสำหรับพันธมิตรเฉพาะหรือแพ็กเกจเป้าหมาย -->
        <package android:name="com.example.partner" />
        
        <!-- อนุญาตการแก้ไขล่วงหน้าของ custom deep linking schemes -->
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="myapp" />
        </intent>
        
        <!-- อนุญาตการแก้ไขล่วงหน้าของโดเมน HTTPS App Link ที่ผ่านการยืนยัน -->
        <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: Custom WebViewClient พร้อมความปลอดภัยของขอบเขตเฟรม, การตรวจสอบ Allowlist, และการกำหนดเส้นทางสำรอง
// ตัวอย่างการรวมอ้างอิง; ตรวจสอบลายเซ็นเมธอดกับสถาปัตยกรรม 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) {
            // ท่าทีด้านความปลอดภัยที่เข้มงวด: ปฏิเสธคำขอหากมีคีย์ Query ที่ไม่รู้จัก
            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()
            // อนุญาตการโหลด subframe HTTP(S) มาตรฐาน แต่ระงับ scheme ที่ไม่ใช่เว็บเพื่อป้องกันการจี้ iframe และข้อผิดพลาด
            return if (scheme == "http" || scheme == "https") {
                false
            } else {
                Log.w("WebViewClient", "ระงับ non-web scheme ใน subframe/iframe: $scheme")
                true // บริโภค/ยกเลิกโดยไม่เปิด Intent ภายนอกหรือทำให้เกิดข้อผิดพลาด Chromium
            }
        }

        val url = request.url.toString()
        return handleUrlNavigation(view, url)
    }

    @Deprecated("Deprecated in Java")
    override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
        if (url == null) return false

        val uri = Uri.parse(url)
        val scheme = uri.scheme?.lowercase()

        // Legacy callback ขาดข้อมูลเมตา isForMainFrame ที่เชื่อถือได้; อย่ากำหนดเส้นทาง scheme ภายนอก
        return if (scheme == "http" || scheme == "https") {
            false // อนุญาตการโหลดเนื้อหาเว็บมาตรฐาน
        } else {
            Log.w("WebViewClient", "บล็อก external scheme บน legacy callback: $scheme")
            true // ระงับข้อผิดพลาด scheme ที่ไม่ได้จัดการโดยไม่เปิด Intent ภายนอก
        }
    }

    private fun handleUrlNavigation(view: WebView?, url: String): Boolean {
        val uri = Uri.parse(url)
        val scheme = uri.scheme?.lowercase() ?: return false

        // 1. จัดการ HTTP / HTTPS และ Internal App Links
        if (scheme == "http" || scheme == "https") {
            // ตรวจสอบว่า URL ตรงกับโดเมน App Link ของแอปเองหรือไม่
            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", "ปฏิเสธ internal App Link ที่ผิดรูปแบบหรือไม่ได้รับอนุญาต: $uri")
                }
                return true // จัดการภายใน; ยกเลิกการโหลด webview
            }
            return false // อนุญาตให้ WebView โหลดเนื้อหาเว็บมาตรฐานตามธรรมชาติ
        }

        // 2. ตรวจสอบ custom schemes ด้วย Allowlist ที่เข้มงวดและการตรึงแพ็กเกจเพื่อป้องกันการจี้
        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) {
                            // สร้าง Intent ACTION_VIEW ใหม่ที่สะอาดแทนการเรียกใช้วัตถุที่แยกวิเคราะห์แบบดิบ
                            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 ที่มี data URI ไม่ถูกต้อง: $rawDataUri")
                            true
                        }
                    } else {
                        Log.w("WebViewClient", "ปฏิเสธ intent ที่ไม่มีข้อมูล payload ของ data URI")
                        true
                    }
                } else {
                    Log.w("WebViewClient", "ปฏิเสธ intent ที่กำหนดเป้าหมายแพ็กเกจที่ไม่ได้รับการยืนยัน: $targetPackage")
                    true // ยกเลิก intent ของแพ็กเกจที่ไม่ได้รับการยืนยัน
                }
            } catch (e: Exception) {
                Log.e("WebViewClient", "ไม่สามารถแยกวิเคราะห์ intent URI: $url", e)
                true // ระงับข้อผิดพลาด scheme ที่ไม่ได้จัดการ
            }
        }

        // ปฏิเสธ scheme ที่ไม่รู้จักเพื่อหลีกเลี่ยงข้อผิดพลาด net::ERR_UNKNOWN_URL_SCHEME
        Log.w("WebViewClient", "บล็อกการนำทางด้วย scheme ที่ไม่ได้รับการยืนยัน: $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}. กำลังเรียกใช้ fallback")
            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 Store ไม่อยู่ กำลังโหลด 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 ยืนยันเส้นทางภายในที่ถูกต้อง การเปลี่ยนผ่าน UI จะต้องดำเนินการบนเธรด UI หลักของ Android หาก internalRouter ทริกเกอร์ fragments, การเปลี่ยน Activity หรือการนำทาง Compose ให้ห่อหุ้มการเรียกใช้ dispatch ภายใน runOnUiThread หรือ coroutine ของวงจรเธรดหลักเพื่อป้องกันการแครชของเธรด

เมทริกซ์การแก้ไขปัญหาและข้อผิดพลาดการเปลี่ยนเส้นทาง Android WebView

คู่มือการแก้ไขปัญหา Android WebView Deep Linking อย่างครอบคลุม

เมทริกซ์ด้านล่างแสดงรายละเอียดลายเซ็นข้อผิดพลาดทั่วไปที่พบเมื่อจัดการ custom schemes ใน Android WebView พร้อมสาเหตุรากเหง้าหลักและการแก้ไขทางวิศวกรรม:

ลายเซ็นข้อผิดพลาด / อาการ สาเหตุหลัก API Levels ที่ได้รับผลกระทบ จุดตรวจสอบการวินิจฉัย การแก้ไขที่แนะนำ
net::ERR_UNKNOWN_URL_SCHEME Scheme ที่ไม่ใช่เว็บไม่ถูกจัดการในตัวโหลด WebView ทุกระดับ API shouldOverrideUrlLoading หายไปหรือคืนค่า false Override delegate, ดักจับ scheme, คืนค่า true
ActivityNotFoundException ไม่มีแอปเป้าหมายบนอุปกรณ์ ทุกระดับ API context.startActivity() ไม่ได้รับการจัดการ ดักจับข้อยกเว้นและดำเนินการ S.browser_fallback_url หรือเส้นทาง Play Store
Iframe Intent Hijacking Subframe ทริกเกอร์ Intent ภายนอก API 24+ request.isForMainFrame ไม่ถูกตรวจสอบ ป้องกันด้วย if (!request.isForMainFrame) และระงับ scheme ที่ไม่ใช่เว็บ
Preflight Query Returns Null ตัวกรองการมองเห็นแพ็กเกจ Android 11+ API 30+ (Android 11+) เรียก resolveActivity โดยไม่มีการประกาศใน Manifest ประกาศ <queries> ใน Manifest หรือใช้ startActivity try-catch โดยตรง
Internal App Link Reload Loop Host App Link ถูกส่งเส้นทางกลับไปยัง WebView ทุกระดับ API โดเมนโฮสต์ไม่ได้รับการจัดการใน delegate ดักจับโดเมนของตนเอง, ตรวจสอบเส้นทาง, ส่งไปยังเราเตอร์เนทีฟ

คำถามที่พบบ่อย (FAQ)

ทำไม Android WebView ของฉันถึงแครชเมื่อเปิด custom URL schemes?
Custom scheme จะทำให้เกิด `net::ERR_UNKNOWN_URL_SCHEME` เมื่อ URL ยังคงอยู่ในกระบวนการโหลดของ WebView แทนที่จะถูกดักจับโดยแอปพลิเคชันหลัก ด้วย `WebViewClient` แบบกำหนดเอง ให้คืนค่า `true` หลังจากจัดการหรือระงับ scheme ที่ไม่ใช่เว็บอย่างปลอดภัย; คืนค่า `false` เฉพาะเมื่อ WebView ควรดำเนินการนำทางเว็บตามปกติเท่านั้น หากแอปพลิเคชันเป้าหมายไม่อยู่ การเรียก `startActivity()` โดยไม่มีบล็อก `try-catch` จะทำให้เกิด `ActivityNotFoundException` ที่ไม่ได้จัดการ
ฉันจะจัดการ intent:// URLs ใน WebView อย่างปลอดภัยโดยไม่มีช่องโหว่ด้านความปลอดภัยได้อย่างไร?
ในการจัดการ `intent://` URI อย่างปลอดภัย ให้แยกวิเคราะห์ URI โดยใช้ `Intent.parseUri(url, Intent.URI_INTENT_SCHEME)`, ตรวจสอบว่าแพ็กเกจเป้าหมายและ data URI ตรงกับรายการที่อนุญาตที่เข้มงวด และสร้าง `Intent(Intent.ACTION_VIEW, safeDataUri)` ใหม่ที่สะอาด เพิ่ม `Intent.CATEGORY_BROWSABLE` อย่างชัดเจนเพื่อให้แน่ใจว่าเฉพาะกิจกรรมที่เรียกดูได้เท่านั้นที่จัดการ Intent นี้ ลบส่วนประกอบที่เป็นอันตรายออก และดำเนินการจัดการ URL สำรองที่ผ่านการตรวจสอบแล้วซึ่งผูกกับแพ็กเกจเป้าหมาย
Android 11 package visibility ส่งผลต่อการดักจับ deep link ใน WebView อย่างไร?
การมองเห็นแพ็กเกจใน Android 11 ไม่ได้ป้องกันไม่ให้ `startActivity()` เปิดกิจกรรมภายนอกที่ตรงกัน อย่างไรก็ตาม หาก `WebViewClient` ของคุณตรวจสอบล่วงหน้าโดยใช้ `resolveActivity()` หรือ `queryIntentActivities()` ก่อนที่จะพยายามเปิดใช้งาน คุณต้องประกาศชื่อแพ็กเกจเป้าหมายหรือ custom intent scheme ภายในองค์ประกอบ `<queries>` ในไฟล์ `AndroidManifest.xml` ของคุณ

สรุปและกรอบการตัดสินใจ

การจัดการ deep linking และ custom schemes ภายใน Android WebView จำเป็นต้องเชื่อมช่องว่างระหว่าง sandbox การแสดงผลเว็บกับการกำหนดเส้นทาง Intent ของระบบปฏิบัติการเนทีฟ การพึ่งพาพฤติกรรมเริ่มต้นของ WebView จะส่งผลให้เกิดข้อผิดพลาดของโปรโตคอลที่ไม่ได้จัดการ ประสบการณ์ผู้ใช้ที่ไม่ดี และช่องโหว่ด้านความปลอดภัยที่อาจเกิดขึ้น

ด้วยการนำ WebViewClient ที่มีประสิทธิภาพไปใช้ซึ่งตรวจสอบขอบเขตเฟรม ตรวจสอบ custom schemes กับรายการที่อนุญาตที่ชัดเจน สร้าง ACTION_VIEW intent ที่ปลอดภัย และจัดการเครือข่ายสำรองของร้านค้า ทีมวิศวกรรมจะลดความล้มเหลวในการจัดการ scheme ของ WebView และสนับสนุนการนำทางที่ยืดหยุ่นมากขึ้น การรวมการกำหนดเส้นทางฝั่งไคลเอ็นต์ที่เชื่อถือได้เข้ากับกรอบงานการระบุแหล่งที่มาบนมือถือที่ครอบคลุมจะช่วยลดความขัดแย้งในการกำหนดเส้นทางข้ามโฟลว์ attribution และสนับสนุนการเดินทางของผู้ใช้ที่สอดคล้องมากขึ้น

เพื่อสำรวจสถาปัตยกรรมการกำหนดเส้นทางพารามิเตอร์และการเชื่อมโยง deep link ของ Android เนทีฟ ให้ปรึกษา เอกสารประกอบการรวม SDK, ดาวน์โหลดไลบรารีไคลเอ็นต์จาก ศูนย์ดาวน์โหลด SDK ของ OpoInstall, สำรวจ การอ้างอิงการนำ attribution บนมือถือไปใช้งาน หรือลงทะเบียนแอปพลิเคชันของคุณบน คอนโซลนักพัฒนา OpoInstall

วัสดุที่เกี่ยวข้อง

Share this article

Keep Discovering

Meta มองเห็นหนทางสู่ความสามารถในการทำกำไรของ Muse? สิ่งที่ทีมเติบโตบนมือถือควรเรียนรู้

Meta มองเห็นหนทางสู่ความสามารถในการทำกำไรของ Muse? สิ่งที่ทีมเติบโตบนมือถือควรเรียนรู้

Meta เผยแนวทางสร้างกำไรให้ Muse เมื่อ AI สำหรับผู้บริโภคเติบโตขึ้น เรียนรู้ว่าการรักษาผู้ใช้งาน, การช็อปปิ้งผ่านเอเจนต์ และการระบุที่มาบนมือถือหล่อหลอมเศรษฐศาสตร์หน่วยธุรกิจอย่างไร

วิธีเพิ่มประสิทธิภาพการแปลงการสแกนแอปเป็นยอดติดตั้งด้วยคิวอาร์โค้ดแบบกำหนดเอง

วิธีเพิ่มประสิทธิภาพการแปลงการสแกนแอปเป็นยอดติดตั้งด้วยคิวอาร์โค้ดแบบกำหนดเอง

เรียนรู้วิธีที่คิวอาร์โค้ดแบบกำหนดเองช่วยเพิ่มประสิทธิภาพการแปลงการสแกนเป็นยอดติดตั้ง ผ่านการจัดกรอบภาพที่มีความคอนทราสต์สูง ข้อความกระตุ้นการกระทำ และการติดตามการระบุแหล่งที่มาที่แม่นยำ

วิธีสร้างโค้ดดาวน์โหลดแอปแบบมีโลโก้แบรนด์พร้อมการปรับแต่ง

วิธีสร้างโค้ดดาวน์โหลดแอปแบบมีโลโก้แบรนด์พร้อมการปรับแต่ง

เรียนรู้วิธีใช้เครื่องมือสร้างคิวอาร์โค้ดที่รองรับโลโก้ เพื่อสร้างโค้ดดาวน์โหลดแอปที่สะท้อนเอกลักษณ์แบรนด์ โดยอาศัยการแก้ไขข้อผิดพลาดระดับ H และการเรนเดอร์ผ่าน HTML5 Canvas