เพราะมันมีกำแพงบางๆคอยกั้นขวางระหว่างเราสอง และกำแพงนั้นมีชื่อว่า Privacy

ย้อนกลับไปตั้งแต่ Android 10 ไปจนถึงเวอร์ชันแรกๆ บนแอนดรอยด์นั้นเปิดให้นักพัฒนาสามารถใช้คำสั่งเพื่อเช็คได้ว่าในอุปกรณ์แอนดรอยด์ของผู้ใช้มีแอปอะไรติดตั้งอยู่บ้าง

val context: Context = /* ... */
context.packageManager
    .getInstalledApplications(0)
    .forEach { applicationInfo ->
        val packageName = applicationInfo.packageName
        val appName = applicationInfo.name
        // Do something
    }

และสามารถดูรายละเอียดต่างๆจาก Android Manifest ที่แอปนั้นๆได้ประกาศไว้นั้นเอง

แต่ทว่า..

ต้องเป็นฟีเจอร์แบบไหน ถึงเรียกคำสั่งแบบนี้ในแอป?

นั่นก็เพราะว่าแอปส่วนใหญ่ไม่จำเป็นต้องใช้คำสั่งแบบนี้เลยซักนิด

และพอได้มองในอีกมุมหนึ่งก็จะพบว่า แอปที่ติดตั้งอยู่ในเครื่องสามารถคาดเดาได้ว่าผู้ใช้มีพฤติกรรมหรือความชอบแบบไหน ได้จากแอปที่ติดตั้งอยู่ในเครื่อง โดยไม่จำเป็นต้องขอ Permission ใดๆเลยซักนิด

จึงทำให้ใน Android 11 ที่เน้นในเรื่อง Privacy และ Security ได้เพิ่มสิ่งที่เรียกว่า Package Visibility เข้ามา เพื่อแก้ปัญหาเรื่องนี้โดยเฉพาะ

Package Visibility คือ?

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

ดังนั้นเมื่อแอปมีการใช้คำสั่งดังกล่าว ก็จะเห็นแค่แอปบางตัวเท่านั้น ซึ่งจะประกอบไปด้วย

  • แอปของเราเอง
  • แอปที่เกี่ยวข้องกับการทำงานของระบบแอนดรอยด์โดยตรง
  • แอปที่ติดตั้งแอปของเรา
  • แอปที่เรียกแอปของเราผ่านคำสั่ง startActivityForResult(...)
  • แอปที่ผูก Service ไว้กับแอปของเรา
  • แอปที่เข้าใช้งาน Content Provicer ของแอปเรา
  • แอปที่มี Content Provider และแอปของเราได้ขอ Permission ในการเข้าใช้งาน
  • แอปที่เป็น IME (Input Method Editor)

แอปที่อยู่นอกเหนือจากเงื่อนไขเหล่านี้ก็จะถูกซ่อนไว้ให้ไม่มองเห็นด้วยความสามารถของ Package Visibility นั่นเอง

โดย Package Visibility จะมีผลก็ต่อเมื่อแอปนั้นๆมี Target API Level 30 ขึ้นไป และเปิดใช้งานบน Andorid 11 ขึ้นไป

Visibility Package ส่งผลกระทบกับนักพัฒนาแอปอย่างไรบ้าง?

ก่อนที่จะหวาดระแวงกันไปมากกว่านี้ มาดูกันก่อนว่า Package Visibility ส่งผลกระทบกับนักพัฒนาอย่างไร

ก่อนอื่นต้องบอกเลยว่าการใช้ Implicit และ Explicit Intent จะไม่ได้รับผลกระทบใดๆจาก Package Visibility แต่จะมี Intent Action บางตัวที่ได้รับผลกระทบทางอ้อม ซึ่งจะมีดังนี้

  • MediaStore.ACTION_IMAGE_CAPTURE
  • MediaStore.ACTION_IMAGE_CAPTUR_SECURE
  • MediaStore.ACTION_VIDEO_CAPTURE

ส่วนกระทบทางอ้อมนั้นเป็นอย่างไร เดี๋ยวพูดถึงทีหลังนะ

โดย Package Visibility จะมีผลกับคำสั่งที่เกี่ยวข้องกับการ Query ข้อมูลของแอปที่ติดตั้งอยู่ในเครื่องของผู้ใช้ ซึ่งจะประกอบไปด้วย

  • PackageManager.getInstalledPackages(...)
  • PackageManager.getInstalledApplications(...)
  • PackageManager.queryIntentActivities(...)
  • PackageManager.queryBroadcastReceivers(...)
  • Intent.resolveActivity(...)
  • Intent.resolveActivityInfo(...)

หรือคำสั่งอื่นๆนอกเหนือจากนี้ที่ Lint ของ Android Studio มีการแจ้งเตือนในลักษณะแบบนี้

ทั้งนี้นักพัฒนาอาจจะมีเงื่อนไขบางอย่างที่จำเป็นต้องใช้คำสั่งดังกล่าว เช่น การสร้าง Implicit Intent ที่แอปในเครื่องของผู้ใช้อาจจะไม่รองรับ จึงต้องมีการใช้ Package Manager เพื่อเช็คดูก่อนว่ามีแอปที่รองรับกับ Implicit Intent ดังกล่าวหรือไม่ เพื่อทำเป็น Fallback ต่อไป

ยกตัวอย่างเช่น แอปของนักพัฒนาต้องการให้เปิดไฟล์ PDF ผ่านแอปอื่นๆที่อยู่ในเครื่อง โดยใช้ Implicit Intent แบบนี้

val intent = Intent(Intent.ACTION_VIEW).apply {
    type = "application/pdf"
    ...
}
startActivity(Intent.createChooser(intent, null))

แต่ทว่าบนอุปกรณ์แอนดรอยด์บางยี่ห้อนั้นอาจจะไม่ได้ติดตั้งแอปสำหรับเปิดไฟล์ PDF ไว้ จึงมีโอกาสที่แอปจะพังจากคำสั่งแบบนี้ได้

ดังนั้นวิธีที่ดีที่สุดจึงเป็นการเพิ่มโค้ดสำหรับเช็คว่าในเครื่องของผู้ใช้มีแอปที่รองรับการเปิดไฟล์ PDF หรือไม่ ถ้าไม่มีก็จะให้เปิด Google Play เพื่อดาวน์โหลดแอปเสียก่อน

val packageManager: PackageManager = /* ... */
val intent = Intent(Intent.ACTION_VIEW).apply {
    type = "application/pdf"
    ...
}
val isAppSupported = intent.resolveActivityInfo(packageManager, 0) != null
if (isAppSupported) {
    startActivity(Intent.createChooser(intent, null))
} else {
    // Suggest the user to download PDF viewer app in Google Play
}

จะเห็นว่าโค้ดดังกล่าวมีการใช้คำสั่ง Intent.resolveActivity(...) เพื่อให้ Implicit Intent สามารถได้อย่างปลอดภัยเท่านั้นเอง ไม่ได้ต้องการทำอย่างอื่นที่ส่งผลต่อ Privacy ของผู้ใช้เลยซักนิด

แต่เมื่อทำงานบน Android 11 เป็นต้นไป ก็จะทำให้คำสั่งนี้มีโอกาสทำงานผิดพลาดได้ ถ้า

  • เครื่องนั้นๆไม่มีแอปสำหรับเปิดไฟล์ PDF ติดมากับเครื่อง
  • ผู้ใช้ติดตั้งแอปสำหรับเปิดไฟล์ PDF จาก Google Play

กลายเป็นว่าแอปของนักพัฒนาจะมองไม่เห็นแอปตัวนี้ทันทีตามเงื่อนไขของ Package Visibility และโค้ดในตัวอย่างนี้ก็จะแจ้งให้ผู้ใช้ติดตั้งแอปจาก Google Play ตลอด

จะให้ลบโค้ดออกไปก็คงไม่ได้ ดังนั้นมาทำให้โค้ดทำงานบน Android 11 ได้อย่างถูกต้องกันเถอะ

การทำให้แอปทำงานร่วมกับ Package Visibility ได้อย่างถูกต้อง

เพื่อให้ระบบแอนดรอยด์รู้ว่าแอปของนักพัฒนามีเหตุผลที่จำเป็นต้องใช้งานคำสั่งที่มีผลต่อ Package Visivility จริงๆ ไม่ได้ขอมั่วซั่ว จะต้องใส่รายละเอียดในการใช้งานผ่าน <queries> ใน Android Manifest

<!-- AndroidManifest.xml -->
<manifest ...>
    ...
    <queries>
        <!-- A specific set of other apps -->
    </queries>
</manifest>

โดยนักพัฒนาจะต้องระบุรายละเอียดลงใน <queries> เพื่อให้ระบบแอนดรอยด์สามารถตรวจสอบได้นั่นเอง (รวมไปถึง Google Play ด้วย)

และการใส่รายละเอียดลงใน <queries> จะมีอยู่ทั้งหมด 3 วิธีด้วยกัน

  • กำหนดด้วย Package Name
  • กำหนดด้วย Intent Filter Signature
  • กำหนดด้วย Content Provider Authority

กำหนดด้วย Package Name

ในกรณีที่อยากจะเจาะจงเป็นรายแอปไปเลย เช่น ต้องการให้ผู้ใช้กดปุ่มแชร์จากในแอปแล้วแชร์เนื้อหาลงบน Facebook ได้เลย จึงมีการใช้คำสั่ง Intent.resolveActivity(...) เพื่อเช็คว่าผู้ใช้มีการติดตั้งแอป Facebook อยู่ในเครื่องหรือไม่

ดังนั้นนักพัฒนาสามารถกำหนดเป็นชื่อ Package Name ของแอปที่ต้องการได้เลย

<queries>
    <package android:name="com.facebook.katana" />
    <package android:name="com.facebook.orca" />
</queries>

กำหนดด้วย Intent Filter Signature

ถ้าใช้เป็น Implicit Intent และต้องการเช็คว่ามีแอปตัวไหนในเครื่องที่รองรับบ้าง ก็ให้กำหนดข้อมูลของ Intent Filter Signature ลงใน <queries> แทนได้เลย

<queries>
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:mimeType="application/pdf" />
    </intent>
</queries>

กำหนดด้วย Content Provider Authority

กรณีที่ต้องการใช้ Implicit Intent เพื่อเข้าถึงข้อมูลจากแอปอื่นๆภายในเครื่องผ่าน Content Provider ก็ให้กำหนด A

<queries>
    <provider android:authorities="com.akexorcist.gallery.provider" />
    <provider android:authorities="com.google.provider.picker.files" />
</queries>

แล้วแอปที่จำเป็นต้องเห็นแอปทั้งหมดในเครื่องจริงๆล่ะ?

อย่างที่บอกไปในตอนแรกว่าแอปส่วนใหญ่ไม่จำเป็นต้องเห็นแอปทั้งหมดในเครื่องของผู้ใช้ แต่ทว่าก็มีแอปบางประเภทที่จำเป็นจริงๆ เช่น

  • Launcher App
  • Accessibility App
  • Browser App
  • Security App

จะเห็นว่าแอปจำพวกนี้จำเป็นต้องเห็นแอปทั้งหมดในเครื่องถึงจะทำงานได้เต็มที่ ดังนั้นทีมแอนดรอยด์จึงเตรียม Permission ตัวหนึ่งไว้ที่ชื่อว่า QUERY_ALL_PACKAGES เพื่อให้แอปเหล่านี้ใช้งานนั่นเอง

<!-- AndroidManifest.xml -->
<manifest ...>
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
    ...
</manifest>

โดย QUERY_ALL_PACKAGES นั้นเป็น Normal Permission จึงไม่ต้องมีโค้ดสำหรับ Request Permission แค่ใส่ไว้ใน Android Manifest ก็พอแล้ว

แบบนี้นักพัฒนาก็แอบใส่ QUERY_ALL_PACKAGES ไปเลยก็ได้สิ? ไม่ต้องใส่ <queries> ให้ยุ่งยาก?

จริงๆไม่ว่าจะเป็นแอปไหนก็สามารถใส่ Permission ตัวนี้ได้และทำงานได้อย่างปกติ แต่อย่างที่บอกไปว่า Permission ตัวนี้ออกแบบมาเพื่อแอปบางประเภทเท่านั้น ดังนั้นในตอนนี้มันอาจจะยังใช้งานได้อยู่ แต่ในอนาคต Google Play จะมีการปรับนโยบายใหม่ซึ่งจะมีตรวจสอบแอปที่ประกาศ QUERY_ALL_PACKAGES ไว้ใน Android Manifest ด้วย เพื่อตรวจสอบว่ามีการใช้งานอย่างเหมาะสมจริงๆหรือไม่

ดังนั้นทางที่ดี ถ้าแอปไม่ได้อยู่ในจำพวกที่จำเป็นต้องใช้ Permission ตัวนี้จริงๆ ก็ควรทำให้มันถูกต้องด้วยการใช้ <queries> ตั้งแต่แรกดีกว่านะ

ผลกระทบทางอ้อมที่ส่งผลต่อ Action ต่างๆใน MediaStore

จากที่บอกไปในตอนแรกว่า ACTION_IMAGE_CAPTURE, ACTION_IMAGE_CAPTUR_SECURE และ ACTION_VIDEO_CAPTURE ที่เป็น Intent Action ของ MediaStore นั้นจะได้รับผลกระทบทางอ้อมจากเรื่องนี้ด้วย

โดยการใช้ Intent Action เหล่านี้บน Android 11 เป็นต้นไป จะเรียกใช้งานได้เฉพาะแอปกล้องที่เป็น Built-in เท่านั้น นั่นหมายความว่าจะไม่สามารถเรียกเปิดแอปกล้องที่เป็น 3rd Party ได้แบบเดิมอีกต่อไป ซึ่งเป็นความตั้งใจของทีมแอนดรอยด์นั่นเอง

Status: Won't Fix (Intended Behavior)

Response from the engineering team:
================================
Yes, this is working as intended. If apps wish to use 3P cameras to handle their 
intent, they have the option of setting an explicit handler package name or 
component (using Intent#setClassName / setPackage / setComponent).

While this makes the handling of the not very common case mentioned here more 
complicated, we believe it's the right trade-off to protect the privacy and 
security of our users.

ถ้ามองในมุมมองของนักพัฒนาก็อาจจะเป็นเรื่องดี เพราะว่าแอปส่วนใหญ่ที่มีการเรียกใช้งานกล้องจากแอปภายนอก ก็อยากจะให้ใช้แอปกล้องที่ติดตั้งมากับเครื่องอยู่แล้ว เพื่อเลี่ยงปัญหาต่างๆจากแอปที่เป็น 3rd Party

หมายเหตุ - ต่อให้กำหนด 3rd Party Camera App เป็น Default App แล้วก็ตาม ระบบแอนดรอยด์ก็จะเรียกแต่ Built-in Camera App อยู่ดี

ในกรณีที่ต้องการเรียกใช้งานแอปกล้องที่เป็น 3rd Party จริงๆ ก็จะต้องระบุ Package Name ลงไปใน Intent โดยตรงแทน

val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
    setPackage("net.sourceforge.opencamera")
}

ถ้าต้องการเช็คว่ามีแอปติดตั้งอยู่ในเครื่องหรือไม่ก็อย่าลืมประกาศ Package Name ไว้ใน <queries> ด้วยล่ะ

และแนะนอนว่า นักพัฒนาจะสร้าง Intent Chooser สำหรับ Intent Action เหล่านี้ของ MediaStore ไม่ได้อีกต่อไปแล้ว

สรุป

Package Visibility ก็เป็นอีกหนึ่งการทำงานใหม่ที่ถูกเพิ่มเข้ามาใน Android 11 เพื่อทำให้ระบบแอนดรอยด์มีความปลอดภัยมากขึ้นและใส่ใจต่อข้อมูลส่วนตัวของผู้ใช้มากขึ้น

ดังนั้นผู้ที่หลงเข้ามาอ่านจะต้องเริ่มจากการเช็คก่อนว่ามีคำสั่งไหนในโปรเจคที่เข้าข่ายของ Package Visibility หรือไม่ ถ้ามีก็ให้ประกาศ <queries> ไว้ใน Android Manifest แล้วกำหนดข้อมูลให้ถูกต้องซะ เพื่อแสดงความโปร่งใสในการเรียกใช้งานคำสั่งต่างๆในโค้ดของเราที่อาจจะเกี่ยวข้องกับความเป็นส่วนตัวของผู้ใช้นั่นเอง

แหล่งข้อมูลอ้างอิง