Flutter में Android 17 का Contact Picker: READ_CONTACTS बदलने का समय

Flutter में Android 17 का Contact Picker: READ_CONTACTS बदलने का समय

विषय-सूची

Android 17 डिवाइस के कॉन्टैक्ट पढ़ने के तरीके में एक बड़ा बदलाव लेकर आया है: अब मैनिफ़ेस्ट में READ_CONTACTS की रीड परमिशन घोषित किए बिना ही सिस्टम पिकर को खोलकर कॉन्टैक्ट चुने जा सकते हैं और उन्हें अपने ऐप में लाया जा सकता है।

इसी वजह से Google ने नई पाबंदियाँ घोषित की हैं: READ_CONTACTS परमिशन अब सिर्फ़ उन ऐप्स के लिए रह गई है जो अकेले सिस्टम पिकर के भरोसे काम नहीं कर सकतीं। यानी अगर आपका ऐप कॉन्टैक्ट को सीधे-सादे तरीके से इम्पोर्ट करता है, तो आपको पिकर इस्तेमाल करना ही पड़ेगा। और अगर ऐप के चलने के लिए यह परमिशन सचमुच ज़रूरी है, तो आपको Play Console में एक घोषणा भेजनी होगी जिसमें बताना होगा कि पिकर आपके लिए क्यों काफ़ी नहीं है। यह नीति 27 जनवरी 2027 से लागू होगी।

हाँ, यह उन्हीं ऐप्स पर लागू होता है जो targetSdk 37 या उससे ऊपर को टारगेट करते हैं। अगर आपका ऐप अब भी इससे पुराने वर्ज़न पर है, तो फ़िलहाल यह नीति आप पर असर नहीं डालती — पर जैसे ही आप टारगेट बढ़ाएँगे, यह आपको पकड़ लेगी।

इस लेख में मैं आपको एक गाइड देने की कोशिश करूँगा कि इसका मतलब क्या है, यह आप पर कैसे असर डालेगा, और क्या आपको वाक़ई परमिशन इस्तेमाल करते रहना है या सिस्टम पिकर से ही काम चल जाएगा। उसके बाद कुछ कोड उदाहरण दूँगा कि मैंने खुद Android 17+ पर सिस्टम पिकर अपने ऐप्स में कैसे लागू किया।

Info

📽 वीडियो संस्करण YouTube पर उपलब्ध है

क्या Google Play की नई कॉन्टैक्ट नीति आपके ऐप पर लागू होती है?

कॉन्टैक्ट इस्तेमाल करने वाले ज़्यादातर ऐप्स READ_CONTACTS परमिशन घोषित किए बिना, सिस्टम पिकर के साथ बिलकुल ठीक चलते रहेंगे।

अगर आपका ऐप कॉन्टैक्ट को कभी-कभार ही छूता है — मसलन उन्हें ऐप में इम्पोर्ट करने के लिए, या किसी ख़ास पल में किसी एक कॉन्टैक्ट की कोई एक जानकारी लेने के लिए — तो सिस्टम पिकर ज़रूरत से ज़्यादा है।

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

उदाहरण के तौर पर मेरा ऐप उधार ट्रैकर लीजिए। एक ऐसा ऐप जहाँ आप कॉन्टैक्ट बनाते हैं और उनसे उधार और भुगतान जोड़ते हैं। कॉन्टैक्ट ऐप के भीतर भी बनाए जा सकते हैं, या सहूलियत के लिए डिवाइस की फ़ोनबुक से इम्पोर्ट किए जा सकते हैं। मेरा यूज़ केस बिलकुल यही है — एक बार चलने वाला काम: «डिवाइस की फ़ोनबुक से कॉन्टैक्ट ऐप के अंदर ले आना»।

मेरे लिए सिस्टम पिकर बिलकुल सही बैठता है। मुझे फ़ोनबुक तक लगातार, अनंत पहुँच नहीं चाहिए; मुझे बस एक तय पल पर एक बार की पहुँच चाहिए ताकि कॉन्टैक्ट लाकर ऐप के अंदर सहेज सकूँ।

⬆️ इस वीडियो में जो पिकर दिख रहा है, वही Android 17 का नया नेटिव पिकर है।

जिन मामलों में परमिशन अब भी जायज़ है, वे ऐसे हैं जिनमें एक बार की चुनाई नहीं, बल्कि पूरी फ़ोनबुक बार-बार चाहिए होती है: फ़ोन या मैसेजिंग ऐप, कॉलर आईडी, या कॉन्टैक्ट का बैकअप और सिंक। अगर आपका ऐप इस तरह का नहीं है, तो बहुत संभव है कि पिकर से काम बन जाए।

Flutter में Android 17 का Contact Picker लागू करना

उदाहरण के लिए एक सीधा-सादा टेस्ट ऐप लेते हैं, जो बस फ़ोनबुक के कॉन्टैक्ट की सूची दिखाता है:

उदाहरण ऐप में कॉन्टैक्ट की सूची

अगर हमें कॉन्टैक्ट «पुराने तरीक़े» से लेने हैं, तो हम flutter_contacts जैसा प्लगइन इस्तेमाल कर सकते हैं:

final deviceContacts = await FlutterContacts.getAll(
  properties: {ContactProperty.photoThumbnail},
);

यह कोड हमें नाम और तस्वीर समेत कॉन्टैक्ट की सूची देगा। हाँ, पहले यूज़र से परमिशन माँगनी होगी, वरना यह कॉल कुछ भी नहीं लौटाएगा:

final status = await FlutterContacts.permissions.request(PermissionType.read);

अब Android 17+ के लिए ज़रूरी बदलाव करते हैं। सबसे पहले, READ_CONTACTS परमिशन को सिर्फ़ Android 16 या उससे नीचे के डिवाइस के लिए घोषित करते हैं:

<uses-permission
  android:name="android.permission.READ_CONTACTS"
  android:maxSdkVersion="36" />

हमें Android 17 के SDK के साथ कंपाइल भी करना होगा, क्योंकि ContactsPickerSessionContract क्लास पुराने वर्ज़न में है ही नहीं। Flutter अब भी डिफ़ॉल्ट रूप से 36 इस्तेमाल करता है, इसलिए इसे ऐप की build.gradle.kts में हाथ से सेट करना पड़ेगा:

android {
    compileSdk = 37

    defaultConfig {
        targetSdk = 37
    }
}

Kotlin से ACTION_PICK_CONTACTS के ज़रिए पिकर खोलना

आगे, इसके लिए कोई प्लगइन भी लिया जा सकता था, पर चूँकि काम काफ़ी आसान है, मैं एक और प्लगइन पर निर्भर होने से बचने के लिए नेटिव इम्प्लीमेंटेशन पसंद करता हूँ। नेटिव हिस्सा और Dart वाला हिस्सा आपस में method channel से बात करते हैं, और सिस्टम पिकर हम Kotlin से ही खोलते हैं, एक से ज़्यादा चुनने के लिए इस तरह सेट करके:

val intent = Intent(ContactsPickerSessionContract.ACTION_PICK_CONTACTS).apply {
    putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
    putExtra(
        ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_SELECTION_LIMIT,
        call.arguments as Int,
    )
    // Read back only the fields below instead of every data field.
    putExtra(
        ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_MATCH_ALL_DATA_FIELDS,
        false,
    )
    putStringArrayListExtra(
        ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
        arrayListOf(
            ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
            ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE,
        ),
    )
}
startActivityForResult(intent, PICK_CONTACTS_REQUEST_CODE)

वह call.arguments as Int चुनाव की सीमा है, जो हमें Dart से method channel के ज़रिए मिलती है।

यूज़र का काम पूरा होने पर पिकर हमें कॉन्टैक्ट सीधे नहीं देता, बल्कि चुनी हुई सूची की ओर इशारा करता एक session URI देता है। उसे ContentResolver से पढ़ना पड़ता है, और यहीं असली बात है: कर्सर हमें हर कॉन्टैक्ट के लिए एक नहीं, बल्कि माँगे गए हर फ़ील्ड के लिए एक पंक्ति देता है। यानी पंक्तियों को समूह में बाँटना पड़ेगा:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    val sessionUri = data?.data
    if (resultCode != Activity.RESULT_OK || sessionUri == null) {
        // The user left the picker without confirming a selection.
        return
    }

    val projection = arrayOf(
        ContactsContract.Contacts.LOOKUP_KEY,
        ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
        ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Photo.PHOTO,
    )

    contentResolver.query(sessionUri, projection, null, null, null)?.use { cursor ->
        while (cursor.moveToNext()) {
            // Group the rows by LOOKUP_KEY, and keep the blob of the row whose
            // MIMETYPE is Photo.CONTENT_ITEM_TYPE as the contact photo.
        }
    }
}

ध्यान रखें कि यह क्वेरी I/O है, इसलिए Dart को जवाब देने से पहले इसे मेन थ्रेड से हटा देना बेहतर है।

इस कोड से हमें नाम और फ़ोटो समेत कॉन्टैक्ट मिल जाते हैं; ऊपर वाले उदाहरण के लिए इससे ज़्यादा कुछ नहीं चाहिए।

चुने हुए कॉन्टैक्ट को Dart में पढ़ना

इसके बाद अपने Dart कोड में एक मेथड बनाते हैं जो बताएगा कि हमें सिस्टम पिकर इस्तेमाल करना है या पुराना प्लगइन। इसके लिए हमें मौजूदा Android वर्ज़न जानना होगा, और यह device_info_plus प्लगइन से हो जाता है:

static const _systemPickerMinAndroidSdk = 37;

Future<bool> _isSystemPickerAvailable() async {
  if (!Platform.isAndroid) return false;

  final androidInfo = await DeviceInfoPlugin().androidInfo;
  return androidInfo.version.sdkInt >= _systemPickerMinAndroidSdk;
}

इसके साथ अब हम सही तरीक़े से कॉन्टैक्ट ले सकते हैं:

Future<List<AppContact>> loadContacts() async {
  if (await _isSystemPickerAvailable()) {
    return await _pickContactsWithSystemPicker();
  }

  return await _readContactsWithPermission();
}

Android 17+ पर हम कॉन्टैक्ट नेटिव साइड से लेते हैं और उन्हें पार्स करते हैं:

/// Opens the system Contact Picker. Returns an empty list if the user leaves
/// without selecting anything.
Future<List<AppContact>> _pickContactsWithSystemPicker() async {
  final response = await _channel.invokeMapMethod<String, Object?>(
    _methodPickContacts,
    _maxSelectedContacts,
  );

  if (response == null || response['wasCancelled'] == true) {
    return const [];
  }

  final contacts = <AppContact>[];
  for (final rawContact in response['contacts'] as List<Object?>) {
    final contact = rawContact as Map<Object?, Object?>;
    final name = contact['name'] as String;
    if (name.isEmpty) continue;

    contacts.add(
      AppContact(
        id: contact['id'] as String,
        name: name,
        photo: contact['photo'] as Uint8List?,
      ),
    );
  }

  return contacts;
}

आख़िरी एक बात ध्यान में रखिए: पिकर यूज़र खोलता है, ऐप नहीं। आप इसे बैकग्राउंड में कॉन्टैक्ट ताज़ा करने के लिए ख़ुद से नहीं खोल सकते, इसलिए अगर चुनी हुई सूची बाद में चाहिए हो, तो उसे सहेज लीजिए।

यहाँ मैंने सिर्फ़ अहम हिस्से दिखाए हैं, पर पूरे काम को बेहतर समझने के लिए आप पूरा प्रोजेक्ट यहाँ देख सकते हैं।

निष्कर्ष

इस लेख में हमने कॉन्टैक्ट पढ़ने की परमिशन को लेकर Google Play की नई नीति पर बहुत तेज़ी से नज़र डाली, और मैंने आपको वह आसान तरीक़ा दिखाया जो मैं ख़ुद अपने ऐप्स में कॉन्टैक्ट के साथ काम करने के लिए अपनाता हूँ।

अगर आपको लगता है कि कॉन्टैक्ट परमिशन आपके लिए बेहद ज़रूरी है, तो याद रखिए कि नीति के 27 जनवरी 2027 को लागू होने से पहले आपको Google Play में उसके लिए बना फ़ॉर्म भरना होगा।

अगली बार मिलते हैं!

साझा करें :