توسيع نطاق إشعارات المراسلة لتشمل Android Auto

يمكن للتطبيقات التي تتيح المراسلة توسيع نطاق إشعارات المراسلة الخاصة بها للسماح لتطبيق Android Auto باستخدامها عند تشغيله. تظهر هذه الإشعارات في Auto وتتيح للمستخدمين قراءة الرسائل والردّ عليها في واجهة متسقة لا تشتّت الانتباه. وعند استخدام MessagingStyle API، ستحصل على إشعارات رسائل محسّنة لجميع أجهزة Android، بما في ذلك Android Auto. تشمل التحسينات واجهة مستخدم متخصّصة في إشعارات الرسائل، وصورًا متحركة محسّنة، وإمكانية عرض الصور المضمّنة.

يوضّح لك هذا الدليل كيفية توسيع نطاق تطبيق يعرض رسائل للمستخدم ويتلقّى ردوده، مثل تطبيق دردشة، ليتمكّن من نقل عملية عرض الرسائل وتلقّي الردود إلى جهاز Auto. من خلال عملية الدمج هذه، يمكن للمستخدمين الاطّلاع على سجل الرسائل من الإشعارات التي تلقّوها خلال جلسة Android Auto النشطة فقط. لعرض الرسائل الواردة قبل بدء جلسة Android Auto النشطة، يمكنك إنشاء تجربة مراسلة مستندة إلى نماذج.

للحصول على إرشادات تصميم ذات صلة، يُرجى الاطّلاع على تطبيقات المراسلة على موقع "التصميم أثناء القيادة".

البدء

لتوفير خدمة المراسلة لأجهزة Auto، يجب أن يوضّح تطبيقك في ملف البيان أنّه متوافق مع Android Auto، وأن يكون قادرًا على تنفيذ ما يلي:

  • إنشاء وإرسال عناصر NotificationCompat.MessagingStyle تحتوي على عناصر الردّ وعناصر وضع علامة "مقروء" Action
  • التعامل مع الردود ووضع علامة "مقروءة" على محادثة باستخدام Service

المفاهيم والأشياء

قبل البدء في تصميم تطبيقك، من المفيد فهم طريقة تعامل Android Auto مع الرسائل.

يُطلق على كل جزء من التواصل اسم رسالة، ويتم تمثيله بواسطة الفئة MessagingStyle.Message. تحتوي الرسالة على المُرسِل ومحتوى الرسالة ووقت إرسالها.

يُطلق على التواصل بين المستخدمين اسم محادثة، ويتم تمثيله بواسطة الكائن MessagingStyle. تحتوي المحادثة، أو MessagingStyle، على عنوان والرسائل وما إذا كانت المحادثة بين مجموعة من المستخدمين.

لإعلام المستخدمين بالتحديثات التي تطرأ على محادثة، مثل رسالة جديدة، تنشر التطبيقات Notification في نظام Android. تستخدم Notification العنصر MessagingStyle لعرض واجهة مستخدم خاصة بالمراسلة في لوحة الإشعارات. تُرسِل منصة Android أيضًا Notification إلى Android Auto، ويتم استخراج MessagingStyle واستخدامه لنشر إشعار من خلال شاشة السيارة.

يتطلّب تطبيق Android Auto أيضًا أن تضيف التطبيقات عناصر Action إلى Notification ليتمكّن المستخدم من الردّ بسرعة على رسالة أو وضع علامة "مقروءة" عليها مباشرةً من لوحة الإشعارات.

باختصار، يتم تمثيل محادثة واحدة بواسطة عنصر Notification يتم تنسيقه باستخدام عنصر MessagingStyle. تحتوي السمة MessagingStyle على جميع الرسائل ضمن تلك المحادثة في عنصر واحد أو أكثر من عناصر MessagingStyle.Message. ولكي يكون التطبيق متوافقًا مع Android Auto، يجب أن يرفق كائنَي Action وNotification بالردّ ووضع علامة "مقروء" على الرسالة.

مسار المراسلة

يوضّح هذا القسم مسار مراسلة نموذجيًا بين تطبيقك وAndroid Auto.

  1. يتلقّى تطبيقك رسالة.
  2. ينشئ تطبيقك إشعارًا من MessagingStyle يتضمّن الردّ وAction كائنَي "وضع علامة مقروء".
  3. يتلقّى Android Auto حدث "إشعار جديد" من نظام التشغيل Android، ويعثر على MessagingStyle والرد Action ووضع علامة "مقروء" Action.
  4. ينشئ Android Auto إشعارًا ويعرضه في السيارة.
  5. إذا نقر المستخدم على الإشعار على شاشة السيارة، سيؤدي ذلك إلى تفعيل Android Auto لعملية وضع علامة "مقروء" Action.
    • في الخلفية، يجب أن يتعامل تطبيقك مع حدث "وضع علامة مقروء".
  6. إذا ردّ المستخدم على الإشعار باستخدام صوته، سيضع Android Auto نسخة مكتوبة من رد المستخدم في الرد Action ثم يرسله.
    • في الخلفية، يجب أن يعالج تطبيقك حدث الردّ هذا.

الافتراضات الأولية

لا ترشدك هذه الصفحة إلى كيفية إنشاء تطبيق مراسلة كامل، بل يتضمّن نموذج الرمز البرمجي التالي بعض العناصر التي يحتاجها تطبيقك قبل البدء في إتاحة المراسلة باستخدام Android Auto:

data class YourAppConversation(
        val id: Int,
        val title: String,
        val recipients: MutableList<YourAppUser>,
        val icon: Bitmap) {
    companion object {
        /** Fetches [YourAppConversation] by its [id]. */
        fun getById(id: Int): YourAppConversation = // ...
    }

    /** Replies to this conversation with the given [message]. */
    fun reply(message: String) {}

    /** Marks this conversation as read. */
    fun markAsRead() {}

    /** Retrieves all unread messages from this conversation. */
    fun getUnreadMessages(): List<YourAppMessage> { return /* ... */ }
}
data class YourAppUser(val id: Int, val name: String, val icon: Uri)
data class YourAppMessage(
    val id: Int,
    val sender: YourAppUser,
    val body: String,
    val timeReceived: Long)

تحديد إمكانية استخدام التطبيق مع Android Auto

عندما يتلقّى Android Auto إشعارًا من تطبيق مراسلة، يتحقّق من أنّ التطبيق قد أعلن عن توافقه مع Android Auto. لتفعيل هذه الميزة، أدرِج الإدخال التالي في بيان تطبيقك:

<application>
    ...
    <meta-data
        android:name="com.google.android.gms.car.application"
        android:resource="@xml/automotive_app_desc"/>
    ...
</application>

يشير إدخال البيان هذا إلى ملف XML آخر عليك إنشاؤه باستخدام المسار التالي: YourAppProject/app/src/main/res/xml/automotive_app_desc.xml. في automotive_app_desc.xml، حدِّد إمكانات Android Auto التي يتوافق معها تطبيقك. على سبيل المثال، للإشارة إلى أنّ تطبيقك يتيح تلقّي الإشعارات، أدرِج ما يلي:

<automotiveApp>
    <uses name="notification" />
</automotiveApp>

إذا كان يمكن ضبط تطبيقك كمعالج تلقائي للرسائل القصيرة، تأكَّد من تضمين عنصر <uses> التالي. وفي حال عدم توفيرها، سيتم استخدام معالج تلقائي مضمّن في Android Auto للتعامل مع رسائل SMS/MMS الواردة عندما يتم ضبط تطبيقك كمعالج تلقائي لرسائل SMS، ما قد يؤدي إلى ظهور إشعارات مكرّرة.

<automotiveApp>
    ...
    <uses name="sms" />
</automotiveApp>

استيراد مكتبة AndroidX الأساسية

يتطلّب إنشاء إشعارات لاستخدامها مع أجهزة Auto مكتبة AndroidX الأساسية. استورِد المكتبة إلى مشروعك على النحو التالي:

  1. في ملف build.gradle ذي المستوى الأعلى، أدرِج تبعية لمستودع Maven الخاص بـ Google، كما هو موضّح في المثال التالي:

Groovy

allprojects {
    repositories {
        google()
    }
}

Kotlin

allprojects {
    repositories {
        google()
    }
}
  1. في ملف build.gradle الخاص بوحدة تطبيقك، أدرِج تبعية مكتبة AndroidX Core، كما هو موضّح في المثال التالي:

Groovy

dependencies {
    // If your app is written in Java
    implementation 'androidx.core:core:1.17.0'

    // If your app is written in Kotlin
    implementation 'androidx.core:core-ktx:1.17.0'
}

Kotlin

dependencies {
    // If your app is written in Java
    implementation("androidx.core:core:1.17.0")

    // If your app is written in Kotlin
    implementation("androidx.core:core-ktx:1.17.0")
}

التعامل مع إجراءات المستخدم

يحتاج تطبيق المراسلة إلى طريقة للتعامل مع تعديل محادثة من خلال Action. بالنسبة إلى Android Auto، هناك نوعان من عناصر Action التي يحتاج تطبيقك إلى التعامل معها، وهما: الردّ ووضع علامة &quot;مقروء&quot;. ننصحك بالتعامل معها باستخدام IntentService، الذي يوفّر المرونة اللازمة للتعامل مع عمليات الاستدعاء التي قد تكون مكلفة في الخلفية، ما يؤدي إلى إتاحة سلسلة التعليمات الرئيسية في تطبيقك.

ما قد يؤدي إلى حظر تطبيقك من Android Auto.

تحديد إجراءات الأهداف

Intent هي سلاسل بسيطة تحدّد الغرض من Intent. بما أنّ خدمة واحدة يمكنها التعامل مع أنواع متعددة من الأهداف، من الأسهل تحديد سلاسل إجراءات متعددة بدلاً من تحديد مكونات IntentService متعددة.

يحتوي تطبيق المراسلة النموذجي في هذا الدليل على نوعَي الإجراءات المطلوبَين: الردّ ووضع علامة "مقروء"، كما هو موضّح في نموذج الرمز التالي.

private const val ACTION_REPLY = "com.example.REPLY"
private const val ACTION_MARK_AS_READ = "com.example.MARK_AS_READ"

إنشاء الخدمة

لإنشاء خدمة تتعامل مع عناصر Action هذه، تحتاج إلى معرّف المحادثة، وهو بنية بيانات عشوائية يحدّدها تطبيقك وتعرّف المحادثة. تحتاج أيضًا إلى مفتاح إدخال عن بُعد، وسنتناول تفاصيله لاحقًا في هذا القسم. ينشئ نموذج الرمز البرمجي التالي خدمة للتعامل مع الإجراءات المطلوبة:

private const val EXTRA_CONVERSATION_ID_KEY = "conversation_id"
private const val REMOTE_INPUT_RESULT_KEY = "reply_input"

/**
 * An [IntentService] that handles reply and mark-as-read actions for
 * [YourAppConversation]s.
 */
class MessagingService : IntentService("MessagingService") {
    override fun onHandleIntent(intent: Intent?) {
        // Fetches internal data.
        val conversationId = intent!!.getIntExtra(EXTRA_CONVERSATION_ID_KEY, -1)

        // Searches the database for that conversation.
        val conversation = YourAppConversation.getById(conversationId)

        // Handles the action that was requested in the intent. The TODOs
        // are addressed in a later section.
        when (intent.action) {
            ACTION_REPLY -> TODO()
            ACTION_MARK_AS_READ -> TODO()
        }
    }
}

لربط هذه الخدمة بتطبيقك، عليك أيضًا تسجيل الخدمة في بيان تطبيقك، كما هو موضّح في المثال التالي:

<application>
    <service android:name="com.example.MessagingService" />
    ...
</application>

إنشاء النوايا والتعامل معها

لا يمكن للتطبيقات الأخرى، بما في ذلك Android Auto، الحصول على Intent الذي يؤدي إلى تشغيل MessagingService، لأنّه يتم تمرير Intent إلى التطبيقات الأخرى من خلال PendingIntent. بسبب هذا القيد، عليك إنشاء عنصر RemoteInput للسماح للتطبيقات الأخرى بتقديم نص الرد إلى تطبيقك، كما هو موضّح في المثال التالي:

/**
 * Creates a [RemoteInput] that lets remote apps provide a response string
 * to the underlying [Intent] within a [PendingIntent].
 */
fun createReplyRemoteInput(context: Context): RemoteInput {
    // RemoteInput.Builder accepts a single parameter: the key to use to store
    // the response in.
    return RemoteInput.Builder(REMOTE_INPUT_RESULT_KEY).build()
    // Note that the RemoteInput has no knowledge of the conversation. This is
    // because the data for the RemoteInput is bound to the reply Intent using
    // static methods in the RemoteInput class.
}

/** Creates an [Intent] that handles replying to the given [appConversation]. */
fun createReplyIntent(
        context: Context, appConversation: YourAppConversation): Intent {
    // Creates the intent backed by the MessagingService.
    val intent = Intent(context, MessagingService::class.java)

    // Lets the MessagingService know this is a reply request.
    intent.action = ACTION_REPLY

    // Provides the ID of the conversation that the reply applies to.
    intent.putExtra(EXTRA_CONVERSATION_ID_KEY, appConversation.id)

    return intent
}

في عبارة التبديل ACTION_REPLY ضمن MessagingService، استخرِج المعلومات التي ستُدرَج في الردّ Intent، كما هو موضّح في المثال التالي:

ACTION_REPLY -> {
    // Extracts reply response from the intent using the same key that the
    // RemoteInput uses.
    val results: Bundle = RemoteInput.getResultsFromIntent(intent)
    val message = results.getString(REMOTE_INPUT_RESULT_KEY)

    // This conversation object comes from the MessagingService.
    conversation.reply(message)
}

يمكنك التعامل مع علامة "مقروءة" Intent بطريقة مماثلة. ومع ذلك، لا تتطلّب RemoteInput، كما هو موضّح في المثال التالي:

/** Creates an [Intent] that handles marking the [appConversation] as read. */
fun createMarkAsReadIntent(
        context: Context, appConversation: YourAppConversation): Intent {
    val intent = Intent(context, MessagingService::class.java)
    intent.action = ACTION_MARK_AS_READ
    intent.putExtra(EXTRA_CONVERSATION_ID_KEY, appConversation.id)
    return intent
}

لا يتطلّب شرط التبديل ACTION_MARK_AS_READ ضمن MessagingService أي منطق إضافي، كما هو موضّح في المثال التالي:

// Marking as read has no other logic.
ACTION_MARK_AS_READ -> conversation.markAsRead()

إشعار المستخدمين بالرسائل

بعد اكتمال عملية معالجة إجراءات المحادثة، تتمثل الخطوة التالية في إنشاء إشعارات متوافقة مع Android Auto.

إنشاء إجراءات

يمكن تمرير عناصر Action إلى تطبيقات أخرى باستخدام Notification لتشغيل طرق في التطبيق الأصلي. بهذه الطريقة، يمكن لـ Android Auto وضع علامة "مقروءة" على محادثة أو الرد عليها.

لإنشاء Action، ابدأ بـ Intent. يوضّح المثال التالي كيفية إنشاء Intent "رد":

fun createReplyAction(
        context: Context, appConversation: YourAppConversation): Action {
    val replyIntent: Intent = createReplyIntent(context, appConversation)
    // ...

بعد ذلك، ضع هذا Intent في PendingIntent، ما يجعله جاهزًا للاستخدام في التطبيقات الخارجية. يؤدي PendingIntent إلى حظر جميع أذونات الوصول إلى Intent المغلف من خلال عرض مجموعة محدّدة فقط من الطرق التي تتيح للتطبيق المستلِم تشغيل Intent أو الحصول على اسم حزمة التطبيق الأصلي. ولا يمكن للتطبيق الخارجي الوصول إلى Intent الأساسي أو البيانات الموجودة فيه.

    // ...
    val replyPendingIntent = PendingIntent.getService(
        context,
        createReplyId(appConversation), // Method explained later.
        replyIntent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
    // ...

قبل إعداد الرد Action، يُرجى العلم أنّ Android Auto يفرض ثلاثة شروط على الرد Action:

  • يجب ضبط الإجراء الدلالي على Action.SEMANTIC_ACTION_REPLY.
  • يجب أن يشير Action إلى أنّه لن يعرض أي واجهة مستخدم عند تشغيله.
  • يجب أن يحتوي Action على RemoteInput واحد.

يُعدّ نموذج الرمز التالي ردًا Action يستوفي المتطلبات المذكورة أعلاه:

    // ...
    val replyAction = Action.Builder(R.drawable.reply, "Reply", replyPendingIntent)
        // Provides context to what firing the Action does.
        .setSemanticAction(Action.SEMANTIC_ACTION_REPLY)

        // The action doesn't show any UI, as required by Android Auto.
        .setShowsUserInterface(false)

        // Don't forget the reply RemoteInput. Android Auto will use this to
        // make a system call that will add the response string into
        // the reply intent so it can be extracted by the messaging app.
        .addRemoteInput(createReplyRemoteInput(context))
        .build()

    return replyAction
}

تتشابه طريقة التعامل مع إجراء وضع علامة "مقروءة"، باستثناء عدم توفّر RemoteInput. لذلك، يتطلّب Android Auto استيفاء شرطَين لاستخدام ميزة "وضع علامة مقروءة" Action:

  • تم ضبط الإجراء الدلالي على Action.SEMANTIC_ACTION_MARK_AS_READ.
  • يشير الإجراء إلى أنّه لن يعرض أي واجهة مستخدم عند تنفيذه.

يُعدِّل نموذج الرمز التالي علامة Action "تمت القراءة" لتلبية هذه المتطلبات:

fun createMarkAsReadAction(
        context: Context, appConversation: YourAppConversation): Action {
    val markAsReadIntent = createMarkAsReadIntent(context, appConversation)
    val markAsReadPendingIntent = PendingIntent.getService(
            context,
            createMarkAsReadId(appConversation), // Method explained below.
            markAsReadIntent,
            PendingIntent.FLAG_UPDATE_CURRENT  or PendingIntent.FLAG_IMMUTABLE)
    val markAsReadAction = Action.Builder(
            R.drawable.mark_as_read, "Mark as Read", markAsReadPendingIntent)
        .setSemanticAction(Action.SEMANTIC_ACTION_MARK_AS_READ)
        .setShowsUserInterface(false)
        .build()
    return markAsReadAction
}

عند إنشاء النوايا المعلّقة، يتم استخدام طريقتَين: createReplyId() وcreateMarkAsReadId(). تعمل هذه الطرق كرموز طلب لكل PendingIntent، ويستخدمها نظام التشغيل Android للتحكّم في النوايا المعلقة الحالية. يجب أن تعرض طُرق create() معرّفات فريدة لكل محادثة، ولكن يجب أن تعرض الطلبات المتكرّرة للمحادثة نفسها المعرّف الفريد الذي تم إنشاؤه سابقًا.

لنفترض أنّ لدينا مثالاً يتضمّن محادثتَين، A وB: رقم تعريف الردّ في المحادثة A هو 100، ورقم تعريف وضع علامة "مقروءة" هو 101. رقم تعريف الردّ في المحادثة B هو 102، ورقم تعريف وضع علامة "مقروءة" هو 103. إذا تم تعديل المحادثة A، سيظلّ معرّفا الردّ والتمييز كرسالة مقروءة هما 100 و101. لمزيد من المعلومات، يُرجى الاطّلاع على PendingIntent.FLAG_UPDATE_CURRENT.

إنشاء MessagingStyle

MessagingStyle هو مشغّل شبكة الجوّال الذي ينقل معلومات المراسلة، وهو ما يستخدمه Android Auto لقراءة كل رسالة بصوت عالٍ في محادثة.

أولاً، يجب تحديد مستخدم الجهاز في شكل كائن Person، كما هو موضّح في المثال التالي:

fun createMessagingStyle(
        context: Context, appConversation: YourAppConversation): MessagingStyle {
    // Method defined by the messaging app.
    val appDeviceUser: YourAppUser = getAppDeviceUser()

    val devicePerson = Person.Builder()
        // The display name (also the name that's read aloud in Android auto).
        .setName(appDeviceUser.name)

        // The icon to show in the notification shade in the system UI (outside
        // of Android Auto).
        .setIcon(appDeviceUser.icon)

        // A unique key in case there are multiple people in this conversation with
        // the same name.
        .setKey(appDeviceUser.id)
        .build()
    // ...

يمكنك بعد ذلك إنشاء العنصر MessagingStyle وتقديم بعض التفاصيل حول المحادثة.

    // ...
    val messagingStyle = MessagingStyle(devicePerson)

    // Sets the conversation title. If the app's target version is lower
    // than P, this will automatically mark the conversation as a group (to
    // maintain backward compatibility). Use `setGroupConversation` after
    // setting the conversation title to explicitly override this behavior. See
    // the documentation for more information.
    messagingStyle.setConversationTitle(appConversation.title)

    // Group conversation means there is more than 1 recipient, so set it as such.
    messagingStyle.setGroupConversation(appConversation.recipients.size > 1)
    // ...

أخيرًا، أضِف الرسائل غير المقروءة.

    // ...
    for (appMessage in appConversation.getUnreadMessages()) {
        // The sender is also represented using a Person object.
        val senderPerson = Person.Builder()
            .setName(appMessage.sender.name)
            .setIcon(appMessage.sender.icon)
            .setKey(appMessage.sender.id)
            .build()

        // Adds the message. More complex messages, like images,
        // can be created and added by instantiating the MessagingStyle.Message
        // class directly. See documentation for details.
        messagingStyle.addMessage(
                appMessage.body, appMessage.timeReceived, senderPerson)
    }

    return messagingStyle
}

تجميع الإشعارات الفورية

بعد إنشاء العنصرَين Action وMessagingStyle، يمكنك إنشاء Notification ونشره.

fun notify(context: Context, appConversation: YourAppConversation) {
    // Creates the actions and MessagingStyle.
    val replyAction = createReplyAction(context, appConversation)
    val markAsReadAction = createMarkAsReadAction(context, appConversation)
    val messagingStyle = createMessagingStyle(context, appConversation)

    // Creates the notification.
    val notification = NotificationCompat.Builder(context, channel)
        // A required field for the Android UI.
        .setSmallIcon(R.drawable.notification_icon)

        // Shows in Android Auto as the conversation image.
        .setLargeIcon(appConversation.icon)

        // Adds MessagingStyle.
        .setStyle(messagingStyle)

        // Adds reply action.
        .addAction(replyAction)

        // Makes the mark-as-read action invisible, so it doesn't appear
        // in the Android UI but the app satisfies Android Auto's
        // mark-as-read Action requirement. Both required actions can be made
        // visible or invisible; it is a stylistic choice.
        .addInvisibleAction(markAsReadAction)

        .build()

    // Posts the notification for the user to see.
    val notificationManagerCompat = NotificationManagerCompat.from(context)
    notificationManagerCompat.notify(appConversation.id, notification)
}

مراجع إضافية

الإبلاغ عن مشكلة في المراسلة في Android Auto

إذا واجهت مشكلة أثناء تطوير تطبيق المراسلة لنظام Android Auto، يمكنك الإبلاغ عنها باستخدام Google Issue Tracker. احرص على ملء جميع المعلومات المطلوبة في نموذج المشكلة.

إنشاء مشكلة جديدة

قبل تسجيل مشكلة جديدة، تحقَّق مما إذا تم الإبلاغ عنها من قبل في قائمة المشاكل. يمكنك الاشتراك في المشاكل والتصويت عليها من خلال النقر على النجمة بجانب المشكلة في أداة التتبُّع. لمزيد من المعلومات، اطّلِع على مقالة الاشتراك في مشكلة.