إنشاء تطبيقات المراسلة لتطبيق Android Auto

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

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

يوضح لك هذا الدليل كيفية توسيع تطبيق يعرض الرسائل للمستخدم ويتلقى ردود المستخدم، مثل تطبيق دردشة، لعرض الرسائل وإيصال الرد على جهاز 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/رسائل الوسائط المتعددة الواردة عند ضبط تطبيقك كمعالج تلقائي للرسائل القصيرة، ما قد يؤدي إلى تلقّي إشعارات مكرَّرة.

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

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

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

  1. في ملف build.gradle ذي المستوى الأعلى، عليك تضمين اعتمادية على مستودع Maven من Google، كما هو موضّح في المثال التالي:

رائع

allprojects {
    repositories {
        google()
    }
}

Kotlin

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

رائع

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

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

Kotlin

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

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

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

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

تحديد الإجراءات المستهدفة

إجراءات 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() معرّفات فريدة لكل محادثة، ولكن يجب أن تعرض المكالمات المتكررة للمحادثة نفسها المعرّف الفريد الذي تم إنشاؤه مسبقًا.

فكر في مثال مع محادثتين، "أ" و"ب": معرف رد المحادثة "أ" هو 100، ومعرف العلامة كمقروءة هو 101. معرف الرد الخاص بالمحادثة "ب" هو 102 ومعرف العلامة كمقروءة هو 103. إذا تم تحديث المحادثة "أ"، فلا يزال معرفا الرد وعلامة كـ "مقروءة" 100 و101. ولمزيد من المعلومات، يمكنك الاطّلاع على PendingIntent.FLAG_UPDATE_CURRENT.

إنشاء نمط رسالة

إنّ 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. احرص على ملء جميع المعلومات المطلوبة في نموذج المشكلة.

إنشاء عدد جديد

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