نظرة عامة على مضيف USB

عندما يكون الجهاز الذي يعمل بنظام التشغيل Android في وضع مضيف USB، فإنّه يعمل كمضيف USB ويشغّل الناقل ويعدد أجهزة USB المتصلة. يتوافق وضع مضيف USB في الإصدار Android 3.1 والإصدارات الأحدث.

نظرة عامة على واجهة برمجة التطبيقات

قبل أن تبدأ، من المهم فهم الفصول التي تحتاج إلى العمل معها. يصف الجدول التالي واجهات برمجة تطبيقات مضيف USB في حزمة android.hardware.usb.

الجدول 1. واجهات برمجة تطبيقات مضيف USB

الفئة الوصف
UsbManager تتيح لك تعداد أجهزة USB المتصلة والاتصال بها.
UsbDevice يمثل جهاز USB متصلاً ويحتوي على طرق للوصول إلى معلومات التعريف والواجهات ونقاط النهاية.
UsbInterface تمثل هذه السمة واجهة لجهاز USB، وتحدّد مجموعة من الوظائف للجهاز. يمكن أن يحتوي الجهاز على واجهة واحدة أو أكثر للاتصال من خلالها.
UsbEndpoint يمثل نقطة نهاية للواجهة، وهي قناة اتصال لهذه الواجهة. ويمكن أن تحتوي الواجهة على نقطة نهاية واحدة أو أكثر، وعادةً ما تحتوي على نقاط نهاية للإدخال والإخراج للاتصال الثنائي بالجهاز.
UsbDeviceConnection يمثل الاتصال بالجهاز الذي ينقل البيانات على نقاط النهاية. تتيح لك هذه الفئة إرسال البيانات بشكل متزامن أو غير متزامن.
UsbRequest ويمثِّل طلبًا غير متزامن للاتصال بجهاز من خلال UsbDeviceConnection.
UsbConstants تحدّد هذه السياسة ثوابت USB التي تتوافق مع التعريفات في linux/usb/ch9.h الخاصة بنواة Linux.

في معظم الحالات، تحتاج إلى استخدام جميع هذه الفئات (تكون UsbRequest مطلوبة فقط في حال إجراء اتصال غير متزامن) عند الاتصال بجهاز USB. وبشكل عام، تحصل على UsbManager لاسترداد UsbDevice المطلوب. عندما يكون الجهاز بحوزتك، عليك العثور على UsbInterface وUsbEndpoint المناسبتَين لتلك الواجهة للتواصل من خلالهما. بعد الحصول على نقطة النهاية الصحيحة، افتح UsbDeviceConnection للاتصال بجهاز USB.

متطلبات بيان Android

توضح القائمة التالية ما تحتاج إلى إضافته إلى ملف بيان تطبيقك قبل التعامل مع واجهات برمجة تطبيقات مضيف USB:

  • بما أنّه لا يمكن ضمان توافق جميع الأجهزة التي تعمل بنظام التشغيل Android مع واجهات برمجة تطبيقات مضيف USB، يمكنك تضمين عنصر <uses-feature> يشير إلى أنّ تطبيقك يستخدم ميزة android.hardware.usb.host.
  • اضبط الحد الأدنى لحزمة تطوير البرامج (SDK) للتطبيق على مستوى واجهة برمجة التطبيقات 12 أو مستوى أعلى. ولا تتوفر واجهات برمجة التطبيقات لمضيف USB في مستويات واجهة برمجة التطبيقات السابقة.
  • إذا أردت أن يتلقّى تطبيقك جهاز USB موصولًا، حدِّد زوجًا من <intent-filter> و<meta-data> لعنصر android.hardware.usb.action.USB_DEVICE_ATTACHED في نشاطك الرئيسي. يشير العنصر <meta-data> إلى ملف XML خارجي يتضمّن معلومات تعريفية حول الجهاز الذي تريد رصده.

    في ملف موارد XML، حدِّد عناصر <usb-device> لأجهزة USB التي تريد فلترتها. تصف القائمة التالية سمات <usb-device>. وبشكل عام، استخدِم معرّف المورّد والمنتج إذا كنت تريد فلترة جهاز معيّن واستخدِم الفئة والفئة الفرعية والبروتوكول إذا أردت الفلترة للوصول إلى مجموعة من أجهزة USB، مثل أجهزة التخزين الكبيرة أو الكاميرات الرقمية. لا يمكنك تحديد أي من هذه السمات أو تحديد كل السمات. لا يتطابق تحديد أي سمات مع كل جهاز USB، لذا نفِّذ هذا الإجراء فقط إذا كان التطبيق يتطلب ذلك:

    • vendor-id
    • product-id
    • class
    • subclass
    • protocol (الجهاز أو الواجهة)

    احفظ ملف المورد في دليل res/xml/. يجب أن يكون اسم ملف المورد (بدون الامتداد .xml) مطابقًا للاسم الذي حدّدته في العنصر <meta-data>. يمكنك الاطّلاع على تنسيق ملف XML المتوفّر في المثال أدناه.

أمثلة على ملف البيان وملفات الموارد

يعرض المثال التالي نموذج البيان وملف المورد المقابل له:

<manifest ...>
    <uses-feature android:name="android.hardware.usb.host" />
    <uses-sdk android:minSdkVersion="12" />
    ...
    <application>
        <activity ...>
            ...
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>

            <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
        </activity>
    </application>
</manifest>

في هذه الحالة، يجب حفظ ملف المورد التالي في res/xml/device_filter.xml وتحديد أنّه يجب فلترة أي جهاز USB يتضمّن السمات المحدّدة:

<?xml version="1.0" encoding="utf-8"?>

<resources>
    <usb-device vendor-id="1234" product-id="5678" class="255" subclass="66" protocol="1" />
</resources>

العمل مع الأجهزة

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

  1. يمكنك استكشاف أجهزة USB المتصلة من خلال استخدام فلتر أهداف ليتم رصدها عندما يوصِّل المستخدم جهاز USB أو من خلال إحصاء أجهزة USB التي سبق أن تم توصيلها.
  2. اطلب من المستخدم الإذن للاتصال بجهاز USB، إذا لم يسبق لك الحصول عليه.
  3. يمكنك التواصل مع جهاز USB عن طريق قراءة البيانات وكتابتها على نقاط النهاية المناسبة للواجهة.

اكتشاف جهاز

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

استخدام فلتر أهداف

للسماح لتطبيقك باكتشاف جهاز USB معيّن، يمكنك تحديد فلتر أهداف من أجل الفلترة للوصول إلى الغرض android.hardware.usb.action.USB_DEVICE_ATTACHED. بالإضافة إلى فلتر الأهداف هذا، عليك تحديد ملف موارد يحدِّد سمات جهاز USB، مثل معرّف المنتج ومعرّف المورّد. عندما يوصل المستخدمون جهازًا يتطابق مع فلتر الجهاز، يعرض النظام لهم مربّع حوار يسألهم ما إذا كانوا يريدون بدء تشغيل التطبيق. في حال قبول المستخدمين، سيحصل تطبيقك تلقائيًا على إذن بالوصول إلى الجهاز إلى أن يتم فصل الجهاز.

يوضّح المثال التالي كيفية الإعلان عن فلتر الأهداف:

<activity ...>
...
    <intent-filter>
        <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
    </intent-filter>

    <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
        android:resource="@xml/device_filter" />
</activity>

يوضّح المثال التالي كيفية الإعلان عن ملف المورد المقابل الذي يحدّد أجهزة USB التي تهمّك:

<?xml version="1.0" encoding="utf-8"?>

<resources>
    <usb-device vendor-id="1234" product-id="5678" />
</resources>

في نشاطك، يمكنك الحصول على UsbDevice الذي يمثّل الجهاز المتصل من الغرض على النحو التالي:

Kotlin

val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)

Java

UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

تعداد الأجهزة

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

Kotlin

val manager = getSystemService(Context.USB_SERVICE) as UsbManager
...
val deviceList = manager.getDeviceList()
val device = deviceList.get("deviceName")

Java

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
...
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
UsbDevice device = deviceList.get("deviceName");

وإذا أردت، يمكنك أيضًا الحصول على مُكرّر من خريطة التجزئة ومعالجة كل جهاز واحدًا تلو الآخر:

Kotlin

val manager = getSystemService(Context.USB_SERVICE) as UsbManager
..
val deviceList: HashMap<String, UsbDevice> = manager.deviceList
deviceList.values.forEach { device ->
    // your code
}

Java

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
...
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();
while(deviceIterator.hasNext()){
    UsbDevice device = deviceIterator.next();
    // your code
}

الحصول على إذن للاتصال بأحد الأجهزة

قبل الاتصال بجهاز USB، يجب أن يحصل التطبيق على إذن من المستخدمين.

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

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

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

Kotlin

private const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"

private val usbReceiver = object : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        if (ACTION_USB_PERMISSION == intent.action) {
            synchronized(this) {
                val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)

                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    device?.apply {
                        // call method to set up device communication
                    }
                } else {
                    Log.d(TAG, "permission denied for device $device")
                }
            }
        }
    }
}

Java

private static final String ACTION_USB_PERMISSION =
    "com.android.example.USB_PERMISSION";
private final BroadcastReceiver usbReceiver = new BroadcastReceiver() {

    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (ACTION_USB_PERMISSION.equals(action)) {
            synchronized (this) {
                UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    if(device != null){
                      // call method to set up device communication
                   }
                }
                else {
                    Log.d(TAG, "permission denied for device " + device);
                }
            }
        }
    }
};

لتسجيل جهاز استقبال البث، أضِف ما يلي في طريقة onCreate() في نشاطك:

Kotlin

private const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"
...
val manager = getSystemService(Context.USB_SERVICE) as UsbManager
...
permissionIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), 0)
val filter = IntentFilter(ACTION_USB_PERMISSION)
registerReceiver(usbReceiver, filter)

Java

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
private static final String ACTION_USB_PERMISSION =
    "com.android.example.USB_PERMISSION";
...
permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(usbReceiver, filter);

لعرض مربّع الحوار الذي يطلب من المستخدمين الإذن بالاتصال بالجهاز، عليك استدعاء طريقة requestPermission():

Kotlin

lateinit var device: UsbDevice
...
usbManager.requestPermission(device, permissionIntent)

Java

UsbDevice device;
...
usbManager.requestPermission(device, permissionIntent);

عندما يردّ المستخدمون على مربّع الحوار، يتلقّى مستقبِل البث الغرض الذي يتضمّن السمة EXTRA_PERMISSION_GRANTED الإضافية، وهي قيمة منطقية تمثّل الإجابة. يُرجى التحقّق من هذه السمة الإضافية لمعرفة القيمة true قبل الاتصال بالجهاز.

التواصل مع جهاز

يمكن أن يكون الاتصال بجهاز USB إما متزامنًا أو غير متزامن. وفي كلتا الحالتين، عليك إنشاء سلسلة محادثات جديدة لإجراء جميع عمليات نقل البيانات عليها، لكي لا تحظر سلسلة محادثات واجهة المستخدم. لإعداد الاتصال بجهاز بشكل صحيح، يجب الحصول على UsbInterface وUsbEndpoint المناسبتَين للجهاز المطلوب الاتصال به وإرسال الطلبات على نقطة النهاية هذه باستخدام UsbDeviceConnection. بشكل عام، يجب أن تتضمن التعليمة البرمجية:

  • راجِع سمات العنصر UsbDevice، مثل معرّف المنتج أو رقم تعريف المورّد أو فئة الجهاز لمعرفة ما إذا كنت تريد الاتصال بالجهاز أم لا.
  • عندما تكون متأكدًا من أنّك تريد الاتصال بالجهاز، ابحث عن رمز UsbInterface المناسب الذي تريد استخدامه للتواصل مع UsbEndpoint بالشكل المناسب لتلك الواجهة. يمكن أن تحتوي الواجهات على نقطة نهاية واحدة أو أكثر، وعادةً ما يكون لها نقطة نهاية للإدخال والإخراج للاتصال الثنائي.
  • عند العثور على نقطة النهاية الصحيحة، افتح UsbDeviceConnection على نقطة النهاية هذه.
  • يمكنك توفير البيانات التي تريد نقلها على نقطة النهاية باستخدام طريقة bulkTransfer() أو controlTransfer(). يجب تنفيذ هذه الخطوة في سلسلة محادثات أخرى لمنع حظر سلسلة التعليمات الرئيسية في واجهة المستخدم. لمزيد من المعلومات حول استخدام سلاسل المحادثات في Android، يُرجى الاطّلاع على العمليات وسلاسل المحادثات.

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

Kotlin

private lateinit var bytes: ByteArray
private val TIMEOUT = 0
private val forceClaim = true

...

device?.getInterface(0)?.also { intf ->
    intf.getEndpoint(0)?.also { endpoint ->
        usbManager.openDevice(device)?.apply {
            claimInterface(intf, forceClaim)
            bulkTransfer(endpoint, bytes, bytes.size, TIMEOUT) //do in another thread
        }
    }
}

Java

private Byte[] bytes;
private static int TIMEOUT = 0;
private boolean forceClaim = true;

...

UsbInterface intf = device.getInterface(0);
UsbEndpoint endpoint = intf.getEndpoint(0);
UsbDeviceConnection connection = usbManager.openDevice(device);
connection.claimInterface(intf, forceClaim);
connection.bulkTransfer(endpoint, bytes, bytes.length, TIMEOUT); //do in another thread

لإرسال البيانات بشكل غير متزامن، يمكنك استخدام الصف UsbRequest من أجل initialize وqueue لطلب غير متزامن، ثم انتظار النتيجة باستخدام requestWait().

إنهاء الاتصال بأحد الأجهزة

عند الانتهاء من الاتصال بجهاز أو إذا كان الجهاز منفصلاً، أغلِق UsbInterface وUsbDeviceConnection من خلال الاتصال بـ releaseInterface() وclose(). للاستماع إلى الأحداث المنفصلة، أنشِئ جهاز استقبال بث على النحو التالي:

Kotlin

var usbReceiver: BroadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {

        if (UsbManager.ACTION_USB_DEVICE_DETACHED == intent.action) {
            val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
            device?.apply {
                // call your method that cleans up and closes communication with the device
            }
        }
    }
}

Java

BroadcastReceiver usbReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();

      if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
            UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
            if (device != null) {
                // call your method that cleans up and closes communication with the device
            }
        }
    }
};

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