सामग्री पर जाएं
सभी पोस्ट

2026 में Android Photo Picker API: पूरी गैलरी की अनुमति दिए बिना फ़ोटो अटैच करना

Android के Photo Picker API की व्यावहारिक गाइड — सिंगल और मल्टीपल सिलेक्शन, MIME फ़िल्टरिंग, Android 13 से पहले के लिए बैकपोर्ट, और यह READ_MEDIA_IMAGES से बेहतर क्यों है।

MFKAPPS 5 मिनट पढ़ना

किसी यूज़र से सिर्फ़ इसलिए उसकी पूरी फ़ोटो लाइब्रेरी देखने की अनुमति माँगना कि वह एक खर्च में एक रसीद की फ़ोटो जोड़ सके — यह उस फ़ीचर की ज़रूरत से कहीं ज़्यादा माँगना है। READ_MEDIA_IMAGES आपके ऐप को डिवाइस की हर फ़ोटो तक स्थायी पहुँच दे देता है — छुट्टियों की तस्वीरें, दूसरों के मैसेज के स्क्रीनशॉट, जो भी उसमें हो — जबकि फ़ीचर को कभी सिर्फ़ यूज़र द्वारा चुनी गई एक ही तस्वीर की ज़रूरत थी। यही बेमेल ठीक वह चीज़ है जिसे दूर करने के लिए Android का Photo Picker बनाया गया।

मैंने इसे Granyn में जोड़ा ताकि यूज़र किसी दर्ज किए गए खर्च में रसीद की फ़ोटो अटैच कर सके। यहाँ बताया गया है कि यह API असल में कैसे काम करता है, और इसने परमिशन रिक्वेस्ट को पूरी तरह ग़ायब कैसे कर दिया।

पुराना तरीक़ा, और यह समस्या क्यों है

Photo Picker से पहले, “यूज़र को फ़ोटो चुनने देना” इन दो रास्तों में से एक होता था: READ_MEDIA_IMAGES (या Android 13 से पहले READ_EXTERNAL_STORAGE) माँगना और खुद MediaStore को क्वेरी करना, या ACTION_GET_CONTENT लॉन्च करके यह उम्मीद करना कि सिस्टम का डॉक्यूमेंट पिकर अलग-अलग OEM स्किन पर एक जैसा व्यवहार करेगा। परमिशन वाला रास्ता काम तो करता है, लेकिन इसकी कीमत चुकानी पड़ती है:

  • एक ऐसे फ़ीचर के लिए रनटाइम परमिशन प्रॉम्प्ट जिसे आदतन मना करना आसान है, जो चुपचाप कई यूज़र्स के लिए उस फ़ीचर को बेकार कर देता है।
  • Play Console का Sensitive Permissions डिक्लेरेशन फ़्लो — व्यापक मीडिया एक्सेस रिव्यू के दौरान ज़्यादा जाँच खींचता है।
  • एक स्थायी एक्सेस जिसे आपको अपनी प्राइवेसी पॉलिसी में सही ठहराना पड़ता है, भले ही आपका ऐप कभी सिर्फ़ यूज़र द्वारा चुनी गई एक ही फ़ोटो को छूता हो।

इनमें से कुछ भी यूज़र को कुछ नहीं देता। फ़ीचर को सिर्फ़ एक इमेज तक, एक बार, रीड एक्सेस चाहिए।

Photo Picker असल में कैसे काम करता है

Photo Picker एक सिस्टम-स्वामित्व वाला UI है — यह एक अलग, भरोसेमंद प्रोसेस में चलता है, और यूज़र जिस इमेज पर टैप करता है वह आपके ऐप को content:// URI के रूप में सौंपी जाती है। आपके ऐप को कभी गैलरी के लिए परमिशन ग्रांट नहीं मिलता; उसे बस वह एक URI मिलता है, सिर्फ़ उस फ़ोटो तक सीमित, जब तक आपको ज़रूरत हो तब तक के लिए।

सिंगल सिलेक्शन के लिए सेटअप में किसी मैनिफ़ेस्ट परमिशन की ज़रूरत नहीं:

class AddExpenseFragment : Fragment() {

    private val pickImage = registerForActivityResult(
        ActivityResultContracts.PickVisualMedia(),
    ) { uri: Uri? ->
        if (uri != null) attachReceiptPhoto(uri)
    }

    private fun launchPicker() {
        pickImage.launch(
            PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly),
        )
    }
}

ActivityResultContracts.PickVisualMedia.ImageOnly पिकर को इमेज तक सीमित करता है; VideoOnly और SingleMimeType("image/png") बाक़ी आम मामलों को कवर करते हैं। कोई <uses-permission> एंट्री नहीं, कोई रनटाइम प्रॉम्प्ट नहीं, कोई रेशनल डायलॉग नहीं जिसकी कॉपी लिखनी पड़े।

एक से ज़्यादा फ़ोटो चुनना

एक रसीद आमतौर पर एक ही फ़ोटो होती है, लेकिन उसी contract में मल्टी-सिलेक्ट वैरिएंट भी है, जिसकी सीमा आप ख़ुद तय करते हैं:

private val pickImages = registerForActivityResult(
    ActivityResultContracts.PickMultipleVisualMedia(maxItems = 5),
) { uris: List<Uri> ->
    uris.forEach { attachReceiptPhoto(it) }
}

सिस्टम UI ख़ुद ही यह सीमा लागू करता है — यूज़र के लिमिट तक पहुँचते ही यह आगे टैप करना डिसेबल कर देता है, आपके कोड को बाद में लंबी लिस्ट को रिजेक्ट करने की ज़रूरत नहीं पड़ती।

जो URI आपको मिलता है वह स्थायी नहीं है

यही वह डिटेल है जहाँ लोग अटक जाते हैं: आपके ActivityResultCallback को सौंपा गया content:// URI सिर्फ़ उस कॉल की अवधि तक पढ़ने योग्य होने की गारंटी रखता है। अगर आप URI स्ट्रिंग को अपने डेटाबेस में सेव करके दिनों बाद खोलने की कोशिश करते हैं, तो SecurityException आ सकता है — यह ग्रांट, परसिस्टेड Storage Access Framework URI की तरह, पिकर सेशन से आगे नहीं टिकता।

समाधान है ज़रूरी बाइट्स को तुरंत, अपने ऐप की मालिकाना फ़ाइल में कॉपी कर लेना:

private fun attachReceiptPhoto(sourceUri: Uri) {
    val destFile = File(requireContext().filesDir, "receipts/${UUID.randomUUID()}.jpg")
    destFile.parentFile?.mkdirs()

    requireContext().contentResolver.openInputStream(sourceUri)?.use { input ->
        destFile.outputStream().use { output -> input.copyTo(output) }
    }

    viewModel.setReceiptPath(destFile.absolutePath)
}

कॉपी को मेन थ्रेड से बाहर करें — यह फ़ाइल I/O है — लेकिन इसे पिकर की Activity रिज़ल्ट कॉलबैक के लौटने से पहले करें, किसी बाद की स्क्रीन पर नहीं। बाइट्स कॉपी होने के बाद, अस्थायी content:// ग्रांट अब मायने नहीं रखता; आपके ऐप के पास अब अपनी एक प्राइवेट फ़ाइल है जिसे वह जब तक चाहे पढ़ सकता है।

Android 13 से पहले के लिए बैकपोर्ट

Photo Picker सिस्टम फ़ीचर के तौर पर Android 13 में आया, लेकिन Google Play services के ज़रिए API 21+ तक बैकपोर्ट किया गया है — वही ActivityResultContracts.PickVisualMedia कॉल पुराने डिवाइसों पर, जहाँ Play services इंस्टॉल है, अपने आप इस बैकपोर्ट पर रिज़ॉल्व हो जाता है। अगर आपका न्यूनतम फ़ीचर सेट सच में नेटिव पिकर के व्यवहार की माँग करता है (जैसे प्रति-ऐप क्लाउड मीडिया प्रोवाइडर इंटीग्रेशन), तो एक चीज़ स्पष्ट रूप से जाँचने लायक है:

val isPhotoPickerAvailable = ActivityResultContracts.PickVisualMedia
    .isPhotoPickerAvailable(requireContext())

व्यवहार में, एक साधारण “फ़ोटो अटैच करो” फ़्लो के लिए आपको इस पर ब्रांच करने की ज़रूरत नहीं — contract अपने आप ठीक से डिग्रेड हो जाता है। यह तब मायने रखता है जब आप ऐसे पिकर-स्पेसिफ़िक UI संकेत दिखाने का फ़ैसला कर रहे हों जो सिर्फ़ नेटिव वर्ज़न पर ही मायने रखते हैं।

असल में क्या मायने रखता था

यहाँ जो हासिल हुआ वह कोई नया जेस्चर या बेहतर दिखने वाला पिकर UI नहीं है — यह है कि परमिशन मॉडल आख़िरकार वही दर्शाता है जो फ़ीचर असल में करता है। एक रसीद की फ़ोटो अटैच करने वाले यूज़र को कभी अपने पूरे कैमरा रोल तक स्थायी एक्सेस देने की ज़रूरत नहीं होनी चाहिए थी, और जब तक Photo Picker मौजूद नहीं था, Android के पास इसे टालने का कोई साफ़ तरीक़ा नहीं था। READ_MEDIA_IMAGES को PickVisualMedia से बदलने से एक मैनिफ़ेस्ट परमिशन, एक रनटाइम प्रॉम्प्ट, और मेरी प्राइवेसी पॉलिसी का एक पैराग्राफ़ हट गया — और फ़ीचर यूज़र की तरफ़ से बिल्कुल वैसे ही काम करता है, बस ज़रूरत से ज़्यादा माँगे बिना।

// संबंधित पठन

जर्नल से और भी

MFKAPPS 6 मिनट पढ़ना

2026 में Android पर बायोमेट्रिक ऑथेंटिकेशन: BiometricPrompt, Keystore, और एक लोकल-फर्स्ट ऐप को लॉक करना

androidx.biometric की व्यावहारिक गाइड — BiometricPrompt, CryptoObject-समर्थित keys, डिवाइस credential fallback, और वे गलतियाँ जिनकी वजह से एक फिंगरप्रिंट चेक असल में कुछ भी सुरक्षित नहीं करता।

#android #engineering #kotlin
MFKAPPS 5 मिनट पढ़ना

2026 में Android पर Health Connect: लोकल-फर्स्ट को छोड़े बिना डेटा पढ़ना और लिखना

2026 में Android के Health Connect API की एक व्यावहारिक गाइड — परमिशन, बैकग्राउंड रीड्स, और लोकल-फर्स्ट ऐप्स को इसे सच नहीं, बल्कि वैकल्पिक क्यों मानना चाहिए।

#android #engineering #kotlin
MFKAPPS 5 मिनट पढ़ना

2026 में androidx.startup: ContentProvider के ढेर के बिना लाइब्रेरी इनिशियलाइज़ेशन का क्रम तय करना

Android की App Startup लाइब्रेरी के लिए एक व्यावहारिक गाइड — initializer को एक ContentProvider में समेटना, dependencies घोषित करना, और वो lazy-init मामले जिनकी जगह यह नहीं ले सकती।

#android #engineering #kotlin