android_sms: تطبيق SMS افتراضي حقيقي على Android بـ Flutter - DEV Community

DEV Community

Cover image for android_sms: تطبيق SMS افتراضي حقيقي على Android بـ Flutter
Abdeldjalil Chougui
Abdeldjalil Chougui

Posted on Originally published at abdeldjalilchougui.biz

android_sms: تطبيق SMS افتراضي حقيقي على Android بـ Flutter

أغلب إضافات SMS في Flutter تسمح لك بإرسال رسالة والاستماع للرسائل الواردة. هذا يكفي لشاشة رمز التحقق (OTP)، لكنه لا يكفي لبناء تطبيق رسائل. في Android، تطبيق واحد فقط في كل مرة يكون تطبيق الرسائل الافتراضي (default SMS app)، ولهذا التطبيق مهام لا يقوم بها غيره: هو الوحيد الذي يستقبل إشارة SMS_DELIVER، وإذا لم يكتب الرسالة الواردة في صندوق الوارد (Inbox) الخاص بالنظام، فلن تُحفظ في أي مكان.

android_sms إضافة (plugin) لـ Flutter مبنية حول هذه القاعدة. تغطي المسار كاملًا عبر الدور الرسمي لتطبيق الرسائل الافتراضي: طلب الدور من المستخدم، الإرسال مع تقارير الحالة، تخزين الرسائل الواردة، قراءة المحادثات، والتعامل مع الرسائل التي تصل والتطبيق مغلق.

ماذا تقدّم

الدور (role). الدالة isDefaultSmsApp() تخبرك إن كان تطبيقك يملك الدور، وrequestDefaultSmsApp() تفتح نافذة Android نفسها. تستعمل RoleManager على Android 10 وما بعده، والـ intent الأقدم ACTION_CHANGE_DEFAULT في الإصدارات السابقة. والـ stream المسمّى defaultSmsAppChanged يُطلق حدثًا كلما اكتسب التطبيق الدور أو فقده، حتى لو غيّره المستخدم من الإعدادات.

الإرسال. الدالة send(address, body) تُرجع معرّفًا للرسالة. النصوص الطويلة تُقسَّم بـ SmsManager.divideMessage، ويمكنك تمرير subscriptionId للإرسال من شريحة محددة. والـ stream sendStatus يعطي حدثًا واحدًا مجمّعًا لكل رسالة: sent عندما تُرسل كل الأجزاء، وfailed بمجرد فشل جزء واحد (مع رمز الخطأ من SmsManager)، وdelivered أو deliveryFailed عندما تُرسل الشبكة تقرير الاستلام. وبصفته التطبيق الافتراضي، يكتب الـ plugin الرسالة في صندوق المُرسَل، وينقلها إلى صندوق الفاشلة إن فشل الإرسال.

الاستقبال. بصفته التطبيق الافتراضي، تُحلَّل كل رسالة واردة، وتُجمع أجزاء الرسائل الطويلة، ثم تُحفظ في صندوق الوارد الخاص بالنظام. إن كان كود Dart يستمع، تصل الرسالة عبر incomingMessages. وإن لم يكن، يشغّل الـ plugin معالجًا في الخلفية (background handler) داخل محرك بلا واجهة، فتستطيع إظهار إشعار حتى والتطبيق مغلق.

صندوق الوارد. الدالة threads() تُرجع المحادثات مع آخر مقتطف، والتاريخ، وعدد الرسائل، وعدد غير المقروء، والمستلمين. وmessages(threadId) تتصفح المحادثة إلى الخلف. أما markRead وdelete وdeleteThread فتعمل فقط للتطبيق الافتراضي، وترمي NotDefaultSmsAppException في غير ذلك، لأن Android يتجاهل هذه الكتابات بصمت إن جاءت من تطبيق آخر.

أرقام الهاتف التي تخص نفس الشخص. الدالة PhoneNormalizer.threadingKey تجعل 0661 23 45 67 و00213661234567 و+213661234567 مفتاحًا واحدًا، مع منطقة افتراضية قابلة للضبط (الجزائر مدمجة)، والأرقام القصيرة، والمرسلين بأسماء حرفية، والأرقام الهندية-العربية. هو مفتاح للتجميع وليس أداة تحقق: لا يفحص طول الرقم ولا نطاقاته.

طريقة الاستعمال

أضِفه من GitHub:

dependencies:
  android_sms:
    git: https://github.com/AbdeldjalilChougui/android_sms
Enter fullscreen mode Exit fullscreen mode

يحتاج minSdk 21، وهو لـ Android فقط. نظام iOS لا يسمح للتطبيقات الخارجية بإرسال الرسائل أو استقبالها أو قراءتها أصلًا.

لا يُظهر Android التطبيق في قائمة "تطبيق الرسائل الافتراضي" إلا إذا صرّح بأربعة مكوّنات: activity للكتابة (SENDTO لـ sms:/smsto:)، وreceiver لـ SMS_DELIVER، وreceiver لـ WAP_PUSH_DELIVER خاص بالـ MMS، وservice للرد برسالة. الـ plugin يوفّر هذه الأصناف (classes)، وتطبيقك يصرّح بها في الـ manifest، حتى لا تحصل التطبيقات التي تقرأ الرسائل فقط على receivers لا تريدها. الـ README يحتوي الـ manifest كاملًا، وتطبيق المثال يستعمله كما هو.

ثم في Dart:

import 'package:android_sms/android_sms.dart';

@pragma('vm:entry-point')
Future<void> onBackgroundSms(IncomingSms sms) async {
  // Runs in a headless engine. The message is already in the Inbox.
}

Future<void> setUpSms() async {
  if (!await AndroidSms.isDefaultSmsApp()) {
    final granted = await AndroidSms.requestDefaultSmsApp();
    if (!granted) return;
  }
  await AndroidSms.setBackgroundMessageHandler(onBackgroundSms);

  AndroidSms.incomingMessages.listen((sms) {
    print('${sms.address}: ${sms.body}');
  });

  AndroidSms.sendStatus.listen((status) {
    if (status.isFailure) print('Failed ${status.messageId}: ${status.errorCode}');
  });

  final id = await AndroidSms.send('+213661234567', 'Hello!', requestDelivery: true);
  print('queued $id');

  for (final t in await AndroidSms.threads(limit: 50)) {
    if (t.hasUnread) await AndroidSms.markRead(t.id);
  }
}
Enter fullscreen mode Exit fullscreen mode

عندما يمنح المستخدم الدور، يمنح Android معه صلاحيات الرسائل. الـ plugin لا يطلب الصلاحيات من تلقاء نفسه، والدالة permissionStatus() تخبرك بما لديك.

الأخطاء لها أنواع محددة (typed errors)، فتتعامل مع كل حالة على حدة:

try {
  await AndroidSms.delete(messageId);
} on NotDefaultSmsAppException {
  // Only the default SMS app may change the Inbox.
} on SmsPermissionException catch (e) {
  print('Missing ${e.permission}');
}
Enter fullscreen mode Exit fullscreen mode

ما يجب معرفته قبل النشر

  • سياسة Google Play. يقصر Play صلاحيات الرسائل على التطبيق الذي اختاره المستخدم كتطبيق رسائل افتراضي، وعلى قائمة قصيرة من الاستثناءات المعتمدة. لهذا لا يضيف الـ plugin أيًّا من هذه الصلاحيات بنفسه: أنت تصرّح فقط بما تسمح به حالة استعمالك.
  • محتوى MMS خارج النطاق. يقرأ الـ plugin ويحفظ فقط ترويسة إشعار الـ MMS (المرسل، الموضوع، الحجم)، ولا يحمّل الصور ولا رسائل المجموعات.
  • حالة الإرسال محفوظة في الذاكرة. إن توقفت العملية في منتصف رسالة متعددة الأجزاء، قد تضيع حالة واحدة، لكن سطر الرسالة في قاعدة النظام يُحدَّث مع كل نتيجة نهائية.
  • ما تم اختباره. منطق Kotlin له اختبارات وحدة (unit tests)، وتطبيق المثال له اختبار تكامل على المحاكي يأخذ الدور، ويحقن رسائل (منها رسائل عربية متعددة الأجزاء)، ويتحقق من الـ streams وصفوف صندوق الوارد والمحادثات وعلامات القراءة والحذف والإرسال. الاختبار تم على محاكي Android (صورة Pixel 10، API 37)، وليس على هاتف حقيقي، ولم تُختبر تقارير الاستلام من شبكة حقيقية.

لماذا بنيته

الخيارات الموجودة في Flutter (telephony ونسخه المشتقة) ترسل وتستمع، لكن ليس فيها مسار طلب الدور، ولا تحفظ الرسائل الواردة. لا مشكلة في ذلك حتى يصبح تطبيقك هو التطبيق الافتراضي: من تلك اللحظة، كل رسالة لا يكتبها في صندوق الوارد تضيع نهائيًا. بناء تطبيق رسائل حقيقي يحتاج الدور، والمكوّنات الأربعة في الـ manifest، والكتابة في صندوق الوارد معًا، فجمعتها في plugin واحد، مع أخطاء محددة الأنواع، وجدول انتقال من telephony في الـ README.

الكود

الكود، والـ README مع الـ manifest كاملًا، وتطبيق المثال، والاختبارات موجودة على GitHub: https://github.com/AbdeldjalilChougui/android_sms. الرخصة MIT، والـ issues والـ pull requests مرحّب بها.


نُشر في الأصل على abdeldjalilchougui.biz.

Top comments (0)